subplot/
metadata.rs

1use crate::{Bindings, SubplotError, TemplateSpec};
2
3use lazy_static::lazy_static;
4use regex::Regex;
5use serde::Deserialize;
6use std::collections::{BTreeMap, HashMap};
7use std::fmt::Debug;
8use std::ops::Deref;
9use std::path::{Path, PathBuf};
10use tracing::trace;
11
12lazy_static! {
13    // Pattern that recognises a YAML block at the beginning of a file.
14    static ref LEADING_YAML_PATTERN: Regex = Regex::new(r"^(?:\S*\n)*(?P<yaml>-{3,}\n([^.].*\n)*\.{3,}\n)(?P<text>(.*\n)*)$").unwrap();
15
16
17    // Pattern that recognises a YAML block at the end of a file.
18    static ref TRAILING_YAML_PATTERN: Regex = Regex::new(r"(?P<text>(.*\n)*)\n*(?P<yaml>-{3,}\n([^.].*\n)*\.{3,}\n)(?:\S*\n)*$").unwrap();
19}
20
21/// Errors from Markdown parsing.
22#[cfg(test)]
23#[derive(Debug, thiserror::Error)]
24pub enum MetadataError {
25    #[error(transparent)]
26    Yaml(#[from] marked_yaml::FromYamlError),
27}
28
29/// Document metadata.
30///
31/// This is expressed in the Markdown input file as an embedded YAML
32/// block.
33///
34/// Note that this structure needs to be able to capture any metadata
35/// block we can work with, in any input file. By being strict here we
36/// make it easier to tell the user when a metadata block has, say, a
37/// misspelled field.
38#[derive(Debug, Default, Clone, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct YamlMetadata {
41    title: String,
42    subtitle: Option<String>,
43    authors: Option<Vec<String>>,
44    date: Option<String>,
45    classes: Option<Vec<String>>,
46    markdowns: Vec<PathBuf>,
47    bindings: Option<Vec<PathBuf>>,
48    documentclass: Option<String>,
49    #[serde(default)]
50    impls: BTreeMap<String, Vec<PathBuf>>,
51    css_embed: Option<Vec<PathBuf>>,
52    css_urls: Option<Vec<String>>,
53}
54
55impl YamlMetadata {
56    #[cfg(test)]
57    fn new(yaml_text: &str) -> Result<Self, MetadataError> {
58        let meta: Self = marked_yaml::from_yaml(0 /* TODO: Track sources */, yaml_text)?;
59        Ok(meta)
60    }
61
62    /// Names of files with the Markdown for the subplot document.
63    pub fn markdowns(&self) -> &[PathBuf] {
64        &self.markdowns
65    }
66
67    /// Title.
68    pub fn title(&self) -> &str {
69        &self.title
70    }
71
72    /// Subtitle.
73    pub fn subtitle(&self) -> Option<&str> {
74        self.subtitle.as_deref()
75    }
76
77    /// Date.
78    pub fn date(&self) -> Option<&str> {
79        self.date.as_deref()
80    }
81
82    /// Set date.
83    pub fn set_date(&mut self, date: String) {
84        self.date = Some(date);
85    }
86
87    /// Authors.
88    pub fn authors(&self) -> Option<&[String]> {
89        self.authors.as_deref()
90    }
91
92    /// Names of bindings files.
93    pub fn bindings_filenames(&self) -> Option<&[PathBuf]> {
94        self.bindings.as_deref()
95    }
96
97    /// Impls section.
98    pub fn impls(&self) -> &BTreeMap<String, Vec<PathBuf>> {
99        &self.impls
100    }
101
102    /// Classes..
103    pub fn classes(&self) -> Option<&[String]> {
104        self.classes.as_deref()
105    }
106
107    /// Documentclass.
108    pub fn documentclass(&self) -> Option<&str> {
109        self.documentclass.as_deref()
110    }
111}
112
113#[cfg(test)]
114mod test {
115    use super::YamlMetadata;
116    use std::path::{Path, PathBuf};
117
118    #[test]
119    fn full_meta() {
120        let meta = YamlMetadata::new(
121            "\
122title: Foo Bar
123date: today
124classes: [json, text]
125impls:
126  python:
127   - foo.py
128   - bar.py
129markdowns:
130- test.md
131bindings:
132- foo.yaml
133- bar.yaml
134",
135        )
136        .unwrap();
137        assert_eq!(meta.title, "Foo Bar");
138        assert_eq!(meta.date.unwrap(), "today");
139        assert_eq!(meta.classes.unwrap(), &["json", "text"]);
140        assert_eq!(meta.markdowns, vec![Path::new("test.md")]);
141        assert_eq!(
142            meta.bindings.unwrap(),
143            &[path("foo.yaml"), path("bar.yaml")]
144        );
145        assert!(!meta.impls.is_empty());
146        for (k, v) in meta.impls.iter() {
147            assert_eq!(k, "python");
148            assert_eq!(v, &[path("foo.py"), path("bar.py")]);
149        }
150    }
151
152    fn path(s: &str) -> PathBuf {
153        PathBuf::from(s)
154    }
155}
156
157/// Metadata of a document, as needed by Subplot.
158#[derive(Debug)]
159pub struct Metadata {
160    basedir: PathBuf,
161    title: String,
162    date: Option<String>,
163    authors: Option<Vec<String>>,
164    markdown_filenames: Vec<PathBuf>,
165    bindings_filenames: Vec<PathBuf>,
166    bindings: Bindings,
167    impls: HashMap<String, DocumentImpl>,
168    /// Extra class names which should be considered 'correct' for this document
169    classes: Vec<String>,
170    css_embed: Vec<String>,
171    css_urls: Vec<String>,
172}
173
174#[derive(Debug)]
175pub struct DocumentImpl {
176    spec: TemplateSpec,
177    functions: Vec<PathBuf>,
178}
179
180impl Metadata {
181    /// Create from YamlMetadata.
182    pub fn from_yaml_metadata<P>(
183        basedir: P,
184        yaml: &YamlMetadata,
185        template: Option<&str>,
186    ) -> Result<Self, SubplotError>
187    where
188        P: AsRef<Path> + Debug,
189    {
190        let mut bindings = Bindings::new();
191        let bindings_filenames = if let Some(filenames) = yaml.bindings_filenames() {
192            get_bindings(filenames, &mut bindings, template)?;
193            filenames.iter().map(|p| p.to_path_buf()).collect()
194        } else {
195            vec![]
196        };
197
198        let mut impls = HashMap::new();
199
200        for (impl_name, functions_filenames) in yaml.impls().iter() {
201            let template_spec = load_template_spec(impl_name)?;
202            let filenames = pathbufs("", functions_filenames);
203            let docimpl = DocumentImpl::new(template_spec, filenames);
204            impls.insert(impl_name.to_string(), docimpl);
205        }
206
207        let classes = if let Some(v) = yaml.classes() {
208            v.iter().map(|s| s.to_string()).collect()
209        } else {
210            vec![]
211        };
212
213        let mut css_embed = vec![];
214        if let Some(filenames) = &yaml.css_embed {
215            for filename in filenames.iter() {
216                let css = std::fs::read(filename)
217                    .map_err(|e| SubplotError::ReadFile(filename.into(), e))?;
218                let css = String::from_utf8(css)
219                    .map_err(|e| SubplotError::FileUtf8(filename.into(), e))?;
220                css_embed.push(css);
221            }
222        }
223
224        let css_urls = if let Some(urls) = &yaml.css_urls {
225            urls.clone()
226        } else {
227            vec![]
228        };
229
230        let meta = Self {
231            basedir: basedir.as_ref().to_path_buf(),
232            title: yaml.title().into(),
233            date: yaml.date().map(|s| s.into()),
234            authors: yaml.authors().map(|a| a.into()),
235            markdown_filenames: yaml.markdowns().into(),
236            bindings_filenames,
237            bindings,
238            impls,
239            classes,
240            css_embed,
241            css_urls,
242        };
243        trace!("metadata: {:#?}", meta);
244
245        Ok(meta)
246    }
247
248    /// Return title of document.
249    pub fn title(&self) -> &str {
250        &self.title
251    }
252
253    /// Return date of document, if any.
254    pub fn date(&self) -> Option<&str> {
255        self.date.as_deref()
256    }
257
258    /// Set date.
259    pub fn set_date(&mut self, date: String) {
260        self.date = Some(date);
261    }
262
263    /// Authors.
264    pub fn authors(&self) -> Option<&[String]> {
265        self.authors.as_deref()
266    }
267
268    /// Return base dir for all relative filenames.
269    pub fn basedir(&self) -> &Path {
270        &self.basedir
271    }
272
273    /// Return filenames of the markdown files.
274    pub fn markdown_filenames(&self) -> &[PathBuf] {
275        &self.markdown_filenames
276    }
277
278    /// Return filename where bindings are specified.
279    pub fn bindings_filenames(&self) -> Vec<&Path> {
280        self.bindings_filenames.iter().map(|f| f.as_ref()).collect()
281    }
282
283    /// Return the document implementation (filenames, spec, etc) for the given template name
284    pub fn document_impl(&self, template: &str) -> Option<&DocumentImpl> {
285        self.impls.get(template)
286    }
287
288    /// Return the templates the document expects to implement
289    pub fn templates(&self) -> impl Iterator<Item = &str> {
290        self.impls.keys().map(String::as_str)
291    }
292
293    /// Return the bindings.
294    pub fn bindings(&self) -> &Bindings {
295        &self.bindings
296    }
297
298    /// The classes which this document also claims are valid
299    pub fn classes(&self) -> impl Iterator<Item = &str> {
300        self.classes.iter().map(Deref::deref)
301    }
302
303    /// Contents of CSS files to embed into the HTML output.
304    pub fn css_embed(&self) -> impl Iterator<Item = &str> {
305        self.css_embed.iter().map(Deref::deref)
306    }
307
308    /// List of CSS urls to add to the HTML output.
309    pub fn css_urls(&self) -> impl Iterator<Item = &str> {
310        self.css_urls.iter().map(Deref::deref)
311    }
312}
313
314impl DocumentImpl {
315    fn new(spec: TemplateSpec, functions: Vec<PathBuf>) -> Self {
316        Self { spec, functions }
317    }
318
319    pub fn functions_filenames(&self) -> impl Iterator<Item = &Path> {
320        self.functions.iter().map(PathBuf::as_path)
321    }
322
323    pub fn spec(&self) -> &TemplateSpec {
324        &self.spec
325    }
326}
327
328fn load_template_spec(template: &str) -> Result<TemplateSpec, SubplotError> {
329    let mut spec_path = PathBuf::from(template);
330    spec_path.push("template");
331    spec_path.push("template.yaml");
332    TemplateSpec::from_file(&spec_path)
333}
334
335fn pathbufs<P>(basedir: P, v: &[PathBuf]) -> Vec<PathBuf>
336where
337    P: AsRef<Path>,
338{
339    let basedir = basedir.as_ref();
340    v.iter().map(|p| basedir.join(p)).collect()
341}
342
343fn get_bindings<P>(
344    filenames: &[P],
345    bindings: &mut Bindings,
346    template: Option<&str>,
347) -> Result<(), SubplotError>
348where
349    P: AsRef<Path> + Debug,
350{
351    for filename in filenames {
352        bindings.add_from_file(filename, template)?;
353    }
354    Ok(())
355}