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::ObsidianBlockQuoteCallout => on_enter_obs_callout(context),
334 Name::ObsidianText => on_enter_obs_text(context),
335 Name::CodeIndented => on_enter_code_indented(context),
336 Name::CodeFenced | Name::MathFlow => on_enter_raw_flow(context),
337 Name::CodeText | Name::MathText => on_enter_raw_text(context),
338 Name::Definition => on_enter_definition(context),
339 Name::DefinitionDestinationString => on_enter_definition_destination_string(context),
340 Name::Emphasis => on_enter_emphasis(context),
341 Name::Frontmatter => on_enter_frontmatter(context),
342 Name::GfmFootnoteDefinition => on_enter_gfm_footnote_definition(context),
343 Name::GfmFootnoteCall => on_enter_gfm_footnote_call(context),
344 Name::GfmStrikethrough => on_enter_gfm_strikethrough(context),
345 Name::GfmTable => on_enter_gfm_table(context),
346 Name::GfmTableBody => on_enter_gfm_table_body(context),
347 Name::GfmTableCell => on_enter_gfm_table_cell(context),
348 Name::GfmTableHead => on_enter_gfm_table_head(context),
349 Name::GfmTableRow => on_enter_gfm_table_row(context),
350 Name::GfmTaskListItemCheck => on_enter_gfm_task_list_item_check(context),
351 Name::HtmlFlow => on_enter_html_flow(context),
352 Name::HtmlText => on_enter_html_text(context),
353 Name::Image => on_enter_image(context),
354 Name::Link => on_enter_link(context),
355 Name::WikilinkStart => on_enter_wikilink(context),
356 Name::ListItemMarker => on_enter_list_item_marker(context),
357 Name::ListOrdered | Name::ListUnordered => on_enter_list(context),
358 Name::Paragraph => on_enter_paragraph(context),
359 Name::Resource => on_enter_resource(context),
360 Name::ResourceDestinationString => on_enter_resource_destination_string(context),
361 Name::Strong => on_enter_strong(context),
362 _ => {}
363 }
364}
365
366/// Handle [`Exit`][Kind::Exit].
367fn exit(context: &mut CompileContext) {
368 match context.events[context.index].name {
369 Name::CodeFencedFenceMeta
370 | Name::MathFlowFenceMeta
371 | Name::MdxJsxTextTag
372 | Name::MdxTextExpression
373 | Name::Resource => {
374 on_exit_drop(context);
375 }
376 Name::MdxEsm | Name::MdxFlowExpression | Name::MdxJsxFlowTag => on_exit_drop_slurp(context),
377 Name::CharacterEscapeValue | Name::CodeTextData | Name::Data | Name::MathTextData => {
378 on_exit_data(context);
379 }
380 Name::AutolinkEmail => on_exit_autolink_email(context),
381 Name::AutolinkProtocol => on_exit_autolink_protocol(context),
382 Name::BlankLineEnding => on_exit_blank_line_ending(context),
383 Name::ObsidianBlockQuoteCallout => on_exit_obs_callout(context),
384 Name::ObsidianText => on_exit_obs_text(context),
385 Name::BlockQuote => on_exit_block_quote(context),
386 Name::CharacterReferenceMarker => on_exit_character_reference_marker(context),
387 Name::CharacterReferenceMarkerNumeric => {
388 on_exit_character_reference_marker_numeric(context);
389 }
390 Name::CharacterReferenceMarkerHexadecimal => {
391 on_exit_character_reference_marker_hexadecimal(context);
392 }
393 Name::CharacterReferenceValue => on_exit_character_reference_value(context),
394 Name::CodeFenced | Name::CodeIndented | Name::MathFlow => on_exit_raw_flow(context),
395 Name::CodeFencedFence | Name::MathFlowFence => on_exit_raw_flow_fence(context),
396 Name::CodeFencedFenceInfo => on_exit_raw_flow_fence_info(context),
397 Name::CodeFlowChunk | Name::MathFlowChunk => on_exit_raw_flow_chunk(context),
398 Name::CodeText | Name::MathText => on_exit_raw_text(context),
399 Name::Definition => on_exit_definition(context),
400 Name::DefinitionDestinationString => on_exit_definition_destination_string(context),
401 Name::DefinitionLabelString => on_exit_definition_label_string(context),
402 Name::DefinitionTitleString => on_exit_definition_title_string(context),
403 Name::Emphasis => on_exit_emphasis(context),
404 Name::Frontmatter => on_exit_frontmatter(context),
405 Name::GfmAutolinkLiteralEmail => on_exit_gfm_autolink_literal_email(context),
406 Name::GfmAutolinkLiteralMailto => on_exit_gfm_autolink_literal_mailto(context),
407 Name::GfmAutolinkLiteralProtocol => on_exit_gfm_autolink_literal_protocol(context),
408 Name::GfmAutolinkLiteralWww => on_exit_gfm_autolink_literal_www(context),
409 Name::GfmAutolinkLiteralXmpp => on_exit_gfm_autolink_literal_xmpp(context),
410 Name::GfmFootnoteCall => on_exit_gfm_footnote_call(context),
411 Name::GfmFootnoteDefinitionLabelString => {
412 on_exit_gfm_footnote_definition_label_string(context);
413 }
414 Name::GfmFootnoteDefinitionPrefix => on_exit_gfm_footnote_definition_prefix(context),
415 Name::GfmFootnoteDefinition => on_exit_gfm_footnote_definition(context),
416 Name::GfmStrikethrough => on_exit_gfm_strikethrough(context),
417 Name::GfmTable => on_exit_gfm_table(context),
418 Name::GfmTableBody => on_exit_gfm_table_body(context),
419 Name::GfmTableCell => on_exit_gfm_table_cell(context),
420 Name::GfmTableHead => on_exit_gfm_table_head(context),
421 Name::GfmTableRow => on_exit_gfm_table_row(context),
422 Name::GfmTaskListItemCheck => on_exit_gfm_task_list_item_check(context),
423 Name::GfmTaskListItemValueChecked => on_exit_gfm_task_list_item_value_checked(context),
424 Name::HardBreakEscape | Name::HardBreakTrailing => on_exit_break(context),
425 Name::HeadingAtx => on_exit_heading_atx(context),
426 Name::HeadingAtxSequence => on_exit_heading_atx_sequence(context),
427 Name::HeadingAtxText => on_exit_heading_atx_text(context),
428 Name::HeadingSetextText => on_exit_heading_setext_text(context),
429 Name::HeadingSetextUnderlineSequence => on_exit_heading_setext_underline_sequence(context),
430 Name::HtmlFlow | Name::HtmlText => on_exit_html(context),
431 Name::HtmlFlowData | Name::HtmlTextData => on_exit_html_data(context),
432 Name::Image | Name::Link => on_exit_media(context),
433 Name::WikilinkEnd => on_exit_wikilink(context),
434 Name::Label => on_exit_label(context),
435 Name::LabelText => on_exit_label_text(context),
436 Name::LineEnding => on_exit_line_ending(context),
437 Name::ListOrdered | Name::ListUnordered => on_exit_list(context),
438 Name::ListItem => on_exit_list_item(context),
439 Name::ListItemValue => on_exit_list_item_value(context),
440 Name::Paragraph => on_exit_paragraph(context),
441 Name::ReferenceString => on_exit_reference_string(context),
442 Name::ResourceDestinationString => on_exit_resource_destination_string(context),
443 Name::ResourceTitleString => on_exit_resource_title_string(context),
444 Name::Strong => on_exit_strong(context),
445 Name::ThematicBreak => on_exit_thematic_break(context),
446 _ => {}
447 }
448}
449
450/// Handle [`Enter`][Kind::Enter]:`*`.
451///
452/// Buffers data.
453fn on_enter_buffer(context: &mut CompileContext) {
454 context.buffer();
455}
456
457/// Handle [`Enter`][Kind::Enter]:[`BlockQuote`][Name::BlockQuote].
458fn on_enter_block_quote(context: &mut CompileContext) {
459 context.tight_stack.push(false);
460 context.line_ending_if_needed();
461 context.push("<blockquote>");
462}
463
464fn on_enter_obs_callout(context: &mut CompileContext) {
465 context.tight_stack.push(true);
466 context.line_ending_if_needed();
467 context.push("<h1>");
468}
469
470fn on_exit_obs_callout(context: &mut CompileContext) {
471 context.tight_stack.pop();
472 context.push("</h1>");
473 context.line_ending_if_needed();
474}
475
476fn on_enter_obs_text(_context: &mut CompileContext) {}
477
478fn on_exit_obs_text(context: &mut CompileContext) {
479 let text = Slice::from_position(
480 context.bytes,
481 &Position::from_exit_event(context.events, context.index),
482 );
483 context.push(text.as_str().trim());
484}
485
486/// Handle [`Enter`][Kind::Enter]:[`CodeIndented`][Name::CodeIndented].
487fn on_enter_code_indented(context: &mut CompileContext) {
488 context.raw_flow_seen_data = Some(false);
489 context.line_ending_if_needed();
490 context.push("<pre><code>");
491}
492
493/// Handle [`Enter`][Kind::Enter]:{[`CodeFenced`][Name::CodeFenced],[`MathFlow`][Name::MathFlow]}.
494fn on_enter_raw_flow(context: &mut CompileContext) {
495 context.raw_flow_seen_data = Some(false);
496 context.line_ending_if_needed();
497 // Note that no `>` is used, which is added later (due to info)
498 context.push("<pre><code");
499 context.raw_flow_fences_count = Some(0);
500
501 if context.events[context.index].name == Name::MathFlow {
502 context.push(" class=\"language-math math-display\"");
503 }
504}
505
506/// Handle [`Enter`][Kind::Enter]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}.
507fn on_enter_raw_text(context: &mut CompileContext) {
508 context.raw_text_inside = true;
509 if !context.image_alt_inside {
510 context.push("<code");
511 if context.events[context.index].name == Name::MathText {
512 context.push(" class=\"language-math math-inline\"");
513 }
514 context.push(">");
515 }
516 context.buffer();
517}
518
519/// Handle [`Enter`][Kind::Enter]:[`Definition`][Name::Definition].
520fn on_enter_definition(context: &mut CompileContext) {
521 context.buffer();
522 context.media_stack.push(Media {
523 image: false,
524 label: None,
525 label_id: None,
526 reference_id: None,
527 destination: None,
528 title: None,
529 });
530}
531
532/// Handle [`Enter`][Kind::Enter]:[`DefinitionDestinationString`][Name::DefinitionDestinationString].
533fn on_enter_definition_destination_string(context: &mut CompileContext) {
534 context.buffer();
535 context.encode_html = false;
536}
537
538/// Handle [`Enter`][Kind::Enter]:[`Emphasis`][Name::Emphasis].
539fn on_enter_emphasis(context: &mut CompileContext) {
540 if !context.image_alt_inside {
541 context.push("<em>");
542 }
543}
544
545/// Handle [`Enter`][Kind::Enter]:[`Frontmatter`][Name::Frontmatter].
546fn on_enter_frontmatter(context: &mut CompileContext) {
547 context.buffer();
548}
549
550/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition].
551fn on_enter_gfm_footnote_definition(context: &mut CompileContext) {
552 context.tight_stack.push(false);
553}
554
555/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteCall`][Name::GfmFootnoteCall].
556fn on_enter_gfm_footnote_call(context: &mut CompileContext) {
557 context.media_stack.push(Media {
558 image: false,
559 label_id: None,
560 label: None,
561 reference_id: None,
562 destination: None,
563 title: None,
564 });
565}
566
567/// Handle [`Enter`][Kind::Enter]:[`GfmStrikethrough`][Name::GfmStrikethrough].
568fn on_enter_gfm_strikethrough(context: &mut CompileContext) {
569 if !context.image_alt_inside {
570 context.push("<del>");
571 }
572}
573
574/// Handle [`Enter`][Kind::Enter]:[`GfmTable`][Name::GfmTable].
575fn on_enter_gfm_table(context: &mut CompileContext) {
576 let align = gfm_table_align(context.events, context.index);
577 context.gfm_table_align = Some(align);
578 context.line_ending_if_needed();
579 context.push("<table>");
580}
581
582/// Handle [`Enter`][Kind::Enter]:[`GfmTableBody`][Name::GfmTableBody].
583fn on_enter_gfm_table_body(context: &mut CompileContext) {
584 context.push("<tbody>");
585}
586
587/// Handle [`Enter`][Kind::Enter]:[`GfmTableCell`][Name::GfmTableCell].
588fn on_enter_gfm_table_cell(context: &mut CompileContext) {
589 let column = context.gfm_table_column;
590 let align = context.gfm_table_align.as_ref().unwrap();
591
592 if column >= align.len() {
593 // Capture cell to ignore it.
594 context.buffer();
595 } else {
596 let value = align[column];
597 context.line_ending_if_needed();
598
599 if context.gfm_table_in_head {
600 context.push("<th");
601 } else {
602 context.push("<td");
603 }
604
605 match value {
606 AlignKind::Left => context.push(" align=\"left\""),
607 AlignKind::Right => context.push(" align=\"right\""),
608 AlignKind::Center => context.push(" align=\"center\""),
609 AlignKind::None => {}
610 }
611
612 context.push(">");
613 }
614}
615
616/// Handle [`Enter`][Kind::Enter]:[`GfmTableHead`][Name::GfmTableHead].
617fn on_enter_gfm_table_head(context: &mut CompileContext) {
618 context.line_ending_if_needed();
619 context.push("<thead>");
620 context.gfm_table_in_head = true;
621}
622
623/// Handle [`Enter`][Kind::Enter]:[`GfmTableRow`][Name::GfmTableRow].
624fn on_enter_gfm_table_row(context: &mut CompileContext) {
625 context.line_ending_if_needed();
626 context.push("<tr>");
627}
628
629/// Handle [`Enter`][Kind::Enter]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck].
630fn on_enter_gfm_task_list_item_check(context: &mut CompileContext) {
631 if !context.image_alt_inside {
632 context.push("<input type=\"checkbox\" ");
633 if !context.options.gfm_task_list_item_checkable {
634 context.push("disabled=\"\" ");
635 }
636 }
637}
638
639/// Handle [`Enter`][Kind::Enter]:[`HtmlFlow`][Name::HtmlFlow].
640fn on_enter_html_flow(context: &mut CompileContext) {
641 context.line_ending_if_needed();
642 if context.options.allow_dangerous_html {
643 context.encode_html = false;
644 }
645}
646
647/// Handle [`Enter`][Kind::Enter]:[`HtmlText`][Name::HtmlText].
648fn on_enter_html_text(context: &mut CompileContext) {
649 if context.options.allow_dangerous_html {
650 context.encode_html = false;
651 }
652}
653
654/// Handle [`Enter`][Kind::Enter]:[`Image`][Name::Image].
655fn on_enter_image(context: &mut CompileContext) {
656 context.media_stack.push(Media {
657 image: true,
658 label_id: None,
659 label: None,
660 reference_id: None,
661 destination: None,
662 title: None,
663 });
664 context.image_alt_inside = true; // Disallow tags.
665}
666
667/// Handle [`Enter`][Kind::Enter]:[`Link`][Name::Link].
668fn on_enter_link(context: &mut CompileContext) {
669 context.media_stack.push(Media {
670 image: false,
671 label_id: None,
672 label: None,
673 reference_id: None,
674 destination: None,
675 title: None,
676 });
677}
678
679/// Handle [`Enter`][Kind::Enter]:[`Link`][Name::WikilinkStart].
680fn on_enter_wikilink(context: &mut CompileContext) {
681 if !context.image_alt_inside {
682 context.push("<strong>");
683 }
684}
685
686fn on_exit_wikilink(context: &mut CompileContext) {
687 if !context.image_alt_inside {
688 context.push("</strong>");
689 }
690}
691
692/// Handle [`Enter`][Kind::Enter]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}.
693fn on_enter_list(context: &mut CompileContext) {
694 let loose = list_loose(context.events, context.index, true);
695 context.tight_stack.push(!loose);
696 context.line_ending_if_needed();
697
698 // Note: no `>`.
699 context.push(if context.events[context.index].name == Name::ListOrdered {
700 "<ol"
701 } else {
702 "<ul"
703 });
704 context.list_expect_first_marker = Some(true);
705}
706
707/// Handle [`Enter`][Kind::Enter]:[`ListItemMarker`][Name::ListItemMarker].
708fn on_enter_list_item_marker(context: &mut CompileContext) {
709 if context.list_expect_first_marker.take().unwrap() {
710 context.push(">");
711 }
712
713 context.line_ending_if_needed();
714
715 context.push("<li>");
716 context.list_expect_first_marker = Some(false);
717}
718
719/// Handle [`Enter`][Kind::Enter]:[`Paragraph`][Name::Paragraph].
720fn on_enter_paragraph(context: &mut CompileContext) {
721 let tight = context.tight_stack.last().unwrap_or(&false);
722
723 if !tight {
724 context.line_ending_if_needed();
725 context.push("<p>");
726 }
727}
728
729/// Handle [`Enter`][Kind::Enter]:[`Resource`][Name::Resource].
730fn on_enter_resource(context: &mut CompileContext) {
731 context.buffer(); // We can have line endings in the resource, ignore them.
732 context.media_stack.last_mut().unwrap().destination = Some(String::new());
733}
734
735/// Handle [`Enter`][Kind::Enter]:[`ResourceDestinationString`][Name::ResourceDestinationString].
736fn on_enter_resource_destination_string(context: &mut CompileContext) {
737 context.buffer();
738 // Ignore encoding the result, as we’ll first percent encode the url and
739 // encode manually after.
740 context.encode_html = false;
741}
742
743/// Handle [`Enter`][Kind::Enter]:[`Strong`][Name::Strong].
744fn on_enter_strong(context: &mut CompileContext) {
745 if !context.image_alt_inside {
746 context.push("<strong>");
747 }
748}
749
750/// Handle [`Exit`][Kind::Exit]:[`AutolinkEmail`][Name::AutolinkEmail].
751fn on_exit_autolink_email(context: &mut CompileContext) {
752 generate_autolink(
753 context,
754 Some("mailto:"),
755 Slice::from_position(
756 context.bytes,
757 &Position::from_exit_event(context.events, context.index),
758 )
759 .as_str(),
760 false,
761 );
762}
763
764/// Handle [`Exit`][Kind::Exit]:[`AutolinkProtocol`][Name::AutolinkProtocol].
765fn on_exit_autolink_protocol(context: &mut CompileContext) {
766 generate_autolink(
767 context,
768 None,
769 Slice::from_position(
770 context.bytes,
771 &Position::from_exit_event(context.events, context.index),
772 )
773 .as_str(),
774 false,
775 );
776}
777
778/// Handle [`Exit`][Kind::Exit]:{[`HardBreakEscape`][Name::HardBreakEscape],[`HardBreakTrailing`][Name::HardBreakTrailing]}.
779fn on_exit_break(context: &mut CompileContext) {
780 if !context.image_alt_inside {
781 context.push("<br />");
782 }
783}
784
785/// Handle [`Exit`][Kind::Exit]:[`BlankLineEnding`][Name::BlankLineEnding].
786fn on_exit_blank_line_ending(context: &mut CompileContext) {
787 context.slurp_one_line_ending = false;
788 if context.index == context.events.len() - 1 {
789 context.line_ending_if_needed();
790 }
791}
792
793/// Handle [`Exit`][Kind::Exit]:[`BlockQuote`][Name::BlockQuote].
794fn on_exit_block_quote(context: &mut CompileContext) {
795 context.tight_stack.pop();
796 context.line_ending_if_needed();
797 context.slurp_one_line_ending = false;
798 context.push("</blockquote>");
799}
800
801/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarker`][Name::CharacterReferenceMarker].
802fn on_exit_character_reference_marker(context: &mut CompileContext) {
803 context.character_reference_marker = Some(b'&');
804}
805
806/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerHexadecimal`][Name::CharacterReferenceMarkerHexadecimal].
807fn on_exit_character_reference_marker_hexadecimal(context: &mut CompileContext) {
808 context.character_reference_marker = Some(b'x');
809}
810
811/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerNumeric`][Name::CharacterReferenceMarkerNumeric].
812fn on_exit_character_reference_marker_numeric(context: &mut CompileContext) {
813 context.character_reference_marker = Some(b'#');
814}
815
816/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceValue`][Name::CharacterReferenceValue].
817fn on_exit_character_reference_value(context: &mut CompileContext) {
818 let marker = context
819 .character_reference_marker
820 .take()
821 .expect("expected `character_reference_kind` to be set");
822 let slice = Slice::from_position(
823 context.bytes,
824 &Position::from_exit_event(context.events, context.index),
825 );
826 let value = decode_character_reference(slice.as_str(), marker, true)
827 .expect("expected to parse only valid named references");
828
829 context.push(&encode(&value, context.encode_html));
830}
831
832/// Handle [`Exit`][Kind::Exit]:{[`CodeFlowChunk`][Name::CodeFlowChunk],[`MathFlowChunk`][Name::MathFlowChunk]}.
833fn on_exit_raw_flow_chunk(context: &mut CompileContext) {
834 context.raw_flow_seen_data = Some(true);
835 context.push(&encode(
836 &Slice::from_position(
837 context.bytes,
838 &Position::from_exit_event(context.events, context.index),
839 )
840 // Must serialize to get virtual spaces.
841 .serialize(),
842 context.encode_html,
843 ));
844}
845
846/// Handle [`Exit`][Kind::Exit]:{[`CodeFencedFence`][Name::CodeFencedFence],[`MathFlowFence`][Name::MathFlowFence]}.
847fn on_exit_raw_flow_fence(context: &mut CompileContext) {
848 let count = context
849 .raw_flow_fences_count
850 .expect("expected `raw_flow_fences_count`");
851
852 if count == 0 {
853 context.push(">");
854 context.slurp_one_line_ending = true;
855 }
856
857 context.raw_flow_fences_count = Some(count + 1);
858}
859
860/// Handle [`Exit`][Kind::Exit]:[`CodeFencedFenceInfo`][Name::CodeFencedFenceInfo].
861///
862/// Note: math (flow) does not support `info`.
863fn on_exit_raw_flow_fence_info(context: &mut CompileContext) {
864 let value = context.resume();
865 context.push(" class=\"language-");
866 context.push(&value);
867 context.push("\"");
868}
869
870/// Handle [`Exit`][Kind::Exit]:{[`CodeFenced`][Name::CodeFenced],[`CodeIndented`][Name::CodeIndented],[`MathFlow`][Name::MathFlow]}.
871fn on_exit_raw_flow(context: &mut CompileContext) {
872 // One special case is if we are inside a container, and the raw (flow) was
873 // not closed (meaning it runs to the end).
874 // In that case, the following line ending, is considered *outside* the
875 // fenced code and block quote by `markdown-rs`, but CM wants to treat that
876 // ending as part of the code.
877 if let Some(count) = context.raw_flow_fences_count {
878 // No closing fence.
879 if count == 1
880 // In a container.
881 && !context.tight_stack.is_empty()
882 // Empty (as the closing is right at the opening fence)
883 && !matches!(context.events[context.index - 1].name, Name::CodeFencedFence | Name::MathFlowFence)
884 {
885 context.line_ending();
886 }
887 }
888
889 // But in most cases, it’s simpler: when we’ve seen some data, emit an extra
890 // line ending when needed.
891 if context
892 .raw_flow_seen_data
893 .take()
894 .expect("`raw_flow_seen_data` must be defined")
895 {
896 context.line_ending_if_needed();
897 }
898
899 context.push("</code></pre>");
900
901 if let Some(count) = context.raw_flow_fences_count.take() {
902 if count < 2 {
903 context.line_ending_if_needed();
904 }
905 }
906
907 context.slurp_one_line_ending = false;
908}
909
910/// Handle [`Exit`][Kind::Exit]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}.
911fn on_exit_raw_text(context: &mut CompileContext) {
912 let result = context.resume();
913 // To do: share with `to_mdast`.
914 let mut bytes = result.as_bytes().to_vec();
915
916 // If we are in a GFM table, we need to decode escaped pipes.
917 // This is a rather weird GFM feature.
918 if context.gfm_table_align.is_some() {
919 let mut index = 0;
920 let mut len = bytes.len();
921
922 while index < len {
923 if index + 1 < len && bytes[index] == b'\\' && bytes[index + 1] == b'|' {
924 bytes.remove(index);
925 len -= 1;
926 }
927
928 index += 1;
929 }
930 }
931
932 let mut trim = false;
933 let mut index = 0;
934 let mut end = bytes.len();
935
936 if end > 2 && bytes[index] == b' ' && bytes[end - 1] == b' ' {
937 index += 1;
938 end -= 1;
939 while index < end && !trim {
940 if bytes[index] != b' ' {
941 trim = true;
942 break;
943 }
944 index += 1;
945 }
946 }
947
948 if trim {
949 bytes.remove(0);
950 bytes.pop();
951 }
952
953 context.raw_text_inside = false;
954 context.push(str::from_utf8(&bytes).unwrap());
955
956 if !context.image_alt_inside {
957 context.push("</code>");
958 }
959}
960
961/// Handle [`Exit`][Kind::Exit]:*.
962///
963/// Resumes, and ignores what was resumed.
964fn on_exit_drop(context: &mut CompileContext) {
965 context.resume();
966}
967
968/// Handle [`Exit`][Kind::Exit]:*.
969///
970/// Resumes, ignores what was resumed, and slurps the following line ending.
971fn on_exit_drop_slurp(context: &mut CompileContext) {
972 context.resume();
973 context.slurp_one_line_ending = true;
974}
975
976/// Handle [`Exit`][Kind::Exit]:{[`CodeTextData`][Name::CodeTextData],[`Data`][Name::Data],[`CharacterEscapeValue`][Name::CharacterEscapeValue]}.
977fn on_exit_data(context: &mut CompileContext) {
978 context.push(&encode(
979 Slice::from_position(
980 context.bytes,
981 &Position::from_exit_event(context.events, context.index),
982 )
983 .as_str(),
984 context.encode_html,
985 ));
986}
987
988/// Handle [`Exit`][Kind::Exit]:[`Definition`][Name::Definition].
989fn on_exit_definition(context: &mut CompileContext) {
990 context.resume();
991 let media = context.media_stack.pop().unwrap();
992 let indices = media.reference_id.unwrap();
993 let id =
994 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str());
995
996 context.definitions.push(Definition {
997 id,
998 destination: media.destination,
999 title: media.title,
1000 });
1001}
1002
1003/// Handle [`Exit`][Kind::Exit]:[`DefinitionDestinationString`][Name::DefinitionDestinationString].
1004fn on_exit_definition_destination_string(context: &mut CompileContext) {
1005 let buf = context.resume();
1006 context.media_stack.last_mut().unwrap().destination = Some(buf);
1007 context.encode_html = true;
1008}
1009
1010/// Handle [`Exit`][Kind::Exit]:[`DefinitionLabelString`][Name::DefinitionLabelString].
1011fn on_exit_definition_label_string(context: &mut CompileContext) {
1012 // Discard label, use the source content instead.
1013 context.resume();
1014 context.media_stack.last_mut().unwrap().reference_id =
1015 Some(Position::from_exit_event(context.events, context.index).to_indices());
1016}
1017
1018/// Handle [`Exit`][Kind::Exit]:[`DefinitionTitleString`][Name::DefinitionTitleString].
1019fn on_exit_definition_title_string(context: &mut CompileContext) {
1020 let buf = context.resume();
1021 context.media_stack.last_mut().unwrap().title = Some(buf);
1022}
1023
1024/// Handle [`Exit`][Kind::Exit]:[`Emphasis`][Name::Emphasis].
1025fn on_exit_emphasis(context: &mut CompileContext) {
1026 if !context.image_alt_inside {
1027 context.push("</em>");
1028 }
1029}
1030
1031/// Handle [`Exit`][Kind::Exit]:[`Frontmatter`][Name::Frontmatter].
1032fn on_exit_frontmatter(context: &mut CompileContext) {
1033 context.resume();
1034 context.slurp_one_line_ending = true;
1035}
1036
1037/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail].
1038fn on_exit_gfm_autolink_literal_email(context: &mut CompileContext) {
1039 generate_autolink(
1040 context,
1041 Some("mailto:"),
1042 Slice::from_position(
1043 context.bytes,
1044 &Position::from_exit_event(context.events, context.index),
1045 )
1046 .as_str(),
1047 true,
1048 );
1049}
1050
1051/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralMailto`][Name::GfmAutolinkLiteralMailto].
1052fn on_exit_gfm_autolink_literal_mailto(context: &mut CompileContext) {
1053 generate_autolink(
1054 context,
1055 None,
1056 Slice::from_position(
1057 context.bytes,
1058 &Position::from_exit_event(context.events, context.index),
1059 )
1060 .as_str(),
1061 true,
1062 );
1063}
1064
1065/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralProtocol`][Name::GfmAutolinkLiteralProtocol].
1066fn on_exit_gfm_autolink_literal_protocol(context: &mut CompileContext) {
1067 generate_autolink(
1068 context,
1069 None,
1070 Slice::from_position(
1071 context.bytes,
1072 &Position::from_exit_event(context.events, context.index),
1073 )
1074 .as_str(),
1075 true,
1076 );
1077}
1078
1079/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralWww`][Name::GfmAutolinkLiteralWww].
1080fn on_exit_gfm_autolink_literal_www(context: &mut CompileContext) {
1081 generate_autolink(
1082 context,
1083 Some("http://"),
1084 Slice::from_position(
1085 context.bytes,
1086 &Position::from_exit_event(context.events, context.index),
1087 )
1088 .as_str(),
1089 true,
1090 );
1091}
1092
1093/// Handle [`Exit`][Kind::Exit]:[`GfmAutolinkLiteralXmpp`][Name::GfmAutolinkLiteralXmpp].
1094fn on_exit_gfm_autolink_literal_xmpp(context: &mut CompileContext) {
1095 generate_autolink(
1096 context,
1097 None,
1098 Slice::from_position(
1099 context.bytes,
1100 &Position::from_exit_event(context.events, context.index),
1101 )
1102 .as_str(),
1103 true,
1104 );
1105}
1106
1107/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteCall`][Name::GfmFootnoteCall].
1108fn on_exit_gfm_footnote_call(context: &mut CompileContext) {
1109 let indices = context.media_stack.pop().unwrap().label_id.unwrap();
1110 let id =
1111 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str());
1112 let safe_id = sanitize(&id.to_lowercase());
1113 let mut call_index = 0;
1114
1115 // See if this has been called before.
1116 while call_index < context.gfm_footnote_definition_calls.len() {
1117 if context.gfm_footnote_definition_calls[call_index].0 == id {
1118 break;
1119 }
1120 call_index += 1;
1121 }
1122
1123 // New.
1124 if call_index == context.gfm_footnote_definition_calls.len() {
1125 context.gfm_footnote_definition_calls.push((id, 0));
1126 }
1127
1128 // Increment.
1129 context.gfm_footnote_definition_calls[call_index].1 += 1;
1130
1131 // No call is output in an image alt, though the definition and
1132 // backreferences are generated as if it was the case.
1133 if context.image_alt_inside {
1134 return;
1135 }
1136
1137 context.push("<sup><a href=\"#");
1138 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1139 context.push(&encode(value, context.encode_html));
1140 } else {
1141 context.push("user-content-");
1142 }
1143 context.push("fn-");
1144 context.push(&safe_id);
1145 context.push("\" id=\"");
1146 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1147 context.push(&encode(value, context.encode_html));
1148 } else {
1149 context.push("user-content-");
1150 }
1151 context.push("fnref-");
1152 context.push(&safe_id);
1153 if context.gfm_footnote_definition_calls[call_index].1 > 1 {
1154 context.push("-");
1155 context.push(
1156 &context.gfm_footnote_definition_calls[call_index]
1157 .1
1158 .to_string(),
1159 );
1160 }
1161 context.push("\" data-footnote-ref=\"\" aria-describedby=\"footnote-label\">");
1162
1163 context.push(&(call_index + 1).to_string());
1164 context.push("</a></sup>");
1165}
1166
1167/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinitionLabelString`][Name::GfmFootnoteDefinitionLabelString].
1168fn on_exit_gfm_footnote_definition_label_string(context: &mut CompileContext) {
1169 context
1170 .gfm_footnote_definition_stack
1171 .push(Position::from_exit_event(context.events, context.index).to_indices());
1172}
1173
1174/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinitionPrefix`][Name::GfmFootnoteDefinitionPrefix].
1175fn on_exit_gfm_footnote_definition_prefix(context: &mut CompileContext) {
1176 // Drop the prefix.
1177 context.resume();
1178 // Capture everything until end of definition.
1179 context.buffer();
1180}
1181
1182/// Handle [`Exit`][Kind::Exit]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition].
1183fn on_exit_gfm_footnote_definition(context: &mut CompileContext) {
1184 let value = context.resume();
1185 let indices = context.gfm_footnote_definition_stack.pop().unwrap();
1186 context.tight_stack.pop();
1187 context.gfm_footnote_definitions.push((
1188 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str()),
1189 value,
1190 ));
1191}
1192
1193/// Handle [`Exit`][Kind::Exit]:[`GfmStrikethrough`][Name::GfmStrikethrough].
1194fn on_exit_gfm_strikethrough(context: &mut CompileContext) {
1195 if !context.image_alt_inside {
1196 context.push("</del>");
1197 }
1198}
1199
1200/// Handle [`Exit`][Kind::Exit]:[`GfmTable`][Name::GfmTable].
1201fn on_exit_gfm_table(context: &mut CompileContext) {
1202 context.gfm_table_align = None;
1203 context.line_ending_if_needed();
1204 context.push("</table>");
1205}
1206
1207/// Handle [`Exit`][Kind::Exit]:[`GfmTableBody`][Name::GfmTableBody].
1208fn on_exit_gfm_table_body(context: &mut CompileContext) {
1209 context.line_ending_if_needed();
1210 context.push("</tbody>");
1211}
1212
1213/// Handle [`Exit`][Kind::Exit]:[`GfmTableCell`][Name::GfmTableCell].
1214fn on_exit_gfm_table_cell(context: &mut CompileContext) {
1215 let align = context.gfm_table_align.as_ref().unwrap();
1216
1217 if context.gfm_table_column < align.len() {
1218 if context.gfm_table_in_head {
1219 context.push("</th>");
1220 } else {
1221 context.push("</td>");
1222 }
1223 } else {
1224 // Stop capturing.
1225 context.resume();
1226 }
1227
1228 context.gfm_table_column += 1;
1229}
1230
1231/// Handle [`Exit`][Kind::Exit]:[`GfmTableHead`][Name::GfmTableHead].
1232fn on_exit_gfm_table_head(context: &mut CompileContext) {
1233 context.gfm_table_in_head = false;
1234 context.line_ending_if_needed();
1235 context.push("</thead>");
1236}
1237
1238/// Handle [`Exit`][Kind::Exit]:[`GfmTableRow`][Name::GfmTableRow].
1239fn on_exit_gfm_table_row(context: &mut CompileContext) {
1240 let mut column = context.gfm_table_column;
1241 let len = context.gfm_table_align.as_ref().unwrap().len();
1242
1243 // Add “phantom” cells, for body rows that are shorter than the delimiter
1244 // row (which is equal to the head row).
1245 while column < len {
1246 on_enter_gfm_table_cell(context);
1247 on_exit_gfm_table_cell(context);
1248 column += 1;
1249 }
1250
1251 context.gfm_table_column = 0;
1252 context.line_ending_if_needed();
1253 context.push("</tr>");
1254}
1255
1256/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck].
1257fn on_exit_gfm_task_list_item_check(context: &mut CompileContext) {
1258 if !context.image_alt_inside {
1259 context.push("/>");
1260 }
1261}
1262
1263/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemValueChecked`][Name::GfmTaskListItemValueChecked].
1264fn on_exit_gfm_task_list_item_value_checked(context: &mut CompileContext) {
1265 if !context.image_alt_inside {
1266 context.push("checked=\"\" ");
1267 }
1268}
1269
1270/// Handle [`Exit`][Kind::Exit]:[`HeadingAtx`][Name::HeadingAtx].
1271fn on_exit_heading_atx(context: &mut CompileContext) {
1272 let rank = context
1273 .heading_atx_rank
1274 .take()
1275 .expect("`heading_atx_rank` must be set in headings");
1276
1277 context.push("</h");
1278 context.push(&rank.to_string());
1279 context.push(">");
1280}
1281
1282/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxSequence`][Name::HeadingAtxSequence].
1283fn on_exit_heading_atx_sequence(context: &mut CompileContext) {
1284 // First fence we see.
1285 if context.heading_atx_rank.is_none() {
1286 let rank = Slice::from_position(
1287 context.bytes,
1288 &Position::from_exit_event(context.events, context.index),
1289 )
1290 .len();
1291 context.line_ending_if_needed();
1292 context.heading_atx_rank = Some(rank);
1293 context.push("<h");
1294 context.push(&rank.to_string());
1295 context.push(">");
1296 }
1297}
1298
1299/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxText`][Name::HeadingAtxText].
1300fn on_exit_heading_atx_text(context: &mut CompileContext) {
1301 let value = context.resume();
1302 context.push(&value);
1303}
1304
1305/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextText`][Name::HeadingSetextText].
1306fn on_exit_heading_setext_text(context: &mut CompileContext) {
1307 let buf = context.resume();
1308 context.heading_setext_buffer = Some(buf);
1309 context.slurp_one_line_ending = true;
1310}
1311
1312/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextUnderlineSequence`][Name::HeadingSetextUnderlineSequence].
1313fn on_exit_heading_setext_underline_sequence(context: &mut CompileContext) {
1314 let text = context
1315 .heading_setext_buffer
1316 .take()
1317 .expect("`heading_atx_rank` must be set in headings");
1318 let position = Position::from_exit_event(context.events, context.index);
1319 let head = context.bytes[position.start.index];
1320 let rank = if head == b'-' { "2" } else { "1" };
1321
1322 context.line_ending_if_needed();
1323 context.push("<h");
1324 context.push(rank);
1325 context.push(">");
1326 context.push(&text);
1327 context.push("</h");
1328 context.push(rank);
1329 context.push(">");
1330}
1331
1332/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlow`][Name::HtmlFlow],[`HtmlText`][Name::HtmlText]}.
1333fn on_exit_html(context: &mut CompileContext) {
1334 context.encode_html = true;
1335}
1336
1337/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlowData`][Name::HtmlFlowData],[`HtmlTextData`][Name::HtmlTextData]}.
1338fn on_exit_html_data(context: &mut CompileContext) {
1339 let slice = Slice::from_position(
1340 context.bytes,
1341 &Position::from_exit_event(context.events, context.index),
1342 );
1343 let value = slice.as_str();
1344
1345 let encoded = if context.options.gfm_tagfilter && context.options.allow_dangerous_html {
1346 encode(&gfm_tagfilter(value), context.encode_html)
1347 } else {
1348 encode(value, context.encode_html)
1349 };
1350
1351 context.push(&encoded);
1352}
1353
1354/// Handle [`Exit`][Kind::Exit]:[`Label`][Name::Label].
1355fn on_exit_label(context: &mut CompileContext) {
1356 let buf = context.resume();
1357 context.media_stack.last_mut().unwrap().label = Some(buf);
1358}
1359
1360/// Handle [`Exit`][Kind::Exit]:[`LabelText`][Name::LabelText].
1361fn on_exit_label_text(context: &mut CompileContext) {
1362 context.media_stack.last_mut().unwrap().label_id =
1363 Some(Position::from_exit_event(context.events, context.index).to_indices());
1364}
1365
1366/// Handle [`Exit`][Kind::Exit]:[`LineEnding`][Name::LineEnding].
1367fn on_exit_line_ending(context: &mut CompileContext) {
1368 if context.raw_text_inside {
1369 context.push(" ");
1370 } else if context.slurp_one_line_ending
1371 // Ignore line endings after definitions.
1372 || (context.index > 1
1373 && (context.events[context.index - 2].name == Name::Definition
1374 || context.events[context.index - 2].name == Name::GfmFootnoteDefinition))
1375 {
1376 context.slurp_one_line_ending = false;
1377 } else {
1378 context.push(&encode(
1379 Slice::from_position(
1380 context.bytes,
1381 &Position::from_exit_event(context.events, context.index),
1382 )
1383 .as_str(),
1384 context.encode_html,
1385 ));
1386 }
1387}
1388
1389/// Handle [`Exit`][Kind::Exit]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}.
1390fn on_exit_list(context: &mut CompileContext) {
1391 context.tight_stack.pop();
1392 context.line_ending();
1393 context.push(if context.events[context.index].name == Name::ListOrdered {
1394 "</ol>"
1395 } else {
1396 "</ul>"
1397 });
1398}
1399
1400/// Handle [`Exit`][Kind::Exit]:[`ListItem`][Name::ListItem].
1401fn on_exit_list_item(context: &mut CompileContext) {
1402 let tight = context.tight_stack.last().unwrap_or(&false);
1403 let before_item = skip::opt_back(
1404 context.events,
1405 context.index - 1,
1406 &[
1407 Name::BlankLineEnding,
1408 Name::BlockQuotePrefix,
1409 Name::LineEnding,
1410 Name::SpaceOrTab,
1411 // Also ignore things that don’t contribute to the document.
1412 Name::Definition,
1413 Name::GfmFootnoteDefinition,
1414 ],
1415 );
1416 let previous = &context.events[before_item];
1417 let tight_paragraph = *tight && previous.name == Name::Paragraph;
1418 let empty_item = previous.name == Name::ListItemPrefix;
1419
1420 context.slurp_one_line_ending = false;
1421
1422 if !tight_paragraph && !empty_item {
1423 context.line_ending_if_needed();
1424 }
1425
1426 context.push("</li>");
1427}
1428
1429/// Handle [`Exit`][Kind::Exit]:[`ListItemValue`][Name::ListItemValue].
1430fn on_exit_list_item_value(context: &mut CompileContext) {
1431 if context.list_expect_first_marker.unwrap() {
1432 let slice = Slice::from_position(
1433 context.bytes,
1434 &Position::from_exit_event(context.events, context.index),
1435 );
1436 let value = slice.as_str().parse::<u32>().ok().unwrap();
1437
1438 if value != 1 {
1439 context.push(" start=\"");
1440 context.push(&value.to_string());
1441 context.push("\"");
1442 }
1443 }
1444}
1445
1446/// Handle [`Exit`][Kind::Exit]:{[`Image`][Name::Image],[`Link`][Name::Link]}.
1447fn on_exit_media(context: &mut CompileContext) {
1448 let mut is_in_image = false;
1449 let mut index = 0;
1450
1451 // Skip current.
1452 let end = context.media_stack.len() - 1;
1453 while index < end {
1454 if context.media_stack[index].image {
1455 is_in_image = true;
1456 break;
1457 }
1458 index += 1;
1459 }
1460
1461 context.image_alt_inside = is_in_image;
1462
1463 let media = context.media_stack.pop().unwrap();
1464 let label = media.label.unwrap();
1465 let id = media.reference_id.or(media.label_id).map(|indices| {
1466 normalize_identifier(Slice::from_indices(context.bytes, indices.0, indices.1).as_str())
1467 });
1468
1469 let definition_index = if media.destination.is_none() {
1470 id.map(|id| {
1471 let mut index = 0;
1472
1473 while index < context.definitions.len() && context.definitions[index].id != id {
1474 index += 1;
1475 }
1476
1477 debug_assert!(
1478 index < context.definitions.len(),
1479 "expected defined definition"
1480 );
1481 index
1482 })
1483 } else {
1484 None
1485 };
1486
1487 if !is_in_image {
1488 if media.image {
1489 context.push("<img src=\"");
1490 } else {
1491 context.push("<a href=\"");
1492 }
1493
1494 let destination = if let Some(index) = definition_index {
1495 context.definitions[index].destination.as_ref()
1496 } else {
1497 media.destination.as_ref()
1498 };
1499
1500 if let Some(destination) = destination {
1501 let allow_dangerous_protocol = context.options.allow_dangerous_protocol
1502 || (context.options.allow_any_img_src && media.image);
1503
1504 let url = if allow_dangerous_protocol {
1505 sanitize(destination)
1506 } else {
1507 sanitize_with_protocols(
1508 destination,
1509 if media.image {
1510 &SAFE_PROTOCOL_SRC
1511 } else {
1512 &SAFE_PROTOCOL_HREF
1513 },
1514 )
1515 };
1516 context.push(&url);
1517 }
1518
1519 if media.image {
1520 context.push("\" alt=\"");
1521 }
1522 }
1523
1524 if media.image {
1525 context.push(&label);
1526 }
1527
1528 if !is_in_image {
1529 context.push("\"");
1530
1531 let title = if let Some(index) = definition_index {
1532 context.definitions[index].title.clone()
1533 } else {
1534 media.title
1535 };
1536
1537 if let Some(title) = title {
1538 context.push(" title=\"");
1539 context.push(&title);
1540 context.push("\"");
1541 }
1542
1543 if media.image {
1544 context.push(" /");
1545 }
1546
1547 context.push(">");
1548 }
1549
1550 if !media.image {
1551 context.push(&label);
1552
1553 if !is_in_image {
1554 context.push("</a>");
1555 }
1556 }
1557}
1558
1559/// Handle [`Exit`][Kind::Exit]:[`Paragraph`][Name::Paragraph].
1560fn on_exit_paragraph(context: &mut CompileContext) {
1561 let tight = context.tight_stack.last().unwrap_or(&false);
1562
1563 if *tight {
1564 context.slurp_one_line_ending = true;
1565 } else {
1566 context.push("</p>");
1567 }
1568}
1569
1570/// Handle [`Exit`][Kind::Exit]:[`ReferenceString`][Name::ReferenceString].
1571fn on_exit_reference_string(context: &mut CompileContext) {
1572 // Drop stuff.
1573 context.resume();
1574
1575 context.media_stack.last_mut().unwrap().reference_id =
1576 Some(Position::from_exit_event(context.events, context.index).to_indices());
1577}
1578
1579/// Handle [`Exit`][Kind::Exit]:[`ResourceDestinationString`][Name::ResourceDestinationString].
1580fn on_exit_resource_destination_string(context: &mut CompileContext) {
1581 let buf = context.resume();
1582 context.media_stack.last_mut().unwrap().destination = Some(buf);
1583 context.encode_html = true;
1584}
1585
1586/// Handle [`Exit`][Kind::Exit]:[`ResourceTitleString`][Name::ResourceTitleString].
1587fn on_exit_resource_title_string(context: &mut CompileContext) {
1588 let buf = context.resume();
1589 context.media_stack.last_mut().unwrap().title = Some(buf);
1590}
1591
1592/// Handle [`Exit`][Kind::Exit]:[`Strong`][Name::Strong].
1593fn on_exit_strong(context: &mut CompileContext) {
1594 if !context.image_alt_inside {
1595 context.push("</strong>");
1596 }
1597}
1598
1599/// Handle [`Exit`][Kind::Exit]:[`ThematicBreak`][Name::ThematicBreak].
1600fn on_exit_thematic_break(context: &mut CompileContext) {
1601 context.line_ending_if_needed();
1602 context.push("<hr />");
1603}
1604
1605/// Generate a footnote section.
1606fn generate_footnote_section(context: &mut CompileContext) {
1607 context.line_ending_if_needed();
1608 context.push("<section data-footnotes=\"\" class=\"footnotes\"><");
1609 if let Some(ref value) = context.options.gfm_footnote_label_tag_name {
1610 context.push(&encode(value, context.encode_html));
1611 } else {
1612 context.push("h2");
1613 }
1614 context.push(" id=\"footnote-label\" ");
1615 if let Some(ref value) = context.options.gfm_footnote_label_attributes {
1616 context.push(value);
1617 } else {
1618 context.push("class=\"sr-only\"");
1619 }
1620 context.push(">");
1621 if let Some(ref value) = context.options.gfm_footnote_label {
1622 context.push(&encode(value, context.encode_html));
1623 } else {
1624 context.push("Footnotes");
1625 }
1626 context.push("</");
1627 if let Some(ref value) = context.options.gfm_footnote_label_tag_name {
1628 context.push(&encode(value, context.encode_html));
1629 } else {
1630 context.push("h2");
1631 }
1632 context.push(">");
1633 context.line_ending();
1634 context.push("<ol>");
1635
1636 let mut index = 0;
1637 while index < context.gfm_footnote_definition_calls.len() {
1638 generate_footnote_item(context, index);
1639 index += 1;
1640 }
1641
1642 context.line_ending();
1643 context.push("</ol>");
1644 context.line_ending();
1645 context.push("</section>");
1646 context.line_ending();
1647}
1648
1649/// Generate a footnote item from a call.
1650fn generate_footnote_item(context: &mut CompileContext, index: usize) {
1651 let id = &context.gfm_footnote_definition_calls[index].0;
1652 let safe_id = sanitize(&id.to_lowercase());
1653
1654 // Find definition: we’ll always find it.
1655 let mut definition_index = 0;
1656 while definition_index < context.gfm_footnote_definitions.len() {
1657 if &context.gfm_footnote_definitions[definition_index].0 == id {
1658 break;
1659 }
1660 definition_index += 1;
1661 }
1662
1663 debug_assert_ne!(
1664 definition_index,
1665 context.gfm_footnote_definitions.len(),
1666 "expected definition"
1667 );
1668
1669 context.line_ending();
1670 context.push("<li id=\"");
1671 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1672 context.push(&encode(value, context.encode_html));
1673 } else {
1674 context.push("user-content-");
1675 }
1676 context.push("fn-");
1677 context.push(&safe_id);
1678 context.push("\">");
1679 context.line_ending();
1680
1681 // Create one or more backreferences.
1682 let mut reference_index = 0;
1683 let mut backreferences = String::new();
1684 while reference_index < context.gfm_footnote_definition_calls[index].1 {
1685 if reference_index != 0 {
1686 backreferences.push(' ');
1687 }
1688 backreferences.push_str("<a href=\"#");
1689 if let Some(ref value) = context.options.gfm_footnote_clobber_prefix {
1690 backreferences.push_str(&encode(value, context.encode_html));
1691 } else {
1692 backreferences.push_str("user-content-");
1693 }
1694 backreferences.push_str("fnref-");
1695 backreferences.push_str(&safe_id);
1696 if reference_index != 0 {
1697 backreferences.push('-');
1698 backreferences.push_str(&(reference_index + 1).to_string());
1699 }
1700 backreferences.push_str("\" data-footnote-backref=\"\" aria-label=\"");
1701 if let Some(ref value) = context.options.gfm_footnote_back_label {
1702 backreferences.push_str(&encode(value, context.encode_html));
1703 } else {
1704 backreferences.push_str("Back to content");
1705 }
1706 backreferences.push_str("\" class=\"data-footnote-backref\">↩");
1707 if reference_index != 0 {
1708 backreferences.push_str("<sup>");
1709 backreferences.push_str(&(reference_index + 1).to_string());
1710 backreferences.push_str("</sup>");
1711 }
1712 backreferences.push_str("</a>");
1713
1714 reference_index += 1;
1715 }
1716
1717 let value = context.gfm_footnote_definitions[definition_index].1.clone();
1718 let bytes = value.as_bytes();
1719 let mut byte_index = bytes.len();
1720 // Move back past EOL.
1721 while byte_index > 0 && matches!(bytes[byte_index - 1], b'\n' | b'\r') {
1722 byte_index -= 1;
1723 }
1724 // Check if it ends in `</p>`.
1725 // This is a bit funky if someone wrote a safe paragraph by hand in
1726 // there.
1727 // But in all other cases, `<` and `>` would be encoded, so we can be
1728 // sure that this is generated by our compiler.
1729 if byte_index > 3
1730 && bytes[byte_index - 4] == b'<'
1731 && bytes[byte_index - 3] == b'/'
1732 && bytes[byte_index - 2] == b'p'
1733 && bytes[byte_index - 1] == b'>'
1734 {
1735 let (before, after) = bytes.split_at(byte_index - 4);
1736 let mut result = String::new();
1737 result.push_str(str::from_utf8(before).unwrap());
1738 result.push(' ');
1739 result.push_str(&backreferences);
1740 result.push_str(str::from_utf8(after).unwrap());
1741 context.push(&result);
1742 } else {
1743 context.push(&value);
1744 context.line_ending_if_needed();
1745 context.push(&backreferences);
1746 }
1747 context.line_ending_if_needed();
1748 context.push("</li>");
1749}
1750
1751/// Generate an autolink (used by unicode autolinks and GFM autolink literals).
1752fn generate_autolink(
1753 context: &mut CompileContext,
1754 protocol: Option<&str>,
1755 value: &str,
1756 is_gfm_literal: bool,
1757) {
1758 let mut is_in_link = false;
1759 let mut index = 0;
1760
1761 while index < context.media_stack.len() {
1762 if !context.media_stack[index].image {
1763 is_in_link = true;
1764 break;
1765 }
1766 index += 1;
1767 }
1768
1769 if !context.image_alt_inside && (!is_in_link || !is_gfm_literal) {
1770 context.push("<a href=\"");
1771 let url = if let Some(protocol) = protocol {
1772 format!("{}{}", protocol, value)
1773 } else {
1774 value.into()
1775 };
1776
1777 let url = if context.options.allow_dangerous_protocol {
1778 sanitize(&url)
1779 } else {
1780 sanitize_with_protocols(&url, &SAFE_PROTOCOL_HREF)
1781 };
1782
1783 context.push(&url);
1784 context.push("\">");
1785 }
1786
1787 context.push(&encode(value, context.encode_html));
1788
1789 if !context.image_alt_inside && (!is_in_link || !is_gfm_literal) {
1790 context.push("</a>");
1791 }
1792}