UNPKG

@keymanapp/kmc-model-info

Version:

Keyman Developer .model_info compiler

240 lines 10.2 kB
/** * Builds a source .model_info file with metadata extracted from .kps file and * compiled files to produce a comprehensive .model_info file. */ import { minKeymanVersion } from "./min-keyman-version.js"; import { ModelInfoCompilerMessages } from "./model-info-compiler-messages.js"; import { KeymanUrls, isValidEmail, validateMITLicense } from "@keymanapp/developer-utils"; /* c8 ignore start */ /** * @public * Description of sources and metadata required to build a .model_info file */ export class ModelInfoSources { /** The identifier for the model */ model_id; /** The data from the .kps file, transformed to kmp.json */ kmpJsonData; /** The path in the keymanapp/lexical-models repo where this model may be found */ sourcePath; /** The compiled model filename and relative path (.js) */ modelFileName; /** The compiled package filename and relative path (.kmp) */ kmpFileName; /** The source package filename and relative path (.kps) */ kpsFilename; /** Last modification date for files in the project folder 'YYYY-MM-DDThh:mm:ssZ' */ lastCommitDate; /** Return an error if project does not meet requirements of lexical-models repository */ forPublishing; } ; ; ; ; /** * @public * Compiles source data from a lexical model project to a .model_info. The * compiler does not read or write from filesystem or network directly, but * relies on callbacks for all external IO. */ export class ModelInfoCompiler { callbacks; options; constructor() { } /** * Initialize the compiler. * Copies options. * @param callbacks - Callbacks for external interfaces, including message * reporting and file io * @param options - Compiler options * @returns false if initialization fails */ async init(callbacks, options) { this.callbacks = callbacks; this.options = { ...options }; return true; } /** * Builds .model_info file with metadata from the model and package source * file. 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 ModelInfoCompiler.init} function to read any input files by disk. * * This function is intended for use within the lexical-models repository. * While many of the parameters could be deduced from each other, they are * specified here to reduce the number of places the filenames are * constructed. * * @param infile - Path to source file. Path will be parsed to find relative * references in the .kpj file, such as .model.ts or * .model.kps file * @param outfile - Path to output file. The file will not be written to, but * will be included in the result for use by * {@link ModelInfoCompiler.write}. * @returns Binary artifacts on success, null on failure. * * @param sources - Details on files from which to extract additional metadata */ async run(inputFilename, outputFilename) { const sources = this.options.sources; /* * Model info looks like this: * * { * "name": "Example Template Model" * "license": "mit", * "version": "1.0.0", * "languages": ["en"], * "authorName": "Example Author", * "authorEmail": "nobody@example.com", * "description": "Example wordlist model" * } * * For full documentation, see: * https://help.keyman.com/developer/cloud/model_info/1.0/ */ let jsFile = null; if (sources.modelFileName) { jsFile = this.loadJsFile(sources.modelFileName); if (!jsFile) { return null; } } let model_info = { languages: [], }; // // Build .model_info file -- some fields have "special" behaviours -- see below // https://api.keyman.com/schemas/model_info.source.json and // https://api.keyman.com/schemas/model_info.distribution.json // https://help.keyman.com/developer/cloud/model_info/1.0 // model_info.id = sources.model_id; model_info.name = sources.kmpJsonData.info.name.description; // License if (!sources.kmpJsonData.options?.licenseFile) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_NoLicenseFound()); return null; } if (!this.isLicenseMIT(this.callbacks.resolveFilename(sources.kpsFilename, sources.kmpJsonData.options.licenseFile))) { return null; } model_info.license = 'mit'; const author = sources.kmpJsonData.info.author; model_info.authorName = author?.description ?? ''; if (author?.url) { // we strip the mailto: from the .kps file for the .model_info const match = author.url.match(/^(mailto\:)?(.+)$/); /* c8 ignore next 3 */ if (match === null) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_InvalidAuthorEmail({ email: author.url })); return null; } if (!isValidEmail(match[2])) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_InvalidAuthorEmail({ email: author.url })); return null; } model_info.authorEmail = match[2]; } // description if (sources.kmpJsonData.info.description?.description) { model_info.description = sources.kmpJsonData.info.description.description.trim(); } else { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_DescriptionIsMissing({ filename: sources.kpsFilename })); return null; } // isRTL -- this is a little bit of a heuristic from a compiled .js // which may need modification if compilers change if (jsFile?.match(/("?)isRTL("?):\s*true/)) { model_info.isRTL = true; } // extract the language identifiers from the language metadata // arrays for each of the lexical models in the kmp.json file, // and merge into a single array of identifiers in the // .model_info file. model_info.languages = sources.kmpJsonData.lexicalModels.reduce((a, e) => [].concat(a, e.languages.map((f) => f.id)), []); // If a last commit date is not given, then just use the current time model_info.lastModifiedDate = sources.lastCommitDate ?? (new Date).toISOString(); model_info.packageFilename = this.callbacks.path.basename(sources.kmpFileName); model_info.packageFileSize = this.callbacks.fileSize(sources.kmpFileName); if (model_info.packageFileSize === undefined) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_FileDoesNotExist({ filename: sources.kmpFileName })); return null; } model_info.jsFilename = this.callbacks.path.basename(sources.modelFileName); model_info.jsFileSize = this.callbacks.fileSize(sources.modelFileName); if (model_info.jsFileSize === undefined) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_FileDoesNotExist({ filename: sources.modelFileName })); return null; } model_info.packageIncludes = sources.kmpJsonData.files.filter((e) => !!e.name.match(/.[ot]tf$/i)).length ? ['fonts'] : []; model_info.version = sources.kmpJsonData.info.version.description; model_info.minKeymanVersion = minKeymanVersion; model_info.helpLink = KeymanUrls.HELP_MODEL(model_info.id); if (sources.sourcePath) { model_info.sourcePath = sources.sourcePath; } const jsonOutput = JSON.stringify(model_info, null, 2); const data = new TextEncoder().encode(jsonOutput); const result = { artifacts: { model_info: { data, filename: outputFilename ?? inputFilename.replace(/\.kpj$/, '.model_info') } } }; return result; } /** * Write artifacts from a successful compile to disk, via callbacks methods. * The artifacts written may include: * * - .model_info file - metadata file used by keyman.com * * @param artifacts - object containing artifact binary data to write out * @returns true on success */ async write(artifacts) { this.callbacks.fs.writeFileSync(artifacts.model_info.filename, artifacts.model_info.data); return true; } isLicenseMIT(filename) { const data = this.callbacks.loadFile(filename); if (!data) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_LicenseFileIsMissing({ filename })); return false; } let license = null; try { license = new TextDecoder().decode(data); } catch (e) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_LicenseFileIsDamaged({ filename })); return false; } if (!license) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_LicenseFileIsDamaged({ filename })); return false; } const message = validateMITLicense(license); if (message != null) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_LicenseIsNotValid({ filename, message })); return false; } return true; } loadJsFile(filename) { const data = this.callbacks.loadFile(filename); if (!data) { this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_FileDoesNotExist({ filename })); return null; } const text = new TextDecoder('utf-8', { fatal: true }).decode(data); return text; } } //# sourceMappingURL=model-info-compiler.js.map