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