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>