mod attrs;
mod slots;
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::PathBuf;
use serde_json::Value;
use crate::context::{CanonicalContext, ContextData, ContextScope, ContextTree};
use crate::discover::{PageKind, PageSource};
use crate::error::{Error, Result};
use crate::fragment::{self, FragmentRegistry};
use crate::funnel::{self, BindDecl, BindSource, DataSource};
use crate::i18n;
use crate::manifest::ManifestMeta;
use crate::parse::{Document, EachDirective, Element, Node, SlotKind};
use crate::render::{Op, PageRenderer, RenderPlan};
use crate::scope;
use crate::tokens::{
contains_forwarded_class_marker, forwarded_class_base, missing_fragment_message, ATTR_CLASS,
DATA_BIND, DATA_EACH,
};
use crate::{AliasOptions, FormsOptions};
pub(crate) use attrs::expand_template;
pub use attrs::fill_attr_templates_in_nodes;
pub use slots::{clear_remaining_named_slots, fill_projection_slots, SlotProjection};
#[derive(Debug, Clone)]
struct FragmentMount {
id: String,
class: Option<String>,
children: Vec<Node>,
each: Option<EachDirective>,
}
fn html_element(doc: &Document) -> Option<&Element> {
doc.children.iter().find_map(|n| match n {
Node::Element(el) if el.name.eq_ignore_ascii_case("html") => Some(el),
_ => None,
})
}
#[must_use]
pub fn html_collection_id(doc: &Document) -> Option<String> {
if let Some(raw) = html_bind_raw(doc) {
if let Ok(BindDecl::Named(name)) = funnel::parse_bind_decl(Some(raw)) {
return Some(name);
}
}
let ids = funnel::data_link_ids(doc);
if ids.len() == 1 {
return Some(ids[0].clone());
}
None
}
pub fn require_collection_id(doc: &Document, source: BindSource<'_>) -> Result<String> {
if let Some(id) = html_collection_id(doc) {
return Ok(id);
}
let ids = funnel::data_link_ids(doc);
let message = match ids.len() {
0 => "this collection route has no data source; add <link rel=\"statica/data\" href=\"...\" id=\"items\"> and bind it from <html data-bind=\"items\"> or <html data-bind=\"{item}\">",
_ => "this collection route has multiple data sources; set <html data-bind=\"id\"> to the collection data id that should drive the route",
};
Err(Error::at(
source.file,
source.source,
&["<html", "data-bind"],
message,
))
}
fn html_bind_raw(doc: &Document) -> Option<&str> {
html_element(doc).and_then(Element::bind_directive)
}
pub fn collection_needles(id: &str) -> [String; 2] {
[
format!("{DATA_BIND}=\"{id}\""),
format!("{DATA_BIND}='{id}'"),
]
}
fn parse_html_bind_decl(doc: &Document) -> Result<BindDecl> {
let raw = html_bind_raw(doc);
if raw.is_none() {
return Ok(BindDecl::None);
}
funnel::parse_bind_decl(raw).map_err(|reason| {
let prop = raw.unwrap_or("");
Error::at(
"<page>",
"",
&[
&format!("{DATA_BIND}=\"{prop}\""),
&format!("{DATA_BIND}='{prop}'"),
],
reason,
)
})
}
pub fn validate_page_binds(
doc: &Document,
source: BindSource<'_>,
extra_roots: &[String],
) -> Result<()> {
let Some(el) = html_element(doc) else {
return Ok(());
};
let decl = parse_html_bind_decl(doc).map_err(|e| e.in_file(source.file, source.source))?;
let mut data_roots = funnel::data_link_ids(doc);
data_roots.extend(extra_roots.iter().cloned());
funnel::validate_page_template_binds("page", &decl, &el.children, source, &data_roots)
}
pub fn validate_collection_page_binds(
doc: &Document,
kind: PageKind,
has_collection_param: bool,
source: BindSource<'_>,
extra_roots: &[String],
) -> Result<()> {
if kind != PageKind::Collection {
return Ok(());
}
if html_bind_raw(doc).is_none() {
return Ok(());
}
if !has_collection_param {
return validate_page_binds(doc, source, extra_roots);
}
require_collection_id(doc, source)?;
validate_page_binds(doc, source, extra_roots)
}
pub fn render_page_document(
registry: &FragmentRegistry,
doc: &Document,
render_plan: &RenderPlan,
source: &PageSource,
current: Option<&Value>,
page_data: &HashMap<String, DataSource>,
aliases: &AliasOptions,
_forms: &FormsOptions,
_manifest: Option<&ManifestMeta>,
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
site: Option<(&str, &str)>,
) -> Result<String> {
let bind = html_element(doc)
.and_then(Element::bind_directive)
.and_then(|raw| funnel::parse_bind_decl(Some(raw)).ok())
.unwrap_or(BindDecl::None);
let canonical = CanonicalContext::new(source, current, page_data, locale, &bind.scope_names());
let bind_ctx = funnel::bind_context(&bind, canonical.value());
let context_data = canonical.as_data_sources(page_data);
let context_tree = ContextTree::new(ContextScope::Page, bind_ctx, context_data.clone());
match PageRenderer::select(registry, doc, locale) {
PageRenderer::CompiledPlan => render_with_compiled_plan(
registry,
render_plan,
current,
context_data.as_map(),
&context_tree.render_context_with_linked_roots(Some(render_plan.linked_roots())),
&context_tree.translated_context_with_linked_roots(
i18n_catalog,
Some(render_plan.linked_roots()),
),
locale,
i18n_catalog,
data_cache,
aliases,
site,
),
PageRenderer::AstMutation => render_with_ast_mutation(
registry,
doc,
current,
context_data.as_map(),
&context_tree,
locale,
i18n_catalog,
data_cache,
aliases,
site,
),
}
}
#[allow(clippy::too_many_arguments)]
fn render_with_ast_mutation(
registry: &FragmentRegistry,
doc: &Document,
current: Option<&Value>,
data_map: &HashMap<String, DataSource>,
context_tree: &ContextTree,
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
) -> Result<String> {
let mut doc = doc.clone();
let ctx = context_tree.render_context();
fill_attr_templates_in_nodes(&mut doc.children, &ctx);
expand_usage_slots_in_nodes(
registry,
&mut doc.children,
current,
data_map,
locale,
i18n_catalog,
data_cache,
aliases,
site,
)?;
let data_t_context = context_tree.translated_context(i18n_catalog);
i18n::apply_data_t(&mut doc.children, &data_t_context);
funnel::strip_authoring(&mut doc);
clear_remaining_named_slots(&mut doc.children);
scope::dedupe_helpers_in_document(&mut doc);
scope::dedupe_styles_in_document(&mut doc);
Ok(crate::parse::serialize_document(&doc))
}
#[allow(clippy::too_many_arguments)]
fn render_with_compiled_plan(
registry: &FragmentRegistry,
render_plan: &RenderPlan,
current: Option<&Value>,
data_map: &HashMap<String, DataSource>,
attr_context: &Value,
text_context: &Value,
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
) -> Result<String> {
let mut out = String::with_capacity(4096);
render_plan.write_doctype(&mut out);
render_plan_ops(
registry,
render_plan.ops(),
current,
data_map,
attr_context,
text_context,
locale,
i18n_catalog,
data_cache,
aliases,
site,
&OpSlotProjection::default(),
&mut out,
)?;
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn render_plan_ops(
registry: &FragmentRegistry,
ops: &[Op],
current: Option<&Value>,
data_map: &HashMap<String, DataSource>,
attr_context: &Value,
text_context: &Value,
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
projected_slots: &OpSlotProjection,
out: &mut String,
) -> Result<()> {
for op in ops {
match op {
Op::Static(text) => out.push_str(text),
Op::AttrTemplate(template) => {
out.push_str(&escape_attr(&expand_template(template, attr_context)));
}
Op::TextTemplate(template) => out.push_str(&expand_template(template, text_context)),
Op::NamedSlot { name, fallback } => {
if let Some(children) = projected_slots.named_children(name) {
render_plan_ops(
registry,
children,
current,
data_map,
attr_context,
text_context,
locale,
i18n_catalog,
data_cache,
aliases,
site,
projected_slots,
out,
)?;
} else {
render_plan_ops(
registry,
fallback,
current,
data_map,
attr_context,
text_context,
locale,
i18n_catalog,
data_cache,
aliases,
site,
projected_slots,
out,
)?;
}
}
Op::DefaultSlot(fallback) => {
let default_children = projected_slots.default_children();
let children = if default_children.is_empty() {
fallback.as_slice()
} else {
default_children
};
render_plan_ops(
registry,
children,
current,
data_map,
attr_context,
text_context,
locale,
i18n_catalog,
data_cache,
aliases,
site,
projected_slots,
out,
)?;
}
Op::SlottedChild { children, .. } => {
render_plan_ops(
registry,
children,
current,
data_map,
attr_context,
text_context,
locale,
i18n_catalog,
data_cache,
aliases,
site,
projected_slots,
out,
)?;
}
Op::Mount {
id,
each,
class,
children,
} => {
render_plan_mount(
registry,
id,
each.as_deref(),
class.as_deref(),
children,
current,
data_map,
locale,
i18n_catalog,
data_cache,
aliases,
site,
out,
)?;
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn render_plan_mount(
registry: &FragmentRegistry,
id: &str,
each: Option<&str>,
class: Option<&str>,
children: &[Op],
current: Option<&Value>,
data_map: &HashMap<String, DataSource>,
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
out: &mut String,
) -> Result<()> {
if let Some(each_expr) = each {
let list = resolve_each_array(each_expr, current, data_map, data_map)
.map_err(|e| relocate_data_err(e, site, each_expr))?;
match list {
Some(items) => {
for item in items.iter() {
render_plan_fragment(
registry,
id,
item,
class,
data_map,
children,
locale,
i18n_catalog,
data_cache,
aliases,
site,
out,
)?;
}
Ok(())
}
None => Ok(()),
}
} else {
let value = current.cloned().unwrap_or(Value::Null);
render_plan_fragment(
registry,
id,
&value,
class,
data_map,
children,
locale,
i18n_catalog,
data_cache,
aliases,
site,
out,
)
}
}
#[allow(clippy::too_many_arguments)]
fn render_plan_fragment(
registry: &FragmentRegistry,
id: &str,
prop_value: &Value,
mount_class: Option<&str>,
parent_data: &HashMap<String, DataSource>,
children: &[Op],
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
out: &mut String,
) -> Result<()> {
let frag = registry.get(id).ok_or_else(|| {
let msg = missing_fragment_message(id);
match site {
Some((file, source)) => {
let dq = format!("id=\"{id}\"");
let sq = format!("id='{id}'");
Error::at(file, source, &[&dq, &sq], msg)
}
None => Error::at_file("<page>", msg),
}
})?;
let frag_data =
registry.resolve_fragment_data(frag, Some(prop_value), locale, data_cache, aliases)?;
let local = ContextData::new(parent_data.clone()).with_links(&frag_data);
let bind_ctx = funnel::bind_context(&frag.bind, prop_value);
let context_tree = ContextTree::new(ContextScope::Fragment, bind_ctx, local.clone());
render_plan_ops(
registry,
&forward_mount_class_to_ops(frag.render_plan.ops(), mount_class),
Some(prop_value),
local.as_map(),
&context_tree.render_context_with_linked_roots(Some(frag.render_plan.linked_roots())),
&context_tree.translated_context_with_linked_roots(
i18n_catalog,
Some(frag.render_plan.linked_roots()),
),
locale,
i18n_catalog,
data_cache,
aliases,
site,
&OpSlotProjection::from_mount_children(children),
out,
)
}
#[derive(Debug, Clone, Default)]
struct OpSlotProjection {
default: Vec<Op>,
named: HashMap<String, Vec<Op>>,
}
impl OpSlotProjection {
fn from_mount_children(children: &[Op]) -> Self {
let mut projection = Self::default();
for child in children {
match child {
Op::SlottedChild { name, children } => {
projection
.named
.entry(name.clone())
.or_default()
.extend(children.iter().cloned());
}
_ => projection.default.push(child.clone()),
}
}
projection
}
fn default_children(&self) -> &[Op] {
&self.default
}
fn named_children(&self, name: &str) -> Option<&[Op]> {
self.named.get(name).map(Vec::as_slice)
}
}
fn forward_mount_class_to_ops(ops: &[Op], mount_class: Option<&str>) -> Vec<Op> {
let Some(mount_class) = mount_class.map(str::trim).filter(|class| !class.is_empty()) else {
return ops.to_vec();
};
ops.iter()
.cloned()
.map(|op| forward_mount_class_to_op(op, mount_class))
.collect()
}
fn forward_mount_class_to_op(op: Op, mount_class: &str) -> Op {
match op {
Op::Static(html) if contains_forwarded_class_marker(&html) => {
Op::Static(forward_mount_class_to_static_html(&html, mount_class))
}
Op::DefaultSlot(children) => {
Op::DefaultSlot(forward_mount_class_to_ops(&children, Some(mount_class)))
}
Op::NamedSlot { name, fallback } => Op::NamedSlot {
name,
fallback: forward_mount_class_to_ops(&fallback, Some(mount_class)),
},
Op::SlottedChild { name, children } => Op::SlottedChild {
name,
children: forward_mount_class_to_ops(&children, Some(mount_class)),
},
Op::Mount {
id,
each,
class,
children,
} => Op::Mount {
id,
each,
class,
children: forward_mount_class_to_ops(&children, Some(mount_class)),
},
other => other,
}
}
fn forward_mount_class_to_static_html(html: &str, mount_class: &str) -> String {
let mut out = String::with_capacity(html.len() + mount_class.len());
let mut cursor = 0;
let mut changed = false;
while let Some(class_start_rel) = html[cursor..].find(" class=\"") {
let class_start = cursor + class_start_rel;
let value_start = class_start + " class=\"".len();
let Some(value_end_rel) = html[value_start..].find('"') else {
break;
};
let value_end = value_start + value_end_rel;
let original = &html[value_start..value_end];
let class = append_forwarded_class_marker(original, mount_class);
out.push_str(&html[cursor..value_start]);
if class == original {
out.push_str(original);
} else {
changed = true;
out.push_str(&escape_attr(&class));
}
cursor = value_end;
}
if !changed {
return html.to_string();
}
out.push_str(&html[cursor..]);
out
}
fn forward_mount_class_to_nodes(nodes: &mut [Node], mount_class: Option<&str>) {
let Some(mount_class) = mount_class.map(str::trim).filter(|class| !class.is_empty()) else {
return;
};
for node in nodes {
if let Node::Element(el) = node {
if let Some(class) = el.attrs.get_mut(ATTR_CLASS) {
*class = append_forwarded_class_marker(class, mount_class);
}
forward_mount_class_to_nodes(&mut el.children, Some(mount_class));
}
}
}
fn append_forwarded_class_marker(class: &str, mount_class: &str) -> String {
let mut changed = false;
let mut out = Vec::new();
for token in class.split_whitespace() {
if let Some("") = forwarded_class_base(token) {
changed = true;
out.extend(mount_class.split_whitespace());
} else if let Some(base) = forwarded_class_base(token) {
changed = true;
out.push(base);
out.extend(mount_class.split_whitespace());
} else {
out.push(token);
}
}
if changed {
out.join(" ")
} else {
class.to_string()
}
}
fn escape_attr(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'"' => out.push_str("""),
'<' => out.push_str("<"),
_ => out.push(c),
}
}
out
}
pub fn transform_page_styles(nodes: &mut [Node]) {
for node in nodes {
if let Node::Element(el) = node {
if el.is_style() {
if let Some(Node::Text(css)) = el.children.first_mut() {
if !css.contains("[data-s=\"") {
if let Ok(ready) = crate::css::transform_css(css, true) {
*css = ready;
}
}
}
}
transform_page_styles(&mut el.children);
}
}
}
pub fn expand_usage_slots_in_nodes(
registry: &FragmentRegistry,
nodes: &mut Vec<Node>,
current: Option<&Value>,
data_map: &HashMap<String, DataSource>,
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
) -> Result<()> {
let mut i = 0;
while i < nodes.len() {
let replace = match &nodes[i] {
Node::Element(el) => match el.slot_kind() {
Some(SlotKind::FragmentMount(id)) => Some(FragmentMount {
id,
class: el.attr(ATTR_CLASS).map(ToOwned::to_owned),
children: el.children.clone(),
each: el.each_directive(),
}),
Some(SlotKind::Named(_) | SlotKind::Default) | None => None,
},
_ => None,
};
if let Some(mount) = replace {
let rendered = if let Some(each) = mount.each {
let list = resolve_each_array(each.expr(), current, data_map, data_map)
.map_err(|e| relocate_data_err(e, site, each.expr()))?;
render_each(
registry,
&mount.id,
list,
mount.class.as_deref(),
data_map,
&mount.children,
locale,
i18n_catalog,
data_cache,
aliases,
site,
each.expr(),
)?
} else {
let value = current.cloned().unwrap_or(Value::Null);
render_fragment_nodes(
registry,
&mount.id,
&value,
mount.class.as_deref(),
data_map,
&mount.children,
locale,
i18n_catalog,
data_cache,
aliases,
site,
)?
};
nodes.splice(i..=i, rendered.iter().cloned());
i += rendered.len().max(1);
} else {
if let Node::Element(el) = &mut nodes[i] {
expand_usage_slots_in_nodes(
registry,
&mut el.children,
current,
data_map,
locale,
i18n_catalog,
data_cache,
aliases,
site,
)?;
}
i += 1;
}
}
Ok(())
}
fn relocate_data_err(err: Error, site: Option<(&str, &str)>, expr: &str) -> Error {
match site {
Some((file, source)) => {
let dq = format!("{DATA_BIND}=\"{expr}\"");
let sq = format!("{DATA_BIND}='{expr}'");
let each_dq = format!("{DATA_EACH}=\"{expr}\"");
let each_sq = format!("{DATA_EACH}='{expr}'");
err.in_file_at(file, source, &[&dq, &sq, &each_dq, &each_sq, expr])
}
None => err,
}
}
fn resolve_each_array<'a>(
expr: &str,
current: Option<&'a Value>,
local_data: &'a HashMap<String, DataSource>,
parent_data: &'a HashMap<String, DataSource>,
) -> Result<Option<Cow<'a, [Value]>>> {
let expr = expr.trim();
if expr.is_empty() {
return Ok(None);
}
if expr == "." {
return match current {
Some(Value::Array(items)) => Ok(Some(Cow::Borrowed(items))),
Some(Value::Null) | None => Ok(None),
Some(_) => Err(Error::at_file(
"<data>",
"data-each can only loop over an array; update the expression to point at an array value",
)),
};
}
let mut parts = expr.split('.').filter(|p| !p.is_empty());
let first = parts
.next()
.ok_or_else(|| Error::at_file("<data>", "data expression is empty"))?;
let rest: Vec<&str> = parts.collect();
if first == "this" {
return array_at_path(current.unwrap_or(&Value::Null), &rest);
}
if let Some(value) = current.and_then(|cur| funnel::read_field(cur, first)) {
if let Some(items) = array_at_path(value, &rest)? {
return Ok(Some(items));
}
}
if let Some(source) = local_data.get(first).or_else(|| parent_data.get(first)) {
if let Some(value) = data_source_value(source) {
return array_at_path(value, &rest);
}
if rest.is_empty() {
return source
.array()
.map(|items| Some(Cow::Owned(items)))
.ok_or_else(|| {
Error::at_file(
"<data>",
"data-each can only loop over an array; the selected data source is not an array",
)
});
}
}
let value = funnel::resolve_expr(expr, current, local_data, parent_data)?;
match value {
Value::Array(items) => Ok(Some(Cow::Owned(items))),
Value::Null => Ok(None),
_ => Err(Error::at_file(
"<data>",
"data-each can only loop over an array; update the expression to point at an array value",
)),
}
}
fn array_at_path<'a>(mut value: &'a Value, path: &[&str]) -> Result<Option<Cow<'a, [Value]>>> {
for part in path {
value = match funnel::read_field(value, part) {
Some(value) => value,
None => return Ok(None),
};
}
match value {
Value::Array(items) => Ok(Some(Cow::Borrowed(items))),
Value::Null => Ok(None),
_ => Err(Error::at_file(
"<data>",
"data-each can only loop over an array; update the expression to point at an array value",
)),
}
}
fn data_source_value(source: &DataSource) -> Option<&Value> {
match source.data.as_ref() {
crate::content::DataSet::Json(value) | crate::content::DataSet::Markdown(value) => {
Some(value)
}
crate::content::DataSet::Records(_)
| crate::content::DataSet::Lines(_)
| crate::content::DataSet::Glob(_) => None,
}
}
fn render_each(
registry: &FragmentRegistry,
id: &str,
list: Option<Cow<'_, [Value]>>,
mount_class: Option<&str>,
data_map: &HashMap<String, DataSource>,
children: &[Node],
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
_each_expr: &str,
) -> Result<Vec<Node>> {
let Some(arr) = list else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for item in arr.iter() {
out.extend(render_fragment_nodes(
registry,
id,
item,
mount_class,
data_map,
children,
locale,
i18n_catalog,
data_cache,
aliases,
site,
)?);
}
Ok(out)
}
fn render_fragment_nodes(
registry: &FragmentRegistry,
id: &str,
prop_value: &Value,
mount_class: Option<&str>,
parent_data: &HashMap<String, DataSource>,
children: &[Node],
locale: Option<&str>,
i18n_catalog: Option<&Value>,
data_cache: &mut HashMap<PathBuf, std::sync::Arc<crate::content::DataSet>>,
aliases: &AliasOptions,
site: Option<(&str, &str)>,
) -> Result<Vec<Node>> {
let frag = registry.get(id).ok_or_else(|| {
let msg = missing_fragment_message(id);
match site {
Some((file, source)) => {
let dq = format!("id=\"{id}\"");
let sq = format!("id='{id}'");
Error::at(file, source, &[&dq, &sq], msg)
}
None => Error::at_file("<page>", msg),
}
})?;
let frag_data =
registry.resolve_fragment_data(frag, Some(prop_value), locale, data_cache, aliases)?;
let local = ContextData::new(parent_data.clone()).with_links(&frag_data);
let bind_ctx = funnel::bind_context(&frag.bind, prop_value);
let context_tree = ContextTree::new(ContextScope::Fragment, bind_ctx, local.clone());
let ctx = context_tree.render_context();
let mut nodes = fragment::template_children(frag);
forward_mount_class_to_nodes(&mut nodes, mount_class);
fill_attr_templates_in_nodes(&mut nodes, &ctx);
let projection = SlotProjection::from_mount_children(children);
fill_projection_slots(&mut nodes, &projection);
expand_usage_slots_in_nodes(
registry,
&mut nodes,
Some(prop_value),
local.as_map(),
locale,
i18n_catalog,
data_cache,
aliases,
site,
)?;
let data_t_context = context_tree.translated_context(i18n_catalog);
i18n::apply_data_t(&mut nodes, &data_t_context);
Ok(nodes)
}