Skip to main content

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}