1use alloc::{
12 format,
13 string::{String, ToString},
14 vec::Vec,
15};
16
17use crate::{ast::*, diagnostic::Diagnostic, validate::validate_document};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum LineEnding {
22 Lf,
24 CrLf,
26}
27
28impl LineEnding {
29 fn as_str(self) -> &'static str {
30 match self {
31 Self::Lf => "\n",
32 Self::CrLf => "\r\n",
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum BulletMarker {
40 Dash,
42 Asterisk,
44 Plus,
46}
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum OrderedDelimiter {
52 Period,
54 Paren,
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub struct SerializeOptions {
63 pub line_ending: LineEnding,
65 pub final_newline: bool,
67 pub bullet: Option<BulletMarker>,
70 pub ordered_delimiter: Option<OrderedDelimiter>,
73 pub fence_marker: Option<FenceMarker>,
77}
78
79impl Default for SerializeOptions {
80 fn default() -> Self {
81 Self {
82 line_ending: LineEnding::Lf,
83 final_newline: true,
84 bullet: None,
85 ordered_delimiter: None,
86 fence_marker: None,
87 }
88 }
89}
90
91#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum SerializeError {
94 InvalidDocument(Vec<Diagnostic>),
96}
97
98impl Document {
99 pub fn to_markdown(&self) -> Result<String, SerializeError> {
101 self.to_markdown_with(&SerializeOptions::default())
102 }
103
104 pub fn to_markdown_with(&self, options: &SerializeOptions) -> Result<String, SerializeError> {
106 let diagnostics = validate_document(self);
107 if !diagnostics.is_empty() {
108 return Err(SerializeError::InvalidDocument(diagnostics));
109 }
110
111 let mut output = write_blocks(&self.children, options, Join::Gap, true)?;
112 if options.line_ending == LineEnding::CrLf {
113 output = lf_to_crlf(&output);
114 }
115 if options.final_newline && !output.is_empty() && !ends_with_carriage_return_ending(&output)
119 {
120 output.push_str(options.line_ending.as_str());
121 }
122 Ok(output)
123 }
124}
125
126fn lf_to_crlf(input: &str) -> String {
129 let mut output = String::with_capacity(input.len());
130 let mut previous = '\0';
131 for ch in input.chars() {
132 if ch == '\n' && previous != '\r' {
133 output.push('\r');
134 }
135 output.push(ch);
136 previous = ch;
137 }
138 output
139}
140
141fn ends_with_carriage_return_ending(input: &str) -> bool {
142 input.ends_with('\r') || input.ends_with("\r\n")
143}
144
145fn push_block_gap(output: &mut String) {
149 if output.ends_with('\r') {
150 output.push('\r');
151 } else if output.ends_with("\r\n") {
152 output.push('\n');
153 } else {
154 output.push_str("\n\n");
155 }
156}
157
158#[derive(Clone, Copy, Eq, PartialEq)]
160enum Join {
161 Gap,
163 Line,
165}
166
167fn write_blocks(
171 blocks: &[Block],
172 options: &SerializeOptions,
173 join: Join,
174 document_start: bool,
175) -> Result<String, SerializeError> {
176 let mut output = String::new();
177 let mut previous_list: Option<char> = None;
180 for (index, block) in blocks.iter().enumerate() {
181 if index > 0 {
182 match join {
183 Join::Gap => push_block_gap(&mut output),
184 Join::Line => output.push('\n'),
185 }
186 }
187 let after_paragraph_line =
188 join == Join::Line && index > 0 && matches!(blocks[index - 1], Block::Paragraph(_));
189 let written = match block {
190 Block::List(list) => {
191 let marker = list_marker(list, options, previous_list);
192 previous_list = Some(marker);
193 write_list(list, options, marker)?
194 }
195 Block::ThematicBreak(node) => {
196 previous_list = None;
197 write_thematic_break(node, document_start && index == 0, after_paragraph_line)
198 }
199 _ => {
200 previous_list = None;
201 write_block(block, options)?
202 }
203 };
204 output.push_str(&written);
205 }
206 Ok(output)
207}
208
209fn write_block(block: &Block, options: &SerializeOptions) -> Result<String, SerializeError> {
210 match block {
211 Block::Paragraph(node) => write_inlines(&node.children, Context::BLOCK),
212 Block::Heading(node) => write_heading(node),
213 Block::ThematicBreak(node) => Ok(write_thematic_break(node, false, false)),
214 Block::BlockQuote(node) => {
215 let inner = write_blocks(&node.children, options, Join::Gap, false)?;
216 Ok(if inner.is_empty() {
217 ">".into()
218 } else {
219 quote_lines(&inner)
220 })
221 }
222 Block::Alert(node) => write_alert(node, options),
223 Block::List(node) => write_list(node, options, list_marker(node, options, None)),
224 Block::CodeBlock(node) => Ok(write_code_block(node, options)),
225 Block::HtmlBlock(node) => Ok(node.value.clone()),
229 Block::HtmlContainer(node) => write_html_container(node, options),
230 Block::Definition(node) => Ok(write_definition(node)),
231 Block::FootnoteDefinition(node) => {
232 let inner = write_blocks(&node.children, options, Join::Gap, false)?;
233 Ok(if inner.starts_with([' ', '\t']) {
237 format!("[^{}]:\n{}", node.label, prefix_lines(&inner, " "))
238 } else {
239 format!("[^{}]: {}", node.label, indent_continuation(&inner))
240 })
241 }
242 Block::Table(node) => write_table(node),
243 Block::MathBlock(node) => {
244 let fence = block_math_fence(&node.value);
245 Ok(fenced_body(&fence, &node.value, &fence))
246 }
247 Block::Frontmatter(node) => {
248 let fence = match node.kind {
249 FrontmatterKind::Yaml => "---",
250 FrontmatterKind::Toml => "+++",
251 };
252 Ok(format!("{fence}\n{}\n{fence}", node.value))
255 }
256 Block::LeafDirective(node) => Ok(format!(
257 "::{}{}{}",
258 node.name,
259 write_directive_label(&node.label)?,
260 write_attributes(&node.attributes)
261 )),
262 Block::ContainerDirective(node) => {
263 let mut inner = write_blocks(&node.children, options, Join::Gap, false)?;
264 let fence = directive_fence(&inner);
265 if !inner.is_empty() {
269 inner.push('\n');
270 }
271 Ok(format!(
272 "{fence}{}{}{}\n{inner}{fence}",
273 node.name,
274 write_directive_label(&node.label)?,
275 write_attributes(&node.attributes)
276 ))
277 }
278 }
279}
280
281fn write_thematic_break(
285 node: &ThematicBreak,
286 at_document_start: bool,
287 after_paragraph_line: bool,
288) -> String {
289 match node.marker {
290 ThematicBreakMarker::Dash if at_document_start || after_paragraph_line => "- - -".into(),
291 ThematicBreakMarker::Dash => "---".into(),
292 ThematicBreakMarker::Asterisk => "***".into(),
293 ThematicBreakMarker::Underscore => "___".into(),
294 }
295}
296
297pub(crate) fn writes_setext(node: &Heading) -> bool {
300 node.kind == HeadingKind::Setext && matches!(node.depth, 1 | 2) && !node.children.is_empty()
301}
302
303fn write_heading(node: &Heading) -> Result<String, SerializeError> {
304 let content = write_inlines(&node.children, Context::HEADING)?;
305 if writes_setext(node) {
306 let marker = if node.depth == 1 { "=" } else { "-" };
307 return Ok(format!(
308 "{content}\n{}",
309 marker.repeat(content.len().max(3))
310 ));
311 }
312 let hashes = "#".repeat(usize::from(node.depth));
313 Ok(if content.is_empty() {
314 hashes
315 } else if reads_as_closing_sequence(&content) {
316 format!("{hashes} {content} #")
319 } else {
320 format!("{hashes} {content}")
321 })
322}
323
324fn reads_as_closing_sequence(content: &str) -> bool {
327 let before = content.trim_end_matches('#');
328 before.len() < content.len() && (before.is_empty() || before.ends_with([' ', '\t']))
329}
330
331fn write_definition(node: &Definition) -> String {
332 let destination = write_destination(&node.destination, node.destination_kind);
333 let mut output = format!("[{}]: {}", node.label, destination);
334 if let Some(title) = &node.title {
335 output.push(' ');
336 output.push_str(&write_title(title));
337 }
338 output
339}
340
341fn write_html_container(
342 node: &HtmlContainer,
343 options: &SerializeOptions,
344) -> Result<String, SerializeError> {
345 match &node.content {
346 HtmlContainerContent::Blocks(children) => {
347 let inner = write_blocks(children, options, Join::Gap, false)?;
348 Ok(if inner.is_empty() {
349 format!("{}\n{}", node.opening.raw, node.closing.raw)
350 } else {
351 format!("{}\n{}\n\n{}", node.opening.raw, inner, node.closing.raw)
352 })
353 }
354 HtmlContainerContent::Inlines(children) => Ok(format!(
355 "{}{}{}",
356 node.opening.raw,
357 write_inlines(children, Context::BLOCK)?,
358 node.closing.raw
359 )),
360 }
361}
362
363fn write_alert(node: &Alert, options: &SerializeOptions) -> Result<String, SerializeError> {
364 let mut output = String::from("> [!");
365 output.push_str(alert_kind_name(node.kind));
366 output.push(']');
367 if let Some(title) = &node.title {
368 if !title.is_empty() {
369 output.push(' ');
370 output.push_str(&title.replace(['\n', '\r'], " "));
372 }
373 }
374 let inner = write_blocks(&node.children, options, Join::Gap, false)?;
375 if !inner.is_empty() {
376 output.push('\n');
377 output.push_str("e_lines(&inner));
378 }
379 Ok(output)
380}
381
382fn quote_lines(inner: &str) -> String {
385 let mut output = prefix_lines(inner, "> ");
386 if inner.ends_with(['\n', '\r']) {
387 output.push('>');
388 }
389 output
390}
391
392fn alert_kind_name(kind: AlertKind) -> &'static str {
393 match kind {
394 AlertKind::Note => "NOTE",
395 AlertKind::Tip => "TIP",
396 AlertKind::Important => "IMPORTANT",
397 AlertKind::Warning => "WARNING",
398 AlertKind::Caution => "CAUTION",
399 }
400}
401
402fn list_marker(list: &List, options: &SerializeOptions, previous: Option<char>) -> char {
408 let marker = if list.ordered {
409 options
410 .ordered_delimiter
411 .map_or_else(|| written_marker(list), ordered_delimiter_char)
412 } else {
413 options
414 .bullet
415 .map_or_else(|| written_marker(list), bullet_char)
416 };
417 if previous == Some(marker) {
418 let order: &[char] = if list.ordered {
419 &['.', ')']
420 } else {
421 &['-', '*', '+']
422 };
423 let at = order.iter().position(|char| *char == marker).unwrap_or(0);
424 return order[(at + 1) % order.len()];
425 }
426 marker
427}
428
429pub(crate) fn written_marker(list: &List) -> char {
433 match (list.ordered, list.delimiter) {
434 (false, ListDelimiter::Asterisk) => '*',
435 (false, ListDelimiter::Plus) => '+',
436 (false, _) => '-',
437 (true, ListDelimiter::Paren) => ')',
438 (true, _) => '.',
439 }
440}
441
442fn bullet_char(bullet: BulletMarker) -> char {
443 match bullet {
444 BulletMarker::Dash => '-',
445 BulletMarker::Asterisk => '*',
446 BulletMarker::Plus => '+',
447 }
448}
449
450fn ordered_delimiter_char(delimiter: OrderedDelimiter) -> char {
451 match delimiter {
452 OrderedDelimiter::Period => '.',
453 OrderedDelimiter::Paren => ')',
454 }
455}
456
457fn write_list(
458 node: &List,
459 options: &SerializeOptions,
460 marker_char: char,
461) -> Result<String, SerializeError> {
462 let mut output = String::new();
463 for (index, item) in node.children.iter().enumerate() {
464 if index > 0 {
465 output.push_str(if node.tight { "\n" } else { "\n\n" });
466 }
467 let marker = if node.ordered {
468 let number = node
471 .start
472 .unwrap_or(1)
473 .saturating_add(index as u64)
474 .min(crate::parse::MAX_ORDERED_NUMBER);
475 format!("{number}{marker_char} ")
476 } else {
477 format!("{marker_char} ")
478 };
479 let inner = write_item_blocks(&item.children, options, node.tight, item.checked)?;
480 let break_after_bullet = breaks_after_bullet(node, item, marker_char);
481 let opens_with_whitespace = opens_with_padding(&inner);
482 if break_after_bullet || opens_with_whitespace {
483 output.push_str(marker.trim_end());
484 output.push('\n');
485 output.push_str(&prefix_lines(&inner, &" ".repeat(marker.len())));
486 } else {
487 output.push_str(&marker);
488 output.push_str(&indent_after_first_line(&inner, marker.len()));
489 }
490 }
491 Ok(output)
492}
493
494fn thematic_break_char(marker: ThematicBreakMarker) -> char {
495 match marker {
496 ThematicBreakMarker::Dash => '-',
497 ThematicBreakMarker::Asterisk => '*',
498 ThematicBreakMarker::Underscore => '_',
499 }
500}
501
502fn breaks_after_bullet(list: &List, item: &ListItem, marker_char: char) -> bool {
508 !list.ordered
509 && item.checked.is_none()
510 && matches!(
511 item.children.first(),
512 Some(Block::ThematicBreak(ThematicBreak { marker, .. }))
513 if thematic_break_char(*marker) == marker_char
514 )
515}
516
517fn opens_with_padding(inner: &str) -> bool {
522 !inner.starts_with(" ") && inner.starts_with([' ', '\t'])
523}
524
525pub(crate) fn first_item_starts_below_its_bullet(list: &List) -> bool {
530 let Some(item) = list.children.first() else {
531 return false;
532 };
533 let options = SerializeOptions::default();
534 if breaks_after_bullet(list, item, list_marker(list, &options, None)) {
535 return true;
536 }
537 if item.checked.is_some() {
538 return false;
539 }
540 match item.children.first() {
541 Some(
542 block @ (Block::Paragraph(_)
543 | Block::Heading(_)
544 | Block::HtmlBlock(_)
545 | Block::CodeBlock(_)),
546 ) => write_block(block, &options).is_ok_and(|written| opens_with_padding(&written)),
547 _ => false,
548 }
549}
550
551fn write_item_blocks(
552 blocks: &[Block],
553 options: &SerializeOptions,
554 tight: bool,
555 task: Option<bool>,
556) -> Result<String, SerializeError> {
557 let join = if tight { Join::Line } else { Join::Gap };
558 let Some(checked) = task else {
559 return write_blocks(blocks, options, join, false);
560 };
561 let checkbox = if checked { "[x]" } else { "[ ]" };
562 let definitions = blocks
563 .iter()
564 .take_while(|block| matches!(block, Block::Definition(_)))
565 .count();
566 if matches!(blocks.get(definitions), Some(Block::Paragraph(_))) {
567 let before = write_blocks(&blocks[..definitions], options, join, false)?;
568 let rest = write_blocks(&blocks[definitions..], options, join, false)?;
569 let mut output = before;
570 if !output.is_empty() {
571 match join {
572 Join::Gap => push_block_gap(&mut output),
573 Join::Line => output.push('\n'),
574 }
575 }
576 output.push_str(checkbox);
577 output.push(' ');
578 output.push_str(&rest);
579 return Ok(output);
580 }
581 let written = write_blocks(blocks, options, join, false)?;
582 Ok(if written.is_empty() {
583 checkbox.into()
584 } else {
585 format!("{checkbox} {written}")
586 })
587}
588
589fn write_code_block(node: &CodeBlock, options: &SerializeOptions) -> String {
590 match node.kind {
591 CodeBlockKind::Indented => {
592 let body = trim_trailing_newline(&node.value);
595 let mut output = prefix_lines(body, " ");
596 let ending = &node.value[body.len()..];
597 if matches!(ending, "\r" | "\r\n") {
598 output.push_str(ending);
599 }
600 output
601 }
602 CodeBlockKind::Fenced { marker, length } => {
603 let marker = code_block_fence_marker(node, marker, options);
604 let (fence, indent) = code_block_fence(&node.value, marker, length.max(3));
605 let mut opener = fence.clone();
606 if let Some(info) = &node.info {
607 opener.push(' ');
608 opener.push_str(&escape_code_info(info));
609 }
610 let body = fenced_body(&opener, &node.value, &fence);
611 if indent == 0 {
612 body
613 } else {
614 prefix_lines(&body, &" ".repeat(indent))
615 }
616 }
617 }
618}
619
620fn fenced_body(opener: &str, value: &str, closer: &str) -> String {
624 let mut output = String::with_capacity(opener.len() + value.len() + closer.len() + 2);
625 output.push_str(opener);
626 output.push('\n');
627 output.push_str(value);
628 if !value.is_empty() && !ends_with_line_ending(value) {
629 output.push('\n');
630 }
631 output.push_str(closer);
632 output
633}
634
635fn code_block_fence_marker(
639 node: &CodeBlock,
640 marker: FenceMarker,
641 options: &SerializeOptions,
642) -> FenceMarker {
643 if node.info.as_deref().is_some_and(|info| info.contains('`')) {
644 return FenceMarker::Tilde;
645 }
646 options.fence_marker.unwrap_or(marker)
647}
648
649fn escape_code_info(input: &str) -> String {
650 let mut output = String::new();
651 let inner_start = input.len() - input.trim_start_matches([' ', '\t']).len();
654 let inner_end = input.trim_end_matches([' ', '\t']).len().max(inner_start);
655 for (offset, char) in input.char_indices() {
656 match char {
657 ' ' | '\t' if offset < inner_start || offset >= inner_end => {
658 output.push_str(&char_reference(char));
659 }
660 '\t' => output.push(char),
661 char if char.is_control() => output.push_str(&char_reference(char)),
662 '\\' | '&' => {
663 output.push('\\');
664 output.push(char);
665 }
666 _ => output.push(char),
667 }
668 }
669 output
670}
671
672fn char_reference(char: char) -> String {
674 format!("&#x{:X};", char as u32)
675}
676
677fn write_table(node: &Table) -> Result<String, SerializeError> {
678 let delimiter_row = node
679 .alignments
680 .iter()
681 .map(|alignment| match alignment {
682 TableAlignment::None => "---",
683 TableAlignment::Left => ":---",
684 TableAlignment::Center => ":---:",
685 TableAlignment::Right => "---:",
686 })
687 .collect::<Vec<_>>()
688 .join(" | ");
689 let mut output = String::new();
690 for (row_index, row) in node.rows.iter().enumerate() {
691 if row_index > 0 {
692 output.push('\n');
693 }
694 let cells = row
695 .cells
696 .iter()
697 .map(|cell| Ok(encode_cell(&write_inlines(&cell.children, Context::BLOCK)?)))
698 .collect::<Result<Vec<_>, SerializeError>>()?;
699 output.push_str(&format!("| {} |", cells.join(" | ")));
700 if row_index == 0 {
701 output.push_str(&format!("\n| {delimiter_row} |"));
702 }
703 }
704 Ok(output)
705}
706
707fn encode_cell(written: &str) -> String {
715 let mut output = String::with_capacity(written.len());
716 let mut backslashes = 0usize;
717 for char in written.chars() {
718 if char == '|' && backslashes % 2 == 0 {
719 output.push('\\');
720 }
721 backslashes = if char == '\\' { backslashes + 1 } else { 0 };
722 output.push(char);
723 }
724 output
725}
726
727#[derive(Clone, Copy, Debug, Eq, PartialEq)]
730struct Context {
731 heading: bool,
733}
734
735impl Context {
736 const BLOCK: Self = Self { heading: false };
737 const HEADING: Self = Self { heading: true };
738}
739
740fn write_inlines(inlines: &[Inline], context: Context) -> Result<String, SerializeError> {
741 let mut output = InlineOut::default();
742 for inline in inlines {
743 write_inline(inline, context, &mut output)?;
744 }
745 Ok(output.text)
746}
747
748#[derive(Default)]
752struct InlineOut {
753 text: String,
754 open_runs: Vec<bool>,
758 scanned: usize,
760}
761
762impl InlineOut {
763 fn scan_open_runs(&mut self) {
767 let mut escaped = false;
768 let mut run = 0;
769 let mut run_escaped = false;
770 for byte in self.text.as_bytes()[self.scanned..]
771 .iter()
772 .copied()
773 .chain(core::iter::once(b' '))
774 {
775 if byte == b'`' {
776 if run == 0 {
777 run_escaped = escaped;
778 }
779 run += 1;
780 escaped = false;
781 continue;
782 }
783 let opening = run - usize::from(run_escaped && run > 0);
784 if opening > 0 {
785 if self.open_runs.len() <= opening {
786 self.open_runs.resize(opening + 1, false);
787 }
788 self.open_runs[opening] = true;
789 }
790 run = 0;
791 escaped = byte == b'\\' && !escaped;
792 }
793 self.scanned = self.text.len();
794 }
795
796 fn is_open_run(&self, length: usize) -> bool {
797 self.open_runs.get(length).copied().unwrap_or(false)
798 }
799
800 fn push_opaque(&mut self, write: impl FnOnce(&mut String)) {
803 self.scan_open_runs();
804 write(&mut self.text);
805 self.scanned = self.text.len();
806 }
807}
808
809impl core::ops::Deref for InlineOut {
810 type Target = String;
811
812 fn deref(&self) -> &String {
813 &self.text
814 }
815}
816
817impl core::ops::DerefMut for InlineOut {
818 fn deref_mut(&mut self) -> &mut String {
819 &mut self.text
820 }
821}
822
823fn write_inline(
824 inline: &Inline,
825 context: Context,
826 out: &mut InlineOut,
827) -> Result<(), SerializeError> {
828 match inline {
829 Inline::Text(node) => out.push_str(&node.value),
830 Inline::Escape(node) => {
831 out.push('\\');
832 out.push(node.value);
833 }
834 Inline::CharacterReference(node) => out.push_str(&node.reference),
835 Inline::Emphasis(node) => {
836 let delimiter = emphasis_delimiter(node.delimiter);
837 write_span(out, delimiter, &node.children, delimiter, context)?;
838 }
839 Inline::Strong(node) => {
840 let delimiter = emphasis_delimiter(node.delimiter).repeat(2);
841 write_span(out, &delimiter, &node.children, &delimiter, context)?;
842 }
843 Inline::Delete(node) => write_span(out, "~~", &node.children, "~~", context)?,
844 Inline::Mark(node) => write_span(out, "==", &node.children, "==", context)?,
845 Inline::InlineFootnote(node) => write_span(out, "^[", &node.children, "]", context)?,
846 Inline::Shortcode(node) => {
847 out.push(':');
848 out.push_str(&node.name);
849 out.push(':');
850 }
851 Inline::Code(node) => write_code_span(node, out),
852 Inline::Link(node) => {
853 write_span(out, "[", &node.children, "](", context)?;
854 out.push_opaque(|out| {
855 write_resource(
856 out,
857 &node.destination,
858 node.destination_kind,
859 node.title.as_ref(),
860 );
861 });
862 }
863 Inline::Autolink(node) => out.push_opaque(|out| match node.form {
864 AutolinkForm::Literal => out.push_str(&node.text),
865 AutolinkForm::Angle => {
866 out.push('<');
867 out.push_str(&node.text);
868 out.push('>');
869 }
870 }),
871 Inline::Image(node) => {
872 write_span(out, "?;
873 out.push_opaque(|out| {
874 write_resource(
875 out,
876 &node.destination,
877 node.destination_kind,
878 node.title.as_ref(),
879 );
880 });
881 }
882 Inline::LinkReference(node) => {
883 write_reference(out, "[", &node.children, node.kind, &node.label, context)?;
884 }
885 Inline::ImageReference(node) => {
886 write_reference(out, "![", &node.alt, node.kind, &node.label, context)?;
887 }
888 Inline::Html(node) => out.push_opaque(|out| out.push_str(&node.value)),
889 Inline::SoftBreak(_) if context.heading => out.push(' '),
890 Inline::SoftBreak(_) => out.push('\n'),
891 Inline::LineBreak(node) => match node.kind {
892 LineBreakKind::Backslash => out.push_str("\\\n"),
893 LineBreakKind::Spaces => out.push_str(" \n"),
894 },
895 Inline::Math(node) => {
896 let math = write_inline_math(node);
897 out.push_opaque(|out| out.push_str(&math));
898 }
899 Inline::FootnoteReference(node) => {
900 out.push_str("[^");
901 out.push_str(&node.label);
902 out.push(']');
903 }
904 Inline::WikiLink(node) => out.push_opaque(|out| {
905 if node.embed {
906 out.push('!');
907 }
908 out.push_str("[[");
909 out.push_str(&node.target);
910 if node.target != node.label {
911 out.push('|');
912 out.push_str(&node.label);
913 }
914 out.push_str("]]");
915 }),
916 Inline::TextDirective(node) => {
917 let label = if node.label.is_empty() {
919 String::new()
920 } else {
921 format!("[{}]", write_inlines(&node.label, context)?)
922 };
923 out.push_opaque(|out| {
924 out.push(':');
925 out.push_str(&node.name);
926 out.push_str(&label);
927 out.push_str(&write_attributes(&node.attributes));
928 });
929 }
930 }
931 Ok(())
932}
933
934fn emphasis_delimiter(delimiter: EmphasisDelimiter) -> &'static str {
935 match delimiter {
936 EmphasisDelimiter::Asterisk => "*",
937 EmphasisDelimiter::Underscore => "_",
938 }
939}
940
941fn write_span(
943 out: &mut InlineOut,
944 open: &str,
945 children: &[Inline],
946 close: &str,
947 context: Context,
948) -> Result<(), SerializeError> {
949 out.push_str(open);
950 for child in children {
951 write_inline(child, context, out)?;
952 }
953 out.push_str(close);
954 Ok(())
955}
956
957fn write_resource(
959 out: &mut String,
960 destination: &str,
961 kind: LinkDestinationKind,
962 title: Option<&Title>,
963) {
964 out.push_str(&write_destination(destination, kind));
965 if let Some(title) = title {
966 out.push(' ');
967 out.push_str(&write_title(title));
968 }
969 out.push(')');
970}
971
972fn write_reference(
978 out: &mut InlineOut,
979 open: &str,
980 children: &[Inline],
981 kind: ReferenceKind,
982 label: &str,
983 context: Context,
984) -> Result<(), SerializeError> {
985 match kind {
986 ReferenceKind::Full => write_span(out, open, children, "]", context)?,
987 ReferenceKind::Shortcut | ReferenceKind::Collapsed => {
988 out.push_str(open);
989 out.push_str(label);
990 out.push(']');
991 }
992 }
993 out.push_opaque(|out| write_reference_kind(out, kind, label));
994 Ok(())
995}
996
997fn write_reference_kind(out: &mut String, kind: ReferenceKind, label: &str) {
998 match kind {
999 ReferenceKind::Shortcut => {}
1000 ReferenceKind::Collapsed => out.push_str("[]"),
1001 ReferenceKind::Full => {
1002 out.push('[');
1003 out.push_str(label);
1005 out.push(']');
1006 }
1007 }
1008}
1009
1010fn write_code_span(node: &CodeInline, out: &mut InlineOut) {
1015 let value = &node.value;
1016 out.scan_open_runs();
1017 let held = backtick_runs(value);
1018 let before = out.len() - out.trim_end_matches('`').len();
1022 let length = (1..)
1023 .find(|length| {
1024 !held.get(*length).copied().unwrap_or(false)
1025 && !out.is_open_run(*length)
1026 && (before == 0 || !out.is_open_run(before + *length))
1027 })
1028 .unwrap_or(1);
1029 let fence = "`".repeat(length);
1030 out.push_str(&fence);
1031 if code_span_needs_padding(value) {
1032 out.push(' ');
1033 out.push_str(value);
1034 out.push(' ');
1035 } else {
1036 out.push_str(value);
1037 }
1038 out.push_str(&fence);
1039 out.scanned = out.text.len();
1041}
1042
1043fn backtick_runs(value: &str) -> Vec<bool> {
1045 let mut runs = alloc::vec![false; value.len() + 1];
1047 let mut run = 0;
1048 for byte in value.bytes() {
1049 if byte == b'`' {
1050 run += 1;
1051 } else {
1052 runs[run] = true;
1053 run = 0;
1054 }
1055 }
1056 runs[run] = true;
1057 runs
1058}
1059
1060fn code_span_needs_padding(input: &str) -> bool {
1061 input.starts_with('`')
1062 || input.ends_with('`')
1063 || (input.starts_with(' ') && input.ends_with(' ') && input.chars().any(|char| char != ' '))
1064}
1065
1066pub(crate) fn write_inline_math(node: &MathInline) -> String {
1067 match node.kind {
1068 MathInlineKind::Code => format!("$`{}`$", node.value),
1069 MathInlineKind::Dollar { dollars } => {
1071 let fence = "$".repeat(usize::from(dollars));
1072 format!("{fence}{}{fence}", node.value)
1073 }
1074 }
1075}
1076
1077fn write_directive_label(label: &[Inline]) -> Result<String, SerializeError> {
1079 if label.is_empty() {
1080 Ok(String::new())
1081 } else {
1082 Ok(format!("[{}]", write_inlines(label, Context::BLOCK)?))
1083 }
1084}
1085
1086fn write_attributes(attributes: &[DirectiveAttribute]) -> String {
1087 if attributes.is_empty() {
1088 return String::new();
1089 }
1090 let mut output = String::from("{");
1091 for (index, attribute) in attributes.iter().enumerate() {
1092 if index > 0 {
1093 output.push(' ');
1094 }
1095 match (&*attribute.name, &attribute.value) {
1096 ("id", Some(value)) if is_directive_shorthand_value(value) => {
1097 output.push('#');
1098 output.push_str(value);
1099 }
1100 ("class", Some(value)) if is_directive_shorthand_value(value) => {
1101 output.push('.');
1102 output.push_str(value);
1103 }
1104 (_, Some(value)) => {
1105 output.push_str(&attribute.name);
1106 output.push_str("=\"");
1107 output.push_str(&escape_title(value, LinkTitleKind::DoubleQuote));
1108 output.push('"');
1109 }
1110 (_, None) => output.push_str(&attribute.name),
1111 }
1112 }
1113 output.push('}');
1114 output
1115}
1116
1117fn is_directive_shorthand_value(input: &str) -> bool {
1118 !input.is_empty()
1119 && input
1120 .chars()
1121 .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1122}
1123
1124fn write_destination(input: &str, kind: LinkDestinationKind) -> String {
1125 match kind {
1126 LinkDestinationKind::Omitted if input.is_empty() => String::new(),
1127 LinkDestinationKind::Angle => {
1128 let mut output = String::from("<");
1129 for char in input.chars() {
1130 match char {
1131 char if char.is_control() => output.push_str(&char_reference(char)),
1132 '\\' | '<' | '>' => {
1133 output.push('\\');
1134 output.push(char);
1135 }
1136 _ => output.push(char),
1137 }
1138 }
1139 output.push('>');
1140 output
1141 }
1142 LinkDestinationKind::Bare | LinkDestinationKind::Omitted if input.is_empty() => "<>".into(),
1143 LinkDestinationKind::Bare | LinkDestinationKind::Omitted => {
1144 let mut output = String::new();
1145 for char in input.chars() {
1146 match char {
1147 char if char.is_control() || char == ' ' => {
1149 output.push_str(&char_reference(char));
1150 }
1151 '(' | ')' | '\\' | '<' | '>' | '&' => {
1152 output.push('\\');
1153 output.push(char);
1154 }
1155 _ => output.push(char),
1156 }
1157 }
1158 output
1159 }
1160 }
1161}
1162
1163fn write_title(title: &Title) -> String {
1164 let (open, close) = match title.kind {
1165 LinkTitleKind::DoubleQuote => ('"', '"'),
1166 LinkTitleKind::SingleQuote => ('\'', '\''),
1167 LinkTitleKind::Paren => ('(', ')'),
1168 };
1169 let mut output = String::new();
1170 output.push(open);
1171 output.push_str(&escape_title(&title.value, title.kind));
1172 output.push(close);
1173 output
1174}
1175
1176fn escape_title(input: &str, kind: LinkTitleKind) -> String {
1177 let mut output = String::new();
1178 for char in input.chars() {
1179 match char {
1180 char if char.is_control() => output.push_str(&char_reference(char)),
1181 '\\' | '&' => {
1182 output.push('\\');
1183 output.push(char);
1184 }
1185 '"' if kind == LinkTitleKind::DoubleQuote => output.push_str("\\\""),
1186 '\'' if kind == LinkTitleKind::SingleQuote => output.push_str("\\'"),
1187 '(' | ')' if kind == LinkTitleKind::Paren => {
1188 output.push('\\');
1189 output.push(char);
1190 }
1191 _ => output.push(char),
1192 }
1193 }
1194 output
1195}
1196
1197fn prefix_lines(input: &str, prefix: &str) -> String {
1199 if input.is_empty() {
1200 return String::new();
1201 }
1202 let bytes = input.as_bytes();
1203 let mut output = String::new();
1204 let mut line_start = 0;
1205 let mut cursor = 0;
1206 while cursor < input.len() {
1207 let eol_end = match bytes[cursor] {
1208 b'\n' => Some(cursor + 1),
1209 b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => Some(cursor + 2),
1210 b'\r' => Some(cursor + 1),
1211 _ => None,
1212 };
1213 if let Some(end) = eol_end {
1214 output.push_str(prefix);
1215 output.push_str(&input[line_start..end]);
1216 cursor = end;
1217 line_start = cursor;
1218 } else {
1219 cursor += 1;
1220 }
1221 }
1222 if line_start < input.len() {
1223 output.push_str(prefix);
1224 output.push_str(&input[line_start..]);
1225 }
1226 output
1227}
1228
1229fn indent_after_first_line(input: &str, width: usize) -> String {
1230 indent_lines_of(input, width)
1231}
1232
1233fn indent_continuation(input: &str) -> String {
1234 indent_lines_of(input, 4)
1235}
1236
1237fn indent_lines_of(input: &str, width: usize) -> String {
1240 let mut output = String::with_capacity(input.len());
1241 let bytes = input.as_bytes();
1242 let mut line_start = 0;
1243 let mut index = 0;
1244 let push = |output: &mut String, line: &str, number: usize| {
1245 if number > 0 {
1246 output.extend(core::iter::repeat_n(' ', width));
1247 }
1248 output.push_str(line);
1249 };
1250 let mut number = 0;
1251 while index < bytes.len() {
1252 let end = match bytes[index] {
1253 b'\n' => Some(index + 1),
1254 b'\r' if bytes.get(index + 1) == Some(&b'\n') => Some(index + 2),
1255 b'\r' => Some(index + 1),
1256 _ => None,
1257 };
1258 match end {
1259 Some(end) => {
1260 push(&mut output, &input[line_start..end], number);
1261 number += 1;
1262 index = end;
1263 line_start = end;
1264 }
1265 None => index += 1,
1266 }
1267 }
1268 if line_start < input.len() {
1269 push(&mut output, &input[line_start..], number);
1270 }
1271 output
1272}
1273
1274fn trim_trailing_newline(input: &str) -> &str {
1275 input.trim_end_matches('\n').trim_end_matches('\r')
1276}
1277
1278fn ends_with_line_ending(input: &str) -> bool {
1279 input.ends_with('\n') || input.ends_with('\r')
1280}
1281
1282fn code_block_fence(value: &str, marker: FenceMarker, min_len: usize) -> (String, usize) {
1289 let char = match marker {
1290 FenceMarker::Backtick => '`',
1291 FenceMarker::Tilde => '~',
1292 };
1293 let closing_like = |line: &str, length: usize| {
1294 let indent = line.len() - line.trim_start_matches(' ').len();
1295 let rest = &line[indent..];
1296 let run = rest.len() - rest.trim_start_matches(char).len();
1297 (indent <= 3 && run >= length && rest[run..].trim_matches([' ', '\t']).is_empty())
1298 .then_some((indent, run))
1299 };
1300 let lines = || value.split(['\n', '\r']);
1301 let least_indent = lines()
1302 .filter_map(|line| closing_like(line, min_len))
1303 .map(|(indent, _)| indent)
1304 .min();
1305 match least_indent {
1306 None => (char.to_string().repeat(min_len), 0),
1307 Some(indent) if indent > 0 => (char.to_string().repeat(min_len), 4 - indent),
1308 Some(_) => {
1309 let mut length = min_len;
1310 for line in lines() {
1311 if let Some((_, run)) = closing_like(line, length) {
1312 length = run + 1;
1313 }
1314 }
1315 (char.to_string().repeat(length), 0)
1316 }
1317 }
1318}
1319
1320fn block_math_fence(input: &str) -> String {
1321 let mut length = 2;
1322 for line in trim_trailing_newline(input).lines() {
1323 let trimmed = line.trim();
1324 if trimmed.len() >= 2 && trimmed.chars().all(|char| char == '$') {
1325 length = length.max(trimmed.len() + 1);
1326 }
1327 }
1328 "$".repeat(length)
1329}
1330
1331fn directive_fence(inner: &str) -> String {
1332 let mut length = 3;
1333 for line in inner.lines() {
1334 if let Some(closing) = directive_closing_fence_len(line) {
1335 length = length.max(closing + 1);
1336 }
1337 }
1338 ":".repeat(length)
1339}
1340
1341fn directive_closing_fence_len(line: &str) -> Option<usize> {
1342 let trimmed = trim_up_to_three_indent_columns(line)?;
1343 let length = trimmed
1344 .as_bytes()
1345 .iter()
1346 .take_while(|byte| **byte == b':')
1347 .count();
1348 if length >= 3 && trimmed[length..].trim().is_empty() {
1349 Some(length)
1350 } else {
1351 None
1352 }
1353}
1354
1355fn trim_up_to_three_indent_columns(input: &str) -> Option<&str> {
1356 let mut columns = 0usize;
1357 let mut bytes = 0usize;
1358 for byte in input.as_bytes() {
1359 match *byte {
1360 b' ' => columns += 1,
1361 b'\t' => columns += 4 - (columns % 4),
1362 _ => break,
1363 }
1364 bytes += 1;
1365 }
1366 (columns <= 3).then_some(&input[bytes..])
1367}