index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. 'use strict'
  2. module.exports = footnotes
  3. var tab = 9 // '\t'
  4. var lineFeed = 10 // '\n'
  5. var space = 32
  6. var exclamationMark = 33 // '!'
  7. var colon = 58 // ':'
  8. var leftSquareBracket = 91 // '['
  9. var backslash = 92 // '\'
  10. var rightSquareBracket = 93 // ']'
  11. var caret = 94 // '^'
  12. var graveAccent = 96 // '`'
  13. var tabSize = 4
  14. var maxSlice = 1024
  15. function footnotes(options) {
  16. var parser = this.Parser
  17. var compiler = this.Compiler
  18. if (isRemarkParser(parser)) {
  19. attachParser(parser, options)
  20. }
  21. if (isRemarkCompiler(compiler)) {
  22. attachCompiler(compiler)
  23. }
  24. }
  25. function isRemarkParser(parser) {
  26. return Boolean(parser && parser.prototype && parser.prototype.blockTokenizers)
  27. }
  28. function isRemarkCompiler(compiler) {
  29. return Boolean(compiler && compiler.prototype && compiler.prototype.visitors)
  30. }
  31. function attachParser(parser, options) {
  32. var settings = options || {}
  33. var proto = parser.prototype
  34. var blocks = proto.blockTokenizers
  35. var spans = proto.inlineTokenizers
  36. var blockMethods = proto.blockMethods
  37. var inlineMethods = proto.inlineMethods
  38. var originalDefinition = blocks.definition
  39. var originalReference = spans.reference
  40. var interruptors = []
  41. var index = -1
  42. var length = blockMethods.length
  43. var method
  44. // Interrupt by anything except for indented code or paragraphs.
  45. while (++index < length) {
  46. method = blockMethods[index]
  47. if (
  48. method === 'newline' ||
  49. method === 'indentedCode' ||
  50. method === 'paragraph' ||
  51. method === 'footnoteDefinition'
  52. ) {
  53. continue
  54. }
  55. interruptors.push([method])
  56. }
  57. interruptors.push(['footnoteDefinition'])
  58. // Insert tokenizers.
  59. if (settings.inlineNotes) {
  60. before(inlineMethods, 'reference', 'inlineNote')
  61. spans.inlineNote = footnote
  62. }
  63. before(blockMethods, 'definition', 'footnoteDefinition')
  64. before(inlineMethods, 'reference', 'footnoteCall')
  65. blocks.definition = definition
  66. blocks.footnoteDefinition = footnoteDefinition
  67. spans.footnoteCall = footnoteCall
  68. spans.reference = reference
  69. proto.interruptFootnoteDefinition = interruptors
  70. reference.locator = originalReference.locator
  71. footnoteCall.locator = locateFootnoteCall
  72. footnote.locator = locateFootnote
  73. function footnoteDefinition(eat, value, silent) {
  74. var self = this
  75. var interruptors = self.interruptFootnoteDefinition
  76. var offsets = self.offset
  77. var length = value.length + 1
  78. var index = 0
  79. var content = []
  80. var label
  81. var labelStart
  82. var labelEnd
  83. var code
  84. var now
  85. var add
  86. var exit
  87. var children
  88. var start
  89. var indent
  90. var contentStart
  91. var lines
  92. var line
  93. // Skip initial whitespace.
  94. while (index < length) {
  95. code = value.charCodeAt(index)
  96. if (code !== tab && code !== space) break
  97. index++
  98. }
  99. // Parse `[^`.
  100. if (value.charCodeAt(index++) !== leftSquareBracket) return
  101. if (value.charCodeAt(index++) !== caret) return
  102. // Parse label.
  103. labelStart = index
  104. while (index < length) {
  105. code = value.charCodeAt(index)
  106. // Exit on white space.
  107. if (
  108. code !== code ||
  109. code === lineFeed ||
  110. code === tab ||
  111. code === space
  112. ) {
  113. return
  114. }
  115. if (code === rightSquareBracket) {
  116. labelEnd = index
  117. index++
  118. break
  119. }
  120. index++
  121. }
  122. // Exit if we didn’t find an end, no label, or there’s no colon.
  123. if (
  124. labelEnd === undefined ||
  125. labelStart === labelEnd ||
  126. value.charCodeAt(index++) !== colon
  127. ) {
  128. return
  129. }
  130. // Found it!
  131. /* istanbul ignore if - never used (yet) */
  132. if (silent) {
  133. return true
  134. }
  135. label = value.slice(labelStart, labelEnd)
  136. // Now, to get all lines.
  137. now = eat.now()
  138. start = 0
  139. indent = 0
  140. contentStart = index
  141. lines = []
  142. while (index < length) {
  143. code = value.charCodeAt(index)
  144. if (code !== code || code === lineFeed) {
  145. line = {
  146. start: start,
  147. contentStart: contentStart || index,
  148. contentEnd: index,
  149. end: index
  150. }
  151. lines.push(line)
  152. // Prepare a new line.
  153. if (code === lineFeed) {
  154. start = index + 1
  155. indent = 0
  156. contentStart = undefined
  157. line.end = start
  158. }
  159. } else if (indent !== undefined) {
  160. if (code === space || code === tab) {
  161. indent += code === space ? 1 : tabSize - (indent % tabSize)
  162. if (indent > tabSize) {
  163. indent = undefined
  164. contentStart = index
  165. }
  166. } else {
  167. // If this line is not indented and it’s either preceded by a blank
  168. // line or starts a new block, exit.
  169. if (
  170. indent < tabSize &&
  171. line &&
  172. (line.contentStart === line.contentEnd ||
  173. interrupt(interruptors, blocks, self, [
  174. eat,
  175. value.slice(index, maxSlice),
  176. true
  177. ]))
  178. ) {
  179. break
  180. }
  181. indent = undefined
  182. contentStart = index
  183. }
  184. }
  185. index++
  186. }
  187. // Remove trailing lines without content.
  188. index = -1
  189. length = lines.length
  190. while (length > 0) {
  191. line = lines[length - 1]
  192. if (line.contentStart !== line.contentEnd) {
  193. break
  194. }
  195. length--
  196. }
  197. // Add all, but ignore the final line feed.
  198. add = eat(value.slice(0, line.contentEnd))
  199. // Add indent offsets and get content w/o indents.
  200. while (++index < length) {
  201. line = lines[index]
  202. offsets[now.line + index] =
  203. (offsets[now.line + index] || 0) + (line.contentStart - line.start)
  204. content.push(value.slice(line.contentStart, line.end))
  205. }
  206. // Parse content.
  207. exit = self.enterBlock()
  208. children = self.tokenizeBlock(content.join(''), now)
  209. exit()
  210. return add({
  211. type: 'footnoteDefinition',
  212. identifier: label.toLowerCase(),
  213. label: label,
  214. children: children
  215. })
  216. }
  217. // Parse a footnote call / footnote reference, such as `[^label]`
  218. function footnoteCall(eat, value, silent) {
  219. var length = value.length + 1
  220. var index = 0
  221. var label
  222. var labelStart
  223. var labelEnd
  224. var code
  225. if (value.charCodeAt(index++) !== leftSquareBracket) return
  226. if (value.charCodeAt(index++) !== caret) return
  227. labelStart = index
  228. while (index < length) {
  229. code = value.charCodeAt(index)
  230. if (
  231. code !== code ||
  232. code === lineFeed ||
  233. code === tab ||
  234. code === space
  235. ) {
  236. return
  237. }
  238. if (code === rightSquareBracket) {
  239. labelEnd = index
  240. index++
  241. break
  242. }
  243. index++
  244. }
  245. if (labelEnd === undefined || labelStart === labelEnd) {
  246. return
  247. }
  248. /* istanbul ignore if - never used (yet) */
  249. if (silent) {
  250. return true
  251. }
  252. label = value.slice(labelStart, labelEnd)
  253. return eat(value.slice(0, index))({
  254. type: 'footnoteReference',
  255. identifier: label.toLowerCase(),
  256. label: label
  257. })
  258. }
  259. // Parse an inline note / footnote, such as `^[text]`
  260. function footnote(eat, value, silent) {
  261. var self = this
  262. var length = value.length + 1
  263. var index = 0
  264. var balance = 0
  265. var now
  266. var code
  267. var contentStart
  268. var contentEnd
  269. var fenceStart
  270. var fenceOpenSize
  271. var fenceCloseSize
  272. if (value.charCodeAt(index++) !== caret) return
  273. if (value.charCodeAt(index++) !== leftSquareBracket) return
  274. contentStart = index
  275. while (index < length) {
  276. code = value.charCodeAt(index)
  277. // EOF:
  278. if (code !== code) {
  279. return
  280. }
  281. // If we’re not in code:
  282. if (fenceOpenSize === undefined) {
  283. if (code === backslash) {
  284. index += 2
  285. } else if (code === leftSquareBracket) {
  286. balance++
  287. index++
  288. } else if (code === rightSquareBracket) {
  289. if (balance === 0) {
  290. contentEnd = index
  291. index++
  292. break
  293. } else {
  294. balance--
  295. index++
  296. }
  297. } else if (code === graveAccent) {
  298. fenceStart = index
  299. fenceOpenSize = 1
  300. while (value.charCodeAt(fenceStart + fenceOpenSize) === graveAccent) {
  301. fenceOpenSize++
  302. }
  303. index += fenceOpenSize
  304. } else {
  305. index++
  306. }
  307. }
  308. // We’re in code:
  309. else {
  310. if (code === graveAccent) {
  311. fenceStart = index
  312. fenceCloseSize = 1
  313. while (
  314. value.charCodeAt(fenceStart + fenceCloseSize) === graveAccent
  315. ) {
  316. fenceCloseSize++
  317. }
  318. index += fenceCloseSize
  319. // Found it, we’re no longer in code!
  320. if (fenceOpenSize === fenceCloseSize) {
  321. fenceOpenSize = undefined
  322. }
  323. fenceCloseSize = undefined
  324. } else {
  325. index++
  326. }
  327. }
  328. }
  329. if (contentEnd === undefined) {
  330. return
  331. }
  332. /* istanbul ignore if - never used (yet) */
  333. if (silent) {
  334. return true
  335. }
  336. now = eat.now()
  337. now.column += 2
  338. now.offset += 2
  339. return eat(value.slice(0, index))({
  340. type: 'footnote',
  341. children: self.tokenizeInline(value.slice(contentStart, contentEnd), now)
  342. })
  343. }
  344. // Do not allow `![^` or `[^` as a normal reference, do pass all other values
  345. // through.
  346. function reference(eat, value, silent) {
  347. var index = 0
  348. if (value.charCodeAt(index) === exclamationMark) index++
  349. if (value.charCodeAt(index) !== leftSquareBracket) return
  350. if (value.charCodeAt(index + 1) === caret) return
  351. return originalReference.call(this, eat, value, silent)
  352. }
  353. // Do not allow `[^` as a normal definition, do pass all other values through.
  354. function definition(eat, value, silent) {
  355. var index = 0
  356. var code = value.charCodeAt(index)
  357. while (code === space || code === tab) code = value.charCodeAt(++index)
  358. if (code !== leftSquareBracket) return
  359. if (value.charCodeAt(index + 1) === caret) return
  360. return originalDefinition.call(this, eat, value, silent)
  361. }
  362. function locateFootnoteCall(value, from) {
  363. return value.indexOf('[', from)
  364. }
  365. function locateFootnote(value, from) {
  366. return value.indexOf('^[', from)
  367. }
  368. }
  369. function attachCompiler(compiler) {
  370. var serializers = compiler.prototype.visitors
  371. var indent = ' '
  372. serializers.footnote = footnote
  373. serializers.footnoteReference = footnoteReference
  374. serializers.footnoteDefinition = footnoteDefinition
  375. function footnote(node) {
  376. return '^[' + this.all(node).join('') + ']'
  377. }
  378. function footnoteReference(node) {
  379. return '[^' + (node.label || node.identifier) + ']'
  380. }
  381. function footnoteDefinition(node) {
  382. var lines = this.all(node).join('\n\n').split('\n')
  383. var index = 0
  384. var length = lines.length
  385. var line
  386. // Indent each line, except the first, that is not empty.
  387. while (++index < length) {
  388. line = lines[index]
  389. if (line === '') continue
  390. lines[index] = indent + line
  391. }
  392. return '[^' + (node.label || node.identifier) + ']: ' + lines.join('\n')
  393. }
  394. }
  395. function before(list, before, value) {
  396. list.splice(list.indexOf(before), 0, value)
  397. }
  398. // Mimics <https://github.com/remarkjs/remark/blob/b4c993e/packages/remark-parse/lib/util/interrupt.js>,
  399. // but simplified for our needs.
  400. function interrupt(list, tokenizers, ctx, parameters) {
  401. var length = list.length
  402. var index = -1
  403. while (++index < length) {
  404. if (tokenizers[list[index][0]].apply(ctx, parameters)) {
  405. return true
  406. }
  407. }
  408. return false
  409. }