1use sim_codec_doc::{BackendId, Inline, MarkupBlock, MarkupDoc, MathSource};
8use sim_kernel::{Error, Expr, Result, Symbol};
9
10pub const ARTICLE_CLASS: &str = "doc/Article";
12
13pub 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
26pub fn article_from_markup(markup: &MarkupDoc) -> Expr {
28 markup.as_expr()
29}
30
31pub 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
44pub 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
61pub 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
76pub 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
96pub 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
110pub fn section(heading: &str) -> Expr {
112 block("section", vec![("title", Expr::String(heading.to_owned()))])
113}
114
115pub fn prose(text: &str) -> Expr {
117 block("prose", vec![("text", Expr::String(text.to_owned()))])
118}
119
120pub fn equation(source: &str) -> Expr {
122 block("equation", vec![("tex", Expr::String(source.to_owned()))])
123}
124
125pub 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
136pub 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
142pub 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
153pub 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}