diff --git a/packages/langium-cli/src/generator/grammar-serializer.ts b/packages/langium-cli/src/generator/grammar-serializer.ts index 03aacec08..8bf0e8ee8 100644 --- a/packages/langium-cli/src/generator/grammar-serializer.ts +++ b/packages/langium-cli/src/generator/grammar-serializer.ts @@ -41,9 +41,13 @@ export function serializeGrammar(services: LangiumCoreServices, grammars: Gramma }); // The json serializer returns strings with \n line delimiter by default // We need to translate these line endings to the OS specific line ending - const json = normalizeEOL(serializedGrammar + let json = normalizeEOL(serializedGrammar .replace(/\\/g, '\\\\') .replace(new RegExp(delimiter, 'g'), '\\' + delimiter)); + if (!production) { + // Escape ${ in template strings + json = json.replace(/\${/g, '\\${'); + } return expandToNode` let loaded${grammar.name}Grammar: Grammar | undefined; diff --git a/packages/langium-cli/test/generator/grammar-serializer.test.ts b/packages/langium-cli/test/generator/grammar-serializer.test.ts index ff924d5d1..19959d933 100644 --- a/packages/langium-cli/test/generator/grammar-serializer.test.ts +++ b/packages/langium-cli/test/generator/grammar-serializer.test.ts @@ -6,15 +6,21 @@ import type { LangiumConfig } from '../../src/package-types.js'; import { EmptyFileSystem, URI, type Grammar } from 'langium'; -import { describe, expect, test } from 'vitest'; +import { afterEach, describe, expect, test } from 'vitest'; import { RelativePath } from '../../src/package-types.js'; import { serializeGrammar } from '../../src/generator/grammar-serializer.js'; import { createLangiumGrammarServices } from 'langium/grammar'; import { expandToString } from 'langium/generate'; +import { clearDocuments } from 'langium/test'; const grammarServices = createLangiumGrammarServices(EmptyFileSystem); describe('Grammar serializer', () => { + + afterEach(() => { + clearDocuments(grammarServices.shared); + }); + test('should include comments of AST elements', async () => { // arrange const config: LangiumConfig = { @@ -45,4 +51,64 @@ describe('Grammar serializer', () => { expect(moduleString).toMatch('"$comment": "/**\\\\n * This is the entry rule\\\\n */"'); }); + test('should escape template strings in development mode', async () => { + // arrange + const config: LangiumConfig = { + projectName: 'Magic', + languages: [], + mode: 'development', + [RelativePath]: '/path/to/magic', + }; + + const grammarText = expandToString` + grammar Test + entry Model: template='\${' backtick='\`' single="'"; + `; + + const document = grammarServices.shared.workspace.LangiumDocumentFactory.fromString(grammarText, URI.file('test.langium')); + grammarServices.shared.workspace.LangiumDocuments.addDocument(document); + await grammarServices.shared.workspace.DocumentBuilder.build([document]); + const grammar = document.parseResult.value; + + // act + const moduleString = serializeGrammar(grammarServices.grammar, [grammar], config); + + // assert + // Escapes `${` sequence correctly + expect(moduleString).toMatch('"value": "\\${"'); + // Escapes the "`" character correctly + expect(moduleString).toMatch('"value": "\\`'); + // Does not escape single quotes + expect(moduleString).toMatch('"value": "\'"'); + }); + + test('should escape single quotes in production mode', async () => { + // arrange + const config: LangiumConfig = { + projectName: 'Magic', + languages: [], + mode: 'production', + [RelativePath]: '/path/to/magic', + }; + + const grammarText = expandToString` + grammar Test + entry Model: single="'" backtick='\`'; + `; + + const document = grammarServices.shared.workspace.LangiumDocumentFactory.fromString(grammarText, URI.file('test.langium')); + grammarServices.shared.workspace.LangiumDocuments.addDocument(document); + await grammarServices.shared.workspace.DocumentBuilder.build([document]); + const grammar = document.parseResult.value; + + // act + const moduleString = serializeGrammar(grammarServices.grammar, [grammar], config); + + // assert + // Escapes single quote character correctly + expect(moduleString).toMatch('"value":"\\\'"'); + // Does not escape backticks + expect(moduleString).toMatch('"value":"`"'); + }); + });