linkify.mjs 4.1 KB

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