feat: implement the bDS2-compatible core Lua API
This commit is contained in:
@@ -1,11 +1,23 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Map, Value as JsonValue};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct MacroRenderContext {
|
||||
pub roots: Map<String, JsonValue>,
|
||||
pub post_id: Option<String>,
|
||||
pub host: Arc<dyn crate::scripting::HostApi>,
|
||||
}
|
||||
|
||||
impl Default for MacroRenderContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
roots: Map::new(),
|
||||
post_id: None,
|
||||
host: Arc::new(crate::scripting::UnavailableHost),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_builtin_macros(markdown: &str, context: &MacroRenderContext) -> String {
|
||||
@@ -77,12 +89,13 @@ fn render_script_macro(
|
||||
"translations": context.roots.get("translations").cloned().unwrap_or(JsonValue::Array(Vec::new())),
|
||||
});
|
||||
let params = serde_json::to_value(args).ok()?;
|
||||
match crate::scripting::execute_many(
|
||||
match crate::scripting::execute_many_with_host(
|
||||
source,
|
||||
entrypoint,
|
||||
&[params, env],
|
||||
crate::scripting::ExecutionKind::Macro,
|
||||
&crate::scripting::ExecutionControl::default(),
|
||||
Arc::clone(&context.host),
|
||||
) {
|
||||
Ok(result) => Some(match result.value {
|
||||
JsonValue::Null => String::new(),
|
||||
@@ -519,6 +532,7 @@ mod tests {
|
||||
&MacroRenderContext {
|
||||
roots,
|
||||
post_id: Some("post-1".to_string()),
|
||||
..MacroRenderContext::default()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -555,6 +569,7 @@ mod tests {
|
||||
&MacroRenderContext {
|
||||
roots,
|
||||
post_id: Some("post-1".into()),
|
||||
..MacroRenderContext::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(rendered, "<aside>Hallo:de</aside>");
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use generation::{
|
||||
write_generated_bytes, write_generated_file,
|
||||
};
|
||||
pub use markdown::render_markdown_to_html;
|
||||
pub(crate) use page_renderer::render_liquid_template_with_host;
|
||||
pub use page_renderer::{RenderError, render_liquid_template};
|
||||
pub use routes::{
|
||||
RenderedPage, build_canonical_post_path, render_starter_list_page,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use liquid::ParserBuilder;
|
||||
use liquid::partials::{EagerCompiler, InMemorySource};
|
||||
use liquid_core::model::ScalarCow;
|
||||
use liquid_core::parser::FilterArguments;
|
||||
use liquid_core::{
|
||||
Display_filter, Expression, Filter, FilterParameters, FilterReflection, FromFilterParameters,
|
||||
ParseFilter, Runtime, Value, ValueView,
|
||||
@@ -14,6 +17,7 @@ use thiserror::Error;
|
||||
use crate::i18n::translate_render;
|
||||
use crate::render::macros::{MacroRenderContext, expand_builtin_macros};
|
||||
use crate::render::render_markdown_to_html;
|
||||
use crate::scripting::{HostApi, UnavailableHost};
|
||||
use crate::util::slugify;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -32,6 +36,20 @@ pub fn render_liquid_template<T: Serialize>(
|
||||
template_source: &str,
|
||||
partials: &HashMap<String, String>,
|
||||
context: &T,
|
||||
) -> Result<String, RenderError> {
|
||||
render_liquid_template_with_host(
|
||||
template_source,
|
||||
partials,
|
||||
context,
|
||||
Arc::new(UnavailableHost),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_liquid_template_with_host<T: Serialize>(
|
||||
template_source: &str,
|
||||
partials: &HashMap<String, String>,
|
||||
context: &T,
|
||||
host: Arc<dyn HostApi>,
|
||||
) -> Result<String, RenderError> {
|
||||
let mut compiled_partials: EagerCompiler<InMemorySource> = EagerCompiler::empty();
|
||||
for (name, content) in partials {
|
||||
@@ -40,7 +58,7 @@ pub fn render_liquid_template<T: Serialize>(
|
||||
|
||||
let parser = ParserBuilder::with_stdlib()
|
||||
.filter(I18n)
|
||||
.filter(Markdown)
|
||||
.filter(Markdown { host })
|
||||
.filter(Slugify)
|
||||
.partials(compiled_partials)
|
||||
.build()?;
|
||||
@@ -121,20 +139,43 @@ struct MarkdownArgs {
|
||||
language_prefix: Option<Expression>,
|
||||
}
|
||||
|
||||
#[derive(Clone, ParseFilter, FilterReflection)]
|
||||
#[derive(Clone, FilterReflection)]
|
||||
#[filter(
|
||||
name = "markdown",
|
||||
description = "Render markdown to HTML and rewrite preview URLs.",
|
||||
parameters(MarkdownArgs),
|
||||
parsed(MarkdownFilter)
|
||||
)]
|
||||
struct Markdown;
|
||||
struct Markdown {
|
||||
host: Arc<dyn HostApi>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromFilterParameters, Display_filter)]
|
||||
impl ParseFilter for Markdown {
|
||||
fn parse(&self, args: FilterArguments<'_>) -> liquid_core::Result<Box<dyn Filter>> {
|
||||
Ok(Box::new(MarkdownFilter {
|
||||
args: MarkdownArgs::from_args(args)?,
|
||||
host: Arc::clone(&self.host),
|
||||
}))
|
||||
}
|
||||
|
||||
fn reflection(&self) -> &dyn FilterReflection {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Display_filter)]
|
||||
#[name = "markdown"]
|
||||
struct MarkdownFilter {
|
||||
#[parameters]
|
||||
args: MarkdownArgs,
|
||||
host: Arc<dyn HostApi>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MarkdownFilter {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("MarkdownFilter")
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for MarkdownFilter {
|
||||
@@ -158,6 +199,7 @@ impl Filter for MarkdownFilter {
|
||||
post_id: runtime
|
||||
.try_get(&[ScalarCow::new("post"), ScalarCow::new("id")])
|
||||
.and_then(|value| value.as_scalar().map(|scalar| scalar.to_kstr().to_string())),
|
||||
host: Arc::clone(&self.host),
|
||||
};
|
||||
|
||||
let expanded = expand_builtin_macros(markdown.as_str(), ¯o_context);
|
||||
@@ -473,8 +515,31 @@ fn trim_html_suffix(value: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{RewriteContextView, render_liquid_template, rewrite_rendered_html_urls};
|
||||
use super::{
|
||||
RewriteContextView, render_liquid_template, render_liquid_template_with_host,
|
||||
rewrite_rendered_html_urls,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::scripting::HostApi;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
struct PostsHost;
|
||||
|
||||
impl HostApi for PostsHost {
|
||||
fn call(
|
||||
&self,
|
||||
namespace: &str,
|
||||
method: &str,
|
||||
_arguments: Vec<Value>,
|
||||
) -> Result<Value, String> {
|
||||
match (namespace, method) {
|
||||
("posts", "get_all") => Ok(json!([{"id":"one"}, {"id":"two"}])),
|
||||
_ => Err("unsupported test capability".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestRewriteContext {
|
||||
canonical_post_path_by_slug: HashMap<String, String>,
|
||||
@@ -520,4 +585,25 @@ mod tests {
|
||||
|
||||
assert_eq!(rendered, "ueber-die-bruecke");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_macros_use_the_supplied_project_host() {
|
||||
let rendered = render_liquid_template_with_host(
|
||||
"{{ post.content | markdown }}",
|
||||
&HashMap::new(),
|
||||
&json!({
|
||||
"post": {"id": "post-1", "content": "[[count]]"},
|
||||
"macro_scripts": {
|
||||
"count": {
|
||||
"source": "function render() return tostring(#bds.posts.get_all()) end",
|
||||
"entrypoint": "render"
|
||||
}
|
||||
}
|
||||
}),
|
||||
Arc::new(PostsHost),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rendered, "<p>2</p>\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap};
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::DbConnection as Connection;
|
||||
use chrono::{Datelike, TimeZone, Utc};
|
||||
@@ -16,8 +17,9 @@ use crate::model::{
|
||||
};
|
||||
use crate::render::{
|
||||
RenderCategorySettings, RenderTemplateLookup, build_canonical_post_path,
|
||||
render_liquid_template, resolve_post_template,
|
||||
render_liquid_template_with_host, resolve_post_template,
|
||||
};
|
||||
use crate::scripting::{CoreHost, HostApi, UnavailableHost};
|
||||
use crate::util::frontmatter::{read_script_file, read_template_file, read_translation_file};
|
||||
use crate::util::slugify;
|
||||
|
||||
@@ -62,7 +64,7 @@ pub struct PreviewRenderResult {
|
||||
pub html: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
struct TemplateBundle {
|
||||
post_templates: Vec<Template>,
|
||||
template_source_by_slug: HashMap<String, String>,
|
||||
@@ -71,6 +73,7 @@ struct TemplateBundle {
|
||||
not_found_template: String,
|
||||
partials: HashMap<String, String>,
|
||||
macro_scripts: HashMap<String, Value>,
|
||||
host: Arc<dyn HostApi>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -268,6 +271,10 @@ fn load_template_bundle(
|
||||
let mut default_list_template = STARTER_POST_LIST_TEMPLATE.to_string();
|
||||
let mut not_found_template = STARTER_NOT_FOUND_TEMPLATE.to_string();
|
||||
let mut macro_scripts = HashMap::new();
|
||||
let host: Arc<dyn HostApi> = CoreHost::from_connection(conn, project_id, data_dir)
|
||||
.map(|host| host.with_offline_mode(true))
|
||||
.map(|host| Arc::new(host) as Arc<dyn HostApi>)
|
||||
.unwrap_or_else(|_| Arc::new(UnavailableHost));
|
||||
|
||||
for script in queries::script::list_scripts_by_project(conn, project_id).unwrap_or_default() {
|
||||
if script.kind != ScriptKind::Macro
|
||||
@@ -368,6 +375,7 @@ fn load_template_bundle(
|
||||
not_found_template,
|
||||
partials,
|
||||
macro_scripts,
|
||||
host,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -661,10 +669,11 @@ fn render_list_route(
|
||||
"not_found_back_label": serde_json::Value::Null,
|
||||
});
|
||||
|
||||
Ok(render_liquid_template(
|
||||
Ok(render_liquid_template_with_host(
|
||||
list_template,
|
||||
&bundle.partials,
|
||||
&context,
|
||||
Arc::clone(&bundle.host),
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -776,10 +785,11 @@ fn render_post_route(
|
||||
"not_found_back_label": serde_json::Value::Null,
|
||||
});
|
||||
|
||||
Ok(render_liquid_template(
|
||||
Ok(render_liquid_template_with_host(
|
||||
&template_source,
|
||||
&bundle.partials,
|
||||
&context,
|
||||
Arc::clone(&bundle.host),
|
||||
)?)
|
||||
}
|
||||
|
||||
@@ -824,10 +834,11 @@ fn render_not_found_route(
|
||||
"canonical_media_path_by_source_path": HashMap::<String, String>::new(),
|
||||
"post_data_json_by_id": HashMap::<String, Value>::new(),
|
||||
});
|
||||
Ok(render_liquid_template(
|
||||
Ok(render_liquid_template_with_host(
|
||||
&bundle.not_found_template,
|
||||
&bundle.partials,
|
||||
&context,
|
||||
Arc::clone(&bundle.host),
|
||||
)?)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user