Markdown parser fork with extended syntax for personal use.
1//! Turn events into a string of HTML.
2use crate::event::{Event, Kind, Name};
3use crate::mdast::AlignKind;
4use crate::util::{
5 character_reference::decode as decode_character_reference,
6 constant::{SAFE_PROTOCOL_HREF, SAFE_PROTOCOL_SRC},
7 encode::encode,
8 gfm_tagfilter::gfm_tagfilter,
9 infer::{gfm_table_align, list_loose},
10 normalize_identifier::normalize_identifier,
11 sanitize_uri::{sanitize, sanitize_with_protocols},
12 skip,
13 slice::{Position, Slice},
14};
15use crate::{CompileOptions, LineEnding};
16use alloc::{
17 format,
18 string::{String, ToString},
19 vec,
20 vec::Vec,
21};
22use core::str;
23
24/// Link, image, or footnote call.
25/// Resource or reference.
26/// Reused for temporary definitions as well, in the first pass.
27#[derive(Debug)]
28struct Media {
29 /// Whether this represents an image (`true`) or a link or definition
30 /// (`false`).
31 image: bool,
32 /// The text between the brackets (`x` in `![x]()` and `[x]()`).
33 ///
34 /// Not interpreted.
35 label_id: Option<(usize, usize)>,
36 /// The result of interpreting the text between the brackets
37 /// (`x` in `![x]()` and `[x]()`).
38 ///
39 /// When this is a link, it contains further text content and thus HTML
40 /// tags.
41 /// Otherwise, when an image, text content is also allowed, but resulting
42 /// tags are ignored.
43 label: Option<String>,
44 /// The string between the explicit brackets of the reference (`y` in
45 /// `[x][y]`), as content.
46 ///
47 /// Not interpreted.
48 reference_id: Option<(usize, usize)>,
49 /// The destination (url).
50 ///
51 /// Interpreted string content.
52 destination: Option<String>,
53 /// The destination (url).
54 ///
55 /// Interpreted string content.
56 title: Option<String>,
57}
58
59/// Representation of a definition.
60#[derive(Debug)]
61struct Definition {
62 /// Identifier.
63 id: String,
64 /// The destination (url).
65 ///
66 /// Interpreted string content.
67 destination: Option<String>,
68 /// The title.
69 ///
70 /// Interpreted string content.
71 title: Option<String>,
72}
73
74/// Context used to compile markdown.
75#[allow(clippy::struct_excessive_bools)]
76#[derive(Debug)]
77struct CompileContext<'a> {
78 // Static info.
79 /// List of events.
80 events: &'a [Event],
81 /// List of bytes.
82 bytes: &'a [u8],
83 /// Configuration.
84 options: &'a CompileOptions,
85 // Fields used by handlers to track the things they need to track to
86 // compile markdown.
87 /// Rank of heading (atx).
88 heading_atx_rank: Option<usize>,
89 /// Buffer of heading (setext) text.
90 heading_setext_buffer: Option<String>,
91 /// Whether raw (flow) (code (fenced), math (flow)) or code (indented) contains data.
92 raw_flow_seen_data: Option<bool>,
93 /// Number of raw (flow) fences.
94 raw_flow_fences_count: Option<usize>,
95 /// Whether we are in code (text).
96 raw_text_inside: bool,
97 /// Whether we are in image text.
98 image_alt_inside: bool,
99 /// Marker of character reference.
100 character_reference_marker: Option<u8>,
101 /// Whether we are expecting the first list item marker.
102 list_expect_first_marker: Option<bool>,
103 /// Stack of media (link, image).
104 media_stack: Vec<Media>,
105 /// Stack of containers.
106 tight_stack: Vec<bool>,
107 /// List of definitions.
108 definitions: Vec<Definition>,
109 /// List of definitions.
110 gfm_footnote_definitions: Vec<(String, String)>,
111 gfm_footnote_definition_calls: Vec<(String, usize)>,
112 gfm_footnote_definition_stack: Vec<(usize, usize)>,
113 /// Whether we are in a GFM table head.
114 gfm_table_in_head: bool,
115 /// Current GFM table alignment.
116 gfm_table_align: Option<Vec<AlignKind>>,
117 /// Current GFM table column.
118 gfm_table_column: usize,
119 // Fields used to influance the current compilation.
120 /// Ignore the next line ending.
121 slurp_one_line_ending: bool,
122 /// Whether to encode HTML.
123 encode_html: bool,
124 // Configuration
125 /// Line ending to use.
126 line_ending_default: LineEnding,
127 // Intermediate results.
128 /// Stack of buffers.
129 buffers: Vec<String>,
130 /// Current event index.
131 index: usize,
132}
133
134impl<'a> CompileContext<'a> {
135 /// Create a new compile context.
136 fn new(
137 events: &'a [Event],
138 bytes: &'a [u8],
139 options: &'a CompileOptions,
140 line_ending: LineEnding,
141 ) -> CompileContext<'a> {
142 CompileContext {
143 events,
144 bytes,
145 heading_atx_rank: None,
146 heading_setext_buffer: None,
147 raw_flow_seen_data: None,
148 raw_flow_fences_count: None,
149 raw_text_inside: false,
150 character_reference_marker: None,
151 list_expect_first_marker: None,
152 media_stack: vec![],
153 definitions: vec![],
154 gfm_footnote_definitions: vec![],
155 gfm_footnote_definition_calls: vec![],
156 gfm_footnote_definition_stack: vec![],
157 gfm_table_in_head: false,
158 gfm_table_align: None,
159 gfm_table_column: 0,
160 tight_stack: vec![],
161 slurp_one_line_ending: false,
162 image_alt_inside: false,
163 encode_html: true,
164 line_ending_default: line_ending,
165 buffers: vec![String::new()],
166 index: 0,
167 options,
168 }
169 }
170
171 /// Push a buffer.
172 fn buffer(&mut self) {
173 self.buffers.push(String::new());
174 }
175
176 /// Pop a buffer, returning its value.
177 fn resume(&mut self) -> String {
178 self.buffers.pop().expect("Cannot resume w/o buffer")
179 }
180
181 /// Push a str to the last buffer.
182 fn push(&mut self, value: &str) {
183 let last_buf_opt = self.buffers.last_mut();
184 let last_buf = last_buf_opt.expect("at least one buffer should exist");
185 last_buf.push_str(value);
186 }
187
188 /// Add a line ending.
189 fn line_ending(&mut self) {
190 let eol = self.line_ending_default.as_str().to_string();
191 self.push(&eol);
192 }
193
194 /// Add a line ending if needed (as in, there’s no eol/eof already).
195 fn line_ending_if_needed(&mut self) {
196 let last_buf_opt = self.buffers.last();
197 let last_buf = last_buf_opt.expect("at least one buffer should exist");
198 let last_byte = last_buf.as_bytes().last();
199
200 if !matches!(last_byte, None | Some(b'\n' | b'\r')) {
201 self.line_ending();
202 }
203 }
204}
205
206/// Turn events and bytes into a string of HTML.
207pub fn compile(events: &[Event], bytes: &[u8], options: &CompileOptions) -> String {
208 let mut index = 0;
209 let mut line_ending_inferred = None;
210
211 // First, we figure out what the used line ending style is.
212 // Stop when we find a line ending.
213 while index < events.len() {
214 let event = &events[index];
215
216 if event.kind == Kind::Exit
217 && (event.name == Name::BlankLineEnding || event.name == Name::LineEnding)
218 {
219 let slice = Slice::from_position(bytes, &Position::from_exit_event(events, index));
220 line_ending_inferred = Some(slice.as_str().parse().unwrap());
221 break;
222 }
223
224 index += 1;
225 }
226
227 // Figure out which line ending style we’ll use.
228 let line_ending_default =
229 line_ending_inferred.unwrap_or_else(|| options.default_line_ending.clone());
230
231 let mut context = CompileContext::new(events, bytes, options, line_ending_default);
232 let mut definition_indices = vec![];
233 let mut index = 0;
234 let mut definition_inside = false;
235
236 // Handle all definitions first.
237 // We must do two passes because we need to compile the events in
238 // definitions which come after references already.
239 //
240 // To speed things up, we collect the places we can jump over for the
241 // second pass.
242 //
243 // We don’t need to handle GFM footnote definitions like this, because
244 // unlike normal definitions, what they produce is not used in calls.
245 // It would also get very complex, because footnote definitions can be
246 // nested.
247 while index < events.len() {
248 let event = &events[index];
249
250 if definition_inside {
251 handle(&mut context, index);
252 }
253
254 if event.kind == Kind::Enter {
255 if event.name == Name::Definition {
256 handle(&mut context, index); // Also handle start.
257 definition_inside = true;
258 definition_indices.push((index, index));
259 }
260 } else if event.name == Name::Definition {
261 definition_inside = false;
262 definition_indices.last_mut().unwrap().1 = index;
263 }
264
265 index += 1;
266 }
267
268 let mut index = 0;
269 let jump_default = (events.len(), events.len());
270 let mut definition_index = 0;
271 let mut jump = definition_indices
272 .get(definition_index)
273 .unwrap_or(&jump_default);
274
275 while index < events.len() {
276 if index == jump.0 {
277 index = jump.1 + 1;
278 definition_index += 1;
279 jump = definition_indices
280 .get(definition_index)
281 .unwrap_or(&jump_default);
282 } else {
283 handle(&mut context, index);
284 index += 1;
285 }
286 }
287
288 // No section to generate.
289 if !context.gfm_footnote_definition_calls.is_empty() {
290 generate_footnote_section(&mut context);
291 }
292
293 debug_assert_eq!(context.buffers.len(), 1, "expected 1 final buffer");
294 context
295 .buffers
296 .first()
297 .expect("expected 1 final buffer")
298 .into()
299}
300
301/// Handle the event at `index`.
302fn handle(context: &mut CompileContext, index: usize) {
303 context.index = index;
304
305 if context.events[index].kind == Kind::Enter {
306 enter(context);
307 } else {
308 exit(context);
309 }
310}
311
312/// Handle [`Enter`][Kind::Enter].
313fn enter(context: &mut CompileContext) {
314 match context.events[context.index].name {
315 Name::CodeFencedFenceInfo
316 | Name::CodeFencedFenceMeta
317 | Name::MathFlowFenceMeta
318 | Name::DefinitionLabelString
319 | Name::DefinitionTitleString
320 | Name::GfmFootnoteDefinitionPrefix
321 | Name::HeadingAtxText
322 | Name::HeadingSetextText
323 | Name::Label
324 | Name::MdxEsm
325 | Name::MdxFlowExpression
326 | Name::MdxTextExpression
327 | Name::MdxJsxFlowTag
328 | Name::MdxJsxTextTag
329 | Name::ReferenceString
330 | Name::ResourceTitleString => on_enter_buffer(context),
331
332 Name::BlockQuote => on_enter_block_quote(context),
333 Name::CodeIndented => on_enter_code_indented(context),
334 Name::CodeFenced | Name::MathFlow => on_enter_raw_flow(context),
335 Name::CodeText | Name::MathText => on_enter_raw_text(context),
336 Name::Definition => on_enter_definition(context),
337 Name::DefinitionDestinationString => on_enter_definition_destination_string(context),
338 Name::Emphasis => on_enter_emphasis(context),
339 Name::Frontmatter => on_enter_frontmatter(context),
340 Name::GfmFootnoteDefinition => on_enter_gfm_footnote_definition(context),
341 Name::GfmFootnoteCall => on_enter_gfm_footnote_call(context),
342 Name::GfmStrikethrough => on_enter_gfm_strikethrough(context),
343 Name::GfmTable => on_enter_gfm_table(context),
344 Name::GfmTableBody => on_enter_gfm_table_body(context),
345 Name::GfmTableCell => on_enter_gfm_table_cell(context),
346 Name::GfmTableHead => on_enter_gfm_table_head(context),
347 Name::GfmTableRow => on_enter_gfm_table_row(context),
348 Name::GfmTaskListItemCheck => on_enter_gfm_task_list_item_check(context),
349 Name::HtmlFlow => on_enter_html_flow(context),
350 Name::HtmlText => on_enter_html_text(context),
351 Name::Image => on_enter_image(context),
352 Name::Link => on_enter_link(context),
353 Name::ListItemMarker => on_enter_list_item_marker(context),
354 Name::ListOrdered | Name::ListUnordered => on_enter_list(context),
355 Name::Paragraph => on_enter_paragraph(context),
356 Name::Resource => on_enter_resource(context),
357 Name::ResourceDestinationString => on_enter_resource_destination_string(context),
358 Name::Strong => on_enter_strong(context),
359 _ => {}
360 }
361}
362
363/// Handle [`Exit`][Kind::Exit].
364fn exit(context: &mut CompileContext) {
365 match context.events[context.index].name {
366 Name::CodeFencedFenceMeta
367 | Name::MathFlowFenceMeta
368 | Name::MdxJsxTextTag
369 | Name::MdxTextExpression
370 | Name::Resource => {
371 on_exit_drop(context);
372 }
373 Name::MdxEsm | Name::MdxFlowExpression | Name::MdxJsxFlowTag => on_exit_drop_slurp(context),
374 Name::CharacterEscapeValue | Name::CodeTextData | Name::Data | Name::MathTextData => {
375 on_exit_data(context);
376 }
377 Name::AutolinkEmail => on_exit_autolink_email(context),
378 Name::AutolinkProtocol => on_exit_autolink_protocol(context),
379 Name::BlankLineEnding => on_exit_blank_line_ending(context),
380 Name::BlockQuote => on_exit_block_quote(context),
381 Name::CharacterReferenceMarker => on_exit_character_reference_marker(context),
382 Name::CharacterReferenceMarkerNumeric => {
383 on_exit_character_reference_marker_numeric(context);
384 }
385 Name::CharacterReferenceMarkerHexadecimal => {
386 on_exit_character_reference_marker_hexadecimal(context);
387 }
388 Name::CharacterReferenceValue => on_exit_character_reference_value(context),
389 Name::CodeFenced | Name::CodeIndented | Name::MathFlow => on_exit_raw_flow(context),
390 Name::CodeFencedFence | Name::MathFlowFence => on_exit_raw_flow_fence(context),
391 Name::CodeFencedFenceInfo => on_exit_raw_flow_fence_info(context),
392 Name::CodeFlowChunk | Name::MathFlowChunk => on_exit_raw_flow_chunk(context),
393 Name::CodeText | Name::MathText => on_exit_raw_text(context),
394 Name::Definition => on_exit_definition(context),
395 Name::DefinitionDestinationString => on_exit_definition_destination_string(context),
396 Name::DefinitionLabelString => on_exit_definition_label_string(context),
397 Name::DefinitionTitleString => on_exit_definition_title_string(context),
398 Name::Emphasis => on_exit_emphasis(context),
399 Name::Frontmatter => on_exit_frontmatter(context),
400 Name::GfmAutolinkLiteralEmail => on_exit_gfm_autolink_literal_email(context),
401 Name::GfmAutolinkLiteralMailto => on_exit_gfm_autolink_literal_mailto(context),
402 Name::GfmAutolinkLiteralProtocol => on_exit_gfm_autolink_literal_protocol(context),
403 Name::GfmAutolinkLiteralWww => on_exit_gfm_autolink_literal_www(context),
404 Name::GfmAutolinkLiteralXmpp => on_exit_gfm_autolink_literal_xmpp(context),
405 Name::GfmFootnoteCall => on_exit_gfm_footnote_call(context),
406 Name::GfmFootnoteDefinitionLabelString => {
407 on_exit_gfm_footnote_definition_label_string(context);
408 }
409 Name::GfmFootnoteDefinitionPrefix => on_exit_gfm_footnote_definition_prefix(context),
410 Name::GfmFootnoteDefinition => on_exit_gfm_footnote_definition(context),
411 Name::GfmStrikethrough => on_exit_gfm_strikethrough(context),
412 Name::GfmTable => on_exit_gfm_table(context),
413 Name::GfmTableBody => on_exit_gfm_table_body(context),
414 Name::GfmTableCell => on_exit_gfm_table_cell(context),
415 Name::GfmTableHead => on_exit_gfm_table_head(context),
416 Name::GfmTableRow => on_exit_gfm_table_row(context),
417 Name::GfmTaskListItemCheck => on_exit_gfm_task_list_item_check(context),
418 Name::GfmTaskListItemValueChecked => on_exit_gfm_task_list_item_value_checked(context),
419 Name::HardBreakEscape | Name::HardBreakTrailing => on_exit_break(context),
420 Name::HeadingAtx => on_exit_heading_atx(context),
421 Name::HeadingAtxSequence => on_exit_heading_atx_sequence(context),
422 Name::HeadingAtxText => on_exit_heading_atx_text(context),
423 Name::HeadingSetextText => on_exit_heading_setext_text(context),
424 Name::HeadingSetextUnderlineSequence => on_exit_heading_setext_underline_sequence(context),
425 Name::HtmlFlow | Name::HtmlText => on_exit_html(context),
426 Name::HtmlFlowData | Name::HtmlTextData => on_exit_html_data(context),
427 Name::Image | Name::Link => on_exit_media(context),
428 Name::Label => on_exit_label(context),
429 Name::LabelText => on_exit_label_text(context),
430 Name::LineEnding => on_exit_line_ending(context),
431 Name::ListOrdered | Name::ListUnordered => on_exit_list(context),
432 Name::ListItem => on_exit_list_item(context),
433 Name::ListItemValue => on_exit_list_item_value(context),
434 Name::Paragraph => on_exit_paragraph(context),
435 Name::ReferenceString => on_exit_reference_string(context),
436 Name::ResourceDestinationString => on_exit_resource_destination_string(context),
437 Name::ResourceTitleString => on_exit_resource_title_string(context),
438 Name::Strong => on_exit_strong(context),
439 Name::ThematicBreak => on_exit_thematic_break(context),
440 _ => {}
441 }
442}
443
444/// Handle [`Enter`][Kind::Enter]:`*`.
445///
446/// Buffers data.
447fn on_enter_buffer(context: &mut CompileContext) {
448 context.buffer();
449}
450
451/// Handle [`Enter`][Kind::Enter]:[`BlockQuote`][Name::BlockQuote].
452fn on_enter_block_quote(context: &mut CompileContext) {
453 context.tight_stack.push(false);
454 context.line_ending_if_needed();
455 context.push("<blockquote>");
456}
457
458/// Handle [`Enter`][Kind::Enter]:[`CodeIndented`][Name::CodeIndented].
459fn on_enter_code_indented(context: &mut CompileContext) {
460 context.raw_flow_seen_data = Some(false);
461 context.line_ending_if_needed();
462 context.push("<pre><code>");
463}
464
465/// Handle [`Enter`][Kind::Enter]:{[`CodeFenced`][Name::CodeFenced],[`MathFlow`][Name::MathFlow]}.
466fn on_enter_raw_flow(context: &mut CompileContext) {
467 context.raw_flow_seen_data = Some(false);
468 context.line_ending_if_needed();
469 // Note that no `>` is used, which is added later (due to info)
470 context.push("<pre><code");
471 context.raw_flow_fences_count = Some(0);
472
473 if context.events[context.index].name == Name::MathFlow {
474 context.push(" class=\"language-math math-display\"");
475 }
476}
477
478/// Handle [`Enter`][Kind::Enter]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}.
479fn on_enter_raw_text(context: &mut CompileContext) {
480 context.raw_text_inside = true;
481 if !context.image_alt_inside {
482 context.push("<code");
483 if context.events[context.index].name == Name::MathText {
484 context.push(" class=\"language-math math-inline\"");
485 }
486 context.push(">");
487 }
488 context.buffer();
489}
490
491/// Handle [`Enter`][Kind::Enter]:[`Definition`][Name::Definition].
492fn on_enter_definition(context: &mut CompileContext) {
493 context.buffer();
494 context.media_stack.push(Media {
495 image: false,
496 label: None,
497 label_id: None,
498 reference_id: None,
499 destination: None,
500 title: None,
501 });
502}
503
504/// Handle [`Enter`][Kind::Enter]:[`DefinitionDestinationString`][Name::DefinitionDestinationString].
505fn on_enter_definition_destination_string(context: &mut CompileContext) {
506 context.buffer();
507 context.encode_html = false;
508}
509
510/// Handle [`Enter`][Kind::Enter]:[`Emphasis`][Name::Emphasis].
511fn on_enter_emphasis(context: &mut CompileContext) {
512 if !context.image_alt_inside {
513 context.push("<em>");
514 }
515}
516
517/// Handle [`Enter`][Kind::Enter]:[`Frontmatter`][Name::Frontmatter].
518fn on_enter_frontmatter(context: &mut CompileContext) {
519 context.buffer();
520}
521
522/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition].
523fn on_enter_gfm_footnote_definition(context: &mut CompileContext) {
524 context.tight_stack.push(false);
525}
526
527/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteCall`][Name::GfmFootnoteCall].
528fn on_enter_gfm_footnote_call(context: &mut CompileContext) {
529 context.media_stack.push(Media {
530 image: false,
531 label_id: None,
532 label: None,
533 reference_id: None,
534 destination: None,
535 title: None,
536 });
537}
538
539/// Handle [`Enter`][Kind::Enter]:[`GfmStrikethrough`][Name::GfmStrikethrough].
540fn on_enter_gfm_strikethrough(context: &mut CompileContext) {
541 if !context.image_alt_inside {
542 context.push("<del>");
543 }
544}
545
546/// Handle [`Enter`][Kind::Enter]:[`GfmTable`][Name::GfmTable].
547fn on_enter_gfm_table(context: &mut CompileContext) {
548 let align = gfm_table_align(context.events, context.index);
549 context.gfm_table_align = Some(align);
550 context.line_ending_if_needed();
551 context.push("<table>");
552}
553
554/// Handle [`Enter`][Kind::Enter]:[`GfmTableBody`][Name::GfmTableBody].
555fn on_enter_gfm_table_body(context: &mut CompileContext) {
556 context.push("<tbody>");
557}
558
559/// Handle [`Enter`][Kind::Enter]:[`GfmTableCell`][Name::GfmTableCell].
560fn on_enter_gfm_table_cell(context: &mut CompileContext) {
561 let column = context.gfm_table_column;
562 let align = context.gfm_table_align.as_ref().unwrap();
563
564 if column >= align.len() {
565 // Capture cell to ignore it.
566 context.buffer();
567 } else {
568 let value = align[column];
569 context.line_ending_if_needed();
570
571 if context.gfm_table_in_head {
572 context.push("<th");
573 } else {
574 context.push("<td");
575 }
576
577 match value {
578 AlignKind::Left => context.push(" align=\"left\""),
579 AlignKind::Right => context.push(" align=\"right\""),
580 AlignKind::Center => context.push(" align=\"center\""),
581 AlignKind::None => {}
582 }
583
584 context.push(">");
585 }
586}
587
588/// Handle [`Enter`][Kind::Enter]:[`GfmTableHead`][Name::GfmTableHead].
589fn on_enter_gfm_table_head(context: &mut CompileContext) {
590 context.line_ending_if_needed();
591 context.push("<thead>");
592 context.gfm_table_in_head = true;
593}
594
595/// Handle [`Enter`][Kind::Enter]:[`GfmTableRow`][Name::GfmTableRow].
596fn on_enter_gfm_table_row(context: &mut CompileContext) {
597 context.line_ending_if_needed();
598 context.push("<tr>");
599}
600
601/// Handle [`Enter`][Kind::Enter]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck].
602fn on_enter_gfm_task_list_item_check(context: &mut CompileContext) {
603 if !context.image_alt_inside {
604 context.push("<input type=\"checkbox\" ");
605 if !context.options.gfm_task_list_item_checkable {
606 context.push("disabled=\"\" ");
607 }
608 }
609}
610
611/// Handle [`Enter`][Kind::Enter]:[`HtmlFlow`][Name::HtmlFlow].
612fn on_enter_html_flow(context: &mut CompileContext) {
613 context.line_ending_if_needed();
614 if context.options.allow_dangerous_html {
615 context.encode_html = false;
616 }
617}
618
619/// Handle [`Enter`][Kind::Enter]:[`HtmlText`][Name::HtmlText].
620fn on_enter_html_text(context: &mut CompileContext) {
621 if context.options.allow_dangerous_html {
622 context.encode_html = false;
623 }
624}
625
626/// Handle [`Enter`][Kind::Enter]:[`Image`][Name::Image].
627fn on_enter_image(context: &mut CompileContext) {
628 context.media_stack.push(Media {
629 image: true,
630 label_id: None,
631 label: None,
632 reference_id: None,
633 destination: None,
634 title: None,
635 });
636 context.image_alt_inside = true; // Disallow tags.
637}
638
639/// Handle [`Enter`][Kind::Enter]:[`Link`][Name::Link].
640fn on_enter_link(context: &mut CompileContext) {
641 context.media_stack.push(Media {
642 image: false,
643 label_id: None,
644 label: None,
645 reference_id: None,
646 destination: None,
647 title: None,
648 });
649}
650
651/// Handle [`Enter`][Kind::Enter]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}.
652fn on_enter_list(context: &mut CompileContext) {
653 let loose = list_loose(context.events, context.index, true);
654 context.tight_stack.push(!loose);
655 context.line_ending_if_needed();
656
657 // Note: no `>`.
658 context.push(if context.events[context.index].name == Name::ListOrdered {
659 "<ol"
660 } else {
661 "<ul"
662 });
663 context.list_expect_first_marker = Some(true);
664}
665
666/// Handle [`Enter`][Kind::Enter]:[`ListItemMarker`][Name::ListItemMarker].
667fn on_enter_list_item_marker(context: &mut CompileContext) {
668 if context.list_expect_first_marker.take().unwrap() {
669 context.push(">");
670 }
671
672 context.line_ending_if_needed();
673
674 context.push("<li>");
675 context.list_expect_first_marker = Some(false);
676}
677
678/// Handle [`Enter`][Kind::Enter]:[`Paragraph`][Name::Paragraph].
679fn on_enter_paragraph(context: &mut CompileContext) {
680 let tight = context.tight_stack.last().unwrap_or(&false);
681
682 if !tight {
683 context.line_ending_if_needed();
684 context.push("<p>");
685 }
686}
687
688/// Handle [`Enter`][Kind::Enter]:[`Resource`][Name::Resource].
689fn on_enter_resource(context: &mut CompileContext) {
690 context.buffer(); // We can have line endings in the resource, ignore them.
691 context.media_stack.last_mut().unwrap().destination = Some(String::new());
692}
693
694/// Handle [`Enter`][Kind::Enter]:[`ResourceDestinationString`][Name::ResourceDestinationString].
695fn on_enter_resource_destination_string(context: &mut CompileContext) {
696 context.buffer();
697 // Ignore encoding the result, as we’ll first percent encode the url and
698 // encode manually after.
699 context.encode_html = false;
700}
701
702/// Handle [`Enter`][Kind::Enter]:[`Strong`][Name::Strong].
703fn on_enter_strong(context: &mut CompileContext) {
704 if !context.image_alt_inside {
705 context.push("<strong>");
706 }
707}
708
709/// Handle [`Exit`][Kind::Exit]:[`AutolinkEmail`][Name::AutolinkEmail].
710fn on_exit_autolink_email(context: &mut CompileContext) {
711 generate_autolink(
712 context,
713 Some("mailto:"),
714 Slice::from_position(
715 context.bytes,
716 &Position::from_exit_event(context.events, context.index),
717 )
718 .as_str(),
719 false,
720 );
721}
722
723/// Handle [`Exit`][Kind::Exit]:[`AutolinkProtocol`][Name::AutolinkProtocol].
724fn on_exit_autolink_protocol(context: &mut CompileContext) {
725 generate_autolink(
726 context,
727 None,
728 Slice::from_position(
729 context.bytes,
730 &Position::from_exit_event(context.events, context.index),
731 )
732 .as_str(),
733 false,
734 );
735}
736
737/// Handle [`Exit`][Kind::Exit]:{[`HardBreakEscape`][Name::HardBreakEscape],[`HardBreakTrailing`][Name::HardBreakTrailing]}.
738fn on_exit_break(context: &mut CompileContext) {
739 if !context.image_alt_inside {
740 context.push("<br />");
741 }
742}
743
744/// Handle [`Exit`][Kind::Exit]:[`BlankLineEnding`][Name::BlankLineEnding].
745fn on_exit_blank_line_ending(context: &mut CompileContext) {
746 context.slurp_one_line_ending = false;
747 if context.index == context.events.len() - 1 {
748 context.line_ending_if_needed();
749 }
750}
751
752/// Handle [`Exit`][Kind::Exit]:[`BlockQuote`][Name::BlockQuote].
753fn on_exit_block_quote(context: &mut CompileContext) {
754 context.tight_stack.pop();
755 context.line_ending_if_needed();
756 context.slurp_one_line_ending = false;
757 context.push("</blockquote>");
758}
759
760/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarker`][Name::CharacterReferenceMarker].
761fn on_exit_character_reference_marker(context: &mut CompileContext) {
762 context.character_reference_marker = Some(b'&');
763}
764
765/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerHexadecimal`][Name::CharacterReferenceMarkerHexadecimal].
766fn on_exit_character_reference_marker_hexadecimal(context: &mut CompileContext) {
767 context.character_reference_marker = Some(b'x');
768}
769
770/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerNumeric`][Name::CharacterReferenceMarkerNumeric].
771fn on_exit_character_reference_marker_numeric(context: &mut CompileContext) {
772 context.character_reference_marker = Some(b'#');
773}
774
775/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceValue`][Name::CharacterReferenceValue].
776fn on_exit_character_reference_value(context: &mut CompileContext) {
777 let marker = context
778 .character_reference_marker
779 .take()
780 .expect("expected `character_reference_kind` to be set");
781 let slice = Slice::from_position(
782 context.bytes,
783 &Position::from_exit_event(context.events, context.index),
784 );
785 let value = decode_character_reference(slice.as_str(), marker, true)
786 .expect("expected to parse only valid named references");
787
788 context.push(&encode(&value, context.encode_html));
789}
790
791/// Handle [`Exit`][Kind::Exit]:{[`CodeFlowChunk`][Name::CodeFlowChunk],[`MathFlowChunk`][Name::MathFlowChunk]}.
792fn on_exit_raw_flow_chunk(context: &mut CompileContext) {
793 context.raw_flow_seen_data = Some(true);
794 context.push(&encode(
795 &Slice::from_position(
796 context.bytes,
797 &Position::from_exit_event(context.events, context.index),
798 )
799 // Must serialize to get virtual spaces.
800 .serialize(),
801 context.encode_html,
802 ));
803}
804
805/// Handle [`Exit`][Kind::Exit]:{[`CodeFencedFence`][Name::CodeFencedFence],[`MathFlowFence`][Name::MathFlowFence]}.
806fn on_exit_raw_flow_fence(context: &mut CompileContext) {
807 let count = context
808 .raw_flow_fences_count
809 .expect("expected `raw_flow_fences_count`");
810
811 if count == 0 {
812 context.push(">");
813 context.slurp_one_line_ending = true;
814 }
815
816 context.raw_flow_fences_count = Some(count + 1);
817}
818
819/// Handle [`Exit`][Kind::Exit]:[`CodeFencedFenceInfo`][Name::CodeFencedFenceInfo].
820///
821/// Note: math (flow) does not support `info`.
822fn on_exit_raw_flow_fence_info(context: &mut CompileContext) {
823 let value = context.resume();
824 context.push(" class=\"language-");
825 context.push(&value);
826 context.push("\"");
827}
828
829/// Handle [`Exit`][Kind::Exit]:{[`CodeFenced`][Name::CodeFenced],[`CodeIndented`][Name::CodeIndented],[`MathFlow`][Name::MathFlow]}.
830fn on_exit_raw_flow(context: &mut CompileContext) {
831 // One special case is if we are inside a container, and the raw (flow) was
832 // not closed (meaning it runs to the end).
833 // In that case, the following line ending, is considered *outside* the
834 // fenced code and block quote by `markdown-rs`, but CM wants to treat that
835 // ending as part of the code.
836 if let Some(count) = context.raw_flow_fences_count {
837 // No closing fence.
838 if count == 1
839 // In a container.
840 && !context.tight_stack.is_empty()
841 // Empty (as the closing is right at the opening fence)
842 && !matches!(context.events[context.index - 1].name, Name::CodeFencedFence | Name::MathFlowFence)
843 {
844 context.line_ending();
845 }
846 }
847
848 // But in most cases, it’s simpler: when we’ve seen some data, emit an extra
849 // line ending when needed.
850 if context
851 .raw_flow_seen_data
852 .take()
853 .expect("`raw_flow_seen_data` must be defined")
854 {
855 context.line_ending_if_needed();
856 }
857
858 context.push("</code></pre>");
859
860 if let Some(count) = context.raw_flow_fences_count.take() {
861 if count < 2 {
862 context.line_ending_if_needed();
863 }
864 }
865
866 context.slurp_one_line_ending = false;
867}
868
869/// Handle [`Exit`][Kind::Exit]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}.
870fn on_exit_raw_text(context: &mut CompileContext) {
871 let result = context.resume();
872 // To do: share with `to_mdast`.
873 let mut bytes = result.as_bytes().to_vec();
874
875 // If we are in a GFM table, we need to decode escaped pipes.
876 // This is a rather weird GFM feature.
877 if context.gfm_table_align.is_some() {
878 let mut index = 0;
879 let mut len = bytes.len();
880
881 while index < len {
882 if index + 1 < len && bytes[index] == b'\\' && bytes[index + 1] == b'|' {
883 bytes.remove(index);
884 len -= 1;
885 }
886
887 index += 1;
888 }
889 }
890
891 let mut trim = false;
892 let mut index = 0;
893 let mut end = bytes.len();
894
895 if end > 2 && bytes[index] == b' ' && bytes[end - 1] == b' ' {
896 index += 1;
897 end -= 1;
898 while index < end && !trim {
899 if bytes[index] != b' ' {
900 trim = true;
901 break;
902 }
903 index += 1;
904 }
905 }
906
907 if trim {
908 bytes.remove(0);
909 bytes.pop();
910 }
911
912 context.raw_text_inside = false;
913 context.push(str::from_utf8(&bytes).unwrap());
914
915 if !context.image_alt_inside {
916 context.push("</code>");
917 }
918}
919
920/// Handle [`Exit`][Kind::Exit]:*.
921///
922/// Resumes, and ignores what was resumed.
923fn on_exit_drop(context: &mut CompileContext) {
924 context.resume();
925}
926
927/// Handle [`Exit`][Kind::Exit]:*.
928///
929/// Resumes, ignores what was resumed, and slurps the following line ending.
930fn on_exit_drop_slurp(context: &mut CompileContext) {
931 context.resume();
932 context.slurp_one_line_ending = true;
933}
934
935/// Handle [`Exit`][Kind::Exit]:{[`CodeTextData`][Name::CodeTextData],[`Data`][Name::Data],[`CharacterEscapeValue`][Name::CharacterEscapeValue]}.
936fn on_exit_data(context: &mut CompileContext) {
937 context.push(&encode(
938 Slice::from_position(
939 context.bytes,
940 &Position::from_exit_event(context.events, context.index),
941 )
942 .as_str(),
943 context.encode_html,
944 ));
945}
946
947/// Handle [`Exit`][Kind::Exit]:[`Definition`][Name::Definition].
948fn on_exit_definition(context: &mut CompileContext) {
949 context.resume();
950 let media = context.media_stack.pop().unwrap();
951 let indices = media.reference_id.unwrap();
952 let id =
953 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str());
954
955 context.definitions.push(Definition {
956 id,
957 destination: media.destination,
958 title: media.title,
959 });
960}
961
962/// Handle [`Exit`][Kind::Exit]:[`DefinitionDestinationString`][Name::DefinitionDestinationString].
963fn on_exit_definition_destination_string(context: &mut CompileContext) {
964 let buf = context.resume();
965 context.media_stack.last_mut().unwrap().destination = Some(buf);
966 context.encode_html = true;
967}
968
969/// Handle [`Exit`][Kind::Exit]:[`DefinitionLabelString`][Name::DefinitionLabelString].
970fn on_exit_definition_label_string(context: &mut CompileContext) {
971 // Discard label, use the source content instead.
972 context.resume();
973 context.media_stack.last_mut().unwrap().reference_id =
974 Some(Position::from_exit_event(context.events, context.index).to_indices());
975}
976
977/// Handle [`Exit`][Kind::Exit]:[`DefinitionTitleString`][Name::DefinitionTitleString].
978fn on_exit_definition_title_string(context: &mut CompileContext) {
979 let buf = context.resume();
980 context.media_stack.last_mut().unwrap().title = Some(buf);
981}
982
983/// Handle [`Exit`][Kind::Exit]:[`Emphasis`][Name::Emphasis].
984fn on_exit_emphasis(context: &mut CompileContext) {
985 if !context.image_alt_inside {
986 context.push("</em>");
987 }
988}
989
990/// Handle [`Exit`][Kind::Exit]:[`Frontmatter`][Name::Frontmatter].
991fn on_exit_frontmatter(context: &mut CompileContext) {
992 context.resume();
993 context.slurp_one_line_ending = true;
994}
995
996/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail].
997fn on_exit_gfm_autolink_literal_email(context: &mut CompileContext) {
998 generate_autolink(
999 context,
1000 Some("mailto:"),
1001 Slice::from_position(
1002 context.bytes,
1003 &Position::from_exit_event(context.events, context.index),
1004 )
1005 .as_str(),
1006 true,
1007 );
1008}
1009
1010/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralMailto`][Name::GfmAutolinkLiteralMailto].
1011fn on_exit_gfm_autolink_literal_mailto(context: &mut CompileContext) {
1012 generate_autolink(
1013 context,
1014 None,
1015 Slice::from_position(
1016 context.bytes,
1017 &Position::from_exit_event(context.events, context.index),
1018 )
1019 .as_str(),
1020 true,
1021 );
1022}
1023
1024/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralProtocol`][Name::GfmAutolinkLiteralProtocol].
1025fn on_exit_gfm_autolink_literal_protocol(context: &mut CompileContext) {
1026 generate_autolink(
1027 context,
1028 None,
1029 Slice::from_position(
1030 context.bytes,
1031 &Position::from_exit_event(context.events, context.index),
1032 )
1033 .as_str(),
1034 true,
1035 );
1036}
1037
1038/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralWww`][Name::GfmAutolinkLiteralWww].
1039fn on_exit_gfm_autolink_literal_www(context: &mut CompileContext) {
1040 generate_autolink(
1041 context,
1042 Some("http://"),
1043 Slice::from_position(
1044 context.bytes,
1045 &Position::from_exit_event(context.events, context.index),
1046 )
1047 .as_str(),
1048 true,
1049 );
1050}
1051
1052/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralXmpp`][Name::GfmAutolinkLiteralXmpp].
1053fn on_exit_gfm_autolink_literal_xmpp(context: &mut CompileContext) {
1054 generate_autolink(
1055 context,
1056 None,
1057 Slice::from_position(
1058 context.bytes,
1059 &Position::from_exit_event(context.events, context.index),
1060 )
1061 .as_str(),
1062 true,
1063 );
1064}
1065
1066/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteCall`][Name::GfmFootnoteCall].
1067fn on_exit_gfm_footnote_call(context: &mut CompileContext) {
1068 let indices = context.media_stack.pop().unwrap().label_id.unwrap();
1069 let id =
1070 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str());
1071 let safe_id = sanitize(&id.to_lowercase());
1072 let mut call_index = 0;
1073
1074 // See if this has been called before.
1075 while call_index < context.gfm_footnote_definition_calls.len() {
1076 if context.gfm_footnote_definition_calls[call_index].0 == id {
1077 break;
1078 }
1079 call_index += 1;
1080 }
1081
1082 // New.
1083 if call_index == context.gfm_footnote_definition_calls.len() {
1084 context.gfm_footnote_definition_calls.push((id, 0));
1085 }
1086
1087 // Increment.
1088 context.gfm_footnote_definition_calls[call_index].1 += 1;
1089
1090 // No call is output in an image alt, though the definition and
1091 // backreferences are generated as if it was the case.
1092 if context.image_alt_inside {
1093 return;
1094 }
1095
1096 context.push("<sup><a href=\"#");
1097 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1098 context.push(&encode(value, context.encode_html));
1099 } else {
1100 context.push("user-content-");
1101 }
1102 context.push("fn-");
1103 context.push(&safe_id);
1104 context.push("\" id=\"");
1105 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1106 context.push(&encode(value, context.encode_html));
1107 } else {
1108 context.push("user-content-");
1109 }
1110 context.push("fnref-");
1111 context.push(&safe_id);
1112 if context.gfm_footnote_definition_calls[call_index].1 > 1 {
1113 context.push("-");
1114 context.push(
1115 &context.gfm_footnote_definition_calls[call_index]
1116 .1
1117 .to_string(),
1118 );
1119 }
1120 context.push("\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">");
1121
1122 context.push(&(call_index + 1).to_string());
1123 context.push("</a></sup>");
1124}
1125
1126/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinitionLabelString`][Name::GfmFootnoteDefinitionLabelString].
1127fn on_exit_gfm_footnote_definition_label_string(context: &mut CompileContext) {
1128 context
1129 .gfm_footnote_definition_stack
1130 .push(Position::from_exit_event(context.events, context.index).to_indices());
1131}
1132
1133/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinitionPrefix`][Name::GfmFootnoteDefinitionPrefix].
1134fn on_exit_gfm_footnote_definition_prefix(context: &mut CompileContext) {
1135 // Drop the prefix.
1136 context.resume();
1137 // Capture everything until end of definition.
1138 context.buffer();
1139}
1140
1141/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition].
1142fn on_exit_gfm_footnote_definition(context: &mut CompileContext) {
1143 let value = context.resume();
1144 let indices = context.gfm_footnote_definition_stack.pop().unwrap();
1145 context.tight_stack.pop();
1146 context.gfm_footnote_definitions.push((
1147 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str()),
1148 value,
1149 ));
1150}
1151
1152/// Handle [`Exit`][Kind::Exit]:[`GfmStrikethrough`][Name::GfmStrikethrough].
1153fn on_exit_gfm_strikethrough(context: &mut CompileContext) {
1154 if !context.image_alt_inside {
1155 context.push("</del>");
1156 }
1157}
1158
1159/// Handle [`Exit`][Kind::Exit]:[`GfmTable`][Name::GfmTable].
1160fn on_exit_gfm_table(context: &mut CompileContext) {
1161 context.gfm_table_align = None;
1162 context.line_ending_if_needed();
1163 context.push("</table>");
1164}
1165
1166/// Handle [`Exit`][Kind::Exit]:[`GfmTableBody`][Name::GfmTableBody].
1167fn on_exit_gfm_table_body(context: &mut CompileContext) {
1168 context.line_ending_if_needed();
1169 context.push("</tbody>");
1170}
1171
1172/// Handle [`Exit`][Kind::Exit]:[`GfmTableCell`][Name::GfmTableCell].
1173fn on_exit_gfm_table_cell(context: &mut CompileContext) {
1174 let align = context.gfm_table_align.as_ref().unwrap();
1175
1176 if context.gfm_table_column < align.len() {
1177 if context.gfm_table_in_head {
1178 context.push("</th>");
1179 } else {
1180 context.push("</td>");
1181 }
1182 } else {
1183 // Stop capturing.
1184 context.resume();
1185 }
1186
1187 context.gfm_table_column += 1;
1188}
1189
1190/// Handle [`Exit`][Kind::Exit]:[`GfmTableHead`][Name::GfmTableHead].
1191fn on_exit_gfm_table_head(context: &mut CompileContext) {
1192 context.gfm_table_in_head = false;
1193 context.line_ending_if_needed();
1194 context.push("</thead>");
1195}
1196
1197/// Handle [`Exit`][Kind::Exit]:[`GfmTableRow`][Name::GfmTableRow].
1198fn on_exit_gfm_table_row(context: &mut CompileContext) {
1199 let mut column = context.gfm_table_column;
1200 let len = context.gfm_table_align.as_ref().unwrap().len();
1201
1202 // Add “phantom” cells, for body rows that are shorter than the delimiter
1203 // row (which is equal to the head row).
1204 while column < len {
1205 on_enter_gfm_table_cell(context);
1206 on_exit_gfm_table_cell(context);
1207 column += 1;
1208 }
1209
1210 context.gfm_table_column = 0;
1211 context.line_ending_if_needed();
1212 context.push("</tr>");
1213}
1214
1215/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck].
1216fn on_exit_gfm_task_list_item_check(context: &mut CompileContext) {
1217 if !context.image_alt_inside {
1218 context.push("/>");
1219 }
1220}
1221
1222/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemValueChecked`][Name::GfmTaskListItemValueChecked].
1223fn on_exit_gfm_task_list_item_value_checked(context: &mut CompileContext) {
1224 if !context.image_alt_inside {
1225 context.push("checked=\"\" ");
1226 }
1227}
1228
1229/// Handle [`Exit`][Kind::Exit]:[`HeadingAtx`][Name::HeadingAtx].
1230fn on_exit_heading_atx(context: &mut CompileContext) {
1231 let rank = context
1232 .heading_atx_rank
1233 .take()
1234 .expect("`heading_atx_rank` must be set in headings");
1235
1236 context.push("</h");
1237 context.push(&rank.to_string());
1238 context.push(">");
1239}
1240
1241/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxSequence`][Name::HeadingAtxSequence].
1242fn on_exit_heading_atx_sequence(context: &mut CompileContext) {
1243 // First fence we see.
1244 if context.heading_atx_rank.is_none() {
1245 let rank = Slice::from_position(
1246 context.bytes,
1247 &Position::from_exit_event(context.events, context.index),
1248 )
1249 .len();
1250 context.line_ending_if_needed();
1251 context.heading_atx_rank = Some(rank);
1252 context.push("<h");
1253 context.push(&rank.to_string());
1254 context.push(">");
1255 }
1256}
1257
1258/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxText`][Name::HeadingAtxText].
1259fn on_exit_heading_atx_text(context: &mut CompileContext) {
1260 let value = context.resume();
1261 context.push(&value);
1262}
1263
1264/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextText`][Name::HeadingSetextText].
1265fn on_exit_heading_setext_text(context: &mut CompileContext) {
1266 let buf = context.resume();
1267 context.heading_setext_buffer = Some(buf);
1268 context.slurp_one_line_ending = true;
1269}
1270
1271/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextUnderlineSequence`][Name::HeadingSetextUnderlineSequence].
1272fn on_exit_heading_setext_underline_sequence(context: &mut CompileContext) {
1273 let text = context
1274 .heading_setext_buffer
1275 .take()
1276 .expect("`heading_atx_rank` must be set in headings");
1277 let position = Position::from_exit_event(context.events, context.index);
1278 let head = context.bytes[position.start.index];
1279 let rank = if head == b'-' { "2" } else { "1" };
1280
1281 context.line_ending_if_needed();
1282 context.push("<h");
1283 context.push(rank);
1284 context.push(">");
1285 context.push(&text);
1286 context.push("</h");
1287 context.push(rank);
1288 context.push(">");
1289}
1290
1291/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlow`][Name::HtmlFlow],[`HtmlText`][Name::HtmlText]}.
1292fn on_exit_html(context: &mut CompileContext) {
1293 context.encode_html = true;
1294}
1295
1296/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlowData`][Name::HtmlFlowData],[`HtmlTextData`][Name::HtmlTextData]}.
1297fn on_exit_html_data(context: &mut CompileContext) {
1298 let slice = Slice::from_position(
1299 context.bytes,
1300 &Position::from_exit_event(context.events, context.index),
1301 );
1302 let value = slice.as_str();
1303
1304 let encoded = if context.options.gfm_tagfilter && context.options.allow_dangerous_html {
1305 encode(&gfm_tagfilter(value), context.encode_html)
1306 } else {
1307 encode(value, context.encode_html)
1308 };
1309
1310 context.push(&encoded);
1311}
1312
1313/// Handle [`Exit`][Kind::Exit]:[`Label`][Name::Label].
1314fn on_exit_label(context: &mut CompileContext) {
1315 let buf = context.resume();
1316 context.media_stack.last_mut().unwrap().label = Some(buf);
1317}
1318
1319/// Handle [`Exit`][Kind::Exit]:[`LabelText`][Name::LabelText].
1320fn on_exit_label_text(context: &mut CompileContext) {
1321 context.media_stack.last_mut().unwrap().label_id =
1322 Some(Position::from_exit_event(context.events, context.index).to_indices());
1323}
1324
1325/// Handle [`Exit`][Kind::Exit]:[`LineEnding`][Name::LineEnding].
1326fn on_exit_line_ending(context: &mut CompileContext) {
1327 if context.raw_text_inside {
1328 context.push(" ");
1329 } else if context.slurp_one_line_ending
1330 // Ignore line endings after definitions.
1331 || (context.index > 1
1332 && (context.events[context.index - 2].name == Name::Definition
1333 || context.events[context.index - 2].name == Name::GfmFootnoteDefinition))
1334 {
1335 context.slurp_one_line_ending = false;
1336 } else {
1337 context.push(&encode(
1338 Slice::from_position(
1339 context.bytes,
1340 &Position::from_exit_event(context.events, context.index),
1341 )
1342 .as_str(),
1343 context.encode_html,
1344 ));
1345 }
1346}
1347
1348/// Handle [`Exit`][Kind::Exit]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}.
1349fn on_exit_list(context: &mut CompileContext) {
1350 context.tight_stack.pop();
1351 context.line_ending();
1352 context.push(if context.events[context.index].name == Name::ListOrdered {
1353 "</ol>"
1354 } else {
1355 "</ul>"
1356 });
1357}
1358
1359/// Handle [`Exit`][Kind::Exit]:[`ListItem`][Name::ListItem].
1360fn on_exit_list_item(context: &mut CompileContext) {
1361 let tight = context.tight_stack.last().unwrap_or(&false);
1362 let before_item = skip::opt_back(
1363 context.events,
1364 context.index - 1,
1365 &[
1366 Name::BlankLineEnding,
1367 Name::BlockQuotePrefix,
1368 Name::LineEnding,
1369 Name::SpaceOrTab,
1370 // Also ignore things that don’t contribute to the document.
1371 Name::Definition,
1372 Name::GfmFootnoteDefinition,
1373 ],
1374 );
1375 let previous = &context.events[before_item];
1376 let tight_paragraph = *tight && previous.name == Name::Paragraph;
1377 let empty_item = previous.name == Name::ListItemPrefix;
1378
1379 context.slurp_one_line_ending = false;
1380
1381 if !tight_paragraph && !empty_item {
1382 context.line_ending_if_needed();
1383 }
1384
1385 context.push("</li>");
1386}
1387
1388/// Handle [`Exit`][Kind::Exit]:[`ListItemValue`][Name::ListItemValue].
1389fn on_exit_list_item_value(context: &mut CompileContext) {
1390 if context.list_expect_first_marker.unwrap() {
1391 let slice = Slice::from_position(
1392 context.bytes,
1393 &Position::from_exit_event(context.events, context.index),
1394 );
1395 let value = slice.as_str().parse::<u32>().ok().unwrap();
1396
1397 if value != 1 {
1398 context.push(" start=\"");
1399 context.push(&value.to_string());
1400 context.push("\"");
1401 }
1402 }
1403}
1404
1405/// Handle [`Exit`][Kind::Exit]:{[`Image`][Name::Image],[`Link`][Name::Link]}.
1406fn on_exit_media(context: &mut CompileContext) {
1407 let mut is_in_image = false;
1408 let mut index = 0;
1409
1410 // Skip current.
1411 let end = context.media_stack.len() - 1;
1412 while index < end {
1413 if context.media_stack[index].image {
1414 is_in_image = true;
1415 break;
1416 }
1417 index += 1;
1418 }
1419
1420 context.image_alt_inside = is_in_image;
1421
1422 let media = context.media_stack.pop().unwrap();
1423 let label = media.label.unwrap();
1424 let id = media.reference_id.or(media.label_id).map(|indices| {
1425 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str())
1426 });
1427
1428 let definition_index = if media.destination.is_none() {
1429 id.map(|id| {
1430 let mut index = 0;
1431
1432 while index < context.definitions.len() && context.definitions[index].id != id {
1433 index += 1;
1434 }
1435
1436 debug_assert!(
1437 index < context.definitions.len(),
1438 "expected defined definition"
1439 );
1440 index
1441 })
1442 } else {
1443 None
1444 };
1445
1446 if !is_in_image {
1447 if media.image {
1448 context.push("<img src=\"");
1449 } else {
1450 context.push("<a href=\"");
1451 }
1452
1453 let destination = if let Some(index) = definition_index {
1454 context.definitions[index].destination.as_ref()
1455 } else {
1456 media.destination.as_ref()
1457 };
1458
1459 if let Some(destination) = destination {
1460 let allow_dangerous_protocol = context.options.allow_dangerous_protocol
1461 || (context.options.allow_any_img_src && media.image);
1462
1463 let url = if allow_dangerous_protocol {
1464 sanitize(destination)
1465 } else {
1466 sanitize_with_protocols(
1467 destination,
1468 if media.image {
1469 &SAFE_PROTOCOL_SRC
1470 } else {
1471 &SAFE_PROTOCOL_HREF
1472 },
1473 )
1474 };
1475 context.push(&url);
1476 }
1477
1478 if media.image {
1479 context.push("\" alt=\"");
1480 }
1481 }
1482
1483 if media.image {
1484 context.push(&label);
1485 }
1486
1487 if !is_in_image {
1488 context.push("\"");
1489
1490 let title = if let Some(index) = definition_index {
1491 context.definitions[index].title.clone()
1492 } else {
1493 media.title
1494 };
1495
1496 if let Some(title) = title {
1497 context.push(" title=\"");
1498 context.push(&title);
1499 context.push("\"");
1500 }
1501
1502 if media.image {
1503 context.push(" /");
1504 }
1505
1506 context.push(">");
1507 }
1508
1509 if !media.image {
1510 context.push(&label);
1511
1512 if !is_in_image {
1513 context.push("</a>");
1514 }
1515 }
1516}
1517
1518/// Handle [`Exit`][Kind::Exit]:[`Paragraph`][Name::Paragraph].
1519fn on_exit_paragraph(context: &mut CompileContext) {
1520 let tight = context.tight_stack.last().unwrap_or(&false);
1521
1522 if *tight {
1523 context.slurp_one_line_ending = true;
1524 } else {
1525 context.push("</p>");
1526 }
1527}
1528
1529/// Handle [`Exit`][Kind::Exit]:[`ReferenceString`][Name::ReferenceString].
1530fn on_exit_reference_string(context: &mut CompileContext) {
1531 // Drop stuff.
1532 context.resume();
1533
1534 context.media_stack.last_mut().unwrap().reference_id =
1535 Some(Position::from_exit_event(context.events, context.index).to_indices());
1536}
1537
1538/// Handle [`Exit`][Kind::Exit]:[`ResourceDestinationString`][Name::ResourceDestinationString].
1539fn on_exit_resource_destination_string(context: &mut CompileContext) {
1540 let buf = context.resume();
1541 context.media_stack.last_mut().unwrap().destination = Some(buf);
1542 context.encode_html = true;
1543}
1544
1545/// Handle [`Exit`][Kind::Exit]:[`ResourceTitleString`][Name::ResourceTitleString].
1546fn on_exit_resource_title_string(context: &mut CompileContext) {
1547 let buf = context.resume();
1548 context.media_stack.last_mut().unwrap().title = Some(buf);
1549}
1550
1551/// Handle [`Exit`][Kind::Exit]:[`Strong`][Name::Strong].
1552fn on_exit_strong(context: &mut CompileContext) {
1553 if !context.image_alt_inside {
1554 context.push("</strong>");
1555 }
1556}
1557
1558/// Handle [`Exit`][Kind::Exit]:[`ThematicBreak`][Name::ThematicBreak].
1559fn on_exit_thematic_break(context: &mut CompileContext) {
1560 context.line_ending_if_needed();
1561 context.push("<hr />");
1562}
1563
1564/// Generate a footnote section.
1565fn generate_footnote_section(context: &mut CompileContext) {
1566 context.line_ending_if_needed();
1567 context.push("<section data-footnotes=\"\" class=\"footnotes\"><");
1568 if let Some(ref value) = context.options.gfm_footnote_label_tag_name {
1569 context.push(&encode(value, context.encode_html));
1570 } else {
1571 context.push("h2");
1572 }
1573 context.push(" id=\"footnote-label\" ");
1574 if let Some(ref value) = context.options.gfm_footnote_label_attributes {
1575 context.push(value);
1576 } else {
1577 context.push("class=\"sr-only\"");
1578 }
1579 context.push(">");
1580 if let Some(ref value) = context.options.gfm_footnote_label {
1581 context.push(&encode(value, context.encode_html));
1582 } else {
1583 context.push("Footnotes");
1584 }
1585 context.push("</");
1586 if let Some(ref value) = context.options.gfm_footnote_label_tag_name {
1587 context.push(&encode(value, context.encode_html));
1588 } else {
1589 context.push("h2");
1590 }
1591 context.push(">");
1592 context.line_ending();
1593 context.push("<ol>");
1594
1595 let mut index = 0;
1596 while index < context.gfm_footnote_definition_calls.len() {
1597 generate_footnote_item(context, index);
1598 index += 1;
1599 }
1600
1601 context.line_ending();
1602 context.push("</ol>");
1603 context.line_ending();
1604 context.push("</section>");
1605 context.line_ending();
1606}
1607
1608/// Generate a footnote item from a call.
1609fn generate_footnote_item(context: &mut CompileContext, index: usize) {
1610 let id = &context.gfm_footnote_definition_calls[index].0;
1611 let safe_id = sanitize(&id.to_lowercase());
1612
1613 // Find definition: we’ll always find it.
1614 let mut definition_index = 0;
1615 while definition_index < context.gfm_footnote_definitions.len() {
1616 if &context.gfm_footnote_definitions[definition_index].0 == id {
1617 break;
1618 }
1619 definition_index += 1;
1620 }
1621
1622 debug_assert_ne!(
1623 definition_index,
1624 context.gfm_footnote_definitions.len(),
1625 "expected definition"
1626 );
1627
1628 context.line_ending();
1629 context.push("<li id=\"");
1630 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1631 context.push(&encode(value, context.encode_html));
1632 } else {
1633 context.push("user-content-");
1634 }
1635 context.push("fn-");
1636 context.push(&safe_id);
1637 context.push("\">");
1638 context.line_ending();
1639
1640 // Create one or more backreferences.
1641 let mut reference_index = 0;
1642 let mut backreferences = String::new();
1643 while reference_index < context.gfm_footnote_definition_calls[index].1 {
1644 if reference_index != 0 {
1645 backreferences.push(' ');
1646 }
1647 backreferences.push_str("<a href=\"#");
1648 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1649 backreferences.push_str(&encode(value, context.encode_html));
1650 } else {
1651 backreferences.push_str("user-content-");
1652 }
1653 backreferences.push_str("fnref-");
1654 backreferences.push_str(&safe_id);
1655 if reference_index != 0 {
1656 backreferences.push('-');
1657 backreferences.push_str(&(reference_index + 1).to_string());
1658 }
1659 backreferences.push_str("\" data-footnote-backref=\"\" aria-label=\"");
1660 if let Some(ref value) = context.options.gfm_footnote_back_label {
1661 backreferences.push_str(&encode(value, context.encode_html));
1662 } else {
1663 backreferences.push_str("Back to content");
1664 }
1665 backreferences.push_str("\" class=\"data-footnote-backref\">↩");
1666 if reference_index != 0 {
1667 backreferences.push_str("<sup>");
1668 backreferences.push_str(&(reference_index + 1).to_string());
1669 backreferences.push_str("</sup>");
1670 }
1671 backreferences.push_str("</a>");
1672
1673 reference_index += 1;
1674 }
1675
1676 let value = context.gfm_footnote_definitions[definition_index].1.clone();
1677 let bytes = value.as_bytes();
1678 let mut byte_index = bytes.len();
1679 // Move back past EOL.
1680 while byte_index > 0 && matches!(bytes[byte_index - 1], b'\n' | b'\r') {
1681 byte_index -= 1;
1682 }
1683 // Check if it ends in `</p>`.
1684 // This is a bit funky if someone wrote a safe paragraph by hand in
1685 // there.
1686 // But in all other cases, `<` and `>` would be encoded, so we can be
1687 // sure that this is generated by our compiler.
1688 if byte_index > 3
1689 && bytes[byte_index - 4] == b'<'
1690 && bytes[byte_index - 3] == b'/'
1691 && bytes[byte_index - 2] == b'p'
1692 && bytes[byte_index - 1] == b'>'
1693 {
1694 let (before, after) = bytes.split_at(byte_index - 4);
1695 let mut result = String::new();
1696 result.push_str(str::from_utf8(before).unwrap());
1697 result.push(' ');
1698 result.push_str(&backreferences);
1699 result.push_str(str::from_utf8(after).unwrap());
1700 context.push(&result);
1701 } else {
1702 context.push(&value);
1703 context.line_ending_if_needed();
1704 context.push(&backreferences);
1705 }
1706 context.line_ending_if_needed();
1707 context.push("</li>");
1708}
1709
1710/// Generate an autolink (used by unicode autolinks and GFM autolink literals).
1711fn generate_autolink(
1712 context: &mut CompileContext,
1713 protocol: Option<&str>,
1714 value: &str,
1715 is_gfm_literal: bool,
1716) {
1717 let mut is_in_link = false;
1718 let mut index = 0;
1719
1720 while index < context.media_stack.len() {
1721 if !context.media_stack[index].image {
1722 is_in_link = true;
1723 break;
1724 }
1725 index += 1;
1726 }
1727
1728 if !context.image_alt_inside && (!is_in_link || !is_gfm_literal) {
1729 context.push("<a href=\"");
1730 let url = if let Some(protocol) = protocol {
1731 format!("{}{}", protocol, value)
1732 } else {
1733 value.into()
1734 };
1735
1736 let url = if context.options.allow_dangerous_protocol {
1737 sanitize(&url)
1738 } else {
1739 sanitize_with_protocols(&url, &SAFE_PROTOCOL_HREF)
1740 };
1741
1742 context.push(&url);
1743 context.push("\">");
1744 }
1745
1746 context.push(&encode(value, context.encode_html));
1747
1748 if !context.image_alt_inside && (!is_in_link || !is_gfm_literal) {
1749 context.push("</a>");
1750 }
1751}