label-end.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /**
  2. * @typedef {import('micromark-util-types').Construct} Construct
  3. * @typedef {import('micromark-util-types').Event} Event
  4. * @typedef {import('micromark-util-types').Resolver} Resolver
  5. * @typedef {import('micromark-util-types').State} State
  6. * @typedef {import('micromark-util-types').Token} Token
  7. * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext
  8. * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
  9. */
  10. import {factoryDestination} from 'micromark-factory-destination'
  11. import {factoryLabel} from 'micromark-factory-label'
  12. import {factoryTitle} from 'micromark-factory-title'
  13. import {factoryWhitespace} from 'micromark-factory-whitespace'
  14. import {markdownLineEndingOrSpace} from 'micromark-util-character'
  15. import {push, splice} from 'micromark-util-chunked'
  16. import {normalizeIdentifier} from 'micromark-util-normalize-identifier'
  17. import {resolveAll} from 'micromark-util-resolve-all'
  18. /** @type {Construct} */
  19. export const labelEnd = {
  20. name: 'labelEnd',
  21. tokenize: tokenizeLabelEnd,
  22. resolveTo: resolveToLabelEnd,
  23. resolveAll: resolveAllLabelEnd
  24. }
  25. /** @type {Construct} */
  26. const resourceConstruct = {
  27. tokenize: tokenizeResource
  28. }
  29. /** @type {Construct} */
  30. const referenceFullConstruct = {
  31. tokenize: tokenizeReferenceFull
  32. }
  33. /** @type {Construct} */
  34. const referenceCollapsedConstruct = {
  35. tokenize: tokenizeReferenceCollapsed
  36. }
  37. /** @type {Resolver} */
  38. function resolveAllLabelEnd(events) {
  39. let index = -1
  40. while (++index < events.length) {
  41. const token = events[index][1]
  42. if (
  43. token.type === 'labelImage' ||
  44. token.type === 'labelLink' ||
  45. token.type === 'labelEnd'
  46. ) {
  47. // Remove the marker.
  48. events.splice(index + 1, token.type === 'labelImage' ? 4 : 2)
  49. token.type = 'data'
  50. index++
  51. }
  52. }
  53. return events
  54. }
  55. /** @type {Resolver} */
  56. function resolveToLabelEnd(events, context) {
  57. let index = events.length
  58. let offset = 0
  59. /** @type {Token} */
  60. let token
  61. /** @type {number | undefined} */
  62. let open
  63. /** @type {number | undefined} */
  64. let close
  65. /** @type {Array<Event>} */
  66. let media
  67. // Find an opening.
  68. while (index--) {
  69. token = events[index][1]
  70. if (open) {
  71. // If we see another link, or inactive link label, we’ve been here before.
  72. if (
  73. token.type === 'link' ||
  74. (token.type === 'labelLink' && token._inactive)
  75. ) {
  76. break
  77. }
  78. // Mark other link openings as inactive, as we can’t have links in
  79. // links.
  80. if (events[index][0] === 'enter' && token.type === 'labelLink') {
  81. token._inactive = true
  82. }
  83. } else if (close) {
  84. if (
  85. events[index][0] === 'enter' &&
  86. (token.type === 'labelImage' || token.type === 'labelLink') &&
  87. !token._balanced
  88. ) {
  89. open = index
  90. if (token.type !== 'labelLink') {
  91. offset = 2
  92. break
  93. }
  94. }
  95. } else if (token.type === 'labelEnd') {
  96. close = index
  97. }
  98. }
  99. const group = {
  100. type: events[open][1].type === 'labelLink' ? 'link' : 'image',
  101. start: Object.assign({}, events[open][1].start),
  102. end: Object.assign({}, events[events.length - 1][1].end)
  103. }
  104. const label = {
  105. type: 'label',
  106. start: Object.assign({}, events[open][1].start),
  107. end: Object.assign({}, events[close][1].end)
  108. }
  109. const text = {
  110. type: 'labelText',
  111. start: Object.assign({}, events[open + offset + 2][1].end),
  112. end: Object.assign({}, events[close - 2][1].start)
  113. }
  114. media = [
  115. ['enter', group, context],
  116. ['enter', label, context]
  117. ]
  118. // Opening marker.
  119. media = push(media, events.slice(open + 1, open + offset + 3))
  120. // Text open.
  121. media = push(media, [['enter', text, context]])
  122. // Always populated by defaults.
  123. // Between.
  124. media = push(
  125. media,
  126. resolveAll(
  127. context.parser.constructs.insideSpan.null,
  128. events.slice(open + offset + 4, close - 3),
  129. context
  130. )
  131. )
  132. // Text close, marker close, label close.
  133. media = push(media, [
  134. ['exit', text, context],
  135. events[close - 2],
  136. events[close - 1],
  137. ['exit', label, context]
  138. ])
  139. // Reference, resource, or so.
  140. media = push(media, events.slice(close + 1))
  141. // Media close.
  142. media = push(media, [['exit', group, context]])
  143. splice(events, open, events.length, media)
  144. return events
  145. }
  146. /**
  147. * @this {TokenizeContext}
  148. * @type {Tokenizer}
  149. */
  150. function tokenizeLabelEnd(effects, ok, nok) {
  151. const self = this
  152. let index = self.events.length
  153. /** @type {Token} */
  154. let labelStart
  155. /** @type {boolean} */
  156. let defined
  157. // Find an opening.
  158. while (index--) {
  159. if (
  160. (self.events[index][1].type === 'labelImage' ||
  161. self.events[index][1].type === 'labelLink') &&
  162. !self.events[index][1]._balanced
  163. ) {
  164. labelStart = self.events[index][1]
  165. break
  166. }
  167. }
  168. return start
  169. /**
  170. * Start of label end.
  171. *
  172. * ```markdown
  173. * > | [a](b) c
  174. * ^
  175. * > | [a][b] c
  176. * ^
  177. * > | [a][] b
  178. * ^
  179. * > | [a] b
  180. * ```
  181. *
  182. * @type {State}
  183. */
  184. function start(code) {
  185. // If there is not an okay opening.
  186. if (!labelStart) {
  187. return nok(code)
  188. }
  189. // If the corresponding label (link) start is marked as inactive,
  190. // it means we’d be wrapping a link, like this:
  191. //
  192. // ```markdown
  193. // > | a [b [c](d) e](f) g.
  194. // ^
  195. // ```
  196. //
  197. // We can’t have that, so it’s just balanced brackets.
  198. if (labelStart._inactive) {
  199. return labelEndNok(code)
  200. }
  201. defined = self.parser.defined.includes(
  202. normalizeIdentifier(
  203. self.sliceSerialize({
  204. start: labelStart.end,
  205. end: self.now()
  206. })
  207. )
  208. )
  209. effects.enter('labelEnd')
  210. effects.enter('labelMarker')
  211. effects.consume(code)
  212. effects.exit('labelMarker')
  213. effects.exit('labelEnd')
  214. return after
  215. }
  216. /**
  217. * After `]`.
  218. *
  219. * ```markdown
  220. * > | [a](b) c
  221. * ^
  222. * > | [a][b] c
  223. * ^
  224. * > | [a][] b
  225. * ^
  226. * > | [a] b
  227. * ^
  228. * ```
  229. *
  230. * @type {State}
  231. */
  232. function after(code) {
  233. // Note: `markdown-rs` also parses GFM footnotes here, which for us is in
  234. // an extension.
  235. // Resource (`[asd](fgh)`)?
  236. if (code === 40) {
  237. return effects.attempt(
  238. resourceConstruct,
  239. labelEndOk,
  240. defined ? labelEndOk : labelEndNok
  241. )(code)
  242. }
  243. // Full (`[asd][fgh]`) or collapsed (`[asd][]`) reference?
  244. if (code === 91) {
  245. return effects.attempt(
  246. referenceFullConstruct,
  247. labelEndOk,
  248. defined ? referenceNotFull : labelEndNok
  249. )(code)
  250. }
  251. // Shortcut (`[asd]`) reference?
  252. return defined ? labelEndOk(code) : labelEndNok(code)
  253. }
  254. /**
  255. * After `]`, at `[`, but not at a full reference.
  256. *
  257. * > 👉 **Note**: we only get here if the label is defined.
  258. *
  259. * ```markdown
  260. * > | [a][] b
  261. * ^
  262. * > | [a] b
  263. * ^
  264. * ```
  265. *
  266. * @type {State}
  267. */
  268. function referenceNotFull(code) {
  269. return effects.attempt(
  270. referenceCollapsedConstruct,
  271. labelEndOk,
  272. labelEndNok
  273. )(code)
  274. }
  275. /**
  276. * Done, we found something.
  277. *
  278. * ```markdown
  279. * > | [a](b) c
  280. * ^
  281. * > | [a][b] c
  282. * ^
  283. * > | [a][] b
  284. * ^
  285. * > | [a] b
  286. * ^
  287. * ```
  288. *
  289. * @type {State}
  290. */
  291. function labelEndOk(code) {
  292. // Note: `markdown-rs` does a bunch of stuff here.
  293. return ok(code)
  294. }
  295. /**
  296. * Done, it’s nothing.
  297. *
  298. * There was an okay opening, but we didn’t match anything.
  299. *
  300. * ```markdown
  301. * > | [a](b c
  302. * ^
  303. * > | [a][b c
  304. * ^
  305. * > | [a] b
  306. * ^
  307. * ```
  308. *
  309. * @type {State}
  310. */
  311. function labelEndNok(code) {
  312. labelStart._balanced = true
  313. return nok(code)
  314. }
  315. }
  316. /**
  317. * @this {TokenizeContext}
  318. * @type {Tokenizer}
  319. */
  320. function tokenizeResource(effects, ok, nok) {
  321. return resourceStart
  322. /**
  323. * At a resource.
  324. *
  325. * ```markdown
  326. * > | [a](b) c
  327. * ^
  328. * ```
  329. *
  330. * @type {State}
  331. */
  332. function resourceStart(code) {
  333. effects.enter('resource')
  334. effects.enter('resourceMarker')
  335. effects.consume(code)
  336. effects.exit('resourceMarker')
  337. return resourceBefore
  338. }
  339. /**
  340. * In resource, after `(`, at optional whitespace.
  341. *
  342. * ```markdown
  343. * > | [a](b) c
  344. * ^
  345. * ```
  346. *
  347. * @type {State}
  348. */
  349. function resourceBefore(code) {
  350. return markdownLineEndingOrSpace(code)
  351. ? factoryWhitespace(effects, resourceOpen)(code)
  352. : resourceOpen(code)
  353. }
  354. /**
  355. * In resource, after optional whitespace, at `)` or a destination.
  356. *
  357. * ```markdown
  358. * > | [a](b) c
  359. * ^
  360. * ```
  361. *
  362. * @type {State}
  363. */
  364. function resourceOpen(code) {
  365. if (code === 41) {
  366. return resourceEnd(code)
  367. }
  368. return factoryDestination(
  369. effects,
  370. resourceDestinationAfter,
  371. resourceDestinationMissing,
  372. 'resourceDestination',
  373. 'resourceDestinationLiteral',
  374. 'resourceDestinationLiteralMarker',
  375. 'resourceDestinationRaw',
  376. 'resourceDestinationString',
  377. 32
  378. )(code)
  379. }
  380. /**
  381. * In resource, after destination, at optional whitespace.
  382. *
  383. * ```markdown
  384. * > | [a](b) c
  385. * ^
  386. * ```
  387. *
  388. * @type {State}
  389. */
  390. function resourceDestinationAfter(code) {
  391. return markdownLineEndingOrSpace(code)
  392. ? factoryWhitespace(effects, resourceBetween)(code)
  393. : resourceEnd(code)
  394. }
  395. /**
  396. * At invalid destination.
  397. *
  398. * ```markdown
  399. * > | [a](<<) b
  400. * ^
  401. * ```
  402. *
  403. * @type {State}
  404. */
  405. function resourceDestinationMissing(code) {
  406. return nok(code)
  407. }
  408. /**
  409. * In resource, after destination and whitespace, at `(` or title.
  410. *
  411. * ```markdown
  412. * > | [a](b ) c
  413. * ^
  414. * ```
  415. *
  416. * @type {State}
  417. */
  418. function resourceBetween(code) {
  419. if (code === 34 || code === 39 || code === 40) {
  420. return factoryTitle(
  421. effects,
  422. resourceTitleAfter,
  423. nok,
  424. 'resourceTitle',
  425. 'resourceTitleMarker',
  426. 'resourceTitleString'
  427. )(code)
  428. }
  429. return resourceEnd(code)
  430. }
  431. /**
  432. * In resource, after title, at optional whitespace.
  433. *
  434. * ```markdown
  435. * > | [a](b "c") d
  436. * ^
  437. * ```
  438. *
  439. * @type {State}
  440. */
  441. function resourceTitleAfter(code) {
  442. return markdownLineEndingOrSpace(code)
  443. ? factoryWhitespace(effects, resourceEnd)(code)
  444. : resourceEnd(code)
  445. }
  446. /**
  447. * In resource, at `)`.
  448. *
  449. * ```markdown
  450. * > | [a](b) d
  451. * ^
  452. * ```
  453. *
  454. * @type {State}
  455. */
  456. function resourceEnd(code) {
  457. if (code === 41) {
  458. effects.enter('resourceMarker')
  459. effects.consume(code)
  460. effects.exit('resourceMarker')
  461. effects.exit('resource')
  462. return ok
  463. }
  464. return nok(code)
  465. }
  466. }
  467. /**
  468. * @this {TokenizeContext}
  469. * @type {Tokenizer}
  470. */
  471. function tokenizeReferenceFull(effects, ok, nok) {
  472. const self = this
  473. return referenceFull
  474. /**
  475. * In a reference (full), at the `[`.
  476. *
  477. * ```markdown
  478. * > | [a][b] d
  479. * ^
  480. * ```
  481. *
  482. * @type {State}
  483. */
  484. function referenceFull(code) {
  485. return factoryLabel.call(
  486. self,
  487. effects,
  488. referenceFullAfter,
  489. referenceFullMissing,
  490. 'reference',
  491. 'referenceMarker',
  492. 'referenceString'
  493. )(code)
  494. }
  495. /**
  496. * In a reference (full), after `]`.
  497. *
  498. * ```markdown
  499. * > | [a][b] d
  500. * ^
  501. * ```
  502. *
  503. * @type {State}
  504. */
  505. function referenceFullAfter(code) {
  506. return self.parser.defined.includes(
  507. normalizeIdentifier(
  508. self.sliceSerialize(self.events[self.events.length - 1][1]).slice(1, -1)
  509. )
  510. )
  511. ? ok(code)
  512. : nok(code)
  513. }
  514. /**
  515. * In reference (full) that was missing.
  516. *
  517. * ```markdown
  518. * > | [a][b d
  519. * ^
  520. * ```
  521. *
  522. * @type {State}
  523. */
  524. function referenceFullMissing(code) {
  525. return nok(code)
  526. }
  527. }
  528. /**
  529. * @this {TokenizeContext}
  530. * @type {Tokenizer}
  531. */
  532. function tokenizeReferenceCollapsed(effects, ok, nok) {
  533. return referenceCollapsedStart
  534. /**
  535. * In reference (collapsed), at `[`.
  536. *
  537. * > 👉 **Note**: we only get here if the label is defined.
  538. *
  539. * ```markdown
  540. * > | [a][] d
  541. * ^
  542. * ```
  543. *
  544. * @type {State}
  545. */
  546. function referenceCollapsedStart(code) {
  547. // We only attempt a collapsed label if there’s a `[`.
  548. effects.enter('reference')
  549. effects.enter('referenceMarker')
  550. effects.consume(code)
  551. effects.exit('referenceMarker')
  552. return referenceCollapsedOpen
  553. }
  554. /**
  555. * In reference (collapsed), at `]`.
  556. *
  557. * > 👉 **Note**: we only get here if the label is defined.
  558. *
  559. * ```markdown
  560. * > | [a][] d
  561. * ^
  562. * ```
  563. *
  564. * @type {State}
  565. */
  566. function referenceCollapsedOpen(code) {
  567. if (code === 93) {
  568. effects.enter('referenceMarker')
  569. effects.consume(code)
  570. effects.exit('referenceMarker')
  571. effects.exit('reference')
  572. return ok
  573. }
  574. return nok(code)
  575. }
  576. }