mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 22:40:26 +01:00
Fix variable matching in twig grammar to ignore ${var} format (#330)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
}
|
||||
|
||||
@tokens {
|
||||
Text { ![$] Text? | "$" (@eof | ![{] Text?) }
|
||||
Text { ![$] Text? | "$" (@eof | ![{] Text? | "{" ![[] Text?) }
|
||||
}
|
||||
|
||||
@external propSource highlight from "./highlight"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const Template = 1,
|
||||
export const
|
||||
Template = 1,
|
||||
Tag = 2,
|
||||
TagOpen = 3,
|
||||
TagContent = 4,
|
||||
TagClose = 5,
|
||||
Text = 6;
|
||||
Text = 6
|
||||
|
||||
106
src-web/components/core/Editor/twig/twig.test.ts
Normal file
106
src-web/components/core/Editor/twig/twig.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
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') {
|
||||
nodes.push(cursor.name);
|
||||
}
|
||||
} while (cursor.next());
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function hasTag(input: string): boolean {
|
||||
return getNodeNames(input).includes('Tag');
|
||||
}
|
||||
|
||||
function hasError(input: string): boolean {
|
||||
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);
|
||||
});
|
||||
|
||||
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 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);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
test('handles ${ at end of string without crash', () => {
|
||||
// Incomplete syntax may produce errors, but should not crash
|
||||
expect(() => parser.parse('hello${')).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles ${[ without closing without crash', () => {
|
||||
// Unclosed tag may produce partial match, but should not crash
|
||||
expect(() => parser.parse('${[unclosed')).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles empty ${[]}', () => {
|
||||
// Empty tags may or may not be valid depending on grammar
|
||||
// Just ensure no crash
|
||||
expect(() => parser.parse('${[]}')).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,18 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LocalTokenGroup, LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import {LRParser, LocalTokenGroup} 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',
|
||||
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",
|
||||
maxTerm: 10,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 2,
|
||||
tokenData:
|
||||
"#]~RTOtbtu!hu;'Sb;'S;=`!]<%lOb~gTU~Otbtuvu;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOU~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TP!}#O#W~#]OY~",
|
||||
tokenizers: [1, new LocalTokenGroup('b~RP#P#QU~XP#q#r[~aOT~~', 17, 4)],
|
||||
topRules: { Template: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
});
|
||||
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)],
|
||||
topRules: {"Template":[0,1]},
|
||||
tokenPrec: 0
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user