label-end.js 12 KB

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