Skip to main content

sim_lib_view_doc/
lens.rs

1//! The article lenses: formatted view, source view, and export.
2//!
3//! Two lenses render the same document value side by side -- a formatted view
4//! (outline plus a block canvas) and a source view (each block's raw value).
5//! Embedded live blocks render through `scene/embed`: a runtime value drawn by
6//! its own lens inside the article, using a cached output when one is attached.
7
8use sim_kernel::{Expr, Symbol};
9use sim_lib_scene::{node, sym};
10use sim_lib_view::{Mode, render_value, universal_scene};
11
12use crate::doc::{block_kind, blocks, field, title};
13
14/// The formatted article lens id.
15pub const ARTICLE_FORMATTED_LENS: &str = "view:doc-article";
16
17/// The source article lens id.
18pub const ARTICLE_SOURCE_LENS: &str = "view:doc-source";
19
20/// Attach a cached rendered output (a Scene) to an embed block.
21pub fn with_cache(block: Expr, scene: Expr) -> Expr {
22    let Expr::Map(mut entries) = block else {
23        return block;
24    };
25    entries.push((Expr::Symbol(Symbol::new("cache")), scene));
26    Expr::Map(entries)
27}
28
29/// Render the formatted article: outline plus a block canvas.
30pub fn article_formatted(doc: &Expr) -> Expr {
31    let canvas = blocks(doc).iter().map(block_formatted).collect();
32    node(
33        "stack",
34        vec![
35            ("role", sym("article")),
36            ("dir", sym("row")),
37            (
38                "children",
39                Expr::List(vec![
40                    article_outline(doc),
41                    node(
42                        "stack",
43                        vec![
44                            ("role", sym("canvas")),
45                            ("dir", sym("column")),
46                            ("children", Expr::List(canvas)),
47                        ],
48                    ),
49                ]),
50            ),
51        ],
52    )
53}
54
55/// Render the source article: the title and each block's raw value as text.
56pub fn article_source(doc: &Expr) -> Expr {
57    let mut children = vec![text(format!(
58        "# {}",
59        title(doc).unwrap_or_else(|| "untitled".to_owned())
60    ))];
61    for block in blocks(doc) {
62        children.push(text(render_value(&block)));
63    }
64    node(
65        "stack",
66        vec![
67            ("role", sym("source")),
68            ("dir", sym("column")),
69            ("children", Expr::List(children)),
70        ],
71    )
72}
73
74/// The outline: a tree of the article's section headings.
75pub fn article_outline(doc: &Expr) -> Expr {
76    let sections = blocks(doc)
77        .iter()
78        .filter(|block| block_kind(block).as_ref().map(|k| k.name.as_ref()) == Some("section"))
79        .map(|block| {
80            let heading = match field(block, "title") {
81                Some(Expr::String(text)) => text.clone(),
82                _ => "section".to_owned(),
83            };
84            text(heading)
85        })
86        .collect();
87    node(
88        "tree",
89        vec![
90            ("label", Expr::String("outline".to_owned())),
91            ("nodes", Expr::List(sections)),
92        ],
93    )
94}
95
96fn block_formatted(block: &Expr) -> Expr {
97    match block_kind(block)
98        .as_ref()
99        .map(|k| k.name.to_string())
100        .as_deref()
101    {
102        Some("section") => node(
103            "text",
104            vec![
105                ("role", sym("heading")),
106                ("text", string_field(block, "title")),
107            ],
108        ),
109        Some("prose") => node("text", vec![("text", string_field(block, "text"))]),
110        Some("equation") => node(
111            "box",
112            vec![
113                ("role", sym("equation")),
114                (
115                    "children",
116                    Expr::List(vec![node(
117                        "text",
118                        vec![("text", string_field(block, "tex"))],
119                    )]),
120                ),
121            ],
122        ),
123        Some("figure") => node(
124            "box",
125            vec![
126                ("role", sym("figure")),
127                (
128                    "children",
129                    Expr::List(vec![node(
130                        "text",
131                        vec![("text", string_field(block, "caption"))],
132                    )]),
133                ),
134            ],
135        ),
136        Some("table") => table_scene(block),
137        Some("citation") => node(
138            "text",
139            vec![
140                ("role", sym("citation")),
141                ("text", string_field(block, "text")),
142            ],
143        ),
144        Some("embed") => embed_scene(block),
145        _ => node(
146            "text",
147            vec![("text", Expr::String("[unknown block]".to_owned()))],
148        ),
149    }
150}
151
152fn embed_scene(block: &Expr) -> Expr {
153    let lens = match field(block, "lens") {
154        Some(Expr::Symbol(symbol)) => symbol.clone(),
155        _ => Symbol::new("view:default"),
156    };
157    // Use a cached output if one is attached, else render the value live.
158    let inner = match field(block, "cache") {
159        Some(cached) => cached.clone(),
160        None => universal_scene(field(block, "value").unwrap_or(&Expr::Nil), Mode::Builder),
161    };
162    node(
163        "embed",
164        vec![("lens", Expr::Symbol(lens)), ("scene", inner)],
165    )
166}
167
168fn table_scene(block: &Expr) -> Expr {
169    let rows = match field(block, "rows") {
170        Some(Expr::List(rows)) => rows
171            .iter()
172            .map(|row| match row {
173                Expr::List(cells) => Expr::List(
174                    cells
175                        .iter()
176                        .map(|cell| Expr::String(render_value(cell)))
177                        .collect(),
178                ),
179                other => Expr::List(vec![Expr::String(render_value(other))]),
180            })
181            .collect(),
182        _ => Vec::new(),
183    };
184    node("table", vec![("rows", Expr::List(rows))])
185}
186
187/// The stable intermediate document value used for export. Export formats are
188/// derived from this single IR; here it is the normalized document value.
189pub fn export_intermediate(doc: &Expr) -> Expr {
190    crate::doc::article(
191        &title(doc).unwrap_or_else(|| "untitled".to_owned()),
192        blocks(doc),
193    )
194}
195
196/// A Markdown export stub over the stable intermediate document value.
197pub fn export_markdown(doc: &Expr) -> String {
198    let mut out = format!(
199        "# {}\n\n",
200        title(doc).unwrap_or_else(|| "untitled".to_owned())
201    );
202    for block in blocks(doc) {
203        let line = match block_kind(&block)
204            .as_ref()
205            .map(|k| k.name.to_string())
206            .as_deref()
207        {
208            Some("section") => format!("## {}\n\n", string_text(&block, "title")),
209            Some("prose") => format!("{}\n\n", string_text(&block, "text")),
210            Some("equation") => format!("$$\n{}\n$$\n\n", string_text(&block, "tex")),
211            Some("citation") => format!("> {}\n\n", string_text(&block, "text")),
212            Some("embed") => "[embedded runtime value]\n\n".to_owned(),
213            _ => String::new(),
214        };
215        out.push_str(&line);
216    }
217    out
218}
219
220fn string_field(block: &Expr, name: &str) -> Expr {
221    Expr::String(string_text(block, name))
222}
223
224fn string_text(block: &Expr, name: &str) -> String {
225    match field(block, name) {
226        Some(Expr::String(text)) => text.clone(),
227        Some(other) => render_value(other),
228        None => String::new(),
229    }
230}
231
232fn text(content: String) -> Expr {
233    node("text", vec![("text", Expr::String(content))])
234}