diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index f5f27c0a39..95b1aabee6 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -207,6 +207,13 @@ export class LdmlKeyboardCompiler { kmx.kmxplus[section.id] = sect as any; } + // give all sections a chance to postValidate + for(let section of sections) { + if(!section.postValidate(kmx.kmxplus[section.id])) { + passed = false; + } + } + return passed ? kmx : null; } } diff --git a/developer/src/kmc-ldml/src/compiler/section-compiler.ts b/developer/src/kmc-ldml/src/compiler/section-compiler.ts index 8744059f7b..f200027786 100644 --- a/developer/src/kmc-ldml/src/compiler/section-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/section-compiler.ts @@ -1,8 +1,9 @@ import { LDMLKeyboard, KMXPlus, CompilerCallbacks } from "@keymanapp/common-types"; import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; -/* istanbul ignore next */ -export class SectionCompiler { +/** newable interface to SectionCompiler c'tor */ +export type SectionCompilerNew = new (source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) => SectionCompiler; +export abstract class SectionCompiler { protected readonly keyboard3: LDMLKeyboard.LKKeyboard; protected readonly callbacks: CompilerCallbacks; @@ -11,19 +12,34 @@ export class SectionCompiler { this.callbacks = callbacks; } - /* c8 ignore next 11 */ - public get id(): SectionIdent { - throw Error(`Internal Error: id() not implemented`); - } - - public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { - throw Error(`Internal Error: compile() not implemented`); - } + public abstract get id(): SectionIdent; + /** + * This is called before compile. + * @returns false if this compiler failed to validate. + */ public validate(): boolean { return true; } + /** + * Perform the compilation for this section, returning the correct Section subclass + * object. + * + * @param sections any declared dependency sections per dependencies() + */ + public abstract compile(sections: KMXPlus.DependencySections): KMXPlus.Section; + + /** + * This is called after all other compile phases have completed, and provides an + * opportunity for late error reporting, for example for invalid strings. + * @param section the compiled section, if any. + * @returns false if validate fails + */ + public postValidate(section?: KMXPlus.Section): boolean { + return true; + } + /** * Get the dependencies for this compiler. * @returns set of dependent sections diff --git a/developer/src/kmc-ldml/src/compiler/tran.ts b/developer/src/kmc-ldml/src/compiler/tran.ts index 89a9d61f1c..2e1944a7cc 100644 --- a/developer/src/kmc-ldml/src/compiler/tran.ts +++ b/developer/src/kmc-ldml/src/compiler/tran.ts @@ -19,7 +19,7 @@ import { MarkerTracker, MarkerUse } from "./marker-tracker.js"; type TransformCompilerType = 'simple' | 'backspace'; -export class TransformCompiler extends SectionCompiler { +export abstract class TransformCompiler extends SectionCompiler { static validateMarkers(keyboard: LDMLKeyboard.LKKeyboard, mt : MarkerTracker): boolean { keyboard?.transforms?.forEach(transforms => diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index ea76da94b3..d21fe99498 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -4,7 +4,7 @@ import 'mocha'; import * as path from 'path'; import { fileURLToPath } from 'url'; -import { SectionCompiler } from '../../src/compiler/section-compiler.js'; +import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js'; import { KMXPlus, LDMLKeyboardXMLSourceFileReader, VisualKeyboard, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types'; import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles import { assert } from 'chai'; @@ -52,7 +52,7 @@ afterEach(function() { }); -export async function loadSectionFixture(compilerClass: typeof SectionCompiler, filename: string, callbacks: TestCompilerCallbacks, dependencies?: typeof SectionCompiler[]): Promise
{ +export async function loadSectionFixture(compilerClass: SectionCompilerNew, filename: string, callbacks: TestCompilerCallbacks, dependencies?: SectionCompilerNew[], postValidateFail?: boolean): Promise
{ callbacks.messages = []; const inputFilename = makePathToFixture(filename); const data = callbacks.loadFile(inputFilename); @@ -83,13 +83,16 @@ export async function loadSectionFixture(compilerClass: typeof SectionCompiler, compiler.dependencies.forEach(dep => assert.ok(sections[dep], `Required dependency '${dep}' for '${compiler.id}' was not supplied: Check the 'dependencies' argument to loadSectionFixture or testCompilationCases`)); - return compiler.compile(sections); + const section = await compiler.compile(sections); + const postValidate = compiler.postValidate(section); + assert.equal(postValidate, !postValidateFail, `expected postValidate() to return ${!postValidateFail}`); + return section; } /** * Recursively load dependencies. Normally they are loaded in SECTION_COMPILERS order */ -async function loadDepsFor(sections: DependencySections, parentCompiler: SectionCompiler, source: LDMLKeyboardXMLSourceFile, callbacks: TestCompilerCallbacks, dependencies?: typeof SectionCompiler[]) { +async function loadDepsFor(sections: DependencySections, parentCompiler: SectionCompiler, source: LDMLKeyboardXMLSourceFile, callbacks: TestCompilerCallbacks, dependencies?: SectionCompilerNew[]) { const parentId = parentCompiler.id; if (!dependencies) { // default dependencies @@ -184,7 +187,11 @@ export interface CompilationCase { /** * Optional dependent sections to load. Will be strs+list+elem if falsy. */ - dependencies?: (typeof SectionCompiler)[]; + dependencies?: (SectionCompilerNew)[]; + /** + * Optional, if true, postValidate() must return false. (must be != postValidate()) + */ + postValidateFail?: boolean; } /** @@ -193,7 +200,7 @@ export interface CompilationCase { * @param compiler argument to loadSectionFixture() * @param callbacks argument to loadSectionFixture() */ -export function testCompilationCases(compiler: typeof SectionCompiler, cases : CompilationCase[], dependencies?: (typeof SectionCompiler)[]) { +export function testCompilationCases(compiler: SectionCompilerNew, cases : CompilationCase[], dependencies?: (SectionCompilerNew)[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); for (let testcase of cases) {