use chrono::{DateTime, Utc}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use serde::Deserialize; use crate::{ config::Config, state::{FileId, TomlError, Treehouse, toml_error_to_diagnostic}, tree::attributes::{Picture, timestamp_from_id}, }; #[derive(Debug, Clone)] pub struct Doc { pub attributes: Attributes, pub text: String, } #[derive(Debug, Clone, Default, Deserialize)] pub struct Attributes { /// Template to use for generating the page. /// Defaults to `_tree.hbs`. #[serde(default)] pub template: Option, /// The unique ID of the doc. /// Required to appear in feeds. /// /// - New format: `doc?{date}-{name}`, where `{date}` is a `YYYY-MM-DD` date, and `{name}` is /// the filename of the document (or otherwise a unique name which doesn't conflict with docs /// made that day.) /// - Old format: `b?{ulid}`, where `{ulid}` is a ULID. /// This follows the format of branches. #[serde(default)] pub id: String, /// Title of the page. /// The only necessary field. /// Unlike tree pages, doc pages always have titles. pub title: String, /// Tags assigned to the document. /// Generally, you want to assign public documents to #all for them to show up on the front page. #[serde(default)] pub tags: Vec, /// Timestamp when the document was last updated. /// Required for inclusion in feeds. /// For pages with old style IDs, this is inferred from the ID. #[serde(default)] pub updated: Option>, /// ID of picture attached to the page, to be used as a thumbnail. #[serde(default)] pub thumbnail: Option, /// Additional scripts to load into to the page. /// These are relative to the /static/js directory. #[serde(default)] pub scripts: Vec, /// Additional styles to load into to the page. /// These are relative to the /static/css directory. #[serde(default)] pub styles: Vec, /// If not `None`, the page will get an additional 'feed' field in template data, containing /// a feed of pages with the specified tag. #[serde(default)] pub include_feed: Option, } #[derive(Debug, Clone, Deserialize)] pub struct IncludeFeed { /// The tag to look for. pub tag: String, /// The title of the feed shown on the page. pub title: String, } impl Doc { pub fn parse( treehouse: &mut Treehouse, config: &Config, file_id: FileId, ) -> (Doc, Vec>) { let mut diagnostics = vec![]; let source = treehouse.source(file_id).input(); let (front_matter, text) = source.split_once("+++").unwrap_or(("", source)); let attributes_span = 0..front_matter.len(); let mut attributes: Attributes = toml_edit::de::from_str(front_matter).unwrap_or_else(|error| { diagnostics.push(toml_error_to_diagnostic(TomlError { message: error.message().to_owned(), span: error.span(), file_id, input_range: attributes_span.clone(), })); Attributes::default() }); // Infer attributes if let Some(branch_id) = attributes.id.strip_prefix("b?") && let Some(timestamp) = timestamp_from_id(branch_id) { attributes.updated = Some(timestamp); } // Emit warnings if !attributes.tags.is_empty() { if attributes.id.is_empty() { diagnostics.push( Diagnostic::warning() .with_code("attr") .with_message("doc is tagged but missing id attribute") .with_labels(vec![Label::primary(file_id, attributes_span.clone())]) .with_notes(vec!["id is required for showing up in feeds".into()]), ); } else if attributes.updated.is_none() { diagnostics.push( Diagnostic::warning() .with_code("attr") .with_message("doc is tagged but missing updated attribute") .with_labels(vec![Label::primary(file_id, attributes_span.clone())]) .with_notes(vec![ "updated attribute is required for showing up in feeds".into(), ]), ); } for tag in &attributes.tags { if !config.feed.tags.contains(tag) { diagnostics.push( Diagnostic::warning() .with_code("attr") .with_message(format!("doc has unregistered tag `{tag}`")) .with_labels(vec![Label::primary(file_id, attributes_span.clone())]), ); } } } ( Doc { attributes, text: text.to_owned(), }, diagnostics, ) } }