Skip to main content

rushdown_meta/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4extern crate alloc;
5
6use alloc::boxed::Box;
7use alloc::format;
8use alloc::rc::Rc;
9use alloc::string::String;
10use alloc::string::ToString;
11use alloc::vec::Vec;
12use core::cell::RefCell;
13use core::result::Result as CoreResult;
14use rushdown::ast::HtmlBlock;
15use rushdown::ast::Table;
16use rushdown::ast::TableBody;
17use rushdown::ast::TableCell;
18use rushdown::ast::TableHeader;
19use rushdown::ast::TableRow;
20use rushdown::parser::ParserOptions;
21
22use rushdown::{
23    as_kind_data_mut, as_type_data, as_type_data_mut,
24    ast::{Arena, CodeBlock, CodeBlockKind, Meta, NodeRef, Text},
25    context::{ContextKey, ContextKeyRegistry, NodeRefValue},
26    parser::{
27        self, AnyAstTransformer, AnyBlockParser, AstTransformer, BlockParser, NoParserOptions,
28        Parser, ParserExtension, ParserExtensionFn, PRIORITY_SETTEXT_HEADING,
29    },
30    text::{self, Reader},
31    util::StringMap,
32};
33
34// Parser {{{
35
36const META_NODE: &str = "rushdown-meta-n";
37
38/// Options for the meta parser.
39#[derive(Debug, Clone, Default)]
40pub struct MetaParserOptions {
41    /// Convert the meta data to a table node.
42    pub table: bool,
43}
44
45impl ParserOptions for MetaParserOptions {}
46
47#[derive(Debug)]
48struct MetaParser {
49    meta_node: ContextKey<NodeRefValue>,
50}
51
52impl MetaParser {
53    /// Returns a new [`MetaParser`].
54    pub fn new(reg: Rc<RefCell<ContextKeyRegistry>>) -> Self {
55        let meta_node = reg.borrow_mut().get_or_create::<NodeRefValue>(META_NODE);
56        Self { meta_node }
57    }
58}
59
60impl BlockParser for MetaParser {
61    fn trigger(&self) -> &[u8] {
62        b"-"
63    }
64
65    fn open(
66        &self,
67        arena: &mut Arena,
68        _parent_ref: NodeRef,
69        reader: &mut text::BasicReader,
70        ctx: &mut parser::Context,
71    ) -> Option<(NodeRef, parser::State)> {
72        let (line, _) = reader.position();
73        if line != 0 {
74            return None;
75        }
76        let (line, _) = reader.peek_line_bytes()?;
77        if !line.starts_with(b"---") {
78            return None;
79        }
80        reader.advance_to_eol();
81        let node_ref = arena.new_node(CodeBlock::new(CodeBlockKind::Fenced, None));
82        ctx.insert(self.meta_node, node_ref);
83        Some((node_ref, parser::State::NO_CHILDREN))
84    }
85
86    fn cont(
87        &self,
88        arena: &mut Arena,
89        node_ref: NodeRef,
90        reader: &mut text::BasicReader,
91        _ctx: &mut parser::Context,
92    ) -> Option<parser::State> {
93        let (line, seg) = reader.peek_line_bytes()?;
94        if line.starts_with(b"---") {
95            reader.advance_to_eol();
96            return None;
97        }
98        as_type_data_mut!(arena, node_ref, Block).append_source_line(seg);
99        Some(parser::State::NO_CHILDREN)
100    }
101
102    fn close(
103        &self,
104        _arena: &mut Arena,
105        _node_ref: NodeRef,
106        _reader: &mut text::BasicReader,
107        _ctx: &mut parser::Context,
108    ) {
109    }
110
111    fn can_interrupt_paragraph(&self) -> bool {
112        true
113    }
114}
115
116impl From<MetaParser> for AnyBlockParser {
117    fn from(p: MetaParser) -> Self {
118        AnyBlockParser::Extension(Box::new(p))
119    }
120}
121
122#[derive(Debug)]
123struct MetaAstTransformer {
124    meta_node: ContextKey<NodeRefValue>,
125    options: MetaParserOptions,
126}
127
128impl MetaAstTransformer {
129    /// Returns a new [`MetaAstTransformer`].
130    pub fn new(reg: Rc<RefCell<ContextKeyRegistry>>, options: MetaParserOptions) -> Self {
131        let meta_node = reg.borrow_mut().get_or_create::<NodeRefValue>(META_NODE);
132        Self { meta_node, options }
133    }
134}
135
136impl AstTransformer for MetaAstTransformer {
137    fn transform(
138        &self,
139        arena: &mut Arena,
140        doc_ref: NodeRef,
141        reader: &mut text::BasicReader,
142        ctx: &mut parser::Context,
143    ) {
144        let Some(meta_ref) = ctx.get(self.meta_node) else {
145            return;
146        };
147        let mut yaml_data = String::new();
148
149        for line in as_type_data!(arena, *meta_ref, Block).source() {
150            yaml_data.push_str(&line.str(reader.source()));
151        }
152        meta_ref.delete(arena);
153        match parse_yaml(&yaml_data) {
154            Ok(meta) => {
155                if let Meta::Mapping(map) = meta {
156                    let m = map.clone();
157                    for (key, value) in map {
158                        as_kind_data_mut!(arena, doc_ref, Document)
159                            .metadata_mut()
160                            .insert(key, value);
161                    }
162                    if self.options.table {
163                        let table_ref = arena.new_node(Table::new());
164                        let header_ref = arena.new_node(TableHeader::new());
165                        let header_row_ref = arena.new_node(TableRow::new());
166                        for (key, _) in m.iter() {
167                            let cell_ref = arena.new_node(TableCell::default());
168                            let text_ref = arena.new_node(Text::new(key.clone()));
169                            cell_ref.append_child(arena, text_ref);
170                            header_row_ref.append_child(arena, cell_ref);
171                        }
172                        header_ref.append_child(arena, header_row_ref);
173                        table_ref.append_child(arena, header_ref);
174
175                        let body_ref = arena.new_node(TableBody::new());
176                        table_ref.append_child(arena, body_ref);
177                        let body_row_ref = arena.new_node(TableRow::new());
178                        for (_, value) in m {
179                            let cell_ref = arena.new_node(TableCell::default());
180                            let text_ref = arena.new_node(Text::new(format_meta(&value)));
181                            cell_ref.append_child(arena, text_ref);
182                            body_row_ref.append_child(arena, cell_ref);
183                        }
184                        body_ref.append_child(arena, body_row_ref);
185                        if let Some(first) = arena[doc_ref].first_child() {
186                            doc_ref.insert_before(arena, first, table_ref);
187                        } else {
188                            doc_ref.append_child(arena, table_ref);
189                        }
190                    }
191                } else {
192                    let mut error_data = HtmlBlock::new(rushdown::ast::HtmlBlockKind::Kind2);
193                    error_data.set_value("<!-- YAML metadata must be a mapping -->\n".to_string());
194                    let error_ref = arena.new_node(error_data);
195                    if let Some(first) = arena[doc_ref].first_child() {
196                        doc_ref.insert_before(arena, first, error_ref);
197                    } else {
198                        doc_ref.append_child(arena, error_ref);
199                    }
200                }
201            }
202            Err(e) => {
203                let mut error_data = HtmlBlock::new(rushdown::ast::HtmlBlockKind::Kind2);
204                error_data.set_value(
205                    format!("<!-- Error parsing YAML metadata: {} -->\n", e).to_string(),
206                );
207                let error_ref = arena.new_node(error_data);
208                if let Some(first) = arena[doc_ref].first_child() {
209                    doc_ref.insert_before(arena, first, error_ref);
210                } else {
211                    doc_ref.append_child(arena, error_ref);
212                }
213            }
214        }
215    }
216}
217
218impl From<MetaAstTransformer> for AnyAstTransformer {
219    fn from(t: MetaAstTransformer) -> Self {
220        AnyAstTransformer::Extension(Box::new(t))
221    }
222}
223
224fn format_meta(meta: &Meta) -> String {
225    match meta {
226        Meta::Null => "null".to_string(),
227        Meta::Bool(b) => b.to_string(),
228        Meta::Int(i) => i.to_string(),
229        Meta::Float(f) => f.to_string(),
230        Meta::String(s) => s.clone(),
231        Meta::Sequence(seq) => {
232            let items: Vec<String> = seq.iter().map(format_meta).collect();
233            format!("[{}]", items.join(", "))
234        }
235        Meta::Mapping(map) => {
236            let items: Vec<String> = map
237                .iter()
238                .map(|(k, v)| format!("{}: {}", k, format_meta(v)))
239                .collect();
240            format!("{{{}}}", items.join(", "))
241        }
242    }
243}
244
245// }}}
246
247// Extension {{{
248
249/// Returns a parser extension that parses metas.
250pub fn meta_parser_extension(options: impl Into<MetaParserOptions>) -> impl ParserExtension {
251    ParserExtensionFn::new(|p: &mut Parser| {
252        p.add_block_parser(
253            MetaParser::new,
254            NoParserOptions,
255            PRIORITY_SETTEXT_HEADING - 100,
256        );
257        p.add_ast_transformer(MetaAstTransformer::new, options.into(), 0);
258    })
259}
260
261// }}}
262
263// YAML {{{
264
265fn to_meta<R: yaml_peg::repr::Repr>(node: &yaml_peg::Node<R>) -> Meta {
266    match node.yaml() {
267        yaml_peg::Yaml::Null => Meta::Null,
268        yaml_peg::Yaml::Bool(b) => Meta::Bool(*b),
269        yaml_peg::Yaml::Int(s) => Meta::Int(s.parse().unwrap_or(0)),
270        yaml_peg::Yaml::Float(s) => Meta::Float(s.parse().unwrap_or(0.0)),
271        yaml_peg::Yaml::Str(s) => Meta::String(s.clone()),
272        yaml_peg::Yaml::Seq(seq) => Meta::Sequence(seq.iter().map(|n| to_meta(n)).collect()),
273        yaml_peg::Yaml::Map(map) => {
274            let mut result = StringMap::with_capacity(map.len());
275            for (k, v) in map.iter() {
276                if let yaml_peg::Yaml::Str(key) = k.yaml() {
277                    result.insert(key.clone(), to_meta(v));
278                }
279            }
280            Meta::Mapping(result)
281        }
282        yaml_peg::Yaml::Alias(_) => Meta::Null, // Aliases are not supported in this
283                                                // implementation
284    }
285}
286
287fn parse_yaml(input: &str) -> CoreResult<Meta, String> {
288    let doc = yaml_peg::parser::parse::<yaml_peg::repr::RcRepr>(input)
289        .map_err(|e| format!("YAML parsing error: {:?}", e))?;
290    if !doc.is_empty() {
291        Ok(to_meta(&doc[0]))
292    } else {
293        Err("YAML document is empty".to_string())
294    }
295}
296
297// }}} YAML