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