1use crate::ast::Ast;
2use crate::raw::{parse_to_doc, ComposedMarkdown, RawDocument, Special};
3use anyhow::Result;
4use linked_hash_map::LinkedHashMap;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8
9#[derive(Debug, PartialEq, Clone, Serialize)]
10pub struct Document<T: Serialize> {
11 pub meta: Metadata,
12 pub content: T,
13 pub code_outputs: HashMap<u64, CodeOutput>,
14}
15
16#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct Metadata {
19 pub title: String,
20 #[serde(default)]
21 pub draft: bool,
22 #[serde(default = "default_true")]
23 pub exercises: bool,
24 #[serde(default)]
25 pub code_solutions: Option<bool>,
26 #[serde(default = "default_true")]
27 pub cell_outputs: bool,
28 #[serde(default)]
29 pub interactive: bool,
30 #[serde(default)]
31 pub editable: bool,
32 #[serde(default)]
33 pub layout: LayoutSettings,
34 #[serde(default)]
35 pub exclude_outputs: Option<Vec<String>>,
36
37 #[serde(flatten)]
38 pub user_defined: LinkedHashMap<String, Value>,
39}
40
41const fn default_true() -> bool {
42 true
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
46pub struct LayoutSettings {
47 pub hide_sidebar: bool,
48}
49
50#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
51pub struct CodeOutput {
52 pub values: Vec<OutputValue>,
53}
54
55#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
56pub enum OutputValue {
57 Plain(String),
58 Text(String),
59 Image(Image),
60 Json(Value),
61 Html(String),
62 Javascript(String),
63 Error(String),
64}
65
66#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
67pub enum Image {
68 Png(String),
69 Svg(String),
70}
71
72fn parse_raw(doc: RawDocument) -> Result<Document<Ast>> {
73 let composed = ComposedMarkdown::from(doc.src);
74 let code_outputs = composed
75 .children
76 .iter()
77 .filter_map(|c| match &c.elem {
78 Special::CodeBlock { inner, .. } => Some((inner.hash, CodeOutput::default())),
79 _ => None,
80 })
81 .collect();
82
83 let ast = composed.into();
84
85 let doc = Document {
86 content: Ast {
87 blocks: ast,
88 source: doc.input,
89 },
90 meta: doc.meta.map_or(
91 Ok::<Metadata, serde_yaml::Error>(Metadata::default()),
92 |meta| serde_yaml::from_str(&meta),
93 )?,
94 code_outputs,
95 };
96
97 Ok(doc)
98}
99
100impl TryFrom<&str> for Document<Ast> {
101 type Error = anyhow::Error;
102
103 fn try_from(value: &str) -> Result<Self, Self::Error> {
104 let raw = parse_to_doc(value)?;
105 parse_raw(raw)
106 }
107}
108
109impl<T: Serialize> Document<T> {
110 pub fn map<O: Serialize, F: Fn(T) -> O>(self, f: F) -> Document<O> {
111 Document {
112 content: f(self.content),
113 meta: self.meta,
114 code_outputs: self.code_outputs,
115 }
118 }
119
120 pub fn try_map<O: Serialize, F: Fn(T) -> Result<O>>(self, f: F) -> Result<Document<O>> {
121 Ok(Document {
122 content: f(self.content)?,
123 meta: self.meta,
124 code_outputs: self.code_outputs,
125 })
128 }
129}