154 lines
5.1 KiB
Rust
154 lines
5.1 KiB
Rust
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<String>,
|
|
|
|
/// 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<String>,
|
|
|
|
/// 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<DateTime<Utc>>,
|
|
|
|
/// ID of picture attached to the page, to be used as a thumbnail.
|
|
#[serde(default)]
|
|
pub thumbnail: Option<Picture>,
|
|
|
|
/// Additional scripts to load into to the page.
|
|
/// These are relative to the /static/js directory.
|
|
#[serde(default)]
|
|
pub scripts: Vec<String>,
|
|
|
|
/// Additional styles to load into to the page.
|
|
/// These are relative to the /static/css directory.
|
|
#[serde(default)]
|
|
pub styles: Vec<String>,
|
|
|
|
/// 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<IncludeFeed>,
|
|
}
|
|
|
|
#[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<Diagnostic<FileId>>) {
|
|
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,
|
|
)
|
|
}
|
|
}
|