diff --git a/common/web/types/package.json b/common/web/types/package.json index 1ba487aafdf..e9f7c412947 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -36,7 +36,6 @@ "restructure": "3.0.1" }, "devDependencies": { - "@types/chai": "^4.1.7", "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", "ajv": "^8.12.0", diff --git a/developer/src/README.md b/developer/src/README.md index d789b63f08c..70edc5ba082 100644 --- a/developer/src/README.md +++ b/developer/src/README.md @@ -90,6 +90,10 @@ node-based next generation compiler, hosts kmc, (and legacy kmlmc, kmlmp) File analysis tools for Keyman files. +### kmc-generate - Generation tools + +Project generation tools for Keyman. + ### kmc-keyboard-info - Keyboard Info Compiler Builds .keyboard_info files for use on the Keyman Cloud keyboard repository diff --git a/developer/src/build.sh b/developer/src/build.sh index bbae5f629ef..c29072a1f17 100755 --- a/developer/src/build.sh +++ b/developer/src/build.sh @@ -24,6 +24,7 @@ builder_describe \ ":help Online documentation" \ ":kmcmplib Compiler - .kmn compiler" \ ":kmc-analyze Compiler - Analysis Tools" \ + ":kmc-generate Compiler - Generation Tools" \ ":kmc-keyboard-info Compiler - .keyboard_info Module" \ ":kmc-kmn Compiler - .kmn to .kmx and .js Keyboard Module" \ ":kmc-ldml Compiler - LDML Keyboard Module" \ diff --git a/developer/src/common/web/test-helpers/index.ts b/developer/src/common/web/test-helpers/index.ts index 93dfb89b2cc..9a68ada61cb 100644 --- a/developer/src/common/web/test-helpers/index.ts +++ b/developer/src/common/web/test-helpers/index.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerError } from '@keymanapp/developer-utils'; +import { fileURLToPath } from 'url'; export { verifyCompilerMessagesObject } from './verifyCompilerMessagesObject.js'; /** @@ -25,6 +26,10 @@ export class TestCompilerCallbacks implements CompilerCallbacks { return this.messages.find((item) => item.code == code) === undefined ? false : true; } + fileURLToPath(url: string | URL): string { + return fileURLToPath(url); + } + /** true of at least one error */ hasError(): boolean { return CompilerError.hasError(this.messages); diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts index e4ed62d7500..226c0f3013c 100644 --- a/developer/src/common/web/utils/src/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -263,6 +263,10 @@ export enum CompilerErrorNamespace { * kmc-keyboard-info 0x9000…0x9FFF */ KeyboardInfoCompiler = 0x9000, + /** + * kmc-generate 0xA000…0xAFFF + */ + Generator = 0xA000, }; /** @@ -290,6 +294,7 @@ export interface CompilerFileSystemCallbacks { readFileSync(path: string, options: { encoding: string; flag?: string; } | string): string; readFileSync(path: string, options?: { encoding?: string | null; flag?: string; } | string | null): string | Uint8Array; writeFileSync(path: string, data: Uint8Array): void; + mkdirSync(path: string, options?: {recursive?: boolean}): string; existsSync(name: string): boolean; } @@ -375,6 +380,8 @@ export interface CompilerCallbacks { reportMessage(event: CompilerEvent): void; debug(msg: string): void; + + fileURLToPath(url: string | URL): string; }; /** @@ -471,6 +478,10 @@ export class CompilerFileCallbacks implements CompilerCallbacks { debug(msg: string): void { return this.parent.debug(msg); } + + fileURLToPath(url: string | URL): string { + return this.parent.fileURLToPath(url); + } } /** diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 21b59cc749f..13e5f12af29 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -43,5 +43,5 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, Compile } from './compiler-interfaces.js'; export { CommonTypesMessages } from './common-messages.js'; - +export * as SourceFilenamePatterns from './source-filename-patterns.js'; export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; diff --git a/developer/src/common/web/utils/src/source-filename-patterns.ts b/developer/src/common/web/utils/src/source-filename-patterns.ts new file mode 100644 index 00000000000..7a0f0162d82 --- /dev/null +++ b/developer/src/common/web/utils/src/source-filename-patterns.ts @@ -0,0 +1,45 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Keyman Developer source filename regular expressions + */ + +/** + * A keyboard package filename SHOULD adhere to this pattern (including file + * extension), lower case alphanumeric and underscore only allowed (a-z, _ only + * for first letter). + */ +export const KEYBOARD_ID_PATTERN_PACKAGE = /^[a-z_][a-z0-9_]*\.(kps|kmp)$/; + +/** + * A lexical model package filename SHOULD adhere to this pattern (including + * file extension). There are three components to the filename: author, bcp47, + * and uniq, separated by period. The filename ends in .model.kps or .model.kmp. + * Each of the author, bcp47, and uniq sections may contain lowercase + * alphanumeric, underscore characters, and the bcp47 section additionally may + * contain hyphen. Digits are not permitted as first letter of each section. + * + * Despite including a bcp47 tag as part of the filename, it is informative only, + * and is not regarded as part of the metadata for the lexical model. + */ +// author .bcp47 .uniq +export const MODEL_ID_PATTERN_PACKAGE = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.(kps|kmp)$/; + +// const MODEL_ID_PATTERN_JS = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.js$/; +// const MODEL_ID_PATTERN_TS = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.ts$/; +// const MODEL_ID_PATTERN_PROJECT = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.kpj$/; + +/** + * Filenames of files contained in a package MAY adhere to this pattern for + * optimum cross-platform compatibility. This is the basename portion of the + * filename, and is case-insensitive. + */ +export const CONTENT_FILE_BASENAME_PATTERN = /^[a-z0-9_+.-]+$/i; // base names can be case insensitive + +/** + * Extensions of files contained in a package MAY adhere to this pattern for optimum + * cross-platform compatibility. This is the extension portion of the filename, + * and should be lower case, and may be empty. + */ +export const CONTENT_FILE_EXTENSION_PATTERN = /^(\.[a-z0-9_-]+)?$/; // extensions should be lower-case or empty + diff --git a/developer/src/common/web/utils/test/TestCompilerCallbacks.ts b/developer/src/common/web/utils/test/TestCompilerCallbacks.ts index d1413e477a9..31fc82a0fef 100644 --- a/developer/src/common/web/utils/test/TestCompilerCallbacks.ts +++ b/developer/src/common/web/utils/test/TestCompilerCallbacks.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { loadFile, resolveFilename } from './helpers/index.js'; import { CompilerCallbacks, CompilerError, CompilerEvent, CompilerFileSystemCallbacks, CompilerPathCallbacks } from '../src/compiler-interfaces.js'; +import { fileURLToPath } from 'url'; // This is related to developer/src/common/web/test-helpers/index.ts but has a slightly different API surface // as this runs at a lower level than the compiler. @@ -34,6 +35,10 @@ export class TestCompilerCallbacks implements CompilerCallbacks { return resolveFilename(baseFilename, filename); } + fileURLToPath(url: string | URL): string { + return fileURLToPath(url); + } + loadFile(filename: string): Uint8Array { // TODO: error management, does it belong here? try { diff --git a/developer/src/kmc-generate/.eslintrc.cjs b/developer/src/kmc-generate/.eslintrc.cjs new file mode 100644 index 00000000000..c0198b84522 --- /dev/null +++ b/developer/src/kmc-generate/.eslintrc.cjs @@ -0,0 +1,21 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * tslint options for kmc-generate + */ + +module.exports = { + parserOptions: { + project: ["./tsconfig.json", "./test/tsconfig.json"], + }, + ignorePatterns: ["test/fixtures/**/*", "src/template/**/*"], + overrides: [ + { + files:"src/**/*.ts", + extends: ["../../../common/tools/eslint/eslintNoNodeImports.js"], + } + ], + rules: { + "prefer-const": 1, + }, +}; diff --git a/developer/src/kmc-generate/build.sh b/developer/src/kmc-generate/build.sh new file mode 100755 index 00000000000..dce1072f530 --- /dev/null +++ b/developer/src/kmc-generate/build.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# Keyman is copyright (C) SIL International. MIT License. +# +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh" + +builder_describe "Build Keyman kmc-generate module" \ + "@/common/web/keyman-version" \ + "@/common/web/types" \ + "@/developer/src/common/web/test-helpers" \ + clean configure build api test publish \ + "--npm-publish+ For publish, do a npm publish, not npm pack (only for CI)" \ + "--dry-run,-n don't actually publish, just dry run" + +builder_describe_outputs \ + configure /node_modules \ + build /developer/src/kmc-generate/build/src/main.js \ + api /developer/build/api/kmc-generate.api.json + +builder_parse "$@" + +#------------------------------------------------------------------------------------------------------------------- + +do_build() { + tsc --build + rm -rf ./build/src/template + mkdir -p ./build/src/template + cp -R ./src/template/ ./build/src/ +} + +do_test() { + eslint . + cd test + tsc --build + cd .. + c8 --reporter=lcov --reporter=text mocha "${builder_extra_params[@]}" +} + +builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo +builder_run_action configure verify_npm_setup +builder_run_action build do_build +builder_run_action api api-extractor run --local --verbose +builder_run_action test do_test +builder_run_action publish builder_publish_npm diff --git a/developer/src/kmc-generate/config/api-extractor.json b/developer/src/kmc-generate/config/api-extractor.json new file mode 100644 index 00000000000..5ab7b50e162 --- /dev/null +++ b/developer/src/kmc-generate/config/api-extractor.json @@ -0,0 +1,12 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../../config/api-extractor.base.json", + "mainEntryPointFilePath": "/build/src/main.d.ts", + "docModel": { + "enabled": true, + "projectFolderUrl": "http://github.com/keymanapp/keyman/tree/master/developer/src/kmc-generate" + } +} diff --git a/developer/src/kmc-generate/package.json b/developer/src/kmc-generate/package.json new file mode 100644 index 00000000000..34eb2b4ebeb --- /dev/null +++ b/developer/src/kmc-generate/package.json @@ -0,0 +1,64 @@ +{ + "name": "@keymanapp/kmc-generate", + "description": "Keyman Developer generate module", + "keywords": [ + "keyboard", + "keyman", + "ldml", + "unicode" + ], + "type": "module", + "exports": { + ".": "./build/src/main.js" + }, + "files": [ + "/build/src/" + ], + "scripts": { + "build": "gosh ./build.sh build", + "lint": "eslint .", + "test": "gosh ./build.sh test" + }, + "author": "Marc Durdin (https://github.com/mcdurdin)", + "license": "MIT", + "bugs": { + "url": "https://github.com/keymanapp/keyman/issues" + }, + "dependencies": { + "@keymanapp/common-types": "*", + "@keymanapp/developer-utils": "*", + "@keymanapp/keyman-version": "*" + }, + "devDependencies": { + "@keymanapp/developer-test-helpers": "*", + "@keymanapp/resources-gosh": "*", + "@types/mocha": "^10.0.0", + "@types/node": "^20.4.1", + "@types/semver": "^7.3.12", + "c8": "^7.12.0", + "chalk": "^2.4.2", + "mocha": "^10.0.0", + "typescript": "^5.4.5" + }, + "mocha": { + "spec": "build/test/**/test-*.js", + "require": [ + "source-map-support/register" + ] + }, + "c8": { + "all": true, + "src": [ + "src/" + ], + "exclude-after-remap": true, + "exclude": [ + "test/", + "src/template/" + ] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/keymanapp/keyman.git" + } +} diff --git a/developer/src/kmc-generate/src/abstract-generator.ts b/developer/src/kmc-generate/src/abstract-generator.ts new file mode 100644 index 00000000000..51758984cd9 --- /dev/null +++ b/developer/src/kmc-generate/src/abstract-generator.ts @@ -0,0 +1,233 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Base interfaces and classes for generating Keyman Developer source files + */ + +import { CompilerCallbacks, CompilerLogLevel, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult } from "@keymanapp/developer-utils"; +import { GeneratorMessages } from './generator-messages.js'; +import { KeymanTargets } from "@keymanapp/common-types"; + + +/** + * @public + * Options for the Keyman Developer project generator + */ +export interface GeneratorOptions /* not inheriting from CompilerBaseOptions */ { + /** + * Reporting level to console, used by NodeCompilerCallbacks (not used in + * compiler modules; all messages are still reported to the internal log) + */ + logLevel?: CompilerLogLevel; + /** + * identifier (basename) of the keyboard or model + */ + id: string; + /** + * supported platforms, only used in Keyman keyboard project generator + */ + targets?: KeymanTargets.KeymanTarget[]; + /** + * output path where project folder will be created + */ + outPath: string; + /** + * descriptive name of the keyboard or lexical model + */ + name?: string; + /** + * name of the copyright holder for the keyboard or lexical model (do not + * include (c) symbol or date) + */ + copyright?: string; + /** + * version of the keyboard or model, 1.0 default for Keyman keyboard, lexical + * model, 1.0.0 default for LDML keyboard + */ + version?: string; + /** + * array of bcp 47 tags which are supported by the keyboard or lexical model + */ + languageTags?: string[]; + /** + * name of the author of the keyboard + */ + author?: string; + /** + * set to true to generate an icon for a Keyman keyboard with the first + * characters of the first specified BCP 47 tag + */ + icon?: boolean; + /** + * description of the keyboard, Markdown permissible + */ + description?: string; + /** + * version of Keyman to reference in source files, defaults to KEYMAN_VERSION.VERSION + */ + keymanVersion?: string; +}; + +/** + * @public + * Internal in-memory build artifacts from a successful generation + */ +export interface GeneratorArtifacts extends KeymanCompilerArtifacts { + /** + * Generated project files to be written to disk + */ + [name:string]: KeymanCompilerArtifact; +}; + +/** + * @public + * Result of a successful generation + */ +export interface GeneratorResult extends KeymanCompilerResult { + /** + * Internal in-memory build artifacts from a successful compilation. Caller + * can write these to disk with {@link AbstractGenerator.write} + */ + artifacts: GeneratorArtifacts; +}; + +/** + * @public + * Common functionality for generating projects. Do not instantiate + * this class, rather instantiate a subclass + */ +export class AbstractGenerator { + /** + * id for the keyboard or model, aka the basename sans extension, foldername + * for the project + */ + // protected get id() { return this._id } + // private _id: string; + + /** + * extension of options.copyright including copyright year + */ + protected fullCopyright: string = ''; + + /** + * identifiers for lines to include when transforming template files, filled + * by child classes + */ + protected get includedPrefixes() { return this._includedPrefixes; } + private _includedPrefixes: string[]; + + /** + * tokens to rewrite in output files + */ + protected get tokenMap() { return this._tokenMap; } + private _tokenMap: {[index:string]: string}; + + /** + * map of all files to be transformed, filled by this class and subclasses + */ + protected get filenameMap() { return this._filenameMap; } + private _filenameMap: {[index:string]: string}; + + /** + * base path for template files in this module + */ + protected get templateBasePath() { return this._templateBasePath; } + private _templateBasePath: string; + + protected static readonly SPath_Source = 'source/'; + protected static readonly SFile_WelcomeHTM = `${this.SPath_Source}welcome.htm`; + protected static readonly SFile_ReadmeHTM = `${this.SPath_Source}readme.htm`; + protected static readonly SFile_HistoryMD = 'HISTORY.md'; + protected static readonly SFile_LicenseMD = 'LICENSE.md'; + protected static readonly SFile_ReadmeMD = 'README.md'; + protected static readonly SFile_GitIgnore = '.gitignore'; + + protected get callbacks(): CompilerCallbacks { return this._callbacks; } + private _callbacks: CompilerCallbacks; + protected get options(): GeneratorOptions { return this._options; } + private _options: GeneratorOptions; + + /** + * Initialize the generator. Copies options. + * @param callbacks - Callbacks for external interfaces, including message + * reporting and file io + * @param options - Generator options + * @returns false if initialization fails + */ + public async init(callbacks: CompilerCallbacks, options: GeneratorOptions): Promise { + this._callbacks = callbacks; + this._options = {...options}; + // this._id = options.id; + this._includedPrefixes = []; + this._filenameMap = {}; + this._tokenMap = {}; + + this._templateBasePath = this.callbacks.path.join( + this.callbacks.path.dirname(this.callbacks.fileURLToPath(import.meta.url)), + 'template' + ); + + // These files are currently always included, for all project types + this.filenameMap[AbstractGenerator.SFile_WelcomeHTM] = AbstractGenerator.SFile_WelcomeHTM; + this.filenameMap[AbstractGenerator.SFile_ReadmeHTM] = AbstractGenerator.SFile_ReadmeHTM; + this.filenameMap[AbstractGenerator.SFile_HistoryMD] = AbstractGenerator.SFile_HistoryMD; + this.filenameMap[AbstractGenerator.SFile_LicenseMD] = AbstractGenerator.SFile_LicenseMD; + this.filenameMap[AbstractGenerator.SFile_ReadmeMD] = AbstractGenerator.SFile_ReadmeMD; + this.filenameMap[AbstractGenerator.SFile_GitIgnore] = AbstractGenerator.SFile_GitIgnore; + + return true; + } + + /** + * Write artifacts from a successful compile to disk, via callbacks methods. + * The artifacts written will include all files from the project, across + * multiple folders. Folders will be created as needed + * + * @param artifacts - object containing artifact binary data to write out + * @returns true on success + */ + public async write(artifacts: GeneratorArtifacts): Promise { + // TODO-GENERATE: this is a little poor because it is carrying state over from the + // previous 'run' call, rather than figuring out the target path from the + // artifacts. Probably should be looking at highest common folder, and then + // failing if that path exists + if(this.targetPathExists()) { + this.callbacks.reportMessage(GeneratorMessages.Error_OutputPathAlreadyExists({outPath: this.targetPath()})); + return false; + } + + for(const key of Object.keys(artifacts)) { + const a = artifacts[key]; + const path = this.callbacks.path.dirname(a.filename); + try { + this.callbacks.fs.mkdirSync(path, {recursive: true}); + /* c8 ignore next 4 */ + } catch(e) { + this.callbacks.reportMessage(GeneratorMessages.Error_CannotCreateFolder({folderName:path, e})); + return false; + } + try { + this.callbacks.fs.writeFileSync(a.filename, a.data); + } catch(e) { + this.callbacks.reportMessage(GeneratorMessages.Error_CannotWriteOutputFile({filename:a.filename, e})); + return false; + } + } + return true; + } + + protected readonly targetPath = () => this.callbacks.path.join(this.options.outPath,this.options.id); + private readonly targetPathExists = () => this.callbacks.fs.existsSync(this.targetPath()); + + /** + * @internal + */ + public readonly unitTestEndpoints = { + targetPath: this.targetPath + } + /** + * @internal + */ + public get test_tokenMap() { return this._tokenMap; } +} + diff --git a/developer/src/kmc-generate/src/basic-generator.ts b/developer/src/kmc-generate/src/basic-generator.ts new file mode 100644 index 00000000000..cb69ef5fdd0 --- /dev/null +++ b/developer/src/kmc-generate/src/basic-generator.ts @@ -0,0 +1,140 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Basic generator -- common file generation functionality + */ + +import { KeymanTargets } from "@keymanapp/common-types"; +import KEYMAN_VERSION from "@keymanapp/keyman-version"; +import { AbstractGenerator, GeneratorArtifacts } from "./abstract-generator.js"; + +/** + * @internal + * Common functionality for generating projects. Do not instantiate + * this class, rather instantiate a subclass + */ +export class BasicGenerator extends AbstractGenerator { + + // We'll generate a keyboard with a default 'en' locale if none is passed in + protected readonly DEFAULT_LOCALE = 'en'; + + protected templatePath: string; + protected languageTags: string[]; + protected resolvedTargets: KeymanTargets.KeymanTarget[]; + + protected preGenerate() { + const dt = new Date(); + + this.languageTags = this.options.languageTags.length + ? this.options.languageTags.map(tag => new Intl.Locale(tag).minimize().toString()) + : [this.DEFAULT_LOCALE]; + //TODO-GENERATE: validate targets + if(this.options.targets.includes(KeymanTargets.KeymanTarget.any) || this.options.targets.length == 0) { + this.resolvedTargets = KeymanTargets.AllKeymanTargets.filter(target => target != KeymanTargets.KeymanTarget.any); + } else { + this.resolvedTargets = [].concat(this.options.targets); + } + + this.tokenMap['$NAME'] = this.options.name; + this.tokenMap['$ID'] = this.options.id; + this.tokenMap['$KEYMANVERSION'] = (this.options.keymanVersion || KEYMAN_VERSION.VERSION) + '.0'; + this.tokenMap['$VERSION'] = this.options.version; + this.tokenMap['$COPYRIGHT'] = '© ' + this.options.copyright; + this.tokenMap['$FULLCOPYRIGHT'] = '© ' + dt.getFullYear().toString() + ' ' + this.options.copyright; + this.tokenMap['$AUTHOR'] = this.options.author || ''; + this.tokenMap['$TARGETS'] = this.resolvedTargets.join(' '); + this.tokenMap['$DESCRIPTION'] = this.options.description || this.options.name; + this.tokenMap['$DATE'] = + dt.getFullYear().toString() + '-' + + (dt.getMonth()+1).toString().padStart(2, '0') + '-' + + dt.getDate().toString().padStart(2, '0'); + this.tokenMap['$PACKAGE_LANGUAGES'] = this.generateLanguageListForPackage(); + this.tokenMap['$PLATFORMS_DOTLIST_README'] = this.getPlatformDotListForReadme(); + } + + protected generate(artifacts: GeneratorArtifacts): boolean { + return this.transformAll(artifacts); + } + + private getLanguageName(tag: string) { + // TODO-GENERATE: probably need to use langtags.json + return new Intl.Locale(tag).language; + } + + private generateLanguageListForPackage(): string { + // + // Central Khmer (Khmer, Cambodia) + // + const result = + ` \n`+ + this.languageTags.map(tag => ` ${this.getLanguageName(tag)}`).join('\n')+ + `\n `; + return result; + } + + private getPlatformDotListForReadme() { + const result = KeymanTargets.AllKeymanTargets + .filter(target => this.resolvedTargets.includes(target)) + .map(target => ` * ${KeymanTargets.SKeymanTargetNames[target]}`) + .join('\n'); + return result; + } + + private readonly transformAll = (artifacts: GeneratorArtifacts) => Object + .keys(this.filenameMap) + .every(src => this.transform(src, this.filenameMap[src], artifacts)); + + private transform(sourceFile: string, destFile: string, artifacts: GeneratorArtifacts) { + destFile = this.callbacks.path.join(this.options.outPath, this.options.id, destFile == '' ? sourceFile : destFile); + sourceFile = this.callbacks.path.join(this.templateBasePath, this.templatePath, sourceFile); + + const sourceData = this.callbacks.loadFile(sourceFile); + if(sourceData == null) { + // internal error -- source file should be readable + throw new Error(`source file ${sourceFile} does not exist`); + } + const template = new TextDecoder('utf-8').decode(sourceData).replace(/\r/g, '').split('\n'); + + const source: string[] = []; + + // Filter out unused lines + + for(const line of template) { + if(!line.match(/\$[A-Z0-9_-]+:/i)) { + source.push(line); + } else { + const tokens = line.split(':', 2); + if(this.includedPrefixes.includes(tokens[0].substring(1))) { + source.push(tokens[1]); + } + } + } + + // Replace all tokens + let dest = source.join('\n'); + Object.keys(this.tokenMap).forEach(token => dest = dest.replaceAll(token, this.tokenMap[token])); + + artifacts[destFile] = { + filename: destFile, + data: new TextEncoder().encode(dest) + }; + + return true; + } + + /** + * @internal + * these are exported only for unit tests, do not use + */ + public readonly test_templatePath = () => this.templatePath; + public readonly test_languageTags = () => this.languageTags; + + public readonly test_preGenerate = () => this.preGenerate(); + public readonly test_generate = (artifacts: GeneratorArtifacts) => this.generate(artifacts); + public readonly test_getLanguageName = (tag: string) => this.getLanguageName(tag); + public readonly test_generateLanguageListForPackage = () => this.generateLanguageListForPackage(); + public readonly test_getPlatformDotListForReadme = () => this.getPlatformDotListForReadme(); + public readonly test_transformAll = (artifacts: GeneratorArtifacts) => this.transformAll(artifacts); + public readonly test_transform = (sourceFile: string, destFile: string, artifacts: GeneratorArtifacts) => + this.transform(sourceFile, destFile, artifacts); +} diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts new file mode 100644 index 00000000000..cda0d82fd2c --- /dev/null +++ b/developer/src/kmc-generate/src/generator-messages.ts @@ -0,0 +1,46 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Messages for kmc-generate + */ + +import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils"; + +const Namespace = CompilerErrorNamespace.Generator; +const SevInfo = CompilerErrorSeverity.Info | Namespace; +// const SevHint = CompilerErrorSeverity.Hint | Namespace; +const SevWarn = CompilerErrorSeverity.Warn | Namespace; +const SevError = CompilerErrorSeverity.Error | Namespace; +const SevFatal = CompilerErrorSeverity.Fatal | Namespace; + +/** + * @internal + */ +export class GeneratorMessages { + static Fatal_UnexpectedException = (o:{e: any}) => m(this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'); + static FATAL_UnexpectedException = SevFatal | 0x0001; + + static Info_GeneratingProject = (o:{type: string, id: string}) => m(this.INFO_GeneratingProject, + `Generating project of type ${def(o.type)} with id ${def(o.id)}`); + static INFO_GeneratingProject = SevInfo | 0x0002; + + static ERROR_CannotCreateFolder = SevError | 0x0003; + static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotCreateFolder, null, + `Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`); + + static ERROR_OutputPathAlreadyExists = SevError | 0x0004; + static Error_OutputPathAlreadyExists = (o:{outPath:string}) => m( + this.ERROR_OutputPathAlreadyExists, + `Output path ${def(o.outPath)} already exists, not overwriting`); + + static ERROR_CannotWriteOutputFile = SevError | 0x0005; + static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotWriteOutputFile, null, + `Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`); + + // See also PackageCompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions + static WARN_ModelIdDoesNotFollowLexicalModelConventions = SevWarn | 0x0006; + static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(this.WARN_ModelIdDoesNotFollowLexicalModelConventions, + `The id ${def(o.id)} does not follow the recommended model id conventions. The id should be all lower case, `+ + `include only alphanumeric characters and underscore (_), not start with a digit, and should have the structure `+ + `..`); +}; diff --git a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts new file mode 100644 index 00000000000..a46a17f53c9 --- /dev/null +++ b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts @@ -0,0 +1,110 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Generate a Keyman keyboard project (.kmn source) + */ + +import { KeymanFileTypes, KeymanTargets } from '@keymanapp/common-types'; +import { KeymanCompiler, } from '@keymanapp/developer-utils'; +import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js'; +import { BasicGenerator } from './basic-generator.js'; + +/** + * @public + * Generate a Keyman keyboard project. The generator does not read or write from + * filesystem or network directly, but relies on callbacks for all external IO. + */ +export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCompiler { + // Future: we probably want to have a more abstract implementation so that we + // can use this for both generate and clone keyboard? + // + // So we have a structure of an entire project passed into to a writer. Even if + // we can't cleanly reuse at least we can copy the code more easily and it will + // be more maintainable. + // + // But for now we are working with plain text approach + + static readonly SFile_Keyboard = 'keyboard'; + static readonly SFile_KeyboardKMN = `${this.SPath_Source}${this.SFile_Keyboard}${KeymanFileTypes.Source.KeymanKeyboard}`; + static readonly SFile_KeyboardKPS = `${this.SPath_Source}${this.SFile_Keyboard}${KeymanFileTypes.Source.Package}`; + static readonly SFile_KeyboardKVKS = `${this.SPath_Source}${this.SFile_Keyboard}${KeymanFileTypes.Source.VisualKeyboard}`; + static readonly SFile_TouchLayout = `${this.SPath_Source}${this.SFile_Keyboard}${KeymanFileTypes.Source.TouchLayout}`; + static readonly SFile_Project = `${this.SFile_Keyboard}${KeymanFileTypes.Source.Project}`; + + /** + * Generate a Keyman Keyboard project. Returns an object containing binary + * artifacts on success. The files are passed in by name, and the compiler + * will use callbacks as passed to the {@link AbstractGenerator.init} + * function to read any input files by disk. + * @returns Binary artifacts on success, null on failure. + */ + async run(): Promise { + this.preGenerate(); + + const artifacts: GeneratorArtifacts = {}; + + this.templatePath = 'kmn-keyboard'; + + this.filenameMap[KeymanKeyboardGenerator.SFile_Project] = this.options.id+KeymanFileTypes.Source.Project; + + if(this.hasKVKS()) { + this.includedPrefixes.push('KVKS'); + this.filenameMap[KeymanKeyboardGenerator.SFile_KeyboardKVKS] = + KeymanKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.VisualKeyboard; + } + + if(this.hasWeb()) { + this.includedPrefixes.push('Web'); + } + + if(this.hasTouchLayout()) { + this.includedPrefixes.push('TouchLayout'); + this.filenameMap[KeymanKeyboardGenerator.SFile_TouchLayout] = + KeymanKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.TouchLayout; + } + + if(this.hasIcon()) { + this.includedPrefixes.push('Icon'); + } + + if(this.hasKMX()) { + this.includedPrefixes.push('KMX'); + } + + this.filenameMap[KeymanKeyboardGenerator.SFile_KeyboardKMN] = + KeymanKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.KeymanKeyboard; + this.filenameMap[KeymanKeyboardGenerator.SFile_KeyboardKPS] = + KeymanKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.Package; + + if(!this.generate(artifacts)) { + return null; + } + + // Special case for creating icon, run after successful creation of other + // project bits and pieces + if(this.hasIcon()) { + this.writeIcon(artifacts); + } + + return {artifacts}; + } + + private readonly targetIncludes = (targets: KeymanTargets.KeymanTarget[]) => { + return this.resolvedTargets.some(t => targets.includes(t)); + } + + private readonly hasKVKS = () => this.targetIncludes(KeymanTargets.KeymanTargetsUsingKVK); + private readonly hasWeb = () => this.targetIncludes(KeymanTargets.KMWKeymanTargets); + private readonly hasKMX = () => this.targetIncludes(KeymanTargets.KMXKeymanTargets); + private readonly hasTouchLayout = () => this.targetIncludes(KeymanTargets.TouchKeymanTargets); + + // TODO-GENERATE, once writeIcon is implemented: + // hasIcon = () => this.options.icon && this.targetIncludes(KeymanTargets.KMXKeymanTargets); + private readonly hasIcon = () => false; + + private writeIcon(artifacts: GeneratorArtifacts) { + // TODO-GENERATE: this will require some effort + // proposal: generate 16x16 icon with 2-3 letters. Following TKeyboardIconGenerator.GenerateIcon + // research for .ico writer in node + } +} diff --git a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts new file mode 100644 index 00000000000..7c3b6617a00 --- /dev/null +++ b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts @@ -0,0 +1,65 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Generate a Keyman LDML keyboard project (.xml source) + */ + +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanCompiler, } from '@keymanapp/developer-utils'; +import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js'; +import { BasicGenerator } from './basic-generator.js'; + +/** + * @public + * Generate a LDML keyboard project. The generator does not read or write from + * filesystem or network directly, but relies on callbacks for all external IO. + */ +export class LdmlKeyboardGenerator extends BasicGenerator implements KeymanCompiler { + protected static readonly SFile_Keyboard = 'keyboard'; + static readonly SFile_KeyboardXML = `${this.SPath_Source}${this.SFile_Keyboard}${KeymanFileTypes.Source.LdmlKeyboard}`; + static readonly SFile_Package = `${this.SPath_Source}${this.SFile_Keyboard}${KeymanFileTypes.Source.Package}`; + static readonly SFile_Project = `${this.SFile_Keyboard}${KeymanFileTypes.Source.Project}`; + + /** + * Generate a LDML Keyboard project. Returns an object containing binary + * artifacts on success. The files are passed in by name, and the compiler + * will use callbacks as passed to the {@link AbstractGenerator.init} + * function to read any input files by disk. + * @returns Binary artifacts on success, null on failure. + */ + async run(): Promise { + this.preGenerate(); + + const artifacts: GeneratorArtifacts = {}; + this.templatePath = 'ldml-keyboard'; + this.filenameMap[LdmlKeyboardGenerator.SFile_Project] = + this.options.id+KeymanFileTypes.Source.Project; + this.filenameMap[LdmlKeyboardGenerator.SFile_KeyboardXML] = + LdmlKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.LdmlKeyboard; + this.filenameMap[LdmlKeyboardGenerator.SFile_Package] = + LdmlKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.Package; + + this.tokenMap['$LANG_TAG'] = this.generateFirstLangTag(); + this.tokenMap['$ADDITIONAL_LANG_TAGS'] = this.generateAlternateLangTags(); + + if(!this.generate(artifacts)) { + return null; + } + + return {artifacts}; + } + + private readonly generateFirstLangTag = () => this.languageTags?.[0] ?? this.DEFAULT_LOCALE; + private readonly generateAlternateLangTags = () => { + if(this.languageTags.length < 2) { + return ''; + } + + const result = + ` \n` + + this.languageTags.slice(1).map(tag => ` `).join('\n')+ + `\n `; + + return result; + } +} diff --git a/developer/src/kmc-generate/src/lexical-model-generator.ts b/developer/src/kmc-generate/src/lexical-model-generator.ts new file mode 100644 index 00000000000..40fa59bfca9 --- /dev/null +++ b/developer/src/kmc-generate/src/lexical-model-generator.ts @@ -0,0 +1,60 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Generate a Keyman lexical model project + */ + +import { KeymanFileTypes } from '@keymanapp/common-types'; +import { KeymanCompiler, SourceFilenamePatterns } from '@keymanapp/developer-utils'; +import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js'; +import { BasicGenerator } from './basic-generator.js'; +import { GeneratorMessages } from './generator-messages.js'; + +/** + * @public + * Generate a Keyman lexical model project. The generator does not read or write + * from filesystem or network directly, but relies on callbacks for all external + * IO. + */ +export class LexicalModelGenerator extends BasicGenerator implements KeymanCompiler { + static readonly SFile_Model = 'model'; + static readonly SFile_ModelTs = `${this.SPath_Source}${this.SFile_Model}.ts`; + static readonly SFile_WordlistTsv = `${this.SPath_Source}wordlist.tsv`; + static readonly SFile_Project = `${this.SFile_Model}${KeymanFileTypes.Source.Project}`; + static readonly SFile_Package = `${this.SPath_Source}${this.SFile_Model}${KeymanFileTypes.Source.Package}`; + + /** + * Generate a Lexical Model project. Returns an object containing binary + * artifacts on success. The files are passed in by name, and the compiler + * will use callbacks as passed to the {@link AbstractGenerator.init} + * function to read any input files by disk. + * @returns Binary artifacts on success, null on failure. + */ + async run(): Promise { + this.preGenerate(); + + const artifacts: GeneratorArtifacts = {}; + + this.templatePath = 'wordlist-lexical-model'; + this.filenameMap[LexicalModelGenerator.SFile_Project] = + this.options.id+KeymanFileTypes.Source.Project; + this.filenameMap[LexicalModelGenerator.SFile_ModelTs] = + LexicalModelGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.Model; + this.filenameMap[LexicalModelGenerator.SFile_WordlistTsv] = + LexicalModelGenerator.SFile_WordlistTsv; + + // Note: lexical models use .model.kps for package extension + const packageFilename = this.options.id+'.model'+KeymanFileTypes.Source.Package; + if(!SourceFilenamePatterns.MODEL_ID_PATTERN_PACKAGE.test(packageFilename)) { + this.callbacks.reportMessage(GeneratorMessages.Warn_ModelIdDoesNotFollowLexicalModelConventions({id: this.options.id})); + } + + this.filenameMap[LexicalModelGenerator.SFile_Package] = LexicalModelGenerator.SPath_Source+packageFilename; + + if(!this.generate(artifacts)) { + return null; + } + + return {artifacts}; + } +} diff --git a/developer/src/kmc-generate/src/main.ts b/developer/src/kmc-generate/src/main.ts new file mode 100644 index 00000000000..f23b92bd70f --- /dev/null +++ b/developer/src/kmc-generate/src/main.ts @@ -0,0 +1,10 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +/* c8 ignore start */ +export { AbstractGenerator, GeneratorOptions } from "./abstract-generator.js"; +export { KeymanKeyboardGenerator } from "./keyman-keyboard-generator.js"; +export { LexicalModelGenerator } from "./lexical-model-generator.js"; +export { LdmlKeyboardGenerator } from "./ldml-keyboard-generator.js"; +export { GeneratorMessages } from "./generator-messages.js"; \ No newline at end of file diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/.gitignore b/developer/src/kmc-generate/src/template/kmn-keyboard/.gitignore new file mode 100644 index 00000000000..65dc5567c89 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/.gitignore @@ -0,0 +1,4 @@ +# This file lists files which should not be committed into source control +build/ +*.kpj.user +node_modules/ diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/HISTORY.md b/developer/src/kmc-generate/src/template/kmn-keyboard/HISTORY.md new file mode 100644 index 00000000000..9a605b69a3a --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/HISTORY.md @@ -0,0 +1,6 @@ +$NAME Change History +==================== + +$VERSION ($DATE) +---------------- +* Created by $AUTHOR diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/LICENSE.md b/developer/src/kmc-generate/src/template/kmn-keyboard/LICENSE.md new file mode 100644 index 00000000000..bb0daf3447f --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +$FULLCOPYRIGHT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/README.md b/developer/src/kmc-generate/src/template/kmn-keyboard/README.md new file mode 100644 index 00000000000..56c60908394 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/README.md @@ -0,0 +1,18 @@ +$NAME keyboard +============== + +Description +----------- +$DESCRIPTION + +Links +----- +Keyboard Homepage: https://keyman.com/keyboards/$ID + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- +$PLATFORMS_DOTLIST_README diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/keyboard.kpj b/developer/src/kmc-generate/src/template/kmn-keyboard/keyboard.kpj new file mode 100644 index 00000000000..7ed1ac72df6 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/keyboard.kpj @@ -0,0 +1,8 @@ + + + + 2.0 + True + True + + diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.keyman-touch-layout b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.keyman-touch-layout new file mode 100644 index 00000000000..222b51a9481 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.keyman-touch-layout @@ -0,0 +1,655 @@ +{ + "phone": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "q" + }, + { + "id": "K_W", + "text": "w" + }, + { + "id": "K_E", + "text": "e" + }, + { + "id": "K_R", + "text": "r" + }, + { + "id": "K_T", + "text": "t" + }, + { + "id": "K_Y", + "text": "y" + }, + { + "id": "K_U", + "text": "u" + }, + { + "id": "K_I", + "text": "i" + }, + { + "id": "K_O", + "text": "o" + }, + { + "id": "K_P", + "text": "p" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_A", + "text": "a", + "pad": "50" + }, + { + "id": "K_S", + "text": "s" + }, + { + "id": "K_D", + "text": "d" + }, + { + "id": "K_F", + "text": "f" + }, + { + "id": "K_G", + "text": "g" + }, + { + "id": "K_H", + "text": "h" + }, + { + "id": "K_J", + "text": "j" + }, + { + "id": "K_K", + "text": "k" + }, + { + "id": "K_L", + "text": "l" + }, + { + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_Z", + "text": "z" + }, + { + "id": "K_X", + "text": "x" + }, + { + "id": "K_C", + "text": "c" + }, + { + "id": "K_V", + "text": "v" + }, + { + "id": "K_B", + "text": "b" + }, + { + "id": "K_N", + "text": "n" + }, + { + "id": "K_M", + "text": "m" + }, + { + "id": "K_PERIOD", + "text": ".", + "sk": [ + { + "text": ",", + "id": "K_COMMA" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "150", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "610", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "Q" + }, + { + "id": "K_W", + "text": "W" + }, + { + "id": "K_E", + "text": "E" + }, + { + "id": "K_R", + "text": "R" + }, + { + "id": "K_T", + "text": "T" + }, + { + "id": "K_Y", + "text": "Y" + }, + { + "id": "K_U", + "text": "U" + }, + { + "id": "K_I", + "text": "I" + }, + { + "id": "K_O", + "text": "O" + }, + { + "id": "K_P", + "text": "P" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_A", + "text": "A", + "pad": "50" + }, + { + "id": "K_S", + "text": "S" + }, + { + "id": "K_D", + "text": "D" + }, + { + "id": "K_F", + "text": "F" + }, + { + "id": "K_G", + "text": "G" + }, + { + "id": "K_H", + "text": "H" + }, + { + "id": "K_J", + "text": "J" + }, + { + "id": "K_K", + "text": "K" + }, + { + "id": "K_L", + "text": "L" + }, + { + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": "2", + "nextlayer": "default" + }, + { + "id": "K_Z", + "text": "Z" + }, + { + "id": "K_X", + "text": "X" + }, + { + "id": "K_C", + "text": "C" + }, + { + "id": "K_V", + "text": "V" + }, + { + "id": "K_B", + "text": "B" + }, + { + "id": "K_N", + "text": "N" + }, + { + "id": "K_M", + "text": "M" + }, + { + "id": "K_PERIOD", + "text": ".", + "layer": "default", + "sk": [ + { + "text": ",", + "id": "K_COMMA", + "layer": "default" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE", + "layer": "default" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH", + "layer": "default" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON", + "layer": "default" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "150", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "610", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_4", + "layer": "shift", + "text": "$", + "pad": "50" + }, + { + "id": "K_2", + "layer": "shift", + "text": "@" + }, + { + "id": "K_3", + "layer": "shift", + "text": "#" + }, + { + "id": "K_5", + "layer": "shift", + "text": "%" + }, + { + "id": "K_7", + "layer": "shift", + "text": "&" + }, + { + "id": "K_HYPHEN", + "layer": "shift", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "=", + "layer": "default" + }, + { + "id": "K_BKSLASH", + "layer": "shift", + "text": "|" + }, + { + "id": "K_BKSLASH", + "text": "\\", + "layer": "default" + }, + { + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_LBRKT", + "text": "[", + "pad": "110", + "sk": [ + { + "id": "U_00AB", + "text": "\u00AB" + }, + { + "id": "K_COMMA", + "text": "<", + "layer": "shift" + }, + { + "id": "K_LBRKT", + "text": "{", + "layer": "shift" + } + ] + }, + { + "id": "K_9", + "layer": "shift", + "text": "(" + }, + { + "id": "K_0", + "layer": "shift", + "text": ")" + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "id": "U_00BB", + "text": "\u00BB" + }, + { + "id": "K_PERIOD", + "text": ">", + "layer": "shift" + }, + { + "id": "K_RBRKT", + "text": "}", + "layer": "shift" + } + ] + }, + { + "id": "K_EQUAL", + "layer": "shift", + "text": "+" + }, + { + "id": "K_HYPHEN", + "text": "-" + }, + { + "id": "K_8", + "layer": "shift", + "text": "*" + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_LOWER", + "text": "*abc*", + "width": "150", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "610", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + } + ] + } +} diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kmn b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kmn new file mode 100644 index 00000000000..589104db35b --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kmn @@ -0,0 +1,14 @@ +c keyboard generated from template at $DATE +c with name "$NAME" +store(&NAME) '$NAME' +store(©RIGHT) '$COPYRIGHT' +store(&VERSION) '10.0' +store(&KEYBOARDVERSION) '$VERSION' +store(&TARGETS) '$TARGETS' +$Icon:store(&BITMAP) '$ID.ico' +$KVKS:store(&VISUALKEYBOARD) '$ID.kvks' +$TouchLayout:store(&LAYOUTFILE) '$ID.keyman-touch-layout' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kps b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kps new file mode 100644 index 00000000000..677104a39a7 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kps @@ -0,0 +1,47 @@ + + + + $KEYMANVERSION + 7.0 + + + readme.htm + ..\LICENSE.md + welcome.htm + + + + $NAME + $COPYRIGHT + $AUTHOR + $DESCRIPTION + + +$KMX: +$KMX: ..\build\$ID.kmx +$KMX: +$Web: +$Web: ..\build\$ID.js +$Web: +$KVKS: +$KVKS: ..\build\$ID.kvk +$KVKS: + + welcome.htm + + + readme.htm + + + ..\LICENSE.md + + + + + $NAME + $ID + $VERSION +$PACKAGE_LANGUAGES + + + diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kvks b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kvks new file mode 100644 index 00000000000..087598da102 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/source/keyboard.kvks @@ -0,0 +1,8 @@ + + +
+ 10.0 + $ID + +
+
diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/source/readme.htm b/developer/src/kmc-generate/src/template/kmn-keyboard/source/readme.htm new file mode 100644 index 00000000000..041cf910315 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + $NAME + + + + +

$NAME

+ +

+ $DESCRIPTION +

+ +

$COPYRIGHT

+ + + diff --git a/developer/src/kmc-generate/src/template/kmn-keyboard/source/welcome.htm b/developer/src/kmc-generate/src/template/kmn-keyboard/source/welcome.htm new file mode 100644 index 00000000000..c003dc2fbf1 --- /dev/null +++ b/developer/src/kmc-generate/src/template/kmn-keyboard/source/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using $NAME + + + + +

Start Using $NAME

+ +

+ $DESCRIPTION +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/.gitignore b/developer/src/kmc-generate/src/template/ldml-keyboard/.gitignore new file mode 100644 index 00000000000..65dc5567c89 --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/.gitignore @@ -0,0 +1,4 @@ +# This file lists files which should not be committed into source control +build/ +*.kpj.user +node_modules/ diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/HISTORY.md b/developer/src/kmc-generate/src/template/ldml-keyboard/HISTORY.md new file mode 100644 index 00000000000..9a605b69a3a --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/HISTORY.md @@ -0,0 +1,6 @@ +$NAME Change History +==================== + +$VERSION ($DATE) +---------------- +* Created by $AUTHOR diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/LICENSE.md b/developer/src/kmc-generate/src/template/ldml-keyboard/LICENSE.md new file mode 100644 index 00000000000..bb0daf3447f --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +$FULLCOPYRIGHT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/README.md b/developer/src/kmc-generate/src/template/ldml-keyboard/README.md new file mode 100644 index 00000000000..56c60908394 --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/README.md @@ -0,0 +1,18 @@ +$NAME keyboard +============== + +Description +----------- +$DESCRIPTION + +Links +----- +Keyboard Homepage: https://keyman.com/keyboards/$ID + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- +$PLATFORMS_DOTLIST_README diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/keyboard.kpj b/developer/src/kmc-generate/src/template/ldml-keyboard/keyboard.kpj new file mode 100644 index 00000000000..7ed1ac72df6 --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/keyboard.kpj @@ -0,0 +1,8 @@ + + + + 2.0 + True + True + + diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.kps b/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.kps new file mode 100644 index 00000000000..274bee3c826 --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.kps @@ -0,0 +1,44 @@ + + + + $KEYMANVERSION + 7.0 + + + readme.htm + ..\LICENSE.md + welcome.htm + + + + $NAME + $COPYRIGHT + $AUTHOR + $DESCRIPTION + + + + ..\build\$ID.kmx + + + ..\build\$ID.kvk + + + welcome.htm + + + readme.htm + + + ..\LICENSE.md + + + + + $NAME + $ID + $VERSION +$PACKAGE_LANGUAGES + + + diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.xml b/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.xml new file mode 100644 index 00000000000..1813fc54ad4 --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/source/keyboard.xml @@ -0,0 +1,40 @@ + + + + +$ADDITIONAL_LANG_TAGS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/source/readme.htm b/developer/src/kmc-generate/src/template/ldml-keyboard/source/readme.htm new file mode 100644 index 00000000000..041cf910315 --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + $NAME + + + + +

$NAME

+ +

+ $DESCRIPTION +

+ +

$COPYRIGHT

+ + + diff --git a/developer/src/kmc-generate/src/template/ldml-keyboard/source/welcome.htm b/developer/src/kmc-generate/src/template/ldml-keyboard/source/welcome.htm new file mode 100644 index 00000000000..c003dc2fbf1 --- /dev/null +++ b/developer/src/kmc-generate/src/template/ldml-keyboard/source/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using $NAME + + + + +

Start Using $NAME

+ +

+ $DESCRIPTION +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/.gitignore b/developer/src/kmc-generate/src/template/wordlist-lexical-model/.gitignore new file mode 100644 index 00000000000..65dc5567c89 --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/.gitignore @@ -0,0 +1,4 @@ +# This file lists files which should not be committed into source control +build/ +*.kpj.user +node_modules/ diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/HISTORY.md b/developer/src/kmc-generate/src/template/wordlist-lexical-model/HISTORY.md new file mode 100644 index 00000000000..9a605b69a3a --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/HISTORY.md @@ -0,0 +1,6 @@ +$NAME Change History +==================== + +$VERSION ($DATE) +---------------- +* Created by $AUTHOR diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/LICENSE.md b/developer/src/kmc-generate/src/template/wordlist-lexical-model/LICENSE.md new file mode 100644 index 00000000000..bb0daf3447f --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +$FULLCOPYRIGHT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/README.md b/developer/src/kmc-generate/src/template/wordlist-lexical-model/README.md new file mode 100644 index 00000000000..c94a8f66b8a --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/README.md @@ -0,0 +1,17 @@ +$NAME lexical model +=================== + +Description +----------- +$DESCRIPTION + +Links +----- + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- +$PLATFORMS_DOTLIST_README diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/model.kpj b/developer/src/kmc-generate/src/template/wordlist-lexical-model/model.kpj new file mode 100644 index 00000000000..aaef818a396 --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/model.kpj @@ -0,0 +1,10 @@ + + + + 2.0 + True + lexicalmodel + True + True + + diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/model.kps b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/model.kps new file mode 100644 index 00000000000..16c1f904bdc --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/model.kps @@ -0,0 +1,42 @@ + + + + $KEYMANVERSION + 12.0 + + + + readme.htm + ..\LICENSE.md + + + $NAME + $COPYRIGHT + $AUTHOR + $VERSION + $DESCRIPTION + + + + ..\build\$ID.model.js + + + welcome.htm + + + readme.htm + + + ..\LICENSE.md + + + + + + $NAME + $ID + $PACKAGE_LANGUAGES + + + + diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/model.ts b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/model.ts new file mode 100644 index 00000000000..529b9df996c --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/model.ts @@ -0,0 +1,14 @@ +/* + $NAME $VERSION + + This is a minimal lexical model source that uses a tab delimited wordlist. + See documentation online at https://help.keyman.com/developer/ for + additional parameters. +*/ + +const source: LexicalModelSource = { + format: 'trie-1.0', + wordBreaker: 'default', + sources: ['wordlist.tsv'], +}; +export default source; \ No newline at end of file diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/readme.htm b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/readme.htm new file mode 100644 index 00000000000..041cf910315 --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + $NAME + + + + +

$NAME

+ +

+ $DESCRIPTION +

+ +

$COPYRIGHT

+ + + diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/welcome.htm b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/welcome.htm new file mode 100644 index 00000000000..e8e6e7b4f5c --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/welcome.htm @@ -0,0 +1,28 @@ + + + + + + Start Using $NAME + + + + +

Start Using $NAME

+ +

+ $DESCRIPTION +

+ +

Wordlist Model Documentation

+ + + +

$COPYRIGHT

+ + + \ No newline at end of file diff --git a/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/wordlist.tsv b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/wordlist.tsv new file mode 100644 index 00000000000..567a98abc7f --- /dev/null +++ b/developer/src/kmc-generate/src/template/wordlist-lexical-model/source/wordlist.tsv @@ -0,0 +1,16 @@ +# +# $NAME $VERSION generated from template. +# +# This is an example tab-separated wordlist file that can be edited in a spreadsheet +# program or regenerated from a wordlist tool. See lexical model documentation at +# https://help.keyman.com/developer/ for tools. +# +# Columns +# ======= +# WORD FREQUENCY (Optional)NOTES +# +the 100 +example 5 +hello 10 +world 8 +wordlist 3 \ No newline at end of file diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/.gitignore b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/.gitignore new file mode 100644 index 00000000000..65dc5567c89 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/.gitignore @@ -0,0 +1,4 @@ +# This file lists files which should not be committed into source control +build/ +*.kpj.user +node_modules/ diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/HISTORY.md b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/HISTORY.md new file mode 100644 index 00000000000..d4aceee4a8c --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/HISTORY.md @@ -0,0 +1,6 @@ +Sample Project Change History +==================== + +1.0 (2024-04-12) +---------------- +* Created by Sample Author diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/LICENSE.md b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/LICENSE.md new file mode 100644 index 00000000000..7dfb994497a --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +© 2024 TheAuthor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/README.md b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/README.md new file mode 100644 index 00000000000..8ebc4631ab7 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/README.md @@ -0,0 +1,18 @@ +Sample Project keyboard +============== + +Description +----------- +# A mighty description + +Links +----- +Keyboard Homepage: https://keyman.com/keyboards/sample + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- + * Windows diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/sample.kpj b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/sample.kpj new file mode 100644 index 00000000000..7ed1ac72df6 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/sample.kpj @@ -0,0 +1,8 @@ + + + + 2.0 + True + True + + diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/readme.htm b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/readme.htm new file mode 100644 index 00000000000..e44886f5fd2 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + Sample Project + + + + +

Sample Project

+ +

+ # A mighty description +

+ +

© TheAuthor

+ + + diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kmn b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kmn new file mode 100644 index 00000000000..97e1c74de5d --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kmn @@ -0,0 +1,12 @@ +c keyboard generated from template at 2024-04-12 +c with name "Sample Project" +store(&NAME) 'Sample Project' +store(©RIGHT) '© TheAuthor' +store(&VERSION) '10.0' +store(&KEYBOARDVERSION) '1.0' +store(&TARGETS) 'windows' +store(&VISUALKEYBOARD) 'sample.kvks' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps new file mode 100644 index 00000000000..62931d3bcfd --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kps @@ -0,0 +1,46 @@ + + + + 17.0.292.0 + 7.0 + + + readme.htm + ..\LICENSE.md + welcome.htm + + + + Sample Project + © TheAuthor + Sample Author + # A mighty description + + + + ..\build\sample.kmx + + + ..\build\sample.kvk + + + welcome.htm + + + readme.htm + + + ..\LICENSE.md + + + + + Sample Project + sample + 1.0 + + en + + + + diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kvks b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kvks new file mode 100644 index 00000000000..cef32581c53 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/sample.kvks @@ -0,0 +1,8 @@ + + +
+ 10.0 + sample + +
+
diff --git a/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/welcome.htm b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/welcome.htm new file mode 100644 index 00000000000..1454a604b61 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/keyman-keyboard/sample/source/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using Sample Project + + + + +

Start Using Sample Project

+ +

+ # A mighty description +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/.gitignore b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/.gitignore new file mode 100644 index 00000000000..65dc5567c89 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/.gitignore @@ -0,0 +1,4 @@ +# This file lists files which should not be committed into source control +build/ +*.kpj.user +node_modules/ diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/HISTORY.md b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/HISTORY.md new file mode 100644 index 00000000000..d4aceee4a8c --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/HISTORY.md @@ -0,0 +1,6 @@ +Sample Project Change History +==================== + +1.0 (2024-04-12) +---------------- +* Created by Sample Author diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/LICENSE.md b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/LICENSE.md new file mode 100644 index 00000000000..7dfb994497a --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +© 2024 TheAuthor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/README.md b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/README.md new file mode 100644 index 00000000000..8ebc4631ab7 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/README.md @@ -0,0 +1,18 @@ +Sample Project keyboard +============== + +Description +----------- +# A mighty description + +Links +----- +Keyboard Homepage: https://keyman.com/keyboards/sample + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- + * Windows diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/sample.kpj b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/sample.kpj new file mode 100644 index 00000000000..7ed1ac72df6 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/sample.kpj @@ -0,0 +1,8 @@ + + + + 2.0 + True + True + + diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/readme.htm b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/readme.htm new file mode 100644 index 00000000000..e44886f5fd2 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + Sample Project + + + + +

Sample Project

+ +

+ # A mighty description +

+ +

© TheAuthor

+ + + diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps new file mode 100644 index 00000000000..62931d3bcfd --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.kps @@ -0,0 +1,46 @@ + + + + 17.0.292.0 + 7.0 + + + readme.htm + ..\LICENSE.md + welcome.htm + + + + Sample Project + © TheAuthor + Sample Author + # A mighty description + + + + ..\build\sample.kmx + + + ..\build\sample.kvk + + + welcome.htm + + + readme.htm + + + ..\LICENSE.md + + + + + Sample Project + sample + 1.0 + + en + + + + diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.xml b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.xml new file mode 100644 index 00000000000..0636dfd26f1 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/sample.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/welcome.htm b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/welcome.htm new file mode 100644 index 00000000000..1454a604b61 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/ldml-keyboard/sample/source/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using Sample Project + + + + +

Start Using Sample Project

+ +

+ # A mighty description +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/.gitignore b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/.gitignore new file mode 100644 index 00000000000..65dc5567c89 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/.gitignore @@ -0,0 +1,4 @@ +# This file lists files which should not be committed into source control +build/ +*.kpj.user +node_modules/ diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/HISTORY.md b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/HISTORY.md new file mode 100644 index 00000000000..d4aceee4a8c --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/HISTORY.md @@ -0,0 +1,6 @@ +Sample Project Change History +==================== + +1.0 (2024-04-12) +---------------- +* Created by Sample Author diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/LICENSE.md b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/LICENSE.md new file mode 100644 index 00000000000..7dfb994497a --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +© 2024 TheAuthor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/README.md b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/README.md new file mode 100644 index 00000000000..32fb70becc5 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/README.md @@ -0,0 +1,27 @@ +Sample Project lexical model +=================== + +Description +----------- +# A mighty description + +Links +----- + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- + * Windows + * macOS + * Linux + * Web + * iPhone + * iPad + * Android phone + * Android tablet + * Mobile devices + * Desktop devices + * Tablet devices diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/sample.en.sample.kpj b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/sample.en.sample.kpj new file mode 100644 index 00000000000..aaef818a396 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/sample.en.sample.kpj @@ -0,0 +1,10 @@ + + + + 2.0 + True + lexicalmodel + True + True + + diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/readme.htm b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/readme.htm new file mode 100644 index 00000000000..e44886f5fd2 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + Sample Project + + + + +

Sample Project

+ +

+ # A mighty description +

+ +

© TheAuthor

+ + + diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps new file mode 100644 index 00000000000..5020fd3eb0d --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.kps @@ -0,0 +1,44 @@ + + + + 17.0.292.0 + 12.0 + + + + readme.htm + ..\LICENSE.md + + + Sample Project + © TheAuthor + Sample Author + 1.0 + # A mighty description + + + + ..\build\sample.en.sample.model.js + + + welcome.htm + + + readme.htm + + + ..\LICENSE.md + + + + + + Sample Project + sample.en.sample + + en + + + + + diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.ts b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.ts new file mode 100644 index 00000000000..3d7ad7efd45 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/sample.en.sample.model.ts @@ -0,0 +1,14 @@ +/* + Sample Project 1.0 + + This is a minimal lexical model source that uses a tab delimited wordlist. + See documentation online at https://help.keyman.com/developer/ for + additional parameters. +*/ + +const source: LexicalModelSource = { + format: 'trie-1.0', + wordBreaker: 'default', + sources: ['wordlist.tsv'], +}; +export default source; \ No newline at end of file diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/welcome.htm b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/welcome.htm new file mode 100644 index 00000000000..5270414e375 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/welcome.htm @@ -0,0 +1,28 @@ + + + + + + Start Using Sample Project + + + + +

Start Using Sample Project

+ +

+ # A mighty description +

+ +

Wordlist Model Documentation

+ + + +

© TheAuthor

+ + + \ No newline at end of file diff --git a/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/wordlist.tsv b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/wordlist.tsv new file mode 100644 index 00000000000..04ce1378ffc --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/lexical-model/sample.en.sample/source/wordlist.tsv @@ -0,0 +1,16 @@ +# +# Sample Project 1.0 generated from template. +# +# This is an example tab-separated wordlist file that can be edited in a spreadsheet +# program or regenerated from a wordlist tool. See lexical model documentation at +# https://help.keyman.com/developer/ for tools. +# +# Columns +# ======= +# WORD FREQUENCY (Optional)NOTES +# +the 100 +example 5 +hello 10 +world 8 +wordlist 3 \ No newline at end of file diff --git a/developer/src/kmc-generate/test/helpers/index.ts b/developer/src/kmc-generate/test/helpers/index.ts new file mode 100644 index 00000000000..8250dfd101a --- /dev/null +++ b/developer/src/kmc-generate/test/helpers/index.ts @@ -0,0 +1,17 @@ +/** + * Helpers and utilities for the Mocha tests. + */ +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Builds a path to the fixture with the given path components. + * + * e.g., makePathToFixture('example.qaa.trivial') + * e.g., makePathToFixture('example.qaa.trivial', 'model.ts') + * + * @param components One or more path components. + */ + export function makePathToFixture(...components: string[]): string { + return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url)); +} diff --git a/developer/src/kmc-generate/test/shared-options.ts b/developer/src/kmc-generate/test/shared-options.ts new file mode 100644 index 00000000000..1010c417b48 --- /dev/null +++ b/developer/src/kmc-generate/test/shared-options.ts @@ -0,0 +1,20 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import { KeymanTargets } from "@keymanapp/common-types"; +import { GeneratorOptions } from "../src/abstract-generator.js"; + +export const options: GeneratorOptions = { + icon: false, + id: 'sample', + languageTags: ['en'], + name: 'Sample Project', + outPath: '.', + targets: [KeymanTargets.KeymanTarget.windows], + version: '1.0', + author: 'Sample Author', + copyright: 'TheAuthor', + description: '# A mighty description', + keymanVersion: '17.0.292' +}; \ No newline at end of file diff --git a/developer/src/kmc-generate/test/test-abstract-generator.ts b/developer/src/kmc-generate/test/test-abstract-generator.ts new file mode 100644 index 00000000000..32bac54017e --- /dev/null +++ b/developer/src/kmc-generate/test/test-abstract-generator.ts @@ -0,0 +1,42 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import 'mocha'; +import * as path from 'path'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; +import { assert } from 'chai'; +import { AbstractGenerator, GeneratorArtifacts } from '../src/abstract-generator.js'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { options } from './shared-options.js'; + +describe('AbstractGenerator', function () { + it('should have a valid targetPath', async function() { + const ag = new AbstractGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert(await ag.init(callbacks, options)); + assert.equal(ag.unitTestEndpoints.targetPath(), path.join('.','sample')); + // assert(ag.verifyInitialize()); + }); + + it('should write out files successfully', async function() { + const ag = new AbstractGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert(await ag.init(callbacks, options)); + + const filename = path.join(path.dirname(fileURLToPath(import.meta.url)), 'test_artifact.bin'); + const data = new Uint8Array([1,2,3]); + const artifacts: GeneratorArtifacts = { + 'test_artifact.bin': {filename, data} + }; + assert(await ag.write(artifacts)); + assert(fs.existsSync(filename)); + const buf = fs.readFileSync(filename); + assert.deepEqual(buf, data); + + fs.unlinkSync(filename); + // assert(ag.verifyInitialize()); + }); +}); + diff --git a/developer/src/kmc-generate/test/test-basic-generator.ts b/developer/src/kmc-generate/test/test-basic-generator.ts new file mode 100644 index 00000000000..b1eb1e420d0 --- /dev/null +++ b/developer/src/kmc-generate/test/test-basic-generator.ts @@ -0,0 +1,23 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import 'mocha'; +import { assert } from 'chai'; +import { BasicGenerator } from '../src/basic-generator.js'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { options } from './shared-options.js'; + +describe('BasicGenerator', function () { + it('should configure default settings', async function() { + const bg = new BasicGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert(await bg.init(callbacks, options)); + bg.test_preGenerate(); + + assert.equal(bg.test_tokenMap['$NAME'], 'Sample Project'); + }); + + // TODO-GENERATE: test transform +}); + diff --git a/developer/src/kmc-generate/test/test-keyman-keyboard-generator.ts b/developer/src/kmc-generate/test/test-keyman-keyboard-generator.ts new file mode 100644 index 00000000000..ac76aa5b937 --- /dev/null +++ b/developer/src/kmc-generate/test/test-keyman-keyboard-generator.ts @@ -0,0 +1,73 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import 'mocha'; +import sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { assert } from 'chai'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { options } from './shared-options.js'; +import { KeymanKeyboardGenerator } from '../src/keyman-keyboard-generator.js'; +import { makePathToFixture } from './helpers/index.js'; + +// Fixture generated with: +// kmc generate keyman-keyboard -L en -n 'Sample Project' -t windows -v 1.0 -a 'Sample Author' -c 'TheAuthor' --description '# A mighty description' sample + +function getFilenames(p: string, base?: string): string[] { + base = base ?? ''; + const files = fs.readdirSync(p); + let result: string[] = []; + for(const file of files) { + const fp = path.join(p, file); + if(fs.statSync(fp).isDirectory()) { + result = result.concat(getFilenames(fp, base + file + path.sep)); + } else { + result.push(base + file); + } + } + return result; +} + +describe('KeymanKeyboardGenerator', function () { + let clock: sinon.SinonFakeTimers; + + before(function() { + // We will always be 12 April 2024 to match test fixtures + clock = sinon.useFakeTimers(new Date(2024, 3, 12)); + }); + + after(function() { + clock.restore(); + }); + + it('should generate a Keyman keyboard from provided options', async function() { + const generator = new KeymanKeyboardGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert(await generator.init(callbacks, options)); + const result = await generator.run(); + assert.exists(result); + assert.exists(result.artifacts); + + const samplePath = makePathToFixture('keyman-keyboard'); + const files = getFilenames(samplePath); + + // Verify that there are no unexpected artifacts + for(const artifact of Object.keys(result.artifacts)) { + assert.include(files, artifact); + } + + // Compare each file content as a UTF-8 string + for(const file of files) { + // TODO-GENERATE: this only works until we have binary files (icons) + if(!fs.statSync(path.join(samplePath, file)).isDirectory()) { + assert.isDefined(result.artifacts[file]); + assert.equal(result.artifacts[file].filename, file); + const fixtureUTF8 = fs.readFileSync(path.join(samplePath, file), 'utf-8').replace(/\r\n/g, '\n'); + const actualUTF8 = new TextDecoder().decode(result.artifacts[file].data); + assert.equal(actualUTF8, fixtureUTF8, `File ${file} does not have expected content`); + } + } + }); +}); diff --git a/developer/src/kmc-generate/test/test-ldml-keyboard-generator.ts b/developer/src/kmc-generate/test/test-ldml-keyboard-generator.ts new file mode 100644 index 00000000000..c08526d9ba4 --- /dev/null +++ b/developer/src/kmc-generate/test/test-ldml-keyboard-generator.ts @@ -0,0 +1,72 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import 'mocha'; +import sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { assert } from 'chai'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { options } from './shared-options.js'; +import { LdmlKeyboardGenerator } from '../src/ldml-keyboard-generator.js'; +import { makePathToFixture } from './helpers/index.js'; + +// Fixture generated with: +// kmc generate ldml-keyboard -L en -n 'Sample Project' -v 1.0 -a 'Sample Author' -c 'TheAuthor' --description '# A mighty description' sample + +function getFilenames(p: string, base?: string): string[] { + base = base ?? ''; + const files = fs.readdirSync(p); + let result: string[] = []; + for(const file of files) { + const fp = path.join(p, file); + if(fs.statSync(fp).isDirectory()) { + result = result.concat(getFilenames(fp, base + file + path.sep)); + } else { + result.push(base + file); + } + } + return result; +} + +describe('LdmlKeyboardGenerator', function () { + let clock: sinon.SinonFakeTimers; + + before(function() { + // We will always be 12 April 2024 to match test fixtures + clock = sinon.useFakeTimers(new Date(2024, 3, 12)); + }); + + after(function() { + clock.restore(); + }); + + it('should generate a LDML keyboard from provided options', async function() { + const generator = new LdmlKeyboardGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert(await generator.init(callbacks, options)); + const result = await generator.run(); + assert.exists(result); + assert.exists(result.artifacts); + + const samplePath = makePathToFixture('ldml-keyboard'); + const files = getFilenames(samplePath); + + // Verify that there are no unexpected artifacts + for(const artifact of Object.keys(result.artifacts)) { + assert.include(files, artifact); + } + + // compare each file content as a UTF-8 string + for(const file of files) { + if(!fs.statSync(path.join(samplePath, file)).isDirectory()) { + assert.isDefined(result.artifacts[file]); + assert.equal(result.artifacts[file].filename, file); + const fixtureUTF8 = fs.readFileSync(path.join(samplePath, file), 'utf-8').replace(/\r\n/g, '\n');; + const actualUTF8 = new TextDecoder().decode(result.artifacts[file].data); + assert.equal(actualUTF8, fixtureUTF8, `File ${file} does not have expected content`); + } + } + }); +}); diff --git a/developer/src/kmc-generate/test/test-lexical-model-generator.ts b/developer/src/kmc-generate/test/test-lexical-model-generator.ts new file mode 100644 index 00000000000..24ee7e1c39c --- /dev/null +++ b/developer/src/kmc-generate/test/test-lexical-model-generator.ts @@ -0,0 +1,78 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import 'mocha'; +import sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { assert } from 'chai'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { options } from './shared-options.js'; +import { LexicalModelGenerator } from '../src/lexical-model-generator.js'; +import { makePathToFixture } from './helpers/index.js'; +import { GeneratorOptions } from '../src/abstract-generator.js'; +import { KeymanTargets } from '@keymanapp/common-types'; + +// Fixture generated with: +// kmc generate lexical-model -L en -n 'Sample Project' -v 1.0 -a 'Sample Author' -c 'TheAuthor' --description '# A mighty description' sample.en.sample + +function getFilenames(p: string, base?: string): string[] { + base = base ?? ''; + const files = fs.readdirSync(p); + let result: string[] = []; + for(const file of files) { + const fp = path.join(p, file); + if(fs.statSync(fp).isDirectory()) { + result = result.concat(getFilenames(fp, base + file + path.sep)); + } else { + result.push(base + file); + } + } + return result; +} + +describe('LexicalModelGenerator', function () { + let clock: sinon.SinonFakeTimers; + + before(function() { + // We will always be 12 April 2024 to match test fixtures + clock = sinon.useFakeTimers(new Date(2024, 3, 12)); + }); + + after(function() { + clock.restore(); + }); + + it('should generate a lexical model from provided options', async function() { + const generator = new LexicalModelGenerator(); + const callbacks = new TestCompilerCallbacks(); + const opts: GeneratorOptions = {...options}; + opts.id = 'sample.en.sample'; + opts.targets = [KeymanTargets.KeymanTarget.any]; + + assert(await generator.init(callbacks, opts)); + const result = await generator.run(); + assert.exists(result); + assert.exists(result.artifacts); + + const samplePath = makePathToFixture('lexical-model'); + const files = getFilenames(samplePath); + + // Verify that there are no unexpected artifacts + for(const artifact of Object.keys(result.artifacts)) { + assert.include(files, artifact); + } + + // compare each file content as a UTF-8 string + for(const file of files) { + if(!fs.statSync(path.join(samplePath, file)).isDirectory()) { + assert.isDefined(result.artifacts[file]); + assert.equal(result.artifacts[file].filename, file); + const fixtureUTF8 = fs.readFileSync(path.join(samplePath, file), 'utf-8').replace(/\r\n/g, '\n');; + const actualUTF8 = new TextDecoder().decode(result.artifacts[file].data); + assert.equal(actualUTF8, fixtureUTF8, `File ${file} does not have expected content`); + } + } + }); +}); diff --git a/developer/src/kmc-generate/test/test-messages.ts b/developer/src/kmc-generate/test/test-messages.ts new file mode 100644 index 00000000000..fa9683ba1d6 --- /dev/null +++ b/developer/src/kmc-generate/test/test-messages.ts @@ -0,0 +1,52 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import 'mocha'; +import * as path from 'path'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; +import { assert } from 'chai'; +import { GeneratorMessages } from '../src/generator-messages.js'; +import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; +import { AbstractGenerator, GeneratorOptions } from '../src/abstract-generator.js'; + +describe('GeneratorMessages', function () { + it('should have a valid GeneratorMessages object', function() { + return verifyCompilerMessagesObject(GeneratorMessages, CompilerErrorNamespace.Generator); + }); + + it('should generate ERROR_OutputPathAlreadyExists if output path already exists', async function () { + const ag = new AbstractGenerator(); + const callbacks = new TestCompilerCallbacks(); + const options: GeneratorOptions = { + id: 'ERROR_OutputPathAlreadyExists', + outPath: path.dirname(fileURLToPath(import.meta.url)), + }; + assert(await ag.init(callbacks, options)); + const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'ERROR_OutputPathAlreadyExists'); + if(!fs.existsSync(dir)) + fs.mkdirSync(dir); + assert.isFalse(await ag.write({})); + assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_OutputPathAlreadyExists), + `messageId ERROR_OutputPathAlreadyExists not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); + + fs.rmdirSync(dir); + }); + + it('should generate ERROR_CannotWriteOutputFile if it cannot create a folder, e.g. invalid filename', async function () { + const ag = new AbstractGenerator(); + const callbacks = new TestCompilerCallbacks(); + const options: GeneratorOptions = { + id: 'ERROR_CannotWriteOutputFile', + outPath: path.dirname(fileURLToPath(import.meta.url)), + }; + assert(await ag.init(callbacks, options)); + assert.isFalse(await ag.write({ + '.': {filename: '.', data: new Uint8Array([1,2,3])} + })); + assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile), + `messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); + }); +}); diff --git a/developer/src/kmc-generate/test/tsconfig.json b/developer/src/kmc-generate/test/tsconfig.json new file mode 100644 index 00000000000..145099305d2 --- /dev/null +++ b/developer/src/kmc-generate/test/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../kmc/tsconfig.kmc-base.json", + + "compilerOptions": { + "rootDir": ".", + "rootDirs": ["./", "../src/"], + "outDir": "../build/test", + "baseUrl": ".", + "strictNullChecks": true, + "paths": { + // "@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"], + "@keymanapp/common-types": ["../../../../common/web/types/src/main"], + "@keymanapp/developer-test-helpers": ["../../common/web/test-helpers/index"], + // "@keymanapp/": ["core/include/ldml/ldml-keyboard-constants"], + }, + }, + "include": [ + "**/test-*.ts", + "./shared-options.ts", + "./helpers/index.ts" + ], + "references": [ + { "path": "../../../../common/web/keyman-version" }, + { "path": "../../../../common/web/types/" }, + { "path": "../../common/web/test-helpers/" }, + { "path": "../" } + ] +} \ No newline at end of file diff --git a/developer/src/kmc-generate/tsconfig.json b/developer/src/kmc-generate/tsconfig.json new file mode 100644 index 00000000000..1fc0a249be4 --- /dev/null +++ b/developer/src/kmc-generate/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + + "compilerOptions": { + "outDir": "build/src/", + "rootDir": "src/", + "baseUrl": ".", + + "paths": { + "@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"], + "@keymanapp/common-types": ["../../../common/web/types/src/main"], + }, + + }, + "include": [ + "src/**/*.ts", + ], + "exclude": [ + "src/template/**/*.ts", + ], + "references": [ + { "path": "../../../common/web/keyman-version" }, + { "path": "../../../common/web/types/" }, + ] +} diff --git a/developer/src/kmc-package/src/compiler/package-validation.ts b/developer/src/kmc-package/src/compiler/package-validation.ts index a55610adad3..3f98f7d830f 100644 --- a/developer/src/kmc-package/src/compiler/package-validation.ts +++ b/developer/src/kmc-package/src/compiler/package-validation.ts @@ -1,22 +1,9 @@ -import { KmpJsonFile, KeymanFileTypes } from '@keymanapp/common-types'; +import { KmpJsonFile, KeymanFileTypes, } from '@keymanapp/common-types'; +import { SourceFilenamePatterns } from '@keymanapp/developer-utils'; import { PackageCompilerMessages } from './package-compiler-messages.js'; import { keymanEngineForWindowsFiles, keymanForWindowsInstallerFiles, keymanForWindowsRedistFiles } from './redist-files.js'; import { isValidEmail, CompilerCallbacks, CompilerOptions } from '@keymanapp/developer-utils'; -// The keyboard ID SHOULD adhere to this pattern: -const KEYBOARD_ID_PATTERN_PACKAGE = /^[a-z_][a-z0-9_]*\.(kps|kmp)$/; - -// The model ID SHOULD adhere to this pattern: -// author .bcp47 .uniq -const MODEL_ID_PATTERN_PACKAGE = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.(kps|kmp)$/; -// const MODEL_ID_PATTERN_JS = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.js$/; -// const MODEL_ID_PATTERN_TS = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.ts$/; -// const MODEL_ID_PATTERN_PROJECT = /^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_-]*\.[a-z_][a-z0-9_]*\.model\.kpj$/; - -// "Content files" within the package should adhere to these pattern: -const CONTENT_FILE_BASENAME_PATTERN = /^[a-z0-9_+.-]+$/i; // base names can be case insensitive -const CONTENT_FILE_EXTENSION_PATTERN = /^(\.[a-z0-9_-]+)?$/; // extensions should be lower-case or empty - /** * @internal */ @@ -108,7 +95,7 @@ export class PackageValidation { filename = this.callbacks.path.basename(filename); - if(!MODEL_ID_PATTERN_PACKAGE.test(filename)) { + if(!SourceFilenamePatterns.MODEL_ID_PATTERN_PACKAGE.test(filename)) { this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageNameDoesNotFollowLexicalModelConventions({filename})); } @@ -128,7 +115,7 @@ export class PackageValidation { filename = this.callbacks.path.basename(filename); - if(!KEYBOARD_ID_PATTERN_PACKAGE.test(filename)) { + if(!SourceFilenamePatterns.KEYBOARD_ID_PATTERN_PACKAGE.test(filename)) { this.callbacks.reportMessage(PackageCompilerMessages.Warn_PackageNameDoesNotFollowKeyboardConventions({filename})); } @@ -158,7 +145,8 @@ export class PackageValidation { const ext = this.callbacks.path.extname(filename); const base = filename.substring(0, filename.length-ext.length); if(this.options.checkFilenameConventions) { - if(!CONTENT_FILE_BASENAME_PATTERN.test(base) || !CONTENT_FILE_EXTENSION_PATTERN.test(ext)) { + if(!SourceFilenamePatterns.CONTENT_FILE_BASENAME_PATTERN.test(base) || + !SourceFilenamePatterns.CONTENT_FILE_EXTENSION_PATTERN.test(ext)) { this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileInPackageDoesNotFollowFilenameConventions({filename})); } } diff --git a/developer/src/kmc/build.sh b/developer/src/kmc/build.sh index a39808a546f..08e89646779 100755 --- a/developer/src/kmc/build.sh +++ b/developer/src/kmc/build.sh @@ -19,6 +19,7 @@ builder_describe "Build Keyman Keyboard Compiler kmc" \ "@/core/include/ldml" \ "@/developer/src/common/web/utils" \ "@/developer/src/kmc-analyze" \ + "@/developer/src/kmc-generate" \ "@/developer/src/kmc-keyboard-info" \ "@/developer/src/kmc-kmn" \ "@/developer/src/kmc-ldml" \ @@ -95,7 +96,10 @@ function do_bundle() { # Manually copy over kmcmplib module cp ../kmc-kmn/build/src/import/kmcmplib/wasm-host.wasm build/dist/ - cp build/dist/* "$BUILD_PATH" + # Manually copy over templates + cp -R ../kmc-generate/build/src/template/ build/dist/ + + cp -R build/dist/* "$BUILD_PATH" builder_finish_action success bundle } diff --git a/developer/src/kmc/package.json b/developer/src/kmc/package.json index 4d36bb1e98c..cfa8ce9466d 100644 --- a/developer/src/kmc/package.json +++ b/developer/src/kmc/package.json @@ -38,6 +38,7 @@ "@keymanapp/developer-utils": "*", "@keymanapp/keyman-version": "*", "@keymanapp/kmc-analyze": "*", + "@keymanapp/kmc-generate": "*", "@keymanapp/kmc-keyboard-info": "*", "@keymanapp/kmc-kmn": "*", "@keymanapp/kmc-ldml": "*", diff --git a/developer/src/kmc/src/commands/generate.ts b/developer/src/kmc/src/commands/generate.ts new file mode 100644 index 00000000000..bac724d9d46 --- /dev/null +++ b/developer/src/kmc/src/commands/generate.ts @@ -0,0 +1,146 @@ +// import * as path from 'path'; +import { Command } from 'commander'; +import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js'; +import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; +import { BaseOptions } from '../util/baseOptions.js'; +import { exitProcess } from '../util/sysexits.js'; +import { GeneratorOptions, KeymanKeyboardGenerator, LdmlKeyboardGenerator, LexicalModelGenerator } from '@keymanapp/kmc-generate'; +import { CompilerCallbacks, KeymanCompiler } from '@keymanapp/developer-utils'; + +/* c8 ignore start */ + +export function declareGenerate(program: Command) { + const command = program.command('generate'); + declareGenerateKmnKeyboard(command); + declareGenerateLdmlKeyboard(command); + declareGenerateLexicalModel(command); +} + +function declareGenerateKmnKeyboard(command: Command) { + const subCommand = command.command('keyman-keyboard '); + BaseOptions.addLogLevel(subCommand); + subCommand + .description('Generate a .kmn keyboard project') + .option('-t, --target ', 'Target platforms', + (value, previous) => previous.concat([value]), []) + .option('-o, --out-path ', 'Output path (may exist)') + .option('-n, --name ', 'Keyboard descriptive name') + .option('-c, --copyright ', 'Copyright holder') + .option('-v, --version ', 'Keyboard version', '1.0') + .option('-L, --language-tag ', 'BCP-47 language tag', + (value, previous) => previous.concat([value]), []) + .option('-a, --author ', 'Name of keyboard author') + .option('-i, --icon', 'Include a generated icon', true) + .option('-d, --description ', 'Short description of the project, Markdown') + .action(generateKmnKeyboard); +} + +function declareGenerateLdmlKeyboard(command: Command) { + const ldmlSubCommand = command.command('ldml-keyboard '); + BaseOptions.addLogLevel(ldmlSubCommand); + ldmlSubCommand + .description('Generate an LDML .xml keyboard project') + .option('-t, --target ', 'Target platforms', + (value, previous) => previous.concat([value]), []) + .option('-o, --out-path ', 'Output path (may exist)') + .option('-n, --name ', 'Keyboard descriptive name') + .option('-c, --copyright ', 'Copyright holder') /* © yyyy */ + .option('-v, --version ', 'Keyboard version', '1.0') + .option('-L, --language-tag ', 'BCP-47 language tag', + (value, previous) => previous.concat([value]), []) + .option('-a, --author ', 'Name of keyboard author') + .option('-d, --description ', 'Short description of the project, Markdown') + .action(generateLdmlKeyboard); +} + +function declareGenerateLexicalModel(command: Command) { + const modelSubCommand = command.command('lexical-model '); + BaseOptions.addLogLevel(modelSubCommand); + modelSubCommand + .description('Generate a wordlist lexical model project') + .option('-o, --out-path ', 'Output path (may exist)') + .option('-n, --name ', 'Keyboard descriptive name') + .option('-c, --copyright ', 'Copyright holder') /* © yyyy */ + .option('-v, --version ', 'Keyboard version', '1.0') + .option('-L, --language-tag ', 'BCP-47 language tag', + (value, previous) => previous.concat([value]), []) + .option('-a, --author ', 'Name of keyboard author') + .option('-d, --description ', 'Short description of the project, Markdown') + .action(generateLexicalModel); + + //TODO:required opts +} + +function commanderOptionsToGeneratorOptions(id: string, options: any): GeneratorOptions { + const result: GeneratorOptions = { + icon: options.icon ?? true, + id, + languageTags: options.languageTag ?? [], + name: options.name ?? id, + outPath: options.outPath ?? '.', + targets: options.target ?? ['any'], + version: options.version ?? '1.0', + author: options.author ?? id, + copyright: options.copyright ?? options.author ?? id, + logLevel: options.logLevel ?? 'info', + description: options.description ?? '', + }; + return result; +} + +const generateKmnKeyboard = async (ids: string | string[], _options: any, commander: any) => + generate(new KeymanKeyboardGenerator(), ids, commander); +const generateLdmlKeyboard = async (ids: string | string[], _options: any, commander: any) => + generate(new LdmlKeyboardGenerator(), ids, commander); +const generateLexicalModel = async (ids: string | string[], _options: any, commander: any) => + generate(new LexicalModelGenerator(), ids, commander); + +async function generate(generator: KeymanCompiler, ids: string | string[], commander: any): Promise { + const commanderOptions = commander.optsWithGlobals(); + const callbacks = new NodeCompilerCallbacks({logLevel: commanderOptions.logLevel ?? 'info'}); + if(!await doGenerate(callbacks, generator, ids, commanderOptions)) { + return await exitProcess(1); + } +} + +/* c8 ignore stop */ + +async function doGenerate(callbacks: CompilerCallbacks, generator: KeymanCompiler, ids: string | string[], commanderOptions: any): Promise { + const id = ids; + + if(!id || typeof id != 'string') { + // Note that commander can pass an array for the ids parameter, so we + // constrain here + callbacks.reportMessage(InfrastructureMessages.Error_GenerateRequiresId()); + return false; + } + + const options = commanderOptionsToGeneratorOptions(id, commanderOptions); + + try { + if(!await generator.init(callbacks, options)) { + // errors will have been reported by the generator + return false; + } + const result = await generator.run(id); // note: id is currently ignored here + if(!result) { + // errors will have been reported by the generator + return false; + } + if(!await generator.write(result.artifacts)) { + // errors will have been reported by the generator + return false; + } + } catch(e) { + /* c8 ignore next 3 */ + callbacks.reportMessage(InfrastructureMessages.Fatal_UnexpectedException({e})); + return false; + } + + return true; +} + +/** @internal */ +export const unitTestEndpoints = { + doGenerate, +} \ No newline at end of file diff --git a/developer/src/kmc/src/kmc.ts b/developer/src/kmc/src/kmc.ts index 726807dbaee..128e5faa44d 100644 --- a/developer/src/kmc/src/kmc.ts +++ b/developer/src/kmc/src/kmc.ts @@ -12,6 +12,7 @@ import { TestKeymanSentry } from './util/TestKeymanSentry.js'; import { exitProcess } from './util/sysexits.js'; import { declareMessage } from './commands/messageCommand.js'; import { kmcSentryOptions } from './util/kmcSentryOptions.js'; +import { declareGenerate } from './commands/generate.js'; await TestKeymanSentry.runTestIfCLRequested(kmcSentryOptions); if(KeymanSentry.isEnabled()) { @@ -50,6 +51,7 @@ async function run() { declareBuild(program); declareAnalyze(program); declareMessage(program); + declareGenerate(program); /* Future commands: declareClean(program); diff --git a/developer/src/kmc/src/messages/infrastructureMessages.ts b/developer/src/kmc/src/messages/infrastructureMessages.ts index b4d8cebd2ef..02a5791f7db 100644 --- a/developer/src/kmc/src/messages/infrastructureMessages.ts +++ b/developer/src/kmc/src/messages/infrastructureMessages.ts @@ -141,5 +141,10 @@ export class InfrastructureMessages { static Error_MessageNamespaceNameNotFound = (o:{message: string}) => m( this.ERROR_MessageNamespaceNameNotFound, `Invalid parameter: --message ${def(o.message)} does not have a recognized namespace`); + + static ERROR_GenerateRequiresId = SevError | 0x0020; + static Error_GenerateRequiresId = () => m( + this.ERROR_GenerateRequiresId, + `The generate command requires a single 'id' parameter`); } diff --git a/developer/src/kmc/src/messages/messageNamespaces.ts b/developer/src/kmc/src/messages/messageNamespaces.ts index fc0e7e4d277..1bbe80518e0 100644 --- a/developer/src/kmc/src/messages/messageNamespaces.ts +++ b/developer/src/kmc/src/messages/messageNamespaces.ts @@ -1,5 +1,6 @@ import { CommonTypesMessages, CompilerErrorNamespace } from '@keymanapp/developer-utils'; import { AnalyzerMessages } from '@keymanapp/kmc-analyze'; +import { GeneratorMessages } from '@keymanapp/kmc-generate'; import { KeyboardInfoCompilerMessages } from '@keymanapp/kmc-keyboard-info'; import { KmnCompilerMessages, KmwCompilerMessages } from '@keymanapp/kmc-kmn'; import { LdmlCompilerMessages } from '@keymanapp/kmc-ldml'; @@ -20,6 +21,7 @@ const messageNamespaces: Record = { [CompilerErrorNamespace.KmwCompiler]: KmwCompilerMessages, [CompilerErrorNamespace.ModelInfoCompiler]: ModelInfoCompilerMessages, [CompilerErrorNamespace.KeyboardInfoCompiler]: KeyboardInfoCompilerMessages, + [CompilerErrorNamespace.Generator]: GeneratorMessages, }; // This works around pain points in enumerating enum members in Typescript @@ -40,6 +42,7 @@ export const messageSources: Record=4" } }, + "developer/src/kmc-generate": { + "name": "@keymanapp/kmc-generate", + "license": "MIT", + "dependencies": { + "@keymanapp/common-types": "*", + "@keymanapp/developer-utils": "*", + "@keymanapp/keyman-version": "*" + }, + "devDependencies": { + "@keymanapp/developer-test-helpers": "*", + "@keymanapp/resources-gosh": "*", + "@types/mocha": "^10.0.0", + "@types/node": "^20.4.1", + "@types/semver": "^7.3.12", + "c8": "^7.12.0", + "chalk": "^2.4.2", + "mocha": "^10.0.0", + "typescript": "^5.4.5" + } + }, + "developer/src/kmc-generate/node_modules/@types/mocha": { + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", + "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", + "dev": true + }, + "developer/src/kmc-generate/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "developer/src/kmc-generate/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "developer/src/kmc-generate/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "developer/src/kmc-generate/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "developer/src/kmc-generate/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "developer/src/kmc-generate/node_modules/supports-color/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "developer/src/kmc-keyboard": { "name": "@keymanapp/kmc-keyboard", "extraneous": true, @@ -2790,6 +2879,10 @@ "resolved": "developer/src/kmc-analyze", "link": true }, + "node_modules/@keymanapp/kmc-generate": { + "resolved": "developer/src/kmc-generate", + "link": true + }, "node_modules/@keymanapp/kmc-keyboard-info": { "resolved": "developer/src/kmc-keyboard-info", "link": true @@ -6421,15 +6514,6 @@ "node": ">=12" } }, - "node_modules/chai/node_modules/check-error": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", - "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, "node_modules/chai/node_modules/deep-eql": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz", @@ -6477,6 +6561,16 @@ "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/check-markdown": { "resolved": "resources/tools/check-markdown", "link": true diff --git a/package.json b/package.json index 51228544692..9185c3ec4ef 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "developer/src/common/web/test-helpers", "developer/src/common/web/utils", "developer/src/kmc-analyze", + "developer/src/kmc-generate", "developer/src/kmc-keyboard-info", "developer/src/kmc-kmn", "developer/src/kmc-ldml", diff --git a/tsconfig.json b/tsconfig.json index 28f8eab2937..080f45c3065 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,8 @@ { "path": "./developer/src/kmc/test/tsconfig.json" }, { "path": "./developer/src/kmc-analyze/tsconfig.json" }, // { "path": "./developer/src/kmc-analyze/test/tsconfig.json" }, + { "path": "./developer/src/kmc-generate/tsconfig.json" }, + { "path": "./developer/src/kmc-generate/test/tsconfig.json" }, { "path": "./developer/src/kmc-kmn/test/tsconfig.json" }, { "path": "./developer/src/kmc-kmn/tsconfig.json" }, { "path": "./developer/src/kmc-keyboard-info/test/tsconfig.json" },