Skip to main content

sim_lib_view_doc/
doc.rs

1//! The document value adapters.
2//!
3//! The canonical document value is [`sim_codec_doc::MarkupDoc`]. The helpers in
4//! this module keep the article-shaped constructor names as compatibility
5//! wrappers while the lenses read and write the shared markup frontend.
6
7use sim_codec_doc::{BackendId, Inline, MarkupBlock, MarkupDoc, MathSource};
8use sim_kernel::{Error, Expr, Result, Symbol};
9
10/// The article class symbol carried in the document's `class` field.
11pub const ARTICLE_CLASS: &str = "doc/Article";
12
13/// The key that tags a block with its kind.
14pub const BLOCK_KEY: &str = "block";
15
16pub use sim_value::access::field;
17
18fn key(name: &str) -> Expr {
19    Expr::Symbol(Symbol::new(name))
20}
21
22fn map(entries: Vec<(&str, Expr)>) -> Expr {
23    Expr::Map(entries.into_iter().map(|(k, v)| (key(k), v)).collect())
24}
25
26/// Build a canonical article document from a markup document.
27pub fn article_from_markup(markup: &MarkupDoc) -> Expr {
28    markup.as_expr()
29}
30
31/// Convert a canonical or compatibility article expression into markup.
32///
33/// # Errors
34///
35/// Returns an error when the expression is neither a `MarkupDoc` value nor a
36/// compatibility article value.
37pub fn markup_from_article(doc: &Expr) -> Result<MarkupDoc> {
38    match MarkupDoc::from_expr(doc) {
39        Ok(markup) => Ok(markup),
40        Err(_) => compatibility_to_markup(doc),
41    }
42}
43
44/// Build an article document from a title and an ordered list of blocks.
45pub fn article(title: &str, blocks: Vec<Expr>) -> Expr {
46    let compatibility = compatibility_article(title, blocks);
47    match compatibility_to_markup(&compatibility) {
48        Ok(markup) => article_from_markup(&markup),
49        Err(_) => compatibility,
50    }
51}
52
53fn compatibility_article(title: &str, blocks: Vec<Expr>) -> Expr {
54    map(vec![
55        ("class", Expr::Symbol(Symbol::qualified("doc", "Article"))),
56        ("title", Expr::String(title.to_owned())),
57        ("blocks", Expr::List(blocks)),
58    ])
59}
60
61/// The article title.
62pub fn title(doc: &Expr) -> Option<String> {
63    if let Ok(markup) = MarkupDoc::from_expr(doc) {
64        return markup.title;
65    }
66    compatibility_title(doc)
67}
68
69fn compatibility_title(doc: &Expr) -> Option<String> {
70    match field(doc, "title") {
71        Some(Expr::String(text)) => Some(text.clone()),
72        _ => None,
73    }
74}
75
76/// The article's ordered blocks.
77pub fn blocks(doc: &Expr) -> Vec<Expr> {
78    if let Ok(markup) = MarkupDoc::from_expr(doc) {
79        return markup
80            .blocks
81            .iter()
82            .enumerate()
83            .map(|(index, block)| markup_block_to_compatibility(index, block))
84            .collect();
85    }
86    compatibility_blocks(doc)
87}
88
89fn compatibility_blocks(doc: &Expr) -> Vec<Expr> {
90    match field(doc, "blocks") {
91        Some(Expr::List(items)) => items.clone(),
92        _ => Vec::new(),
93    }
94}
95
96/// The kind of a block (the `block` tag), if present.
97pub fn block_kind(block: &Expr) -> Option<Symbol> {
98    match field(block, BLOCK_KEY) {
99        Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
100        _ => None,
101    }
102}
103
104fn block(kind: &str, mut entries: Vec<(&str, Expr)>) -> Expr {
105    let mut pairs = vec![(BLOCK_KEY, Expr::Symbol(Symbol::new(kind)))];
106    pairs.append(&mut entries);
107    map(pairs)
108}
109
110/// A section heading block.
111pub fn section(heading: &str) -> Expr {
112    block("section", vec![("title", Expr::String(heading.to_owned()))])
113}
114
115/// A prose paragraph block.
116pub fn prose(text: &str) -> Expr {
117    block("prose", vec![("text", Expr::String(text.to_owned()))])
118}
119
120/// An equation block carrying its source (for example TeX).
121pub fn equation(source: &str) -> Expr {
122    block("equation", vec![("tex", Expr::String(source.to_owned()))])
123}
124
125/// A figure block.
126pub fn figure(caption: &str, src: &str) -> Expr {
127    block(
128        "figure",
129        vec![
130            ("caption", Expr::String(caption.to_owned())),
131            ("src", Expr::String(src.to_owned())),
132        ],
133    )
134}
135
136/// A table block from rows of cell values.
137pub fn table(rows: Vec<Vec<Expr>>) -> Expr {
138    let rows = rows.into_iter().map(Expr::List).collect();
139    block("table", vec![("rows", Expr::List(rows))])
140}
141
142/// A citation block.
143pub fn citation(cite_key: &str, text: &str) -> Expr {
144    block(
145        "citation",
146        vec![
147            ("key", Expr::Symbol(Symbol::new(cite_key))),
148            ("text", Expr::String(text.to_owned())),
149        ],
150    )
151}
152
153/// An embedded-runtime block: a runtime `value` rendered by `lens` inside the
154/// article. Cached output (a precomputed Scene) may be attached later.
155pub fn embed_block(value: Expr, lens: &str) -> Expr {
156    block(
157        "embed",
158        vec![("value", value), ("lens", Expr::Symbol(Symbol::new(lens)))],
159    )
160}
161
162fn compatibility_to_markup(doc: &Expr) -> Result<MarkupDoc> {
163    let title = compatibility_title(doc)
164        .ok_or_else(|| Error::Eval("article is missing title".to_owned()))?;
165    let blocks = compatibility_blocks(doc);
166    let blocks = blocks
167        .iter()
168        .map(compatibility_block_to_markup)
169        .collect::<Result<Vec<_>>>()?;
170    Ok(MarkupDoc {
171        title: Some(title),
172        blocks,
173        attrs: Default::default(),
174        source: None,
175    })
176}
177
178fn compatibility_block_to_markup(block: &Expr) -> Result<MarkupBlock> {
179    match block_kind(block)
180        .as_ref()
181        .map(|kind| kind.name.to_string())
182        .as_deref()
183    {
184        Some("section") => Ok(MarkupBlock::Heading {
185            level: 2,
186            text: vec![Inline::Text(string_text(block, "title"))],
187            id: None,
188            span: None,
189        }),
190        Some("prose") => Ok(MarkupBlock::Paragraph {
191            content: vec![Inline::Text(string_text(block, "text"))],
192            span: None,
193        }),
194        Some("equation") => Ok(MarkupBlock::MathBlock {
195            source: MathSource {
196                notation: "tex".to_owned(),
197                text: string_text(block, "tex"),
198            },
199            span: None,
200        }),
201        Some("figure") => Ok(MarkupBlock::Figure {
202            src: string_text(block, "src"),
203            caption: vec![Inline::Text(string_text(block, "caption"))],
204            span: None,
205        }),
206        Some("table") => Ok(table_to_markup(block)),
207        Some("citation") => Ok(MarkupBlock::Raw {
208            backend: BackendId::new("view-doc/citation"),
209            text: string_text(block, "text"),
210            span: None,
211        }),
212        Some("embed") => Ok(MarkupBlock::Raw {
213            backend: BackendId::new("view-doc/embed"),
214            text: field(block, "value").map(expr_text).unwrap_or_default(),
215            span: None,
216        }),
217        Some(other) => Ok(MarkupBlock::Raw {
218            backend: BackendId::new(format!("view-doc/{other}")),
219            text: expr_text(block),
220            span: None,
221        }),
222        None => Err(Error::Eval(
223            "article block is missing block kind".to_owned(),
224        )),
225    }
226}
227
228fn table_to_markup(block: &Expr) -> MarkupBlock {
229    let rows = match field(block, "rows") {
230        Some(Expr::List(rows)) => rows
231            .iter()
232            .map(|row| match row {
233                Expr::List(cells) => cells
234                    .iter()
235                    .map(|cell| vec![Inline::Text(expr_text(cell))])
236                    .collect(),
237                other => vec![vec![Inline::Text(expr_text(other))]],
238            })
239            .collect::<Vec<Vec<Vec<Inline>>>>(),
240        _ => Vec::new(),
241    };
242    let mut rows = rows.into_iter();
243    MarkupBlock::Table {
244        header: rows.next().unwrap_or_default(),
245        rows: rows.collect(),
246        span: None,
247    }
248}
249
250fn markup_block_to_compatibility(index: usize, markup_block: &MarkupBlock) -> Expr {
251    match markup_block {
252        MarkupBlock::Heading { text, .. } => section(&inline_text(text)),
253        MarkupBlock::Paragraph { content, .. } => prose(&inline_text(content)),
254        MarkupBlock::MathBlock { source, .. } => equation(&source.text),
255        MarkupBlock::Figure { src, caption, .. } => figure(&inline_text(caption), src),
256        MarkupBlock::Table { header, rows, .. } => {
257            let all_rows = std::iter::once(header)
258                .chain(rows.iter())
259                .map(|row| {
260                    row.iter()
261                        .map(|cell| Expr::String(inline_text(cell)))
262                        .collect()
263                })
264                .collect();
265            table(all_rows)
266        }
267        MarkupBlock::Raw { backend, text, .. } if backend.as_str() == "view-doc/citation" => {
268            citation("citation", text)
269        }
270        MarkupBlock::Raw { backend, text, .. } if backend.as_str() == "view-doc/embed" => {
271            embed_block(Expr::String(text.clone()), "view:default")
272        }
273        MarkupBlock::CodeBlock { code, .. } => prose(code),
274        MarkupBlock::Quote { blocks, .. } => prose(
275            &blocks
276                .iter()
277                .map(block_summary)
278                .collect::<Vec<_>>()
279                .join("\n"),
280        ),
281        MarkupBlock::List { items, .. } => {
282            let text = items
283                .iter()
284                .map(|item| item.iter().map(block_summary).collect::<Vec<_>>().join(" "))
285                .collect::<Vec<_>>()
286                .join("\n");
287            prose(&text)
288        }
289        MarkupBlock::Raw { text, .. } => block(
290            "raw",
291            vec![
292                ("key", Expr::String(format!("raw-{index}"))),
293                ("text", Expr::String(text.clone())),
294            ],
295        ),
296    }
297}
298
299fn block_summary(block: &MarkupBlock) -> String {
300    match block {
301        MarkupBlock::Heading { text, .. } => inline_text(text),
302        MarkupBlock::Paragraph { content, .. } => inline_text(content),
303        MarkupBlock::CodeBlock { code, .. } => code.clone(),
304        MarkupBlock::MathBlock { source, .. } => source.text.clone(),
305        MarkupBlock::Quote { blocks, .. } => blocks
306            .iter()
307            .map(block_summary)
308            .collect::<Vec<_>>()
309            .join("\n"),
310        MarkupBlock::List { items, .. } => items
311            .iter()
312            .map(|item| item.iter().map(block_summary).collect::<Vec<_>>().join(" "))
313            .collect::<Vec<_>>()
314            .join("\n"),
315        MarkupBlock::Table { header, rows, .. } => std::iter::once(header)
316            .chain(rows.iter())
317            .flat_map(|row| row.iter().map(|cell| inline_text(cell)))
318            .collect::<Vec<_>>()
319            .join(" "),
320        MarkupBlock::Figure { src, caption, .. } => format!("{} {}", inline_text(caption), src)
321            .trim()
322            .to_owned(),
323        MarkupBlock::Raw { text, .. } => text.clone(),
324    }
325}
326
327fn inline_text(items: &[Inline]) -> String {
328    let mut out = String::new();
329    for item in items {
330        match item {
331            Inline::Text(text) | Inline::Code(text) => out.push_str(text),
332            Inline::Emph(children) | Inline::Strong(children) => {
333                out.push_str(&inline_text(children))
334            }
335            Inline::Link { label, .. } => out.push_str(&inline_text(label)),
336            Inline::Math(source) => out.push_str(&source.text),
337            Inline::Raw { text, .. } => out.push_str(text),
338        }
339    }
340    out
341}
342
343fn string_text(expr: &Expr, name: &str) -> String {
344    match field(expr, name) {
345        Some(Expr::String(text)) => text.clone(),
346        Some(other) => expr_text(other),
347        None => String::new(),
348    }
349}
350
351fn expr_text(expr: &Expr) -> String {
352    match expr {
353        Expr::String(text) => text.clone(),
354        Expr::Symbol(symbol) => symbol.to_string(),
355        Expr::Number(number) => number.canonical.clone(),
356        Expr::Bool(value) => value.to_string(),
357        Expr::Nil => String::new(),
358        other => format!("{other:?}"),
359    }
360}