Markdown parser fork with extended syntax for personal use.
at hack 1792 lines 60 kB view raw
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}