more improvements

This commit is contained in:
avitex 2021-03-04 11:54:34 +11:00
parent 738285dced
commit 3bc02dcb02
Signed by: avitex
GPG Key ID: 38C76CBF3749D62C
21 changed files with 307 additions and 178 deletions

View File

@ -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"]

View File

@ -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 }} <small>([edit]({{ doc.id | domain_id_link(for="edit") }}))</small>
{{ document.name }} <small>([edit]({{ link_for(for="document", id=document.id, edit=true) }}))</small>
{%- 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 %}

View File

@ -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 }}
```

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -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) }}

View File

@ -4,4 +4,4 @@
[STORM](./index.md)
{{ macros::summary_list(instances=summary) }}
{{ macros::summary_list(items=summary) }}

View File

@ -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]

View File

@ -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<NonZeroU16>,
}
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<Self, Self::Err> {
@ -153,7 +153,7 @@ impl FromStr for ModelId {
}
}
impl Serialize for ModelId {
impl Serialize for Instance {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<Self, Self::Err> {
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")

View File

@ -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<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, 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<M>(
&self,
model_id: ModelId,
instance: Instance,
document: Document<M>,
) -> 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(())

View File

@ -1,5 +1,6 @@
mod directory;
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
@ -32,28 +33,47 @@ impl From<std::io::Error> for RegistryError {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct EditLinks {
page: Option<String>,
document: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct RegistryConfig {
edit_link: Option<String>,
pub edit: EditLinks,
pub content: HashMap<String, String>,
}
fn build_edit_link(
name: &str,
template: Option<&str>,
context: Context,
) -> Result<String, RegistryError> {
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<String, RegistryError> {
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<String, RegistryError> {
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<String, RegistryError> {
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<Arc<RegistryConfig>, RegistryError>;
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, RegistryError>
where
M: DomainModel;
@ -74,7 +94,7 @@ pub trait Registry: Send + Sync {
async fn put_document<M>(
&self,
model_id: ModelId,
instance: Instance,
document: Document<M>,
) -> Result<(), RegistryError>
where
@ -90,11 +110,11 @@ where
(**self).get_config().await
}
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, RegistryError>
where
M: DomainModel,
{
(**self).get_document(model_id).await
(**self).get_document(instance).await
}
async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, RegistryError>
@ -113,13 +133,13 @@ where
async fn put_document<M>(
&self,
model_id: ModelId,
instance: Instance,
document: Document<M>,
) -> 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<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, 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<M>(
&self,
model_id: ModelId,
instance: Instance,
document: Document<M>,
) -> 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,
}
}
}

View File

@ -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<String, Value>,
site_url: Option<String>,
},
LoadAndRender {
src: PathBuf,
global: Map<String, Value>,
site_url: Option<String>,
templates: Tera,
summary: Vec<SummaryItem>,
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<R>(
&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<Tera, MDBookEngineError> {
fn save_domain_model<M>(
summary: &mut Vec<SummaryItem>,
documents: &mut DocumentMap,
model_id: ModelId,
instance: Instance,
doc: &Document<M>,
) 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<RegistryConfig>,
) -> 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<DocumentMap>,
registry_config: Arc<RegistryConfig>,
global: Map<String, Value>,
site_url: Option<String>,
) {
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<String, Value>| {
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<DomainId, Error> {
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<String, Value>| {
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<String, Value>| {
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<String, Value>| {
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<String, Value>| {
let id = parse_domain_id(value)?;
tera.register_function("document", move |args: &HashMap<String, Value>| {
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<DomainId, Error> {
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<String, Value>| {
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<String, Value>| {
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"))
}
});
}

View File

@ -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<R>(
&mut self,
id: ModelId,
id: Instance,
doc: GenericDocument,
registry: &R,
) -> Result<(), RendererError>