All files / src/types/kpj kpj-file-reader.ts

86.32% Statements 101/117
71.87% Branches 23/32
100% Functions 8/8
86.32% Lines 101/117

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 1181x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 7x 7x 7x 7x 7x 7x 7x 4x 65x 65x     65x 4x 7x 7x 5x 5x 5x             5x 5x 5x 20x 20x 20x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x       5x 5x 5x 5x 5x 5x 5x 5x 5x   5x 5x 5x 5x 5x     5x 5x 5x 5x 5x 5x 5x 63x 63x 63x 63x 63x 63x 63x 6x 6x 6x 6x 6x 63x 57x 63x 6x 6x 6x 63x 5x 5x 5x 5x 5x 5x 5x 5x 7x     7x 3x 3x 7x 7x 7x 5x  
import { KeymanXMLReader } from '../../index.js';
import { KPJFile, KPJFileProject } from './kpj-file.js';
import { util } from '@keymanapp/common-types';
import { KeymanDeveloperProject, KeymanDeveloperProjectFile10, KeymanDeveloperProjectType } from './keyman-developer-project.js';
import { SchemaValidators } from '@keymanapp/common-types';
import { CompilerAsyncCallbacks } from "../../compiler-callbacks.js";
 
export class KPJFileReader {
  constructor(private callbacks: CompilerAsyncCallbacks) {
  }
 
  public read(file: Uint8Array): KPJFile {
    let data: KPJFile;
 
    data = new KeymanXMLReader('kpj')
      .parse(new TextDecoder().decode(file));
 
    data = this.boxArrays(data);
    if(data.KeymanDeveloperProject?.Files?.File?.length) {
      for(const file of data.KeymanDeveloperProject?.Files?.File) {
        // xml2js imports <Details/> as '' so we will just delete the empty string
        if(typeof file.Details == 'string') {
          delete file.Details;
        }
      }
    }
    return data as KPJFile;
  }
 
  public validate(source: KPJFile): void {
    if(!SchemaValidators.default.kpj(source)) {
      if(!SchemaValidators.default.kpj90(source)) {
        // If the legacy schema also does not validate, then we will only report
        // the errors against the modern schema
        throw new Error(JSON.stringify((<any>SchemaValidators.default.kpj).errors));
      }
    }
  }
 
  private boolFromString(value: string, def: boolean) {
    value = (value || '').toLowerCase();
    if(value === 'true') return true;
    if(value === 'false') return false;
    return def;
  }
 
  public async transform(projectFilename: string, source: KPJFile): Promise<KeymanDeveloperProject> {
    // NOTE: at this point, the xml should have been validated
    // and matched the schema result so we can assume the source
    // is a valid shape
    const project = source.KeymanDeveloperProject;
    const result: KeymanDeveloperProject = new KeymanDeveloperProject(projectFilename, project.Options?.Version || "1.0", this.callbacks);
    if(result.options.version == '2.0') {
      result.options.buildPath = (project.Options?.BuildPath || result.options.buildPath).replace(/\\/g, '/');
      result.options.sourcePath = (project.Options?.SourcePath || result.options.sourcePath).replace(/\\/g, '/');
      result.options.skipMetadataFiles = this.boolFromString(project.Options?.SkipMetadataFiles, false);
    } else {
      result.options.buildPath = (project.Options?.BuildPath || '').replace(/\\/g, '/');
      result.options.skipMetadataFiles = this.boolFromString(project.Options?.SkipMetadataFiles, true);
    }
    result.options.checkFilenameConventions = this.boolFromString(project.Options?.CheckFilenameConventions, false);
    result.options.compilerWarningsAsErrors = this.boolFromString(project.Options?.CompilerWarningsAsErrors, false);
    result.options.warnDeprecatedCode = this.boolFromString(project.Options?.WarnDeprecatedCode, true);
    result.options.projectType =
      project.Options?.ProjectType == 'keyboard' ? KeymanDeveloperProjectType.Keyboard :
      project.Options?.ProjectType == 'lexicalmodel' ? KeymanDeveloperProjectType.LexicalModel :
      KeymanDeveloperProjectType.Keyboard; // Default is keyboard if missing
 
    if(result.options.version == '1.0') {
      this.transformFilesVersion10(project, result);
    } else {
      await result.populateFiles();
    }
 
    return result;
  }
 
  private transformFilesVersion10(project: KPJFileProject, result: KeymanDeveloperProject) {
    const ids: { [id: string]: KeymanDeveloperProjectFile10; } = {};
    for (const sourceFile of project.Files?.File) {
      const file: KeymanDeveloperProjectFile10 = new KeymanDeveloperProjectFile10(
        sourceFile.ID || '',
        (sourceFile.Filepath || '').replace(/\\/g, '/'),
        sourceFile.FileVersion || '',
        this.callbacks.path
      );
      if (sourceFile.Details) {
        file.details.copyright = sourceFile.Details.Copyright;
        file.details.name = sourceFile.Details.Name;
        file.details.message = sourceFile.Details.Message;
        file.details.version = sourceFile.Details.Version;
      }
      if (sourceFile.ParentFileID && ids[sourceFile.ParentFileID]) {
        ids[sourceFile.ParentFileID].childFiles.push(file);
      } else {
        result.files.push(file);
        ids[file.id] = file;
      }
    }
  }
 
  /**
   * xml2js will not place single-entry objects into arrays.
   * Easiest way to fix this is to box them ourselves as needed
   * @param source KVKSourceFile
   */
  private boxArrays(source: KPJFile) {
    if(!source.KeymanDeveloperProject) {
      return source;
    }
    if(!source.KeymanDeveloperProject.Files || typeof source.KeymanDeveloperProject.Files == 'string') {
      source.KeymanDeveloperProject.Files = {File:[]};
    }
    util.boxXmlArray(source.KeymanDeveloperProject.Files, 'File');
    return source;
  }
}