mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 15:31:19 +02:00
Run oxfmt across repo, add format script and docs
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
||||
import { startCompletion } from '@codemirror/autocomplete';
|
||||
import type { TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import type { Completion, CompletionContext } from "@codemirror/autocomplete";
|
||||
import { startCompletion } from "@codemirror/autocomplete";
|
||||
import type { TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
|
||||
const openTag = '${[ ';
|
||||
const closeTag = ' ]}';
|
||||
const openTag = "${[ ";
|
||||
const closeTag = " ]}";
|
||||
|
||||
export type TwigCompletionOptionVariable = {
|
||||
type: 'variable';
|
||||
type: "variable";
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionNamespace = {
|
||||
type: 'namespace';
|
||||
type: "namespace";
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionFunction = TemplateFunction & {
|
||||
type: 'function';
|
||||
type: "function";
|
||||
};
|
||||
|
||||
export type TwigCompletionOption = (
|
||||
@@ -50,17 +50,17 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
|
||||
const completions: Completion[] = options
|
||||
.flatMap((o): Completion[] => {
|
||||
const matchSegments = toMatch.text.replace(/^\$/, '').split('.');
|
||||
const optionSegments = o.name.split('.');
|
||||
const matchSegments = toMatch.text.replace(/^\$/, "").split(".");
|
||||
const optionSegments = o.name.split(".");
|
||||
|
||||
// If not on the last segment, only complete the namespace
|
||||
if (matchSegments.length < optionSegments.length) {
|
||||
const prefix = optionSegments.slice(0, matchSegments.length).join('.');
|
||||
const prefix = optionSegments.slice(0, matchSegments.length).join(".");
|
||||
return [
|
||||
{
|
||||
label: `${prefix}.*`,
|
||||
type: 'namespace',
|
||||
detail: 'namespace',
|
||||
type: "namespace",
|
||||
detail: "namespace",
|
||||
apply: (view, _completion, from, to) => {
|
||||
const insert = `${prefix}.`;
|
||||
view.dispatch({
|
||||
@@ -75,13 +75,13 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
}
|
||||
|
||||
// If on the last segment, wrap the entire tag
|
||||
const inner = o.type === 'function' ? `${o.name}()` : o.name;
|
||||
const inner = o.type === "function" ? `${o.name}()` : o.name;
|
||||
return [
|
||||
{
|
||||
label: o.name,
|
||||
info: o.description,
|
||||
detail: o.type,
|
||||
type: o.type === 'variable' ? 'variable' : 'function',
|
||||
type: o.type === "variable" ? "variable" : "function",
|
||||
apply: (view, _completion, from, to) => {
|
||||
const insert = openTag + inner + closeTag;
|
||||
view.dispatch({
|
||||
@@ -94,7 +94,7 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
})
|
||||
.filter((v) => v != null);
|
||||
|
||||
const uniqueCompletions = uniqueBy(completions, 'label');
|
||||
const uniqueCompletions = uniqueBy(completions, "label");
|
||||
const sortedCompletions = uniqueCompletions.sort((a, b) => {
|
||||
const boostDiff = defaultBoost(b) - defaultBoost(a);
|
||||
if (boostDiff !== 0) return boostDiff;
|
||||
@@ -119,9 +119,9 @@ export function uniqueBy<T, K extends keyof T>(arr: T[], key: K): T[] {
|
||||
}
|
||||
|
||||
export function defaultBoost(o: Completion) {
|
||||
if (o.type === 'variable') return 4;
|
||||
if (o.type === 'constant') return 3;
|
||||
if (o.type === 'function') return 2;
|
||||
if (o.type === 'namespace') return 1;
|
||||
if (o.type === "variable") return 4;
|
||||
if (o.type === "constant") return 3;
|
||||
if (o.type === "function") return 2;
|
||||
if (o.type === "namespace") return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { LanguageSupport } from '@codemirror/language';
|
||||
import { LRLanguage } from '@codemirror/language';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import type { WrappedEnvironmentVariable } from '../../../../hooks/useEnvironmentVariables';
|
||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||
import { genericCompletion } from '../genericCompletion';
|
||||
import { textLanguage } from '../text/extension';
|
||||
import type { TwigCompletionOption } from './completion';
|
||||
import { twigCompletion } from './completion';
|
||||
import { templateTagsPlugin } from './templateTags';
|
||||
import { parser as twigParser } from './twig';
|
||||
import type { LanguageSupport } from "@codemirror/language";
|
||||
import { LRLanguage } from "@codemirror/language";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { parseMixed } from "@lezer/common";
|
||||
import type { WrappedEnvironmentVariable } from "../../../../hooks/useEnvironmentVariables";
|
||||
import type { GenericCompletionConfig } from "../genericCompletion";
|
||||
import { genericCompletion } from "../genericCompletion";
|
||||
import { textLanguage } from "../text/extension";
|
||||
import type { TwigCompletionOption } from "./completion";
|
||||
import { twigCompletion } from "./completion";
|
||||
import { templateTagsPlugin } from "./templateTags";
|
||||
import { parser as twigParser } from "./twig";
|
||||
|
||||
export function twig({
|
||||
base,
|
||||
@@ -35,7 +35,7 @@ export function twig({
|
||||
environmentVariables.map((v) => ({
|
||||
name: v.variable.name,
|
||||
value: v.variable.value,
|
||||
type: 'variable',
|
||||
type: "variable",
|
||||
label: v.variable.name,
|
||||
description: `Inherited from ${v.source}`,
|
||||
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
||||
@@ -74,12 +74,12 @@ function mixLanguage(base: LanguageSupport): LRLanguage {
|
||||
|
||||
return {
|
||||
parser: base.language.parser,
|
||||
overlay: (node) => node.type.name === 'Text',
|
||||
overlay: (node) => node.type.name === "Text",
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const language = LRLanguage.define({ name: 'twig', parser });
|
||||
const language = LRLanguage.define({ name: "twig", parser });
|
||||
mixedLanguagesCache[base.language.name] = language;
|
||||
return language;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
TagOpen: t.bracket,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Range } from '@codemirror/state';
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
|
||||
class PathPlaceholderWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
@@ -22,15 +22,15 @@ class PathPlaceholderWidget extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
elt.className = 'x-theme-templateTag x-theme-templateTag--secondary template-tag';
|
||||
const elt = document.createElement("span");
|
||||
elt.className = "x-theme-templateTag x-theme-templateTag--secondary template-tag";
|
||||
elt.textContent = this.rawText;
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
elt.addEventListener("click", this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement) {
|
||||
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||
dom.removeEventListener("click", this.#clickListenerCallback);
|
||||
super.destroy(dom);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,14 @@ function pathParameters(
|
||||
from,
|
||||
to,
|
||||
enter(node) {
|
||||
if (node.name === 'Text') {
|
||||
if (node.name === "Text") {
|
||||
// Find the `url` node and then jump into it to find the placeholders
|
||||
for (let i = node.from; i < node.to; i++) {
|
||||
const innerTree = syntaxTree(view.state).resolveInner(i);
|
||||
if (innerTree.node.name === 'url') {
|
||||
if (innerTree.node.name === "url") {
|
||||
innerTree.toTree().iterate({
|
||||
enter(node) {
|
||||
if (node.name !== 'Placeholder') return;
|
||||
if (node.name !== "Placeholder") return;
|
||||
const globalFrom = innerTree.node.from + node.from;
|
||||
const globalTo = innerTree.node.from + node.to;
|
||||
const rawText = view.state.doc.sliceString(globalFrom, globalTo);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Range } from '@codemirror/state';
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import type { SyntaxNodeRef } from '@lezer/common';
|
||||
import { applyFormInputDefaults, validateTemplateFunctionArgs } from '@yaakapp-internal/lib';
|
||||
import type { FormInput, JsonPrimitive, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import { parseTemplate } from '@yaakapp-internal/templates';
|
||||
import type { TwigCompletionOption } from './completion';
|
||||
import { collectArgumentValues } from './util';
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
import type { SyntaxNodeRef } from "@lezer/common";
|
||||
import { applyFormInputDefaults, validateTemplateFunctionArgs } from "@yaakapp-internal/lib";
|
||||
import type { FormInput, JsonPrimitive, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import { parseTemplate } from "@yaakapp-internal/templates";
|
||||
import type { TwigCompletionOption } from "./completion";
|
||||
import { collectArgumentValues } from "./util";
|
||||
|
||||
class TemplateTagWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
@@ -34,24 +34,24 @@ class TemplateTagWidget extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
const elt = document.createElement("span");
|
||||
elt.className = `x-theme-templateTag template-tag ${
|
||||
this.option.invalid
|
||||
? 'x-theme-templateTag--danger'
|
||||
: this.option.type === 'variable'
|
||||
? 'x-theme-templateTag--primary'
|
||||
: 'x-theme-templateTag--info'
|
||||
? "x-theme-templateTag--danger"
|
||||
: this.option.type === "variable"
|
||||
? "x-theme-templateTag--primary"
|
||||
: "x-theme-templateTag--info"
|
||||
}`;
|
||||
elt.title = this.option.invalid ? 'Not Found' : (this.option.value ?? '');
|
||||
elt.setAttribute('data-tag-type', this.option.type);
|
||||
if (typeof this.option.label === 'string') elt.textContent = this.option.label;
|
||||
elt.title = this.option.invalid ? "Not Found" : (this.option.value ?? "");
|
||||
elt.setAttribute("data-tag-type", this.option.type);
|
||||
if (typeof this.option.label === "string") elt.textContent = this.option.label;
|
||||
else elt.appendChild(this.option.label);
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
elt.addEventListener("click", this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement) {
|
||||
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||
dom.removeEventListener("click", this.#clickListenerCallback);
|
||||
super.destroy(dom);
|
||||
}
|
||||
|
||||
@@ -72,34 +72,34 @@ function templateTags(
|
||||
from,
|
||||
to,
|
||||
enter(node) {
|
||||
if (node.name === 'Tag') {
|
||||
if (node.name === "Tag") {
|
||||
// Don't decorate if the cursor is inside the match
|
||||
if (isSelectionInsideNode(view, node)) return;
|
||||
|
||||
const rawTag = view.state.doc.sliceString(node.from, node.to);
|
||||
|
||||
// TODO: Search `node.tree` instead of using Regex here
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, "").replace(/\s*]}$/, "");
|
||||
let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner;
|
||||
|
||||
if (inner.includes('\n')) {
|
||||
if (inner.includes("\n")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The beta named the function `Response` but was changed in stable.
|
||||
// Keep this here for a while because there's no easy way to migrate
|
||||
if (name === 'Response') {
|
||||
name = 'response';
|
||||
if (name === "Response") {
|
||||
name = "response";
|
||||
}
|
||||
|
||||
let option = options.find(
|
||||
(o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)),
|
||||
(o) => o.name === name || (o.type === "function" && o.aliases?.includes(name)),
|
||||
);
|
||||
|
||||
if (option == null) {
|
||||
const from = node.from; // Cache here so the reference doesn't change
|
||||
option = {
|
||||
type: 'variable',
|
||||
type: "variable",
|
||||
invalid: true,
|
||||
name: inner,
|
||||
value: null,
|
||||
@@ -110,7 +110,7 @@ function templateTags(
|
||||
};
|
||||
}
|
||||
|
||||
if (option.type === 'function') {
|
||||
if (option.type === "function") {
|
||||
const tokens = parseTemplate(rawTag);
|
||||
const rawValues = collectArgumentValues(tokens, option);
|
||||
const values = applyFormInputDefaults(option.args, rawValues);
|
||||
@@ -175,49 +175,49 @@ function makeFunctionLabel(
|
||||
): HTMLElement | string {
|
||||
if (fn.args.length === 0) return fn.name;
|
||||
|
||||
const $outer = document.createElement('span');
|
||||
$outer.className = 'fn';
|
||||
const $bOpen = document.createElement('span');
|
||||
$bOpen.className = 'fn-bracket';
|
||||
$bOpen.textContent = '(';
|
||||
const $outer = document.createElement("span");
|
||||
$outer.className = "fn";
|
||||
const $bOpen = document.createElement("span");
|
||||
$bOpen.className = "fn-bracket";
|
||||
$bOpen.textContent = "(";
|
||||
$outer.appendChild(document.createTextNode(fn.name));
|
||||
$outer.appendChild($bOpen);
|
||||
|
||||
const $inner = document.createElement('span');
|
||||
$inner.className = 'fn-inner';
|
||||
$inner.title = '';
|
||||
const $inner = document.createElement("span");
|
||||
$inner.className = "fn-inner";
|
||||
$inner.title = "";
|
||||
fn.previewArgs?.forEach((name: string, i: number, all: string[]) => {
|
||||
const v = String(values[name] || '');
|
||||
const v = String(values[name] || "");
|
||||
if (!v) return;
|
||||
if (all.length > 1) {
|
||||
const $c = document.createElement('span');
|
||||
$c.className = 'fn-arg-name';
|
||||
const $c = document.createElement("span");
|
||||
$c.className = "fn-arg-name";
|
||||
$c.textContent = i > 0 ? `, ${name}=` : `${name}=`;
|
||||
$inner.appendChild($c);
|
||||
}
|
||||
|
||||
const $v = document.createElement('span');
|
||||
$v.className = 'fn-arg-value';
|
||||
$v.textContent = v.includes(' ') ? `'${v}'` : v;
|
||||
const $v = document.createElement("span");
|
||||
$v.className = "fn-arg-value";
|
||||
$v.textContent = v.includes(" ") ? `'${v}'` : v;
|
||||
$inner.appendChild($v);
|
||||
});
|
||||
fn.args.forEach((a: FormInput, i: number) => {
|
||||
if (!('name' in a)) return;
|
||||
if (!("name" in a)) return;
|
||||
const v = values[a.name];
|
||||
if (v == null) return;
|
||||
if (i > 0) $inner.title += '\n';
|
||||
if (i > 0) $inner.title += "\n";
|
||||
$inner.title += `${a.name} = ${JSON.stringify(v)}`;
|
||||
});
|
||||
|
||||
if ($inner.childNodes.length === 0) {
|
||||
$inner.appendChild(document.createTextNode('…'));
|
||||
$inner.appendChild(document.createTextNode("…"));
|
||||
}
|
||||
|
||||
$outer.appendChild($inner);
|
||||
|
||||
const $bClose = document.createElement('span');
|
||||
$bClose.className = 'fn-bracket';
|
||||
$bClose.textContent = ')';
|
||||
const $bClose = document.createElement("span");
|
||||
$bClose.className = "fn-bracket";
|
||||
$bClose.textContent = ")";
|
||||
$outer.appendChild($bClose);
|
||||
|
||||
return $outer;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* oxlint-disable no-template-curly-in-string */
|
||||
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { parser } from './twig';
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parser } from "./twig";
|
||||
|
||||
function getNodeNames(input: string): string[] {
|
||||
const tree = parser.parse(input);
|
||||
const nodes: string[] = [];
|
||||
const cursor = tree.cursor();
|
||||
do {
|
||||
if (cursor.name !== 'Template') {
|
||||
if (cursor.name !== "Template") {
|
||||
nodes.push(cursor.name);
|
||||
}
|
||||
} while (cursor.next());
|
||||
@@ -16,93 +16,93 @@ function getNodeNames(input: string): string[] {
|
||||
}
|
||||
|
||||
function hasTag(input: string): boolean {
|
||||
return getNodeNames(input).includes('Tag');
|
||||
return getNodeNames(input).includes("Tag");
|
||||
}
|
||||
|
||||
function hasError(input: string): boolean {
|
||||
return getNodeNames(input).includes('⚠');
|
||||
return getNodeNames(input).includes("⚠");
|
||||
}
|
||||
|
||||
describe('twig grammar', () => {
|
||||
describe('${[var]} format (valid template tags)', () => {
|
||||
test('parses simple variable as Tag', () => {
|
||||
expect(hasTag('${[var]}')).toBe(true);
|
||||
expect(hasError('${[var]}')).toBe(false);
|
||||
describe("twig grammar", () => {
|
||||
describe("${[var]} format (valid template tags)", () => {
|
||||
test("parses simple variable as Tag", () => {
|
||||
expect(hasTag("${[var]}")).toBe(true);
|
||||
expect(hasError("${[var]}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses variable with whitespace as Tag', () => {
|
||||
expect(hasTag('${[ var ]}')).toBe(true);
|
||||
expect(hasError('${[ var ]}')).toBe(false);
|
||||
test("parses variable with whitespace as Tag", () => {
|
||||
expect(hasTag("${[ var ]}")).toBe(true);
|
||||
expect(hasError("${[ var ]}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses embedded variable as Tag', () => {
|
||||
expect(hasTag('hello ${[name]} world')).toBe(true);
|
||||
expect(hasError('hello ${[name]} world')).toBe(false);
|
||||
test("parses embedded variable as Tag", () => {
|
||||
expect(hasTag("hello ${[name]} world")).toBe(true);
|
||||
expect(hasError("hello ${[name]} world")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses function call as Tag', () => {
|
||||
expect(hasTag('${[fn()]}')).toBe(true);
|
||||
expect(hasError('${[fn()]}')).toBe(false);
|
||||
test("parses function call as Tag", () => {
|
||||
expect(hasTag("${[fn()]}")).toBe(true);
|
||||
expect(hasError("${[fn()]}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('${var} format (should be plain text, not tags)', () => {
|
||||
test('parses ${var} as plain Text without errors', () => {
|
||||
expect(hasTag('${var}')).toBe(false);
|
||||
expect(hasError('${var}')).toBe(false);
|
||||
describe("${var} format (should be plain text, not tags)", () => {
|
||||
test("parses ${var} as plain Text without errors", () => {
|
||||
expect(hasTag("${var}")).toBe(false);
|
||||
expect(hasError("${var}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses embedded ${var} as plain Text', () => {
|
||||
expect(hasTag('hello ${name} world')).toBe(false);
|
||||
expect(hasError('hello ${name} world')).toBe(false);
|
||||
test("parses embedded ${var} as plain Text", () => {
|
||||
expect(hasTag("hello ${name} world")).toBe(false);
|
||||
expect(hasError("hello ${name} world")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses JSON with ${var} as plain Text', () => {
|
||||
test("parses JSON with ${var} as plain Text", () => {
|
||||
const json = '{"key": "${value}"}';
|
||||
expect(hasTag(json)).toBe(false);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
|
||||
test('parses multiple ${var} as plain Text', () => {
|
||||
expect(hasTag('${a} and ${b}')).toBe(false);
|
||||
expect(hasError('${a} and ${b}')).toBe(false);
|
||||
test("parses multiple ${var} as plain Text", () => {
|
||||
expect(hasTag("${a} and ${b}")).toBe(false);
|
||||
expect(hasError("${a} and ${b}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed content', () => {
|
||||
test('distinguishes ${var} from ${[var]} in same string', () => {
|
||||
const input = '${plain} and ${[tag]}';
|
||||
describe("mixed content", () => {
|
||||
test("distinguishes ${var} from ${[var]} in same string", () => {
|
||||
const input = "${plain} and ${[tag]}";
|
||||
expect(hasTag(input)).toBe(true);
|
||||
expect(hasError(input)).toBe(false);
|
||||
});
|
||||
|
||||
test('parses JSON with ${[var]} as having Tag', () => {
|
||||
test("parses JSON with ${[var]} as having Tag", () => {
|
||||
const json = '{"key": "${[value]}"}';
|
||||
expect(hasTag(json)).toBe(true);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('handles $ at end of string', () => {
|
||||
expect(hasError('hello$')).toBe(false);
|
||||
expect(hasTag('hello$')).toBe(false);
|
||||
describe("edge cases", () => {
|
||||
test("handles $ at end of string", () => {
|
||||
expect(hasError("hello$")).toBe(false);
|
||||
expect(hasTag("hello$")).toBe(false);
|
||||
});
|
||||
|
||||
test('handles ${ at end of string without crash', () => {
|
||||
test("handles ${ at end of string without crash", () => {
|
||||
// Incomplete syntax may produce errors, but should not crash
|
||||
expect(() => parser.parse('hello${')).not.toThrow();
|
||||
expect(() => parser.parse("hello${")).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles ${[ without closing without crash', () => {
|
||||
test("handles ${[ without closing without crash", () => {
|
||||
// Unclosed tag may produce partial match, but should not crash
|
||||
expect(() => parser.parse('${[unclosed')).not.toThrow();
|
||||
expect(() => parser.parse("${[unclosed")).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles empty ${[]}', () => {
|
||||
test("handles empty ${[]}", () => {
|
||||
// Empty tags may or may not be valid depending on grammar
|
||||
// Just ensure no crash
|
||||
expect(() => parser.parse('${[]}')).not.toThrow();
|
||||
expect(() => parser.parse("${[]}")).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LocalTokenGroup, LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LocalTokenGroup, LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
|
||||
stateData: 'g~OUROYPO~OSTO~OSTOTXO~O',
|
||||
goto: 'nXPPY^PPPbhTROSTQOSQSORVSQUQRWU',
|
||||
nodeNames: '⚠ Template Tag TagOpen TagContent TagClose Text',
|
||||
stateData: "g~OUROYPO~OSTO~OSTOTXO~O",
|
||||
goto: "nXPPY^PPPbhTROSTQOSQSORVSQUQRWU",
|
||||
nodeNames: "⚠ Template Tag TagOpen TagContent TagClose Text",
|
||||
maxTerm: 10,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 2,
|
||||
tokenData:
|
||||
"#{~RTOtbtu!zu;'Sb;'S;=`!o<%lOb~gTU~Otbtuvu;'Sb;'S;=`!o<%lOb~yVO#ob#o#p!`#p;'Sb;'S;=`!o<%l~b~Ob~~!u~!cSO!}b#O;'Sb;'S;=`!o<%lOb~!rP;=`<%lb~!zOU~~!}VO#ob#o#p#d#p;'Sb;'S;=`!o<%l~b~Ob~~!u~#gTO!}b!}#O#v#O;'Sb;'S;=`!o<%lOb~#{OY~",
|
||||
tokenizers: [1, new LocalTokenGroup('b~RP#P#QU~XP#q#r[~aOT~~', 17, 4)],
|
||||
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
|
||||
topRules: { Template: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FormInput, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import type { Tokens } from '@yaakapp-internal/templates';
|
||||
import type { FormInput, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import type { Tokens } from "@yaakapp-internal/templates";
|
||||
|
||||
/**
|
||||
* Process the initial tokens from the template and merge those with the default values pulled from
|
||||
@@ -8,21 +8,21 @@ import type { Tokens } from '@yaakapp-internal/templates';
|
||||
export function collectArgumentValues(initialTokens: Tokens, templateFunction: TemplateFunction) {
|
||||
const initial: Record<string, string | boolean> = {};
|
||||
const initialArgs =
|
||||
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
||||
initialTokens.tokens[0]?.type === "tag" && initialTokens.tokens[0]?.val.type === "fn"
|
||||
? initialTokens.tokens[0]?.val.args
|
||||
: [];
|
||||
|
||||
const processArg = (arg: FormInput) => {
|
||||
if ('inputs' in arg && arg.inputs) {
|
||||
if ("inputs" in arg && arg.inputs) {
|
||||
arg.inputs.forEach(processArg);
|
||||
}
|
||||
if (!('name' in arg)) return;
|
||||
if (!("name" in arg)) return;
|
||||
|
||||
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
||||
const initialArgValue =
|
||||
initialArg?.value.type === 'str'
|
||||
initialArg?.value.type === "str"
|
||||
? initialArg?.value.text
|
||||
: initialArg?.value.type === 'bool'
|
||||
: initialArg?.value.type === "bool"
|
||||
? initialArg.value.value
|
||||
: undefined;
|
||||
const value = initialArgValue ?? arg.defaultValue;
|
||||
|
||||
Reference in New Issue
Block a user