unit Keyman.Developer.System.KeyboardProjectTemplate; interface uses System.SysUtils, kpsfile, UKeymanTargets; type EKeyboardProjectTemplate = class(Exception); TKeyboardProjectTemplate = class private FName: string; FBasePath: string; FKeyboardID: string; FTargets: TKeymanTargets; FIconFilename: string; FOSKFilename: string; FTouchLayoutFilename: string; FAuthor: string; FCopyright: string; FVersion: string; FBCP47Tags: string; FIncludeIcon: Boolean; function GetFilename(Base: string): string; function GetIconFilename: string; function GetKeyboardFilename: string; function GetOSKFilename: string; function GetPackageFilename: string; function GetProjectFilename: string; function GetTouchLayoutFilename: string; function HasIcon: Boolean; function HasKMX: Boolean; function HasKVKS: Boolean; function HasTouchLayout: Boolean; procedure WriteKMN; procedure WriteKPS; procedure WriteKVKS; procedure WriteTouchLayout; procedure WriteKPJ; procedure WriteRepositoryMetadata; procedure WriteDocumentation; procedure WriteKeyboardInfo; procedure Transform(const SourceFile: string; DestFile: string = ''); function DataPath: string; procedure SetKeyboardLanguages(kps: TkpsFile); public constructor Create(const BasePath, KeyboardID: string; Targets: TKeymanTargets); destructor Destroy; override; procedure Generate; property Name: string read FName write FName; property Copyright: string read FCopyright write FCopyright; property Author: string read FAuthor write FAuthor; property Version: string read FVersion write FVersion; property BCP47Tags: string read FBCP47Tags write FBCP47Tags; property IncludeIcon: Boolean read FIncludeIcon write FIncludeIcon; property KeyboardFilename: string read GetKeyboardFilename; property ProjectFilename: string read GetProjectFilename; property PackageFilename: string read GetPackageFilename; property OSKFilename: string read GetOSKFilename; property IconFilename: string read GetIconFilename; property TouchLayoutFilename: string read GetTouchLayoutFilename; end; implementation uses System.Classes, System.StrUtils, BCP47Tag, Keyman.System.LanguageCodeUtils, KeyboardParser, KeymanVersion, kmxfileconsts, Keyman.Developer.System.Project.kmnProjectFile, Keyman.Developer.System.Project.kpsProjectFile, PackageInfo, Keyman.Developer.System.Project.ProjectFile, RedistFiles, TouchLayout, utilfiletypes, utilstr, VisualKeyboard; { TKeyboardProjectTemplate } constructor TKeyboardProjectTemplate.Create(const BasePath, KeyboardID: string; Targets: TKeymanTargets); begin inherited Create; FBasePath := IncludeTrailingPathDelimiter(ExpandFileName(BasePath)); FKeyboardID := LowerCase(KeyboardID); FTargets := Targets; FVersion := '1.0'; end; destructor TKeyboardProjectTemplate.Destroy; begin inherited Destroy; end; procedure TKeyboardProjectTemplate.Generate; begin if not ForceDirectories(FBasePath + FKeyboardID + '\source') then raise EKeyboardProjectTemplate.Create('Could not create destination path '+FBasePath+FKeyboardID); WriteDocumentation; WriteKMN; WriteKPS; if HasKVKS then WriteKVKS; if HasTouchLayout then WriteTouchLayout; WriteKPJ; WriteRepositoryMetadata; WriteKeyboardInfo; end; function TKeyboardProjectTemplate.GetFilename(Base: string): string; begin if Base = '' then Result := '' else if ExtractFileExt(Base) = Base then Result := FBasePath + FKeyboardID + '\source\' + FKeyboardID + Base else Result := FBasePath + FKeyboardID + '\source\' + Base; end; function TKeyboardProjectTemplate.GetIconFilename: string; begin if FIncludeIcon then Result := GetFilename(FIconFilename) else Result := ''; end; function TKeyboardProjectTemplate.GetKeyboardFilename: string; begin Result := GetFilename(Ext_KeymanSource); end; function TKeyboardProjectTemplate.GetOSKFilename: string; begin Result := GetFilename(FOSKFilename); end; function TKeyboardProjectTemplate.GetPackageFilename: string; begin Result := GetFilename(Ext_PackageSource); end; function TKeyboardProjectTemplate.GetProjectFilename: string; begin // Project file goes in parent folder Result := FBasePath + FKeyboardID + '\' + FKeyboardID + Ext_ProjectSource; end; function TKeyboardProjectTemplate.GetTouchLayoutFilename: string; begin Result := GetFilename(FTouchLayoutFilename); end; function TKeyboardProjectTemplate.HasIcon: Boolean; begin Result := FIncludeIcon and ((KMXKeymanTargets+[ktAny]) * FTargets <> []); end; function TKeyboardProjectTemplate.HasKMX: Boolean; begin Result := (KMXKeymanTargets+[ktAny]) * FTargets <> []; end; function TKeyboardProjectTemplate.HasKVKS: Boolean; begin Result := (KeymanTargetsUsingKVK+[ktAny]) * FTargets <> []; end; function TKeyboardProjectTemplate.HasTouchLayout: Boolean; begin Result := (TouchKeymanTargets+[ktAny]) * FTargets <> []; end; procedure TKeyboardProjectTemplate.WriteDocumentation; begin // Write readme.htm and welcome.htm Transform('source\welcome.htm'); Transform('source\readme.htm'); end; procedure TKeyboardProjectTemplate.WriteKeyboardInfo; begin // Write keyboardid.keyboard_info Transform('keyboard.keyboard_info', FKeyboardID+'.keyboard_info'); end; procedure TKeyboardProjectTemplate.WriteKMN; var FKeyboardSource: string; kp: TKeyboardParser; sl: TStringList; begin kp := TKeyboardParser.Create; try kp.FileName := KeyboardFilename; kp.InitialComment := #13#10+ FKeyboardID+' generated from template at '+FormatDateTime('yyyy-mm-dd hh:nn:ss', Now)+#13#10+ 'with name "'+FName+'"'#13#10; kp.SetSystemStoreValue(ssName, FName); if FCopyright <> '' then kp.SetSystemStoreValue(ssCopyright, FCopyright); kp.SetSystemStoreValue(ssVersion, SKeymanKeyboardVersion); kp.SetSystemStoreValue(ssKeyboardVersion, FVersion); kp.SetSystemStoreValue(ssTargets, KeymanTargetsToString(FTargets)); if HasIcon then kp.Features.Add(kfIcon); if HasKVKS then kp.Features.Add(kfOSK); if HasTouchLayout then kp.Features.Add(kfTouchLayout); kp.AddRequiredLines; FKeyboardSource := kp.KeyboardText; FIconFilename := kp.GetSystemStoreValue(ssBitmap); FOSKFilename := kp.GetSystemStoreValue(ssVisualKeyboard); FTouchLayoutFilename := kp.GetSystemStoreValue(ssLayoutFile); sl := TStringList.Create; try sl.Text := FKeyboardSource; sl.SaveToFile(KeyboardFilename, TEncoding.UTF8); finally sl.Free; end; finally kp.Free; end; end; procedure TKeyboardProjectTemplate.WriteKPJ; var kpj: TProject; begin kpj := TProject.Create(GetProjectFilename); try kpj.Options.BuildPath := '$PROJECTPATH\build'; kpj.Options.WarnDeprecatedCode := True; kpj.Options.CompilerWarningsAsErrors := True; kpj.Files.Add(TkmnProjectFile.Create(kpj, GetKeyboardFilename, nil)); kpj.Files.Add(TkpsProjectFile.Create(kpj, GetPackageFilename, nil)); kpj.Save; finally kpj.Free; end; end; procedure TKeyboardProjectTemplate.WriteKPS; var kps: TKPSFile; f: TPackageContentFile; pk: TPackageKeyboard; begin kps := TKPSFile.Create; try // Set kps metadata kps.Info.Desc[PackageInfo_Name] := FName; kps.Info.Desc[PackageInfo_Copyright] := FCopyright; kps.Info.Desc[PackageInfo_Author] := FAuthor; kps.KPSOptions.FollowKeyboardVersion := True; kps.FileName := GetPackageFilename; // Add .kmx if HasKMX then begin f := TPackageContentFile.Create(kps); f.FileName := FBasePath + FKeyboardID + '\build\' + FKeyboardID + Ext_KeymanFile; kps.Files.Add(f); end; // Add .js if HasTouchLayout then begin f := TPackageContentFile.Create(kps); f.FileName := FBasePath + FKeyboardID + '\build\' + FKeyboardID + Ext_Javascript; kps.Files.Add(f); end; // Add .kvk if HasKVKS then begin f := TPackageContentFile.Create(kps); f.FileName := FBasePath + FKeyboardID + '\build\' + FKeyboardID + Ext_VisualKeyboard; kps.Files.Add(f); end; // Add welcome f := TPackageContentFile.Create(kps); f.FileName := FBasePath + FKeyboardID + '\source\welcome.htm'; kps.Files.Add(f); // Add readme f := TPackageContentFile.Create(kps); f.FileName := FBasePath + FKeyboardID + '\source\readme.htm'; kps.Files.Add(f); kps.Options.ReadmeFile := f; // Add metadata about the keyboard pk := TPackageKeyboard.Create(kps); pk.Name := FName; pk.ID := FKeyboardID; pk.Version := FVersion; kps.Keyboards.Add(pk); SetKeyboardLanguages(kps); kps.SaveXML; finally kps.Free; end; end; procedure TKeyboardProjectTemplate.SetKeyboardLanguages(kps: TkpsFile); var tag, tags, s: string; bcp47tag: TBCP47Tag; pkl: TPackageKeyboardLanguage; begin tags := Trim(FBCP47Tags); tag := StrToken(tags, ' '); while tag <> '' do begin pkl := TPackageKeyboardLanguage.Create(kps); bcp47tag := TBCP47Tag.Create(tag); try bcp47tag.Canonicalize; pkl.ID := bcp47tag.Tag; if not TLanguageCodeUtils.BCP47Languages.TryGetValue(bcp47tag.Language, pkl.Name) then begin pkl.Name := bcp47tag.Language; end; if bcp47tag.Script <> '' then begin if TLanguageCodeUtils.BCP47Languages.TryGetValue(bcp47tag.Script, s) then pkl.Name := pkl.Name + ' ('+s+')'; end; finally bcp47tag.Free; end; kps.Keyboards[0].Languages.Add(pkl); tag := StrToken(tags, ' '); end; end; procedure TKeyboardProjectTemplate.WriteKVKS; var vk: TVisualKeyboard; begin Assert(HasKVKS); vk := TVisualKeyboard.Create; try vk.Header.AssociatedKeyboard := FKeyboardID; vk.SaveToFile(OSKFilename, kvksfXML); finally vk.Free; end; end; procedure TKeyboardProjectTemplate.WriteRepositoryMetadata; begin Transform('HISTORY.md'); Transform('LICENSE.md'); Transform('README.md'); end; procedure TKeyboardProjectTemplate.WriteTouchLayout; var tl: TTouchLayout; JSON: string; ss: TStringStream; sl: TStringList; const CTouchLayoutBasicFile = 'template-basic'+Ext_KeymanTouchLayout; begin Assert(HasTouchLayout); sl := TStringList.Create; // We use a stringlist because may have a BOM try sl.LoadFromFile(GetLayoutBuilderPath + CTouchLayoutBasicFile, TEncoding.UTF8); JSON := sl.Text; finally sl.Free; end; tl := TTouchLayout.Create; try // We'll load and save to ensure we get formatting tl.Load(JSON); JSON := tl.Save(True); // We don't want a BOM on output ss := TStringStream.Create(JSON, TEncoding.UTF8); try ss.SaveToFile(TouchLayoutFilename); finally ss.Free; end; finally tl.Free; end; end; function TKeyboardProjectTemplate.DataPath: string; begin Result := GetRedistProjectTemplatePath; end; procedure TKeyboardProjectTemplate.Transform(const SourceFile: string; DestFile: string = ''); var s: string; ss: TStringStream; function GetLanguageTagListForKeyboardInfo: string; var tags, tag: string; bcp47tag: TBCP47Tag; begin // We won't worry about line breaks... tags := Trim(FBCP47Tags); tag := StrToken(tags, ' '); Result := ''; while tag <> '' do begin if Result <> '' then Result := Result + ', '; bcp47tag := TBCP47Tag.Create(tag); try bcp47tag.Canonicalize; Result := Result + '"' + bcp47tag.Tag + '"'; finally bcp47tag.Free; end; tag := StrToken(tags, ' '); end; end; function GetPlatformsDotListForReadme: string; var kt: TKeymanTarget; begin Result := ''; for kt := Low(TKeymanTarget) to High(TKeymanTarget) do begin if kt = ktAny then Continue; Result := Result + ' * ' + SKeymanTargetNames[kt] + #13#10; end; end; begin if DestFile = '' then DestFile := SourceFile; ss := TStringStream.Create('', TEncoding.UTF8); try ss.LoadFromFile(DataPath + SourceFile); s := ss.DataString; finally ss.Free; end; s := ReplaceStr(s, '$NAME', FName); s := ReplaceStr(s, '$VERSION', FVersion); s := ReplaceStr(s, '$COPYRIGHT', FCopyright); s := ReplaceStr(s, '$AUTHOR', FAuthor); s := ReplaceStr(s, '$DATE', FormatDateTime('yyyy-mm-dd', Now)); if Pos('$LANGUAGES_KEYBOARD_INFO', s) > 0 then s := ReplaceStr(s, '$LANGUAGES_KEYBOARD_INFO', GetLanguageTagListForKeyboardInfo); if Pos('$PLATFORMS_DOTLIST_README', s) > 0 then s := ReplaceStr(s, '$PLATFORMS_DOTLIST_README', GetPlatformsDotListForReadme); ss := TStringStream.Create(s, TEncoding.UTF8); try ss.SaveToFile(FBasePath + FKeyboardID + '\' + DestFile); finally ss.Free; end; end; end.