mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-20 07:41:22 +02: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 {
|
@tokens {
|
||||||
Text { ![$] Text? | "$" (@eof | ![{] Text?) }
|
Text { ![$] Text? | "$" (@eof | ![{] Text? | "{" ![[] Text?) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@external propSource highlight from "./highlight"
|
@external propSource highlight from "./highlight"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
export const Template = 1,
|
export const
|
||||||
|
Template = 1,
|
||||||
Tag = 2,
|
Tag = 2,
|
||||||
TagOpen = 3,
|
TagOpen = 3,
|
||||||
TagContent = 4,
|
TagContent = 4,
|
||||||
TagClose = 5,
|
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.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import { LocalTokenGroup, LRParser } from '@lezer/lr';
|
import {LRParser, LocalTokenGroup} from "@lezer/lr"
|
||||||
import { highlight } from './highlight';
|
import {highlight} from "./highlight"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states:
|
states: "!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
|
||||||
"!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
|
stateData: "g~OUROYPO~OSTO~OSTOTXO~O",
|
||||||
stateData: 'g~OUROYPO~OSTO~OSTOTXO~O',
|
goto: "nXPPY^PPPbhTROSTQOSQSORVSQUQRWU",
|
||||||
goto: 'nXPPY^PPPbhTROSTQOSQSORVSQUQRWU',
|
nodeNames: "⚠ Template Tag TagOpen TagContent TagClose Text",
|
||||||
nodeNames: '⚠ Template Tag TagOpen TagContent TagClose Text',
|
|
||||||
maxTerm: 10,
|
maxTerm: 10,
|
||||||
propSources: [highlight],
|
propSources: [highlight],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 2,
|
repeatNodeCount: 2,
|
||||||
tokenData:
|
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~",
|
||||||
"#]~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)],
|
||||||
tokenizers: [1, new LocalTokenGroup('b~RP#P#QU~XP#q#r[~aOT~~', 17, 4)],
|
topRules: {"Template":[0,1]},
|
||||||
topRules: { Template: [0, 1] },
|
tokenPrec: 0
|
||||||
tokenPrec: 0,
|
})
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user