From 29a84758d77db158c04737af1b90c982d0961ae6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 13 Feb 2025 14:22:18 +0100 Subject: [PATCH] Validate --- .../common/CodeEditor/CodeEditor.svelte | 123 +++++++++++++++++- .../src/components/common/CodeEditor/index.ts | 1 + .../common/bindings/BindingPanel.svelte | 1 + packages/builder/src/types/bindings.ts | 4 +- packages/types/src/ui/bindings/helper.ts | 1 + 5 files changed, 126 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 2acde47539..646c8bb92b 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -2,6 +2,7 @@ import { Label } from "@budibase/bbui" import { onMount, createEventDispatcher, onDestroy } from "svelte" import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates" + import Handlebars from "handlebars" import { autocompletion, @@ -40,16 +41,18 @@ indentMore, indentLess, } from "@codemirror/commands" + import { setDiagnostics } from "@codemirror/lint" + import type { Diagnostic } from "@codemirror/lint" import { Compartment, EditorState } from "@codemirror/state" import { javascript } from "@codemirror/lang-javascript" import { EditorModes } from "./" import { themeStore } from "@/stores/portal" import type { EditorMode } from "@budibase/types" - import type { BindingCompletion } from "@/types" + import type { BindingCompletion, BindingCompletionOption } from "@/types" export let label: string | undefined = undefined - // TODO: work out what best type fits this export let completions: BindingCompletion[] = [] + export let options: BindingCompletionOption[] = [] export let mode: EditorMode = EditorModes.Handlebars export let value: string | null = "" export let placeholder: string | null = null @@ -245,6 +248,95 @@ ] } + async function validateHbsTemplate( + editor: EditorView, + template: string, + helpers: Record + ): Promise { + const diagnostics: Diagnostic[] = [] + + try { + const ast = Handlebars.parse(template) + + function traverseNodes(nodes: any[]) { + nodes.forEach(node => { + if ( + node.type === "MustacheStatement" && + node.path.type === "PathExpression" + ) { + const helperName = node.path.original + + const from = + editor.state.doc.line(node.loc.start.line).from + + node.loc.start.column + const to = + editor.state.doc.line(node.loc.end.line).from + + node.loc.end.column + + if (!(helperName in helpers)) { + diagnostics.push({ + from, + to, + severity: "warning", + message: `"${helperName}" handler does not exist.`, + }) + return + } + + const expectedParams = helpers[helperName] + + if (expectedParams) { + const providedParams = node.params + if (providedParams.length !== expectedParams.length) { + diagnostics.push({ + from, + to, + severity: "error", + message: `Helper "${helperName}" expects ${ + expectedParams.length + } parameters (${expectedParams.join(", ")}), but got ${ + providedParams.length + }.`, + }) + } + } + } + + if (node.program) { + traverseNodes(node.program.body) + } + }) + } + + traverseNodes(ast.body) + } catch (e: any) { + diagnostics.push({ + from: 0, + to: template.length, + severity: "error", + message: `Syntax error: ${e.message}`, + }) + } + + return diagnostics + } + + // function getCompletions(): ((_: CompletionContext) => any)[] { + // switch (mode.name) { + // case "handlebars": + // return [hbAutocomplete([...completions])] + + // case "javascript": + // return [jsAutocomplete([...completions])] + + // case "text/html": + // return [] + + // default: + // throw utils.unreachable(mode) + // } + // } + // None of this is reactive, but it never has been, so we just assume most // config flags aren't changed at runtime // TODO: work out type for base @@ -255,6 +347,7 @@ complete.push( autocompletion({ override: [...completions], + // override: [...completions.map(c => c.completionDelegate)], closeOnBlur: true, icons: false, optionClass: completion => @@ -340,6 +433,31 @@ return complete } + function validate( + value: string | null, + editor: EditorView, + mode: EditorMode, + options: BindingCompletionOption[] + ) { + if (!value) { + return + } + + const expectedHelpers: Record = {} + + for (const option of options) { + expectedHelpers[option.label] = option.args || [] + } + + if (mode === EditorModes.Handlebars) { + validateHbsTemplate(editor, value, expectedHelpers).then(diagnostics => { + editor?.dispatch(setDiagnostics(editor.state, diagnostics)) + }) + } + } + + $: validate(value, editor, mode, options) + const initEditor = () => { const baseExtensions = buildBaseExtensions() @@ -366,7 +484,6 @@ {/if} -
diff --git a/packages/builder/src/components/common/CodeEditor/index.ts b/packages/builder/src/components/common/CodeEditor/index.ts index 5529484665..4db4d610b8 100644 --- a/packages/builder/src/components/common/CodeEditor/index.ts +++ b/packages/builder/src/components/common/CodeEditor/index.ts @@ -83,6 +83,7 @@ const helpersToCompletion = ( const helper = helpers[helperName] return { label: helperName, + args: helper.args, info: () => buildHelperInfoNode(helper), type: "helper", section: helperSection, diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 935c6d48bc..2de5a89bdc 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -373,6 +373,7 @@ bind:getCaretPosition bind:insertAtPos {completions} + options={autocompleteOptions} autofocus={autofocusEditor} placeholder={placeholder || "Add bindings by typing {{ or use the menu on the right"} diff --git a/packages/builder/src/types/bindings.ts b/packages/builder/src/types/bindings.ts index 00571f1d8b..7dc9c4f5ca 100644 --- a/packages/builder/src/types/bindings.ts +++ b/packages/builder/src/types/bindings.ts @@ -5,4 +5,6 @@ export type BindingCompletion = (context: CompletionContext) => { options: Completion[] } | null -export type BindingCompletionOption = Completion +export interface BindingCompletionOption extends Completion { + args?: any[] +} diff --git a/packages/types/src/ui/bindings/helper.ts b/packages/types/src/ui/bindings/helper.ts index 5ebd305958..5de040cf3d 100644 --- a/packages/types/src/ui/bindings/helper.ts +++ b/packages/types/src/ui/bindings/helper.ts @@ -1,4 +1,5 @@ export interface Helper { example: string description: string + args: any[] }