From 3bc02dcb023203b257d5e3900e77a8f8fc45c53d Mon Sep 17 00:00:00 2001 From: avitex Date: Thu, 4 Mar 2021 11:54:34 +1100 Subject: [PATCH] more improvements --- Cargo.toml | 2 +- book/macros.tera | 37 +++-- book/observe/event.instance.tera | 14 +- book/observe/event.tera | 8 +- book/react/action.instance.tera | 14 +- book/react/action.tera | 8 +- book/react/stage.instance.tera | 8 +- book/react/stage.tera | 8 +- book/source/intelligence.instance.tera | 11 +- book/source/intelligence.tera | 8 +- book/source/provider.instance.tera | 8 +- book/source/provider.tera | 10 +- book/source/requirement.instance.tera | 8 +- book/source/requirement.tera | 8 +- book/summary.tera | 2 +- registry/config.toml | 6 +- src/domains/common.rs | 31 ++-- src/registry/directory.rs | 18 +-- src/registry/mod.rs | 60 ++++--- src/render/mdbook.rs | 210 ++++++++++++++++--------- src/render/mod.rs | 6 +- 21 files changed, 307 insertions(+), 178 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b18db4..712214f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ publish = false build = "build.rs" [features] -default = ["cli"] +default = ["cli", "mdbook-renderer"] cli = ["structopt", "tokio/macros", "tokio/rt-multi-thread"] mdbook-renderer = ["mdbook", "includedir", "includedir_codegen", "glob", "phf"] diff --git a/book/macros.tera b/book/macros.tera index cff55a7..36c81b8 100644 --- a/book/macros.tera +++ b/book/macros.tera @@ -6,18 +6,26 @@ {% macro references(refs) %} {%- if refs | length == 0 %}No references{% endif -%} {%- for ref in refs -%} -- {{ ref | autolink }} +{%- if ref is url -%} +- <{{ ref }}> +{%- else -%} +- {{ ref }} +{%- endif -%} {% endfor -%} {% endmacro references %} {% macro doc_title(doc) -%} -{{ doc.name }} ([edit]({{ doc.id | domain_id_link(for="edit") }})) +{{ document.name }} ([edit]({{ link_for(for="document", id=document.id, edit=true) }})) {%- endmacro doc_title %} +{% macro model_title(domain, model) -%} +{{ model | capitalize }} +{%- endmacro model_title %} + {% macro doc_details(doc) -%} -| Title | {{ doc.name }} | +| Title | {{ document.name }} | |:---------------------------:|:------------------------| -{{ self::doc_details_next(title="ID", value=doc.id)}} +{{ self::doc_details_next(title="ID", value=document.id) }} {%- endmacro doc_details %} {% macro doc_details_next(title, value) -%} @@ -37,21 +45,21 @@ {%- endmacro doc_details_tags %} {% macro doc_rich_link(doc) -%} -{{ doc.name }} ([{{ doc.id }}]({{ global(key="site_url")}}{{ doc.id | domain_id_link }})) +{{ document.name }} ([{{ document.id }}]({{ link_for(for="document", id=document.id) }})) {%- endmacro doc_rich_link %} -{% macro summary_table(instances) -%} +{% macro summary_table(items) -%} | ID | Name | |:---------------------------:|:------------------------| -{% for item in instances -%} -| [{{ item.id }}]({{ global(key="site_url") }}{{ item.id | domain_id_link }}) | {{ item.name }} | +{% for item in items -%} +| [{{ item.id }}]({{ link_for(for="document", id=item.id) }}) | {{ item.name }} | {% endfor %} {%- endmacro summary_table %} -{% macro summary_list(instances) %} +{% macro summary_list(items) %} {%- set last_domain = "" -%} {%- set last_model = "" -%} -{%- for item in summary -%} +{%- for item in items -%} {%- set id_parts = item.id | domain_id -%} {%- if last_domain != id_parts.domain -%} {%- set_global last_domain = id_parts.domain %} @@ -59,8 +67,13 @@ {%- endif -%} {%- if last_model != id_parts.model -%} {%- set_global last_model = id_parts.model %} -- [{{ id_parts.model | capitalize }}](./{{ item.id | domain_id_link(for="model") }}) +- [{{ id_parts.model | capitalize }}]({{ id_parts.domain ~ "/" ~ id_parts.model ~ ".md" }}) {%- endif %} - - [{{ item.name }}](./{{ item.id | domain_id_link }}) + - [{{ item.name }}]({{ id_parts.domain ~ "/" ~ id_parts.model ~ "/" ~id_parts.instance ~ ".md" }}) {%- endfor -%} {% endmacro summary_list %} + +{% macro registry_content(key, default="") %} +{%- set content = global(key="registry") | get(key="content") %} +{%- if content is containing(key) %}{{ get(key=key) }}{% else %}{{ default }}{% endif -%} +{% endmacro registry_content %} diff --git a/book/observe/event.instance.tera b/book/observe/event.instance.tera index a4a8649..df4a881 100644 --- a/book/observe/event.instance.tera +++ b/book/observe/event.instance.tera @@ -1,20 +1,20 @@ {% import "macros.tera" as macros %} -# {{ macros::doc_title(doc=doc) }} +# {{ macros::doc_title(doc=document) }} -{{ macros::doc_details(doc=doc) }} -{%- if doc.WindowsEvent %} +{{ macros::doc_details(doc=document) }} +{%- if document.WindowsEvent %} {{ macros::doc_details_next(title="Type", value="Windows event") }} {% endif -%} -{{ macros::doc_details_next(title="Description", value=doc.description) }} +{{ macros::doc_details_next(title="Description", value=document.description) }} ## Description -{{ macros::content(content=doc.content) }} +{{ macros::content(content=document.content) }} -{% if doc.WindowsEvent %} +{% if document.WindowsEvent %} ## Samples -{% for sample in doc.WindowsEvent.sample %} +{% for sample in document.WindowsEvent.sample %} ```xml {{ sample.xml }} ``` diff --git a/book/observe/event.tera b/book/observe/event.tera index cd3e493..4fe946f 100644 --- a/book/observe/event.tera +++ b/book/observe/event.tera @@ -1,7 +1,11 @@ {% import "macros.tera" as macros %} -# Event +# {{ macros::model_title(domain="observe", model="event") }} +{% if content %} +{{ content }} +{% else %} Observable items use to detect and respond to threat actor behaviour. +{% endif %} -{{ macros::summary_table(instances=instances) }} +{{ macros::summary_table(items=instances) }} diff --git a/book/react/action.instance.tera b/book/react/action.instance.tera index 57be87d..24debc7 100644 --- a/book/react/action.instance.tera +++ b/book/react/action.instance.tera @@ -1,17 +1,17 @@ {% import "macros.tera" as macros %} -{% set stage = doc.stage | get_doc %} +{% set stage = document(id=document.stage) %} -# {{ macros::doc_title(doc=doc) }} +# {{ macros::doc_title(doc=document) }} -{{ macros::doc_details(doc=doc) }} +{{ macros::doc_details(doc=document) }} {{ macros::doc_details_next(title="Stage", value=macros::doc_rich_link(doc=stage)) }} -{{ macros::doc_details_next(title="Description", value=doc.description) }} -{{ macros::doc_details_tags(tags=doc.tags) }} +{{ macros::doc_details_next(title="Description", value=document.description) }} +{{ macros::doc_details_tags(tags=document.tags) }} ## Description -{{ macros::content(content=doc.content) }} +{{ macros::content(content=document.content) }} ## References -{{ macros::references(refs=doc.references) }} +{{ macros::references(refs=document.references) }} diff --git a/book/react/action.tera b/book/react/action.tera index 359764a..a34c923 100644 --- a/book/react/action.tera +++ b/book/react/action.tera @@ -1,7 +1,11 @@ {% import "macros.tera" as macros %} -# Action +# {{ macros::model_title(domain="react", model="action") }} +{% if content %} +{{ content }} +{% else %} An atomic human action assigned to an response stage. +{% endif %} -{{ macros::summary_table(instances=instances) }} +{{ macros::summary_table(items=instances) }} diff --git a/book/react/stage.instance.tera b/book/react/stage.instance.tera index 2649043..f787768 100644 --- a/book/react/stage.instance.tera +++ b/book/react/stage.instance.tera @@ -1,10 +1,10 @@ {% import "macros.tera" as macros %} -# {{ macros::doc_title(doc=doc) }} +# {{ macros::doc_title(doc=document) }} -{{ macros::doc_details(doc=doc) }} -{{ macros::doc_details_next(title="Description", value=doc.description) }} +{{ macros::doc_details(doc=document) }} +{{ macros::doc_details_next(title="Description", value=document.description) }} ## Description -{{ macros::content(content=doc.content) }} +{{ macros::content(content=document.content) }} diff --git a/book/react/stage.tera b/book/react/stage.tera index 17571fc..90e1b2c 100644 --- a/book/react/stage.tera +++ b/book/react/stage.tera @@ -1,7 +1,11 @@ {% import "macros.tera" as macros %} -# Stage +# {{ macros::model_title(domain="react", model="stage") }} +{% if content %} +{{ content }} +{% else %} Phase of a response to observed threat behaviour. +{% endif %} -{{ macros::summary_table(instances=instances) }} +{{ macros::summary_table(items=instances) }} diff --git a/book/source/intelligence.instance.tera b/book/source/intelligence.instance.tera index bd5038a..1697f88 100644 --- a/book/source/intelligence.instance.tera +++ b/book/source/intelligence.instance.tera @@ -1,14 +1,15 @@ {% import "macros.tera" as macros %} +{% set provider = document(id=document.provider) %} -# {{ macros::doc_title(doc=doc) }} +# {{ macros::doc_title(doc=document) }} -{{ macros::doc_details(doc=doc) }} -{{ macros::doc_details_next(title="Provider", value=doc.provider) }} +{{ macros::doc_details(doc=document) }} +{{ macros::doc_details_next(title="Provider", value=macros::doc_rich_link(doc=provider)) }} ## Description -{{ macros::content(content=doc.content) }} +{{ macros::content(content=document.content) }} ## References -{{ macros::references(refs=doc.references) }} +{{ macros::references(refs=document.references) }} diff --git a/book/source/intelligence.tera b/book/source/intelligence.tera index 8798965..98d271c 100644 --- a/book/source/intelligence.tera +++ b/book/source/intelligence.tera @@ -1,7 +1,11 @@ {% import "macros.tera" as macros %} -# Intelligence +# {{ macros::model_title(domain="source", model="intelligence") }} +{% if content %} +{{ content }} +{% else %} Feed or result of a query used to satisfy intelligence requirements. +{% endif %} -{{ macros::summary_table(instances=instances) }} +{{ macros::summary_table(items=instances) }} diff --git a/book/source/provider.instance.tera b/book/source/provider.instance.tera index 51f8ef8..19bf537 100644 --- a/book/source/provider.instance.tera +++ b/book/source/provider.instance.tera @@ -1,13 +1,13 @@ {% import "macros.tera" as macros %} -# {{ macros::doc_title(doc=doc) }} +# {{ macros::doc_title(doc=document) }} -{{ macros::doc_details(doc=doc) }} +{{ macros::doc_details(doc=document) }} ## Description -{{ macros::content(content=doc.content) }} +{{ macros::content(content=document.content) }} ## References -{{ macros::references(refs=doc.references) }} +{{ macros::references(refs=document.references) }} diff --git a/book/source/provider.tera b/book/source/provider.tera index 12d6a29..ae7cb91 100644 --- a/book/source/provider.tera +++ b/book/source/provider.tera @@ -1,7 +1,13 @@ {% import "macros.tera" as macros %} -# Provider +# {{ macros::model_title(domain="source", model="provider") }} +{% if content %} +{{ content }} +{% else %} An internal or external supplier of intelligence. +{% endif %} -{{ macros::summary_table(instances=instances) }} +{{ macros::registry_content(key="source-provider") }} + +{{ macros::summary_table(items=instances) }} diff --git a/book/source/requirement.instance.tera b/book/source/requirement.instance.tera index 51f8ef8..19bf537 100644 --- a/book/source/requirement.instance.tera +++ b/book/source/requirement.instance.tera @@ -1,13 +1,13 @@ {% import "macros.tera" as macros %} -# {{ macros::doc_title(doc=doc) }} +# {{ macros::doc_title(doc=document) }} -{{ macros::doc_details(doc=doc) }} +{{ macros::doc_details(doc=document) }} ## Description -{{ macros::content(content=doc.content) }} +{{ macros::content(content=document.content) }} ## References -{{ macros::references(refs=doc.references) }} +{{ macros::references(refs=document.references) }} diff --git a/book/source/requirement.tera b/book/source/requirement.tera index a803c1b..2db0141 100644 --- a/book/source/requirement.tera +++ b/book/source/requirement.tera @@ -1,7 +1,11 @@ {% import "macros.tera" as macros %} -# Requirement +# {{ macros::model_title(domain="source", model="requirement") }} +{% if content %} +{{ content }} +{% else %} Collection of intelligence used by actions, detections and mitigations. +{% endif %} -{{ macros::summary_table(instances=instances) }} +{{ macros::summary_table(items=instances) }} diff --git a/book/summary.tera b/book/summary.tera index 11ac248..37047a1 100644 --- a/book/summary.tera +++ b/book/summary.tera @@ -4,4 +4,4 @@ [STORM](./index.md) -{{ macros::summary_list(instances=summary) }} +{{ macros::summary_list(items=summary) }} diff --git a/registry/config.toml b/registry/config.toml index a2ee499..7b094ca 100644 --- a/registry/config.toml +++ b/registry/config.toml @@ -1 +1,5 @@ -edit-link = "https://localhost/registry/{{domain}}/{{model}}/{{model_id}}.md" +[edit] +page = "https://localhost/{{page}}.md" +document = "https://localhost/registry/{{domain}}/{{model}}/{{instance}}.md" + +[content] diff --git a/src/domains/common.rs b/src/domains/common.rs index 87a2791..0552454 100644 --- a/src/domains/common.rs +++ b/src/domains/common.rs @@ -115,12 +115,12 @@ impl DomainModelKind { /////////////////////////////////////////////////////////////////////////////// #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)] -pub struct ModelId { +pub struct Instance { pub major: u16, pub minor: Option, } -impl ModelId { +impl Instance { pub fn new(major: u16) -> Self { Self { major, minor: None } } @@ -130,7 +130,7 @@ impl ModelId { } } -impl fmt::Display for ModelId { +impl fmt::Display for Instance { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:0>5}", self.major)?; if let Some(minor) = self.minor { @@ -140,7 +140,7 @@ impl fmt::Display for ModelId { } } -impl FromStr for ModelId { +impl FromStr for Instance { type Err = ParseIntError; fn from_str(s: &str) -> Result { @@ -153,7 +153,7 @@ impl FromStr for ModelId { } } -impl Serialize for ModelId { +impl Serialize for Instance { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -162,7 +162,7 @@ impl Serialize for ModelId { } } -impl<'de> Deserialize<'de> for ModelId { +impl<'de> Deserialize<'de> for Instance { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -177,12 +177,12 @@ impl<'de> Deserialize<'de> for ModelId { #[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)] pub struct DomainId { pub kind: DomainModelKind, - pub model_id: ModelId, + pub instance: Instance, } impl DomainId { - pub fn new(kind: DomainModelKind, model_id: ModelId) -> Self { - Self { kind, model_id } + pub fn new(kind: DomainModelKind, instance: Instance) -> Self { + Self { kind, instance } } } @@ -196,7 +196,7 @@ impl fmt::Debug for DomainId { impl fmt::Display for DomainId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}{}", <&'static str>::from(&self.kind), self.model_id) + write!(f, "{}{}", <&'static str>::from(&self.kind), self.instance) } } @@ -205,11 +205,12 @@ impl FromStr for DomainId { fn from_str(s: &str) -> Result { if s.len() > 3 && s.is_char_boundary(3) { - let (kind, model_id) = s.split_at(3); - if let (Ok(kind), Ok(model_id)) = - (DomainModelKind::from_str(kind), ModelId::from_str(model_id)) - { - return Ok(Self { kind, model_id }); + let (kind, instance) = s.split_at(3); + if let (Ok(kind), Ok(instance)) = ( + DomainModelKind::from_str(kind), + Instance::from_str(instance), + ) { + return Ok(Self { kind, instance }); } } Err("invalid storm id") diff --git a/src/registry/directory.rs b/src/registry/directory.rs index 19ed37f..3507679 100644 --- a/src/registry/directory.rs +++ b/src/registry/directory.rs @@ -27,7 +27,7 @@ impl DirectoryRegistry { fn domain_id_path(&self, id: DomainId) -> PathBuf { let (domain, model) = id.kind.parts(); self.path - .join(format!("{}/{}/{}.md", domain, model, id.model_id)) + .join(format!("{}/{}/{}.md", domain, model, id.instance)) } } @@ -41,11 +41,11 @@ impl Registry for DirectoryRegistry { .map_err(Into::into) } - async fn get_document(&self, model_id: ModelId) -> Result, RegistryError> + async fn get_document(&self, instance: Instance) -> Result, RegistryError> where M: DomainModel, { - let id = DomainId::new(M::kind(), model_id); + let id = DomainId::new(M::kind(), instance); let path = self.domain_id_path(id); if !path.is_file() { return Err(RegistryError::NotFound(id)); @@ -61,7 +61,7 @@ impl Registry for DirectoryRegistry { let ids = self.get_document_ids(M::kind()).await?; let mut docs = Vec::with_capacity(ids.len()); for id in ids { - docs.push((id, self.get_document(id.model_id).await?)); + docs.push((id, self.get_document(id.instance).await?)); } Ok(docs) } @@ -80,9 +80,9 @@ impl Registry for DirectoryRegistry { let path = entry.path(); if path.is_file() { let filestem = path.file_stem().unwrap().to_string_lossy(); - let model_id: ModelId = filestem.as_ref().parse().unwrap(); - if !model_id.is_example() { - ids.push(DomainId::new(kind, model_id)) + let instance: Instance = filestem.as_ref().parse().unwrap(); + if !instance.is_example() { + ids.push(DomainId::new(kind, instance)) } } } @@ -91,13 +91,13 @@ impl Registry for DirectoryRegistry { async fn put_document( &self, - model_id: ModelId, + instance: Instance, document: Document, ) -> Result<(), RegistryError> where M: DomainModel, { - let path = self.domain_id_path(DomainId::new(M::kind(), model_id)); + let path = self.domain_id_path(DomainId::new(M::kind(), instance)); let document = document.to_string(); fs::write(path, document.as_str()).await?; Ok(()) diff --git a/src/registry/mod.rs b/src/registry/mod.rs index a460ebd..2639478 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -1,5 +1,6 @@ mod directory; +use std::collections::HashMap; use std::fmt::Debug; use std::sync::Arc; @@ -32,28 +33,47 @@ impl From for RegistryError { } } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct EditLinks { + page: Option, + document: Option, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct RegistryConfig { - edit_link: Option, + pub edit: EditLinks, + pub content: HashMap, +} + +fn build_edit_link( + name: &str, + template: Option<&str>, + context: Context, +) -> Result { + let template = + template.ok_or_else(|| RegistryError::Config(anyhow!("link for `{}` not set", name)))?; + Tera::one_off(template, &context, false) + .context("error rendering edit link") + .map_err(RegistryError::Config) } impl RegistryConfig { - pub fn edit_link(&self, domain_id: DomainId) -> Result { - let edit_link = self - .edit_link - .as_deref() - .ok_or_else(|| RegistryError::Config(anyhow!("edit-link option not set")))?; + pub fn document_edit_link(&self, domain_id: DomainId) -> Result { let (domain, model) = domain_id.kind.parts(); let mut context = Context::new(); context.insert("id", &domain_id); context.insert("domain", domain); context.insert("model", model); - context.insert("model_id", &domain_id.model_id); + context.insert("instance", &domain_id.instance); + build_edit_link("document", self.edit.document.as_deref(), context) + } - Tera::one_off(edit_link, &context, false) - .context("error rendering edit link") - .map_err(RegistryError::Config) + pub fn page_edit_link(&self, page: &str) -> Result { + let mut context = Context::new(); + context.insert("page", &page); + build_edit_link("page", self.edit.page.as_deref(), context) } } @@ -61,7 +81,7 @@ impl RegistryConfig { pub trait Registry: Send + Sync { async fn get_config(&self) -> Result, RegistryError>; - async fn get_document(&self, model_id: ModelId) -> Result, RegistryError> + async fn get_document(&self, instance: Instance) -> Result, RegistryError> where M: DomainModel; @@ -74,7 +94,7 @@ pub trait Registry: Send + Sync { async fn put_document( &self, - model_id: ModelId, + instance: Instance, document: Document, ) -> Result<(), RegistryError> where @@ -90,11 +110,11 @@ where (**self).get_config().await } - async fn get_document(&self, model_id: ModelId) -> Result, RegistryError> + async fn get_document(&self, instance: Instance) -> Result, RegistryError> where M: DomainModel, { - (**self).get_document(model_id).await + (**self).get_document(instance).await } async fn get_documents(&self) -> Result)>, RegistryError> @@ -113,13 +133,13 @@ where async fn put_document( &self, - model_id: ModelId, + instance: Instance, document: Document, ) -> Result<(), RegistryError> where M: DomainModel, { - (**self).put_document(model_id, document).await + (**self).put_document(instance, document).await } } @@ -136,12 +156,12 @@ impl Registry for SupportedRegistry { } } - async fn get_document(&self, model_id: ModelId) -> Result, RegistryError> + async fn get_document(&self, instance: Instance) -> Result, RegistryError> where M: DomainModel, { match self { - Self::Directory(r) => r.get_document(model_id).await, + Self::Directory(r) => r.get_document(instance).await, } } @@ -165,14 +185,14 @@ impl Registry for SupportedRegistry { async fn put_document( &self, - model_id: ModelId, + instance: Instance, document: Document, ) -> Result<(), RegistryError> where M: DomainModel, { match self { - Self::Directory(r) => r.put_document(model_id, document).await, + Self::Directory(r) => r.put_document(instance, document).await, } } } diff --git a/src/render/mdbook.rs b/src/render/mdbook.rs index 95c6123..416420b 100644 --- a/src/render/mdbook.rs +++ b/src/render/mdbook.rs @@ -15,7 +15,7 @@ use tokio::fs; include!(concat!(env!("OUT_DIR"), "/mdbook-templates.rs")); use crate::document::Document; -use crate::domains::common::{DomainId, DomainModel, ModelId}; +use crate::domains::common::{DomainId, DomainModel, Instance}; use crate::domains::GenericDocument; use crate::registry::{Registry, RegistryConfig}; @@ -52,11 +52,11 @@ pub struct MDBookEngine { enum Inner { Start { src: PathBuf, - global: Map, + site_url: Option, }, LoadAndRender { src: PathBuf, - global: Map, + site_url: Option, templates: Tera, summary: Vec, documents: DocumentMap, @@ -66,17 +66,17 @@ enum Inner { impl MDBookEngine { pub fn new(root: &Path, config: &MDBookConfig) -> Self { - let mut global = Map::new(); - if let Ok(Some(HtmlConfig { site_url, .. })) = config.get_deserialized_opt("output.html") { - global.insert( - "site_url".to_owned(), - site_url.as_deref().unwrap_or("/").to_owned().into(), - ); - } + let site_url = if let Ok(Some(HtmlConfig { site_url, .. })) = + config.get_deserialized_opt("output.html") + { + site_url + } else { + None + }; Self { inner: Inner::Start { src: root.join(&config.book.src), - global, + site_url, }, } } @@ -89,13 +89,13 @@ impl Engine for MDBookEngine { where R: Registry, { - if let Inner::Start { src, global } = mem::replace(&mut self.inner, Inner::Finish) { + if let Inner::Start { src, site_url } = mem::replace(&mut self.inner, Inner::Finish) { let templates = load_templates(&src).await?; let documents = HashMap::new(); let summary = Vec::new(); self.inner = Inner::LoadAndRender { src, - global, + site_url, templates, documents, summary, @@ -106,7 +106,7 @@ impl Engine for MDBookEngine { async fn push_document( &mut self, - model_id: ModelId, + instance: Instance, doc: GenericDocument, _registry: &R, ) -> Result<(), RendererError> @@ -124,7 +124,7 @@ impl Engine for MDBookEngine { match doc { $( GenericDocument::$kind(doc) => { - save_domain_model(summary, documents, model_id, &doc); + save_domain_model(summary, documents, instance, &doc); } )+ } @@ -160,7 +160,7 @@ impl Engine for MDBookEngine { { if let Inner::LoadAndRender { src, - global, + site_url, mut templates, documents, summary, @@ -169,8 +169,13 @@ impl Engine for MDBookEngine { let summary = Arc::new(summary); let documents = Arc::new(documents); let registry_config = registry.get_config().await?; - register_filters(&mut templates, documents.clone(), registry_config, global); - render_indexes(&templates, &src, &summary).await?; + register_filters( + &mut templates, + documents.clone(), + registry_config.clone(), + site_url, + ); + render_indexes(&templates, &src, &summary, registry_config).await?; render_summary(&templates, &src, &summary).await?; render_documents(&templates, &src, documents).await?; } @@ -206,12 +211,12 @@ async fn load_templates(src: &Path) -> Result { fn save_domain_model( summary: &mut Vec, documents: &mut DocumentMap, - model_id: ModelId, + instance: Instance, doc: &Document, ) where M: DomainModel, { - let id = DomainId::new(M::kind(), model_id); + let id = DomainId::new(M::kind(), instance); if let Value::Object(mut document) = tera::to_value(&doc).unwrap() { document.insert("id".to_owned(), tera::to_value(&id).unwrap()); documents.insert(id, document); @@ -233,10 +238,10 @@ async fn render_documents( let template_path = format!("{}.instance.tera", model_path); let output_path = src .join(model_path) - .join(id.model_id.to_string()) + .join(id.instance.to_string()) .with_extension("md"); let mut context = Context::new(); - context.insert("doc", &document); + context.insert("document", &document); let contents = templates.render(&template_path, &context)?; fs::write(output_path, contents).await?; } @@ -250,7 +255,6 @@ async fn render_summary( ) -> Result<(), MDBookEngineError> { let mut context = Context::new(); context.insert("summary", summary); - let contents = templates.render("summary.tera", &context)?; let summary_path = src.join("SUMMARY.md"); fs::write(summary_path, contents).await?; @@ -261,6 +265,7 @@ async fn render_indexes( templates: &Tera, src: &Path, summary: &[SummaryItem], + registry_config: Arc, ) -> Result<(), MDBookEngineError> { let mut last_i = 0; let mut last_item = if let Some(item) = summary.get(0) { @@ -282,7 +287,11 @@ async fn render_indexes( fs::create_dir(&model_dir).await?; } + let content = registry_config + .content + .get(&format!("{}-{}", domain, model)); let mut context = Context::new(); + context.insert("content", &content); context.insert("instances", &summary[last_i..i]); let model_index = model_dir.with_extension("md"); @@ -302,83 +311,138 @@ fn register_filters( tera: &mut Tera, documents: Arc, registry_config: Arc, - global: Map, + site_url: Option, ) { use tera::{try_get_value, Error}; + let site_url = site_url.as_deref().unwrap_or("/").to_owned(); + + let mut global: HashMap<&str, Value> = HashMap::new(); + global.insert("site_url", tera::to_value(&site_url).unwrap()); + global.insert("registry", tera::to_value(&*registry_config).unwrap()); + tera.register_function("global", move |args: &HashMap| { - if let Some(Value::String(key)) = args.get("key") { - Ok(global.get(key).cloned().unwrap_or(Value::Null)) + if let Some(Value::String(full_key)) = args.get("key") { + let mut keys = full_key.split('.'); + let mut value = global.get(keys.next().unwrap()); + for key in keys { + match value { + Some(Value::Object(map)) => { + value = map.get(key); + } + None => { + return Err(Error::msg(format_args!( + "no key `{}` section of `{}`", + key, full_key + ))) + } + Some(_) => { + return Err(Error::msg(format_args!( + "value cannot be indexed with `{}`", + key + ))) + } + } + } + Ok(value.cloned().unwrap_or(Value::Null)) } else { - Err(Error::msg("expected `key` for `global` function")) + Err(Error::msg("expected valid `key` arg for `global` function")) } }); - fn parse_domain_id(value: &Value) -> Result { - let id = try_get_value!("link", "value", String, value); - id.parse() - .map_err(|_| Error::msg(format_args!("failed to parse domain id `{}`", id))) - } - - tera.register_filter( - "domain_id_link", - move |value: &Value, args: &HashMap| { - let id = parse_domain_id(value)?; - let (domain, model) = id.kind.parts(); - let link = match args.get("for").and_then(|v| v.as_str()) { - None => format!("{}/{}/{}.md", domain, model, id.model_id), - Some("model") => format!("{}/{}.md", domain, model), - Some("edit") => registry_config.edit_link(id).map_err(Error::msg)?, - Some(_) => { + tera.register_function("link_for", move |args: &HashMap| { + let link_for = if let Some(Value::String(link_for)) = args.get("for") { + link_for + } else { + return Err(Error::msg( + "expected string `for` arg for `link_for` function", + )); + }; + let edit = match args.get("edit") { + Some(Value::Bool(edit)) => *edit, + Some(_) => return Err(Error::msg("`edit` arg for `link_for` must be a boolean")), + None => false, + }; + let link = match link_for.as_str() { + "document" => match args.get("id") { + Some(value) => { + let id = parse_domain_id("link_for", value)?; + if edit { + registry_config.document_edit_link(id).map_err(Error::msg)? + } else { + let (domain, model) = id.kind.parts(); + format!("{}{}/{}/{}.md", site_url, domain, model, id.instance) + } + } + _ => { return Err(Error::msg( - "domain_id_link `for` arg can only be `model|edit`", + "`link_for(for=\"document\")` `` expects `id` arg", )) } - }; - Ok(Value::String(link)) - }, - ); + }, + "page" => match args.get("path") { + Some(Value::String(page)) if edit => { + registry_config.page_edit_link(page).map_err(Error::msg)? + } + Some(Value::String(page)) => { + format!("{}{}.md", site_url, page) + } + _ => return Err(Error::msg("`link_for(for=\"path\")` `` expects `path` arg")), + }, + _ => { + return Err(Error::msg( + "`link_for` `for` arg can only be `document|page`", + )) + } + }; + Ok(Value::String(link)) + }); - tera.register_filter( - "domain_id", - move |value: &Value, _: &HashMap| { - let id = parse_domain_id(value)?; - let (domain, model) = id.kind.parts(); - let mut parts = Map::with_capacity(4); - parts.insert("domain".to_owned(), domain.into()); - parts.insert("model".to_owned(), model.into()); - parts.insert("model_id".to_owned(), id.model_id.to_string().into()); - Ok(Value::Object(parts)) - }, - ); - - tera.register_filter( - "get_doc", - move |value: &Value, _: &HashMap| { - let id = parse_domain_id(value)?; + tera.register_function("document", move |args: &HashMap| { + if let Some(value) = args.get("id") { + let id = parse_domain_id("document", value)?; documents .get(&id) .cloned() .map(Value::Object) .ok_or_else(|| Error::msg(format_args!("unknown document id {}", id))) + } else { + Err(Error::msg("expected `id` arg for `document` function")) + } + }); + + fn parse_domain_id(within: &str, value: &Value) -> Result { + let id = try_get_value!(within, "id", String, value); + id.parse() + .map_err(|_| Error::msg(format_args!("failed to parse domain id `{}`", id))) + } + + tera.register_filter( + "domain_id", + move |value: &Value, _: &HashMap| { + let id = parse_domain_id("domain_id", value)?; + let (domain, model) = id.kind.parts(); + let mut parts = Map::with_capacity(4); + parts.insert("domain".to_owned(), domain.into()); + parts.insert("model".to_owned(), model.into()); + parts.insert("instance".to_owned(), id.instance.to_string().into()); + Ok(Value::Object(parts)) }, ); - tera.register_filter("autolink", |value: &Value, _: &HashMap| { + tera.register_tester("url", |value: Option<&Value>, _: &[Value]| { lazy_static! { static ref URL: Regex = Regex::new( r"(?ix) - \b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))) + ^\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))$ ", ) .unwrap(); } - let text = try_get_value!("autolink", "value", String, value); - let text = if text.is_empty() { - text + if let Some(Value::String(text)) = value { + Ok(URL.is_match(text)) } else { - URL.replace_all(text.as_str(), "[$0]($0)").into_owned() - }; - Ok(Value::String(text)) + Err(Error::msg("expected string for `url` test")) + } }); } diff --git a/src/render/mod.rs b/src/render/mod.rs index 81da18e..48d4444 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use thiserror::Error; use crate::document::Document; -use crate::domains::common::{DomainId, DomainModel, ModelId}; +use crate::domains::common::{DomainId, DomainModel, Instance}; use crate::domains::GenericDocument; use crate::registry::{Registry, RegistryError}; @@ -39,7 +39,7 @@ where $( for (id, doc) in self.get_documents().await? { self.engine - .push_document(id.model_id, GenericDocument::$kind(doc), &self.registry) + .push_document(id.instance, GenericDocument::$kind(doc), &self.registry) .await?; } )* @@ -92,7 +92,7 @@ pub trait Engine { async fn push_document( &mut self, - id: ModelId, + id: Instance, doc: GenericDocument, registry: &R, ) -> Result<(), RendererError>