Skip to main content

sim_lib_view_doc/
doc.rs

1//! The document value model.
2//!
3//! A scientific article is a round-trippable SIM value: a map of a title and an
4//! ordered list of semantic blocks. Blocks are tagged with a `block` key (not
5//! `kind`, which is reserved for scene nodes) so a document is plain data,
6//! distinct from the Scene a lens renders from it. Block kinds: section, prose,
7//! equation, figure, table, citation, and embed (an embedded runtime value).
8
9use sim_kernel::{Expr, Symbol};
10
11/// The article class symbol carried in the document's `class` field.
12pub const ARTICLE_CLASS: &str = "doc/Article";
13
14/// The key that tags a block with its kind.
15pub const BLOCK_KEY: &str = "block";
16
17fn key(name: &str) -> Expr {
18    Expr::Symbol(Symbol::new(name))
19}
20
21fn map(entries: Vec<(&str, Expr)>) -> Expr {
22    Expr::Map(entries.into_iter().map(|(k, v)| (key(k), v)).collect())
23}
24
25/// Look up a string-keyed field of a map value.
26pub fn field<'a>(value: &'a Expr, name: &str) -> Option<&'a Expr> {
27    let Expr::Map(entries) = value else {
28        return None;
29    };
30    entries.iter().find_map(|(entry_key, entry_value)| {
31        matches!(entry_key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
32            .then_some(entry_value)
33    })
34}
35
36/// Build an article document from a title and an ordered list of blocks.
37pub fn article(title: &str, blocks: Vec<Expr>) -> Expr {
38    map(vec![
39        ("class", Expr::Symbol(Symbol::qualified("doc", "Article"))),
40        ("title", Expr::String(title.to_owned())),
41        ("blocks", Expr::List(blocks)),
42    ])
43}
44
45/// The article title.
46pub fn title(doc: &Expr) -> Option<String> {
47    match field(doc, "title") {
48        Some(Expr::String(text)) => Some(text.clone()),
49        _ => None,
50    }
51}
52
53/// The article's ordered blocks.
54pub fn blocks(doc: &Expr) -> Vec<Expr> {
55    match field(doc, "blocks") {
56        Some(Expr::List(items)) => items.clone(),
57        _ => Vec::new(),
58    }
59}
60
61/// The kind of a block (the `block` tag), if present.
62pub fn block_kind(block: &Expr) -> Option<Symbol> {
63    match field(block, BLOCK_KEY) {
64        Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
65        _ => None,
66    }
67}
68
69fn block(kind: &str, mut entries: Vec<(&str, Expr)>) -> Expr {
70    let mut pairs = vec![(BLOCK_KEY, Expr::Symbol(Symbol::new(kind)))];
71    pairs.append(&mut entries);
72    map(pairs)
73}
74
75/// A section heading block.
76pub fn section(heading: &str) -> Expr {
77    block("section", vec![("title", Expr::String(heading.to_owned()))])
78}
79
80/// A prose paragraph block.
81pub fn prose(text: &str) -> Expr {
82    block("prose", vec![("text", Expr::String(text.to_owned()))])
83}
84
85/// An equation block carrying its source (for example TeX).
86pub fn equation(source: &str) -> Expr {
87    block("equation", vec![("tex", Expr::String(source.to_owned()))])
88}
89
90/// A figure block.
91pub fn figure(caption: &str, src: &str) -> Expr {
92    block(
93        "figure",
94        vec![
95            ("caption", Expr::String(caption.to_owned())),
96            ("src", Expr::String(src.to_owned())),
97        ],
98    )
99}
100
101/// A table block from rows of cell values.
102pub fn table(rows: Vec<Vec<Expr>>) -> Expr {
103    let rows = rows.into_iter().map(Expr::List).collect();
104    block("table", vec![("rows", Expr::List(rows))])
105}
106
107/// A citation block.
108pub fn citation(cite_key: &str, text: &str) -> Expr {
109    block(
110        "citation",
111        vec![
112            ("key", Expr::Symbol(Symbol::new(cite_key))),
113            ("text", Expr::String(text.to_owned())),
114        ],
115    )
116}
117
118/// An embedded-runtime block: a runtime `value` rendered by `lens` inside the
119/// article. Cached output (a precomputed Scene) may be attached later.
120pub fn embed_block(value: Expr, lens: &str) -> Expr {
121    block(
122        "embed",
123        vec![("value", value), ("lens", Expr::Symbol(Symbol::new(lens)))],
124    )
125}