mod functions;
use functions::{rdf_asset, rdf_link};
use language_tags::LanguageTag;
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
use gofer;
use iref::IriBuf;
use minijinja::{Value, context};
use taganak_core::graphs::Graph;
use taganak_framework::prelude::{StreamExt as _, TryStreamExt};
use taganak_orm::{
GraphORM, GraphORMError, GraphORMMeta,
re::{BasicPrefixMap, GraphError, GraphView, PrefixMap, Term},
};
use tokio::{
fs::{File, create_dir_all},
io::AsyncWriteExt,
task::spawn_blocking,
};
use tracing::{debug, info, warn};
use crate::{Params, env::environment, res::object::GraphObject};
#[derive(Debug, PartialEq, GraphORM)]
#[rdf(
prefix(ns = "trinja", iri = "https://trinja.taganak.net/vocab/"),
prefix(ns = "dc", iri = "http://purl.org/dc/elements/1.1/"),
prefix(ns = "sh", iri = "http://www.w3.org/ns/shacl#"),
default(prefix = "trinja")
)]
pub struct Site {
#[rdf(predicate = "dc:title")]
pub title: Option<String>,
#[rdf(predicate = "trinja:asset")]
assets: Option<HashSet<Term>>,
#[rdf(predicate = "trinja:page")]
pages: Option<HashSet<Term>>,
#[rdf(predicate = "trinja:genericPage")]
generic_pages: Option<HashSet<Term>>,
#[rdf(predicate = "trinja:defaultTemplate")]
pub default_template: Option<Term>,
#[rdf(predicate = "sh:declare")]
pub prefix_declarations: HashSet<PrefixDeclaration>,
}
#[derive(Debug, Clone, PartialEq, Hash, Eq, GraphORM)]
#[rdf(
prefix(ns = "trinja", iri = "https://trinja.taganak.net/vocab/"),
prefix(ns = "dc", iri = "http://purl.org/dc/elements/1.1/"),
default(prefix = "trinja")
)]
pub struct Page {
output: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Hash, Eq, GraphORM)]
#[rdf(
prefix(ns = "trinja", iri = "https://trinja.taganak.net/vocab/"),
prefix(ns = "dc", iri = "http://purl.org/dc/elements/1.1/"),
prefix(ns = "sh", iri = "http://www.w3.org/ns/shacl#"),
default(prefix = "trinja")
)]
pub struct GenericPage {
#[rdf(predicate = "sh:targetClass")]
target_class: Option<Term>,
#[rdf(predicate = "trinja:outputTemplate")]
output_template: Option<String>,
}
#[derive(Debug, PartialEq, Hash, Eq, GraphORM)]
#[rdf(
prefix(ns = "trinja", iri = "https://trinja.taganak.net/vocab/"),
prefix(ns = "dc", iri = "http://purl.org/dc/elements/1.1/"),
default(prefix = "trinja")
)]
pub struct Asset {
source: Option<Term>,
output: Option<String>,
}
#[derive(Debug, PartialEq, Hash, Eq, GraphORM)]
#[rdf(
prefix(ns = "sh", iri = "http://www.w3.org/ns/shacl#"),
default(prefix = "sh")
)]
pub struct PrefixDeclaration {
prefix: String,
namespace: IriBuf,
}
impl GenericPage {
fn output_for<G: Graph + std::fmt::Debug + Sync + 'static>(
&self,
subject: Arc<Term>,
params: &Params<'static, G>,
) -> Option<String> {
if let Some(template) = &self.output_template {
let env = environment(params.clone().environment(None), None);
let params = params.clone().environment(Some(env)).to_with_env().unwrap();
Some(params
.environment
.read()
.unwrap()
.render_str(template, context! {
this => Value::from_object(GraphObject::new(params.clone().base(Some(subject.clone())).to_params())),
})
.unwrap())
} else {
None
}
}
async fn into_pages<G: Graph + std::fmt::Debug + Sync + 'static>(
self,
params: &Params<'static, G>,
) -> Result<Vec<GraphORMMeta<Page>>, super::Error> {
let rdf_type = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
.try_into()
.unwrap();
let mut pages = Vec::new();
if let Some(target_class) = &self.target_class {
info!(
"Collecting subjects for generic page target class {}",
target_class
);
let graph = params.graph.read().unwrap();
let view = graph.view().await;
let mut target_subjects = view
.subjects(Some(&rdf_type), Some(target_class), None)
.await?;
while let Some(target_subject) = target_subjects.next().await {
let target_subject: Arc<Term> = target_subject.unwrap();
let target_subject_clone = target_subject.clone();
let params_clone = params.clone();
let self_clone = self.clone();
let page = Page {
output: spawn_blocking(move || {
self_clone.output_for(target_subject_clone, ¶ms_clone)
})
.await
.unwrap(),
};
let meta = GraphORMMeta::new(page).with_subject(target_subject);
pages.push(meta);
}
} else {
warn!("Generic page does not have a target");
}
Ok(pages)
}
}
impl Site {
pub async fn make_params<G>(
graph: G,
subject: IriBuf,
language: Option<LanguageTag>,
) -> Params<'static, G>
where
G: Graph,
{
let subject = Term::NamedNode(subject);
let base = Arc::new(Term::NamedNode(subject.to_iri().unwrap().clone()));
let mut pm = BasicPrefixMap::new();
for (ns, iri) in [
("dc", "http://purl.org/dc/elements/1.1/"),
("foaf", "http://xmlns.com/foaf/0.1/"),
("doap", "http://usefulinc.com/ns/doap#"),
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
("xsd", "http://www.w3.org/2001/XMLSchema#"),
("schema", "http://schema.org/"),
("trinja", "https://trinja.taganak.net/vocab/"),
] {
pm.set(ns.to_string(), iri.to_string())
.expect("static data");
}
Params {
base: Some(base),
graph: Arc::new(RwLock::new(graph)),
prefix_map: Arc::new(pm),
language,
environment: None,
}
}
pub async fn new<G>(mut params: Params<'_, G>) -> Result<(Self, Params<'_, G>), GraphORMError>
where
G: Graph,
{
let site = {
let graph = params.graph.read().unwrap();
let view = graph.view().await;
Site::deserialize(view, params.base.as_ref().unwrap()).await?
};
let pm = Arc::make_mut(&mut params.prefix_map);
for decl in &site.prefix_declarations {
pm.set(decl.prefix.clone(), decl.namespace.to_string())
.unwrap();
}
Ok((site.into_deref(), params))
}
pub async fn assets<G>(&self, params: &Params<'_, G>) -> Result<HashSet<Term>, GraphError>
where
G: Graph,
{
Ok(match self.assets {
Some(ref terms) => {
debug!("Assets explicitly linked to site");
terms.clone()
}
None => {
warn!("Assets not defined on site; searching all trinja:Asset on graph");
let graph = params.graph.read().unwrap();
let view = graph.view().await;
view.subjects(
Some(
&"<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
.try_into()
.unwrap(),
),
Some(
&"<https://trinja.taganak.net/vocab/Asset>"
.try_into()
.unwrap(),
),
None,
)
.await?
.map_ok(|t| (*t).clone())
.try_collect()
.await?
}
})
}
pub async fn static_pages<G>(&self, params: &Params<'_, G>) -> Result<HashSet<Term>, GraphError>
where
G: Graph,
{
let rdf_type = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
.try_into()
.unwrap();
Ok(match self.pages {
Some(ref terms) => {
debug!("Pages explicitly defined on site");
terms.clone()
}
None => {
warn!("Pages not defined on site; searching all trinja:Page on graph");
let graph = params.graph.read().unwrap();
let view = graph.view().await;
view.subjects(
Some(&rdf_type),
Some(
&"<https://trinja.taganak.net/vocab/Page>"
.try_into()
.unwrap(),
),
None,
)
.await?
.map_ok(|t| (*t).clone())
.try_collect()
.await?
}
})
}
pub async fn generic_pages<G>(
&self,
params: &Params<'_, G>,
) -> Result<HashSet<Term>, GraphError>
where
G: Graph,
{
let rdf_type = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
.try_into()
.unwrap();
Ok(match self.generic_pages {
Some(ref terms) => {
debug!("Generic pages explicitly defined on site");
terms.clone()
}
None => {
warn!(
"generic pages not defined on site; searching all trinja:GenericPage on graph"
);
let graph = params.graph.read().unwrap();
let view = graph.view().await;
view.subjects(
Some(&rdf_type),
Some(
&"<https://trinja.taganak.net/vocab/GenericPage>"
.try_into()
.unwrap(),
),
None,
)
.await?
.map_ok(|t| (*t).clone())
.try_collect()
.await?
}
})
}
pub async fn pages_map<G>(
&self,
params: &Params<'static, G>,
) -> Result<
HashMap<Arc<Term>, (GraphORMMeta<Page>, Option<GraphORMMeta<GenericPage>>)>,
GraphError,
>
where
G: Graph + std::fmt::Debug + Sync + 'static,
{
let mut pages_map = HashMap::new();
let graph = params.graph.read().unwrap();
let view = graph.view().await;
for subject in &self.static_pages(params).await? {
pages_map.insert(
Arc::new(subject.clone()),
(
Page::deserialize(view.clone(), subject).await.unwrap(),
None,
),
);
}
for subject in &self.generic_pages(params).await? {
let generic_page = GenericPage::deserialize(view.clone(), subject)
.await
.unwrap();
for page in generic_page
.clone()
.into_deref()
.into_pages(params)
.await
.unwrap()
{
pages_map.insert(
page.subject().unwrap().clone(),
(page, Some(generic_page.clone())),
);
}
}
Ok(pages_map)
}
pub async fn build<G>(
&self,
param: Params<'static, G>,
output: &Path,
) -> Result<(), super::Error>
where
G: Graph + core::fmt::Debug + Sync + 'static,
{
create_dir_all(&output).await?;
info!(
"Building site, starting from {}, to {}",
param.base.as_ref().unwrap(),
output.to_str().unwrap()
);
self.build_assets(¶m, output).await?;
self.build_pages(¶m, output).await?;
Ok(())
}
async fn build_pages<G>(
&self,
params: &Params<'static, G>,
output: &Path,
) -> Result<(), super::Error>
where
G: Graph + core::fmt::Debug + Sync + 'static,
{
let pages_map = self.pages_map(params).await.unwrap();
for (subject, (page_meta, generic_page_meta)) in &pages_map {
let mut out_rel = PathBuf::from(match &page_meta.output {
Some(output) => {
debug!(?output, "Page has explicit output path");
output.clone()
}
None => {
debug!("Calculating output path relative to site base");
let page_iri = subject.to_iri().cloned().unwrap();
page_iri
.relative_to(¶ms.base.as_ref().unwrap().to_iri().unwrap())
.to_string()
}
});
if out_rel.extension().is_none() {
out_rel.set_extension("html");
}
let out = output.join(out_rel);
if let Some(generic_page) = &generic_page_meta {
info!(
"Rendering generic page {} instance {} into {}",
generic_page.subject().unwrap(),
subject,
out.to_str().unwrap()
);
} else {
info!(
"Rendering static page {} into {}",
subject,
out.to_str().unwrap()
);
}
let env = environment(
params.clone().environment(None).base(Some(subject.clone())),
self.default_template.clone(),
);
let params = params.clone().environment(Some(env)).to_with_env().unwrap();
params.environment.write().unwrap().add_global(
"site",
Value::from_object(GraphObject::new(params.clone().to_params())),
);
params.environment.write().unwrap().add_global(
"rdf_asset",
Value::from_object(rdf_asset::RdfAssetFn::new(
params.clone().to_params().environment(None),
page_meta.clone(),
)),
);
params.environment.write().unwrap().add_global(
"rdf_link",
Value::from_object(rdf_link::RdfLinkFn::new(
params.clone().to_params().environment(None),
page_meta.clone(),
pages_map.clone(),
)),
);
let obj = GraphObject::new(params.base(Some(subject.clone())).to_params());
let rendered = spawn_blocking(move || obj.into_rendered())
.await
.unwrap()
.unwrap();
create_dir_all(out.parent().unwrap()).await?;
let mut out_file = File::options()
.create(true)
.truncate(true)
.write(true)
.open(out)
.await?;
out_file.write_all(rendered.as_bytes()).await?;
}
Ok(())
}
async fn build_assets<G>(
&self,
params: &Params<'_, G>,
output: &Path,
) -> Result<(), super::Error>
where
G: Graph,
{
let graph = params.graph.read().unwrap();
let view = graph.view().await;
for subject in self.assets(params).await? {
let asset = Asset::deserialize(view.clone(), &subject)
.await
.unwrap()
.into_deref();
let out_rel = PathBuf::from(match asset.output {
Some(output) => {
debug!(?output, "Asset has explicit output path");
output.clone()
}
None => {
debug!("Calculating output path relative to site base");
let page_iri = subject.to_iri().cloned().unwrap();
page_iri
.relative_to(¶ms.base.as_ref().unwrap().to_iri().unwrap())
.to_string()
}
});
let out = output.join(out_rel);
let source = match asset.source {
Some(iri) => iri.to_iri().unwrap().clone(),
None => subject.to_iri().unwrap().clone(),
};
debug!("Asset {} has source {}", subject, source);
info!(
"Storing asset from {} into {}",
source,
out.to_str().unwrap()
);
spawn_blocking(move || {
let mut reader = gofer::open(&source).unwrap();
std::fs::create_dir_all(out.parent().unwrap()).unwrap();
let mut out_file = std::fs::File::options()
.create(true)
.truncate(true)
.write(true)
.open(out)
.unwrap();
let bytes = std::io::copy(&mut reader, &mut out_file).unwrap();
debug!(?subject, "Stored asset with {} bytes length", bytes);
})
.await
.unwrap();
}
Ok(())
}
}