label-end.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  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. import {codes, constants, types} from 'micromark-util-symbol'
  19. import {ok as assert} from 'devlop'
  20. /** @type {Construct} */
  21. export const labelEnd = {
  22. name: 'labelEnd',
  23. tokenize: tokenizeLabelEnd,
  24. resolveTo: resolveToLabelEnd,
  25. resolveAll: resolveAllLabelEnd
  26. }
  27. /** @type {Construct} */
  28. const resourceConstruct = {tokenize: tokenizeResource}
  29. /** @type {Construct} */
  30. const referenceFullConstruct = {tokenize: tokenizeReferenceFull}
  31. /** @type {Construct} */
  32. const referenceCollapsedConstruct = {tokenize: tokenizeReferenceCollapsed}
  33. /** @type {Resolver} */
  34. function resolveAllLabelEnd(events) {
  35. let index = -1
  36. while (++index < events.length) {
  37. const token = events[index][1]
  38. if (
  39. token.type === types.labelImage ||
  40. token.type === types.labelLink ||
  41. token.type === types.labelEnd
  42. ) {
  43. // Remove the marker.
  44. events.splice(index + 1, token.type === types.labelImage ? 4 : 2)
  45. token.type = types.data
  46. index++
  47. }
  48. }
  49. return events
  50. }
  51. /** @type {Resolver} */
  52. function resolveToLabelEnd(events, context) {
  53. let index = events.length
  54. let offset = 0
  55. /** @type {Token} */
  56. let token
  57. /** @type {number | undefined} */
  58. let open
  59. /** @type {number | undefined} */
  60. let close
  61. /** @type {Array<Event>} */
  62. let media
  63. // Find an opening.
  64. while (index--) {
  65. token = events[index][1]
  66. if (open) {
  67. // If we see another link, or inactive link label, we’ve been here before.
  68. if (
  69. token.type === types.link ||
  70. (token.type === types.labelLink && token._inactive)
  71. ) {
  72. break
  73. }
  74. // Mark other link openings as inactive, as we can’t have links in
  75. // links.
  76. if (events[index][0] === 'enter' && token.type === types.labelLink) {
  77. token._inactive = true
  78. }
  79. } else if (close) {
  80. if (
  81. events[index][0] === 'enter' &&
  82. (token.type === types.labelImage || token.type === types.labelLink) &&
  83. !token._balanced
  84. ) {
  85. open = index
  86. if (token.type !== types.labelLink) {
  87. offset = 2
  88. break
  89. }
  90. }
  91. } else if (token.type === types.labelEnd) {
  92. close = index
  93. }
  94. }
  95. assert(open !== undefined, '`open` is supposed to be found')
  96. assert(close !== undefined, '`close` is supposed to be found')
  97. const group = {
  98. type: events[open][1].type === types.labelLink ? types.link : types.image,
  99. start: Object.assign({}, events[open][1].start),
  100. end: Object.assign({}, events[events.length - 1][1].end)
  101. }
  102. const label = {
  103. type: types.label,
  104. start: Object.assign({}, events[open][1].start),
  105. end: Object.assign({}, events[close][1].end)
  106. }
  107. const text = {
  108. type: types.labelText,
  109. start: Object.assign({}, events[open + offset + 2][1].end),
  110. end: Object.assign({}, events[close - 2][1].start)
  111. }
  112. media = [
  113. ['enter', group, context],
  114. ['enter', label, context]
  115. ]
  116. // Opening marker.
  117. media = push(media, events.slice(open + 1, open + offset + 3))
  118. // Text open.
  119. media = push(media, [['enter', text, context]])
  120. // Always populated by defaults.
  121. assert(
  122. context.parser.constructs.insideSpan.null,
  123. 'expected `insideSpan.null` to be populated'
  124. )
  125. // Between.
  126. media = push(
  127. media,
  128. resolveAll(
  129. context.parser.constructs.insideSpan.null,
  130. events.slice(open + offset + 4, close - 3),
  131. context
  132. )
  133. )
  134. // Text close, marker close, label close.
  135. media = push(media, [
  136. ['exit', text, context],
  137. events[close - 2],
  138. events[close - 1],
  139. ['exit', label, context]
  140. ])
  141. // Reference, resource, or so.
  142. media = push(media, events.slice(close + 1))
  143. // Media close.
  144. media = push(media, [['exit', group, context]])
  145. splice(events, open, events.length, media)
  146. return events
  147. }
  148. /**
  149. * @this {TokenizeContext}
  150. * @type {Tokenizer}
  151. */
  152. function tokenizeLabelEnd(effects, ok, nok) {
  153. const self = this
  154. let index = self.events.length
  155. /** @type {Token} */
  156. let labelStart
  157. /** @type {boolean} */
  158. let defined
  159. // Find an opening.
  160. while (index--) {
  161. if (
  162. (self.events[index][1].type === types.labelImage ||
  163. self.events[index][1].type === types.labelLink) &&
  164. !self.events[index][1]._balanced
  165. ) {
  166. labelStart = self.events[index][1]
  167. break
  168. }
  169. }
  170. return start
  171. /**
  172. * Start of label end.
  173. *
  174. * ```markdown
  175. * > | [a](b) c
  176. * ^
  177. * > | [a][b] c
  178. * ^
  179. * > | [a][] b
  180. * ^
  181. * > | [a] b
  182. * ```
  183. *
  184. * @type {State}
  185. */
  186. function start(code) {
  187. assert(code === codes.rightSquareBracket, 'expected `]`')
  188. // If there is not an okay opening.
  189. if (!labelStart) {
  190. return nok(code)
  191. }
  192. // If the corresponding label (link) start is marked as inactive,
  193. // it means we’d be wrapping a link, like this:
  194. //
  195. // ```markdown
  196. // > | a [b [c](d) e](f) g.
  197. // ^
  198. // ```
  199. //
  200. // We can’t have that, so it’s just balanced brackets.
  201. if (labelStart._inactive) {
  202. return labelEndNok(code)
  203. }
  204. defined = self.parser.defined.includes(
  205. normalizeIdentifier(
  206. self.sliceSerialize({start: labelStart.end, end: self.now()})
  207. )
  208. )
  209. effects.enter(types.labelEnd)
  210. effects.enter(types.labelMarker)
  211. effects.consume(code)
  212. effects.exit(types.labelMarker)
  213. effects.exit(types.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 === codes.leftParenthesis) {
  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 === codes.leftSquareBracket) {
  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. assert(code === codes.leftParenthesis, 'expected left paren')
  334. effects.enter(types.resource)
  335. effects.enter(types.resourceMarker)
  336. effects.consume(code)
  337. effects.exit(types.resourceMarker)
  338. return resourceBefore
  339. }
  340. /**
  341. * In resource, after `(`, at optional whitespace.
  342. *
  343. * ```markdown
  344. * > | [a](b) c
  345. * ^
  346. * ```
  347. *
  348. * @type {State}
  349. */
  350. function resourceBefore(code) {
  351. return markdownLineEndingOrSpace(code)
  352. ? factoryWhitespace(effects, resourceOpen)(code)
  353. : resourceOpen(code)
  354. }
  355. /**
  356. * In resource, after optional whitespace, at `)` or a destination.
  357. *
  358. * ```markdown
  359. * > | [a](b) c
  360. * ^
  361. * ```
  362. *
  363. * @type {State}
  364. */
  365. function resourceOpen(code) {
  366. if (code === codes.rightParenthesis) {
  367. return resourceEnd(code)
  368. }
  369. return factoryDestination(
  370. effects,
  371. resourceDestinationAfter,
  372. resourceDestinationMissing,
  373. types.resourceDestination,
  374. types.resourceDestinationLiteral,
  375. types.resourceDestinationLiteralMarker,
  376. types.resourceDestinationRaw,
  377. types.resourceDestinationString,
  378. constants.linkResourceDestinationBalanceMax
  379. )(code)
  380. }
  381. /**
  382. * In resource, after destination, at optional whitespace.
  383. *
  384. * ```markdown
  385. * > | [a](b) c
  386. * ^
  387. * ```
  388. *
  389. * @type {State}
  390. */
  391. function resourceDestinationAfter(code) {
  392. return markdownLineEndingOrSpace(code)
  393. ? factoryWhitespace(effects, resourceBetween)(code)
  394. : resourceEnd(code)
  395. }
  396. /**
  397. * At invalid destination.
  398. *
  399. * ```markdown
  400. * > | [a](<<) b
  401. * ^
  402. * ```
  403. *
  404. * @type {State}
  405. */
  406. function resourceDestinationMissing(code) {
  407. return nok(code)
  408. }
  409. /**
  410. * In resource, after destination and whitespace, at `(` or title.
  411. *
  412. * ```markdown
  413. * > | [a](b ) c
  414. * ^
  415. * ```
  416. *
  417. * @type {State}
  418. */
  419. function resourceBetween(code) {
  420. if (
  421. code === codes.quotationMark ||
  422. code === codes.apostrophe ||
  423. code === codes.leftParenthesis
  424. ) {
  425. return factoryTitle(
  426. effects,
  427. resourceTitleAfter,
  428. nok,
  429. types.resourceTitle,
  430. types.resourceTitleMarker,
  431. types.resourceTitleString
  432. )(code)
  433. }
  434. return resourceEnd(code)
  435. }
  436. /**
  437. * In resource, after title, at optional whitespace.
  438. *
  439. * ```markdown
  440. * > | [a](b "c") d
  441. * ^
  442. * ```
  443. *
  444. * @type {State}
  445. */
  446. function resourceTitleAfter(code) {
  447. return markdownLineEndingOrSpace(code)
  448. ? factoryWhitespace(effects, resourceEnd)(code)
  449. : resourceEnd(code)
  450. }
  451. /**
  452. * In resource, at `)`.
  453. *
  454. * ```markdown
  455. * > | [a](b) d
  456. * ^
  457. * ```
  458. *
  459. * @type {State}
  460. */
  461. function resourceEnd(code) {
  462. if (code === codes.rightParenthesis) {
  463. effects.enter(types.resourceMarker)
  464. effects.consume(code)
  465. effects.exit(types.resourceMarker)
  466. effects.exit(types.resource)
  467. return ok
  468. }
  469. return nok(code)
  470. }
  471. }
  472. /**
  473. * @this {TokenizeContext}
  474. * @type {Tokenizer}
  475. */
  476. function tokenizeReferenceFull(effects, ok, nok) {
  477. const self = this
  478. return referenceFull
  479. /**
  480. * In a reference (full), at the `[`.
  481. *
  482. * ```markdown
  483. * > | [a][b] d
  484. * ^
  485. * ```
  486. *
  487. * @type {State}
  488. */
  489. function referenceFull(code) {
  490. assert(code === codes.leftSquareBracket, 'expected left bracket')
  491. return factoryLabel.call(
  492. self,
  493. effects,
  494. referenceFullAfter,
  495. referenceFullMissing,
  496. types.reference,
  497. types.referenceMarker,
  498. types.referenceString
  499. )(code)
  500. }
  501. /**
  502. * In a reference (full), after `]`.
  503. *
  504. * ```markdown
  505. * > | [a][b] d
  506. * ^
  507. * ```
  508. *
  509. * @type {State}
  510. */
  511. function referenceFullAfter(code) {
  512. return self.parser.defined.includes(
  513. normalizeIdentifier(
  514. self.sliceSerialize(self.events[self.events.length - 1][1]).slice(1, -1)
  515. )
  516. )
  517. ? ok(code)
  518. : nok(code)
  519. }
  520. /**
  521. * In reference (full) that was missing.
  522. *
  523. * ```markdown
  524. * > | [a][b d
  525. * ^
  526. * ```
  527. *
  528. * @type {State}
  529. */
  530. function referenceFullMissing(code) {
  531. return nok(code)
  532. }
  533. }
  534. /**
  535. * @this {TokenizeContext}
  536. * @type {Tokenizer}
  537. */
  538. function tokenizeReferenceCollapsed(effects, ok, nok) {
  539. return referenceCollapsedStart
  540. /**
  541. * In reference (collapsed), at `[`.
  542. *
  543. * > 👉 **Note**: we only get here if the label is defined.
  544. *
  545. * ```markdown
  546. * > | [a][] d
  547. * ^
  548. * ```
  549. *
  550. * @type {State}
  551. */
  552. function referenceCollapsedStart(code) {
  553. // We only attempt a collapsed label if there’s a `[`.
  554. assert(code === codes.leftSquareBracket, 'expected left bracket')
  555. effects.enter(types.reference)
  556. effects.enter(types.referenceMarker)
  557. effects.consume(code)
  558. effects.exit(types.referenceMarker)
  559. return referenceCollapsedOpen
  560. }
  561. /**
  562. * In reference (collapsed), at `]`.
  563. *
  564. * > 👉 **Note**: we only get here if the label is defined.
  565. *
  566. * ```markdown
  567. * > | [a][] d
  568. * ^
  569. * ```
  570. *
  571. * @type {State}
  572. */
  573. function referenceCollapsedOpen(code) {
  574. if (code === codes.rightSquareBracket) {
  575. effects.enter(types.referenceMarker)
  576. effects.consume(code)
  577. effects.exit(types.referenceMarker)
  578. effects.exit(types.reference)
  579. return ok
  580. }
  581. return nok(code)
  582. }
  583. }