yamd/lib.rs
1//! YAMD - Yet Another Markdown Document (flavour)
2//!
3//! Simplified version of [CommonMark](https://spec.commonmark.org/).
4//!
5//! For formatting check [`YAMD`](nodes::Yamd) struct documentation.
6//!
7//! # Quick start
8//!
9//! ```rust
10//! use yamd::deserialize;
11//!
12//! let input = "# Hello\n\nA paragraph with **bold** text.";
13//! let yamd = deserialize(input);
14//!
15//! // Access the AST
16//! assert_eq!(yamd.body.len(), 2);
17//!
18//! // Round-trip back to markdown
19//! assert_eq!(yamd.to_string(), input);
20//! ```
21//!
22//! # Two APIs
23//!
24//! - [`deserialize`] returns a nested [`Yamd`](nodes::Yamd) document — a tree of typed nodes,
25//! suitable for walking, pattern-matching, or round-tripping back to markdown via
26//! [`Display`](std::fmt::Display). The AST makes invalid nestings unrepresentable, and
27//! `deserialize` is fuzz-tested for panic-freedom and property-tested for round-trip fidelity.
28//! - [`parse`] returns a flat `Vec<`[`Op`](op::Op)`>` of Start/End/Value events, where
29//! [`Content`](op::Content) borrows from the source when possible. Reach for it when you want
30//! streaming rendering or zero-copy text processing without materializing the full tree.
31//! [`to_yamd`] promotes an event stream to the tree form. Fuzz-tested for panic-freedom
32//! (transitively, via `deserialize`); the AST's type-level invariants and round-trip property
33//! do not apply at this layer.
34//!
35//! # Reasoning
36//!
37//! YAMD exchanges CommonMark's context-dependent rules for a uniform set: every node is treated
38//! the same, and escaping is resolved at the lexer. The goal is a parser that's easier to reason
39//! about locally, with fewer special cases to remember.
40//!
41//! Rendering is out of scope; [`Yamd`](nodes::Yamd) is an AST you walk and render however you
42//! like. With the `serde` feature enabled, the AST is also serde-serializable.
43//!
44//! # Difference from CommonMark
45//!
46//! YAMD reuses most of CommonMark's syntax but diverges in a few places.
47//!
48//! ## Escaping
49//!
50//! Escaping is handled at the [lexer] level: any character following `\` is treated as a
51//! [literal](lexer::TokenKind::Literal).
52//!
53//! Example:
54//!
55//! | YAMD | HTML equivalent |
56//! |-----------|-----------------|
57//! | `\**foo**`|`<p>**foo**</p>` |
58//!
59//! ## Precedence
60//!
61//! [CommonMark](https://spec.commonmark.org/0.31.2/#precedence) distinguishes container blocks from
62//! leaf blocks and gives container-block markers higher precedence. YAMD does not distinguish block
63//! types — every node is treated the same, so there are no precedence rules to remember.
64//!
65//! Example:
66//!
67//! | YAMD | HTML equivalent |
68//! |-----------------------|-----------------------------------------------|
69//! | ``- `one\n- two` `` | `<ol><li><code>one\n- two</code></li></ol>` |
70//!
71//!
72//! To get two separate [ListItem](nodes::ListItem)s, escape the backticks:
73//!
74//! | YAMD | HTML equivalent |
75//! |---------------------------|-------------------------------------------|
76//! | ``- \`one\n- two\` `` | ``<ol><li>`one</li><li>two`</li><ol>`` |
77//!
78//! The reasoning: issues like this should be caught by tooling such as linters or language servers
79//! — that tooling doesn't exist yet.
80//!
81//! ## Nodes
82//!
83//! See [nodes] for the full list of supported nodes and their formatting. Start with [YAMD](nodes::Yamd).
84//!
85//! # MSRV
86//!
87//! YAMD minimal supported Rust version is 1.87.
88
89#[deny(missing_docs, rustdoc::broken_intra_doc_links)]
90pub mod lexer;
91pub mod nodes;
92pub mod op;
93
94#[doc(inline)]
95pub use nodes::Yamd;
96pub use op::parse;
97pub use op::to_yamd;
98
99/// Deserialize a string into a Yamd struct
100/// # Example
101/// ```
102/// use yamd::deserialize;
103/// let input = "# header";
104/// let yamd = deserialize(input);
105/// ```
106pub fn deserialize(input: &str) -> Yamd {
107 let ops = op::parse(input);
108 op::to_yamd(&ops, input)
109}
110
111#[cfg(test)]
112mod tests {
113 use pretty_assertions::assert_eq;
114
115 use crate::{
116 deserialize,
117 nodes::{Anchor, Heading, Paragraph, Yamd},
118 };
119
120 #[test]
121 fn test_deserialize() {
122 let input = "# header";
123 let expected = Yamd::new(
124 None,
125 vec![Heading::new(1, vec![String::from("header").into()]).into()],
126 );
127 let actual = deserialize(input);
128 assert_eq!(expected, actual);
129 }
130
131 #[test]
132 fn deserialize_text_containing_utf8() {
133 let input = "## 🤔\n\n[link 😉](url)";
134 let expected = Yamd::new(
135 None,
136 vec![
137 Heading::new(2, vec![String::from("🤔").into()]).into(),
138 Paragraph::new(vec![Anchor::new("link 😉", "url").into()]).into(),
139 ],
140 );
141 let actual = deserialize(input);
142 assert_eq!(expected, actual);
143 }
144}