code-fenced.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * @typedef {import('micromark-util-types').Code} Code
  3. * @typedef {import('micromark-util-types').Construct} Construct
  4. * @typedef {import('micromark-util-types').State} State
  5. * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext
  6. * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
  7. */
  8. import {factorySpace} from 'micromark-factory-space'
  9. import {markdownLineEnding, markdownSpace} from 'micromark-util-character'
  10. /** @type {Construct} */
  11. const nonLazyContinuation = {
  12. tokenize: tokenizeNonLazyContinuation,
  13. partial: true
  14. }
  15. /** @type {Construct} */
  16. export const codeFenced = {
  17. name: 'codeFenced',
  18. tokenize: tokenizeCodeFenced,
  19. concrete: true
  20. }
  21. /**
  22. * @this {TokenizeContext}
  23. * @type {Tokenizer}
  24. */
  25. function tokenizeCodeFenced(effects, ok, nok) {
  26. const self = this
  27. /** @type {Construct} */
  28. const closeStart = {
  29. tokenize: tokenizeCloseStart,
  30. partial: true
  31. }
  32. let initialPrefix = 0
  33. let sizeOpen = 0
  34. /** @type {NonNullable<Code>} */
  35. let marker
  36. return start
  37. /**
  38. * Start of code.
  39. *
  40. * ```markdown
  41. * > | ~~~js
  42. * ^
  43. * | alert(1)
  44. * | ~~~
  45. * ```
  46. *
  47. * @type {State}
  48. */
  49. function start(code) {
  50. // To do: parse whitespace like `markdown-rs`.
  51. return beforeSequenceOpen(code)
  52. }
  53. /**
  54. * In opening fence, after prefix, at sequence.
  55. *
  56. * ```markdown
  57. * > | ~~~js
  58. * ^
  59. * | alert(1)
  60. * | ~~~
  61. * ```
  62. *
  63. * @type {State}
  64. */
  65. function beforeSequenceOpen(code) {
  66. const tail = self.events[self.events.length - 1]
  67. initialPrefix =
  68. tail && tail[1].type === 'linePrefix'
  69. ? tail[2].sliceSerialize(tail[1], true).length
  70. : 0
  71. marker = code
  72. effects.enter('codeFenced')
  73. effects.enter('codeFencedFence')
  74. effects.enter('codeFencedFenceSequence')
  75. return sequenceOpen(code)
  76. }
  77. /**
  78. * In opening fence sequence.
  79. *
  80. * ```markdown
  81. * > | ~~~js
  82. * ^
  83. * | alert(1)
  84. * | ~~~
  85. * ```
  86. *
  87. * @type {State}
  88. */
  89. function sequenceOpen(code) {
  90. if (code === marker) {
  91. sizeOpen++
  92. effects.consume(code)
  93. return sequenceOpen
  94. }
  95. if (sizeOpen < 3) {
  96. return nok(code)
  97. }
  98. effects.exit('codeFencedFenceSequence')
  99. return markdownSpace(code)
  100. ? factorySpace(effects, infoBefore, 'whitespace')(code)
  101. : infoBefore(code)
  102. }
  103. /**
  104. * In opening fence, after the sequence (and optional whitespace), before info.
  105. *
  106. * ```markdown
  107. * > | ~~~js
  108. * ^
  109. * | alert(1)
  110. * | ~~~
  111. * ```
  112. *
  113. * @type {State}
  114. */
  115. function infoBefore(code) {
  116. if (code === null || markdownLineEnding(code)) {
  117. effects.exit('codeFencedFence')
  118. return self.interrupt
  119. ? ok(code)
  120. : effects.check(nonLazyContinuation, atNonLazyBreak, after)(code)
  121. }
  122. effects.enter('codeFencedFenceInfo')
  123. effects.enter('chunkString', {
  124. contentType: 'string'
  125. })
  126. return info(code)
  127. }
  128. /**
  129. * In info.
  130. *
  131. * ```markdown
  132. * > | ~~~js
  133. * ^
  134. * | alert(1)
  135. * | ~~~
  136. * ```
  137. *
  138. * @type {State}
  139. */
  140. function info(code) {
  141. if (code === null || markdownLineEnding(code)) {
  142. effects.exit('chunkString')
  143. effects.exit('codeFencedFenceInfo')
  144. return infoBefore(code)
  145. }
  146. if (markdownSpace(code)) {
  147. effects.exit('chunkString')
  148. effects.exit('codeFencedFenceInfo')
  149. return factorySpace(effects, metaBefore, 'whitespace')(code)
  150. }
  151. if (code === 96 && code === marker) {
  152. return nok(code)
  153. }
  154. effects.consume(code)
  155. return info
  156. }
  157. /**
  158. * In opening fence, after info and whitespace, before meta.
  159. *
  160. * ```markdown
  161. * > | ~~~js eval
  162. * ^
  163. * | alert(1)
  164. * | ~~~
  165. * ```
  166. *
  167. * @type {State}
  168. */
  169. function metaBefore(code) {
  170. if (code === null || markdownLineEnding(code)) {
  171. return infoBefore(code)
  172. }
  173. effects.enter('codeFencedFenceMeta')
  174. effects.enter('chunkString', {
  175. contentType: 'string'
  176. })
  177. return meta(code)
  178. }
  179. /**
  180. * In meta.
  181. *
  182. * ```markdown
  183. * > | ~~~js eval
  184. * ^
  185. * | alert(1)
  186. * | ~~~
  187. * ```
  188. *
  189. * @type {State}
  190. */
  191. function meta(code) {
  192. if (code === null || markdownLineEnding(code)) {
  193. effects.exit('chunkString')
  194. effects.exit('codeFencedFenceMeta')
  195. return infoBefore(code)
  196. }
  197. if (code === 96 && code === marker) {
  198. return nok(code)
  199. }
  200. effects.consume(code)
  201. return meta
  202. }
  203. /**
  204. * At eol/eof in code, before a non-lazy closing fence or content.
  205. *
  206. * ```markdown
  207. * > | ~~~js
  208. * ^
  209. * > | alert(1)
  210. * ^
  211. * | ~~~
  212. * ```
  213. *
  214. * @type {State}
  215. */
  216. function atNonLazyBreak(code) {
  217. return effects.attempt(closeStart, after, contentBefore)(code)
  218. }
  219. /**
  220. * Before code content, not a closing fence, at eol.
  221. *
  222. * ```markdown
  223. * | ~~~js
  224. * > | alert(1)
  225. * ^
  226. * | ~~~
  227. * ```
  228. *
  229. * @type {State}
  230. */
  231. function contentBefore(code) {
  232. effects.enter('lineEnding')
  233. effects.consume(code)
  234. effects.exit('lineEnding')
  235. return contentStart
  236. }
  237. /**
  238. * Before code content, not a closing fence.
  239. *
  240. * ```markdown
  241. * | ~~~js
  242. * > | alert(1)
  243. * ^
  244. * | ~~~
  245. * ```
  246. *
  247. * @type {State}
  248. */
  249. function contentStart(code) {
  250. return initialPrefix > 0 && markdownSpace(code)
  251. ? factorySpace(
  252. effects,
  253. beforeContentChunk,
  254. 'linePrefix',
  255. initialPrefix + 1
  256. )(code)
  257. : beforeContentChunk(code)
  258. }
  259. /**
  260. * Before code content, after optional prefix.
  261. *
  262. * ```markdown
  263. * | ~~~js
  264. * > | alert(1)
  265. * ^
  266. * | ~~~
  267. * ```
  268. *
  269. * @type {State}
  270. */
  271. function beforeContentChunk(code) {
  272. if (code === null || markdownLineEnding(code)) {
  273. return effects.check(nonLazyContinuation, atNonLazyBreak, after)(code)
  274. }
  275. effects.enter('codeFlowValue')
  276. return contentChunk(code)
  277. }
  278. /**
  279. * In code content.
  280. *
  281. * ```markdown
  282. * | ~~~js
  283. * > | alert(1)
  284. * ^^^^^^^^
  285. * | ~~~
  286. * ```
  287. *
  288. * @type {State}
  289. */
  290. function contentChunk(code) {
  291. if (code === null || markdownLineEnding(code)) {
  292. effects.exit('codeFlowValue')
  293. return beforeContentChunk(code)
  294. }
  295. effects.consume(code)
  296. return contentChunk
  297. }
  298. /**
  299. * After code.
  300. *
  301. * ```markdown
  302. * | ~~~js
  303. * | alert(1)
  304. * > | ~~~
  305. * ^
  306. * ```
  307. *
  308. * @type {State}
  309. */
  310. function after(code) {
  311. effects.exit('codeFenced')
  312. return ok(code)
  313. }
  314. /**
  315. * @this {TokenizeContext}
  316. * @type {Tokenizer}
  317. */
  318. function tokenizeCloseStart(effects, ok, nok) {
  319. let size = 0
  320. return startBefore
  321. /**
  322. *
  323. *
  324. * @type {State}
  325. */
  326. function startBefore(code) {
  327. effects.enter('lineEnding')
  328. effects.consume(code)
  329. effects.exit('lineEnding')
  330. return start
  331. }
  332. /**
  333. * Before closing fence, at optional whitespace.
  334. *
  335. * ```markdown
  336. * | ~~~js
  337. * | alert(1)
  338. * > | ~~~
  339. * ^
  340. * ```
  341. *
  342. * @type {State}
  343. */
  344. function start(code) {
  345. // Always populated by defaults.
  346. // To do: `enter` here or in next state?
  347. effects.enter('codeFencedFence')
  348. return markdownSpace(code)
  349. ? factorySpace(
  350. effects,
  351. beforeSequenceClose,
  352. 'linePrefix',
  353. self.parser.constructs.disable.null.includes('codeIndented')
  354. ? undefined
  355. : 4
  356. )(code)
  357. : beforeSequenceClose(code)
  358. }
  359. /**
  360. * In closing fence, after optional whitespace, at sequence.
  361. *
  362. * ```markdown
  363. * | ~~~js
  364. * | alert(1)
  365. * > | ~~~
  366. * ^
  367. * ```
  368. *
  369. * @type {State}
  370. */
  371. function beforeSequenceClose(code) {
  372. if (code === marker) {
  373. effects.enter('codeFencedFenceSequence')
  374. return sequenceClose(code)
  375. }
  376. return nok(code)
  377. }
  378. /**
  379. * In closing fence sequence.
  380. *
  381. * ```markdown
  382. * | ~~~js
  383. * | alert(1)
  384. * > | ~~~
  385. * ^
  386. * ```
  387. *
  388. * @type {State}
  389. */
  390. function sequenceClose(code) {
  391. if (code === marker) {
  392. size++
  393. effects.consume(code)
  394. return sequenceClose
  395. }
  396. if (size >= sizeOpen) {
  397. effects.exit('codeFencedFenceSequence')
  398. return markdownSpace(code)
  399. ? factorySpace(effects, sequenceCloseAfter, 'whitespace')(code)
  400. : sequenceCloseAfter(code)
  401. }
  402. return nok(code)
  403. }
  404. /**
  405. * After closing fence sequence, after optional whitespace.
  406. *
  407. * ```markdown
  408. * | ~~~js
  409. * | alert(1)
  410. * > | ~~~
  411. * ^
  412. * ```
  413. *
  414. * @type {State}
  415. */
  416. function sequenceCloseAfter(code) {
  417. if (code === null || markdownLineEnding(code)) {
  418. effects.exit('codeFencedFence')
  419. return ok(code)
  420. }
  421. return nok(code)
  422. }
  423. }
  424. }
  425. /**
  426. * @this {TokenizeContext}
  427. * @type {Tokenizer}
  428. */
  429. function tokenizeNonLazyContinuation(effects, ok, nok) {
  430. const self = this
  431. return start
  432. /**
  433. *
  434. *
  435. * @type {State}
  436. */
  437. function start(code) {
  438. if (code === null) {
  439. return nok(code)
  440. }
  441. effects.enter('lineEnding')
  442. effects.consume(code)
  443. effects.exit('lineEnding')
  444. return lineStart
  445. }
  446. /**
  447. *
  448. *
  449. * @type {State}
  450. */
  451. function lineStart(code) {
  452. return self.parser.lazy[self.now().line] ? nok(code) : ok(code)
  453. }
  454. }