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, TextQualifier},
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
169                                .new_node(Text::with_qualifiers(key.clone(), TextQualifier::RAW));
170                            cell_ref.append_child(arena, text_ref);
171                            header_row_ref.append_child(arena, cell_ref);
172                        }
173                        header_ref.append_child(arena, header_row_ref);
174                        table_ref.append_child(arena, header_ref);
175
176                        let body_ref = arena.new_node(TableBody::new());
177                        table_ref.append_child(arena, body_ref);
178                        let body_row_ref = arena.new_node(TableRow::new());
179                        for (_, value) in m {
180                            let cell_ref = arena.new_node(TableCell::default());
181                            let text_ref = arena.new_node(Text::new(format_meta(&value)));
182                            cell_ref.append_child(arena, text_ref);
183                            body_row_ref.append_child(arena, cell_ref);
184                        }
185                        body_ref.append_child(arena, body_row_ref);
186                        if let Some(first) = arena[doc_ref].first_child() {
187                            doc_ref.insert_before(arena, first, table_ref);
188                        } else {
189                            doc_ref.append_child(arena, table_ref);
190                        }
191                    }
192                } else {
193                    let mut error_data = HtmlBlock::new(rushdown::ast::HtmlBlockKind::Kind2);
194                    error_data.set_value("<!-- YAML metadata must be a mapping -->\n".to_string());
195                    let error_ref = arena.new_node(error_data);
196                    if let Some(first) = arena[doc_ref].first_child() {
197                        doc_ref.insert_before(arena, first, error_ref);
198                    } else {
199                        doc_ref.append_child(arena, error_ref);
200                    }
201                }
202            }
203            Err(e) => {
204                let mut error_data = HtmlBlock::new(rushdown::ast::HtmlBlockKind::Kind2);
205                error_data.set_value(
206                    format!("<!-- Error parsing YAML metadata: {} -->\n", e).to_string(),
207                );
208                let error_ref = arena.new_node(error_data);
209                if let Some(first) = arena[doc_ref].first_child() {
210                    doc_ref.insert_before(arena, first, error_ref);
211                } else {
212                    doc_ref.append_child(arena, error_ref);
213                }
214            }
215        }
216    }
217}
218
219impl From<MetaAstTransformer> for AnyAstTransformer {
220    fn from(t: MetaAstTransformer) -> Self {
221        AnyAstTransformer::Extension(Box::new(t))
222    }
223}
224
225fn format_meta(meta: &Meta) -> String {
226    match meta {
227        Meta::Null => "null".to_string(),
228        Meta::Bool(b) => b.to_string(),
229        Meta::Int(i) => i.to_string(),
230        Meta::Float(f) => f.to_string(),
231        Meta::String(s) => s.clone(),
232        Meta::Sequence(seq) => {
233            let items: Vec<String> = seq.iter().map(format_meta).collect();
234            format!("[{}]", items.join(", "))
235        }
236        Meta::Mapping(map) => {
237            let items: Vec<String> = map
238                .iter()
239                .map(|(k, v)| format!("{}: {}", k, format_meta(v)))
240                .collect();
241            format!("{{{}}}", items.join(", "))
242        }
243    }
244}
245
246// }}}
247
248// Extension {{{
249
250/// Returns a parser extension that parses metas.
251pub fn meta_parser_extension(options: impl Into<MetaParserOptions>) -> impl ParserExtension {
252    ParserExtensionFn::new(|p: &mut Parser| {
253        p.add_block_parser(
254            MetaParser::new,
255            NoParserOptions,
256            PRIORITY_SETTEXT_HEADING - 100,
257        );
258        p.add_ast_transformer(MetaAstTransformer::new, options.into(), 0);
259    })
260}
261
262// }}}
263
264// YAML {{{
265
266fn to_meta<R: yaml_peg::repr::Repr>(node: &yaml_peg::Node<R>) -> Meta {
267    match node.yaml() {
268        yaml_peg::Yaml::Null => Meta::Null,
269        yaml_peg::Yaml::Bool(b) => Meta::Bool(*b),
270        yaml_peg::Yaml::Int(s) => Meta::Int(s.parse().unwrap_or(0)),
271        yaml_peg::Yaml::Float(s) => Meta::Float(s.parse().unwrap_or(0.0)),
272        yaml_peg::Yaml::Str(s) => Meta::String(s.clone()),
273        yaml_peg::Yaml::Seq(seq) => Meta::Sequence(seq.iter().map(|n| to_meta(n)).collect()),
274        yaml_peg::Yaml::Map(map) => {
275            let mut result = StringMap::with_capacity(map.len());
276            for (k, v) in map.iter() {
277                if let yaml_peg::Yaml::Str(key) = k.yaml() {
278                    result.insert(key.clone(), to_meta(v));
279                }
280            }
281            Meta::Mapping(result)
282        }
283        yaml_peg::Yaml::Alias(_) => Meta::Null, // Aliases are not supported in this
284                                                // implementation
285    }
286}
287
288fn parse_yaml(input: &str) -> CoreResult<Meta, String> {
289    let doc = yaml_peg::parser::parse::<yaml_peg::repr::RcRepr>(input)
290        .map_err(|e| format!("YAML parsing error: {:?}", e))?;
291    if !doc.is_empty() {
292        Ok(to_meta(&doc[0]))
293    } else {
294        Err("YAML document is empty".to_string())
295    }
296}
297
298// }}} YAML