Markdown parser fork with extended syntax for personal use.
1//! Label end occurs in the [text][] content type.
2//!
3//! ## Grammar
4//!
5//! Label end forms with the following BNF
6//! (<small>see [construct][crate::construct] for character groups</small>):
7//!
8//! ```bnf
9//! label_end ::= ']' [resource | reference_full | reference_collapsed]
10//!
11//! resource ::= '(' [space_or_tab_eol] destination [space_or_tab_eol title] [space_or_tab_eol] ')'
12//! reference_full ::= '[' label ']'
13//! reference_collapsed ::= '[' ']'
14//!
15//! ; See the `destination`, `title`, and `label` constructs for the BNF of
16//! ; those parts.
17//! ```
18//!
19//! See [`destination`][destination], [`label`][label], and [`title`][title]
20//! for grammar, notes, and recommendations on each part.
21//!
22//! In the case of a resource, the destination and title are given directly
23//! with the label end.
24//! In the case of a reference, this information is provided by a matched
25//! [definition][].
26//! Full references (`[x][y]`) match to definitions through their explicit,
27//! second, label (`y`).
28//! Collapsed references (`[x][]`) and shortcut references (`[x]`) match by
29//! interpreting the text provided between the first, implicit, label (`x`).
30//! To match, the effective label of the reference must be equal to the label
31//! of the definition after normalizing with
32//! [`normalize_identifier`][].
33//!
34//! Importantly, while the label of a full reference *can* include [string][]
35//! content, and in case of collapsed and shortcut references even [text][]
36//! content, that content is not considered when matching.
37//! To illustrate, neither label matches the definition:
38//!
39//! ```markdown
40//! [a&b]: https://example.com
41//!
42//! [x][a&b], [a\&b][]
43//! ```
44//!
45//! When the resource or reference matches, the destination forms the `href`
46//! attribute in case of a [label start (link)][label_start_link], and an
47//! `src` attribute in case of a [label start (image)][label_start_image].
48//! The title is formed, optionally, on either `<a>` or `<img>`.
49//! When matched with a [gfm label start (footnote)][gfm_label_start_footnote],
50//! no reference or resource can follow the label end.
51//!
52//! For info on how to encode characters in URLs, see
53//! [`destination`][destination].
54//! For info on how characters are encoded as `href` on `<a>` or `src` on
55//! `<img>` when compiling, see
56//! [`sanitize_uri`][sanitize_uri].
57//!
58//! In case of a matched [gfm label start (footnote)][gfm_label_start_footnote],
59//! a counter is injected.
60//! In case of a matched [label start (link)][label_start_link], the interpreted
61//! content between it and the label end, is placed between the opening and
62//! closing tags.
63//! In case of a matched [label start (image)][label_start_image], the text is
64//! also interpreted, but used *without* the resulting tags:
65//!
66//! ```markdown
67//! [a *b* c](#)
68//!
69//! 
70//! ```
71//!
72//! Yields:
73//!
74//! ```html
75//! <p><a href="#">a <em>b</em> c</a></p>
76//! <p><img src="#" alt="a b c" /></p>
77//! ```
78//!
79//! It is possible to use images in links.
80//! It’s somewhat possible to have links in images (the text will be used, not
81//! the HTML, see above).
82//! But it’s not possible to use links (or footnotes, which result in links)
83//! in links.
84//! The “deepest” link (or footnote) wins.
85//! To illustrate:
86//!
87//! ```markdown
88//! a [b [c](#) d](#) e
89//! ```
90//!
91//! Yields:
92//!
93//! ```html
94//! <p>a [b <a href="#">c</a> d](#) e</p>
95//! ```
96//!
97//! This limitation is imposed because links in links is invalid according to
98//! HTML.
99//! Technically though, in markdown it is still possible to construct them by
100//! using an [autolink][] in a link.
101//! You definitely should not do that.
102//!
103//! ## HTML
104//!
105//! Label end does not, on its own, relate to anything in HTML.
106//! When matched with a [label start (link)][label_start_link], they together
107//! relate to the `<a>` element in HTML.
108//! See [*§ 4.5.1 The `a` element*][html_a] in the HTML spec for more info.
109//! It can also match with [label start (image)][label_start_image], in which
110//! case they form an `<img>` element.
111//! See [*§ 4.8.3 The `img` element*][html_img] in the HTML spec for more info.
112//! It can also match with [gfm label start (footnote)][gfm_label_start_footnote],
113//! in which case they form `<sup>` and `<a>` elements in HTML.
114//! See [*§ 4.5.19 The `sub` and `sup` elements*][html_sup] and
115//! [*§ 4.5.1 The `a` element*][html_a] in the HTML spec for more info.
116//!
117//! ## Recommendation
118//!
119//! It is recommended to use labels for links instead of [autolinks][autolink].
120//! Labels allow more characters in URLs, and allow relative URLs and `www.`
121//! URLs.
122//! They also allow for descriptive text to explain the URL in prose.
123//!
124//! In footnotes, it’s recommended to use words instead of numbers (or letters
125//! or anything with an order) as calls.
126//! That makes it easier to reuse and reorder footnotes.
127//!
128//! ## Tokens
129//!
130//! * [`Data`][Name::Data]
131//! * [`GfmFootnoteCall`][Name::GfmFootnoteCall]
132//! * [`Image`][Name::Image]
133//! * [`Label`][Name::Label]
134//! * [`LabelEnd`][Name::LabelEnd]
135//! * [`LabelMarker`][Name::LabelMarker]
136//! * [`LabelText`][Name::LabelText]
137//! * [`LineEnding`][Name::LineEnding]
138//! * [`Link`][Name::Link]
139//! * [`Reference`][Name::Reference]
140//! * [`ReferenceMarker`][Name::ReferenceMarker]
141//! * [`ReferenceString`][Name::ReferenceString]
142//! * [`Resource`][Name::Resource]
143//! * [`ResourceDestination`][Name::ResourceDestination]
144//! * [`ResourceDestinationLiteral`][Name::ResourceDestinationLiteral]
145//! * [`ResourceDestinationLiteralMarker`][Name::ResourceDestinationLiteralMarker]
146//! * [`ResourceDestinationRaw`][Name::ResourceDestinationRaw]
147//! * [`ResourceDestinationString`][Name::ResourceDestinationString]
148//! * [`ResourceMarker`][Name::ResourceMarker]
149//! * [`ResourceTitle`][Name::ResourceTitle]
150//! * [`ResourceTitleMarker`][Name::ResourceTitleMarker]
151//! * [`ResourceTitleString`][Name::ResourceTitleString]
152//! * [`SpaceOrTab`][Name::SpaceOrTab]
153//!
154//! ## References
155//!
156//! * [`label-end.js` in `micromark`](https://github.com/micromark/micromark/blob/main/packages/micromark-core-commonmark/dev/lib/label-end.js)
157//! * [`micromark-extension-gfm-task-list-item`](https://github.com/micromark/micromark-extension-gfm-footnote)
158//! * [*§ 4.7 Link reference definitions* in `CommonMark`](https://spec.commonmark.org/0.31/#link-reference-definitions)
159//! * [*§ 6.3 Links* in `CommonMark`](https://spec.commonmark.org/0.31/#links)
160//! * [*§ 6.4 Images* in `CommonMark`](https://spec.commonmark.org/0.31/#images)
161//!
162//! > 👉 **Note**: Footnotes are not specified in GFM yet.
163//! > See [`github/cmark-gfm#270`](https://github.com/github/cmark-gfm/issues/270)
164//! > for the related issue.
165//!
166//! [string]: crate::construct::string
167//! [text]: crate::construct::text
168//! [destination]: crate::construct::partial_destination
169//! [title]: crate::construct::partial_title
170//! [label]: crate::construct::partial_label
171//! [label_start_image]: crate::construct::label_start_image
172//! [label_start_link]: crate::construct::label_start_link
173//! [gfm_label_start_footnote]: crate::construct::gfm_label_start_footnote
174//! [definition]: crate::construct::definition
175//! [autolink]: crate::construct::autolink
176//! [sanitize_uri]: crate::util::sanitize_uri::sanitize
177//! [normalize_identifier]: crate::util::normalize_identifier::normalize_identifier
178//! [html_a]: https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element
179//! [html_img]: https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element
180//! [html_sup]: https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-sub-and-sup-elements
181
182use crate::construct::partial_space_or_tab_eol::space_or_tab_eol;
183use crate::event::{Event, Kind, Name};
184use crate::resolve::Name as ResolveName;
185use crate::state::{Name as StateName, State};
186use crate::subtokenize::Subresult;
187use crate::tokenizer::{Label, LabelKind, LabelStart, Tokenizer};
188use crate::util::{
189 constant::RESOURCE_DESTINATION_BALANCE_MAX,
190 normalize_identifier::normalize_identifier,
191 skip,
192 slice::{Position, Slice},
193};
194use alloc::{string::String, vec};
195
196/// Start of label end.
197///
198/// ```markdown
199/// > | [a](b) c
200/// ^
201/// > | [a][b] c
202/// ^
203/// > | [a][] b
204/// ^
205/// > | [a] b
206/// ```
207pub fn start(tokenizer: &mut Tokenizer) -> State {
208 if Some(b']') == tokenizer.current && tokenizer.parse_state.options.constructs.label_end {
209 // If there is an okay opening:
210 if !tokenizer.tokenize_state.label_starts.is_empty() {
211 let label_start = tokenizer.tokenize_state.label_starts.last().unwrap();
212
213 tokenizer.tokenize_state.end = tokenizer.events.len();
214
215 // If the corresponding label (link) start is marked as inactive,
216 // it means we’d be wrapping a link, like this:
217 //
218 // ```markdown
219 // > | a [b [c](d) e](f) g.
220 // ^
221 // ```
222 //
223 // We can’t have that, so it’s just balanced brackets.
224 if label_start.inactive {
225 return State::Retry(StateName::LabelEndNok);
226 }
227
228 tokenizer.enter(Name::LabelEnd);
229 tokenizer.enter(Name::LabelMarker);
230 tokenizer.consume();
231 tokenizer.exit(Name::LabelMarker);
232 tokenizer.exit(Name::LabelEnd);
233 return State::Next(StateName::LabelEndAfter);
234 }
235 }
236
237 State::Nok
238}
239
240/// After `]`.
241///
242/// ```markdown
243/// > | [a](b) c
244/// ^
245/// > | [a][b] c
246/// ^
247/// > | [a][] b
248/// ^
249/// > | [a] b
250/// ^
251/// ```
252pub fn after(tokenizer: &mut Tokenizer) -> State {
253 let start_index = tokenizer.tokenize_state.label_starts.len() - 1;
254 let start = &tokenizer.tokenize_state.label_starts[start_index];
255
256 let indices = (
257 tokenizer.events[start.start.1].point.index,
258 tokenizer.events[tokenizer.tokenize_state.end].point.index,
259 );
260
261 // We don’t care about virtual spaces, so `indices` and `as_str` are fine.
262 let mut id = normalize_identifier(
263 Slice::from_indices(tokenizer.parse_state.bytes, indices.0, indices.1).as_str(),
264 );
265
266 // See if this matches a footnote definition.
267 if start.kind == LabelKind::GfmFootnote {
268 if tokenizer.parse_state.gfm_footnote_definitions.contains(&id) {
269 return State::Retry(StateName::LabelEndOk);
270 }
271
272 // Nope, this might be a normal link?
273 tokenizer.tokenize_state.label_starts[start_index].kind = LabelKind::GfmUndefinedFootnote;
274 let mut new_id = String::new();
275 new_id.push('^');
276 new_id.push_str(&id);
277 id = new_id;
278 }
279
280 let defined = tokenizer.parse_state.definitions.contains(&id);
281
282 match tokenizer.current {
283 // Resource (`[asd](fgh)`)?
284 Some(b'(') => {
285 tokenizer.attempt(
286 State::Next(StateName::LabelEndOk),
287 State::Next(if defined {
288 StateName::LabelEndOk
289 } else {
290 StateName::LabelEndNok
291 }),
292 );
293 State::Retry(StateName::LabelEndResourceStart)
294 }
295 // Full (`[asd][fgh]`) or collapsed (`[asd][]`) reference?
296 Some(b'[') => {
297 tokenizer.attempt(
298 State::Next(StateName::LabelEndOk),
299 State::Next(if defined {
300 StateName::LabelEndReferenceNotFull
301 } else {
302 StateName::LabelEndNok
303 }),
304 );
305 State::Retry(StateName::LabelEndReferenceFull)
306 }
307 // Shortcut (`[asd]`) reference?
308 _ => State::Retry(if defined {
309 StateName::LabelEndOk
310 } else {
311 StateName::LabelEndNok
312 }),
313 }
314}
315
316/// After `]`, at `[`, but not at a full reference.
317///
318/// > 👉 **Note**: we only get here if the label is defined.
319///
320/// ```markdown
321/// > | [a][] b
322/// ^
323/// > | [a] b
324/// ^
325/// ```
326pub fn reference_not_full(tokenizer: &mut Tokenizer) -> State {
327 tokenizer.attempt(
328 State::Next(StateName::LabelEndOk),
329 State::Next(StateName::LabelEndNok),
330 );
331 State::Retry(StateName::LabelEndReferenceCollapsed)
332}
333
334/// Done, we found something.
335///
336/// ```markdown
337/// > | [a](b) c
338/// ^
339/// > | [a][b] c
340/// ^
341/// > | [a][] b
342/// ^
343/// > | [a] b
344/// ^
345/// ```
346pub fn ok(tokenizer: &mut Tokenizer) -> State {
347 // Remove the start.
348 let label_start = tokenizer.tokenize_state.label_starts.pop().unwrap();
349
350 // If this is a link or footnote, we need to mark earlier link starts as no
351 // longer viable for use (as they would otherwise contain a link).
352 // These link starts are still looking for balanced closing brackets, so
353 // we can’t remove them, but we can mark them.
354 if label_start.kind != LabelKind::Image {
355 let mut index = 0;
356 while index < tokenizer.tokenize_state.label_starts.len() {
357 let label_start = &mut tokenizer.tokenize_state.label_starts[index];
358 if label_start.kind != LabelKind::Image {
359 label_start.inactive = true;
360 }
361 index += 1;
362 }
363 }
364
365 tokenizer.tokenize_state.labels.push(Label {
366 kind: label_start.kind,
367 start: label_start.start,
368 end: (tokenizer.tokenize_state.end, tokenizer.events.len() - 1),
369 });
370 tokenizer.tokenize_state.end = 0;
371 tokenizer.register_resolver_before(ResolveName::Label);
372 State::Ok
373}
374
375/// Done, it’s nothing.
376///
377/// There was an okay opening, but we didn’t match anything.
378///
379/// ```markdown
380/// > | [a](b c
381/// ^
382/// > | [a][b c
383/// ^
384/// > | [a] b
385/// ^
386/// ```
387pub fn nok(tokenizer: &mut Tokenizer) -> State {
388 let start = tokenizer.tokenize_state.label_starts.pop().unwrap();
389 tokenizer.tokenize_state.label_starts_loose.push(start);
390 tokenizer.tokenize_state.end = 0;
391 State::Nok
392}
393
394/// At a resource.
395///
396/// ```markdown
397/// > | [a](b) c
398/// ^
399/// ```
400pub fn resource_start(tokenizer: &mut Tokenizer) -> State {
401 match tokenizer.current {
402 Some(b'(') => {
403 tokenizer.enter(Name::Resource);
404 tokenizer.enter(Name::ResourceMarker);
405 tokenizer.consume();
406 tokenizer.exit(Name::ResourceMarker);
407 State::Next(StateName::LabelEndResourceBefore)
408 }
409 _ => unreachable!("expected `(`"),
410 }
411}
412
413/// In resource, after `(`, at optional whitespace.
414///
415/// ```markdown
416/// > | [a](b) c
417/// ^
418/// ```
419pub fn resource_before(tokenizer: &mut Tokenizer) -> State {
420 if matches!(tokenizer.current, Some(b'\t' | b'\n' | b' ')) {
421 tokenizer.attempt(
422 State::Next(StateName::LabelEndResourceOpen),
423 State::Next(StateName::LabelEndResourceOpen),
424 );
425 State::Retry(space_or_tab_eol(tokenizer))
426 } else {
427 State::Retry(StateName::LabelEndResourceOpen)
428 }
429}
430
431/// In resource, after optional whitespace, at `)` or a destination.
432///
433/// ```markdown
434/// > | [a](b) c
435/// ^
436/// ```
437pub fn resource_open(tokenizer: &mut Tokenizer) -> State {
438 if let Some(b')') = tokenizer.current {
439 State::Retry(StateName::LabelEndResourceEnd)
440 } else {
441 tokenizer.tokenize_state.token_1 = Name::ResourceDestination;
442 tokenizer.tokenize_state.token_2 = Name::ResourceDestinationLiteral;
443 tokenizer.tokenize_state.token_3 = Name::ResourceDestinationLiteralMarker;
444 tokenizer.tokenize_state.token_4 = Name::ResourceDestinationRaw;
445 tokenizer.tokenize_state.token_5 = Name::ResourceDestinationString;
446 tokenizer.tokenize_state.size_b = RESOURCE_DESTINATION_BALANCE_MAX;
447
448 tokenizer.attempt(
449 State::Next(StateName::LabelEndResourceDestinationAfter),
450 State::Next(StateName::LabelEndResourceDestinationMissing),
451 );
452 State::Retry(StateName::DestinationStart)
453 }
454}
455
456/// In resource, after destination, at optional whitespace.
457///
458/// ```markdown
459/// > | [a](b) c
460/// ^
461/// ```
462pub fn resource_destination_after(tokenizer: &mut Tokenizer) -> State {
463 tokenizer.tokenize_state.token_1 = Name::Data;
464 tokenizer.tokenize_state.token_2 = Name::Data;
465 tokenizer.tokenize_state.token_3 = Name::Data;
466 tokenizer.tokenize_state.token_4 = Name::Data;
467 tokenizer.tokenize_state.token_5 = Name::Data;
468 tokenizer.tokenize_state.size_b = 0;
469
470 if matches!(tokenizer.current, Some(b'\t' | b'\n' | b' ')) {
471 tokenizer.attempt(
472 State::Next(StateName::LabelEndResourceBetween),
473 State::Next(StateName::LabelEndResourceEnd),
474 );
475 State::Retry(space_or_tab_eol(tokenizer))
476 } else {
477 State::Retry(StateName::LabelEndResourceEnd)
478 }
479}
480
481/// At invalid destination.
482///
483/// ```markdown
484/// > | [a](<<) b
485/// ^
486/// ```
487pub fn resource_destination_missing(tokenizer: &mut Tokenizer) -> State {
488 tokenizer.tokenize_state.token_1 = Name::Data;
489 tokenizer.tokenize_state.token_2 = Name::Data;
490 tokenizer.tokenize_state.token_3 = Name::Data;
491 tokenizer.tokenize_state.token_4 = Name::Data;
492 tokenizer.tokenize_state.token_5 = Name::Data;
493 tokenizer.tokenize_state.size_b = 0;
494 State::Nok
495}
496
497/// In resource, after destination and whitespace, at `(` or title.
498///
499/// ```markdown
500/// > | [a](b ) c
501/// ^
502/// ```
503pub fn resource_between(tokenizer: &mut Tokenizer) -> State {
504 match tokenizer.current {
505 Some(b'"' | b'\'' | b'(') => {
506 tokenizer.tokenize_state.token_1 = Name::ResourceTitle;
507 tokenizer.tokenize_state.token_2 = Name::ResourceTitleMarker;
508 tokenizer.tokenize_state.token_3 = Name::ResourceTitleString;
509 tokenizer.attempt(
510 State::Next(StateName::LabelEndResourceTitleAfter),
511 State::Nok,
512 );
513 State::Retry(StateName::TitleStart)
514 }
515 _ => State::Retry(StateName::LabelEndResourceEnd),
516 }
517}
518
519/// In resource, after title, at optional whitespace.
520///
521/// ```markdown
522/// > | [a](b "c") d
523/// ^
524/// ```
525pub fn resource_title_after(tokenizer: &mut Tokenizer) -> State {
526 tokenizer.tokenize_state.token_1 = Name::Data;
527 tokenizer.tokenize_state.token_2 = Name::Data;
528 tokenizer.tokenize_state.token_3 = Name::Data;
529
530 if matches!(tokenizer.current, Some(b'\t' | b'\n' | b' ')) {
531 tokenizer.attempt(
532 State::Next(StateName::LabelEndResourceEnd),
533 State::Next(StateName::LabelEndResourceEnd),
534 );
535 State::Retry(space_or_tab_eol(tokenizer))
536 } else {
537 State::Retry(StateName::LabelEndResourceEnd)
538 }
539}
540
541/// In resource, at `)`.
542///
543/// ```markdown
544/// > | [a](b) d
545/// ^
546/// ```
547pub fn resource_end(tokenizer: &mut Tokenizer) -> State {
548 match tokenizer.current {
549 Some(b')') => {
550 tokenizer.enter(Name::ResourceMarker);
551 tokenizer.consume();
552 tokenizer.exit(Name::ResourceMarker);
553 tokenizer.exit(Name::Resource);
554 State::Ok
555 }
556 _ => State::Nok,
557 }
558}
559
560/// In reference (full), at `[`.
561///
562/// ```markdown
563/// > | [a][b] d
564/// ^
565/// ```
566pub fn reference_full(tokenizer: &mut Tokenizer) -> State {
567 match tokenizer.current {
568 Some(b'[') => {
569 tokenizer.tokenize_state.token_1 = Name::Reference;
570 tokenizer.tokenize_state.token_2 = Name::ReferenceMarker;
571 tokenizer.tokenize_state.token_3 = Name::ReferenceString;
572 tokenizer.attempt(
573 State::Next(StateName::LabelEndReferenceFullAfter),
574 State::Next(StateName::LabelEndReferenceFullMissing),
575 );
576 State::Retry(StateName::LabelStart)
577 }
578 _ => unreachable!("expected `[`"),
579 }
580}
581
582/// In reference (full), after `]`.
583///
584/// ```markdown
585/// > | [a][b] d
586/// ^
587/// ```
588pub fn reference_full_after(tokenizer: &mut Tokenizer) -> State {
589 tokenizer.tokenize_state.token_1 = Name::Data;
590 tokenizer.tokenize_state.token_2 = Name::Data;
591 tokenizer.tokenize_state.token_3 = Name::Data;
592
593 if tokenizer
594 .parse_state
595 .definitions
596 // We don’t care about virtual spaces, so `as_str` is fine.
597 .contains(&normalize_identifier(
598 Slice::from_position(
599 tokenizer.parse_state.bytes,
600 &Position::from_exit_event(
601 &tokenizer.events,
602 skip::to_back(
603 &tokenizer.events,
604 tokenizer.events.len() - 1,
605 &[Name::ReferenceString],
606 ),
607 ),
608 )
609 .as_str(),
610 ))
611 {
612 State::Ok
613 } else {
614 State::Nok
615 }
616}
617
618/// In reference (full) that was missing.
619///
620/// ```markdown
621/// > | [a][b d
622/// ^
623/// ```
624pub fn reference_full_missing(tokenizer: &mut Tokenizer) -> State {
625 tokenizer.tokenize_state.token_1 = Name::Data;
626 tokenizer.tokenize_state.token_2 = Name::Data;
627 tokenizer.tokenize_state.token_3 = Name::Data;
628 State::Nok
629}
630
631/// In reference (collapsed), at `[`.
632///
633/// > 👉 **Note**: we only get here if the label is defined.
634///
635/// ```markdown
636/// > | [a][] d
637/// ^
638/// ```
639pub fn reference_collapsed(tokenizer: &mut Tokenizer) -> State {
640 // We only attempt a collapsed label if there’s a `[`.
641 debug_assert_eq!(tokenizer.current, Some(b'['), "expected opening bracket");
642 tokenizer.enter(Name::Reference);
643 tokenizer.enter(Name::ReferenceMarker);
644 tokenizer.consume();
645 tokenizer.exit(Name::ReferenceMarker);
646 State::Next(StateName::LabelEndReferenceCollapsedOpen)
647}
648
649/// In reference (collapsed), at `]`.
650///
651/// > 👉 **Note**: we only get here if the label is defined.
652///
653/// ```markdown
654/// > | [a][] d
655/// ^
656/// ```
657pub fn reference_collapsed_open(tokenizer: &mut Tokenizer) -> State {
658 match tokenizer.current {
659 Some(b']') => {
660 tokenizer.enter(Name::ReferenceMarker);
661 tokenizer.consume();
662 tokenizer.exit(Name::ReferenceMarker);
663 tokenizer.exit(Name::Reference);
664 State::Ok
665 }
666 _ => State::Nok,
667 }
668}
669
670/// Resolve images, links, and footnotes.
671///
672/// This turns matching label starts and label ends into links, images, and
673/// footnotes, and turns unmatched label starts back into data.
674pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
675 // Inject labels.
676 let labels = tokenizer.tokenize_state.labels.split_off(0);
677 inject_labels(tokenizer, &labels);
678 // Handle loose starts.
679 let starts = tokenizer.tokenize_state.label_starts.split_off(0);
680 mark_as_data(tokenizer, &starts);
681 let starts = tokenizer.tokenize_state.label_starts_loose.split_off(0);
682 mark_as_data(tokenizer, &starts);
683
684 tokenizer.map.consume(&mut tokenizer.events);
685 None
686}
687
688/// Inject links/images/footnotes.
689fn inject_labels(tokenizer: &mut Tokenizer, labels: &[Label]) {
690 // Add grouping events.
691 let mut index = 0;
692 while index < labels.len() {
693 let label = &labels[index];
694 let group_name = if label.kind == LabelKind::GfmFootnote {
695 Name::GfmFootnoteCall
696 } else if label.kind == LabelKind::Image {
697 Name::Image
698 } else {
699 Name::Link
700 };
701
702 // If this is a fine link, which starts with a footnote start that did
703 // not match, we need to inject the caret as data.
704 let mut caret = vec![];
705
706 if label.kind == LabelKind::GfmUndefinedFootnote {
707 // Add caret.
708 caret.push(Event {
709 kind: Kind::Enter,
710 name: Name::Data,
711 // Enter:GfmFootnoteCallMarker.
712 point: tokenizer.events[label.start.1 - 2].point.clone().clone(),
713 link: None,
714 });
715 caret.push(Event {
716 kind: Kind::Exit,
717 name: Name::Data,
718 // Exit:GfmFootnoteCallMarker.
719 point: tokenizer.events[label.start.1 - 1].point.clone(),
720 link: None,
721 });
722 // Change and move label end.
723 tokenizer.events[label.start.0].name = Name::LabelLink;
724 tokenizer.events[label.start.1].name = Name::LabelLink;
725 tokenizer.events[label.start.1].point = caret[0].point.clone();
726 // Remove the caret.
727 // Enter:GfmFootnoteCallMarker, Exit:GfmFootnoteCallMarker.
728 tokenizer.map.add(label.start.1 - 2, 2, vec![]);
729 }
730
731 // Insert a group enter and label enter.
732 tokenizer.map.add(
733 label.start.0,
734 0,
735 vec![
736 Event {
737 kind: Kind::Enter,
738 name: group_name.clone(),
739 point: tokenizer.events[label.start.0].point.clone(),
740 link: None,
741 },
742 Event {
743 kind: Kind::Enter,
744 name: Name::Label,
745 point: tokenizer.events[label.start.0].point.clone(),
746 link: None,
747 },
748 ],
749 );
750
751 // Empty events not allowed.
752 // Though: if this was what looked like a footnote, but didn’t match,
753 // it’s a link instead, and we need to inject the `^`.
754 if label.start.1 != label.end.0 || !caret.is_empty() {
755 tokenizer.map.add_before(
756 label.start.1 + 1,
757 0,
758 vec![Event {
759 kind: Kind::Enter,
760 name: Name::LabelText,
761 point: tokenizer.events[label.start.1].point.clone(),
762 link: None,
763 }],
764 );
765 tokenizer.map.add(
766 label.end.0,
767 0,
768 vec![Event {
769 kind: Kind::Exit,
770 name: Name::LabelText,
771 point: tokenizer.events[label.end.0].point.clone(),
772 link: None,
773 }],
774 );
775 }
776
777 if !caret.is_empty() {
778 tokenizer.map.add(label.start.1 + 1, 0, caret);
779 }
780
781 // Insert a label exit.
782 tokenizer.map.add(
783 label.end.0 + 4,
784 0,
785 vec![Event {
786 kind: Kind::Exit,
787 name: Name::Label,
788 point: tokenizer.events[label.end.0 + 3].point.clone(),
789 link: None,
790 }],
791 );
792
793 // Insert a group exit.
794 tokenizer.map.add(
795 label.end.1 + 1,
796 0,
797 vec![Event {
798 kind: Kind::Exit,
799 name: group_name,
800 point: tokenizer.events[label.end.1].point.clone(),
801 link: None,
802 }],
803 );
804
805 index += 1;
806 }
807}
808
809/// Remove loose label starts.
810fn mark_as_data(tokenizer: &mut Tokenizer, events: &[LabelStart]) {
811 let mut index = 0;
812
813 while index < events.len() {
814 let data_enter_index = events[index].start.0;
815 let data_exit_index = events[index].start.1;
816
817 tokenizer.map.add(
818 data_enter_index,
819 data_exit_index - data_enter_index + 1,
820 vec![
821 Event {
822 kind: Kind::Enter,
823 name: Name::Data,
824 point: tokenizer.events[data_enter_index].point.clone(),
825 link: None,
826 },
827 Event {
828 kind: Kind::Exit,
829 name: Name::Data,
830 point: tokenizer.events[data_exit_index].point.clone(),
831 link: None,
832 },
833 ],
834 );
835
836 index += 1;
837 }
838}