1use alloc::{
8 format,
9 string::{String, ToString},
10 vec::Vec,
11};
12
13use crate::{
14 ast::*,
15 diagnostic::Diagnostic,
16 memo::{pattern_starts, PathMemo, Positions, Step},
17 parse::{gfm_table_can_start_source, line_starts_html_block},
18 validate::validate_document,
19};
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum LineEnding {
24 Lf,
26 CrLf,
28}
29
30impl LineEnding {
31 fn as_str(self) -> &'static str {
32 match self {
33 Self::Lf => "\n",
34 Self::CrLf => "\r\n",
35 }
36 }
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
42#[non_exhaustive]
43pub struct SerializeOptions {
44 pub line_ending: LineEnding,
46 pub final_newline: bool,
48 pub bullet: ListDelimiter,
50 pub ordered_delimiter: ListDelimiter,
52 pub fence_marker: FenceMarker,
54}
55
56impl Default for SerializeOptions {
57 fn default() -> Self {
58 Self {
59 line_ending: LineEnding::Lf,
60 final_newline: true,
61 bullet: ListDelimiter::Dash,
62 ordered_delimiter: ListDelimiter::Period,
63 fence_marker: FenceMarker::Backtick,
64 }
65 }
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
70pub enum SerializeError {
71 InvalidDocument(Vec<Diagnostic>),
73 UnsupportedNode(&'static str),
75}
76
77impl Document {
78 pub fn to_markdown(&self) -> Result<String, SerializeError> {
80 self.to_markdown_with(&SerializeOptions::default())
81 }
82
83 pub fn to_markdown_with(&self, options: &SerializeOptions) -> Result<String, SerializeError> {
85 let diagnostics = validate_document(self);
86 if !diagnostics.is_empty() {
87 return Err(SerializeError::InvalidDocument(diagnostics));
88 }
89
90 serialize_document_body(self, options)
91 }
92}
93
94fn serialize_document_body(
95 document: &Document,
96 options: &SerializeOptions,
97) -> Result<String, SerializeError> {
98 let mut output = serialize_blocks_at_start(&document.children, options, true)?;
99 if options.line_ending == LineEnding::CrLf {
100 output = output.replace('\n', "\r\n");
101 }
102 if options.final_newline
103 && !output.is_empty()
104 && !output.ends_with(options.line_ending.as_str())
105 {
106 output.push_str(options.line_ending.as_str());
107 }
108 Ok(output)
109}
110
111fn serialize_blocks_at_start(
116 blocks: &[Block],
117 options: &SerializeOptions,
118 document_start: bool,
119) -> Result<String, SerializeError> {
120 let mut output = String::new();
121 for (index, block) in blocks.iter().enumerate() {
122 if index > 0 {
123 output.push_str("\n\n");
124 }
125 let at_document_start = document_start && index == 0;
126 if let (
127 Block::List(list),
128 Some(Block::CodeBlock(CodeBlock {
129 kind: CodeBlockKind::Indented,
130 ..
131 })),
132 ) = (block, blocks.get(index + 1))
133 {
134 output.push_str(&serialize_list_with_marker_spacing(
135 list, options, " ", " ",
136 )?);
137 } else {
138 output.push_str(&serialize_block(block, options, at_document_start)?);
139 }
140 }
141 Ok(output)
142}
143
144fn serialize_block(
145 block: &Block,
146 options: &SerializeOptions,
147 at_document_start: bool,
148) -> Result<String, SerializeError> {
149 match block {
150 Block::Paragraph(node) => serialize_paragraph(node, options),
151 Block::Heading(node) => {
152 let content = serialize_inlines(&node.children, options)?;
153 let setext_representable = matches!(node.depth, 1 | 2);
158 Ok(match node.kind {
159 HeadingKind::Setext if setext_representable => {
160 let marker = if node.depth == 1 { '=' } else { '-' };
161 format!(
162 "{}\n{}",
163 content,
164 marker.to_string().repeat(content.len().max(3))
165 )
166 }
167 _ if content.is_empty() => "#".repeat(node.depth as usize),
168 _ => format!(
169 "{} {}",
170 "#".repeat(node.depth as usize),
171 escape_atx_heading_content(&content)
172 ),
173 })
174 }
175 Block::ThematicBreak(node) => Ok(match node.marker {
176 ThematicBreakMarker::Dash if at_document_start => "- - -".into(),
182 ThematicBreakMarker::Dash => "---".into(),
183 ThematicBreakMarker::Asterisk => "***".into(),
184 ThematicBreakMarker::Underscore => "___".into(),
185 }),
186 Block::BlockQuote(node) => {
187 let inner = serialize_blocks_at_start(&node.children, options, false)?;
188 if inner.is_empty() {
189 Ok(">".into())
190 } else {
191 Ok(prefix_lines(&inner, "> "))
192 }
193 }
194 Block::Alert(node) => serialize_alert(node, options),
195 Block::List(node) => serialize_list(node, options),
196 Block::DescriptionList(node) => serialize_description_list(node, options),
197 Block::CodeBlock(node) => serialize_code_block(node, options),
198 Block::HtmlBlock(node) => Ok(trim_trailing_newline(&node.value).into()),
199 Block::HtmlContainer(node) => serialize_html_container(node, options),
200 Block::Definition(node) => {
201 let destination = serialize_destination_kind(
202 &node.destination,
203 node.destination_kind,
204 InlineSerializeContext::default(),
205 );
206 let mut label = if node.meta.span.is_some() {
207 escape_definition_label_source(&node.label)
208 } else {
209 escape_reference_label_with_pipe(&node.label, false)
210 };
211 if node.meta.span.is_none() && label.starts_with('^') {
212 label.insert(0, '\\');
213 }
214 let mut output = format!("[{}]: {}", label, destination);
215 if let (Some(title), Some(title_kind)) = (&node.title, node.title_kind) {
216 output.push(' ');
217 output.push_str(&serialize_title_kind(
218 title,
219 title_kind,
220 InlineSerializeContext::default(),
221 ));
222 }
223 Ok(output)
224 }
225 Block::FootnoteDefinition(node) => {
226 let inner = serialize_blocks_at_start(&node.children, options, false)?;
227 let label = if node.meta.span.is_some() {
228 escape_footnote_label_source(&node.label)
229 } else {
230 escape_footnote_label_semantic(&node.label)
231 };
232 Ok(format!("[^{}]: {}", label, indent_continuation(&inner)))
233 }
234 Block::Table(node) => serialize_table(node, options),
235 Block::MathBlock(node) => {
236 let fence = block_math_fence(&node.value);
237 Ok(format!(
238 "{fence}\n{}\n{fence}",
239 trim_trailing_newline(&node.value)
240 ))
241 }
242 Block::Frontmatter(node) => {
243 let fence = match node.kind {
244 FrontmatterKind::Yaml => "---",
245 FrontmatterKind::Toml => "+++",
246 };
247 Ok(format!(
248 "{fence}\n{}\n{fence}",
249 trim_trailing_newline(&node.value)
250 ))
251 }
252 Block::MdxEsm(node) => Ok(node.value.clone()),
253 Block::MdxExpression(node) => Ok(format!("{{{}}}", node.value)),
254 Block::MdxJsx(node) => Ok(node.value.clone()),
255 Block::LeafDirective(node) => Ok(format!(
256 "::{}{}{}",
257 node.name,
258 serialize_directive_label(&node.label, options)?,
259 serialize_attributes(&node.attributes)
260 )),
261 Block::ContainerDirective(node) => {
262 let inner = serialize_blocks_at_start(&node.children, options, false)?;
263 let fence = directive_fence(&inner);
264 Ok(format!(
265 "{fence}{}{}{}\n{}\n{fence}",
266 node.name,
267 serialize_directive_label(&node.label, options)?,
268 serialize_attributes(&node.attributes),
269 inner
270 ))
271 }
272 }
273}
274
275fn serialize_html_container(
276 node: &HtmlContainer,
277 options: &SerializeOptions,
278) -> Result<String, SerializeError> {
279 match &node.content {
280 HtmlContainerContent::Blocks(children) => {
281 let inner = serialize_blocks_at_start(children, options, false)?;
282 if inner.is_empty() {
283 Ok(format!("{}\n{}", node.opening.raw, node.closing.raw))
284 } else {
285 Ok(format!(
286 "{}\n{}\n\n{}",
287 node.opening.raw, inner, node.closing.raw
288 ))
289 }
290 }
291 HtmlContainerContent::Inlines(children) => Ok(format!(
292 "{}{}{}",
293 node.opening.raw,
294 serialize_inlines(children, options)?,
295 node.closing.raw
296 )),
297 }
298}
299
300fn escape_atx_heading_content(content: &str) -> String {
305 let trimmed_len = content.trim_end_matches([' ', '\t']).len();
306 let trimmed = &content[..trimmed_len];
307 let hash_start = trimmed.trim_end_matches('#').len();
308 let preceded_by_whitespace = trimmed[..hash_start]
309 .chars()
310 .next_back()
311 .is_some_and(|char| char == ' ' || char == '\t');
312 if hash_start == trimmed_len || !preceded_by_whitespace {
313 return content.into();
314 }
315 let mut output = String::with_capacity(content.len() + 1);
316 output.push_str(&content[..hash_start]);
317 output.push('\\');
318 output.push_str(&content[hash_start..]);
319 output
320}
321
322fn serialize_paragraph(
323 node: &Paragraph,
324 options: &SerializeOptions,
325) -> Result<String, SerializeError> {
326 let mut output = serialize_inlines(&node.children, options)?;
327 if let Some(offset) = paragraph_html_block_escape_offset(&output) {
328 output.insert(offset, '\\');
329 }
330 if let Some(offset) = paragraph_table_escape_offset(&output) {
331 output.insert(offset, '\\');
332 }
333 Ok(output)
334}
335
336fn paragraph_html_block_escape_offset(input: &str) -> Option<usize> {
337 let first_line = input.split('\n').next().unwrap_or(input);
338 if !line_starts_html_block(first_line) {
339 return None;
340 }
341
342 Some(
343 first_line
344 .as_bytes()
345 .iter()
346 .take_while(|byte| **byte == b' ')
347 .count(),
348 )
349}
350
351fn paragraph_table_escape_offset(input: &str) -> Option<usize> {
352 let first_line_end = input.find('\n')?;
353 let first_line = &input[..first_line_end];
354 let second_line_start = first_line_end + 1;
355 let second_line_end = input[second_line_start..]
356 .find('\n')
357 .map(|offset| second_line_start + offset)
358 .unwrap_or(input.len());
359 let second_line = &input[second_line_start..second_line_end];
360
361 if !gfm_table_can_start_source(first_line, second_line) {
362 return None;
363 }
364
365 second_line
366 .find('-')
367 .map(|offset| second_line_start + offset)
368}
369
370fn serialize_alert(node: &Alert, options: &SerializeOptions) -> Result<String, SerializeError> {
371 let mut output = String::from("> [!");
372 output.push_str(alert_kind_name(node.kind));
373 output.push(']');
374 if let Some(title) = &node.title {
375 if !title.is_empty() {
376 output.push(' ');
377 output.push_str(&escape_alert_title(title));
378 }
379 }
380 let inner = serialize_blocks_at_start(&node.children, options, false)?;
381 if !inner.is_empty() {
382 output.push('\n');
383 output.push_str(&prefix_lines(&inner, "> "));
384 }
385 Ok(output)
386}
387
388fn alert_kind_name(kind: AlertKind) -> &'static str {
389 match kind {
390 AlertKind::Note => "NOTE",
391 AlertKind::Tip => "TIP",
392 AlertKind::Important => "IMPORTANT",
393 AlertKind::Warning => "WARNING",
394 AlertKind::Caution => "CAUTION",
395 }
396}
397
398fn escape_alert_title(input: &str) -> String {
399 let mut output = String::new();
400 for char in input.chars() {
401 match char {
402 '\n' | '\r' => output.push(' '),
403 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
404 _ => output.push(char),
405 }
406 }
407 output
408}
409
410fn serialize_list(node: &List, options: &SerializeOptions) -> Result<String, SerializeError> {
411 serialize_list_with_marker_spacing(node, options, "", " ")
412}
413
414fn serialize_list_with_marker_spacing(
415 node: &List,
416 options: &SerializeOptions,
417 marker_prefix: &str,
418 marker_padding: &str,
419) -> Result<String, SerializeError> {
420 let mut output = String::new();
421 for (index, item) in node.children.iter().enumerate() {
422 if index > 0 {
423 if node.tight {
424 output.push('\n');
425 } else {
426 output.push_str("\n\n");
427 }
428 }
429 let list_delimiter = if node.ordered {
430 if options.ordered_delimiter == SerializeOptions::default().ordered_delimiter {
431 node.delimiter
432 } else {
433 options.ordered_delimiter
434 }
435 } else if options.bullet == SerializeOptions::default().bullet {
436 node.delimiter
437 } else {
438 options.bullet
439 };
440 let marker = if node.ordered {
441 let start = node.start.unwrap_or(1).saturating_add(index as u64);
442 let delimiter = ordered_list_marker(list_delimiter);
443 format!("{marker_prefix}{start}{delimiter}{marker_padding}")
444 } else {
445 format!(
446 "{marker_prefix}{}{marker_padding}",
447 unordered_list_marker(list_delimiter)
448 )
449 };
450 let mut inner = serialize_item_blocks(&item.children, options, node.tight)?;
451 if !node.ordered && unordered_list_marker(list_delimiter) == '*' {
452 inner = disambiguate_asterisk_list_item(inner);
453 }
454 if let Some(checked) = item.checked {
455 if let Some(rest) = inner.strip_prefix("- ") {
456 inner = rest.into();
457 }
458 let checkbox = if checked { "[x] " } else { "[ ] " };
459 inner = format!("{checkbox}{inner}");
460 }
461 if !node.tight
462 && node.children.len() == 1
463 && matches!(item.children.as_slice(), [Block::Paragraph(_)])
464 && !inner.is_empty()
465 {
466 output.push_str(marker.trim_end());
467 output.push_str("\n\n");
468 output.push_str(&prefix_lines(&inner, &" ".repeat(marker.len())));
469 continue;
470 }
471 output.push_str(&marker);
472 output.push_str(&indent_after_first_line(&inner, marker.len()));
473 }
474 Ok(output)
475}
476
477fn disambiguate_asterisk_list_item(inner: String) -> String {
478 let first_line_end = inner.find('\n').unwrap_or(inner.len());
479 let first_line = &inner[..first_line_end];
480 if !asterisk_bullet_first_line_is_thematic_break(first_line) {
481 return inner;
482 }
483 let mut output = String::from("---");
484 output.push_str(&inner[first_line_end..]);
485 output
486}
487
488fn asterisk_bullet_first_line_is_thematic_break(first_line: &str) -> bool {
495 first_line.len() >= 2 && first_line.bytes().all(|byte| byte == b'*')
496}
497
498fn serialize_item_blocks(
499 blocks: &[Block],
500 options: &SerializeOptions,
501 tight: bool,
502) -> Result<String, SerializeError> {
503 let mut output = String::new();
504 for (index, block) in blocks.iter().enumerate() {
505 if index > 0 {
506 if tight {
507 output.push('\n');
508 } else {
509 output.push_str("\n\n");
510 }
511 }
512 output.push_str(&serialize_block(block, options, false)?);
513 }
514 Ok(output)
515}
516
517fn serialize_description_list(
518 node: &DescriptionList,
519 options: &SerializeOptions,
520) -> Result<String, SerializeError> {
521 let mut output = String::new();
522 for (item_index, item) in node.children.iter().enumerate() {
523 if item_index > 0 {
524 output.push_str(if node.tight { "\n" } else { "\n\n" });
525 }
526 output.push_str(&serialize_inlines(&item.term, options)?);
527 for (detail_index, detail) in item.details.iter().enumerate() {
528 if node.tight && detail.children.len() == 1 {
529 if let Block::Paragraph(paragraph) = &detail.children[0] {
530 output.push('\n');
531 output.push_str(": ");
532 output.push_str(&serialize_inlines(¶graph.children, options)?);
533 continue;
534 }
535 }
536 if !node.tight && detail_index == 0 {
542 output.push('\n');
543 }
544 output.push_str("\n:");
545 let inner = serialize_blocks_at_start(&detail.children, options, false)?;
546 if !inner.is_empty() {
547 output.push('\n');
548 output.push_str(&indent_lines(&inner, 4));
549 }
550 }
551 }
552 Ok(output)
553}
554
555fn serialize_code_block(
556 node: &CodeBlock,
557 options: &SerializeOptions,
558) -> Result<String, SerializeError> {
559 match node.kind {
560 CodeBlockKind::Indented => Ok(prefix_lines(trim_trailing_newline(&node.value), " ")),
561 CodeBlockKind::Fenced { marker, length } => {
562 let marker = code_block_fence_marker(node, marker, options);
563 let fence = fence_for(&node.value, marker, length.max(3));
564 let mut opener = fence.clone();
565 if let Some(info) = &node.info {
566 opener.push(' ');
567 opener.push_str(&escape_code_info(info));
568 }
569 let mut output = opener;
570 output.push('\n');
571 output.push_str(&node.value);
572 if !ends_with_line_ending(&node.value) {
573 output.push('\n');
574 }
575 output.push_str(&fence);
576 Ok(output)
577 }
578 }
579}
580
581fn code_block_fence_marker(
582 node: &CodeBlock,
583 marker: FenceMarker,
584 options: &SerializeOptions,
585) -> FenceMarker {
586 if node.info.as_deref().is_some_and(|info| info.contains('`')) {
587 return FenceMarker::Tilde;
588 }
589 if options.fence_marker == SerializeOptions::default().fence_marker {
590 marker
591 } else {
592 options.fence_marker
593 }
594}
595
596fn escape_code_info(input: &str) -> String {
597 let mut output = String::new();
598 for char in input.chars() {
599 match char {
600 '\n' => output.push_str("
"),
601 '\r' => output.push_str("
"),
602 '\t' => output.push(char),
603 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
604 '\\' | '&' => {
605 output.push('\\');
606 output.push(char);
607 }
608 _ => output.push(char),
609 }
610 }
611 output
612}
613
614fn serialize_table(node: &Table, options: &SerializeOptions) -> Result<String, SerializeError> {
615 let header = &node.rows[0];
616 let mut output = serialize_table_row(header, options)?;
617 output.push('\n');
618 output.push('|');
619 output.push(' ');
620 output.push_str(
621 &node
622 .alignments
623 .iter()
624 .map(|alignment| match alignment {
625 TableAlignment::None => "---",
626 TableAlignment::Left => ":---",
627 TableAlignment::Center => ":---:",
628 TableAlignment::Right => "---:",
629 })
630 .collect::<Vec<_>>()
631 .join(" | "),
632 );
633 output.push(' ');
634 output.push('|');
635 for row in node.rows.iter().skip(1) {
636 output.push('\n');
637 output.push_str(&serialize_table_row(row, options)?);
638 }
639 Ok(output)
640}
641
642fn serialize_table_row(
643 row: &TableRow,
644 options: &SerializeOptions,
645) -> Result<String, SerializeError> {
646 let mut cells = Vec::new();
647 for cell in &row.cells {
648 let cell = serialize_inlines_with_context(
649 &cell.children,
650 options,
651 InlineSerializeContext::table_cell(),
652 )?;
653 if table_cell_has_unescaped_pipe(&cell) {
654 return Err(SerializeError::UnsupportedNode(
655 "table cell inline contains a pipe that cannot be escaped without changing source",
656 ));
657 }
658 cells.push(cell);
659 }
660 Ok(format!("| {} |", cells.join(" | ")))
661}
662
663#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
664struct InlineSerializeContext {
665 table_cell: bool,
666 avoid_star_edges: bool,
667}
668
669impl InlineSerializeContext {
670 const fn table_cell() -> Self {
671 Self {
672 table_cell: true,
673 avoid_star_edges: false,
674 }
675 }
676
677 const fn avoiding_star_edges(self) -> Self {
678 Self {
679 table_cell: self.table_cell,
680 avoid_star_edges: true,
681 }
682 }
683}
684
685fn serialize_inlines(
686 inlines: &[Inline],
687 options: &SerializeOptions,
688) -> Result<String, SerializeError> {
689 serialize_inlines_with_context(inlines, options, InlineSerializeContext::default())
690}
691
692fn escape_trailing_bang(output: &mut String) {
697 if output.ends_with('!') && !output.ends_with("\\!") {
698 output.pop();
699 output.push_str("\\!");
700 }
701}
702
703fn escape_trailing_less_than(output: &mut String) {
709 if output.ends_with('<') && !output.ends_with("\\<") {
710 output.pop();
711 output.push_str("\\<");
712 }
713}
714
715fn escape_trailing_email_local(output: &mut String) {
722 let Some(last) = output.chars().next_back() else {
723 return;
724 };
725 if !last.is_ascii_alphanumeric() {
730 return;
731 }
732 output.pop();
733 output.push_str(&alloc::format!("&#{};", last as u32));
734}
735
736fn is_gfm_literal_autolink(inline: &Inline) -> bool {
739 matches!(
740 inline,
741 Inline::Autolink(node) if matches!(node.kind, AutolinkKind::GfmLiteral { .. })
742 )
743}
744
745fn is_gfm_literal_email(inline: &Inline) -> bool {
746 matches!(
747 inline,
748 Inline::Autolink(node)
749 if matches!(&node.kind, AutolinkKind::GfmLiteral { original }
750 if node.destination.strip_prefix("mailto:") == Some(original.as_str()))
751 )
752}
753
754fn encode_leading_char_after_autolink(value: &str) -> Option<(String, &str)> {
762 let first = value.chars().next()?;
763 if first.is_ascii() {
764 return None;
767 }
768 let encoded = alloc::format!("&#x{:X};", first as u32);
769 Some((encoded, &value[first.len_utf8()..]))
770}
771
772fn serialize_inlines_with_context(
773 inlines: &[Inline],
774 options: &SerializeOptions,
775 context: InlineSerializeContext,
776) -> Result<String, SerializeError> {
777 let mut output = String::new();
778 let mut output_line = OutputLine::default();
779 for (index, inline) in inlines.iter().enumerate() {
780 match inline {
781 Inline::Text(node) => {
782 let after_literal_autolink = index
783 .checked_sub(1)
784 .is_some_and(|prev| is_gfm_literal_autolink(&inlines[prev]));
785 let before_literal_autolink =
786 inlines.get(index + 1).is_some_and(is_gfm_literal_autolink);
787
788 let (lead, body) = match after_literal_autolink
791 .then(|| encode_leading_char_after_autolink(&node.value))
792 .flatten()
793 {
794 Some((encoded, rest)) => (encoded, rest),
795 None => (String::new(), node.value.as_str()),
796 };
797
798 let (escape_body, trailing_ws) = if before_literal_autolink {
805 let head = body.trim_end_matches([' ', '\t']);
806 (head, &body[head.len()..])
807 } else {
808 (body, "")
809 };
810
811 output.push_str(&lead);
812 output.push_str(&escape_text_with_context(
813 escape_body,
814 lead.is_empty()
815 && trailing_ws.len() != body.len()
816 && output_line.len(&output) == 0,
817 trailing_ws.is_empty() && text_is_at_line_end(inlines, index),
818 context,
819 ));
820 output.push_str(trailing_ws);
821 }
822 Inline::Escape(node) => {
823 output.push('\\');
824 output.push(node.value);
825 }
826 Inline::CharacterReference(node) => output.push_str(&node.reference),
827 Inline::Emphasis(node) => {
828 let children = serialize_inlines_with_context(&node.children, options, context)?;
829 let touches_underscore = children.starts_with('_')
830 || children.ends_with('_')
831 || children.starts_with("\\_")
832 || children.ends_with("\\_");
833 let abuts_star = output.ends_with('*') && !touches_underscore;
838 let prefer_underscore = (context.avoid_star_edges && !touches_underscore)
839 || abuts_star
840 || children.starts_with('*')
841 || children.ends_with('*');
842 let delimiter = if prefer_underscore { '_' } else { '*' };
843 let children = if delimiter == '*' {
844 serialize_inlines_with_context(
845 &node.children,
846 options,
847 context.avoiding_star_edges(),
848 )?
849 } else {
850 children
851 };
852 output.push(delimiter);
853 output.push_str(&children);
854 output.push(delimiter);
855 }
856 Inline::Strong(node) => {
857 let children = serialize_inlines_with_context(
858 &node.children,
859 options,
860 context.avoiding_star_edges(),
861 )?;
862 output.push_str("**");
868 output.push_str(&children);
869 output.push_str("**");
870 }
871 Inline::Underline(node) => {
872 output.push_str("__");
873 output.push_str(&serialize_inlines_with_context(
874 &node.children,
875 options,
876 context,
877 )?);
878 output.push_str("__");
879 }
880 Inline::Delete(node) => {
881 let children = serialize_inlines_with_context(&node.children, options, context)?;
882 let marker = match node.marker {
883 DeleteMarker::SingleTilde => "~",
884 DeleteMarker::DoubleTilde => "~~",
885 };
886 output.push_str(marker);
887 output.push_str(&children);
888 output.push_str(marker);
889 }
890 Inline::Insert(node) => {
891 output.push_str("++");
892 output.push_str(&serialize_inlines_with_context(
893 &node.children,
894 options,
895 context,
896 )?);
897 output.push_str("++");
898 }
899 Inline::Mark(node) => {
900 output.push_str("==");
901 output.push_str(&serialize_inlines_with_context(
902 &node.children,
903 options,
904 context,
905 )?);
906 output.push_str("==");
907 }
908 Inline::Subscript(node) => {
909 output.push('~');
910 output.push_str(&serialize_inlines_with_context(
911 &node.children,
912 options,
913 context,
914 )?);
915 output.push('~');
916 }
917 Inline::Superscript(node) => {
918 output.push('^');
919 output.push_str(&serialize_inlines_with_context(
920 &node.children,
921 options,
922 context,
923 )?);
924 output.push('^');
925 }
926 Inline::Spoiler(node) => {
927 output.push_str("||");
928 output.push_str(&serialize_inlines_with_context(
929 &node.children,
930 options,
931 context,
932 )?);
933 output.push_str("||");
934 }
935 Inline::Shortcode(node) => {
936 output.push(':');
937 output.push_str(&node.name);
938 output.push(':');
939 }
940 Inline::Code(node) => {
941 if node.fence_length > 0 && !node.raw.is_empty() {
942 let fence = "`".repeat(node.fence_length);
943 let raw = if context.table_cell {
944 table_cell_escape_code_pipes(&node.raw)
945 } else {
946 node.raw.clone()
947 };
948 output.push_str(&fence);
949 output.push_str(&raw);
950 output.push_str(&fence);
951 continue;
952 }
953 if node.value.is_empty() {
954 output.push_str("`` ``");
955 continue;
956 }
957 let value = if context.table_cell {
958 table_cell_escape_code_pipes(&node.value)
959 } else {
960 node.value.clone()
961 };
962 let fence = inline_code_fence(&value);
963 output.push_str(&fence);
964 if code_span_needs_padding(&value) {
965 output.push(' ');
966 output.push_str(&value);
967 output.push(' ');
968 } else {
969 output.push_str(&value);
970 }
971 output.push_str(&fence);
972 }
973 Inline::Link(node) => {
974 escape_trailing_bang(&mut output);
975 output.push('[');
976 output.push_str(&serialize_inlines_with_context(
977 &node.children,
978 options,
979 context,
980 )?);
981 output.push_str("](");
982 output.push_str(&serialize_destination_kind(
983 &node.destination,
984 node.destination_kind,
985 context,
986 ));
987 if let (Some(title), Some(title_kind)) = (&node.title, node.title_kind) {
988 output.push(' ');
989 output.push_str(&serialize_title_kind(title, title_kind, context));
990 }
991 output.push(')');
992 }
993 Inline::Image(node) => {
994 output.push_str(";
999 output.push_str(&serialize_destination_kind(
1000 &node.destination,
1001 node.destination_kind,
1002 context,
1003 ));
1004 if let (Some(title), Some(title_kind)) = (&node.title, node.title_kind) {
1005 output.push(' ');
1006 output.push_str(&serialize_title_kind(title, title_kind, context));
1007 }
1008 output.push(')');
1009 }
1010 Inline::LinkReference(node) => {
1011 let children = serialize_inlines_with_context(&node.children, options, context)?;
1012 let children_identifier = normalize_reference_label(&children);
1013 escape_trailing_bang(&mut output);
1014 push_reference_body(
1015 &mut output,
1016 node.kind,
1017 &children,
1018 children_identifier == node.identifier,
1019 &reference_explicit_label(node.meta.span.is_some(), &node.label, context),
1020 );
1021 }
1022 Inline::ImageReference(node) => {
1023 let alt = serialize_inlines_with_context(&node.alt, options, context)?;
1024 let alt_identifier = normalize_reference_label(&alt);
1025 output.push('!');
1026 push_reference_body(
1027 &mut output,
1028 node.kind,
1029 &alt,
1030 alt_identifier == node.identifier,
1031 &reference_explicit_label(node.meta.span.is_some(), &node.label, context),
1032 );
1033 }
1034 Inline::Autolink(node) => match &node.kind {
1035 AutolinkKind::Angle => {
1036 output.push('<');
1037 output.push_str(&node.destination);
1038 output.push('>');
1039 }
1040 AutolinkKind::GfmLiteral { original } => {
1044 let is_bare_email = node.destination == alloc::format!("mailto:{original}");
1048 let follows_literal_email_plus = original.starts_with('+')
1049 && index
1050 .checked_sub(1)
1051 .is_some_and(|prev| is_gfm_literal_email(&inlines[prev]));
1052 if is_bare_email && !follows_literal_email_plus {
1053 escape_trailing_email_local(&mut output);
1054 } else {
1055 escape_trailing_less_than(&mut output);
1056 }
1057 output.push_str(original);
1058 }
1059 },
1060 Inline::Html(node) => output.push_str(&node.value),
1061 Inline::SoftBreak(_) => output.push('\n'),
1062 Inline::LineBreak(node) => match node.kind {
1063 LineBreakKind::Backslash => output.push_str("\\\n"),
1064 LineBreakKind::Spaces => output.push_str(" \n"),
1065 },
1066 Inline::Math(node) => {
1067 output.push_str(&serialize_inline_math_with_context(node, context)?);
1068 }
1069 Inline::FootnoteReference(node) => {
1070 escape_trailing_bang(&mut output);
1071 output.push_str("[^");
1072 if node.meta.span.is_some() {
1073 output.push_str(&escape_footnote_label_source(&node.label));
1074 } else {
1075 output.push_str(&escape_footnote_label_semantic(&node.label));
1076 }
1077 output.push(']');
1078 }
1079 Inline::InlineFootnote(node) => {
1080 output.push_str("^[");
1081 output.push_str(&serialize_inlines_with_context(
1082 &node.children,
1083 options,
1084 context,
1085 )?);
1086 output.push(']');
1087 }
1088 Inline::WikiLink(node) => {
1089 output.push_str("[[");
1090 let target = escape_wikilink_part(&node.target);
1091 let label = escape_wikilink_part(&node.label);
1092 if node.target == node.label {
1093 output.push_str(&target);
1094 } else {
1095 match node.label_order {
1096 WikiLinkLabelOrder::AfterPipe => {
1097 output.push_str(&target);
1098 output.push('|');
1099 output.push_str(&label);
1100 }
1101 WikiLinkLabelOrder::BeforePipe => {
1102 output.push_str(&label);
1103 output.push('|');
1104 output.push_str(&target);
1105 }
1106 }
1107 }
1108 output.push_str("]]");
1109 }
1110 Inline::MdxExpression(node) => {
1111 output.push('{');
1112 output.push_str(&node.value);
1113 output.push('}');
1114 }
1115 Inline::MdxJsx(node) => output.push_str(&node.value),
1116 Inline::TextDirective(node) => {
1117 output.push(':');
1118 output.push_str(&node.name);
1119 output.push_str(&serialize_directive_label_with_context(
1120 &node.label,
1121 options,
1122 context,
1123 )?);
1124 output.push_str(&serialize_attributes_with_context(
1125 &node.attributes,
1126 context,
1127 ));
1128 }
1129 }
1130 }
1131 Ok(output)
1132}
1133
1134fn serialize_directive_label(
1135 label: &[Inline],
1136 options: &SerializeOptions,
1137) -> Result<String, SerializeError> {
1138 serialize_directive_label_with_context(label, options, InlineSerializeContext::default())
1139}
1140
1141fn serialize_directive_label_with_context(
1142 label: &[Inline],
1143 options: &SerializeOptions,
1144 context: InlineSerializeContext,
1145) -> Result<String, SerializeError> {
1146 if label.is_empty() {
1147 Ok(String::new())
1148 } else {
1149 Ok(format!(
1150 "[{}]",
1151 serialize_inlines_with_context(label, options, context)?
1152 ))
1153 }
1154}
1155
1156fn serialize_attributes(attributes: &[DirectiveAttribute]) -> String {
1157 serialize_attributes_with_context(attributes, InlineSerializeContext::default())
1158}
1159
1160fn serialize_attributes_with_context(
1161 attributes: &[DirectiveAttribute],
1162 context: InlineSerializeContext,
1163) -> String {
1164 if attributes.is_empty() {
1165 return String::new();
1166 }
1167 let mut output = String::from("{");
1168 for (index, attribute) in attributes.iter().enumerate() {
1169 if index > 0 {
1170 output.push(' ');
1171 }
1172 match (&*attribute.name, &attribute.value) {
1173 ("id", Some(value)) if is_directive_shorthand_value(value) => {
1174 output.push('#');
1175 output.push_str(value);
1176 }
1177 ("class", Some(value)) if is_directive_shorthand_value(value) => {
1178 output.push('.');
1179 output.push_str(value);
1180 }
1181 (_, Some(value)) => {
1182 output.push_str(&attribute.name);
1183 output.push('=');
1184 output.push('"');
1185 output.push_str(&escape_title_with_context(
1186 value,
1187 LinkTitleKind::DoubleQuote,
1188 context,
1189 ));
1190 output.push('"');
1191 }
1192 (_, None) => output.push_str(&attribute.name),
1193 }
1194 }
1195 output.push('}');
1196 output
1197}
1198
1199fn is_directive_shorthand_value(input: &str) -> bool {
1200 !input.is_empty()
1201 && input
1202 .chars()
1203 .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1204}
1205
1206fn text_is_at_line_end(inlines: &[Inline], index: usize) -> bool {
1207 matches!(
1208 inlines.get(index + 1),
1209 None | Some(Inline::SoftBreak(_)) | Some(Inline::LineBreak(_))
1210 )
1211}
1212
1213#[cfg(test)]
1214mod escape_scan_tests;
1215
1216const ATTENTION_MARKERS: [&str; 5] = ["*", "_", "++", "==", "~~"];
1218
1219struct TextScan<'a> {
1224 input: &'a str,
1225 marker_starts: [Positions; ATTENTION_MARKERS.len()],
1226 marker_closers: [PathMemo; ATTENTION_MARKERS.len()],
1227 last_occurrences: Vec<(&'static str, Option<usize>)>,
1228 backtick_runs: Option<SameCharRuns>,
1229 dollar_runs: Option<SameCharRuns>,
1230 current_run: Option<(u8, usize, usize)>,
1232}
1233
1234impl<'a> TextScan<'a> {
1235 fn new(input: &'a str) -> Self {
1236 Self {
1237 input,
1238 marker_starts: Default::default(),
1239 marker_closers: Default::default(),
1240 last_occurrences: Vec::new(),
1241 backtick_runs: None,
1242 dollar_runs: None,
1243 current_run: None,
1244 }
1245 }
1246
1247 fn run_len_from(&mut self, needle: u8, offset: usize) -> usize {
1250 if let Some((byte, start, end)) = self.current_run {
1251 if byte == needle && start <= offset && offset <= end {
1252 return end - offset;
1253 }
1254 }
1255 let end = offset
1256 + self.input.as_bytes()[offset..]
1257 .iter()
1258 .take_while(|byte| **byte == needle)
1259 .count();
1260 self.current_run = Some((needle, offset, end));
1261 end - offset
1262 }
1263
1264 fn occurs_from(&mut self, pattern: &'static str, from: usize) -> bool {
1266 let last = match self
1267 .last_occurrences
1268 .iter()
1269 .find(|(cached, _)| *cached == pattern)
1270 {
1271 Some((_, last)) => *last,
1272 None => {
1273 let last = self.input.rfind(pattern);
1274 self.last_occurrences.push((pattern, last));
1275 last
1276 }
1277 };
1278 last.is_some_and(|last| last >= from)
1279 }
1280
1281 fn attention_closer_follows(&mut self, marker: &str, from: usize, underscore: bool) -> bool {
1284 let input = self.input;
1285 let slot = ATTENTION_MARKERS
1286 .iter()
1287 .position(|known| *known == marker)
1288 .expect("attention marker");
1289 let marker = ATTENTION_MARKERS[slot];
1290 let starts = &mut self.marker_starts[slot];
1291 self.marker_closers[slot]
1292 .resolve(input.len() + 1, from, |cursor| {
1293 let Some(candidate) =
1294 starts.first_at_or_after(cursor, || pattern_starts(input, marker))
1295 else {
1296 return Step::Done(None);
1297 };
1298 if !input[candidate + marker.len()..].starts_with(marker)
1299 && text_delimiter_can_close(input, candidate, marker.len(), underscore)
1300 {
1301 Step::Done(Some(candidate))
1302 } else {
1303 Step::Next(candidate + marker.len())
1304 }
1305 })
1306 .is_some()
1307 }
1308
1309 fn exact_run_follows(&mut self, needle: u8, from: usize, run_len: usize) -> bool {
1312 let input = self.input;
1313 let runs = match needle {
1314 b'`' => &mut self.backtick_runs,
1315 _ => &mut self.dollar_runs,
1316 }
1317 .get_or_insert_with(|| SameCharRuns::new(input, needle));
1318 runs.has_run_ending_at_or_after(from + run_len, run_len)
1319 }
1320}
1321
1322struct SameCharRuns {
1325 ends: Vec<usize>,
1326 longest_from: Vec<usize>,
1327}
1328
1329impl SameCharRuns {
1330 fn new(input: &str, needle: u8) -> Self {
1331 let bytes = input.as_bytes();
1332 let mut ends = Vec::new();
1333 let mut lens = Vec::new();
1334 let mut index = 0;
1335 while index < bytes.len() {
1336 if bytes[index] == needle {
1337 let start = index;
1338 while bytes.get(index) == Some(&needle) {
1339 index += 1;
1340 }
1341 ends.push(index);
1342 lens.push(index - start);
1343 } else {
1344 index += 1;
1345 }
1346 }
1347 let mut longest_from = lens;
1348 for index in (0..longest_from.len().saturating_sub(1)).rev() {
1349 longest_from[index] = longest_from[index].max(longest_from[index + 1]);
1350 }
1351 Self { ends, longest_from }
1352 }
1353
1354 fn has_run_ending_at_or_after(&self, min_end: usize, run_len: usize) -> bool {
1356 let first = self.ends.partition_point(|end| *end < min_end);
1357 self.longest_from
1358 .get(first)
1359 .is_some_and(|longest| *longest >= run_len)
1360 }
1361}
1362
1363fn escape_text_with_context(
1364 input: &str,
1365 preserve_leading: bool,
1366 preserve_trailing: bool,
1367 context: InlineSerializeContext,
1368) -> String {
1369 let avoid_star_edges = context.avoid_star_edges;
1370 let mut output = String::new();
1371 let mut output_line = OutputLine::default();
1372 let mut scan = TextScan::new(input);
1373 let mut line_digit_prefix = 0usize;
1374 let trailing_start = if preserve_trailing {
1375 input
1376 .trim_end_matches(|char| matches!(char, ' ' | '\t'))
1377 .len()
1378 } else {
1379 input.len()
1380 };
1381 let mut chars = input.char_indices().peekable();
1382 let mut at_leading_edge = preserve_leading;
1383 while let Some((offset, char)) = chars.next() {
1384 if char == '\n' {
1385 output.push_str("
");
1386 at_leading_edge = false;
1387 continue;
1388 }
1389 if char == '\r' {
1390 output.push_str("
");
1391 at_leading_edge = false;
1392 continue;
1393 }
1394 if (at_leading_edge || offset >= trailing_start) && char == ' ' {
1395 output.push_str(" ");
1396 continue;
1397 }
1398 if (at_leading_edge || offset >= trailing_start) && char == '\t' {
1399 output.push_str("	");
1400 continue;
1401 }
1402 if char.is_control() {
1403 output.push_str(&format!("&#x{:X};", char as u32));
1404 at_leading_edge = false;
1405 continue;
1406 }
1407 at_leading_edge = false;
1408 if line_digit_prefix == output_line.len(&output) && char.is_ascii_digit() {
1409 output.push(char);
1410 line_digit_prefix += 1;
1411 continue;
1412 }
1413 if char == ':'
1414 && (input[..offset].ends_with("http") || input[..offset].ends_with("https"))
1415 && input[offset + char.len_utf8()..].starts_with("//")
1416 {
1417 output.push('\\');
1418 output.push(char);
1419 line_digit_prefix = usize::MAX;
1420 continue;
1421 }
1422 if char == '.' && input[..offset].ends_with("www") {
1423 output.push('\\');
1424 output.push(char);
1425 line_digit_prefix = usize::MAX;
1426 continue;
1427 }
1428 if char == '@' {
1429 if at_sign_can_start_email_autolink(input, offset) {
1430 output.push_str("@");
1431 } else {
1432 output.push(char);
1433 }
1434 line_digit_prefix = usize::MAX;
1435 continue;
1436 }
1437 if line_digit_prefix != usize::MAX
1438 && line_digit_prefix > 0
1439 && matches!(char, '.' | ')')
1440 && chars
1441 .peek()
1442 .map(|(_, next)| next.is_whitespace())
1443 .unwrap_or(true)
1444 {
1445 output.push('\\');
1446 output.push(char);
1447 line_digit_prefix = usize::MAX;
1448 continue;
1449 }
1450 if output_line.len(&output) == 0
1451 && matches!(char, '-' | '+')
1452 && chars
1453 .peek()
1454 .map(|(_, next)| next.is_whitespace())
1455 .unwrap_or(true)
1456 {
1457 output.push('\\');
1458 output.push(char);
1459 line_digit_prefix = usize::MAX;
1460 continue;
1461 }
1462 if output_line.len(&output) == 0
1463 && ((char == '-' && chars.peek().is_some_and(|(_, next)| *next == '-')) || char == '=')
1464 {
1465 output.push('\\');
1466 output.push(char);
1467 line_digit_prefix = usize::MAX;
1468 continue;
1469 }
1470 line_digit_prefix = usize::MAX;
1471 match char {
1472 '*' if avoid_star_edges => output.push_str("*"),
1473 '|' if context.table_cell => output.push_str("|"),
1474 '|' if output_line.len(&output) == 0 => {
1475 output.push('\\');
1476 output.push(char);
1477 }
1478 '`' if text_code_span_can_start(input, offset, &mut scan) => {
1479 output.push('\\');
1480 output.push(char);
1481 }
1482 '*' if text_attention_delimiter_can_start(input, offset, "*", false, &mut scan) => {
1483 output.push('\\');
1484 output.push(char);
1485 }
1486 '_' if text_attention_delimiter_can_start(input, offset, "_", true, &mut scan) => {
1487 output.push('\\');
1488 output.push(char);
1489 }
1490 '<' if text_less_than_can_start_inline(input, offset, &mut scan) => {
1491 output.push('\\');
1492 output.push(char);
1493 }
1494 '>' if output_line.len(&output) == 0 => {
1495 output.push('\\');
1496 output.push(char);
1497 }
1498 '{' if scan.occurs_from("}", offset + char.len_utf8()) => {
1499 output.push('\\');
1500 output.push(char);
1501 }
1502 '#' if text_atx_heading_can_start(input, offset, output_line.len(&output)) => {
1503 output.push('\\');
1504 output.push(char);
1505 }
1506 '|' if text_spoiler_can_start(input, offset, &mut scan) => output.push_str("|"),
1507 '$' if text_math_can_start(input, offset, &mut scan) => {
1508 output.push('\\');
1509 output.push(char);
1510 }
1511 '!' if input[offset + char.len_utf8()..].starts_with('[') => {
1512 output.push('\\');
1513 output.push(char);
1514 }
1515 '~' if text_tilde_can_start(input, offset, &mut scan) => {
1516 output.push('\\');
1517 output.push(char);
1518 }
1519 '^' if text_caret_can_start(input, offset, &mut scan) => {
1520 output.push('\\');
1521 output.push(char);
1522 }
1523 '+' if text_attention_delimiter_can_start(input, offset, "++", false, &mut scan) => {
1524 output.push('\\');
1525 output.push(char);
1526 }
1527 '=' if text_attention_delimiter_can_start(input, offset, "==", false, &mut scan) => {
1528 output.push('\\');
1529 output.push(char);
1530 }
1531 '&' if text_character_reference_can_start(input, offset) => {
1532 output.push('\\');
1533 output.push(char);
1534 }
1535 '\\' | '[' | ']' => {
1536 output.push('\\');
1537 output.push(char);
1538 }
1539 _ => output.push(char),
1540 }
1541 }
1542 output
1543}
1544
1545fn text_code_span_can_start(input: &str, offset: usize, scan: &mut TextScan) -> bool {
1546 let marker_len = scan.run_len_from(b'`', offset);
1547 if marker_len == 0 || text_char_at_edge(input, offset, marker_len) {
1548 return true;
1549 }
1550 scan.exact_run_follows(b'`', offset + marker_len, marker_len)
1551}
1552
1553fn text_attention_delimiter_can_start(
1554 input: &str,
1555 offset: usize,
1556 marker: &str,
1557 underscore: bool,
1558 scan: &mut TextScan,
1559) -> bool {
1560 if !input[offset..].starts_with(marker) {
1561 return false;
1562 }
1563 if input[offset + marker.len()..].starts_with(marker)
1564 || text_char_at_edge(input, offset, marker.len())
1565 {
1566 return true;
1567 }
1568 if !text_delimiter_can_open(input, offset, marker.len(), underscore) {
1569 return false;
1570 }
1571
1572 scan.attention_closer_follows(marker, offset + marker.len(), underscore)
1573}
1574
1575fn text_delimiter_can_open(
1576 input: &str,
1577 offset: usize,
1578 marker_len: usize,
1579 underscore: bool,
1580) -> bool {
1581 let flanking = text_delimiter_flanking(input, offset, marker_len);
1582 if underscore {
1583 flanking.left
1584 && (!flanking.right
1585 || flanking
1586 .previous
1587 .is_some_and(|char| char.is_ascii_punctuation()))
1588 } else {
1589 flanking.left
1590 }
1591}
1592
1593fn text_delimiter_can_close(
1594 input: &str,
1595 offset: usize,
1596 marker_len: usize,
1597 underscore: bool,
1598) -> bool {
1599 let flanking = text_delimiter_flanking(input, offset, marker_len);
1600 if underscore {
1601 flanking.right
1602 && (!flanking.left
1603 || flanking
1604 .next
1605 .is_some_and(|char| char.is_ascii_punctuation()))
1606 } else {
1607 flanking.right
1608 }
1609}
1610
1611#[derive(Clone, Copy)]
1612struct TextDelimiterFlanking {
1613 left: bool,
1614 right: bool,
1615 previous: Option<char>,
1616 next: Option<char>,
1617}
1618
1619fn text_delimiter_flanking(input: &str, offset: usize, marker_len: usize) -> TextDelimiterFlanking {
1620 let previous = input[..offset].chars().next_back();
1621 let next = input[offset + marker_len..].chars().next();
1622
1623 let previous_whitespace = previous.is_none_or(char::is_whitespace);
1624 let next_whitespace = next.is_none_or(char::is_whitespace);
1625 let previous_punctuation = previous.is_some_and(|char| char.is_ascii_punctuation());
1626 let next_punctuation = next.is_some_and(|char| char.is_ascii_punctuation());
1627
1628 let left = next.is_some()
1629 && !next_whitespace
1630 && !(next_punctuation && !previous_whitespace && !previous_punctuation);
1631 let right = previous.is_some()
1632 && !previous_whitespace
1633 && !(previous_punctuation && !next_whitespace && !next_punctuation);
1634
1635 TextDelimiterFlanking {
1636 left,
1637 right,
1638 previous,
1639 next,
1640 }
1641}
1642
1643fn text_less_than_can_start_inline(input: &str, offset: usize, scan: &mut TextScan) -> bool {
1644 let after_offset = offset + '<'.len_utf8();
1645 let after = &input[after_offset..];
1646 if scan.occurs_from(">", after_offset) {
1647 let next = after.chars().next();
1648 return next.is_some_and(|char| {
1649 char.is_ascii_alphabetic() || matches!(char, '/' | '!' | '?' | '_')
1650 }) || after.starts_with("http://")
1651 || after.starts_with("https://")
1652 || scan.occurs_from("@", after_offset);
1653 }
1654 false
1655}
1656
1657fn text_atx_heading_can_start(input: &str, offset: usize, output_line_len: usize) -> bool {
1658 if output_line_len != 0 {
1659 return false;
1660 }
1661 let hashes = same_char_run_len(input, offset, '#');
1662 (1..=6).contains(&hashes)
1663 && input[offset + hashes..]
1664 .chars()
1665 .next()
1666 .is_none_or(char::is_whitespace)
1667}
1668
1669fn text_spoiler_can_start(input: &str, offset: usize, scan: &mut TextScan) -> bool {
1670 input[offset..].starts_with("||")
1671 && !input[offset + "||".len()..].starts_with('|')
1672 && scan.occurs_from("||", offset + "||".len())
1673}
1674
1675fn text_math_can_start(input: &str, offset: usize, scan: &mut TextScan) -> bool {
1676 let marker_len = scan.run_len_from(b'$', offset);
1681 if marker_len == 0 || text_char_at_edge(input, offset, marker_len) {
1682 return true;
1683 }
1684 scan.exact_run_follows(b'$', offset + marker_len, marker_len)
1685}
1686
1687fn text_tilde_can_start(input: &str, offset: usize, scan: &mut TextScan) -> bool {
1688 if input[offset..].starts_with("~~") {
1689 return text_attention_delimiter_can_start(input, offset, "~~", false, scan)
1690 || text_simple_delimiter_can_start(input, offset, "~", scan);
1691 }
1692 text_simple_delimiter_can_start(input, offset, "~", scan)
1693}
1694
1695fn text_caret_can_start(input: &str, offset: usize, scan: &mut TextScan) -> bool {
1696 input[offset + '^'.len_utf8()..].starts_with('[')
1697 || text_simple_delimiter_can_start(input, offset, "^", scan)
1698}
1699
1700fn text_simple_delimiter_can_start(
1701 input: &str,
1702 offset: usize,
1703 marker: &'static str,
1704 scan: &mut TextScan,
1705) -> bool {
1706 if text_char_at_edge(input, offset, marker.len())
1707 || input[offset + marker.len()..].starts_with(marker)
1708 || input[..offset].ends_with(marker)
1709 {
1710 return true;
1711 }
1712 scan.occurs_from(marker, offset + marker.len())
1713}
1714
1715fn text_character_reference_can_start(input: &str, offset: usize) -> bool {
1716 let after = &input[offset + '&'.len_utf8()..];
1717 if let Some(rest) = after.strip_prefix('#') {
1718 let (digits, rest) = if let Some(hex) = rest.strip_prefix(['x', 'X']) {
1719 (
1720 hex.chars()
1721 .take_while(|char| char.is_ascii_hexdigit())
1722 .count(),
1723 hex,
1724 )
1725 } else {
1726 (
1727 rest.chars()
1728 .take_while(|char| char.is_ascii_digit())
1729 .count(),
1730 rest,
1731 )
1732 };
1733 return digits > 0 && rest[digits..].starts_with(';');
1734 }
1735
1736 let name_len = after
1737 .chars()
1738 .take_while(|char| char.is_ascii_alphanumeric())
1739 .count();
1740 name_len > 0 && after[name_len..].starts_with(';')
1741}
1742
1743fn text_char_at_edge(input: &str, offset: usize, len: usize) -> bool {
1744 offset == 0 || offset + len >= input.len()
1745}
1746
1747fn same_char_run_len(input: &str, offset: usize, needle: char) -> usize {
1748 input[offset..]
1749 .chars()
1750 .take_while(|char| *char == needle)
1751 .map(char::len_utf8)
1752 .sum()
1753}
1754
1755fn at_sign_can_start_email_autolink(input: &str, offset: usize) -> bool {
1756 let before = input[..offset]
1757 .chars()
1758 .next_back()
1759 .is_some_and(|char| char.is_ascii_alphanumeric());
1760 if !before {
1761 return false;
1762 }
1763
1764 let mut saw_domain_char = false;
1765 let mut saw_dot = false;
1766 let mut saw_domain_char_after_dot = false;
1767 for char in input[offset + 1..].chars() {
1768 if char.is_ascii_alphanumeric() {
1769 saw_domain_char = true;
1770 if saw_dot {
1771 saw_domain_char_after_dot = true;
1772 }
1773 continue;
1774 }
1775 if char == '.' && saw_domain_char {
1776 saw_dot = true;
1777 continue;
1778 }
1779 if matches!(char, '-' | '_') && saw_domain_char {
1780 continue;
1781 }
1782 break;
1783 }
1784 saw_domain_char_after_dot
1785}
1786
1787#[derive(Default)]
1791struct OutputLine {
1792 line_start: usize,
1793 scanned: usize,
1794}
1795
1796impl OutputLine {
1797 fn len(&mut self, output: &str) -> usize {
1798 if let Some(newline) = output.as_bytes()[self.scanned..]
1799 .iter()
1800 .rposition(|byte| *byte == b'\n')
1801 {
1802 self.line_start = self.scanned + newline + 1;
1803 }
1804 self.scanned = output.len();
1805 output.len() - self.line_start
1806 }
1807}
1808
1809fn escape_destination_with_pipe(input: &str, escape_pipe: bool) -> String {
1810 let mut output = String::new();
1811 for char in input.chars() {
1812 match char {
1813 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
1814 '|' if escape_pipe => {
1815 output.push('\\');
1816 output.push(char);
1817 }
1818 '(' | ')' | '\\' | '<' | '>' | '&' => {
1822 output.push('\\');
1823 output.push(char);
1824 }
1825 _ => output.push(char),
1826 }
1827 }
1828 output
1829}
1830
1831fn normalize_reference_label(input: &str) -> String {
1838 crate::parse::normalize_label(input)
1839}
1840
1841fn push_reference_body(
1853 output: &mut String,
1854 kind: ReferenceKind,
1855 rendered: &str,
1856 children_match_identifier: bool,
1857 escaped_label: &str,
1858) {
1859 let use_label_body = !children_match_identifier && !matches!(kind, ReferenceKind::Full);
1864 let body = if use_label_body {
1865 escaped_label
1866 } else {
1867 rendered
1868 };
1869
1870 output.push('[');
1871 output.push_str(body);
1872 output.push(']');
1873
1874 match kind {
1875 ReferenceKind::Shortcut => {}
1876 ReferenceKind::Collapsed => output.push_str("[]"),
1877 ReferenceKind::Full => {
1878 output.push('[');
1879 output.push_str(escaped_label);
1880 output.push(']');
1881 }
1882 }
1883}
1884
1885fn reference_explicit_label(
1891 from_source: bool,
1892 label: &str,
1893 context: InlineSerializeContext,
1894) -> String {
1895 if from_source {
1896 escape_reference_label_source(label, context.table_cell)
1897 } else {
1898 escape_reference_label_with_pipe(label, context.table_cell)
1899 }
1900}
1901
1902fn escape_definition_label_source(input: &str) -> String {
1909 escape_reference_label_source(input, false)
1910}
1911
1912fn escape_reference_label_source(input: &str, escape_pipe: bool) -> String {
1913 let mut output = String::new();
1914 for char in input.chars() {
1915 match char {
1916 '\t' | '\n' | '\r' => output.push(char),
1924 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
1925 '|' if escape_pipe => {
1926 output.push('\\');
1927 output.push(char);
1928 }
1929 _ => output.push(char),
1930 }
1931 }
1932 output
1933}
1934
1935fn escape_reference_label_with_pipe(input: &str, escape_pipe: bool) -> String {
1936 escape_label_syntax(input, escape_pipe, false)
1937}
1938
1939fn escape_footnote_label_source(input: &str) -> String {
1940 let mut output = String::new();
1941 for char in input.chars() {
1942 match char {
1943 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
1944 _ => output.push(char),
1945 }
1946 }
1947 output
1948}
1949
1950fn escape_footnote_label_semantic(input: &str) -> String {
1951 escape_label_syntax(input, false, true)
1952}
1953
1954fn escape_label_syntax(input: &str, escape_pipe: bool, escape_whitespace: bool) -> String {
1955 let mut output = String::new();
1956 for char in input.chars() {
1957 match char {
1958 char if char.is_whitespace() && escape_whitespace => {
1959 output.push_str(&format!("&#x{:X};", char as u32));
1960 }
1961 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
1962 '|' if escape_pipe => {
1963 output.push('\\');
1964 output.push(char);
1965 }
1966 '\\' | '[' | ']' => {
1967 output.push('\\');
1968 output.push(char);
1969 }
1970 _ => output.push(char),
1971 }
1972 }
1973 output
1974}
1975
1976fn escape_wikilink_part(input: &str) -> String {
1977 let mut output = String::new();
1978 for char in input.chars() {
1979 match char {
1980 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
1981 '\\' | '[' | ']' | '|' => {
1982 output.push('\\');
1983 output.push(char);
1984 }
1985 _ => output.push(char),
1986 }
1987 }
1988 output
1989}
1990
1991fn serialize_destination_kind(
1992 input: &str,
1993 kind: LinkDestinationKind,
1994 context: InlineSerializeContext,
1995) -> String {
1996 match kind {
1997 LinkDestinationKind::Omitted if input.is_empty() => String::new(),
1998 LinkDestinationKind::Angle => {
1999 let mut output = String::from("<");
2000 output.push_str(&escape_angle_destination_with_context(input, context));
2001 output.push('>');
2002 output
2003 }
2004 LinkDestinationKind::Bare | LinkDestinationKind::Omitted => {
2005 if input.is_empty() {
2006 "<>".into()
2007 } else if input.contains(' ') {
2008 let mut output = String::from("<");
2012 output.push_str(&escape_angle_destination_with_context(input, context));
2013 output.push('>');
2014 output
2015 } else {
2016 escape_destination_with_pipe(input, context.table_cell)
2017 }
2018 }
2019 }
2020}
2021
2022fn escape_angle_destination_with_context(input: &str, context: InlineSerializeContext) -> String {
2023 let mut output = String::new();
2024 for char in input.chars() {
2025 match char {
2026 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
2027 '|' if context.table_cell => {
2028 output.push('\\');
2029 output.push(char);
2030 }
2031 '\\' | '<' | '>' => {
2032 output.push('\\');
2033 output.push(char);
2034 }
2035 _ => output.push(char),
2036 }
2037 }
2038 output
2039}
2040
2041fn serialize_title_kind(
2042 input: &str,
2043 kind: LinkTitleKind,
2044 context: InlineSerializeContext,
2045) -> String {
2046 let (open, close) = match kind {
2047 LinkTitleKind::DoubleQuote => ('"', '"'),
2048 LinkTitleKind::SingleQuote => ('\'', '\''),
2049 LinkTitleKind::Paren => ('(', ')'),
2050 };
2051 let mut output = String::new();
2052 output.push(open);
2053 output.push_str(&escape_title_with_context(input, kind, context));
2054 output.push(close);
2055 output
2056}
2057
2058fn escape_title_with_context(
2059 input: &str,
2060 kind: LinkTitleKind,
2061 context: InlineSerializeContext,
2062) -> String {
2063 let mut output = String::new();
2064 for char in input.chars() {
2065 match char {
2066 char if char.is_control() => output.push_str(&format!("&#x{:X};", char as u32)),
2067 '|' if context.table_cell => {
2068 output.push('\\');
2069 output.push(char);
2070 }
2071 '\\' | '&' => {
2072 output.push('\\');
2073 output.push(char);
2074 }
2075 '"' if kind == LinkTitleKind::DoubleQuote => {
2076 output.push('\\');
2077 output.push(char);
2078 }
2079 '\'' if kind == LinkTitleKind::SingleQuote => {
2080 output.push('\\');
2081 output.push(char);
2082 }
2083 '(' | ')' if kind == LinkTitleKind::Paren => {
2084 output.push('\\');
2085 output.push(char);
2086 }
2087 _ => output.push(char),
2088 }
2089 }
2090 output
2091}
2092
2093fn unordered_list_marker(delimiter: ListDelimiter) -> char {
2094 match delimiter {
2095 ListDelimiter::Dash => '-',
2096 ListDelimiter::Asterisk => '*',
2097 ListDelimiter::Plus => '+',
2098 ListDelimiter::Period | ListDelimiter::Paren => '-',
2099 }
2100}
2101
2102fn ordered_list_marker(delimiter: ListDelimiter) -> char {
2103 match delimiter {
2104 ListDelimiter::Paren => ')',
2105 ListDelimiter::Dash
2106 | ListDelimiter::Asterisk
2107 | ListDelimiter::Plus
2108 | ListDelimiter::Period => '.',
2109 }
2110}
2111
2112fn prefix_lines(input: &str, prefix: &str) -> String {
2113 if input.is_empty() {
2114 return String::new();
2115 }
2116 let bytes = input.as_bytes();
2117 let mut output = String::new();
2118 let mut line_start = 0;
2119 let mut cursor = 0;
2120 while cursor < input.len() {
2121 let eol_end = match bytes[cursor] {
2122 b'\n' => Some(cursor + 1),
2123 b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => Some(cursor + 2),
2124 b'\r' => Some(cursor + 1),
2125 _ => None,
2126 };
2127 if let Some(end) = eol_end {
2128 output.push_str(prefix);
2129 output.push_str(&input[line_start..end]);
2130 cursor = end;
2131 line_start = cursor;
2132 } else {
2133 cursor += 1;
2134 }
2135 }
2136 if line_start < input.len() {
2137 output.push_str(prefix);
2138 output.push_str(&input[line_start..]);
2139 }
2140 output
2141}
2142
2143fn indent_after_first_line(input: &str, width: usize) -> String {
2144 let indent = " ".repeat(width);
2145 input
2146 .lines()
2147 .enumerate()
2148 .map(|(index, line)| {
2149 if index == 0 {
2150 line.into()
2151 } else {
2152 format!("{indent}{line}")
2153 }
2154 })
2155 .collect::<Vec<String>>()
2156 .join("\n")
2157}
2158
2159fn indent_lines(input: &str, width: usize) -> String {
2160 let indent = " ".repeat(width);
2161 input
2162 .lines()
2163 .map(|line| {
2164 if line.is_empty() {
2165 String::new()
2166 } else {
2167 format!("{indent}{line}")
2168 }
2169 })
2170 .collect::<Vec<String>>()
2171 .join("\n")
2172}
2173
2174fn indent_continuation(input: &str) -> String {
2175 input
2176 .lines()
2177 .enumerate()
2178 .map(|(index, line)| {
2179 if index == 0 {
2180 line.into()
2181 } else {
2182 format!(" {line}")
2183 }
2184 })
2185 .collect::<Vec<String>>()
2186 .join("\n")
2187}
2188
2189fn trim_trailing_newline(input: &str) -> &str {
2190 input.trim_end_matches('\n').trim_end_matches('\r')
2191}
2192
2193fn ends_with_line_ending(input: &str) -> bool {
2194 input.ends_with('\n') || input.ends_with('\r')
2195}
2196
2197fn fence_for(input: &str, marker: FenceMarker, min_len: usize) -> String {
2198 let char = match marker {
2199 FenceMarker::Backtick => '`',
2200 FenceMarker::Tilde => '~',
2201 };
2202 let longest = longest_char_streak(input, char);
2203 char.to_string().repeat(min_len.max(longest + 1))
2204}
2205
2206fn inline_code_fence(input: &str) -> String {
2207 fence_for(input, FenceMarker::Backtick, 1)
2208}
2209
2210fn code_span_needs_padding(input: &str) -> bool {
2211 input.starts_with('`')
2212 || input.ends_with('`')
2213 || (input.starts_with(' ') && input.ends_with(' ') && input.chars().any(|char| char != ' '))
2214}
2215
2216fn table_cell_escape_code_pipes(input: &str) -> String {
2217 let mut output = String::with_capacity(input.len());
2218 for char in input.chars() {
2219 if char == '|' {
2220 output.push('\\');
2221 }
2222 output.push(char);
2223 }
2224 output
2225}
2226
2227fn block_math_fence(input: &str) -> String {
2228 let mut length = 2;
2229 for line in trim_trailing_newline(input).lines() {
2230 let trimmed = line.trim();
2231 if trimmed.len() >= 2 && trimmed.chars().all(|char| char == '$') {
2232 length = length.max(trimmed.len() + 1);
2233 }
2234 }
2235 "$".repeat(length)
2236}
2237
2238fn serialize_inline_math_with_context(
2239 node: &MathInline,
2240 context: InlineSerializeContext,
2241) -> Result<String, SerializeError> {
2242 let input = node.value.as_str();
2243
2244 if context.table_cell && input.contains('|') {
2249 if input.contains("`$") {
2250 return Err(SerializeError::UnsupportedNode(
2251 "inline math containing a table pipe and a code-math close",
2252 ));
2253 }
2254 let input = table_cell_escape_code_pipes(input);
2255 return Ok(format!("$`{input}`$"));
2256 }
2257
2258 match node.kind {
2259 MathInlineKind::Code => {
2260 if input.contains("`$") {
2261 return Err(SerializeError::UnsupportedNode(
2262 "inline math (code-math form) containing a `$` close",
2263 ));
2264 }
2265 Ok(format!("$`{input}`$"))
2266 }
2267 MathInlineKind::Dollar { dollars } => {
2273 let fence = "$".repeat(usize::from(dollars));
2274 Ok(format!("{fence}{input}{fence}"))
2275 }
2276 }
2277}
2278
2279fn table_cell_has_unescaped_pipe(input: &str) -> bool {
2283 input.contains('|') && !crate::parse::table_row_delimiters(input, true).is_empty()
2284}
2285
2286fn longest_char_streak(input: &str, needle: char) -> usize {
2287 let mut longest = 0;
2288 let mut current = 0;
2289 for char in input.chars() {
2290 if char == needle {
2291 current += 1;
2292 longest = longest.max(current);
2293 } else {
2294 current = 0;
2295 }
2296 }
2297 longest
2298}
2299
2300fn directive_fence(inner: &str) -> String {
2301 ":".repeat(directive_fence_len(inner))
2302}
2303
2304fn directive_fence_len(inner: &str) -> usize {
2305 let mut max = 3;
2306 for line in inner.lines() {
2307 if let Some(length) = directive_closing_fence_len(line) {
2308 max = max.max(length + 1);
2309 }
2310 }
2311 max
2312}
2313
2314fn directive_closing_fence_len(line: &str) -> Option<usize> {
2315 let trimmed = trim_up_to_three_indent_columns(line)?;
2316 let length = trimmed
2317 .as_bytes()
2318 .iter()
2319 .take_while(|byte| **byte == b':')
2320 .count();
2321 if length >= 3 && trimmed[length..].trim().is_empty() {
2322 Some(length)
2323 } else {
2324 None
2325 }
2326}
2327
2328fn trim_up_to_three_indent_columns(input: &str) -> Option<&str> {
2329 let mut columns = 0usize;
2330 let mut bytes = 0usize;
2331 for byte in input.as_bytes() {
2332 match *byte {
2333 b' ' => columns += 1,
2334 b'\t' => columns += 4 - (columns % 4),
2335 _ => break,
2336 }
2337 bytes += 1;
2338 }
2339 (columns <= 3).then_some(&input[bytes..])
2340}