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 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 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#[cfg(test)]
23#[derive(Debug, thiserror::Error)]
24pub enum MetadataError {
25 #[error(transparent)]
26 Yaml(#[from] marked_yaml::FromYamlError),
27}
28
29#[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 , yaml_text)?;
59 Ok(meta)
60 }
61
62 pub fn markdowns(&self) -> &[PathBuf] {
64 &self.markdowns
65 }
66
67 pub fn title(&self) -> &str {
69 &self.title
70 }
71
72 pub fn subtitle(&self) -> Option<&str> {
74 self.subtitle.as_deref()
75 }
76
77 pub fn date(&self) -> Option<&str> {
79 self.date.as_deref()
80 }
81
82 pub fn set_date(&mut self, date: String) {
84 self.date = Some(date);
85 }
86
87 pub fn authors(&self) -> Option<&[String]> {
89 self.authors.as_deref()
90 }
91
92 pub fn bindings_filenames(&self) -> Option<&[PathBuf]> {
94 self.bindings.as_deref()
95 }
96
97 pub fn impls(&self) -> &BTreeMap<String, Vec<PathBuf>> {
99 &self.impls
100 }
101
102 pub fn classes(&self) -> Option<&[String]> {
104 self.classes.as_deref()
105 }
106
107 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#[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 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 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 pub fn title(&self) -> &str {
250 &self.title
251 }
252
253 pub fn date(&self) -> Option<&str> {
255 self.date.as_deref()
256 }
257
258 pub fn set_date(&mut self, date: String) {
260 self.date = Some(date);
261 }
262
263 pub fn authors(&self) -> Option<&[String]> {
265 self.authors.as_deref()
266 }
267
268 pub fn basedir(&self) -> &Path {
270 &self.basedir
271 }
272
273 pub fn markdown_filenames(&self) -> &[PathBuf] {
275 &self.markdown_filenames
276 }
277
278 pub fn bindings_filenames(&self) -> Vec<&Path> {
280 self.bindings_filenames.iter().map(|f| f.as_ref()).collect()
281 }
282
283 pub fn document_impl(&self, template: &str) -> Option<&DocumentImpl> {
285 self.impls.get(template)
286 }
287
288 pub fn templates(&self) -> impl Iterator<Item = &str> {
290 self.impls.keys().map(String::as_str)
291 }
292
293 pub fn bindings(&self) -> &Bindings {
295 &self.bindings
296 }
297
298 pub fn classes(&self) -> impl Iterator<Item = &str> {
300 self.classes.iter().map(Deref::deref)
301 }
302
303 pub fn css_embed(&self) -> impl Iterator<Item = &str> {
305 self.css_embed.iter().map(Deref::deref)
306 }
307
308 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}