Merge pull request #15552 from Budibase/BUDI-9038/validate-hbs
Validate handlebars
This commit is contained in:
commit
da2a7ab83f
|
@ -386,7 +386,7 @@
|
||||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||||
} else if (editableColumn.type === FieldType.FORMULA) {
|
} else if (editableColumn.type === FieldType.FORMULA) {
|
||||||
editableColumn.formulaType = "dynamic"
|
editableColumn.formulaType = "dynamic"
|
||||||
editableColumn.responseType = field.responseType || FIELDS.STRING.type
|
editableColumn.responseType = field?.responseType || FIELDS.STRING.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,16 +40,19 @@
|
||||||
indentMore,
|
indentMore,
|
||||||
indentLess,
|
indentLess,
|
||||||
} from "@codemirror/commands"
|
} from "@codemirror/commands"
|
||||||
|
import { setDiagnostics } from "@codemirror/lint"
|
||||||
import { Compartment, EditorState } from "@codemirror/state"
|
import { Compartment, EditorState } from "@codemirror/state"
|
||||||
|
import type { Extension } from "@codemirror/state"
|
||||||
import { javascript } from "@codemirror/lang-javascript"
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
import { EditorModes } from "./"
|
import { EditorModes } from "./"
|
||||||
import { themeStore } from "@/stores/portal"
|
import { themeStore } from "@/stores/portal"
|
||||||
import type { EditorMode } from "@budibase/types"
|
import type { EditorMode } from "@budibase/types"
|
||||||
import type { BindingCompletion } from "@/types"
|
import type { BindingCompletion, CodeValidator } from "@/types"
|
||||||
|
import { validateHbsTemplate } from "./validator/hbs"
|
||||||
|
|
||||||
export let label: string | undefined = undefined
|
export let label: string | undefined = undefined
|
||||||
// TODO: work out what best type fits this
|
|
||||||
export let completions: BindingCompletion[] = []
|
export let completions: BindingCompletion[] = []
|
||||||
|
export let validations: CodeValidator | null = null
|
||||||
export let mode: EditorMode = EditorModes.Handlebars
|
export let mode: EditorMode = EditorModes.Handlebars
|
||||||
export let value: string | null = ""
|
export let value: string | null = ""
|
||||||
export let placeholder: string | null = null
|
export let placeholder: string | null = null
|
||||||
|
@ -248,7 +251,7 @@
|
||||||
// None of this is reactive, but it never has been, so we just assume most
|
// None of this is reactive, but it never has been, so we just assume most
|
||||||
// config flags aren't changed at runtime
|
// config flags aren't changed at runtime
|
||||||
// TODO: work out type for base
|
// TODO: work out type for base
|
||||||
const buildExtensions = (base: any[]) => {
|
const buildExtensions = (base: Extension[]) => {
|
||||||
let complete = [...base]
|
let complete = [...base]
|
||||||
|
|
||||||
if (autocompleteEnabled) {
|
if (autocompleteEnabled) {
|
||||||
|
@ -340,6 +343,24 @@
|
||||||
return complete
|
return complete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate(
|
||||||
|
value: string | null,
|
||||||
|
editor: EditorView | undefined,
|
||||||
|
mode: EditorMode,
|
||||||
|
validations: CodeValidator | null
|
||||||
|
) {
|
||||||
|
if (!value || !validations || !editor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === EditorModes.Handlebars) {
|
||||||
|
const diagnostics = validateHbsTemplate(value, validations)
|
||||||
|
editor.dispatch(setDiagnostics(editor.state, diagnostics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: validate(value, editor, mode, validations)
|
||||||
|
|
||||||
const initEditor = () => {
|
const initEditor = () => {
|
||||||
const baseExtensions = buildBaseExtensions()
|
const baseExtensions = buildBaseExtensions()
|
||||||
|
|
||||||
|
@ -366,7 +387,6 @@
|
||||||
<Label size="S">{label}</Label>
|
<Label size="S">{label}</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class={`code-editor ${mode?.name || ""}`}>
|
<div class={`code-editor ${mode?.name || ""}`}>
|
||||||
<div tabindex="-1" bind:this={textarea} />
|
<div tabindex="-1" bind:this={textarea} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -83,6 +83,8 @@ const helpersToCompletion = (
|
||||||
const helper = helpers[helperName]
|
const helper = helpers[helperName]
|
||||||
return {
|
return {
|
||||||
label: helperName,
|
label: helperName,
|
||||||
|
args: helper.args,
|
||||||
|
requiresBlock: helper.requiresBlock,
|
||||||
info: () => buildHelperInfoNode(helper),
|
info: () => buildHelperInfoNode(helper),
|
||||||
type: "helper",
|
type: "helper",
|
||||||
section: helperSection,
|
section: helperSection,
|
||||||
|
@ -136,9 +138,13 @@ export const hbAutocomplete = (
|
||||||
baseCompletions: BindingCompletionOption[]
|
baseCompletions: BindingCompletionOption[]
|
||||||
): BindingCompletion => {
|
): BindingCompletion => {
|
||||||
function coreCompletion(context: CompletionContext) {
|
function coreCompletion(context: CompletionContext) {
|
||||||
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
if (!baseCompletions.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
let options = baseCompletions || []
|
const bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
||||||
|
|
||||||
|
const options = baseCompletions
|
||||||
|
|
||||||
if (!bindingStart) {
|
if (!bindingStart) {
|
||||||
return null
|
return null
|
||||||
|
@ -149,7 +155,7 @@ export const hbAutocomplete = (
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const query = bindingStart.text.replace(match[0], "")
|
const query = bindingStart.text.replace(match[0], "")
|
||||||
let filtered = bindingFilter(options, query)
|
const filtered = bindingFilter(options, query)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: bindingStart.from + match[0].length,
|
from: bindingStart.from + match[0].length,
|
||||||
|
@ -169,8 +175,12 @@ export const jsAutocomplete = (
|
||||||
baseCompletions: BindingCompletionOption[]
|
baseCompletions: BindingCompletionOption[]
|
||||||
): BindingCompletion => {
|
): BindingCompletion => {
|
||||||
function coreCompletion(context: CompletionContext) {
|
function coreCompletion(context: CompletionContext) {
|
||||||
let jsBinding = wrappedAutocompleteMatch(context)
|
if (!baseCompletions.length) {
|
||||||
let options = baseCompletions || []
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsBinding = wrappedAutocompleteMatch(context)
|
||||||
|
const options = baseCompletions
|
||||||
|
|
||||||
if (jsBinding) {
|
if (jsBinding) {
|
||||||
// Accommodate spaces
|
// Accommodate spaces
|
||||||
|
@ -209,6 +219,10 @@ function setAutocomplete(
|
||||||
options: BindingCompletionOption[]
|
options: BindingCompletionOption[]
|
||||||
): BindingCompletion {
|
): BindingCompletion {
|
||||||
return function (context: CompletionContext) {
|
return function (context: CompletionContext) {
|
||||||
|
if (!options.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (wrappedAutocompleteMatch(context)) {
|
if (wrappedAutocompleteMatch(context)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* global hbs */
|
||||||
|
import Handlebars from "handlebars"
|
||||||
|
import type { Diagnostic } from "@codemirror/lint"
|
||||||
|
import { CodeValidator } from "@/types"
|
||||||
|
|
||||||
|
function isMustacheStatement(
|
||||||
|
node: hbs.AST.Statement
|
||||||
|
): node is hbs.AST.MustacheStatement {
|
||||||
|
return node.type === "MustacheStatement"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockStatement(
|
||||||
|
node: hbs.AST.Statement
|
||||||
|
): node is hbs.AST.BlockStatement {
|
||||||
|
return node.type === "BlockStatement"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPathExpression(
|
||||||
|
node: hbs.AST.Statement
|
||||||
|
): node is hbs.AST.PathExpression {
|
||||||
|
return node.type === "PathExpression"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateHbsTemplate(
|
||||||
|
text: string,
|
||||||
|
validations: CodeValidator
|
||||||
|
): Diagnostic[] {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = Handlebars.parse(text, {})
|
||||||
|
|
||||||
|
const lineOffsets: number[] = []
|
||||||
|
let offset = 0
|
||||||
|
for (const line of text.split("\n")) {
|
||||||
|
lineOffsets.push(offset)
|
||||||
|
offset += line.length + 1 // +1 for newline character
|
||||||
|
}
|
||||||
|
|
||||||
|
function traverseNodes(
|
||||||
|
nodes: hbs.AST.Statement[],
|
||||||
|
options?: {
|
||||||
|
ignoreMissing?: boolean
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const ignoreMissing = options?.ignoreMissing || false
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (isMustacheStatement(node) && isPathExpression(node.path)) {
|
||||||
|
const helperName = node.path.original
|
||||||
|
|
||||||
|
const from =
|
||||||
|
lineOffsets[node.loc.start.line - 1] + node.loc.start.column
|
||||||
|
const to = lineOffsets[node.loc.end.line - 1] + node.loc.end.column
|
||||||
|
|
||||||
|
if (!(helperName in validations)) {
|
||||||
|
if (!ignoreMissing) {
|
||||||
|
diagnostics.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
severity: "warning",
|
||||||
|
message: `"${helperName}" handler does not exist.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { arguments: expectedArguments = [], requiresBlock } =
|
||||||
|
validations[helperName]
|
||||||
|
|
||||||
|
if (requiresBlock && !isBlockStatement(node)) {
|
||||||
|
diagnostics.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
severity: "error",
|
||||||
|
message: `Helper "${helperName}" requires a body:\n{{#${helperName} ...}} [body] {{/${helperName}}}`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const providedParams = node.params
|
||||||
|
|
||||||
|
if (providedParams.length !== expectedArguments.length) {
|
||||||
|
diagnostics.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
severity: "error",
|
||||||
|
message: `Helper "${helperName}" expects ${
|
||||||
|
expectedArguments.length
|
||||||
|
} parameters (${expectedArguments.join(", ")}), but got ${
|
||||||
|
providedParams.length
|
||||||
|
}.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBlockStatement(node)) {
|
||||||
|
traverseNodes(node.program.body, { ignoreMissing: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseNodes(ast.body, { ignoreMissing: true })
|
||||||
|
} catch (e: any) {
|
||||||
|
diagnostics.push({
|
||||||
|
from: 0,
|
||||||
|
to: text.length,
|
||||||
|
severity: "error",
|
||||||
|
message: `The handlebars code is not valid:\n${e.message}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { validateHbsTemplate } from "../hbs"
|
||||||
|
import { CodeValidator } from "@/types"
|
||||||
|
|
||||||
|
describe("hbs validator", () => {
|
||||||
|
it("validate empty strings", () => {
|
||||||
|
const text = ""
|
||||||
|
const validators = {}
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("validate strings without hbs expressions", () => {
|
||||||
|
const text = "first line\nand another one"
|
||||||
|
const validators = {}
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("basic expressions", () => {
|
||||||
|
const validators = {
|
||||||
|
fieldName: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("validate valid expressions", () => {
|
||||||
|
const text = "{{ fieldName }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not throw on missing validations", () => {
|
||||||
|
const text = "{{ anotherFieldName }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Waiting for missing fields validation
|
||||||
|
it.skip("throws on untrimmed invalid expressions", () => {
|
||||||
|
const text = " {{ anotherFieldName }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 4,
|
||||||
|
message: `"anotherFieldName" handler does not exist.`,
|
||||||
|
severity: "warning",
|
||||||
|
to: 26,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Waiting for missing fields validation
|
||||||
|
it.skip("throws on invalid expressions between valid lines", () => {
|
||||||
|
const text =
|
||||||
|
"literal expression\nthe value is {{ anotherFieldName }}\nanother expression"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 32,
|
||||||
|
message: `"anotherFieldName" handler does not exist.`,
|
||||||
|
severity: "warning",
|
||||||
|
to: 54,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("expressions with whitespaces", () => {
|
||||||
|
const validators = {
|
||||||
|
[`field name`]: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("validates expressions with whitespaces", () => {
|
||||||
|
const text = `{{ [field name] }}`
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Waiting for missing fields validation
|
||||||
|
it.skip("throws if not wrapped between brackets", () => {
|
||||||
|
const text = `{{ field name }}`
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
message: `"field" handler does not exist.`,
|
||||||
|
severity: "warning",
|
||||||
|
to: 16,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("expressions with parameters", () => {
|
||||||
|
const validators: CodeValidator = {
|
||||||
|
helperFunction: {
|
||||||
|
arguments: ["a", "b", "c"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it("validate valid params", () => {
|
||||||
|
const text = "{{ helperFunction 1 99 'a' }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws on too few params", () => {
|
||||||
|
const text = "{{ helperFunction 100 }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
message: `Helper "helperFunction" expects 3 parameters (a, b, c), but got 1.`,
|
||||||
|
severity: "error",
|
||||||
|
to: 24,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws on too many params", () => {
|
||||||
|
const text = "{{ helperFunction 1 99 'a' 100 }}"
|
||||||
|
|
||||||
|
const result = validateHbsTemplate(text, validators)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
message: `Helper "helperFunction" expects 3 parameters (a, b, c), but got 4.`,
|
||||||
|
severity: "error",
|
||||||
|
to: 34,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -42,7 +42,7 @@
|
||||||
JSONValue,
|
JSONValue,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import type { Log } from "@budibase/string-templates"
|
import type { Log } from "@budibase/string-templates"
|
||||||
import type { BindingCompletion, BindingCompletionOption } from "@/types"
|
import type { CodeValidator } from "@/types"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
export let placeholder = null
|
export let placeholder = null
|
||||||
export let showTabBar = true
|
export let showTabBar = true
|
||||||
|
|
||||||
let mode: BindingMode | null
|
let mode: BindingMode
|
||||||
let sidePanel: SidePanel | null
|
let sidePanel: SidePanel | null
|
||||||
let initialValueJS = value?.startsWith?.("{{ js ")
|
let initialValueJS = value?.startsWith?.("{{ js ")
|
||||||
let jsValue: string | null = initialValueJS ? value : null
|
let jsValue: string | null = initialValueJS ? value : null
|
||||||
|
@ -88,13 +88,37 @@
|
||||||
| null
|
| null
|
||||||
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
||||||
$: requestEval(runtimeExpression, context, snippets)
|
$: requestEval(runtimeExpression, context, snippets)
|
||||||
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
|
||||||
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
||||||
$: hbsCompletions = getHBSCompletions(bindingCompletions)
|
|
||||||
$: jsCompletions = getJSCompletions(bindingCompletions, snippets, {
|
$: bindingOptions = bindingsToCompletions(bindings, editorMode)
|
||||||
useHelpers: allowHelpers,
|
$: helperOptions = allowHelpers ? getHelperCompletions(editorMode) : []
|
||||||
useSnippets,
|
$: snippetsOptions =
|
||||||
})
|
usingJS && useSnippets && snippets?.length ? snippets : []
|
||||||
|
|
||||||
|
$: completions = !usingJS
|
||||||
|
? [hbAutocomplete([...bindingOptions, ...helperOptions])]
|
||||||
|
: [
|
||||||
|
jsAutocomplete(bindingOptions),
|
||||||
|
jsHelperAutocomplete(helperOptions),
|
||||||
|
snippetAutoComplete(snippetsOptions),
|
||||||
|
]
|
||||||
|
|
||||||
|
$: validations = {
|
||||||
|
...bindingOptions.reduce<CodeValidator>((validations, option) => {
|
||||||
|
validations[option.label] = {
|
||||||
|
arguments: [],
|
||||||
|
}
|
||||||
|
return validations
|
||||||
|
}, {}),
|
||||||
|
...helperOptions.reduce<CodeValidator>((validations, option) => {
|
||||||
|
validations[option.label] = {
|
||||||
|
arguments: option.args,
|
||||||
|
requiresBlock: option.requiresBlock,
|
||||||
|
}
|
||||||
|
return validations
|
||||||
|
}, {}),
|
||||||
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// Ensure a valid side panel option is always selected
|
// Ensure a valid side panel option is always selected
|
||||||
if (sidePanel && !sidePanelOptions.includes(sidePanel)) {
|
if (sidePanel && !sidePanelOptions.includes(sidePanel)) {
|
||||||
|
@ -102,38 +126,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHBSCompletions = (bindingCompletions: BindingCompletionOption[]) => {
|
|
||||||
return [
|
|
||||||
hbAutocomplete([
|
|
||||||
...bindingCompletions,
|
|
||||||
...getHelperCompletions(EditorModes.Handlebars),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getJSCompletions = (
|
|
||||||
bindingCompletions: BindingCompletionOption[],
|
|
||||||
snippets: Snippet[] | null,
|
|
||||||
config: {
|
|
||||||
useHelpers: boolean
|
|
||||||
useSnippets: boolean
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const completions: BindingCompletion[] = []
|
|
||||||
if (bindingCompletions.length) {
|
|
||||||
completions.push(jsAutocomplete([...bindingCompletions]))
|
|
||||||
}
|
|
||||||
if (config.useHelpers) {
|
|
||||||
completions.push(
|
|
||||||
jsHelperAutocomplete([...getHelperCompletions(EditorModes.JS)])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (config.useSnippets && snippets) {
|
|
||||||
completions.push(snippetAutoComplete(snippets))
|
|
||||||
}
|
|
||||||
return completions
|
|
||||||
}
|
|
||||||
|
|
||||||
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
||||||
let options = []
|
let options = []
|
||||||
if (allowHBS) {
|
if (allowHBS) {
|
||||||
|
@ -213,7 +205,7 @@
|
||||||
bindings: EnrichedBinding[],
|
bindings: EnrichedBinding[],
|
||||||
context: any,
|
context: any,
|
||||||
snippets: Snippet[] | null
|
snippets: Snippet[] | null
|
||||||
) => {
|
): EnrichedBinding[] => {
|
||||||
// Create a single big array to enrich in one go
|
// Create a single big array to enrich in one go
|
||||||
const bindingStrings = bindings.map(binding => {
|
const bindingStrings = bindings.map(binding => {
|
||||||
if (binding.runtimeBinding.startsWith('trim "')) {
|
if (binding.runtimeBinding.startsWith('trim "')) {
|
||||||
|
@ -290,7 +282,7 @@
|
||||||
jsValue = null
|
jsValue = null
|
||||||
hbsValue = null
|
hbsValue = null
|
||||||
updateValue(null)
|
updateValue(null)
|
||||||
mode = targetMode
|
mode = targetMode!
|
||||||
targetMode = null
|
targetMode = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,13 +357,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
{#if mode === BindingMode.Text}
|
{#if mode === BindingMode.Text}
|
||||||
{#key hbsCompletions}
|
{#key completions}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={hbsValue}
|
value={hbsValue}
|
||||||
on:change={onChangeHBSValue}
|
on:change={onChangeHBSValue}
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:insertAtPos
|
bind:insertAtPos
|
||||||
completions={hbsCompletions}
|
{completions}
|
||||||
|
{validations}
|
||||||
autofocus={autofocusEditor}
|
autofocus={autofocusEditor}
|
||||||
placeholder={placeholder ||
|
placeholder={placeholder ||
|
||||||
"Add bindings by typing {{ or use the menu on the right"}
|
"Add bindings by typing {{ or use the menu on the right"}
|
||||||
|
@ -379,18 +372,18 @@
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{:else if mode === BindingMode.JavaScript}
|
{:else if mode === BindingMode.JavaScript}
|
||||||
{#key jsCompletions}
|
{#key completions}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={jsValue ? decodeJSBinding(jsValue) : jsValue}
|
value={jsValue ? decodeJSBinding(jsValue) : jsValue}
|
||||||
on:change={onChangeJSValue}
|
on:change={onChangeJSValue}
|
||||||
completions={jsCompletions}
|
{completions}
|
||||||
mode={EditorModes.JS}
|
mode={EditorModes.JS}
|
||||||
bind:getCaretPosition
|
bind:getCaretPosition
|
||||||
bind:insertAtPos
|
bind:insertAtPos
|
||||||
autofocus={autofocusEditor}
|
autofocus={autofocusEditor}
|
||||||
placeholder={placeholder ||
|
placeholder={placeholder ||
|
||||||
"Add bindings by typing $ or use the menu on the right"}
|
"Add bindings by typing $ or use the menu on the right"}
|
||||||
jsBindingWrapping={bindingCompletions.length > 0}
|
jsBindingWrapping={completions.length > 0}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -5,4 +5,15 @@ export type BindingCompletion = (context: CompletionContext) => {
|
||||||
options: Completion[]
|
options: Completion[]
|
||||||
} | null
|
} | null
|
||||||
|
|
||||||
export type BindingCompletionOption = Completion
|
export interface BindingCompletionOption extends Completion {
|
||||||
|
args?: any[]
|
||||||
|
requiresBlock?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CodeValidator = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
arguments?: any[]
|
||||||
|
requiresBlock?: boolean
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
|
@ -14,5 +14,6 @@
|
||||||
"assets/*": ["assets/*"],
|
"assets/*": ["assets/*"],
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"exclude": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export interface Helper {
|
export interface Helper {
|
||||||
example: string
|
example: string
|
||||||
description: string
|
description: string
|
||||||
|
args: any[]
|
||||||
|
requiresBlock?: boolean
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue