replace LiquidJs rendering with simplified renderer

Removed dependency on LiquidJS and replaced it with a custom templating solution using lodash `get`. Updated corresponding components, editors, and tests to align with the new rendering approach. Removed unused filters and tags.
This commit is contained in:
dswbx
2025-04-29 11:09:58 +02:00
parent eec8b64bae
commit 1ef2287d34
14 changed files with 147 additions and 274 deletions
@@ -0,0 +1,70 @@
import { describe, expect, test } from "bun:test";
import { SimpleRenderer } from "core";
describe(SimpleRenderer, () => {
const renderer = new SimpleRenderer(
{
name: "World",
views: 123,
nested: {
foo: "bar",
baz: ["quz", "foo"],
},
someArray: [1, 2, 3],
enabled: true,
},
{
renderKeys: true,
},
);
test("strings", async () => {
const tests = [
["Hello {{ name }}, count: {{views}}", "Hello World, count: 123"],
["Nested: {{nested.foo}}", "Nested: bar"],
["Nested: {{nested.baz[0]}}", "Nested: quz"],
] as const;
for (const [template, expected] of tests) {
expect(await renderer.renderString(template)).toEqual(expected);
}
});
test("arrays", async () => {
const tests = [
[
["{{someArray[0]}}", "{{someArray[1]}}", "{{someArray[2]}}"],
["1", "2", "3"],
],
] as const;
for (const [template, expected] of tests) {
const result = await renderer.render(template);
expect(result).toEqual(expected as any);
}
});
test("objects", async () => {
const tests = [
[
{
foo: "{{name}}",
bar: "{{views}}",
baz: "{{nested.foo}}",
quz: "{{nested.baz[0]}}",
},
{
foo: "World",
bar: "123",
baz: "bar",
quz: "quz",
},
],
] as const;
for (const [template, expected] of tests) {
const result = await renderer.render(template);
expect(result).toEqual(expected as any);
}
});
});
+16 -35
View File
@@ -1,17 +1,13 @@
import { Liquid, LiquidError } from "liquidjs";
import type { RenderOptions } from "liquidjs/dist/liquid-options";
import { BkndError } from "../errors";
import { get } from "lodash-es";
export type TemplateObject = Record<string, string | Record<string, string>>;
export type TemplateTypes = string | TemplateObject;
export type TemplateTypes = string | TemplateObject | any;
export type SimpleRendererOptions = RenderOptions & {
export type SimpleRendererOptions = {
renderKeys?: boolean;
};
export class SimpleRenderer {
private engine = new Liquid();
constructor(
private variables: Record<string, any> = {},
private options: SimpleRendererOptions = {},
@@ -33,44 +29,29 @@ export class SimpleRenderer {
flat = String(template);
}
const checks = ["{{", "{%", "{#", "{:"];
const checks = ["{{"];
return checks.some((check) => flat.includes(check));
}
async render<Given extends TemplateTypes>(template: Given): Promise<Given> {
try {
if (typeof template === "string") {
return (await this.renderString(template)) as unknown as Given;
} else if (Array.isArray(template)) {
return (await Promise.all(
template.map((item) => this.render(item)),
)) as unknown as Given;
} else if (typeof template === "object") {
return (await this.renderObject(template)) as unknown as Given;
}
} catch (e) {
if (e instanceof LiquidError) {
const details = {
name: e.name,
token: {
kind: e.token.kind,
input: e.token.input,
begin: e.token.begin,
end: e.token.end,
},
};
async render<Given extends TemplateTypes = TemplateTypes>(template: Given): Promise<Given> {
if (typeof template === "undefined" || template === null) return template;
throw new BkndError(e.message, details, "liquid");
}
throw e;
if (typeof template === "string") {
return (await this.renderString(template)) as unknown as Given;
} else if (Array.isArray(template)) {
return (await Promise.all(template.map((item) => this.render(item)))) as unknown as Given;
} else if (typeof template === "object") {
return (await this.renderObject(template as any)) as unknown as Given;
}
throw new Error("Invalid template type");
}
async renderString(template: string): Promise<string> {
return this.engine.parseAndRender(template, this.variables, this.options);
return template.replace(/{{\s*([^{}]+?)\s*}}/g, (_, expr: string) => {
const value = get(this.variables, expr.trim());
return value == null ? "" : String(value);
});
}
async renderObject(template: TemplateObject): Promise<TemplateObject> {
+1 -1
View File
@@ -106,7 +106,7 @@ export abstract class Task<Params extends TObject = TObject, Output = unknown> {
inputs: object = {},
): Promise<StaticDecode<S>> {
const newParams: any = {};
const renderer = new SimpleRenderer(inputs, { strictVariables: true, renderKeys: true });
const renderer = new SimpleRenderer(inputs, { renderKeys: true });
for (const [key, value] of Object.entries(params)) {
if (value && SimpleRenderer.hasMarkup(value)) {
+4 -3
View File
@@ -1,12 +1,12 @@
import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { json } from "@codemirror/lang-json";
import { type LiquidCompletionConfig, liquid } from "@codemirror/lang-liquid";
import { html } from "@codemirror/lang-html";
import { useTheme } from "ui/client/use-theme";
export type CodeEditorProps = ReactCodeMirrorProps & {
_extensions?: Partial<{
json: boolean;
liquid: LiquidCompletionConfig;
html: boolean;
}>;
};
@@ -31,7 +31,8 @@ export default function CodeEditor({
case "json":
return json();
case "liquid":
return liquid(config);
case "html":
return html(config);
}
return undefined;
})
+23
View File
@@ -0,0 +1,23 @@
import { Suspense, lazy } from "react";
import { twMerge } from "tailwind-merge";
import type { CodeEditorProps } from "./CodeEditor";
const CodeEditor = lazy(() => import("./CodeEditor"));
export function HtmlEditor({ editable, ...props }: CodeEditorProps) {
return (
<Suspense fallback={null}>
<CodeEditor
className={twMerge(
"flex w-full border border-muted bg-white rounded-lg",
!editable && "opacity-70",
)}
editable={editable}
_extensions={{
html: true,
}}
{...props}
/>
</Suspense>
);
}
@@ -1,125 +0,0 @@
import { Suspense, lazy } from "react";
import { twMerge } from "tailwind-merge";
import type { CodeEditorProps } from "./CodeEditor";
const CodeEditor = lazy(() => import("./CodeEditor"));
const filters = [
{ label: "abs" },
{ label: "append" },
{ label: "array_to_sentence_string" },
{ label: "at_least" },
{ label: "at_most" },
{ label: "capitalize" },
{ label: "ceil" },
{ label: "cgi_escape" },
{ label: "compact" },
{ label: "concat" },
{ label: "date" },
{ label: "date_to_long_string" },
{ label: "date_to_rfc822" },
{ label: "date_to_string" },
{ label: "date_to_xmlschema" },
{ label: "default" },
{ label: "divided_by" },
{ label: "downcase" },
{ label: "escape" },
{ label: "escape_once" },
{ label: "find" },
{ label: "find_exp" },
{ label: "first" },
{ label: "floor" },
{ label: "group_by" },
{ label: "group_by_exp" },
{ label: "inspect" },
{ label: "join" },
{ label: "json" },
{ label: "jsonify" },
{ label: "last" },
{ label: "lstrip" },
{ label: "map" },
{ label: "minus" },
{ label: "modulo" },
{ label: "newline_to_br" },
{ label: "normalize_whitespace" },
{ label: "number_of_words" },
{ label: "plus" },
{ label: "pop" },
{ label: "push" },
{ label: "prepend" },
{ label: "raw" },
{ label: "remove" },
{ label: "remove_first" },
{ label: "remove_last" },
{ label: "replace" },
{ label: "replace_first" },
{ label: "replace_last" },
{ label: "reverse" },
{ label: "round" },
{ label: "rstrip" },
{ label: "shift" },
{ label: "size" },
{ label: "slice" },
{ label: "slugify" },
{ label: "sort" },
{ label: "sort_natural" },
{ label: "split" },
{ label: "strip" },
{ label: "strip_html" },
{ label: "strip_newlines" },
{ label: "sum" },
{ label: "times" },
{ label: "to_integer" },
{ label: "truncate" },
{ label: "truncatewords" },
{ label: "uniq" },
{ label: "unshift" },
{ label: "upcase" },
{ label: "uri_escape" },
{ label: "url_decode" },
{ label: "url_encode" },
{ label: "where" },
{ label: "where_exp" },
{ label: "xml_escape" },
];
const tags = [
{ label: "assign" },
{ label: "capture" },
{ label: "case" },
{ label: "comment" },
{ label: "cycle" },
{ label: "decrement" },
{ label: "echo" },
{ label: "else" },
{ label: "elsif" },
{ label: "for" },
{ label: "if" },
{ label: "include" },
{ label: "increment" },
{ label: "layout" },
{ label: "liquid" },
{ label: "raw" },
{ label: "render" },
{ label: "tablerow" },
{ label: "unless" },
{ label: "when" },
];
export function LiquidJsEditor({ editable, ...props }: CodeEditorProps) {
return (
<Suspense fallback={null}>
<CodeEditor
className={twMerge(
"flex w-full border border-muted bg-white rounded-lg",
!editable && "opacity-70",
)}
editable={editable}
_extensions={{
liquid: { filters, tags },
}}
{...props}
/>
</Suspense>
);
}
@@ -1,9 +1,9 @@
import type { FieldProps } from "@rjsf/utils";
import { LiquidJsEditor } from "../../../code/LiquidJsEditor";
import { Label } from "../templates/FieldTemplate";
import { HtmlEditor } from "ui/components/code/HtmlEditor";
// @todo: move editor to lazy loading component
export default function LiquidJsField({
export default function HtmlField({
formData,
onChange,
disabled,
@@ -20,7 +20,7 @@ export default function LiquidJsField({
return (
<div className="flex flex-col gap-2">
<Label label={props.name} id={id} />
<LiquidJsEditor value={formData} editable={!isDisabled} onChange={handleChange} />
<HtmlEditor value={formData} editable={!isDisabled} onChange={handleChange} />
</div>
);
}
@@ -1,10 +1,10 @@
import JsonField from "./JsonField";
import LiquidJsField from "./LiquidJsField";
import MultiSchemaField from "./MultiSchemaField";
import HtmlField from "./HtmlField";
export const fields = {
AnyOfField: MultiSchemaField,
OneOfField: MultiSchemaField,
JsonField,
LiquidJsField,
HtmlField,
};
@@ -10,7 +10,7 @@ export function RenderTaskComponent(props: TaskComponentProps) {
onChange={console.log}
uiSchema={{
render: {
"ui:field": "LiquidJsField",
"ui:field": "HtmlField",
},
}}
/>
@@ -1,12 +1,12 @@
import { IconWorld } from "@tabler/icons-react";
import { LiquidJsEditor } from "ui/components/code/LiquidJsEditor";
import { BaseNode } from "../BaseNode";
import { HtmlEditor } from "ui/components/code/HtmlEditor";
export function RenderNode(props) {
return (
<BaseNode {...props} onChangeName={console.log} Icon={IconWorld} className="w-[400px]">
<form className="flex flex-col gap-3">
<LiquidJsEditor value={props.params.render} onChange={console.log} />
<HtmlEditor value={props.params.render} onChange={console.log} />
</form>
</BaseNode>
);
@@ -90,7 +90,7 @@ export const FlowsSettings = ({ schema, config }) => {
uiSchema={{
params: {
render: {
"ui:field": "LiquidJsField",
"ui:field": "HtmlField",
},
},
}}
@@ -1,12 +1,12 @@
import { TextInput } from "@mantine/core";
import { LiquidJsEditor } from "../../../components/code/LiquidJsEditor";
import * as Formy from "../../../components/form/Formy";
import { HtmlEditor } from "ui/components/code/HtmlEditor";
export function LiquidJsTest() {
return (
<div className="flex flex-col p-4 gap-3">
<h1>LiquidJsTest</h1>
<LiquidJsEditor />
<HtmlEditor />
<TextInput />
<Formy.Input />