linkify.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. // Replace link-like texts with link nodes.
  2. //
  3. // Currently restricted by `md.validateLink()` to http/https/ftp
  4. //
  5. 'use strict';
  6. var arrayReplaceAt = require('../common/utils').arrayReplaceAt;
  7. function isLinkOpen(str) {
  8. return /^<a[>\s]/i.test(str);
  9. }
  10. function isLinkClose(str) {
  11. return /^<\/a\s*>/i.test(str);
  12. }
  13. module.exports = function linkify(state) {
  14. var i, j, l, tokens, token, currentToken, nodes, ln, text, pos, lastPos,
  15. level, htmlLinkLevel, url, fullUrl, urlText,
  16. blockTokens = state.tokens,
  17. links;
  18. if (!state.md.options.linkify) { return; }
  19. for (j = 0, l = blockTokens.length; j < l; j++) {
  20. if (blockTokens[j].type !== 'inline' ||
  21. !state.md.linkify.pretest(blockTokens[j].content)) {
  22. continue;
  23. }
  24. tokens = blockTokens[j].children;
  25. htmlLinkLevel = 0;
  26. // We scan from the end, to keep position when new tags added.
  27. // Use reversed logic in links start/end match
  28. for (i = tokens.length - 1; i >= 0; i--) {
  29. currentToken = tokens[i];
  30. // Skip content of markdown links
  31. if (currentToken.type === 'link_close') {
  32. i--;
  33. while (tokens[i].level !== currentToken.level && tokens[i].type !== 'link_open') {
  34. i--;
  35. }
  36. continue;
  37. }
  38. // Skip content of html tag links
  39. if (currentToken.type === 'html_inline') {
  40. if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) {
  41. htmlLinkLevel--;
  42. }
  43. if (isLinkClose(currentToken.content)) {
  44. htmlLinkLevel++;
  45. }
  46. }
  47. if (htmlLinkLevel > 0) { continue; }
  48. if (currentToken.type === 'text' && state.md.linkify.test(currentToken.content)) {
  49. text = currentToken.content;
  50. links = state.md.linkify.match(text);
  51. // Now split string to nodes
  52. nodes = [];
  53. level = currentToken.level;
  54. lastPos = 0;
  55. for (ln = 0; ln < links.length; ln++) {
  56. url = links[ln].url;
  57. fullUrl = state.md.normalizeLink(url);
  58. if (!state.md.validateLink(fullUrl)) { continue; }
  59. urlText = links[ln].text;
  60. // Linkifier might send raw hostnames like "example.com", where url
  61. // starts with domain name. So we prepend http:// in those cases,
  62. // and remove it afterwards.
  63. //
  64. if (!links[ln].schema) {
  65. urlText = state.md.normalizeLinkText('http://' + urlText).replace(/^http:\/\//, '');
  66. } else if (links[ln].schema === 'mailto:' && !/^mailto:/i.test(urlText)) {
  67. urlText = state.md.normalizeLinkText('mailto:' + urlText).replace(/^mailto:/, '');
  68. } else {
  69. urlText = state.md.normalizeLinkText(urlText);
  70. }
  71. pos = links[ln].index;
  72. if (pos > lastPos) {
  73. token = new state.Token('text', '', 0);
  74. token.content = text.slice(lastPos, pos);
  75. token.level = level;
  76. nodes.push(token);
  77. }
  78. token = new state.Token('link_open', 'a', 1);
  79. token.attrs = [ [ 'href', fullUrl ] ];
  80. token.level = level++;
  81. token.markup = 'linkify';
  82. token.info = 'auto';
  83. nodes.push(token);
  84. token = new state.Token('text', '', 0);
  85. token.content = urlText;
  86. token.level = level;
  87. nodes.push(token);
  88. token = new state.Token('link_close', 'a', -1);
  89. token.level = --level;
  90. token.markup = 'linkify';
  91. token.info = 'auto';
  92. nodes.push(token);
  93. lastPos = links[ln].lastIndex;
  94. }
  95. if (lastPos < text.length) {
  96. token = new state.Token('text', '', 0);
  97. token.content = text.slice(lastPos);
  98. token.level = level;
  99. nodes.push(token);
  100. }
  101. // replace current node
  102. blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
  103. }
  104. }
  105. }
  106. };