use lazy_static::lazy_static; use regex::Regex; use serde::Deserialize; use serde_yaml::Value; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; lazy_static! { // Pattern that recognises a YAML block at the beginning of a file. static ref LEADING_YAML_PATTERN: Regex = Regex::new(r"^(?:\S*\n)*(?P-{3,}\n([^.].*\n)*\.{3,}\n)(?P(.*\n)*)$").unwrap(); // Pattern that recognises a YAML block at the end of a file. static ref TRAILING_YAML_PATTERN: Regex = Regex::new(r"(?P(.*\n)*)\n*(?P-{3,}\n([^.].*\n)*\.{3,}\n)(?:\S*\n)*$").unwrap(); } /// Errors from Markdown parsing. #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] Regex(#[from] regex::Error), #[error("Markdown doesn't contain a YAML block for document metadata")] NoMetadata, #[error(transparent)] Yaml(#[from] serde_yaml::Error), } /// Document metadata. /// /// This is expressed in the Markdown input file as an embedded YAML /// block. /// /// Note that this structure needs to be able to capture any metadata /// block we can work with, in any input file. By being strict here we /// make it easier to tell the user when a metadata block has, say, a /// misspelled field. #[derive(Debug, Default, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct YamlMetadata { title: String, subtitle: Option, authors: Option>, date: Option, classes: Option>, bibliography: Option>, markdowns: Vec, bindings: Option>, documentclass: Option, #[serde(default)] impls: BTreeMap>, pandoc: Option>, } impl YamlMetadata { #[cfg(test)] fn new(yaml_text: &str) -> Result { let meta: Self = serde_yaml::from_str(yaml_text)?; Ok(meta) } /// Name of file with the Markdown for the subplot document. pub fn markdown(&self) -> &Path { &self.markdowns[0] } /// Title. pub fn title(&self) -> &str { &self.title } /// Subtitle. pub fn subtitle(&self) -> Option<&str> { self.subtitle.as_deref() } /// Date. pub fn date(&self) -> Option<&str> { self.date.as_deref() } /// Authors. pub fn authors(&self) -> Option<&[String]> { self.authors.as_deref() } /// Names of bindings files. pub fn bindings_filenames(&self) -> Option<&[PathBuf]> { self.bindings.as_deref() } /// Impls section. pub fn impls(&self) -> &BTreeMap> { &self.impls } /// Bibliographies. pub fn bibliographies(&self) -> Option<&[PathBuf]> { self.bibliography.as_deref() } /// Classes.. pub fn classes(&self) -> Option<&[String]> { self.classes.as_deref() } /// Documentclass. pub fn documentclass(&self) -> Option<&str> { self.documentclass.as_deref() } /// Pandoc metadata. pub fn pandoc(&self) -> Option<&HashMap> { if let Some(x) = &self.pandoc { Some(x) } else { None } } } #[cfg(test)] mod test { use super::YamlMetadata; use std::path::{Path, PathBuf}; #[test] fn full_meta() { let meta = YamlMetadata::new( "\ title: Foo Bar date: today classes: [json, text] impls: python: - foo.py - bar.py bibliography: - foo.bib - bar.bib markdowns: - test.md bindings: - foo.yaml - bar.yaml ", ) .unwrap(); assert_eq!(meta.title, "Foo Bar"); assert_eq!(meta.date.unwrap(), "today"); assert_eq!(meta.classes.unwrap(), &["json", "text"]); assert_eq!( meta.bibliography.unwrap(), &[path("foo.bib"), path("bar.bib")] ); assert_eq!(meta.markdowns, vec![Path::new("test.md")]); assert_eq!( meta.bindings.unwrap(), &[path("foo.yaml"), path("bar.yaml")] ); assert!(!meta.impls.is_empty()); for (k, v) in meta.impls.iter() { assert_eq!(k, "python"); assert_eq!(v, &[path("foo.py"), path("bar.py")]); } } fn path(s: &str) -> PathBuf { PathBuf::from(s) } }