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 let break_after_bullet = breaks_after_bullet(list, item, marker_char);
1198 let opens_with_whitespace = opens_with_padding(&inner);
1199 Ok(if break_after_bullet || opens_with_whitespace {
1200 format!(
1201 "{}\n{}",
1202 marker.trim_end(),
1203 prefix_container_lines(&inner, &" ".repeat(marker.len()))
1204 )
1205 } else {
1206 format!("{marker}{}", indent_after_first_line(&inner, marker.len()))
1207 })
1208}
1209
1210fn thematic_break_char(marker: ThematicBreakMarker) -> char {
1211 match marker {
1212 ThematicBreakMarker::Dash => '-',
1213 ThematicBreakMarker::Asterisk => '*',
1214 ThematicBreakMarker::Underscore => '_',
1215 }
1216}
1217
1218fn breaks_after_bullet(list: &List, item: &ListItem, marker_char: char) -> bool {
1224 !list.ordered
1225 && item.checked.is_none()
1226 && matches!(
1227 item.children.first(),
1228 Some(Block::ThematicBreak(ThematicBreak { marker, .. }))
1229 if thematic_break_char(*marker) == marker_char
1230 )
1231}
1232
1233fn opens_with_padding(inner: &str) -> bool {
1238 !inner.starts_with(" ") && inner.starts_with([' ', '\t'])
1239}
1240
1241pub(crate) fn written_after_paragraph_interrupts(block: &Block) -> bool {
1247 match block {
1248 Block::Heading(heading) => !writes_setext(heading),
1249 Block::List(list) => {
1250 crate::parse::interrupts_paragraph(block) && !first_item_starts_below_its_bullet(list)
1251 }
1252 _ => crate::parse::interrupts_paragraph(block),
1253 }
1254}
1255
1256pub(crate) fn first_item_starts_below_its_bullet(list: &List) -> bool {
1261 let Some(item) = list.children.first() else {
1262 return false;
1263 };
1264 let options = SerializeOptions::default();
1265 if breaks_after_bullet(list, item, list_marker(list, &options, None)) {
1266 return true;
1267 }
1268 if item.checked.is_some() {
1269 return false;
1270 }
1271 match item.children.first() {
1272 Some(
1273 block @ (Block::Paragraph(_)
1274 | Block::Heading(_)
1275 | Block::HtmlBlock(_)
1276 | Block::CodeBlock(_)),
1277 ) => write_block(block, &options).is_ok_and(|written| opens_with_padding(&written)),
1278 _ => false,
1279 }
1280}
1281
1282fn write_item_blocks(
1283 blocks: &[Block],
1284 options: &SerializeOptions,
1285 tight: bool,
1286 task: Option<bool>,
1287) -> Result<String, SerializeError> {
1288 let join = if tight { Join::Line } else { Join::Gap };
1289 let Some(checked) = task else {
1290 return write_blocks(blocks, options, join, false, None);
1291 };
1292 let checkbox = if checked { "[x]" } else { "[ ]" };
1293 let definitions = blocks
1294 .iter()
1295 .take_while(|block| matches!(block, Block::Definition(_)))
1296 .count();
1297 if matches!(blocks.get(definitions), Some(Block::Paragraph(_))) {
1298 let before = write_blocks(&blocks[..definitions], options, join, false, None)?;
1299 let rest = write_blocks(&blocks[definitions..], options, join, false, None)?;
1300 let mut output = before;
1301 if !output.is_empty() {
1302 match join {
1303 Join::Gap => push_block_gap(&mut output),
1304 Join::Line => output.push('\n'),
1305 }
1306 }
1307 output.push_str(checkbox);
1308 output.push(' ');
1309 output.push_str(&rest);
1310 return Ok(output);
1311 }
1312 let written = write_blocks(blocks, options, join, false, None)?;
1313 Ok(if written.is_empty() {
1314 checkbox.into()
1315 } else {
1316 format!("{checkbox} {written}")
1317 })
1318}
1319
1320fn write_code_block(node: &CodeBlock, options: &SerializeOptions) -> String {
1321 match node.kind {
1322 CodeBlockKind::Indented => {
1323 let body = trim_trailing_newline(&node.value);
1326 let mut output = prefix_lines(body, " ");
1327 let ending = &node.value[body.len()..];
1328 if matches!(ending, "\r" | "\r\n") {
1329 output.push_str(ending);
1330 }
1331 output
1332 }
1333 CodeBlockKind::Fenced { marker, length } => {
1334 let marker = code_block_fence_marker(node, marker, options);
1335 let (fence, indent) = code_block_fence(&node.value, marker, length.max(3));
1336 let mut opener = fence.clone();
1337 if let Some(info) = &node.info {
1338 opener.push(' ');
1339 opener.push_str(&escape_code_info(info));
1340 }
1341 let body = fenced_body(&opener, &node.value, &fence);
1342 if indent == 0 {
1343 body
1344 } else {
1345 prefix_lines(&body, &" ".repeat(indent))
1346 }
1347 }
1348 }
1349}
1350
1351fn fenced_body(opener: &str, value: &str, closer: &str) -> String {
1355 let mut output = String::with_capacity(opener.len() + value.len() + closer.len() + 2);
1356 output.push_str(opener);
1357 output.push('\n');
1358 output.push_str(value);
1359 if !value.is_empty() && !ends_with_line_ending(value) {
1360 output.push('\n');
1361 }
1362 output.push_str(closer);
1363 output
1364}
1365
1366fn code_block_fence_marker(
1370 node: &CodeBlock,
1371 marker: FenceMarker,
1372 options: &SerializeOptions,
1373) -> FenceMarker {
1374 if node.info.as_deref().is_some_and(|info| info.contains('`')) {
1375 return FenceMarker::Tilde;
1376 }
1377 options.fence_marker.unwrap_or(marker)
1378}
1379
1380fn escape_code_info(input: &str) -> String {
1381 let mut output = String::new();
1382 let inner_start = input.len() - input.trim_start_matches([' ', '\t']).len();
1385 let inner_end = input.trim_end_matches([' ', '\t']).len().max(inner_start);
1386 for (offset, char) in input.char_indices() {
1387 match char {
1388 ' ' | '\t' if offset < inner_start || offset >= inner_end => {
1389 output.push_str(&char_reference(char));
1390 }
1391 '\t' => output.push(char),
1392 char if char.is_control() => output.push_str(&char_reference(char)),
1393 '\\' | '&' => {
1394 output.push('\\');
1395 output.push(char);
1396 }
1397 _ => output.push(char),
1398 }
1399 }
1400 output
1401}
1402
1403fn char_reference(char: char) -> String {
1405 format!("&#x{:X};", char as u32)
1406}
1407
1408fn write_table(node: &Table) -> Result<String, SerializeError> {
1409 let mut output = String::new();
1410 for (row_index, row) in node.rows.iter().enumerate() {
1411 if row_index > 0 {
1412 output.push('\n');
1413 }
1414 output.push_str(&write_row(row)?);
1415 if row_index == 0 {
1416 output.push('\n');
1417 output.push_str(&write_delimiter_row(&node.alignments));
1418 }
1419 }
1420 Ok(output)
1421}
1422
1423fn write_delimiter_row(alignments: &[TableAlignment]) -> String {
1424 let cells = alignments
1425 .iter()
1426 .map(|alignment| match alignment {
1427 TableAlignment::None => "---",
1428 TableAlignment::Left => ":---",
1429 TableAlignment::Center => ":---:",
1430 TableAlignment::Right => "---:",
1431 })
1432 .collect::<Vec<_>>();
1433 format!("| {} |", cells.join(" | "))
1434}
1435
1436fn write_row(row: &TableRow) -> Result<String, SerializeError> {
1437 let cells = row
1438 .cells
1439 .iter()
1440 .map(|cell| {
1441 Ok(encode_cell(&match one_line(cell.source.as_deref(), true) {
1442 Some(text) => text.into(),
1443 None => write_inlines(&cell.children, Context::BLOCK)?,
1444 }))
1445 })
1446 .collect::<Result<Vec<_>, SerializeError>>()?;
1447 Ok(format!("| {} |", cells.join(" | ")))
1448}
1449
1450fn encode_cell(written: &str) -> String {
1458 let mut output = String::with_capacity(written.len());
1459 let mut backslashes = 0usize;
1460 for char in written.chars() {
1461 if char == '|' && backslashes % 2 == 0 {
1462 output.push('\\');
1463 }
1464 backslashes = if char == '\\' { backslashes + 1 } else { 0 };
1465 output.push(char);
1466 }
1467 output
1468}
1469
1470#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1473struct Context {
1474 heading: bool,
1476}
1477
1478impl Context {
1479 const BLOCK: Self = Self { heading: false };
1480 const HEADING: Self = Self { heading: true };
1481}
1482
1483fn write_inlines(inlines: &[Inline], context: Context) -> Result<String, SerializeError> {
1484 let mut output = InlineOut::default();
1485 for inline in inlines {
1486 write_inline(inline, context, &mut output)?;
1487 }
1488 Ok(output.text)
1489}
1490
1491#[derive(Default)]
1495struct InlineOut {
1496 text: String,
1497 open_runs: Vec<bool>,
1501 scanned: usize,
1503}
1504
1505impl InlineOut {
1506 fn scan_open_runs(&mut self) {
1510 let mut escaped = false;
1511 let mut run = 0;
1512 let mut run_escaped = false;
1513 for byte in self.text.as_bytes()[self.scanned..]
1514 .iter()
1515 .copied()
1516 .chain(core::iter::once(b' '))
1517 {
1518 if byte == b'`' {
1519 if run == 0 {
1520 run_escaped = escaped;
1521 }
1522 run += 1;
1523 escaped = false;
1524 continue;
1525 }
1526 let opening = run - usize::from(run_escaped && run > 0);
1527 if opening > 0 {
1528 if self.open_runs.len() <= opening {
1529 self.open_runs.resize(opening + 1, false);
1530 }
1531 self.open_runs[opening] = true;
1532 }
1533 run = 0;
1534 escaped = byte == b'\\' && !escaped;
1535 }
1536 self.scanned = self.text.len();
1537 }
1538
1539 fn is_open_run(&self, length: usize) -> bool {
1540 self.open_runs.get(length).copied().unwrap_or(false)
1541 }
1542
1543 fn push_opaque(&mut self, write: impl FnOnce(&mut String)) {
1546 self.scan_open_runs();
1547 write(&mut self.text);
1548 self.scanned = self.text.len();
1549 }
1550}
1551
1552impl core::ops::Deref for InlineOut {
1553 type Target = String;
1554
1555 fn deref(&self) -> &String {
1556 &self.text
1557 }
1558}
1559
1560impl core::ops::DerefMut for InlineOut {
1561 fn deref_mut(&mut self) -> &mut String {
1562 &mut self.text
1563 }
1564}
1565
1566fn write_inline(
1567 inline: &Inline,
1568 context: Context,
1569 out: &mut InlineOut,
1570) -> Result<(), SerializeError> {
1571 match inline {
1572 Inline::Text(node) => out.push_str(&node.value),
1573 Inline::Escape(node) => {
1574 out.push('\\');
1575 out.push(node.value);
1576 }
1577 Inline::CharacterReference(node) => out.push_str(&node.reference),
1578 Inline::Emphasis(node) => {
1579 let delimiter = emphasis_delimiter(node.delimiter);
1580 write_span(out, delimiter, &node.children, delimiter, context)?;
1581 }
1582 Inline::Strong(node) => {
1583 let delimiter = emphasis_delimiter(node.delimiter).repeat(2);
1584 write_span(out, &delimiter, &node.children, &delimiter, context)?;
1585 }
1586 Inline::Delete(node) => write_span(out, "~~", &node.children, "~~", context)?,
1587 Inline::Mark(node) => write_span(out, "==", &node.children, "==", context)?,
1588 Inline::InlineFootnote(node) => write_span(out, "^[", &node.children, "]", context)?,
1589 Inline::Shortcode(node) => {
1590 out.push(':');
1591 out.push_str(&node.name);
1592 out.push(':');
1593 }
1594 Inline::Tag(node) => {
1595 out.push('#');
1596 out.push_str(&node.name);
1597 }
1598 Inline::Code(node) => write_code_span(node, out),
1599 Inline::Link(node) => {
1600 write_span(out, "[", &node.children, "](", context)?;
1601 out.push_opaque(|out| {
1602 write_resource(
1603 out,
1604 &node.destination,
1605 node.destination_kind,
1606 node.title.as_ref(),
1607 );
1608 });
1609 }
1610 Inline::Autolink(node) => out.push_opaque(|out| match node.form {
1611 AutolinkForm::Literal => out.push_str(&node.text),
1612 AutolinkForm::Angle => {
1613 out.push('<');
1614 out.push_str(&node.text);
1615 out.push('>');
1616 }
1617 }),
1618 Inline::Image(node) => {
1619 write_span(out, "?;
1620 out.push_opaque(|out| {
1621 write_resource(
1622 out,
1623 &node.destination,
1624 node.destination_kind,
1625 node.title.as_ref(),
1626 );
1627 });
1628 }
1629 Inline::LinkReference(node) => {
1630 write_reference(out, "[", &node.children, node.kind, &node.label, context)?;
1631 }
1632 Inline::ImageReference(node) => {
1633 write_reference(out, "![", &node.alt, node.kind, &node.label, context)?;
1634 }
1635 Inline::Html(node) => out.push_opaque(|out| out.push_str(&node.value)),
1636 Inline::SoftBreak(_) if context.heading => out.push(' '),
1637 Inline::SoftBreak(_) => out.push('\n'),
1638 Inline::LineBreak(node) => match node.kind {
1639 LineBreakKind::Backslash => out.push_str("\\\n"),
1640 LineBreakKind::Spaces => out.push_str(" \n"),
1641 },
1642 Inline::Math(node) => {
1643 let math = write_inline_math(node);
1644 out.push_opaque(|out| out.push_str(&math));
1645 }
1646 Inline::FootnoteReference(node) => {
1647 out.push_str("[^");
1648 out.push_str(&node.label);
1649 out.push(']');
1650 }
1651 Inline::WikiLink(node) => out.push_opaque(|out| {
1652 if node.embed {
1653 out.push('!');
1654 }
1655 out.push_str("[[");
1656 out.push_str(&node.target);
1657 if node.target != node.label {
1658 out.push('|');
1659 out.push_str(&node.label);
1660 }
1661 out.push_str("]]");
1662 }),
1663 Inline::TextDirective(node) => {
1664 let label = if node.label.is_empty() {
1666 String::new()
1667 } else {
1668 format!("[{}]", write_inlines(&node.label, context)?)
1669 };
1670 out.push_opaque(|out| {
1671 out.push(':');
1672 out.push_str(&node.name);
1673 out.push_str(&label);
1674 out.push_str(&write_attributes(&node.attributes));
1675 });
1676 }
1677 }
1678 Ok(())
1679}
1680
1681fn emphasis_delimiter(delimiter: EmphasisDelimiter) -> &'static str {
1682 match delimiter {
1683 EmphasisDelimiter::Asterisk => "*",
1684 EmphasisDelimiter::Underscore => "_",
1685 }
1686}
1687
1688fn write_span(
1690 out: &mut InlineOut,
1691 open: &str,
1692 children: &[Inline],
1693 close: &str,
1694 context: Context,
1695) -> Result<(), SerializeError> {
1696 out.push_str(open);
1697 for child in children {
1698 write_inline(child, context, out)?;
1699 }
1700 out.push_str(close);
1701 Ok(())
1702}
1703
1704fn write_resource(
1706 out: &mut String,
1707 destination: &str,
1708 kind: LinkDestinationKind,
1709 title: Option<&Title>,
1710) {
1711 out.push_str(&write_destination(destination, kind));
1712 if let Some(title) = title {
1713 out.push(' ');
1714 out.push_str(&write_title(title));
1715 }
1716 out.push(')');
1717}
1718
1719fn write_reference(
1725 out: &mut InlineOut,
1726 open: &str,
1727 children: &[Inline],
1728 kind: ReferenceKind,
1729 label: &str,
1730 context: Context,
1731) -> Result<(), SerializeError> {
1732 match kind {
1733 ReferenceKind::Full => write_span(out, open, children, "]", context)?,
1734 ReferenceKind::Shortcut | ReferenceKind::Collapsed => {
1735 out.push_str(open);
1736 out.push_str(label);
1737 out.push(']');
1738 }
1739 }
1740 out.push_opaque(|out| write_reference_kind(out, kind, label));
1741 Ok(())
1742}
1743
1744fn write_reference_kind(out: &mut String, kind: ReferenceKind, label: &str) {
1745 match kind {
1746 ReferenceKind::Shortcut => {}
1747 ReferenceKind::Collapsed => out.push_str("[]"),
1748 ReferenceKind::Full => {
1749 out.push('[');
1750 out.push_str(label);
1752 out.push(']');
1753 }
1754 }
1755}
1756
1757fn write_code_span(node: &CodeInline, out: &mut InlineOut) {
1762 let value = &node.value;
1763 out.scan_open_runs();
1764 let held = backtick_runs(value);
1765 let before = out.len() - out.trim_end_matches('`').len();
1769 let length = (1..)
1770 .find(|length| {
1771 !held.get(*length).copied().unwrap_or(false)
1772 && !out.is_open_run(*length)
1773 && (before == 0 || !out.is_open_run(before + *length))
1774 })
1775 .unwrap_or(1);
1776 let fence = "`".repeat(length);
1777 out.push_str(&fence);
1778 if code_span_needs_padding(value) {
1779 out.push(' ');
1780 out.push_str(value);
1781 out.push(' ');
1782 } else {
1783 out.push_str(value);
1784 }
1785 out.push_str(&fence);
1786 out.scanned = out.text.len();
1788}
1789
1790fn backtick_runs(value: &str) -> Vec<bool> {
1792 let mut runs = alloc::vec![false; value.len() + 1];
1794 let mut run = 0;
1795 for byte in value.bytes() {
1796 if byte == b'`' {
1797 run += 1;
1798 } else {
1799 runs[run] = true;
1800 run = 0;
1801 }
1802 }
1803 runs[run] = true;
1804 runs
1805}
1806
1807fn code_span_needs_padding(input: &str) -> bool {
1808 input.starts_with('`')
1809 || input.ends_with('`')
1810 || (input.starts_with(' ') && input.ends_with(' ') && input.chars().any(|char| char != ' '))
1811}
1812
1813pub(crate) fn write_inline_math(node: &MathInline) -> String {
1814 match node.kind {
1815 MathInlineKind::Code => format!("$`{}`$", node.value),
1816 MathInlineKind::Dollar { dollars } => {
1818 let fence = "$".repeat(usize::from(dollars));
1819 format!("{fence}{}{fence}", node.value)
1820 }
1821 }
1822}
1823
1824fn write_directive_label(label: &[Inline], source: Option<&str>) -> Result<String, SerializeError> {
1827 if let Some(text) = one_line(source, true) {
1828 return Ok(if text.is_empty() {
1829 String::new()
1830 } else {
1831 format!("[{text}]")
1832 });
1833 }
1834 if label.is_empty() {
1835 Ok(String::new())
1836 } else {
1837 Ok(format!("[{}]", write_inlines(label, Context::BLOCK)?))
1838 }
1839}
1840
1841fn one_line(source: Option<&str>, single: bool) -> Option<&str> {
1845 source.filter(|text| !single || !text.contains(['\n', '\r']))
1846}
1847
1848fn write_attributes(attributes: &[DirectiveAttribute]) -> String {
1849 if attributes.is_empty() {
1850 return String::new();
1851 }
1852 let mut output = String::from("{");
1853 for (index, attribute) in attributes.iter().enumerate() {
1854 if index > 0 {
1855 output.push(' ');
1856 }
1857 match (&*attribute.name, &attribute.value) {
1858 ("id", Some(value)) if is_directive_shorthand_value(value) => {
1859 output.push('#');
1860 output.push_str(value);
1861 }
1862 ("class", Some(value)) if is_directive_shorthand_value(value) => {
1863 output.push('.');
1864 output.push_str(value);
1865 }
1866 (_, Some(value)) => {
1867 output.push_str(&attribute.name);
1868 output.push_str("=\"");
1869 output.push_str(&escape_title(value, LinkTitleKind::DoubleQuote));
1870 output.push('"');
1871 }
1872 (_, None) => output.push_str(&attribute.name),
1873 }
1874 }
1875 output.push('}');
1876 output
1877}
1878
1879fn is_directive_shorthand_value(input: &str) -> bool {
1880 !input.is_empty()
1881 && input
1882 .chars()
1883 .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1884}
1885
1886fn write_destination(input: &str, kind: LinkDestinationKind) -> String {
1887 match kind {
1888 LinkDestinationKind::Omitted if input.is_empty() => String::new(),
1889 LinkDestinationKind::Angle => {
1890 let mut output = String::from("<");
1891 for char in input.chars() {
1892 match char {
1893 char if char.is_control() => output.push_str(&char_reference(char)),
1894 '\\' | '<' | '>' => {
1895 output.push('\\');
1896 output.push(char);
1897 }
1898 _ => output.push(char),
1899 }
1900 }
1901 output.push('>');
1902 output
1903 }
1904 LinkDestinationKind::Bare | LinkDestinationKind::Omitted if input.is_empty() => "<>".into(),
1905 LinkDestinationKind::Bare | LinkDestinationKind::Omitted => {
1906 let mut output = String::new();
1907 for char in input.chars() {
1908 match char {
1909 char if char.is_control() || char == ' ' => {
1911 output.push_str(&char_reference(char));
1912 }
1913 '(' | ')' | '\\' | '<' | '>' | '&' => {
1914 output.push('\\');
1915 output.push(char);
1916 }
1917 _ => output.push(char),
1918 }
1919 }
1920 output
1921 }
1922 }
1923}
1924
1925fn write_title(title: &Title) -> String {
1926 let (open, close) = match title.kind {
1927 LinkTitleKind::DoubleQuote => ('"', '"'),
1928 LinkTitleKind::SingleQuote => ('\'', '\''),
1929 LinkTitleKind::Paren => ('(', ')'),
1930 };
1931 let mut output = String::new();
1932 output.push(open);
1933 output.push_str(&escape_title(&title.value, title.kind));
1934 output.push(close);
1935 output
1936}
1937
1938fn escape_title(input: &str, kind: LinkTitleKind) -> String {
1939 let mut output = String::new();
1940 for char in input.chars() {
1941 match char {
1942 char if char.is_control() => output.push_str(&char_reference(char)),
1943 '\\' | '&' => {
1944 output.push('\\');
1945 output.push(char);
1946 }
1947 '"' if kind == LinkTitleKind::DoubleQuote => output.push_str("\\\""),
1948 '\'' if kind == LinkTitleKind::SingleQuote => output.push_str("\\'"),
1949 '(' | ')' if kind == LinkTitleKind::Paren => {
1950 output.push('\\');
1951 output.push(char);
1952 }
1953 _ => output.push(char),
1954 }
1955 }
1956 output
1957}
1958
1959fn prefix_container_lines(input: &str, prefix: &str) -> String {
1963 prefix_lines_with(input, prefix, prefix.trim_end_matches([' ', '\t']))
1964}
1965
1966fn prefix_lines(input: &str, prefix: &str) -> String {
1968 prefix_lines_with(input, prefix, prefix)
1969}
1970
1971fn prefix_lines_with(input: &str, prefix: &str, empty_prefix: &str) -> String {
1974 if input.is_empty() {
1975 return String::new();
1976 }
1977 let bytes = input.as_bytes();
1978 let mut output = String::new();
1979 let mut line_start = 0;
1980 let mut cursor = 0;
1981 while cursor < input.len() {
1982 let eol_end = match bytes[cursor] {
1983 b'\n' => Some(cursor + 1),
1984 b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => Some(cursor + 2),
1985 b'\r' => Some(cursor + 1),
1986 _ => None,
1987 };
1988 if let Some(end) = eol_end {
1989 output.push_str(if cursor == line_start {
1990 empty_prefix
1991 } else {
1992 prefix
1993 });
1994 output.push_str(&input[line_start..end]);
1995 cursor = end;
1996 line_start = cursor;
1997 } else {
1998 cursor += 1;
1999 }
2000 }
2001 if line_start < input.len() {
2002 output.push_str(prefix);
2003 output.push_str(&input[line_start..]);
2004 }
2005 output
2006}
2007
2008fn indent_after_first_line(input: &str, width: usize) -> String {
2009 indent_lines_of(input, width)
2010}
2011
2012fn indent_continuation(input: &str) -> String {
2013 indent_lines_of(input, 4)
2014}
2015
2016fn indent_lines_of(input: &str, width: usize) -> String {
2020 let mut output = String::with_capacity(input.len());
2021 let bytes = input.as_bytes();
2022 let mut line_start = 0;
2023 let mut index = 0;
2024 let push = |output: &mut String, line: &str, number: usize| {
2025 if number > 0 && !line.starts_with(['\n', '\r']) {
2026 output.extend(core::iter::repeat_n(' ', width));
2027 }
2028 output.push_str(line);
2029 };
2030 let mut number = 0;
2031 while index < bytes.len() {
2032 let end = match bytes[index] {
2033 b'\n' => Some(index + 1),
2034 b'\r' if bytes.get(index + 1) == Some(&b'\n') => Some(index + 2),
2035 b'\r' => Some(index + 1),
2036 _ => None,
2037 };
2038 match end {
2039 Some(end) => {
2040 push(&mut output, &input[line_start..end], number);
2041 number += 1;
2042 index = end;
2043 line_start = end;
2044 }
2045 None => index += 1,
2046 }
2047 }
2048 if line_start < input.len() {
2049 push(&mut output, &input[line_start..], number);
2050 }
2051 output
2052}
2053
2054fn trim_trailing_newline(input: &str) -> &str {
2055 input.trim_end_matches('\n').trim_end_matches('\r')
2056}
2057
2058fn ends_with_line_ending(input: &str) -> bool {
2059 input.ends_with('\n') || input.ends_with('\r')
2060}
2061
2062fn code_block_fence(value: &str, marker: FenceMarker, min_len: usize) -> (String, usize) {
2069 let char = match marker {
2070 FenceMarker::Backtick => '`',
2071 FenceMarker::Tilde => '~',
2072 };
2073 let closing_like = |line: &str, length: usize| {
2074 let indent = line.len() - line.trim_start_matches(' ').len();
2075 let rest = &line[indent..];
2076 let run = rest.len() - rest.trim_start_matches(char).len();
2077 (indent <= 3 && run >= length && rest[run..].trim_matches([' ', '\t']).is_empty())
2078 .then_some((indent, run))
2079 };
2080 let lines = || value.split(['\n', '\r']);
2081 let least_indent = lines()
2082 .filter_map(|line| closing_like(line, min_len))
2083 .map(|(indent, _)| indent)
2084 .min();
2085 match least_indent {
2086 None => (char.to_string().repeat(min_len), 0),
2087 Some(indent) if indent > 0 => (char.to_string().repeat(min_len), 4 - indent),
2088 Some(_) => {
2089 let mut length = min_len;
2090 for line in lines() {
2091 if let Some((_, run)) = closing_like(line, length) {
2092 length = run + 1;
2093 }
2094 }
2095 (char.to_string().repeat(length), 0)
2096 }
2097 }
2098}
2099
2100fn block_math_fence(input: &str) -> String {
2101 let mut length = 2;
2102 for line in trim_trailing_newline(input).lines() {
2103 let trimmed = line.trim();
2104 if trimmed.len() >= 2 && trimmed.chars().all(|char| char == '$') {
2105 length = length.max(trimmed.len() + 1);
2106 }
2107 }
2108 "$".repeat(length)
2109}
2110
2111fn directive_fence(inner: &str) -> String {
2112 let mut length = 3;
2113 for line in inner.lines() {
2114 if let Some(closing) = directive_closing_fence_len(line) {
2115 length = length.max(closing + 1);
2116 }
2117 }
2118 ":".repeat(length)
2119}
2120
2121fn directive_closing_fence_len(line: &str) -> Option<usize> {
2122 let trimmed = trim_up_to_three_indent_columns(line)?;
2123 let length = trimmed
2124 .as_bytes()
2125 .iter()
2126 .take_while(|byte| **byte == b':')
2127 .count();
2128 if length >= 3 && trimmed[length..].trim().is_empty() {
2129 Some(length)
2130 } else {
2131 None
2132 }
2133}
2134
2135fn trim_up_to_three_indent_columns(input: &str) -> Option<&str> {
2136 let mut columns = 0usize;
2137 let mut bytes = 0usize;
2138 for byte in input.as_bytes() {
2139 match *byte {
2140 b' ' => columns += 1,
2141 b'\t' => columns += 4 - (columns % 4),
2142 _ => break,
2143 }
2144 bytes += 1;
2145 }
2146 (columns <= 3).then_some(&input[bytes..])
2147}
2148
2149#[cfg(test)]
2150mod tests {
2151 use super::*;
2152
2153 #[test]
2155 fn the_read_back_check_names_each_block_that_reads_differently() {
2156 let blocks = crate::parse("> x\n\n# y\n\nz").document.children;
2157 assert_eq!(misread(&blocks, "> x\n\n# y\n\nz", &[0, 2]), [0usize; 0]);
2158 assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[0, 2]), [0]);
2160 assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[2]), [0usize; 0]);
2161 assert_eq!(misread(&blocks, "> x\nz\n\n# y", &[0, 2]), [0, 2]);
2163 }
2164}