/app/internal_packages/message-list/lib/autolinker.ts

https://github.com/Foundry376/Mailspring · TypeScript · 99 lines · 87 code · 7 blank · 5 comment · 30 complexity · 406c7a5138541a903911de0f90ab188c MD5 · raw file

  1. import { RegExpUtils, DOMUtils } from 'mailspring-exports';
  2. function _matchesAnyRegexp(text, regexps) {
  3. for (const excludeRegexp of regexps) {
  4. if (excludeRegexp.test(text)) {
  5. return true;
  6. }
  7. }
  8. return false;
  9. }
  10. function _runOnTextNode(node, matchers) {
  11. if (node.parentElement) {
  12. const withinScript = node.parentElement.tagName === 'SCRIPT';
  13. const withinStyle = node.parentElement.tagName === 'STYLE';
  14. const withinA = node.parentElement.closest('a') !== null;
  15. if (withinScript || withinA || withinStyle) {
  16. return;
  17. }
  18. }
  19. if (node.textContent.trim().length < 4) {
  20. return;
  21. }
  22. let longest = null;
  23. let longestLength = null;
  24. for (const [prefix, regex, options = {}] of matchers) {
  25. regex.lastIndex = 0;
  26. const match = regex.exec(node.textContent);
  27. if (match !== null) {
  28. if (options.exclude && _matchesAnyRegexp(match[0], options.exclude)) {
  29. continue;
  30. }
  31. if (match[0].length > longestLength) {
  32. longest = [prefix, match];
  33. longestLength = match[0].length;
  34. }
  35. }
  36. }
  37. if (longest) {
  38. const [prefix, match] = longest;
  39. const href = `${prefix}${match[0]}`;
  40. const range = document.createRange();
  41. range.setStart(node, match.index);
  42. range.setEnd(node, match.index + match[0].length);
  43. const aTag = DOMUtils.wrap(range, 'A');
  44. aTag.href = href;
  45. aTag.title = href;
  46. return;
  47. }
  48. }
  49. export function autolink(doc, { async } = { async: false }) {
  50. // Traverse the new DOM tree and make things that look like links clickable,
  51. // and ensure anything with an href has a title attribute.
  52. const textWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
  53. const matchers = [
  54. [
  55. 'mailto:',
  56. RegExpUtils.emailRegex(),
  57. {
  58. // Technically, gmail.com/bengotow@gmail.com is an email address. After
  59. // matching, manully exclude any email that follows the .*[/?].*@ pattern.
  60. exclude: [/\..*[/|?].*@/],
  61. },
  62. ],
  63. ['tel:', RegExpUtils.phoneRegex()],
  64. ['', RegExpUtils.mailspringCommandRegex()],
  65. ['', RegExpUtils.urlRegex()],
  66. ];
  67. if (async) {
  68. const fn = deadline => {
  69. while (textWalker.nextNode()) {
  70. _runOnTextNode(textWalker.currentNode, matchers);
  71. if (deadline.timeRemaining() <= 0) {
  72. window.requestIdleCallback(fn, { timeout: 500 });
  73. return;
  74. }
  75. }
  76. };
  77. window.requestIdleCallback(fn, { timeout: 500 });
  78. } else {
  79. while (textWalker.nextNode()) {
  80. _runOnTextNode(textWalker.currentNode, matchers);
  81. }
  82. }
  83. // Traverse the new DOM tree and make sure everything with an href has a title.
  84. const aTagWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, {
  85. acceptNode: node =>
  86. (node as HTMLLinkElement).href ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
  87. });
  88. while (aTagWalker.nextNode()) {
  89. const n = aTagWalker.currentNode as HTMLElement;
  90. n.title = n.getAttribute('href');
  91. }
  92. }