1use alloc::{
16 format,
17 string::{String, ToString},
18 vec::Vec,
19};
20
21use alloc::collections::BTreeMap;
22
23use crate::{
24 ast::*, diagnostic::Diagnostic, leaves, parse::Definitions, parse::InlineSource, span::Span,
25 validate::validate_document,
26};
27
28mod reuse;
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum LineEnding {
33 Lf,
35 CrLf,
37}
38
39impl LineEnding {
40 fn as_str(self) -> &'static str {
41 match self {
42 Self::Lf => "\n",
43 Self::CrLf => "\r\n",
44 }
45 }
46}
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum BulletMarker {
51 Dash,
53 Asterisk,
55 Plus,
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum OrderedDelimiter {
63 Period,
65 Paren,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
72#[non_exhaustive]
73pub struct SerializeOptions {
74 pub line_ending: LineEnding,
76 pub final_newline: bool,
78 pub bullet: Option<BulletMarker>,
81 pub ordered_delimiter: Option<OrderedDelimiter>,
84 pub fence_marker: Option<FenceMarker>,
88}
89
90impl Default for SerializeOptions {
91 fn default() -> Self {
92 Self {
93 line_ending: LineEnding::Lf,
94 final_newline: true,
95 bullet: None,
96 ordered_delimiter: None,
97 fence_marker: None,
98 }
99 }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
104pub enum SerializeError {
105 InvalidDocument(Vec<Diagnostic>),
107}
108
109impl Document {
110 pub fn to_markdown(&self) -> Result<String, SerializeError> {
114 self.to_markdown_with(&SerializeOptions::default())
115 }
116
117 pub fn to_markdown_with(&self, options: &SerializeOptions) -> Result<String, SerializeError> {
122 Ok(self.write(options)?.0)
123 }
124
125 pub(crate) fn write(
129 &self,
130 options: &SerializeOptions,
131 ) -> Result<(String, usize), SerializeError> {
132 let diagnostics = validate_document(self);
133 if !diagnostics.is_empty() {
134 return Err(SerializeError::InvalidDocument(diagnostics));
135 }
136 let current;
139 let definitions = Definitions::of(self);
140 let blocks = if leaves::any_kept(&self.children) {
141 let mut blocks = self.children.clone();
142 leaves::visit_mut(&mut blocks, &mut |leaf| {
143 if let Some(text) = leaf.source.as_deref() {
144 if crate::parse::parse_inline_tree(text, &definitions).inlines != *leaf.inlines
145 {
146 *leaf.source = None;
147 }
148 }
149 });
150 current = blocks;
151 ¤t
152 } else {
153 &self.children
154 };
155 let Some(source) = self.source() else {
156 return Ok((write_canonical(blocks, options)?, 0));
157 };
158 let base = crate::parse(source).document;
161 let write =
162 |whole: &[bool]| write_reusing(blocks, &base, source, options, &definitions, whole);
163 let opens_with_break = matches!(blocks.first(), Some(Block::ThematicBreak(_)));
166 let mut whole = alloc::vec![false; blocks.len()];
167 let (mut output, mut partly) = write(&whole)?;
168 if opens_with_break && opens_frontmatter(&output) {
169 whole[0] = true;
170 (output, partly) = write(&whole)?;
171 }
172 if partly.is_empty() {
173 return Ok((output, 0));
174 }
175 let misread = misread(blocks, &output, &partly);
176 if misread.is_empty() {
177 return Ok((output, 0));
178 }
179 for index in &misread {
180 whole[*index] = true;
181 }
182 output = write(&whole)?.0;
183 if opens_with_break && !whole[0] && opens_frontmatter(&output) {
184 whole[0] = true;
185 output = write(&whole)?.0;
186 }
187 Ok((output, misread.len()))
188 }
189}
190
191fn opens_frontmatter(output: &str) -> bool {
194 let mut lines = output.split(['\n', '\r']);
195 let opening = lines.next().and_then(crate::parse::frontmatter_fence_kind);
196 opening.is_some() && lines.any(|line| crate::parse::frontmatter_fence_kind(line) == opening)
197}
198
199fn write_canonical(blocks: &[Block], options: &SerializeOptions) -> Result<String, SerializeError> {
201 let mut output = write_blocks(blocks, options, Join::Gap, true, None)?;
202 if options.line_ending == LineEnding::CrLf {
203 output = lf_to_crlf(&output);
204 }
205 let ends_with_blank_line = matches!(blocks.last(), Some(Block::BlankLine(_)));
210 if options.final_newline
211 && !blocks.is_empty()
212 && (ends_with_blank_line || !ends_with_carriage_return_ending(&output))
213 {
214 output.push_str(options.line_ending.as_str());
215 }
216 Ok(output)
217}
218
219fn write_reusing(
237 blocks: &[Block],
238 base_document: &Document,
239 source: &str,
240 options: &SerializeOptions,
241 definitions: &Definitions,
242 whole: &[bool],
243) -> Result<(String, Vec<usize>), SerializeError> {
244 let base = &base_document.children;
245 let mut correspondence = Correspondence {
246 base: base_document,
247 sources: None,
248 };
249 let base_definitions = Definitions::of(base_document);
250 let mut reuse = reuse::Reuse {
251 source,
252 options,
253 correspondence: &mut correspondence,
254 definitions,
255 base_definitions: &base_definitions,
256 };
257 let span_of = |block: &Block| block.span().expect("a reused block has a span");
258 let mut outs = Vec::with_capacity(blocks.len());
259 let mut open_lists = alloc::vec![false; blocks.len()];
261 for (index, block) in blocks.iter().enumerate() {
262 let found = block.span().and_then(|span| {
263 let at = base
264 .binary_search_by_key(&span.start, |base| {
265 base.span().map_or(usize::MAX, |span| span.start)
266 })
267 .ok()?;
268 (base[at].span() == Some(span)
269 && core::mem::discriminant(&base[at]) == core::mem::discriminant(block))
270 .then_some(at)
271 });
272 outs.push(match found {
273 Some(at) => match reuse.block(block, &base[at])? {
274 reuse::Out::Kept(_) => (Some(at), None),
275 reuse::Out::Lines(_) | reuse::Out::OpenList(_) if whole[index] => (None, None),
276 reuse::Out::Lines(lines) => (None, Some((at, lines))),
277 reuse::Out::OpenList(lines) => {
278 open_lists[index] = true;
279 (None, Some((at, lines)))
280 }
281 reuse::Out::Render => (None, None),
282 },
283 None => (None, None),
284 });
285 }
286 let mut reused: Vec<Option<usize>> = outs.iter().map(|(at, _)| *at).collect();
287 let mut partly: Vec<Option<(usize, Vec<reuse::Line>)>> =
289 outs.into_iter().map(|(_, lines)| lines).collect();
290 let placed =
293 |reused: &[Option<usize>], partly: &[Option<(usize, Vec<reuse::Line>)>], index: usize| {
294 reused[index].or(partly[index].as_ref().map(|(at, _)| *at))
295 };
296 if blocks.len() == base.len()
297 && reused
298 .iter()
299 .enumerate()
300 .all(|(index, at)| *at == Some(index))
301 {
302 return Ok((source.into(), Vec::new()));
303 }
304 let separated = separated_boundaries(blocks);
308 let base_separated = separated_boundaries(base);
309 let keeps_gap = |partly: &[Option<(usize, Vec<reuse::Line>)>], index: usize, at: usize| {
314 if index == 0 || at == 0 || separated[index] != base_separated[at] {
315 return false;
316 }
317 let left = base[at - 1].span().expect("a parsed block has a span");
318 let right = base[at].span().expect("a parsed block has a span");
319 let left_end = left.start + strip_line_ending(&source[left.start..left.end]).len();
320 reuse::keeps_gap(
321 source,
322 &source[left_end..right.start],
323 (
324 &blocks[index - 1],
325 left,
326 partly[index - 1]
327 .as_ref()
328 .map(|(_, lines)| lines.as_slice()),
329 ),
330 (
331 &blocks[index],
332 right,
333 partly[index].as_ref().map(|(_, lines)| lines.as_slice()),
334 ),
335 open_lists[index - 1],
336 true,
337 )
338 };
339 for index in (0..blocks.len().saturating_sub(1)).rev() {
342 let Some(at) = placed(&reused, &partly, index) else {
343 continue;
344 };
345 if placed(&reused, &partly, index + 1) == Some(at + 1)
346 && keeps_gap(&partly, index + 1, at + 1)
347 {
348 continue;
349 }
350 match &partly[index] {
351 Some((_, lines)) => {
352 let mut text = reuse::join(lines, "\n");
353 text.push('\n');
354 if !closed_by_a_blank_line(&text) {
355 partly[index] = None;
356 }
357 }
358 None => {
359 let span = span_of(&blocks[index]);
360 if !closed_by_a_blank_line(&source[span.start..span.end]) {
361 reused[index] = None;
362 }
363 }
364 }
365 }
366
367 let mut output = String::with_capacity(source.len());
368 let mut previous_at: Option<usize> = None;
372 let mut placement = reuse::Placement::default();
373 let mut previous_ending = "";
374 let mut previous_body_end = 0;
375 let mut ends_with_source_tail = false;
378 let mut run_follows = true;
381 for (index, block) in blocks.iter().enumerate() {
382 if let (Block::List(list), Some(_)) = (block, &partly[index]) {
385 if placement.previous_list() == Some(written_marker(list)) {
386 partly[index] = None;
387 }
388 }
389 let follows_predecessor = matches!(
390 (previous_at, placed(&reused, &partly, index)),
391 (Some(previous), Some(at)) if previous + 1 == at && keeps_gap(&partly, index, at)
392 );
393 let follows_run = follows_predecessor && run_follows;
398 run_follows = index == 0 || !matches!(block, Block::BlankLine(_)) || follows_run;
399 let taken_in = |text: &str| placement.takes_in(block, text, follows_run);
402 if let Some((_, lines)) = &partly[index] {
403 if taken_in(&lines[0].body) {
404 partly[index] = None;
405 }
406 }
407 let mut at = reused[index];
408 if let Some(position) = at {
409 let start = span_of(block).start;
410 let rewritten = matches!(block, Block::ThematicBreak(_))
415 && index == 0
416 && (position != 0 || whole[0] && source[start..].starts_with("---"))
417 || taken_in(&source[start..]);
418 if rewritten {
419 at = None;
420 }
421 }
422 if index > 0 {
423 if follows_predecessor {
424 let start = span_of(block).start;
425 output.push_str(&source[previous_body_end..start]);
426 } else if separated[index] {
427 output.push_str(previous_ending);
430 match previous_ending {
431 "" => push_gap(&mut output, options),
432 "\r" => output.push('\r'),
433 _ => output.push_str(options.line_ending.as_str()),
434 }
435 } else {
436 output.push_str(match previous_ending {
438 "" => options.line_ending.as_str(),
439 own => own,
440 });
441 }
442 }
443 previous_ending = "";
444 match at {
445 Some(position) => {
446 let span = span_of(block);
447 let body = strip_line_ending(&source[span.start..span.end]);
448 let body_end = span.start + body.len();
449 if index == 0 && position == 0 {
450 output.push_str(&source[..span.start]);
451 }
452 output.push_str(body);
453 if index + 1 == blocks.len() && position + 1 == base.len() {
454 output.push_str(&source[body_end..]);
455 ends_with_source_tail = true;
456 }
457 previous_ending = &source[body_end..span.end];
458 previous_body_end = body_end;
459 let marker = match block {
460 Block::List(list) => Some(written_marker(list)),
461 _ => None,
462 };
463 placement.wrote(block, marker, false);
464 }
465 None if partly[index].is_some() => {
466 let (position, lines) = partly[index].as_ref().expect("written partly");
467 let span = base[*position].span().expect("a parsed block has a span");
468 let body_end = span.start + strip_line_ending(&source[span.start..span.end]).len();
469 if index == 0 && *position == 0 {
470 output.push_str(&source[..span.start]);
471 }
472 output.push_str(&reuse::join(lines, options.line_ending.as_str()));
473 if index + 1 == blocks.len() && *position + 1 == base.len() {
474 output.push_str(&source[body_end..]);
475 ends_with_source_tail = true;
476 }
477 previous_body_end = body_end;
478 let marker = match block {
479 Block::List(list) => Some(written_marker(list)),
480 _ => None,
481 };
482 placement.wrote(block, marker, open_lists[index]);
483 at = Some(*position);
484 }
485 None => {
486 let mut text = write_blocks(
487 core::slice::from_ref(block),
488 options,
489 Join::Gap,
490 index == 0,
491 placement.previous_list(),
492 )?;
493 if options.line_ending == LineEnding::CrLf {
494 text = lf_to_crlf(&text);
495 }
496 output.push_str(&text);
497 let marker = match block {
498 Block::List(list) => {
499 Some(list_marker(list, options, placement.previous_list()))
500 }
501 _ => None,
502 };
503 placement.wrote(block, marker, false);
504 }
505 }
506 previous_at = at;
507 }
508 let ends_with_blank_line = matches!(blocks.last(), Some(Block::BlankLine(_)));
509 if !ends_with_source_tail
510 && options.final_newline
511 && !blocks.is_empty()
512 && (ends_with_blank_line || !ends_with_carriage_return_ending(&output))
513 {
514 output.push_str(match previous_ending {
517 "" => options.line_ending.as_str(),
518 own => own,
519 });
520 }
521 let partly = partly
522 .iter()
523 .enumerate()
524 .filter_map(|(index, lines)| lines.as_ref().map(|_| index))
525 .collect();
526 Ok((output, partly))
527}
528
529fn misread(blocks: &[Block], output: &str, partly: &[usize]) -> Vec<usize> {
534 let read = crate::parse(output).document.children;
535 if read.len() != blocks.len() {
536 return partly.to_vec();
537 }
538 partly
539 .iter()
540 .copied()
541 .filter(|index| !reads_as(&read[*index], &blocks[*index]))
542 .collect()
543}
544
545fn reads_as(read: &Block, block: &Block) -> bool {
549 let mut read = crate::compare::normalized_blocks(core::slice::from_ref(read));
550 let mut block = crate::compare::normalized_blocks(core::slice::from_ref(block));
551 number_items(&mut read);
552 number_items(&mut block);
553 read == block
554}
555
556fn number_items(blocks: &mut [Block]) {
558 for block in blocks {
559 match block {
560 Block::List(list) => {
561 let mut previous = None;
562 for item in &mut list.children {
563 if list.ordered {
564 item.number = Some(item_number(item, &mut previous));
565 }
566 number_items(&mut item.children);
567 }
568 }
569 Block::BlockQuote(node) => number_items(&mut node.children),
570 Block::Alert(node) => number_items(&mut node.children),
571 Block::FootnoteDefinition(node) => number_items(&mut node.children),
572 Block::ContainerDirective(node) => number_items(&mut node.children),
573 Block::HtmlContainer(node) => {
574 if let HtmlContainerContent::Blocks(children) = &mut node.content {
575 number_items(children);
576 }
577 }
578 Block::Paragraph(_)
579 | Block::Heading(_)
580 | Block::ThematicBreak(_)
581 | Block::CodeBlock(_)
582 | Block::HtmlBlock(_)
583 | Block::Definition(_)
584 | Block::Table(_)
585 | Block::MathBlock(_)
586 | Block::Frontmatter(_)
587 | Block::LeafDirective(_)
588 | Block::BlankLine(_) => {}
589 }
590 }
591}
592
593struct Correspondence<'a> {
598 base: &'a Document,
599 sources: Option<BTreeMap<(usize, usize), InlineSource>>,
602}
603
604impl Correspondence<'_> {
605 fn sources(&mut self) -> &BTreeMap<(usize, usize), InlineSource> {
607 let base_document = self.base;
608 self.sources.get_or_insert_with(|| {
609 let mut sources = BTreeMap::new();
610 base_document.each_leaf_source(|_, span, source| {
611 sources.insert((span.start, span.end), source);
612 });
613 sources
614 })
615 }
616
617 fn source_of(&mut self, span: Span) -> Option<&InlineSource> {
619 self.sources().get(&(span.start, span.end))
620 }
621
622 fn holds(&mut self, block: &Block, base: &Block) -> bool {
623 if !leaves::any_kept(core::slice::from_ref(block)) {
624 return block == base;
625 }
626 let sources = self.sources();
627 let mut block = block.clone();
630 leaves::visit_mut(core::slice::from_mut(&mut block), &mut |leaf| {
631 let (Some(text), Some(span)) = (leaf.source.as_deref(), leaf.span) else {
632 return;
633 };
634 if let Some(source) = sources
635 .get(&(span.start, span.end))
636 .filter(|source| source.text == text)
637 {
638 crate::parse::inline_spans_to_source(source, leaf.inlines);
639 *leaf.source = None;
640 }
641 });
642 block == *base
643 }
644}
645
646fn separated_boundaries(blocks: &[Block]) -> Vec<bool> {
650 let mut separated = alloc::vec![false; blocks.len()];
651 let mut block_follows = false;
652 for index in (1..blocks.len()).rev() {
653 block_follows |= !matches!(blocks[index], Block::BlankLine(_));
654 separated[index] = block_follows && !matches!(blocks[index - 1], Block::BlankLine(_));
655 }
656 separated
657}
658
659fn moves_into_previous(
671 block: &Block,
672 text: &str,
673 last: Option<&Block>,
674 previous_list: Option<char>,
675) -> bool {
676 match block {
677 Block::BlankLine(_) => false,
678 Block::List(list) if previous_list == Some(written_marker(list)) => true,
679 _ if matches!(last, Some(Block::List(_) | Block::FootnoteDefinition(_))) => {
680 text.starts_with([' ', '\t'])
681 }
682 Block::CodeBlock(_) => false,
683 _ => opens_with_code_indent(text),
684 }
685}
686
687fn opens_with_code_indent(text: &str) -> bool {
690 let mut column = 0;
691 for char in text.chars() {
692 match char {
693 ' ' => column += 1,
694 '\t' => column += 4 - column % 4,
695 _ => break,
696 }
697 if column >= 4 {
698 return true;
699 }
700 }
701 false
702}
703
704fn closed_by_a_blank_line(text: &str) -> bool {
709 let gap = if text.ends_with('\n') {
710 "\n"
711 } else if text.ends_with('\r') {
712 "\r"
713 } else {
714 "\n\n"
715 };
716 let probe = format!("{text}{gap}x");
717 let children = crate::parse(&probe).document.children;
718 let Some((Block::Paragraph(last), rest)) = children.split_last() else {
719 return false;
720 };
721 last.meta
722 .span
723 .is_some_and(|span| span.start + 1 == probe.len())
724 && rest.iter().all(|block| {
725 matches!(block, Block::BlankLine(_))
726 || block.span().is_some_and(|span| span.end <= text.len())
727 })
728}
729
730fn push_gap(output: &mut String, options: &SerializeOptions) {
732 let from = output.len();
733 push_block_gap(output);
734 if options.line_ending == LineEnding::CrLf {
735 let gap = lf_to_crlf(&output[from..]);
736 output.truncate(from);
737 output.push_str(&gap);
738 }
739}
740
741fn strip_line_ending(text: &str) -> &str {
743 text.strip_suffix("\r\n")
744 .or_else(|| text.strip_suffix('\n'))
745 .or_else(|| text.strip_suffix('\r'))
746 .unwrap_or(text)
747}
748
749fn lf_to_crlf(input: &str) -> String {
752 let mut output = String::with_capacity(input.len());
753 let mut previous = '\0';
754 for ch in input.chars() {
755 if ch == '\n' && previous != '\r' {
756 output.push('\r');
757 }
758 output.push(ch);
759 previous = ch;
760 }
761 output
762}
763
764fn ends_with_carriage_return_ending(input: &str) -> bool {
765 input.ends_with('\r') || input.ends_with("\r\n")
766}
767
768fn push_block_gap(output: &mut String) {
772 if output.ends_with('\r') {
773 output.push('\r');
774 } else if output.ends_with("\r\n") {
775 output.push('\n');
776 } else {
777 output.push_str("\n\n");
778 }
779}
780
781#[derive(Clone, Copy, Eq, PartialEq)]
783enum Join {
784 Gap,
786 Line,
788}
789
790fn write_blocks(
798 blocks: &[Block],
799 options: &SerializeOptions,
800 join: Join,
801 document_start: bool,
802 previous_list: Option<char>,
803) -> Result<String, SerializeError> {
804 let mut output = String::new();
805 let mut previous_list = previous_list;
808 let mut wrote_block = false;
811 let mut blank_lines = 0;
812 for (index, block) in blocks.iter().enumerate() {
813 if let Block::BlankLine(_) = block {
814 blank_lines += 1;
815 continue;
816 }
817 if wrote_block {
818 match join {
819 Join::Gap => push_block_gap(&mut output),
820 Join::Line => output.push('\n'),
821 }
822 }
823 output.extend(core::iter::repeat_n('\n', blank_lines));
824 blank_lines = 0;
825 let after_paragraph_line =
826 join == Join::Line && index > 0 && matches!(blocks[index - 1], Block::Paragraph(_));
827 let written = match block {
828 Block::List(list) => {
829 let marker = list_marker(list, options, previous_list);
830 previous_list = Some(marker);
831 write_list(list, options, marker)?
832 }
833 Block::ThematicBreak(node) => {
834 previous_list = None;
835 write_thematic_break(node, document_start && index == 0, after_paragraph_line)
836 }
837 _ => {
838 previous_list = None;
839 write_block(block, options)?
840 }
841 };
842 output.push_str(&written);
843 wrote_block = true;
844 }
845 let trailing = if wrote_block {
848 blank_lines
849 } else {
850 blank_lines.saturating_sub(1)
851 };
852 output.extend(core::iter::repeat_n('\n', trailing));
853 Ok(output)
854}
855
856fn write_block(block: &Block, options: &SerializeOptions) -> Result<String, SerializeError> {
860 match block {
861 Block::BlankLine(_) => Ok(String::new()),
862 Block::Paragraph(node) => match &node.source {
863 Some(text) => Ok(text.clone()),
864 None => write_inlines(&node.children, Context::BLOCK),
865 },
866 Block::Heading(node) => write_heading(node),
867 Block::ThematicBreak(node) => Ok(write_thematic_break(node, false, false)),
868 Block::BlockQuote(node) => {
869 let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
870 Ok(if inner.is_empty() {
872 ">".into()
873 } else {
874 quote_lines(&inner)
875 })
876 }
877 Block::Alert(node) => write_alert(node, options),
878 Block::List(node) => write_list(node, options, list_marker(node, options, None)),
879 Block::CodeBlock(node) => Ok(write_code_block(node, options)),
880 Block::HtmlBlock(node) => Ok(node.value.clone()),
884 Block::HtmlContainer(node) => write_html_container(node, options),
885 Block::Definition(node) => Ok(write_definition(node)),
886 Block::FootnoteDefinition(node) => {
887 let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
888 Ok(if inner.starts_with([' ', '\t']) {
892 format!(
893 "[^{}]:\n{}",
894 node.label,
895 prefix_container_lines(&inner, " ")
896 )
897 } else {
898 format!("[^{}]: {}", node.label, indent_continuation(&inner))
899 })
900 }
901 Block::Table(node) => write_table(node),
902 Block::MathBlock(node) => {
903 let fence = block_math_fence(&node.value);
904 Ok(fenced_body(&fence, &node.value, &fence))
905 }
906 Block::Frontmatter(node) => {
907 let fence = match node.kind {
908 FrontmatterKind::Yaml => "---",
909 FrontmatterKind::Toml => "+++",
910 };
911 Ok(format!("{fence}\n{}\n{fence}", node.value))
914 }
915 Block::LeafDirective(node) => Ok(format!(
916 "::{}{}{}",
917 node.name,
918 write_directive_label(&node.label, node.label_source.as_deref())?,
919 write_attributes(&node.attributes)
920 )),
921 Block::ContainerDirective(node) => {
922 let mut inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
923 let fence = directive_fence(&inner);
924 if !node.children.is_empty() {
928 inner.push('\n');
929 }
930 Ok(format!(
931 "{fence}{}{}{}\n{inner}{fence}",
932 node.name,
933 write_directive_label(&node.label, node.label_source.as_deref())?,
934 write_attributes(&node.attributes)
935 ))
936 }
937 }
938}
939
940fn write_thematic_break(
944 node: &ThematicBreak,
945 at_document_start: bool,
946 after_paragraph_line: bool,
947) -> String {
948 match node.marker {
949 ThematicBreakMarker::Dash if at_document_start || after_paragraph_line => "- - -".into(),
950 ThematicBreakMarker::Dash => "---".into(),
951 ThematicBreakMarker::Asterisk => "***".into(),
952 ThematicBreakMarker::Underscore => "___".into(),
953 }
954}
955
956pub(crate) fn writes_setext(node: &Heading) -> bool {
959 node.kind == HeadingKind::Setext && matches!(node.depth, 1 | 2) && !node.children.is_empty()
960}
961
962fn write_heading(node: &Heading) -> Result<String, SerializeError> {
963 let setext = writes_setext(node);
964 let content = match one_line(node.source.as_deref(), !setext) {
965 Some(text) => text.into(),
966 None => write_inlines(&node.children, Context::HEADING)?,
967 };
968 if setext {
969 let marker = if node.depth == 1 { "=" } else { "-" };
970 return Ok(format!(
971 "{content}\n{}",
972 marker.repeat(content.len().max(3))
973 ));
974 }
975 let hashes = "#".repeat(usize::from(node.depth));
976 Ok(if content.is_empty() {
977 hashes
978 } else if reads_as_closing_sequence(&content) {
979 format!("{hashes} {content} #")
982 } else {
983 format!("{hashes} {content}")
984 })
985}
986
987fn reads_as_closing_sequence(content: &str) -> bool {
990 let before = content.trim_end_matches('#');
991 before.len() < content.len() && (before.is_empty() || before.ends_with([' ', '\t']))
992}
993
994fn write_definition(node: &Definition) -> String {
995 let destination = write_destination(&node.destination, node.destination_kind);
996 let mut output = format!("[{}]: {}", node.label, destination);
997 if let Some(title) = &node.title {
998 output.push(' ');
999 output.push_str(&write_title(title));
1000 }
1001 output
1002}
1003
1004fn write_html_container(
1005 node: &HtmlContainer,
1006 options: &SerializeOptions,
1007) -> Result<String, SerializeError> {
1008 match &node.content {
1009 HtmlContainerContent::Blocks(children) => {
1010 let inner = write_blocks(children, options, Join::Gap, false, None)?;
1011 let blank_lines = children
1014 .iter()
1015 .all(|block| matches!(block, Block::BlankLine(_)));
1016 Ok(if blank_lines {
1017 format!(
1018 "{}\n{}{}",
1019 node.opening.raw,
1020 "\n".repeat(children.len()),
1021 node.closing.raw
1022 )
1023 } else {
1024 format!("{}\n{}\n\n{}", node.opening.raw, inner, node.closing.raw)
1025 })
1026 }
1027 HtmlContainerContent::Inlines(children) => Ok(format!(
1028 "{}{}{}",
1029 node.opening.raw,
1030 write_inlines(children, Context::BLOCK)?,
1031 node.closing.raw
1032 )),
1033 }
1034}
1035
1036fn write_alert(node: &Alert, options: &SerializeOptions) -> Result<String, SerializeError> {
1037 let mut output = String::from("> [!");
1038 output.push_str(alert_kind_name(node.kind));
1039 output.push(']');
1040 if let Some(title) = &node.title {
1041 if !title.is_empty() {
1042 output.push(' ');
1043 output.push_str(&title.replace(['\n', '\r'], " "));
1045 }
1046 }
1047 let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
1048 if !inner.is_empty() {
1049 output.push('\n');
1050 output.push_str("e_lines(&inner));
1051 } else if !node.children.is_empty() {
1052 output.push_str("\n>");
1054 }
1055 Ok(output)
1056}
1057
1058fn quote_lines(inner: &str) -> String {
1061 let mut output = prefix_container_lines(inner, "> ");
1062 if inner.ends_with(['\n', '\r']) {
1063 output.push('>');
1064 }
1065 output
1066}
1067
1068fn alert_kind_name(kind: AlertKind) -> &'static str {
1069 match kind {
1070 AlertKind::Note => "NOTE",
1071 AlertKind::Tip => "TIP",
1072 AlertKind::Important => "IMPORTANT",
1073 AlertKind::Warning => "WARNING",
1074 AlertKind::Caution => "CAUTION",
1075 }
1076}
1077
1078fn list_marker(list: &List, options: &SerializeOptions, previous: Option<char>) -> char {
1084 let marker = if list.ordered {
1085 options
1086 .ordered_delimiter
1087 .map_or_else(|| written_marker(list), ordered_delimiter_char)
1088 } else {
1089 options
1090 .bullet
1091 .map_or_else(|| written_marker(list), bullet_char)
1092 };
1093 if previous == Some(marker) {
1094 let order: &[char] = if list.ordered {
1095 &['.', ')']
1096 } else {
1097 &['-', '*', '+']
1098 };
1099 let at = order.iter().position(|char| *char == marker).unwrap_or(0);
1100 return order[(at + 1) % order.len()];
1101 }
1102 marker
1103}
1104
1105pub(crate) fn written_marker(list: &List) -> char {
1109 match (list.ordered, list.delimiter) {
1110 (false, ListDelimiter::Asterisk) => '*',
1111 (false, ListDelimiter::Plus) => '+',
1112 (false, _) => '-',
1113 (true, ListDelimiter::Paren) => ')',
1114 (true, _) => '.',
1115 }
1116}
1117
1118fn bullet_char(bullet: BulletMarker) -> char {
1119 match bullet {
1120 BulletMarker::Dash => '-',
1121 BulletMarker::Asterisk => '*',
1122 BulletMarker::Plus => '+',
1123 }
1124}
1125
1126fn ordered_delimiter_char(delimiter: OrderedDelimiter) -> char {
1127 match delimiter {
1128 OrderedDelimiter::Period => '.',
1129 OrderedDelimiter::Paren => ')',
1130 }
1131}
1132
1133fn write_list(
1134 node: &List,
1135 options: &SerializeOptions,
1136 marker_char: char,
1137) -> Result<String, SerializeError> {
1138 let mut output = String::new();
1139 let mut previous: Option<u64> = None;
1141 for (index, item) in node.children.iter().enumerate() {
1142 if index > 0 {
1143 output.push_str(if node.tight { "\n" } else { "\n\n" });
1144 }
1145 output.push_str(&write_item(
1146 node,
1147 item,
1148 options,
1149 marker_char,
1150 &mut previous,
1151 )?);
1152 }
1153 Ok(output)
1154}
1155
1156fn item_number(item: &ListItem, previous: &mut Option<u64>) -> u64 {
1159 let number = item.number.unwrap_or_else(|| {
1162 previous.map_or(1, |previous| {
1163 previous
1164 .saturating_add(1)
1165 .min(crate::parse::MAX_ORDERED_NUMBER)
1166 })
1167 });
1168 *previous = Some(number);
1169 number
1170}
1171
1172pub(crate) fn written_opens_fence_like(block: &Block) -> bool {
1176 write_block(block, &SerializeOptions::default()).is_ok_and(|written| {
1177 let content = written.trim_start_matches(' ');
1178 written.len() - content.len() < 4 && content.starts_with(['`', '~'])
1179 })
1180}
1181
1182fn write_item(
1185 list: &List,
1186 item: &ListItem,
1187 options: &SerializeOptions,
1188 marker_char: char,
1189 previous: &mut Option<u64>,
1190) -> Result<String, SerializeError> {
1191 let marker = if list.ordered {
1192 format!("{}{marker_char} ", item_number(item, previous))
1193 } else {
1194 format!("{marker_char} ")
1195 };
1196 let inner = write_item_blocks(&item.children, options, list.tight, item.checked)?;
1197 Ok(
1198 if starts_below_bullet(list, item, marker_char, first_line(&inner)) {
1199 format!(
1200 "{}\n{}",
1201 marker.trim_end(),
1202 prefix_container_lines(&inner, &" ".repeat(marker.len()))
1203 )
1204 } else {
1205 format!("{marker}{}", indent_after_first_line(&inner, marker.len()))
1206 },
1207 )
1208}
1209
1210fn starts_below_bullet(list: &List, item: &ListItem, marker_char: char, first_line: &str) -> bool {
1216 opens_with_padding(first_line)
1217 || (!list.ordered
1218 && item.checked.is_none()
1219 && reads_as_thematic_break(marker_char, first_line))
1220}
1221
1222pub(super) fn reads_as_thematic_break(marker_char: char, rest: &str) -> bool {
1225 matches!(marker_char, '-' | '*')
1226 && rest
1227 .chars()
1228 .all(|char| matches!(char, ' ' | '\t') || char == marker_char)
1229 && rest.chars().filter(|char| *char == marker_char).count() >= 2
1230}
1231
1232fn item_first_line(
1235 list: &List,
1236 item: &ListItem,
1237 marker_char: char,
1238 options: &SerializeOptions,
1239) -> String {
1240 let marker = if list.ordered {
1241 format!("{}{marker_char}", item.number.unwrap_or(1))
1242 } else {
1243 String::from(marker_char)
1244 };
1245 let content = item_content_first_line(item, options);
1246 if starts_below_bullet(list, item, marker_char, &content) {
1247 marker
1248 } else {
1249 format!("{marker} {content}")
1250 }
1251}
1252
1253fn item_content_first_line(item: &ListItem, options: &SerializeOptions) -> String {
1257 if item.checked.is_some() {
1258 return String::from("[");
1259 }
1260 match item.children.first() {
1261 None => String::new(),
1262 Some(Block::List(nested)) => match nested.children.first() {
1263 Some(first) => {
1264 item_first_line(nested, first, list_marker(nested, options, None), options)
1265 }
1266 None => String::new(),
1267 },
1268 Some(
1269 block @ (Block::Paragraph(_)
1270 | Block::Heading(_)
1271 | Block::HtmlBlock(_)
1272 | Block::CodeBlock(_)
1273 | Block::ThematicBreak(_)),
1274 ) => write_block(block, options)
1275 .map(|written| String::from(first_line(&written)))
1276 .unwrap_or_default(),
1277 Some(_) => String::from(">"),
1279 }
1280}
1281
1282fn first_line(text: &str) -> &str {
1284 text.split(['\n', '\r']).next().unwrap_or_default()
1285}
1286
1287fn opens_with_padding(inner: &str) -> bool {
1292 !inner.starts_with(" ") && inner.starts_with([' ', '\t'])
1293}
1294
1295pub(crate) fn written_after_paragraph_interrupts(block: &Block) -> bool {
1301 match block {
1302 Block::Heading(heading) => !writes_setext(heading),
1303 Block::List(list) => {
1304 crate::parse::interrupts_paragraph(block) && !first_item_starts_below_its_bullet(list)
1305 }
1306 _ => crate::parse::interrupts_paragraph(block),
1307 }
1308}
1309
1310pub(crate) fn first_item_starts_below_its_bullet(list: &List) -> bool {
1315 let Some(item) = list.children.first() else {
1316 return false;
1317 };
1318 let options = SerializeOptions::default();
1319 let content = item_content_first_line(item, &options);
1320 starts_below_bullet(list, item, list_marker(list, &options, None), &content)
1321}
1322
1323fn write_item_blocks(
1327 blocks: &[Block],
1328 options: &SerializeOptions,
1329 tight: bool,
1330 task: Option<bool>,
1331) -> Result<String, SerializeError> {
1332 let join = if tight { Join::Line } else { Join::Gap };
1333 let Some(checked) = task else {
1334 return write_blocks(blocks, options, join, false, None);
1335 };
1336 let checkbox = if checked { "[x]" } else { "[ ]" };
1337 let definitions = blocks
1338 .iter()
1339 .take_while(|block| matches!(block, Block::Definition(_)))
1340 .count();
1341 if matches!(blocks.get(definitions), Some(Block::Paragraph(_))) {
1342 let before = write_blocks(&blocks[..definitions], options, join, false, None)?;
1343 let rest = write_blocks(&blocks[definitions..], options, join, false, None)?;
1344 let mut output = before;
1345 if !output.is_empty() {
1346 match join {
1347 Join::Gap => push_block_gap(&mut output),
1348 Join::Line => output.push('\n'),
1349 }
1350 }
1351 output.push_str(checkbox);
1352 output.push(' ');
1353 output.push_str(&rest);
1354 return Ok(output);
1355 }
1356 let written = write_blocks(blocks, options, join, false, None)?;
1357 Ok(if written.is_empty() {
1358 checkbox.into()
1359 } else {
1360 format!("{checkbox} {written}")
1361 })
1362}
1363
1364fn write_code_block(node: &CodeBlock, options: &SerializeOptions) -> String {
1365 match node.kind {
1366 CodeBlockKind::Indented => {
1367 let body = trim_trailing_newline(&node.value);
1370 let mut output = prefix_lines(body, " ");
1371 let ending = &node.value[body.len()..];
1372 if matches!(ending, "\r" | "\r\n") {
1373 output.push_str(ending);
1374 }
1375 output
1376 }
1377 CodeBlockKind::Fenced { marker, length } => {
1378 let marker = code_block_fence_marker(node, marker, options);
1379 let (fence, indent) = code_block_fence(&node.value, marker, length.max(3));
1380 let mut opener = fence.clone();
1381 if let Some(info) = &node.info {
1382 opener.push(' ');
1383 opener.push_str(&escape_code_info(info));
1384 }
1385 let body = fenced_body(&opener, &node.value, &fence);
1386 if indent == 0 {
1387 body
1388 } else {
1389 prefix_lines(&body, &" ".repeat(indent))
1390 }
1391 }
1392 }
1393}
1394
1395fn fenced_body(opener: &str, value: &str, closer: &str) -> String {
1399 let mut output = String::with_capacity(opener.len() + value.len() + closer.len() + 2);
1400 output.push_str(opener);
1401 output.push('\n');
1402 output.push_str(value);
1403 if !value.is_empty() && !ends_with_line_ending(value) {
1404 output.push('\n');
1405 }
1406 output.push_str(closer);
1407 output
1408}
1409
1410fn code_block_fence_marker(
1414 node: &CodeBlock,
1415 marker: FenceMarker,
1416 options: &SerializeOptions,
1417) -> FenceMarker {
1418 if node.info.as_deref().is_some_and(|info| info.contains('`')) {
1419 return FenceMarker::Tilde;
1420 }
1421 options.fence_marker.unwrap_or(marker)
1422}
1423
1424fn escape_code_info(input: &str) -> String {
1425 let mut output = String::new();
1426 let inner_start = input.len() - input.trim_start_matches([' ', '\t']).len();
1429 let inner_end = input.trim_end_matches([' ', '\t']).len().max(inner_start);
1430 for (offset, char) in input.char_indices() {
1431 match char {
1432 ' ' | '\t' if offset < inner_start || offset >= inner_end => {
1433 output.push_str(&char_reference(char));
1434 }
1435 '\t' => output.push(char),
1436 char if char.is_control() => output.push_str(&char_reference(char)),
1437 '\\' | '&' => {
1438 output.push('\\');
1439 output.push(char);
1440 }
1441 _ => output.push(char),
1442 }
1443 }
1444 output
1445}
1446
1447fn char_reference(char: char) -> String {
1449 format!("&#x{:X};", char as u32)
1450}
1451
1452fn write_table(node: &Table) -> Result<String, SerializeError> {
1453 let mut output = String::new();
1454 for (row_index, row) in node.rows.iter().enumerate() {
1455 if row_index > 0 {
1456 output.push('\n');
1457 }
1458 output.push_str(&write_row(row)?);
1459 if row_index == 0 {
1460 output.push('\n');
1461 output.push_str(&write_delimiter_row(&node.alignments));
1462 }
1463 }
1464 Ok(output)
1465}
1466
1467fn write_delimiter_row(alignments: &[TableAlignment]) -> String {
1468 let cells = alignments
1469 .iter()
1470 .map(|alignment| match alignment {
1471 TableAlignment::None => "---",
1472 TableAlignment::Left => ":---",
1473 TableAlignment::Center => ":---:",
1474 TableAlignment::Right => "---:",
1475 })
1476 .collect::<Vec<_>>();
1477 format!("| {} |", cells.join(" | "))
1478}
1479
1480fn write_row(row: &TableRow) -> Result<String, SerializeError> {
1481 let cells = row
1482 .cells
1483 .iter()
1484 .map(|cell| {
1485 Ok(encode_cell(&match one_line(cell.source.as_deref(), true) {
1486 Some(text) => text.into(),
1487 None => write_inlines(&cell.children, Context::BLOCK)?,
1488 }))
1489 })
1490 .collect::<Result<Vec<_>, SerializeError>>()?;
1491 Ok(format!("| {} |", cells.join(" | ")))
1492}
1493
1494fn encode_cell(written: &str) -> String {
1502 let mut output = String::with_capacity(written.len());
1503 let mut backslashes = 0usize;
1504 for char in written.chars() {
1505 if char == '|' && backslashes % 2 == 0 {
1506 output.push('\\');
1507 }
1508 backslashes = if char == '\\' { backslashes + 1 } else { 0 };
1509 output.push(char);
1510 }
1511 output
1512}
1513
1514#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1517struct Context {
1518 heading: bool,
1520}
1521
1522impl Context {
1523 const BLOCK: Self = Self { heading: false };
1524 const HEADING: Self = Self { heading: true };
1525}
1526
1527fn write_inlines(inlines: &[Inline], context: Context) -> Result<String, SerializeError> {
1528 let mut output = InlineOut::default();
1529 for inline in inlines {
1530 write_inline(inline, context, &mut output)?;
1531 }
1532 Ok(output.text)
1533}
1534
1535#[derive(Default)]
1539struct InlineOut {
1540 text: String,
1541 open_runs: Vec<bool>,
1545 scanned: usize,
1547}
1548
1549impl InlineOut {
1550 fn scan_open_runs(&mut self) {
1554 let mut escaped = false;
1555 let mut run = 0;
1556 let mut run_escaped = false;
1557 for byte in self.text.as_bytes()[self.scanned..]
1558 .iter()
1559 .copied()
1560 .chain(core::iter::once(b' '))
1561 {
1562 if byte == b'`' {
1563 if run == 0 {
1564 run_escaped = escaped;
1565 }
1566 run += 1;
1567 escaped = false;
1568 continue;
1569 }
1570 let opening = run - usize::from(run_escaped && run > 0);
1571 if opening > 0 {
1572 if self.open_runs.len() <= opening {
1573 self.open_runs.resize(opening + 1, false);
1574 }
1575 self.open_runs[opening] = true;
1576 }
1577 run = 0;
1578 escaped = byte == b'\\' && !escaped;
1579 }
1580 self.scanned = self.text.len();
1581 }
1582
1583 fn is_open_run(&self, length: usize) -> bool {
1584 self.open_runs.get(length).copied().unwrap_or(false)
1585 }
1586
1587 fn push_opaque(&mut self, write: impl FnOnce(&mut String)) {
1590 self.scan_open_runs();
1591 write(&mut self.text);
1592 self.scanned = self.text.len();
1593 }
1594}
1595
1596impl core::ops::Deref for InlineOut {
1597 type Target = String;
1598
1599 fn deref(&self) -> &String {
1600 &self.text
1601 }
1602}
1603
1604impl core::ops::DerefMut for InlineOut {
1605 fn deref_mut(&mut self) -> &mut String {
1606 &mut self.text
1607 }
1608}
1609
1610fn write_inline(
1611 inline: &Inline,
1612 context: Context,
1613 out: &mut InlineOut,
1614) -> Result<(), SerializeError> {
1615 match inline {
1616 Inline::Text(node) => out.push_str(&node.value),
1617 Inline::Escape(node) => {
1618 out.push('\\');
1619 out.push(node.value);
1620 }
1621 Inline::CharacterReference(node) => out.push_str(&node.reference),
1622 Inline::Emphasis(node) => {
1623 let delimiter = emphasis_delimiter(node.delimiter);
1624 write_span(out, delimiter, &node.children, delimiter, context)?;
1625 }
1626 Inline::Strong(node) => {
1627 let delimiter = emphasis_delimiter(node.delimiter).repeat(2);
1628 write_span(out, &delimiter, &node.children, &delimiter, context)?;
1629 }
1630 Inline::Delete(node) => write_span(out, "~~", &node.children, "~~", context)?,
1631 Inline::Mark(node) => write_span(out, "==", &node.children, "==", context)?,
1632 Inline::InlineFootnote(node) => write_span(out, "^[", &node.children, "]", context)?,
1633 Inline::Shortcode(node) => {
1634 out.push(':');
1635 out.push_str(&node.name);
1636 out.push(':');
1637 }
1638 Inline::Tag(node) => {
1639 out.push('#');
1640 out.push_str(&node.name);
1641 }
1642 Inline::Code(node) => write_code_span(node, out),
1643 Inline::Link(node) => {
1644 write_span(out, "[", &node.children, "](", context)?;
1645 out.push_opaque(|out| {
1646 write_resource(
1647 out,
1648 &node.destination,
1649 node.destination_kind,
1650 node.title.as_ref(),
1651 );
1652 });
1653 }
1654 Inline::Autolink(node) => out.push_opaque(|out| match node.form {
1655 AutolinkForm::Literal => out.push_str(&node.text),
1656 AutolinkForm::Angle => {
1657 out.push('<');
1658 out.push_str(&node.text);
1659 out.push('>');
1660 }
1661 }),
1662 Inline::Image(node) => {
1663 write_span(out, "?;
1664 out.push_opaque(|out| {
1665 write_resource(
1666 out,
1667 &node.destination,
1668 node.destination_kind,
1669 node.title.as_ref(),
1670 );
1671 });
1672 }
1673 Inline::LinkReference(node) => {
1674 write_reference(out, "[", &node.children, node.kind, &node.label, context)?;
1675 }
1676 Inline::ImageReference(node) => {
1677 write_reference(out, "![", &node.alt, node.kind, &node.label, context)?;
1678 }
1679 Inline::Html(node) => out.push_opaque(|out| out.push_str(&node.value)),
1680 Inline::SoftBreak(_) if context.heading => out.push(' '),
1681 Inline::SoftBreak(_) => out.push('\n'),
1682 Inline::LineBreak(node) => match node.kind {
1683 LineBreakKind::Backslash => out.push_str("\\\n"),
1684 LineBreakKind::Spaces => out.push_str(" \n"),
1685 },
1686 Inline::Math(node) => {
1687 let math = write_inline_math(node);
1688 out.push_opaque(|out| out.push_str(&math));
1689 }
1690 Inline::FootnoteReference(node) => {
1691 out.push_str("[^");
1692 out.push_str(&node.label);
1693 out.push(']');
1694 }
1695 Inline::WikiLink(node) => out.push_opaque(|out| {
1696 if node.embed {
1697 out.push('!');
1698 }
1699 out.push_str("[[");
1700 out.push_str(&node.target);
1701 if node.target != node.label {
1702 out.push('|');
1703 out.push_str(&node.label);
1704 }
1705 out.push_str("]]");
1706 }),
1707 Inline::TextDirective(node) => {
1708 let label = if node.label.is_empty() {
1710 String::new()
1711 } else {
1712 format!("[{}]", write_inlines(&node.label, context)?)
1713 };
1714 out.push_opaque(|out| {
1715 out.push(':');
1716 out.push_str(&node.name);
1717 out.push_str(&label);
1718 out.push_str(&write_attributes(&node.attributes));
1719 });
1720 }
1721 }
1722 Ok(())
1723}
1724
1725fn emphasis_delimiter(delimiter: EmphasisDelimiter) -> &'static str {
1726 match delimiter {
1727 EmphasisDelimiter::Asterisk => "*",
1728 EmphasisDelimiter::Underscore => "_",
1729 }
1730}
1731
1732fn write_span(
1734 out: &mut InlineOut,
1735 open: &str,
1736 children: &[Inline],
1737 close: &str,
1738 context: Context,
1739) -> Result<(), SerializeError> {
1740 out.push_str(open);
1741 for child in children {
1742 write_inline(child, context, out)?;
1743 }
1744 out.push_str(close);
1745 Ok(())
1746}
1747
1748fn write_resource(
1750 out: &mut String,
1751 destination: &str,
1752 kind: LinkDestinationKind,
1753 title: Option<&Title>,
1754) {
1755 out.push_str(&write_destination(destination, kind));
1756 if let Some(title) = title {
1757 out.push(' ');
1758 out.push_str(&write_title(title));
1759 }
1760 out.push(')');
1761}
1762
1763fn write_reference(
1769 out: &mut InlineOut,
1770 open: &str,
1771 children: &[Inline],
1772 kind: ReferenceKind,
1773 label: &str,
1774 context: Context,
1775) -> Result<(), SerializeError> {
1776 match kind {
1777 ReferenceKind::Full => write_span(out, open, children, "]", context)?,
1778 ReferenceKind::Shortcut | ReferenceKind::Collapsed => {
1779 out.push_str(open);
1780 out.push_str(label);
1781 out.push(']');
1782 }
1783 }
1784 out.push_opaque(|out| write_reference_kind(out, kind, label));
1785 Ok(())
1786}
1787
1788fn write_reference_kind(out: &mut String, kind: ReferenceKind, label: &str) {
1789 match kind {
1790 ReferenceKind::Shortcut => {}
1791 ReferenceKind::Collapsed => out.push_str("[]"),
1792 ReferenceKind::Full => {
1793 out.push('[');
1794 out.push_str(label);
1796 out.push(']');
1797 }
1798 }
1799}
1800
1801fn write_code_span(node: &CodeInline, out: &mut InlineOut) {
1806 let value = &node.value;
1807 out.scan_open_runs();
1808 let held = backtick_runs(value);
1809 let before = out.len() - out.trim_end_matches('`').len();
1813 let length = (1..)
1814 .find(|length| {
1815 !held.get(*length).copied().unwrap_or(false)
1816 && !out.is_open_run(*length)
1817 && (before == 0 || !out.is_open_run(before + *length))
1818 })
1819 .unwrap_or(1);
1820 let fence = "`".repeat(length);
1821 out.push_str(&fence);
1822 if code_span_needs_padding(value) {
1823 out.push(' ');
1824 out.push_str(value);
1825 out.push(' ');
1826 } else {
1827 out.push_str(value);
1828 }
1829 out.push_str(&fence);
1830 out.scanned = out.text.len();
1832}
1833
1834fn backtick_runs(value: &str) -> Vec<bool> {
1836 let mut runs = alloc::vec![false; value.len() + 1];
1838 let mut run = 0;
1839 for byte in value.bytes() {
1840 if byte == b'`' {
1841 run += 1;
1842 } else {
1843 runs[run] = true;
1844 run = 0;
1845 }
1846 }
1847 runs[run] = true;
1848 runs
1849}
1850
1851fn code_span_needs_padding(input: &str) -> bool {
1852 input.starts_with('`')
1853 || input.ends_with('`')
1854 || (input.starts_with(' ') && input.ends_with(' ') && input.chars().any(|char| char != ' '))
1855}
1856
1857pub(crate) fn write_inline_math(node: &MathInline) -> String {
1858 match node.kind {
1859 MathInlineKind::Code => format!("$`{}`$", node.value),
1860 MathInlineKind::Dollar { dollars } => {
1862 let fence = "$".repeat(usize::from(dollars));
1863 format!("{fence}{}{fence}", node.value)
1864 }
1865 }
1866}
1867
1868fn write_directive_label(label: &[Inline], source: Option<&str>) -> Result<String, SerializeError> {
1871 if let Some(text) = one_line(source, true) {
1872 return Ok(if text.is_empty() {
1873 String::new()
1874 } else {
1875 format!("[{text}]")
1876 });
1877 }
1878 if label.is_empty() {
1879 Ok(String::new())
1880 } else {
1881 Ok(format!("[{}]", write_inlines(label, Context::BLOCK)?))
1882 }
1883}
1884
1885fn one_line(source: Option<&str>, single: bool) -> Option<&str> {
1889 source.filter(|text| !single || !text.contains(['\n', '\r']))
1890}
1891
1892fn write_attributes(attributes: &[DirectiveAttribute]) -> String {
1893 if attributes.is_empty() {
1894 return String::new();
1895 }
1896 let mut output = String::from("{");
1897 for (index, attribute) in attributes.iter().enumerate() {
1898 if index > 0 {
1899 output.push(' ');
1900 }
1901 match (&*attribute.name, &attribute.value) {
1902 ("id", Some(value)) if is_directive_shorthand_value(value) => {
1903 output.push('#');
1904 output.push_str(value);
1905 }
1906 ("class", Some(value)) if is_directive_shorthand_value(value) => {
1907 output.push('.');
1908 output.push_str(value);
1909 }
1910 (_, Some(value)) => {
1911 output.push_str(&attribute.name);
1912 output.push_str("=\"");
1913 output.push_str(&escape_title(value, LinkTitleKind::DoubleQuote));
1914 output.push('"');
1915 }
1916 (_, None) => output.push_str(&attribute.name),
1917 }
1918 }
1919 output.push('}');
1920 output
1921}
1922
1923fn is_directive_shorthand_value(input: &str) -> bool {
1924 !input.is_empty()
1925 && input
1926 .chars()
1927 .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1928}
1929
1930fn write_destination(input: &str, kind: LinkDestinationKind) -> String {
1931 match kind {
1932 LinkDestinationKind::Omitted if input.is_empty() => String::new(),
1933 LinkDestinationKind::Angle => {
1934 let mut output = String::from("<");
1935 for char in input.chars() {
1936 match char {
1937 char if char.is_control() => output.push_str(&char_reference(char)),
1938 '\\' | '<' | '>' => {
1939 output.push('\\');
1940 output.push(char);
1941 }
1942 _ => output.push(char),
1943 }
1944 }
1945 output.push('>');
1946 output
1947 }
1948 LinkDestinationKind::Bare | LinkDestinationKind::Omitted if input.is_empty() => "<>".into(),
1949 LinkDestinationKind::Bare | LinkDestinationKind::Omitted => {
1950 let mut output = String::new();
1951 for char in input.chars() {
1952 match char {
1953 char if char.is_control() || char == ' ' => {
1955 output.push_str(&char_reference(char));
1956 }
1957 '(' | ')' | '\\' | '<' | '>' | '&' => {
1958 output.push('\\');
1959 output.push(char);
1960 }
1961 _ => output.push(char),
1962 }
1963 }
1964 output
1965 }
1966 }
1967}
1968
1969fn write_title(title: &Title) -> String {
1970 let (open, close) = match title.kind {
1971 LinkTitleKind::DoubleQuote => ('"', '"'),
1972 LinkTitleKind::SingleQuote => ('\'', '\''),
1973 LinkTitleKind::Paren => ('(', ')'),
1974 };
1975 let mut output = String::new();
1976 output.push(open);
1977 output.push_str(&escape_title(&title.value, title.kind));
1978 output.push(close);
1979 output
1980}
1981
1982fn escape_title(input: &str, kind: LinkTitleKind) -> String {
1983 let mut output = String::new();
1984 for char in input.chars() {
1985 match char {
1986 char if char.is_control() => output.push_str(&char_reference(char)),
1987 '\\' | '&' => {
1988 output.push('\\');
1989 output.push(char);
1990 }
1991 '"' if kind == LinkTitleKind::DoubleQuote => output.push_str("\\\""),
1992 '\'' if kind == LinkTitleKind::SingleQuote => output.push_str("\\'"),
1993 '(' | ')' if kind == LinkTitleKind::Paren => {
1994 output.push('\\');
1995 output.push(char);
1996 }
1997 _ => output.push(char),
1998 }
1999 }
2000 output
2001}
2002
2003fn prefix_container_lines(input: &str, prefix: &str) -> String {
2007 prefix_lines_with(input, prefix, prefix.trim_end_matches([' ', '\t']))
2008}
2009
2010fn prefix_lines(input: &str, prefix: &str) -> String {
2012 prefix_lines_with(input, prefix, prefix)
2013}
2014
2015fn prefix_lines_with(input: &str, prefix: &str, empty_prefix: &str) -> String {
2018 if input.is_empty() {
2019 return String::new();
2020 }
2021 let bytes = input.as_bytes();
2022 let mut output = String::new();
2023 let mut line_start = 0;
2024 let mut cursor = 0;
2025 while cursor < input.len() {
2026 let eol_end = match bytes[cursor] {
2027 b'\n' => Some(cursor + 1),
2028 b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => Some(cursor + 2),
2029 b'\r' => Some(cursor + 1),
2030 _ => None,
2031 };
2032 if let Some(end) = eol_end {
2033 output.push_str(if cursor == line_start {
2034 empty_prefix
2035 } else {
2036 prefix
2037 });
2038 output.push_str(&input[line_start..end]);
2039 cursor = end;
2040 line_start = cursor;
2041 } else {
2042 cursor += 1;
2043 }
2044 }
2045 if line_start < input.len() {
2046 output.push_str(prefix);
2047 output.push_str(&input[line_start..]);
2048 }
2049 output
2050}
2051
2052fn indent_after_first_line(input: &str, width: usize) -> String {
2053 indent_lines_of(input, width)
2054}
2055
2056fn indent_continuation(input: &str) -> String {
2057 indent_lines_of(input, 4)
2058}
2059
2060fn indent_lines_of(input: &str, width: usize) -> String {
2064 let mut output = String::with_capacity(input.len());
2065 let bytes = input.as_bytes();
2066 let mut line_start = 0;
2067 let mut index = 0;
2068 let push = |output: &mut String, line: &str, number: usize| {
2069 if number > 0 && !line.starts_with(['\n', '\r']) {
2070 output.extend(core::iter::repeat_n(' ', width));
2071 }
2072 output.push_str(line);
2073 };
2074 let mut number = 0;
2075 while index < bytes.len() {
2076 let end = match bytes[index] {
2077 b'\n' => Some(index + 1),
2078 b'\r' if bytes.get(index + 1) == Some(&b'\n') => Some(index + 2),
2079 b'\r' => Some(index + 1),
2080 _ => None,
2081 };
2082 match end {
2083 Some(end) => {
2084 push(&mut output, &input[line_start..end], number);
2085 number += 1;
2086 index = end;
2087 line_start = end;
2088 }
2089 None => index += 1,
2090 }
2091 }
2092 if line_start < input.len() {
2093 push(&mut output, &input[line_start..], number);
2094 }
2095 output
2096}
2097
2098fn trim_trailing_newline(input: &str) -> &str {
2099 input.trim_end_matches('\n').trim_end_matches('\r')
2100}
2101
2102fn ends_with_line_ending(input: &str) -> bool {
2103 input.ends_with('\n') || input.ends_with('\r')
2104}
2105
2106fn code_block_fence(value: &str, marker: FenceMarker, min_len: usize) -> (String, usize) {
2113 let char = match marker {
2114 FenceMarker::Backtick => '`',
2115 FenceMarker::Tilde => '~',
2116 };
2117 let closing_like = |line: &str, length: usize| {
2118 let indent = line.len() - line.trim_start_matches(' ').len();
2119 let rest = &line[indent..];
2120 let run = rest.len() - rest.trim_start_matches(char).len();
2121 (indent <= 3 && run >= length && rest[run..].trim_matches([' ', '\t']).is_empty())
2122 .then_some((indent, run))
2123 };
2124 let lines = || value.split(['\n', '\r']);
2125 let least_indent = lines()
2126 .filter_map(|line| closing_like(line, min_len))
2127 .map(|(indent, _)| indent)
2128 .min();
2129 match least_indent {
2130 None => (char.to_string().repeat(min_len), 0),
2131 Some(indent) if indent > 0 => (char.to_string().repeat(min_len), 4 - indent),
2132 Some(_) => {
2133 let mut length = min_len;
2134 for line in lines() {
2135 if let Some((_, run)) = closing_like(line, length) {
2136 length = run + 1;
2137 }
2138 }
2139 (char.to_string().repeat(length), 0)
2140 }
2141 }
2142}
2143
2144fn block_math_fence(input: &str) -> String {
2145 let mut length = 2;
2146 for line in trim_trailing_newline(input).lines() {
2147 let trimmed = line.trim();
2148 if trimmed.len() >= 2 && trimmed.chars().all(|char| char == '$') {
2149 length = length.max(trimmed.len() + 1);
2150 }
2151 }
2152 "$".repeat(length)
2153}
2154
2155fn directive_fence(inner: &str) -> String {
2156 let mut length = 3;
2157 for line in inner.lines() {
2158 if let Some(closing) = directive_closing_fence_len(line) {
2159 length = length.max(closing + 1);
2160 }
2161 }
2162 ":".repeat(length)
2163}
2164
2165fn directive_closing_fence_len(line: &str) -> Option<usize> {
2166 let trimmed = trim_up_to_three_indent_columns(line)?;
2167 let length = trimmed
2168 .as_bytes()
2169 .iter()
2170 .take_while(|byte| **byte == b':')
2171 .count();
2172 if length >= 3 && trimmed[length..].trim().is_empty() {
2173 Some(length)
2174 } else {
2175 None
2176 }
2177}
2178
2179fn trim_up_to_three_indent_columns(input: &str) -> Option<&str> {
2180 let mut columns = 0usize;
2181 let mut bytes = 0usize;
2182 for byte in input.as_bytes() {
2183 match *byte {
2184 b' ' => columns += 1,
2185 b'\t' => columns += 4 - (columns % 4),
2186 _ => break,
2187 }
2188 bytes += 1;
2189 }
2190 (columns <= 3).then_some(&input[bytes..])
2191}
2192
2193#[cfg(test)]
2194mod tests {
2195 use super::*;
2196
2197 #[test]
2199 fn the_read_back_check_names_each_block_that_reads_differently() {
2200 let blocks = crate::parse("> x\n\n# y\n\nz").document.children;
2201 assert_eq!(misread(&blocks, "> x\n\n# y\n\nz", &[0, 2]), [0usize; 0]);
2202 assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[0, 2]), [0]);
2204 assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[2]), [0usize; 0]);
2205 assert_eq!(misread(&blocks, "> x\nz\n\n# y", &[0, 2]), [0, 2]);
2207 }
2208}