Skip to main content

sim_codec_doc/
markup.rs

1//! Shared semantic markup document IR and `Expr` projection.
2
3use std::collections::BTreeMap;
4
5use sim_kernel::{Error, Expr, Result};
6use sim_value::build::{entry, list, map, sym, text, uint};
7
8mod decode;
9mod expr;
10mod model;
11
12pub use decode::decode_markup_doc;
13use expr::*;
14pub use model::{
15    BackendId, Inline, MarkupBlock, MarkupDoc, MathSource, SourceDoc, Span, SpanState,
16};
17
18impl MarkupDoc {
19    /// Project this markup document into ordinary SIM data.
20    pub fn as_expr(&self) -> Expr {
21        let mut entries = vec![
22            entry("kind", sym("markup-doc")),
23            entry(
24                "blocks",
25                list(self.blocks.iter().map(MarkupBlock::as_expr).collect()),
26            ),
27            entry("attrs", attrs_expr(&self.attrs)),
28        ];
29        if let Some(title) = &self.title {
30            entries.push(entry("title", text(title)));
31        }
32        if let Some(source) = &self.source {
33            entries.push(entry("source", source.as_expr()));
34        }
35        Expr::Map(entries)
36    }
37
38    /// Reconstruct a markup document from ordinary SIM data.
39    pub fn from_expr(expr: &Expr) -> Result<Self> {
40        let entries = map_entries(expr, "markup document")?;
41        require_kind(entries, "markup-doc", "markup document")?;
42        let title = optional_string(entries, "title")?.map(str::to_owned);
43        let blocks = required_list(entries, "blocks", "markup document")?
44            .iter()
45            .map(MarkupBlock::from_expr)
46            .collect::<Result<Vec<_>>>()?;
47        let attrs = match field(entries, "attrs") {
48            Some(Expr::Map(attrs)) => attrs_from_entries(attrs.as_slice())?,
49            Some(_) => return Err(Error::Eval("markup attrs must be a map".to_owned())),
50            None => BTreeMap::new(),
51        };
52        let source = match field(entries, "source") {
53            Some(expr) => Some(SourceDoc::from_expr(expr)?),
54            None => None,
55        };
56        Ok(Self {
57            title,
58            blocks,
59            attrs,
60            source,
61        })
62    }
63
64    /// Render this document to a deterministic source string.
65    ///
66    /// When verbatim source is present, it wins. Documents constructed directly
67    /// from semantic blocks use a small Markdown-like writer.
68    pub fn to_source_text(&self) -> String {
69        if let Some(source) = &self.source {
70            return source.text.clone();
71        }
72        let mut out = String::new();
73        for (index, block) in self.blocks.iter().enumerate() {
74            if index > 0 {
75                out.push_str("\n\n");
76            }
77            block.write_source(&mut out);
78        }
79        out
80    }
81}
82
83impl SourceDoc {
84    fn as_expr(&self) -> Expr {
85        map(vec![
86            ("backend", text(&self.backend.0)),
87            ("text", text(&self.text)),
88        ])
89    }
90
91    fn from_expr(expr: &Expr) -> Result<Self> {
92        let entries = map_entries(expr, "markup source")?;
93        Ok(Self {
94            backend: BackendId(required_string(entries, "backend", "markup source")?.to_owned()),
95            text: required_string(entries, "text", "markup source")?.to_owned(),
96        })
97    }
98}
99
100impl Span {
101    fn as_expr(&self) -> Expr {
102        map(vec![
103            ("start", uint(self.start as u64)),
104            ("end", uint(self.end as u64)),
105            ("state", sym(self.state.as_str())),
106        ])
107    }
108
109    fn from_expr(expr: &Expr) -> Result<Self> {
110        let entries = map_entries(expr, "span")?;
111        Ok(Self {
112            start: required_usize(entries, "start", "span")?,
113            end: required_usize(entries, "end", "span")?,
114            state: match field(entries, "state") {
115                Some(value) => SpanState::from_expr(value)?,
116                None => SpanState::Preserved,
117            },
118        })
119    }
120}
121
122impl SpanState {
123    fn as_str(&self) -> &'static str {
124        match self {
125            Self::Preserved => "preserved",
126            Self::Dirty => "dirty",
127        }
128    }
129
130    fn from_expr(expr: &Expr) -> Result<Self> {
131        match expr {
132            Expr::Symbol(symbol) if symbol.namespace.is_none() => match symbol.name.as_ref() {
133                "preserved" => Ok(Self::Preserved),
134                "dirty" => Ok(Self::Dirty),
135                other => Err(Error::Eval(format!("unknown span state {other}"))),
136            },
137            Expr::String(value) => match value.as_str() {
138                "preserved" => Ok(Self::Preserved),
139                "dirty" => Ok(Self::Dirty),
140                other => Err(Error::Eval(format!("unknown span state {other}"))),
141            },
142            _ => Err(Error::Eval("span state must be a symbol".to_owned())),
143        }
144    }
145}
146
147impl MathSource {
148    fn as_expr(&self) -> Expr {
149        map(vec![
150            ("notation", text(&self.notation)),
151            ("text", text(&self.text)),
152        ])
153    }
154
155    fn from_expr(expr: &Expr) -> Result<Self> {
156        let entries = map_entries(expr, "math source")?;
157        Ok(Self {
158            notation: required_string(entries, "notation", "math source")?.to_owned(),
159            text: required_string(entries, "text", "math source")?.to_owned(),
160        })
161    }
162}
163
164impl MarkupBlock {
165    /// Project this block into ordinary SIM data.
166    pub fn as_expr(&self) -> Expr {
167        match self {
168            Self::Heading {
169                level,
170                text: heading,
171                id,
172                span,
173            } => {
174                let mut entries = vec![
175                    entry("kind", sym("heading")),
176                    entry("level", uint(u64::from(*level))),
177                    entry("text", inline_list(heading)),
178                ];
179                push_optional_string(&mut entries, "id", id);
180                push_optional_span(&mut entries, span);
181                Expr::Map(entries)
182            }
183            Self::Paragraph { content, span } => {
184                let mut entries = vec![
185                    entry("kind", sym("paragraph")),
186                    entry("content", inline_list(content)),
187                ];
188                push_optional_span(&mut entries, span);
189                Expr::Map(entries)
190            }
191            Self::CodeBlock { lang, code, span } => {
192                let mut entries = vec![entry("kind", sym("code-block")), entry("code", text(code))];
193                push_optional_string(&mut entries, "lang", lang);
194                push_optional_span(&mut entries, span);
195                Expr::Map(entries)
196            }
197            Self::MathBlock { source, span } => {
198                let mut entries = vec![
199                    entry("kind", sym("math-block")),
200                    entry("source", source.as_expr()),
201                ];
202                push_optional_span(&mut entries, span);
203                Expr::Map(entries)
204            }
205            Self::Quote { blocks, span } => {
206                let mut entries = vec![
207                    entry("kind", sym("quote")),
208                    entry("blocks", block_list(blocks)),
209                ];
210                push_optional_span(&mut entries, span);
211                Expr::Map(entries)
212            }
213            Self::List {
214                ordered,
215                items,
216                span,
217            } => {
218                let mut entries = vec![
219                    entry("kind", sym("list")),
220                    entry("ordered", Expr::Bool(*ordered)),
221                    entry(
222                        "items",
223                        list(items.iter().map(|item| block_list(item)).collect()),
224                    ),
225                ];
226                push_optional_span(&mut entries, span);
227                Expr::Map(entries)
228            }
229            Self::Table { header, rows, span } => {
230                let mut entries = vec![
231                    entry("kind", sym("table")),
232                    entry(
233                        "header",
234                        list(header.iter().map(|cell| inline_list(cell)).collect()),
235                    ),
236                    entry(
237                        "rows",
238                        list(
239                            rows.iter()
240                                .map(|row| list(row.iter().map(|cell| inline_list(cell)).collect()))
241                                .collect(),
242                        ),
243                    ),
244                ];
245                push_optional_span(&mut entries, span);
246                Expr::Map(entries)
247            }
248            Self::Figure { src, caption, span } => {
249                let mut entries = vec![
250                    entry("kind", sym("figure")),
251                    entry("src", text(src)),
252                    entry("caption", inline_list(caption)),
253                ];
254                push_optional_span(&mut entries, span);
255                Expr::Map(entries)
256            }
257            Self::Raw {
258                backend,
259                text: raw,
260                span,
261            } => {
262                let mut entries = vec![
263                    entry("kind", sym("raw")),
264                    entry("backend", text(&backend.0)),
265                    entry("text", text(raw)),
266                ];
267                push_optional_span(&mut entries, span);
268                Expr::Map(entries)
269            }
270        }
271    }
272
273    /// Reconstruct a block from ordinary SIM data.
274    pub fn from_expr(expr: &Expr) -> Result<Self> {
275        let entries = map_entries(expr, "markup block")?;
276        match required_kind(entries, "markup block")?.as_str() {
277            "heading" => Ok(Self::Heading {
278                level: required_u8(entries, "level", "heading")?,
279                text: inline_vec(required_list(entries, "text", "heading")?)?,
280                id: optional_string(entries, "id")?.map(str::to_owned),
281                span: optional_span(entries)?,
282            }),
283            "paragraph" => Ok(Self::Paragraph {
284                content: inline_vec(required_list(entries, "content", "paragraph")?)?,
285                span: optional_span(entries)?,
286            }),
287            "code-block" => Ok(Self::CodeBlock {
288                lang: optional_string(entries, "lang")?.map(str::to_owned),
289                code: required_string(entries, "code", "code block")?.to_owned(),
290                span: optional_span(entries)?,
291            }),
292            "math-block" => Ok(Self::MathBlock {
293                source: MathSource::from_expr(required_field(entries, "source", "math block")?)?,
294                span: optional_span(entries)?,
295            }),
296            "quote" => Ok(Self::Quote {
297                blocks: block_vec(required_list(entries, "blocks", "quote")?)?,
298                span: optional_span(entries)?,
299            }),
300            "list" => {
301                let items = required_list(entries, "items", "list")?
302                    .iter()
303                    .map(|item| block_vec(as_list(item, "list item")?))
304                    .collect::<Result<Vec<_>>>()?;
305                Ok(Self::List {
306                    ordered: required_bool(entries, "ordered", "list")?,
307                    items,
308                    span: optional_span(entries)?,
309                })
310            }
311            "table" => {
312                let header = required_list(entries, "header", "table")?
313                    .iter()
314                    .map(|cell| inline_vec(as_list(cell, "table header cell")?))
315                    .collect::<Result<Vec<_>>>()?;
316                let rows = required_list(entries, "rows", "table")?
317                    .iter()
318                    .map(|row| {
319                        as_list(row, "table row")?
320                            .iter()
321                            .map(|cell| inline_vec(as_list(cell, "table cell")?))
322                            .collect::<Result<Vec<_>>>()
323                    })
324                    .collect::<Result<Vec<_>>>()?;
325                Ok(Self::Table {
326                    header,
327                    rows,
328                    span: optional_span(entries)?,
329                })
330            }
331            "figure" => Ok(Self::Figure {
332                src: required_string(entries, "src", "figure")?.to_owned(),
333                caption: inline_vec(required_list(entries, "caption", "figure")?)?,
334                span: optional_span(entries)?,
335            }),
336            "raw" => Ok(Self::Raw {
337                backend: BackendId(required_string(entries, "backend", "raw block")?.to_owned()),
338                text: required_string(entries, "text", "raw block")?.to_owned(),
339                span: optional_span(entries)?,
340            }),
341            other => Err(Error::Eval(format!("unknown markup block kind {other}"))),
342        }
343    }
344
345    fn write_source(&self, out: &mut String) {
346        match self {
347            Self::Heading { level, text, .. } => {
348                out.push_str(&"#".repeat(usize::from(*level).max(1)));
349                out.push(' ');
350                write_inlines(out, text);
351            }
352            Self::Paragraph { content, .. } => write_inlines(out, content),
353            Self::CodeBlock { lang, code, .. } => {
354                out.push_str("```");
355                if let Some(lang) = lang {
356                    out.push_str(lang);
357                }
358                out.push('\n');
359                out.push_str(code);
360                if !code.ends_with('\n') {
361                    out.push('\n');
362                }
363                out.push_str("```");
364            }
365            Self::MathBlock { source, .. } => {
366                out.push_str("$$\n");
367                out.push_str(&source.text);
368                out.push_str("\n$$");
369            }
370            Self::Quote { blocks, .. } => {
371                let text = blocks_to_source(blocks);
372                for (index, line) in text.lines().enumerate() {
373                    if index > 0 {
374                        out.push('\n');
375                    }
376                    out.push_str("> ");
377                    out.push_str(line);
378                }
379            }
380            Self::List { ordered, items, .. } => {
381                for (index, item) in items.iter().enumerate() {
382                    if index > 0 {
383                        out.push('\n');
384                    }
385                    if *ordered {
386                        out.push_str(&format!("{}. ", index + 1));
387                    } else {
388                        out.push_str("- ");
389                    }
390                    out.push_str(&blocks_to_source(item).replace('\n', "\n  "));
391                }
392            }
393            Self::Table { header, rows, .. } => {
394                write_table_row(out, header);
395                out.push('\n');
396                write_table_separator(out, header.len());
397                for row in rows {
398                    out.push('\n');
399                    write_table_row(out, row);
400                }
401            }
402            Self::Figure { src, caption, .. } => {
403                out.push_str("![");
404                write_inlines(out, caption);
405                out.push_str("](");
406                out.push_str(src);
407                out.push(')');
408            }
409            Self::Raw { text, .. } => out.push_str(text),
410        }
411    }
412}
413
414impl Inline {
415    fn as_expr(&self) -> Expr {
416        match self {
417            Self::Text(value) => map(vec![("kind", sym("text")), ("text", text(value))]),
418            Self::Emph(items) => map(vec![("kind", sym("emph")), ("content", inline_list(items))]),
419            Self::Strong(items) => map(vec![
420                ("kind", sym("strong")),
421                ("content", inline_list(items)),
422            ]),
423            Self::Code(value) => map(vec![("kind", sym("code")), ("text", text(value))]),
424            Self::Link { label, target } => map(vec![
425                ("kind", sym("link")),
426                ("label", inline_list(label)),
427                ("target", text(target)),
428            ]),
429            Self::Math(source) => map(vec![("kind", sym("math")), ("source", source.as_expr())]),
430            Self::Raw { backend, text: raw } => map(vec![
431                ("kind", sym("raw")),
432                ("backend", text(&backend.0)),
433                ("text", text(raw)),
434            ]),
435        }
436    }
437
438    fn from_expr(expr: &Expr) -> Result<Self> {
439        if let Expr::String(value) = expr {
440            return Ok(Self::Text(value.clone()));
441        }
442        let entries = map_entries(expr, "inline")?;
443        match required_kind(entries, "inline")?.as_str() {
444            "text" => Ok(Self::Text(
445                required_string(entries, "text", "inline")?.to_owned(),
446            )),
447            "emph" => Ok(Self::Emph(inline_vec(required_list(
448                entries, "content", "inline",
449            )?)?)),
450            "strong" => Ok(Self::Strong(inline_vec(required_list(
451                entries, "content", "inline",
452            )?)?)),
453            "code" => Ok(Self::Code(
454                required_string(entries, "text", "inline")?.to_owned(),
455            )),
456            "link" => Ok(Self::Link {
457                label: inline_vec(required_list(entries, "label", "inline")?)?,
458                target: required_string(entries, "target", "inline")?.to_owned(),
459            }),
460            "math" => Ok(Self::Math(MathSource::from_expr(required_field(
461                entries, "source", "inline",
462            )?)?)),
463            "raw" => Ok(Self::Raw {
464                backend: BackendId(required_string(entries, "backend", "inline")?.to_owned()),
465                text: required_string(entries, "text", "inline")?.to_owned(),
466            }),
467            other => Err(Error::Eval(format!("unknown inline kind {other}"))),
468        }
469    }
470}