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(
238 blocks: &[Block],
239 base_document: &Document,
240 source: &str,
241 options: &SerializeOptions,
242 definitions: &Definitions,
243 whole: &[bool],
244) -> Result<(String, Vec<usize>), SerializeError> {
245 let base = &base_document.children;
246 let mut correspondence = Correspondence {
247 base: base_document,
248 sources: None,
249 };
250 let base_definitions = Definitions::of(base_document);
251 let mut reuse = reuse::Reuse {
252 source,
253 options,
254 correspondence: &mut correspondence,
255 definitions,
256 base_definitions: &base_definitions,
257 };
258 let span_of = |block: &Block| block.span().expect("a reused block has a span");
259 let mut outs = Vec::with_capacity(blocks.len());
260 let mut open_lists = alloc::vec![false; blocks.len()];
262 for (index, block) in blocks.iter().enumerate() {
263 let found = block.span().and_then(|span| {
264 let at = base
265 .binary_search_by_key(&span.start, |base| {
266 base.span().map_or(usize::MAX, |span| span.start)
267 })
268 .ok()?;
269 (base[at].span() == Some(span)
270 && core::mem::discriminant(&base[at]) == core::mem::discriminant(block))
271 .then_some(at)
272 });
273 outs.push(match found {
274 Some(at) => match reuse.block(block, &base[at])? {
275 reuse::Out::Kept(_) => (Some(at), None),
276 reuse::Out::Lines(_) | reuse::Out::OpenList(_) if whole[index] => (None, None),
277 reuse::Out::Lines(lines) => (None, Some((at, lines))),
278 reuse::Out::OpenList(lines) => {
279 open_lists[index] = true;
280 (None, Some((at, lines)))
281 }
282 reuse::Out::Render => (None, None),
283 },
284 None => (None, None),
285 });
286 }
287 let mut reused: Vec<Option<usize>> = outs.iter().map(|(at, _)| *at).collect();
288 let mut partly: Vec<Option<(usize, Vec<reuse::Line>)>> =
290 outs.into_iter().map(|(_, lines)| lines).collect();
291 let placed =
294 |reused: &[Option<usize>], partly: &[Option<(usize, Vec<reuse::Line>)>], index: usize| {
295 reused[index].or(partly[index].as_ref().map(|(at, _)| *at))
296 };
297 if blocks.len() == base.len()
298 && reused
299 .iter()
300 .enumerate()
301 .all(|(index, at)| *at == Some(index))
302 {
303 return Ok((source.into(), Vec::new()));
304 }
305 let separated = separated_boundaries(blocks);
309 let base_separated = separated_boundaries(base);
310 let keeps_gap = |partly: &[Option<(usize, Vec<reuse::Line>)>], index: usize, at: usize| {
315 if index == 0 || at == 0 || separated[index] != base_separated[at] {
316 return false;
317 }
318 let left = base[at - 1].span().expect("a parsed block has a span");
319 let right = base[at].span().expect("a parsed block has a span");
320 let left_end = left.start + strip_line_ending(&source[left.start..left.end]).len();
321 reuse::keeps_gap(
322 source,
323 &source[left_end..right.start],
324 (
325 &blocks[index - 1],
326 left,
327 partly[index - 1]
328 .as_ref()
329 .map(|(_, lines)| lines.as_slice()),
330 ),
331 (
332 &blocks[index],
333 right,
334 partly[index].as_ref().map(|(_, lines)| lines.as_slice()),
335 ),
336 open_lists[index - 1],
337 true,
338 )
339 };
340 for index in (0..blocks.len().saturating_sub(1)).rev() {
343 let Some(at) = placed(&reused, &partly, index) else {
344 continue;
345 };
346 if placed(&reused, &partly, index + 1) == Some(at + 1)
347 && keeps_gap(&partly, index + 1, at + 1)
348 {
349 continue;
350 }
351 match &partly[index] {
352 Some((_, lines)) => {
353 let mut text = reuse::join(lines, "\n");
354 text.push('\n');
355 if !closed_by_a_blank_line(&text) {
356 partly[index] = None;
357 }
358 }
359 None => {
360 let span = span_of(&blocks[index]);
361 if !closed_by_a_blank_line(&source[span.start..span.end]) {
362 reused[index] = None;
363 }
364 }
365 }
366 }
367
368 let mut output = String::with_capacity(source.len());
369 let mut previous_at: Option<usize> = None;
373 let mut placement = reuse::Placement::default();
374 let mut previous_ending = "";
375 let mut previous_body_end = 0;
376 let mut ends_with_source_tail = false;
379 let mut run_follows = true;
382 for (index, block) in blocks.iter().enumerate() {
383 if let (Block::List(list), Some(_)) = (block, &partly[index]) {
386 if placement.previous_list() == Some(written_marker(list)) {
387 partly[index] = None;
388 }
389 }
390 let follows_predecessor = matches!(
391 (previous_at, placed(&reused, &partly, index)),
392 (Some(previous), Some(at)) if previous + 1 == at && keeps_gap(&partly, index, at)
393 );
394 let follows_run = follows_predecessor && run_follows;
399 run_follows = index == 0 || !matches!(block, Block::BlankLine(_)) || follows_run;
400 let taken_in = |text: &str| placement.takes_in(block, text, follows_run);
403 if let Some((_, lines)) = &partly[index] {
404 if taken_in(&lines[0].body) {
405 partly[index] = None;
406 }
407 }
408 let mut at = reused[index];
409 if at.is_some() {
410 let start = span_of(block).start;
411 let rewritten = matches!(block, Block::ThematicBreak(_))
415 && index == 0
416 && whole[0]
417 && source[start..].starts_with("---")
418 || taken_in(&source[start..]);
419 if rewritten {
420 at = None;
421 }
422 }
423 if index > 0 {
424 if follows_predecessor {
425 let start = span_of(block).start;
426 output.push_str(&source[previous_body_end..start]);
427 } else if separated[index] {
428 output.push_str(previous_ending);
431 match previous_ending {
432 "" => push_gap(&mut output, options),
433 "\r" => output.push('\r'),
434 _ => output.push_str(options.line_ending.as_str()),
435 }
436 } else {
437 output.push_str(match previous_ending {
439 "" => options.line_ending.as_str(),
440 own => own,
441 });
442 }
443 }
444 previous_ending = "";
445 match at {
446 Some(position) => {
447 let span = span_of(block);
448 let body = strip_line_ending(&source[span.start..span.end]);
449 let body_end = span.start + body.len();
450 if index == 0 && position == 0 {
451 output.push_str(&source[..span.start]);
452 }
453 output.push_str(body);
454 if index + 1 == blocks.len() && position + 1 == base.len() {
455 output.push_str(&source[body_end..]);
456 ends_with_source_tail = true;
457 }
458 previous_ending = &source[body_end..span.end];
459 previous_body_end = body_end;
460 let marker = match block {
461 Block::List(list) => Some(written_marker(list)),
462 _ => None,
463 };
464 placement.wrote(block, marker, false);
465 }
466 None if partly[index].is_some() => {
467 let (position, lines) = partly[index].as_ref().expect("written partly");
468 let span = base[*position].span().expect("a parsed block has a span");
469 let body_end = span.start + strip_line_ending(&source[span.start..span.end]).len();
470 if index == 0 && *position == 0 {
471 output.push_str(&source[..span.start]);
472 }
473 output.push_str(&reuse::join(lines, options.line_ending.as_str()));
474 if index + 1 == blocks.len() && *position + 1 == base.len() {
475 output.push_str(&source[body_end..]);
476 ends_with_source_tail = true;
477 }
478 previous_body_end = body_end;
479 let marker = match block {
480 Block::List(list) => Some(written_marker(list)),
481 _ => None,
482 };
483 placement.wrote(block, marker, open_lists[index]);
484 at = Some(*position);
485 }
486 None => {
487 let mut text = write_blocks(
488 core::slice::from_ref(block),
489 options,
490 Join::Gap,
491 index == 0,
492 placement.previous_list(),
493 )?;
494 if options.line_ending == LineEnding::CrLf {
495 text = lf_to_crlf(&text);
496 }
497 output.push_str(&text);
498 let marker = match block {
499 Block::List(list) => {
500 Some(list_marker(list, options, placement.previous_list()))
501 }
502 _ => None,
503 };
504 placement.wrote(block, marker, false);
505 }
506 }
507 previous_at = at;
508 }
509 let ends_with_blank_line = matches!(blocks.last(), Some(Block::BlankLine(_)));
510 if !ends_with_source_tail
511 && options.final_newline
512 && !blocks.is_empty()
513 && (ends_with_blank_line || !ends_with_carriage_return_ending(&output))
514 {
515 output.push_str(match previous_ending {
518 "" => options.line_ending.as_str(),
519 own => own,
520 });
521 }
522 let partly = partly
523 .iter()
524 .enumerate()
525 .filter_map(|(index, lines)| lines.as_ref().map(|_| index))
526 .collect();
527 Ok((output, partly))
528}
529
530fn misread(blocks: &[Block], output: &str, partly: &[usize]) -> Vec<usize> {
535 let read = crate::parse(output).document.children;
536 if read.len() != blocks.len() {
537 return partly.to_vec();
538 }
539 partly
540 .iter()
541 .copied()
542 .filter(|index| !reads_as(&read[*index], &blocks[*index]))
543 .collect()
544}
545
546fn reads_as(read: &Block, block: &Block) -> bool {
550 let mut read = crate::compare::normalized_blocks(core::slice::from_ref(read));
551 let mut block = crate::compare::normalized_blocks(core::slice::from_ref(block));
552 number_items(&mut read);
553 number_items(&mut block);
554 read == block
555}
556
557fn number_items(blocks: &mut [Block]) {
559 for block in blocks {
560 match block {
561 Block::List(list) => {
562 let mut previous = None;
563 for item in &mut list.children {
564 if list.ordered {
565 item.number = Some(item_number(item, &mut previous));
566 }
567 number_items(&mut item.children);
568 }
569 }
570 Block::BlockQuote(node) => number_items(&mut node.children),
571 Block::Alert(node) => number_items(&mut node.children),
572 Block::FootnoteDefinition(node) => number_items(&mut node.children),
573 Block::ContainerDirective(node) => number_items(&mut node.children),
574 Block::HtmlContainer(node) => {
575 if let HtmlContainerContent::Blocks(children) = &mut node.content {
576 number_items(children);
577 }
578 }
579 Block::Paragraph(_)
580 | Block::Heading(_)
581 | Block::ThematicBreak(_)
582 | Block::CodeBlock(_)
583 | Block::HtmlBlock(_)
584 | Block::Definition(_)
585 | Block::Table(_)
586 | Block::MathBlock(_)
587 | Block::Frontmatter(_)
588 | Block::LeafDirective(_)
589 | Block::BlankLine(_) => {}
590 }
591 }
592}
593
594struct Correspondence<'a> {
599 base: &'a Document,
600 sources: Option<BTreeMap<(usize, usize), InlineSource>>,
603}
604
605impl Correspondence<'_> {
606 fn sources(&mut self) -> &BTreeMap<(usize, usize), InlineSource> {
608 let base_document = self.base;
609 self.sources.get_or_insert_with(|| {
610 let mut sources = BTreeMap::new();
611 base_document.each_leaf_source(|_, span, source| {
612 sources.insert((span.start, span.end), source);
613 });
614 sources
615 })
616 }
617
618 fn source_of(&mut self, span: Span) -> Option<&InlineSource> {
620 self.sources().get(&(span.start, span.end))
621 }
622
623 fn holds(&mut self, block: &Block, base: &Block) -> bool {
624 if !leaves::any_kept(core::slice::from_ref(block)) {
625 return block == base;
626 }
627 let sources = self.sources();
628 let mut block = block.clone();
631 leaves::visit_mut(core::slice::from_mut(&mut block), &mut |leaf| {
632 let (Some(text), Some(span)) = (leaf.source.as_deref(), leaf.span) else {
633 return;
634 };
635 if let Some(source) = sources
636 .get(&(span.start, span.end))
637 .filter(|source| source.text == text)
638 {
639 crate::parse::inline_spans_to_source(source, leaf.inlines);
640 *leaf.source = None;
641 }
642 });
643 block == *base
644 }
645}
646
647fn separated_boundaries(blocks: &[Block]) -> Vec<bool> {
651 let mut separated = alloc::vec![false; blocks.len()];
652 let mut block_follows = false;
653 for index in (1..blocks.len()).rev() {
654 block_follows |= !matches!(blocks[index], Block::BlankLine(_));
655 separated[index] = block_follows && !matches!(blocks[index - 1], Block::BlankLine(_));
656 }
657 separated
658}
659
660fn moves_into_previous(
673 block: &Block,
674 text: &str,
675 last: Option<&Block>,
676 previous_list: Option<char>,
677) -> bool {
678 match block {
679 Block::BlankLine(_) => false,
680 Block::List(list) if previous_list == Some(written_marker(list)) => true,
681 _ if matches!(last, Some(Block::List(_) | Block::FootnoteDefinition(_))) => {
682 text.starts_with([' ', '\t'])
683 }
684 Block::CodeBlock(_) => false,
685 _ => opens_with_code_indent(text),
686 }
687}
688
689fn opens_with_code_indent(text: &str) -> bool {
692 let mut column = 0;
693 for char in text.chars() {
694 match char {
695 ' ' => column += 1,
696 '\t' => column += 4 - column % 4,
697 _ => break,
698 }
699 if column >= 4 {
700 return true;
701 }
702 }
703 false
704}
705
706fn closed_by_a_blank_line(text: &str) -> bool {
711 let gap = if text.ends_with('\n') {
712 "\n"
713 } else if text.ends_with('\r') {
714 "\r"
715 } else {
716 "\n\n"
717 };
718 let probe = format!("{text}{gap}x");
719 let children = crate::parse(&probe).document.children;
720 let Some((Block::Paragraph(last), rest)) = children.split_last() else {
721 return false;
722 };
723 last.meta
724 .span
725 .is_some_and(|span| span.start + 1 == probe.len())
726 && rest.iter().all(|block| {
727 matches!(block, Block::BlankLine(_))
728 || block.span().is_some_and(|span| span.end <= text.len())
729 })
730}
731
732fn push_gap(output: &mut String, options: &SerializeOptions) {
734 let from = output.len();
735 push_block_gap(output);
736 if options.line_ending == LineEnding::CrLf {
737 let gap = lf_to_crlf(&output[from..]);
738 output.truncate(from);
739 output.push_str(&gap);
740 }
741}
742
743fn strip_line_ending(text: &str) -> &str {
745 text.strip_suffix("\r\n")
746 .or_else(|| text.strip_suffix('\n'))
747 .or_else(|| text.strip_suffix('\r'))
748 .unwrap_or(text)
749}
750
751fn lf_to_crlf(input: &str) -> String {
754 let mut output = String::with_capacity(input.len());
755 let mut previous = '\0';
756 for ch in input.chars() {
757 if ch == '\n' && previous != '\r' {
758 output.push('\r');
759 }
760 output.push(ch);
761 previous = ch;
762 }
763 output
764}
765
766fn ends_with_carriage_return_ending(input: &str) -> bool {
767 input.ends_with('\r') || input.ends_with("\r\n")
768}
769
770fn push_block_gap(output: &mut String) {
774 if output.ends_with('\r') {
775 output.push('\r');
776 } else if output.ends_with("\r\n") {
777 output.push('\n');
778 } else {
779 output.push_str("\n\n");
780 }
781}
782
783#[derive(Clone, Copy, Eq, PartialEq)]
785enum Join {
786 Gap,
788 Line,
790}
791
792fn write_blocks(
800 blocks: &[Block],
801 options: &SerializeOptions,
802 join: Join,
803 document_start: bool,
804 previous_list: Option<char>,
805) -> Result<String, SerializeError> {
806 let mut output = String::new();
807 let mut previous_list = previous_list;
810 let mut wrote_block = false;
813 let mut blank_lines = 0;
814 for (index, block) in blocks.iter().enumerate() {
815 if let Block::BlankLine(_) = block {
816 blank_lines += 1;
817 continue;
818 }
819 if wrote_block {
820 match join {
821 Join::Gap => push_block_gap(&mut output),
822 Join::Line => output.push('\n'),
823 }
824 }
825 output.extend(core::iter::repeat_n('\n', blank_lines));
826 blank_lines = 0;
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)
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)),
868 Block::BlockQuote(node) => {
869 let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
870 Ok(if inner.is_empty() {
873 "> ".into()
874 } else {
875 let mut written = quote_lines(&inner);
876 if written.starts_with(">\n") || written.starts_with(">\r") {
877 written.insert(1, ' ');
878 }
879 written
880 })
881 }
882 Block::Alert(node) => write_alert(node, options),
883 Block::List(node) => write_list(node, options, list_marker(node, options, None)),
884 Block::CodeBlock(node) => Ok(write_code_block(node, options)),
885 Block::HtmlBlock(node) => Ok(node.value.clone()),
889 Block::HtmlContainer(node) => write_html_container(node, options),
890 Block::Definition(node) => Ok(write_definition(node)),
891 Block::FootnoteDefinition(node) => {
892 let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
893 Ok(if inner.starts_with([' ', '\t']) {
897 format!(
898 "[^{}]:\n{}",
899 node.label,
900 prefix_container_lines(&inner, " ")
901 )
902 } else {
903 format!("[^{}]: {}", node.label, indent_continuation(&inner))
904 })
905 }
906 Block::Table(node) => write_table(node),
907 Block::MathBlock(node) => {
908 let fence = block_math_fence(&node.value);
909 Ok(fenced_body(&fence, &node.value, &fence))
910 }
911 Block::Frontmatter(node) => {
912 let fence = match node.kind {
913 FrontmatterKind::Yaml => "---",
914 FrontmatterKind::Toml => "+++",
915 };
916 Ok(format!("{fence}\n{}\n{fence}", node.value))
919 }
920 Block::LeafDirective(node) => Ok(format!(
921 "::{}{}{}",
922 node.name,
923 write_directive_label(&node.label, node.label_source.as_deref())?,
924 write_attributes(&node.attributes)
925 )),
926 Block::ContainerDirective(node) => {
927 let mut inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
928 let fence = directive_fence(&inner);
929 if !node.children.is_empty() {
933 inner.push('\n');
934 }
935 Ok(format!(
936 "{fence}{}{}{}\n{inner}{fence}",
937 node.name,
938 write_directive_label(&node.label, node.label_source.as_deref())?,
939 write_attributes(&node.attributes)
940 ))
941 }
942 }
943}
944
945fn write_thematic_break(node: &ThematicBreak, at_document_start: bool) -> String {
948 match node.marker {
949 ThematicBreakMarker::Dash if at_document_start => "- - -".into(),
950 ThematicBreakMarker::Dash => "---".into(),
951 ThematicBreakMarker::Asterisk => "***".into(),
952 ThematicBreakMarker::Underscore => "___".into(),
953 }
954}
955
956fn write_heading(node: &Heading) -> Result<String, SerializeError> {
958 let content = match one_line(node.source.as_deref(), true) {
959 Some(text) => text.into(),
960 None => write_inlines(&node.children, Context::HEADING)?,
961 };
962 let hashes = "#".repeat(usize::from(node.depth));
963 Ok(if content.is_empty() {
964 format!("{hashes} ")
965 } else if reads_as_closing_sequence(&content) {
966 format!("{hashes} {content} #")
969 } else {
970 format!("{hashes} {content}")
971 })
972}
973
974fn reads_as_closing_sequence(content: &str) -> bool {
977 let before = content.trim_end_matches('#');
978 before.len() < content.len() && (before.is_empty() || before.ends_with([' ', '\t']))
979}
980
981fn write_definition(node: &Definition) -> String {
982 let destination = write_destination(&node.destination, node.destination_kind);
983 let mut output = format!("[{}]: {}", node.label, destination);
984 if let Some(title) = &node.title {
985 output.push(' ');
986 output.push_str(&write_title(title));
987 }
988 output
989}
990
991fn write_html_container(
992 node: &HtmlContainer,
993 options: &SerializeOptions,
994) -> Result<String, SerializeError> {
995 match &node.content {
996 HtmlContainerContent::Blocks(children) => {
997 let inner = write_blocks(children, options, Join::Gap, false, None)?;
998 let blank_lines = children
1001 .iter()
1002 .all(|block| matches!(block, Block::BlankLine(_)));
1003 Ok(if blank_lines {
1004 format!(
1005 "{}\n{}{}",
1006 node.opening.raw,
1007 "\n".repeat(children.len()),
1008 node.closing.raw
1009 )
1010 } else {
1011 format!("{}\n{}\n\n{}", node.opening.raw, inner, node.closing.raw)
1012 })
1013 }
1014 HtmlContainerContent::Inlines(children) => Ok(format!(
1015 "{}{}{}",
1016 node.opening.raw,
1017 write_inlines(children, Context::BLOCK)?,
1018 node.closing.raw
1019 )),
1020 }
1021}
1022
1023fn write_alert(node: &Alert, options: &SerializeOptions) -> Result<String, SerializeError> {
1024 let mut output = String::from("> [!");
1025 output.push_str(alert_kind_name(node.kind));
1026 output.push(']');
1027 if let Some(title) = &node.title {
1028 if !title.is_empty() {
1029 output.push(' ');
1030 output.push_str(&title.replace(['\n', '\r'], " "));
1032 }
1033 }
1034 let inner = write_blocks(&node.children, options, Join::Gap, false, None)?;
1035 if !inner.is_empty() {
1036 output.push('\n');
1037 output.push_str("e_lines(&inner));
1038 } else if !node.children.is_empty() {
1039 output.push_str("\n>");
1041 }
1042 Ok(output)
1043}
1044
1045fn quote_lines(inner: &str) -> String {
1048 let mut output = prefix_container_lines(inner, "> ");
1049 if inner.ends_with(['\n', '\r']) {
1050 output.push('>');
1051 }
1052 output
1053}
1054
1055fn alert_kind_name(kind: AlertKind) -> &'static str {
1056 match kind {
1057 AlertKind::Note => "NOTE",
1058 AlertKind::Tip => "TIP",
1059 AlertKind::Important => "IMPORTANT",
1060 AlertKind::Warning => "WARNING",
1061 AlertKind::Caution => "CAUTION",
1062 }
1063}
1064
1065fn list_marker(list: &List, options: &SerializeOptions, previous: Option<char>) -> char {
1071 let marker = if list.ordered {
1072 options
1073 .ordered_delimiter
1074 .map_or_else(|| written_marker(list), ordered_delimiter_char)
1075 } else {
1076 options
1077 .bullet
1078 .map_or_else(|| written_marker(list), bullet_char)
1079 };
1080 if previous == Some(marker) {
1081 let order: &[char] = if list.ordered {
1082 &['.', ')']
1083 } else {
1084 &['-', '*', '+']
1085 };
1086 let at = order.iter().position(|char| *char == marker).unwrap_or(0);
1087 return order[(at + 1) % order.len()];
1088 }
1089 marker
1090}
1091
1092pub(crate) fn written_marker(list: &List) -> char {
1096 match (list.ordered, list.delimiter) {
1097 (false, ListDelimiter::Asterisk) => '*',
1098 (false, ListDelimiter::Plus) => '+',
1099 (false, _) => '-',
1100 (true, ListDelimiter::Paren) => ')',
1101 (true, _) => '.',
1102 }
1103}
1104
1105fn bullet_char(bullet: BulletMarker) -> char {
1106 match bullet {
1107 BulletMarker::Dash => '-',
1108 BulletMarker::Asterisk => '*',
1109 BulletMarker::Plus => '+',
1110 }
1111}
1112
1113fn ordered_delimiter_char(delimiter: OrderedDelimiter) -> char {
1114 match delimiter {
1115 OrderedDelimiter::Period => '.',
1116 OrderedDelimiter::Paren => ')',
1117 }
1118}
1119
1120fn write_list(
1121 node: &List,
1122 options: &SerializeOptions,
1123 marker_char: char,
1124) -> Result<String, SerializeError> {
1125 let mut output = String::new();
1126 let mut previous: Option<u64> = None;
1128 for (index, item) in node.children.iter().enumerate() {
1129 if index > 0 {
1130 output.push_str(if node.tight { "\n" } else { "\n\n" });
1131 }
1132 output.push_str(&write_item(
1133 node,
1134 item,
1135 options,
1136 marker_char,
1137 &mut previous,
1138 )?);
1139 }
1140 Ok(output)
1141}
1142
1143fn item_number(item: &ListItem, previous: &mut Option<u64>) -> u64 {
1146 let number = item.number.unwrap_or_else(|| {
1149 previous.map_or(1, |previous| {
1150 previous
1151 .saturating_add(1)
1152 .min(crate::parse::MAX_ORDERED_NUMBER)
1153 })
1154 });
1155 *previous = Some(number);
1156 number
1157}
1158
1159pub(crate) fn written_opens_fence_like(block: &Block) -> bool {
1163 write_block(block, &SerializeOptions::default()).is_ok_and(|written| {
1164 let content = written.trim_start_matches(' ');
1165 written.len() - content.len() < 4 && content.starts_with(['`', '~'])
1166 })
1167}
1168
1169fn write_item(
1174 list: &List,
1175 item: &ListItem,
1176 options: &SerializeOptions,
1177 marker_char: char,
1178 previous: &mut Option<u64>,
1179) -> Result<String, SerializeError> {
1180 let marker = if list.ordered {
1181 format!("{}{marker_char} ", item_number(item, previous))
1182 } else {
1183 format!("{marker_char} ")
1184 };
1185 let inner = write_item_blocks(&item.children, options, list.tight, item.checked)?;
1186 Ok(format!(
1187 "{marker}{}",
1188 indent_after_first_line(&inner, marker.len())
1189 ))
1190}
1191
1192fn misreads_after_marker(
1199 list: &List,
1200 item: &ListItem,
1201 marker_char: char,
1202 first_line: &str,
1203) -> bool {
1204 opens_with_padding(first_line)
1205 || (!list.ordered
1206 && item.checked.is_none()
1207 && reads_as_thematic_break(marker_char, first_line))
1208}
1209
1210pub(super) fn reads_as_thematic_break(marker_char: char, rest: &str) -> bool {
1213 matches!(marker_char, '-' | '*')
1214 && rest
1215 .chars()
1216 .all(|char| matches!(char, ' ' | '\t') || char == marker_char)
1217 && rest.chars().filter(|char| *char == marker_char).count() >= 2
1218}
1219
1220fn item_first_line(
1223 list: &List,
1224 item: &ListItem,
1225 marker_char: char,
1226 options: &SerializeOptions,
1227) -> String {
1228 let marker = if list.ordered {
1229 format!("{}{marker_char}", item.number.unwrap_or(1))
1230 } else {
1231 String::from(marker_char)
1232 };
1233 format!("{marker} {}", item_content_first_line(item, options))
1234}
1235
1236fn item_content_first_line(item: &ListItem, options: &SerializeOptions) -> String {
1240 if item.checked.is_some() {
1241 return String::from("[");
1242 }
1243 match item.children.first() {
1244 None => String::new(),
1245 Some(Block::List(nested)) => match nested.children.first() {
1246 Some(first) => {
1247 item_first_line(nested, first, list_marker(nested, options, None), options)
1248 }
1249 None => String::new(),
1250 },
1251 Some(
1252 block @ (Block::Paragraph(_)
1253 | Block::Heading(_)
1254 | Block::HtmlBlock(_)
1255 | Block::CodeBlock(_)
1256 | Block::ThematicBreak(_)),
1257 ) => write_block(block, options)
1258 .map(|written| String::from(first_line(&written)))
1259 .unwrap_or_default(),
1260 Some(_) => String::from(">"),
1262 }
1263}
1264
1265fn first_line(text: &str) -> &str {
1267 text.split(['\n', '\r']).next().unwrap_or_default()
1268}
1269
1270fn opens_with_padding(inner: &str) -> bool {
1274 !inner.starts_with(" ") && inner.starts_with([' ', '\t'])
1275}
1276
1277pub(crate) fn item_misreads_after_marker(list: &List, item: &ListItem) -> bool {
1282 let options = SerializeOptions::default();
1283 let content = item_content_first_line(item, &options);
1284 misreads_after_marker(list, item, written_marker(list), &content)
1285}
1286
1287fn write_item_blocks(
1291 blocks: &[Block],
1292 options: &SerializeOptions,
1293 tight: bool,
1294 task: Option<bool>,
1295) -> Result<String, SerializeError> {
1296 let join = if tight { Join::Line } else { Join::Gap };
1297 let Some(checked) = task else {
1298 return write_blocks(blocks, options, join, false, None);
1299 };
1300 let checkbox = if checked { "[x]" } else { "[ ]" };
1301 let definitions = blocks
1302 .iter()
1303 .take_while(|block| matches!(block, Block::Definition(_)))
1304 .count();
1305 if matches!(blocks.get(definitions), Some(Block::Paragraph(_))) {
1306 let before = write_blocks(&blocks[..definitions], options, join, false, None)?;
1307 let rest = write_blocks(&blocks[definitions..], options, join, false, None)?;
1308 let mut output = before;
1309 if !output.is_empty() {
1310 match join {
1311 Join::Gap => push_block_gap(&mut output),
1312 Join::Line => output.push('\n'),
1313 }
1314 }
1315 output.push_str(checkbox);
1316 output.push(' ');
1317 output.push_str(&rest);
1318 return Ok(output);
1319 }
1320 let written = write_blocks(blocks, options, join, false, None)?;
1321 Ok(if written.is_empty() {
1322 checkbox.into()
1323 } else {
1324 format!("{checkbox} {written}")
1325 })
1326}
1327
1328fn write_code_block(node: &CodeBlock, options: &SerializeOptions) -> String {
1329 match node.kind {
1330 CodeBlockKind::Indented => {
1331 let body = trim_trailing_newline(&node.value);
1334 let mut output = prefix_lines(body, " ");
1335 let ending = &node.value[body.len()..];
1336 if matches!(ending, "\r" | "\r\n") {
1337 output.push_str(ending);
1338 }
1339 output
1340 }
1341 CodeBlockKind::Fenced { marker, length } => {
1342 let marker = code_block_fence_marker(node, marker, options);
1343 let (fence, indent) = code_block_fence(&node.value, marker, length.max(3));
1344 let mut opener = fence.clone();
1345 if let Some(info) = &node.info {
1346 let info = escape_code_info(info);
1347 if fence.ends_with(|char| info.starts_with(char)) {
1350 opener.push(' ');
1351 }
1352 opener.push_str(&info);
1353 }
1354 let body = fenced_body(&opener, &node.value, &fence);
1355 if indent == 0 {
1356 body
1357 } else {
1358 prefix_lines(&body, &" ".repeat(indent))
1359 }
1360 }
1361 }
1362}
1363
1364fn fenced_body(opener: &str, value: &str, closer: &str) -> String {
1368 let mut output = String::with_capacity(opener.len() + value.len() + closer.len() + 2);
1369 output.push_str(opener);
1370 output.push('\n');
1371 output.push_str(value);
1372 if !value.is_empty() && !ends_with_line_ending(value) {
1373 output.push('\n');
1374 }
1375 output.push_str(closer);
1376 output
1377}
1378
1379fn code_block_fence_marker(
1383 node: &CodeBlock,
1384 marker: FenceMarker,
1385 options: &SerializeOptions,
1386) -> FenceMarker {
1387 if node.info.as_deref().is_some_and(|info| info.contains('`')) {
1388 return FenceMarker::Tilde;
1389 }
1390 options.fence_marker.unwrap_or(marker)
1391}
1392
1393fn escape_code_info(input: &str) -> String {
1394 let mut output = String::new();
1395 let inner_start = input.len() - input.trim_start_matches([' ', '\t']).len();
1398 let inner_end = input.trim_end_matches([' ', '\t']).len().max(inner_start);
1399 for (offset, char) in input.char_indices() {
1400 match char {
1401 ' ' | '\t' if offset < inner_start || offset >= inner_end => {
1402 output.push_str(&char_reference(char));
1403 }
1404 '\t' => output.push(char),
1405 char if char.is_control() => output.push_str(&char_reference(char)),
1406 '\\' | '&' => {
1407 output.push('\\');
1408 output.push(char);
1409 }
1410 _ => output.push(char),
1411 }
1412 }
1413 output
1414}
1415
1416fn char_reference(char: char) -> String {
1418 format!("&#x{:X};", char as u32)
1419}
1420
1421fn write_table(node: &Table) -> Result<String, SerializeError> {
1422 let mut output = String::new();
1423 for (row_index, row) in node.rows.iter().enumerate() {
1424 if row_index > 0 {
1425 output.push('\n');
1426 }
1427 output.push_str(&write_row(row)?);
1428 if row_index == 0 {
1429 output.push('\n');
1430 output.push_str(&write_delimiter_row(&node.alignments));
1431 }
1432 }
1433 Ok(output)
1434}
1435
1436fn write_delimiter_row(alignments: &[TableAlignment]) -> String {
1437 let cells = alignments
1438 .iter()
1439 .map(|alignment| match alignment {
1440 TableAlignment::None => "---",
1441 TableAlignment::Left => ":---",
1442 TableAlignment::Center => ":---:",
1443 TableAlignment::Right => "---:",
1444 })
1445 .collect::<Vec<_>>();
1446 format!("| {} |", cells.join(" | "))
1447}
1448
1449fn write_row(row: &TableRow) -> Result<String, SerializeError> {
1450 let cells = row
1451 .cells
1452 .iter()
1453 .map(|cell| {
1454 Ok(encode_cell(&match one_line(cell.source.as_deref(), true) {
1455 Some(text) => text.into(),
1456 None => write_inlines(&cell.children, Context::BLOCK)?,
1457 }))
1458 })
1459 .collect::<Result<Vec<_>, SerializeError>>()?;
1460 Ok(format!("| {} |", cells.join(" | ")))
1461}
1462
1463fn encode_cell(written: &str) -> String {
1471 let mut output = String::with_capacity(written.len());
1472 let mut backslashes = 0usize;
1473 for char in written.chars() {
1474 if char == '|' && backslashes % 2 == 0 {
1475 output.push('\\');
1476 }
1477 backslashes = if char == '\\' { backslashes + 1 } else { 0 };
1478 output.push(char);
1479 }
1480 output
1481}
1482
1483#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1486struct Context {
1487 heading: bool,
1489}
1490
1491impl Context {
1492 const BLOCK: Self = Self { heading: false };
1493 const HEADING: Self = Self { heading: true };
1494}
1495
1496fn write_inlines(inlines: &[Inline], context: Context) -> Result<String, SerializeError> {
1497 let mut output = InlineOut::default();
1498 for inline in inlines {
1499 write_inline(inline, context, &mut output)?;
1500 }
1501 Ok(output.text)
1502}
1503
1504#[derive(Default)]
1508struct InlineOut {
1509 text: String,
1510 open_runs: Vec<bool>,
1514 scanned: usize,
1516}
1517
1518impl InlineOut {
1519 fn scan_open_runs(&mut self) {
1523 let mut escaped = false;
1524 let mut run = 0;
1525 let mut run_escaped = false;
1526 for byte in self.text.as_bytes()[self.scanned..]
1527 .iter()
1528 .copied()
1529 .chain(core::iter::once(b' '))
1530 {
1531 if byte == b'`' {
1532 if run == 0 {
1533 run_escaped = escaped;
1534 }
1535 run += 1;
1536 escaped = false;
1537 continue;
1538 }
1539 let opening = run - usize::from(run_escaped && run > 0);
1540 if opening > 0 {
1541 if self.open_runs.len() <= opening {
1542 self.open_runs.resize(opening + 1, false);
1543 }
1544 self.open_runs[opening] = true;
1545 }
1546 run = 0;
1547 escaped = byte == b'\\' && !escaped;
1548 }
1549 self.scanned = self.text.len();
1550 }
1551
1552 fn is_open_run(&self, length: usize) -> bool {
1553 self.open_runs.get(length).copied().unwrap_or(false)
1554 }
1555
1556 fn push_opaque(&mut self, write: impl FnOnce(&mut String)) {
1559 self.scan_open_runs();
1560 write(&mut self.text);
1561 self.scanned = self.text.len();
1562 }
1563}
1564
1565impl core::ops::Deref for InlineOut {
1566 type Target = String;
1567
1568 fn deref(&self) -> &String {
1569 &self.text
1570 }
1571}
1572
1573impl core::ops::DerefMut for InlineOut {
1574 fn deref_mut(&mut self) -> &mut String {
1575 &mut self.text
1576 }
1577}
1578
1579fn write_inline(
1580 inline: &Inline,
1581 context: Context,
1582 out: &mut InlineOut,
1583) -> Result<(), SerializeError> {
1584 match inline {
1585 Inline::Text(node) => out.push_str(&node.value),
1586 Inline::Escape(node) => {
1587 out.push('\\');
1588 out.push(node.value);
1589 }
1590 Inline::CharacterReference(node) => out.push_str(&node.reference),
1591 Inline::Emphasis(node) => {
1592 let delimiter = emphasis_delimiter(node.delimiter);
1593 write_span(out, delimiter, &node.children, delimiter, context)?;
1594 }
1595 Inline::Strong(node) => {
1596 let delimiter = emphasis_delimiter(node.delimiter).repeat(2);
1597 write_span(out, &delimiter, &node.children, &delimiter, context)?;
1598 }
1599 Inline::Delete(node) => write_span(out, "~~", &node.children, "~~", context)?,
1600 Inline::Mark(node) => write_span(out, "==", &node.children, "==", context)?,
1601 Inline::InlineFootnote(node) => write_span(out, "^[", &node.children, "]", context)?,
1602 Inline::Shortcode(node) => {
1603 out.push(':');
1604 out.push_str(&node.name);
1605 out.push(':');
1606 }
1607 Inline::Tag(node) => {
1608 out.push('#');
1609 out.push_str(&node.name);
1610 }
1611 Inline::Code(node) => write_code_span(node, out),
1612 Inline::Link(node) => {
1613 write_span(out, "[", &node.children, "](", context)?;
1614 out.push_opaque(|out| {
1615 write_resource(
1616 out,
1617 &node.destination,
1618 node.destination_kind,
1619 node.title.as_ref(),
1620 );
1621 });
1622 }
1623 Inline::Autolink(node) => out.push_opaque(|out| match node.form {
1624 AutolinkForm::Literal => out.push_str(&node.text),
1625 AutolinkForm::Angle => {
1626 out.push('<');
1627 out.push_str(&node.text);
1628 out.push('>');
1629 }
1630 }),
1631 Inline::Image(node) => {
1632 write_span(out, "?;
1633 out.push_opaque(|out| {
1634 write_resource(
1635 out,
1636 &node.destination,
1637 node.destination_kind,
1638 node.title.as_ref(),
1639 );
1640 });
1641 }
1642 Inline::LinkReference(node) => {
1643 write_reference(out, "[", &node.children, node.kind, &node.label, context)?;
1644 }
1645 Inline::ImageReference(node) => {
1646 write_reference(out, "![", &node.alt, node.kind, &node.label, context)?;
1647 }
1648 Inline::Html(node) => out.push_opaque(|out| out.push_str(&node.value)),
1649 Inline::SoftBreak(_) if context.heading => out.push(' '),
1650 Inline::SoftBreak(_) => out.push('\n'),
1651 Inline::LineBreak(node) => match node.kind {
1652 LineBreakKind::Backslash => out.push_str("\\\n"),
1653 LineBreakKind::Spaces => out.push_str(" \n"),
1654 },
1655 Inline::Math(node) => {
1656 let math = write_inline_math(node);
1657 out.push_opaque(|out| out.push_str(&math));
1658 }
1659 Inline::FootnoteReference(node) => {
1660 out.push_str("[^");
1661 out.push_str(&node.label);
1662 out.push(']');
1663 }
1664 Inline::WikiLink(node) => out.push_opaque(|out| {
1665 if node.embed {
1666 out.push('!');
1667 }
1668 out.push_str("[[");
1669 out.push_str(&node.target);
1670 if node.target != node.label {
1671 out.push('|');
1672 out.push_str(&node.label);
1673 }
1674 out.push_str("]]");
1675 }),
1676 Inline::TextDirective(node) => {
1677 let label = if node.label.is_empty() {
1679 String::new()
1680 } else {
1681 format!("[{}]", write_inlines(&node.label, context)?)
1682 };
1683 out.push_opaque(|out| {
1684 out.push(':');
1685 out.push_str(&node.name);
1686 out.push_str(&label);
1687 out.push_str(&write_attributes(&node.attributes));
1688 });
1689 }
1690 }
1691 Ok(())
1692}
1693
1694fn emphasis_delimiter(delimiter: EmphasisDelimiter) -> &'static str {
1695 match delimiter {
1696 EmphasisDelimiter::Asterisk => "*",
1697 EmphasisDelimiter::Underscore => "_",
1698 }
1699}
1700
1701fn write_span(
1703 out: &mut InlineOut,
1704 open: &str,
1705 children: &[Inline],
1706 close: &str,
1707 context: Context,
1708) -> Result<(), SerializeError> {
1709 out.push_str(open);
1710 for child in children {
1711 write_inline(child, context, out)?;
1712 }
1713 out.push_str(close);
1714 Ok(())
1715}
1716
1717fn write_resource(
1719 out: &mut String,
1720 destination: &str,
1721 kind: LinkDestinationKind,
1722 title: Option<&Title>,
1723) {
1724 out.push_str(&write_destination(destination, kind));
1725 if let Some(title) = title {
1726 out.push(' ');
1727 out.push_str(&write_title(title));
1728 }
1729 out.push(')');
1730}
1731
1732fn write_reference(
1738 out: &mut InlineOut,
1739 open: &str,
1740 children: &[Inline],
1741 kind: ReferenceKind,
1742 label: &str,
1743 context: Context,
1744) -> Result<(), SerializeError> {
1745 match kind {
1746 ReferenceKind::Full => write_span(out, open, children, "]", context)?,
1747 ReferenceKind::Shortcut | ReferenceKind::Collapsed => {
1748 out.push_str(open);
1749 out.push_str(label);
1750 out.push(']');
1751 }
1752 }
1753 out.push_opaque(|out| write_reference_kind(out, kind, label));
1754 Ok(())
1755}
1756
1757fn write_reference_kind(out: &mut String, kind: ReferenceKind, label: &str) {
1758 match kind {
1759 ReferenceKind::Shortcut => {}
1760 ReferenceKind::Collapsed => out.push_str("[]"),
1761 ReferenceKind::Full => {
1762 out.push('[');
1763 out.push_str(label);
1765 out.push(']');
1766 }
1767 }
1768}
1769
1770fn write_code_span(node: &CodeInline, out: &mut InlineOut) {
1775 let value = &node.value;
1776 out.scan_open_runs();
1777 let held = backtick_runs(value);
1778 let before = out.len() - out.trim_end_matches('`').len();
1782 let length = (1..)
1783 .find(|length| {
1784 !held.get(*length).copied().unwrap_or(false)
1785 && !out.is_open_run(*length)
1786 && (before == 0 || !out.is_open_run(before + *length))
1787 })
1788 .unwrap_or(1);
1789 let fence = "`".repeat(length);
1790 out.push_str(&fence);
1791 if code_span_needs_padding(value) {
1792 out.push(' ');
1793 out.push_str(value);
1794 out.push(' ');
1795 } else {
1796 out.push_str(value);
1797 }
1798 out.push_str(&fence);
1799 out.scanned = out.text.len();
1801}
1802
1803fn backtick_runs(value: &str) -> Vec<bool> {
1805 let mut runs = alloc::vec![false; value.len() + 1];
1807 let mut run = 0;
1808 for byte in value.bytes() {
1809 if byte == b'`' {
1810 run += 1;
1811 } else {
1812 runs[run] = true;
1813 run = 0;
1814 }
1815 }
1816 runs[run] = true;
1817 runs
1818}
1819
1820fn code_span_needs_padding(input: &str) -> bool {
1821 input.starts_with('`')
1822 || input.ends_with('`')
1823 || (input.starts_with(' ') && input.ends_with(' ') && input.chars().any(|char| char != ' '))
1824}
1825
1826pub(crate) fn write_inline_math(node: &MathInline) -> String {
1827 match node.kind {
1828 MathInlineKind::Code => format!("$`{}`$", node.value),
1829 MathInlineKind::Dollar { dollars } => {
1831 let fence = "$".repeat(usize::from(dollars));
1832 format!("{fence}{}{fence}", node.value)
1833 }
1834 }
1835}
1836
1837fn write_directive_label(label: &[Inline], source: Option<&str>) -> Result<String, SerializeError> {
1840 if let Some(text) = one_line(source, true) {
1841 return Ok(if text.is_empty() {
1842 String::new()
1843 } else {
1844 format!("[{text}]")
1845 });
1846 }
1847 if label.is_empty() {
1848 Ok(String::new())
1849 } else {
1850 Ok(format!("[{}]", write_inlines(label, Context::BLOCK)?))
1851 }
1852}
1853
1854fn one_line(source: Option<&str>, single: bool) -> Option<&str> {
1858 source.filter(|text| !single || !text.contains(['\n', '\r']))
1859}
1860
1861fn write_attributes(attributes: &[DirectiveAttribute]) -> String {
1862 if attributes.is_empty() {
1863 return String::new();
1864 }
1865 let mut output = String::from("{");
1866 for (index, attribute) in attributes.iter().enumerate() {
1867 if index > 0 {
1868 output.push(' ');
1869 }
1870 match (&*attribute.name, &attribute.value) {
1871 ("id", Some(value)) if is_directive_shorthand_value(value) => {
1872 output.push('#');
1873 output.push_str(value);
1874 }
1875 ("class", Some(value)) if is_directive_shorthand_value(value) => {
1876 output.push('.');
1877 output.push_str(value);
1878 }
1879 (_, Some(value)) => {
1880 output.push_str(&attribute.name);
1881 output.push_str("=\"");
1882 output.push_str(&escape_title(value, LinkTitleKind::DoubleQuote));
1883 output.push('"');
1884 }
1885 (_, None) => output.push_str(&attribute.name),
1886 }
1887 }
1888 output.push('}');
1889 output
1890}
1891
1892fn is_directive_shorthand_value(input: &str) -> bool {
1893 !input.is_empty()
1894 && input
1895 .chars()
1896 .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1897}
1898
1899fn write_destination(input: &str, kind: LinkDestinationKind) -> String {
1900 match kind {
1901 LinkDestinationKind::Omitted if input.is_empty() => String::new(),
1902 LinkDestinationKind::Angle => {
1903 let mut output = String::from("<");
1904 for char in input.chars() {
1905 match char {
1906 char if char.is_control() => output.push_str(&char_reference(char)),
1907 '\\' | '<' | '>' => {
1908 output.push('\\');
1909 output.push(char);
1910 }
1911 _ => output.push(char),
1912 }
1913 }
1914 output.push('>');
1915 output
1916 }
1917 LinkDestinationKind::Bare | LinkDestinationKind::Omitted if input.is_empty() => "<>".into(),
1918 LinkDestinationKind::Bare | LinkDestinationKind::Omitted => {
1919 let mut output = String::new();
1920 for char in input.chars() {
1921 match char {
1922 char if char.is_control() || char == ' ' => {
1924 output.push_str(&char_reference(char));
1925 }
1926 '(' | ')' | '\\' | '<' | '>' | '&' => {
1927 output.push('\\');
1928 output.push(char);
1929 }
1930 _ => output.push(char),
1931 }
1932 }
1933 output
1934 }
1935 }
1936}
1937
1938fn write_title(title: &Title) -> String {
1939 let (open, close) = match title.kind {
1940 LinkTitleKind::DoubleQuote => ('"', '"'),
1941 LinkTitleKind::SingleQuote => ('\'', '\''),
1942 LinkTitleKind::Paren => ('(', ')'),
1943 };
1944 let mut output = String::new();
1945 output.push(open);
1946 output.push_str(&escape_title(&title.value, title.kind));
1947 output.push(close);
1948 output
1949}
1950
1951fn escape_title(input: &str, kind: LinkTitleKind) -> String {
1952 let mut output = String::new();
1953 for char in input.chars() {
1954 match char {
1955 char if char.is_control() => output.push_str(&char_reference(char)),
1956 '\\' | '&' => {
1957 output.push('\\');
1958 output.push(char);
1959 }
1960 '"' if kind == LinkTitleKind::DoubleQuote => output.push_str("\\\""),
1961 '\'' if kind == LinkTitleKind::SingleQuote => output.push_str("\\'"),
1962 '(' | ')' if kind == LinkTitleKind::Paren => {
1963 output.push('\\');
1964 output.push(char);
1965 }
1966 _ => output.push(char),
1967 }
1968 }
1969 output
1970}
1971
1972fn prefix_container_lines(input: &str, prefix: &str) -> String {
1976 prefix_lines_with(input, prefix, prefix.trim_end_matches([' ', '\t']))
1977}
1978
1979fn prefix_lines(input: &str, prefix: &str) -> String {
1981 prefix_lines_with(input, prefix, prefix)
1982}
1983
1984fn prefix_lines_with(input: &str, prefix: &str, empty_prefix: &str) -> String {
1987 if input.is_empty() {
1988 return String::new();
1989 }
1990 let bytes = input.as_bytes();
1991 let mut output = String::new();
1992 let mut line_start = 0;
1993 let mut cursor = 0;
1994 while cursor < input.len() {
1995 let eol_end = match bytes[cursor] {
1996 b'\n' => Some(cursor + 1),
1997 b'\r' if bytes.get(cursor + 1) == Some(&b'\n') => Some(cursor + 2),
1998 b'\r' => Some(cursor + 1),
1999 _ => None,
2000 };
2001 if let Some(end) = eol_end {
2002 output.push_str(if cursor == line_start {
2003 empty_prefix
2004 } else {
2005 prefix
2006 });
2007 output.push_str(&input[line_start..end]);
2008 cursor = end;
2009 line_start = cursor;
2010 } else {
2011 cursor += 1;
2012 }
2013 }
2014 if line_start < input.len() {
2015 output.push_str(prefix);
2016 output.push_str(&input[line_start..]);
2017 }
2018 output
2019}
2020
2021fn indent_after_first_line(input: &str, width: usize) -> String {
2022 indent_lines_of(input, width)
2023}
2024
2025fn indent_continuation(input: &str) -> String {
2026 indent_lines_of(input, 4)
2027}
2028
2029fn indent_lines_of(input: &str, width: usize) -> String {
2033 let mut output = String::with_capacity(input.len());
2034 let bytes = input.as_bytes();
2035 let mut line_start = 0;
2036 let mut index = 0;
2037 let push = |output: &mut String, line: &str, number: usize| {
2038 if number > 0 && !line.starts_with(['\n', '\r']) {
2039 output.extend(core::iter::repeat_n(' ', width));
2040 }
2041 output.push_str(line);
2042 };
2043 let mut number = 0;
2044 while index < bytes.len() {
2045 let end = match bytes[index] {
2046 b'\n' => Some(index + 1),
2047 b'\r' if bytes.get(index + 1) == Some(&b'\n') => Some(index + 2),
2048 b'\r' => Some(index + 1),
2049 _ => None,
2050 };
2051 match end {
2052 Some(end) => {
2053 push(&mut output, &input[line_start..end], number);
2054 number += 1;
2055 index = end;
2056 line_start = end;
2057 }
2058 None => index += 1,
2059 }
2060 }
2061 if line_start < input.len() {
2062 push(&mut output, &input[line_start..], number);
2063 }
2064 output
2065}
2066
2067fn trim_trailing_newline(input: &str) -> &str {
2068 input.trim_end_matches('\n').trim_end_matches('\r')
2069}
2070
2071fn ends_with_line_ending(input: &str) -> bool {
2072 input.ends_with('\n') || input.ends_with('\r')
2073}
2074
2075fn code_block_fence(value: &str, marker: FenceMarker, min_len: usize) -> (String, usize) {
2082 let char = match marker {
2083 FenceMarker::Backtick => '`',
2084 FenceMarker::Tilde => '~',
2085 };
2086 let closing_like = |line: &str, length: usize| {
2087 let indent = line.len() - line.trim_start_matches(' ').len();
2088 let rest = &line[indent..];
2089 let run = rest.len() - rest.trim_start_matches(char).len();
2090 (indent <= 3 && run >= length && rest[run..].trim_matches([' ', '\t']).is_empty())
2091 .then_some((indent, run))
2092 };
2093 let lines = || value.split(['\n', '\r']);
2094 let least_indent = lines()
2095 .filter_map(|line| closing_like(line, min_len))
2096 .map(|(indent, _)| indent)
2097 .min();
2098 match least_indent {
2099 None => (char.to_string().repeat(min_len), 0),
2100 Some(indent) if indent > 0 => (char.to_string().repeat(min_len), 4 - indent),
2101 Some(_) => {
2102 let mut length = min_len;
2103 for line in lines() {
2104 if let Some((_, run)) = closing_like(line, length) {
2105 length = run + 1;
2106 }
2107 }
2108 (char.to_string().repeat(length), 0)
2109 }
2110 }
2111}
2112
2113fn block_math_fence(input: &str) -> String {
2114 let mut length = 2;
2115 for line in trim_trailing_newline(input).lines() {
2116 let trimmed = line.trim();
2117 if trimmed.len() >= 2 && trimmed.chars().all(|char| char == '$') {
2118 length = length.max(trimmed.len() + 1);
2119 }
2120 }
2121 "$".repeat(length)
2122}
2123
2124fn directive_fence(inner: &str) -> String {
2125 let mut length = 3;
2126 for line in inner.lines() {
2127 if let Some(closing) = directive_closing_fence_len(line) {
2128 length = length.max(closing + 1);
2129 }
2130 }
2131 ":".repeat(length)
2132}
2133
2134fn directive_closing_fence_len(line: &str) -> Option<usize> {
2135 let trimmed = trim_up_to_three_indent_columns(line)?;
2136 let length = trimmed
2137 .as_bytes()
2138 .iter()
2139 .take_while(|byte| **byte == b':')
2140 .count();
2141 if length >= 3 && trimmed[length..].trim().is_empty() {
2142 Some(length)
2143 } else {
2144 None
2145 }
2146}
2147
2148fn trim_up_to_three_indent_columns(input: &str) -> Option<&str> {
2149 let mut columns = 0usize;
2150 let mut bytes = 0usize;
2151 for byte in input.as_bytes() {
2152 match *byte {
2153 b' ' => columns += 1,
2154 b'\t' => columns += 4 - (columns % 4),
2155 _ => break,
2156 }
2157 bytes += 1;
2158 }
2159 (columns <= 3).then_some(&input[bytes..])
2160}
2161
2162#[cfg(test)]
2163mod tests {
2164 use super::*;
2165
2166 #[test]
2168 fn the_read_back_check_names_each_block_that_reads_differently() {
2169 let blocks = crate::parse("> x\n\n# y\n\nz").document.children;
2170 assert_eq!(misread(&blocks, "> x\n\n# y\n\nz", &[0, 2]), [0usize; 0]);
2171 assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[0, 2]), [0]);
2173 assert_eq!(misread(&blocks, "> w\n\n# y\n\nz", &[2]), [0usize; 0]);
2174 assert_eq!(misread(&blocks, "> x\nz\n\n# y", &[0, 2]), [0, 2]);
2176 }
2177}