reference.mjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { isSpace, normalizeReference } from '../common/utils.mjs'
  2. export default function reference (state, startLine, _endLine, silent) {
  3. let pos = state.bMarks[startLine] + state.tShift[startLine]
  4. let max = state.eMarks[startLine]
  5. let nextLine = startLine + 1
  6. // if it's indented more than 3 spaces, it should be a code block
  7. if (state.sCount[startLine] - state.blkIndent >= 4) { return false }
  8. if (state.src.charCodeAt(pos) !== 0x5B/* [ */) { return false }
  9. function getNextLine (nextLine) {
  10. const endLine = state.lineMax
  11. if (nextLine >= endLine || state.isEmpty(nextLine)) {
  12. // empty line or end of input
  13. return null
  14. }
  15. let isContinuation = false
  16. // this would be a code block normally, but after paragraph
  17. // it's considered a lazy continuation regardless of what's there
  18. if (state.sCount[nextLine] - state.blkIndent > 3) { isContinuation = true }
  19. // quirk for blockquotes, this line should already be checked by that rule
  20. if (state.sCount[nextLine] < 0) { isContinuation = true }
  21. if (!isContinuation) {
  22. const terminatorRules = state.md.block.ruler.getRules('reference')
  23. const oldParentType = state.parentType
  24. state.parentType = 'reference'
  25. // Some tags can terminate paragraph without empty line.
  26. let terminate = false
  27. for (let i = 0, l = terminatorRules.length; i < l; i++) {
  28. if (terminatorRules[i](state, nextLine, endLine, true)) {
  29. terminate = true
  30. break
  31. }
  32. }
  33. state.parentType = oldParentType
  34. if (terminate) {
  35. // terminated by another block
  36. return null
  37. }
  38. }
  39. const pos = state.bMarks[nextLine] + state.tShift[nextLine]
  40. const max = state.eMarks[nextLine]
  41. // max + 1 explicitly includes the newline
  42. return state.src.slice(pos, max + 1)
  43. }
  44. let str = state.src.slice(pos, max + 1)
  45. max = str.length
  46. let labelEnd = -1
  47. for (pos = 1; pos < max; pos++) {
  48. const ch = str.charCodeAt(pos)
  49. if (ch === 0x5B /* [ */) {
  50. return false
  51. } else if (ch === 0x5D /* ] */) {
  52. labelEnd = pos
  53. break
  54. } else if (ch === 0x0A /* \n */) {
  55. const lineContent = getNextLine(nextLine)
  56. if (lineContent !== null) {
  57. str += lineContent
  58. max = str.length
  59. nextLine++
  60. }
  61. } else if (ch === 0x5C /* \ */) {
  62. pos++
  63. if (pos < max && str.charCodeAt(pos) === 0x0A) {
  64. const lineContent = getNextLine(nextLine)
  65. if (lineContent !== null) {
  66. str += lineContent
  67. max = str.length
  68. nextLine++
  69. }
  70. }
  71. }
  72. }
  73. if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return false }
  74. // [label]: destination 'title'
  75. // ^^^ skip optional whitespace here
  76. for (pos = labelEnd + 2; pos < max; pos++) {
  77. const ch = str.charCodeAt(pos)
  78. if (ch === 0x0A) {
  79. const lineContent = getNextLine(nextLine)
  80. if (lineContent !== null) {
  81. str += lineContent
  82. max = str.length
  83. nextLine++
  84. }
  85. } else if (isSpace(ch)) {
  86. /* eslint no-empty:0 */
  87. } else {
  88. break
  89. }
  90. }
  91. // [label]: destination 'title'
  92. // ^^^^^^^^^^^ parse this
  93. const destRes = state.md.helpers.parseLinkDestination(str, pos, max)
  94. if (!destRes.ok) { return false }
  95. const href = state.md.normalizeLink(destRes.str)
  96. if (!state.md.validateLink(href)) { return false }
  97. pos = destRes.pos
  98. // save cursor state, we could require to rollback later
  99. const destEndPos = pos
  100. const destEndLineNo = nextLine
  101. // [label]: destination 'title'
  102. // ^^^ skipping those spaces
  103. const start = pos
  104. for (; pos < max; pos++) {
  105. const ch = str.charCodeAt(pos)
  106. if (ch === 0x0A) {
  107. const lineContent = getNextLine(nextLine)
  108. if (lineContent !== null) {
  109. str += lineContent
  110. max = str.length
  111. nextLine++
  112. }
  113. } else if (isSpace(ch)) {
  114. /* eslint no-empty:0 */
  115. } else {
  116. break
  117. }
  118. }
  119. // [label]: destination 'title'
  120. // ^^^^^^^ parse this
  121. let titleRes = state.md.helpers.parseLinkTitle(str, pos, max)
  122. while (titleRes.can_continue) {
  123. const lineContent = getNextLine(nextLine)
  124. if (lineContent === null) break
  125. str += lineContent
  126. pos = max
  127. max = str.length
  128. nextLine++
  129. titleRes = state.md.helpers.parseLinkTitle(str, pos, max, titleRes)
  130. }
  131. let title
  132. if (pos < max && start !== pos && titleRes.ok) {
  133. title = titleRes.str
  134. pos = titleRes.pos
  135. } else {
  136. title = ''
  137. pos = destEndPos
  138. nextLine = destEndLineNo
  139. }
  140. // skip trailing spaces until the rest of the line
  141. while (pos < max) {
  142. const ch = str.charCodeAt(pos)
  143. if (!isSpace(ch)) { break }
  144. pos++
  145. }
  146. if (pos < max && str.charCodeAt(pos) !== 0x0A) {
  147. if (title) {
  148. // garbage at the end of the line after title,
  149. // but it could still be a valid reference if we roll back
  150. title = ''
  151. pos = destEndPos
  152. nextLine = destEndLineNo
  153. while (pos < max) {
  154. const ch = str.charCodeAt(pos)
  155. if (!isSpace(ch)) { break }
  156. pos++
  157. }
  158. }
  159. }
  160. if (pos < max && str.charCodeAt(pos) !== 0x0A) {
  161. // garbage at the end of the line
  162. return false
  163. }
  164. const label = normalizeReference(str.slice(1, labelEnd))
  165. if (!label) {
  166. // CommonMark 0.20 disallows empty labels
  167. return false
  168. }
  169. // Reference can not terminate anything. This check is for safety only.
  170. /* istanbul ignore if */
  171. if (silent) { return true }
  172. if (typeof state.env.references === 'undefined') {
  173. state.env.references = {}
  174. }
  175. if (typeof state.env.references[label] === 'undefined') {
  176. state.env.references[label] = { title, href }
  177. }
  178. state.line = nextLine
  179. return true
  180. }