mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
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:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user