base_markdown/markdown/mod.rs
1// Released under the MIT License.
2// Copyright, 2024, by Bnchi.
3// Copyright, 2024, by Titus Wormer.
4// Copyright, 2026, by Samuel Williams.
5
6//! API.
7//!
8//! JS equivalent: https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/index.js.
9
10// Formatting rules are adapted from `mdast-util-to-markdown`; keep the parser's stricter lints on the surrounding API.
11#![allow(clippy::pedantic)]
12
13pub use self::configure::{IndentOptions, LineWrapping, Options};
14use self::state::{Info, State};
15use crate::{message::Message, Document, NodeId};
16use alloc::string::String;
17
18extern crate alloc;
19mod configure;
20mod construct_name;
21mod escape;
22mod handle;
23mod state;
24mod util;
25
26/// Serialize an arena document to Markdown.
27///
28/// The output always uses LF line endings: CRLF and lone CR line endings in node values, such as code, HTML, and text, are written as LF.
29///
30/// # Errors
31/// Returns invalid formatting options, unsupported syntax metadata, or unsupported content shapes.
32pub fn to_markdown<S: AsRef<str>>(document: &Document<S>) -> Result<String, Message> {
33 to_markdown_with_options(document, &Options::default())
34}
35
36/// Serialize an arena document with formatting options.
37///
38/// # Errors
39/// Returns invalid formatting options, unsupported syntax metadata, or unsupported content shapes.
40pub fn to_markdown_with_options<S: AsRef<str>>(
41 document: &Document<S>,
42 options: &Options,
43) -> Result<String, Message> {
44 to_markdown_node(document, document.root(), options)
45}
46
47/// Serialize a document subtree with formatting options.
48///
49/// The output always uses LF line endings. Non-empty block subtrees end with a line ending; inline (phrasing) subtrees, such as text, emphasis, or a link, are returned without an added line ending.
50///
51/// # Errors
52/// Returns an invalid node index, invalid formatting options, unsupported syntax metadata, or unsupported content shapes.
53pub fn to_markdown_node<S: AsRef<str>>(
54 document: &Document<S>,
55 node: NodeId,
56 options: &Options,
57) -> Result<String, Message> {
58 if document.node(node).is_none() {
59 return Err(state::invalid_node());
60 }
61 let kind = &document.node(node).unwrap().kind;
62 let inline = state::is_phrasing(kind)
63 || matches!(kind, crate::stream::EventKind::Opaque { block: false, .. });
64 let mut state = State::new(options);
65 state.serialization_root = Some(node);
66 let mut result = state.handle(document, node, &Info::new("\n", "\n"))?;
67 // Markdown treats CR, LF, and CRLF alike, so a single line ending keeps output stable across platforms.
68 if result.contains('\r') {
69 result = result.replace("\r\n", "\n").replace('\r', "\n");
70 }
71 // Only block output is line-terminated, matching cmark's renderer.
72 if !inline && !result.is_empty() && !result.ends_with('\n') {
73 result.push('\n');
74 }
75 Ok(result)
76}