spiegel-keyman/developer/src/tike/compile/MergeKeyboardInfo.pas
Marc Durdin aa9788ad46 chore(common): remove deprecated fields from keyboard_info
Removes the following fields:
* legacyId
* documentationFilename
* documentationFileSize
* links
* related[].note

None of these fields are needed any longer; see #9351 for steps to
remove data.

Eliminates difference between keyboard_info source and distribution,
renaming to keyboard_info.schema.json, and updating the required members
of the json accordingly.

Will have corresponding commits in keyman.com, api.keyman.com,
help.keyman.com, and keyboards repositories.
2023-08-11 10:34:38 +07:00

1134 lines
31 KiB
ObjectPascal

{ Merges a source .keyboard_info file with data programatically extracted from a .kmp file and a keyboard .js file
- Command line parameters:
kmcomp -m <keyboard.kmp> <keyboard.js> [-add-help-link <help path>] <keyboard.keyboard_info>
Note: if keyboard.kmp or keyboard.js do not exist, merge_compiled_keyboard_info will continue to work, just will
not attempt to pull data from them
#
# There are a number of fields we can fill in programatically.
#
# id -- from .keyboard_info name
# name -- from kmp.inf, js
# authorName -- from kmp.inf
# authorEmail -- from kmp.inf
# lastModifiedDate -- build time: this only gets refreshed when the version num increments so it's close enough then
# packageFilename -- from $keyboard_info_packageFilename
# packageFileSize -- get from the size of the file
# jsFilename -- from $keyboard_info_jsFilename
# jsFileSize -- get from the size of the file
# isRTL -- from .js, KRTL\s*=\s*1
# encodings -- from .kmx (existence of .js implies unicode)
# packageIncludes -- from kmp.inf?
# version -- from kmp.inf, js
# minKeymanVersion -- from kmp.inf, kmx, js
# platformSupport -- deduce from whether kmp exists, js exists
# languages -- given the BCP 47 ids, generate the subtag names:
# displayName, languageName - (required)
# scriptName, regionName - if not blank
}
unit MergeKeyboardInfo;
interface
uses
System.Json,
compile,
kmpinffile,
packageinfo,
TempFileManager,
kmxfile,
kmxfileconsts,
Keyman.Developer.System.Project.ProjectLog,
UKeymanTargets;
type
TKeyboardInfoMap = record
Filename: string;
Info: TKeyboardInfo;
end;
TJSKeyboardInfoMap = record
Filename: string;
Data: string;
Standalone: Boolean;
end;
TMergeKeyboardInfo = class
private
json: TJSONObject;
FSilent: Boolean;
FCallback: TProjectLogObjectEvent;
FBaseName, FJsFile, FKmpFile, FJsonFile: string;
FMergingValidateIds: Boolean;
FKMPInfFile: TKMPInfFile;
FPackageKMXFileInfos: array of TKeyboardInfoMap;
FPackageJSFileInfos: array of TKeyboardInfoMap;
FJSFileInfo: TJSKeyboardInfoMap;
FVersion: string;
FSourcePath: string;
FHelpLink: string;
function Failed(message: string): Boolean;
function Execute: Boolean; overload;
function LoadJsonFile: Boolean;
function LoadKMPFile: Boolean;
function LoadJSFile: Boolean;
constructor Create(ASourcePath, AJsFile, AKmpFile, AJsonFile, AHelpLink: string; AMergingValidateIds, ASilent: Boolean; ACallback: TProjectLogObjectEvent);
procedure AddAuthor;
procedure AddAuthorEmail;
procedure CheckOrAddEncodings;
procedure CheckOrAddFileSizes;
procedure CheckOrAddID;
procedure CheckOrAddJsFilename;
procedure AddLastModifiedDate;
procedure CheckOrAddMinKeymanVersion;
procedure AddName;
procedure CheckOrAddPackageFilename;
procedure AddPackageIncludes;
procedure AddHelpLink;
procedure AddPlatformSupport;
procedure CheckOrAddVersion;
procedure AddSubtagNames(id: String; o: TJSONObject);
procedure CheckOrMigrateLanguages;
function SaveJsonFile: Boolean;
procedure CheckPackageKeyboardFilenames;
procedure AddIsRTL;
procedure AddSourcePath;
public
destructor Destroy; override;
class function Execute(ASourcePath, AJsFile, AKmpFile, AJsonFile, AHelpLink: string; AMergingValidateIds, ASilent: Boolean; ACallback: TProjectLogObjectEvent): Boolean; overload;
end;
implementation
uses
Soap.XsBuiltIns,
System.Classes,
System.Generics.Collections,
System.RegularExpressions,
System.SysUtils,
System.Zip,
Keyman.System.RegExGroupHelperRSP19902,
BCP47Tag,
JsonUtil,
Keyman.System.KeyboardInfoFile,
Keyman.System.KeyboardUtils,
Keyman.System.LanguageCodeUtils,
utilfiletypes,
VersionInfo;
type
EInvalidKeyboardInfo = class(Exception)
end;
{ TMergeKeyboardInfo }
class function TMergeKeyboardInfo.Execute(ASourcePath, AJsFile, AKmpFile, AJsonFile, AHelpLink: string;
AMergingValidateIds, ASilent: Boolean; ACallback: TProjectLogObjectEvent): Boolean;
begin
with TMergeKeyboardInfo.Create(ASourcePath, AJsFile, AKmpFile, AJsonFile, AHelpLink, AMergingValidateIds, ASilent, ACallback) do
try
Result := Execute;
finally
Free;
end;
end;
constructor TMergeKeyboardInfo.Create(ASourcePath, AJsFile, AKmpFile, AJsonFile, AHelpLink: string;
AMergingValidateIds, ASilent: Boolean; ACallback: TProjectLogObjectEvent);
begin
inherited Create;
FSourcePath := ASourcePath;
FMergingValidateIds := AMergingValidateIds;
FHelpLink := AHelpLink;
FSilent := ASilent;
FCallback := ACallback;
if not SameText(ExtractFileExt(AJsFile), '.js') then
begin
FKmpFile := AJsFile;
FJsFile := AKmpFile;
end
else
begin
FKmpFile := AKmpFile;
FJsFile := AJsFile;
end;
FJsonFile := AJsonFile;
FBaseName := ChangeFileExt(ExtractFileName(FJsonFile), '');
end;
destructor TMergeKeyboardInfo.Destroy;
begin
inherited Destroy;
json.Free;
FKMPInfFile.Free;
end;
function TMergeKeyboardInfo.Execute: Boolean;
begin
try
if not LoadJsonFile then
Exit(Failed('Could not parse keyboard_info file '+FJsonFile));
if not LoadKMPFile then
Exit(Failed('Could not load KMP file '+FKmpFile));
if not LoadJSFile then
Exit(Failed('Could not load JS file '+FJsFile));
CheckPackageKeyboardFilenames;
CheckOrAddID;
CheckOrMigrateLanguages;
AddName;
AddIsRTL;
AddAuthor;
AddAuthorEmail;
AddLastModifiedDate;
AddSourcePath;
CheckOrAddVersion; // must be called before CheckOrAddJsFilename
CheckOrAddPackageFilename;
CheckOrAddJsFilename;
CheckOrAddEncodings;
CheckOrAddFileSizes;
AddPackageIncludes;
CheckOrAddMinKeymanVersion;
AddHelpLink;
AddPlatformSupport;
if not SaveJsonFile then
Exit(Failed('Could not save updated keyboard_info file '+FJsonFile));
except
on E:EInvalidKeyboardInfo do
begin
Failed('Invalid .keyboard_info file: '+E.Message);
Exit(False);
end;
on E:Exception do
begin
Failed('Fatal error '+E.ClassName+': '+E.Message);
Exit(False);
end;
end;
Result := True;
end;
function TMergeKeyboardInfo.LoadJsonFile: Boolean;
begin
try
with TStringStream.Create('', TEncoding.UTF8) do
try
LoadFromFile(FJsonFile);
json := TJSONObject.ParseJsonValue(DataString) as TJSONObject;
finally
Free;
end;
except
on E:Exception do
Exit(Failed(E.Message));
end;
Result := Assigned(json);
end;
function TMergeKeyboardInfo.LoadKMPFile: Boolean;
var
FKMXTempFile, FKMPInfTempFile: TTempFile;
i, j: Integer;
LocalHeader: TZipHeader;
Zip: TZipFile;
procedure SaveMemberToFile(MemberFilename, OutFilename: string);
var
ZipMemberStream, OutFileStream: TStream;
begin
OutFileStream := TFileStream.Create(OutFilename, fmCreate);
try
Zip.Read(MemberFilename, ZipMemberStream, LocalHeader);
try
OutFileStream.CopyFrom(ZipMemberStream, 0);
finally
ZipMemberStream.Free;
end;
finally
OutFileStream.Free;
end;
end;
begin
if FKMPFile = '' then
Exit(True);
if not SameText(ExtractFileExt(FKMPFile), '.kmp') then
Exit(Failed('packageFile must be a .kmp file '+FKMPFile));
try
Zip := TZipFile.Create;
try
Zip.Open(FKMPFile, zmRead);
FKMPInfFile := TKMPInfFile.Create;
if Zip.IndexOf('kmp.json') >= 0 then
begin
FKMPInfTempFile := TTempFileManager.Get('.json');
try
FKMPInfFile.FileName := FKMPInfTempFile.Name;
SaveMemberToFile('kmp.json', FKMPInfTempFile.Name);
FKMPInfFile.LoadJson;
finally
FKMPInfTempFile.Free;
end;
end
else
begin
FKMPInfTempFile := TTempFileManager.Get('.inf');
try
FKMPInfFile.FileName := FKMPInfTempFile.Name;
SaveMemberToFile('kmp.inf', FKMPInfTempFile.Name);
FKMPInfFile.LoadIni;
finally
FKMPInfTempFile.Free;
end;
end;
FKMXTempFile := TTempFileManager.Get('.kmx');
try
if FKMPInfFile.Keyboards.Count > 0 then
begin
for i := 0 to FKMPInfFile.Keyboards.Count - 1 do
begin
for j := 0 to High(Zip.FileNames) do
begin
// Add the KMX to FPackageKMXFileinfos
if SameText(Zip.FileName[j], FKMPInfFile.Keyboards[i].ID + '.kmx') then
begin
SetLength(FPackageKMXFileInfos, Length(FPackageKMXFileInfos)+1);
SaveMemberToFile(Zip.FileNames[j], FKMXTempFile.Name);
FPackageKMXFileInfos[High(FPackageKMXFileInfos)].Filename := Zip.FileNames[j];
GetKeyboardInfo(FKMXTempFile.Name, False, FPackageKMXFileInfos[High(FPackageKMXFileInfos)].Info, False);
end;
// Add the JS to FPackageJSFileInfos
if SameText(Zip.FileNames[j], FKMPInfFile.Keyboards[i].ID + '.js') then
begin
SetLength(FPackageJSFileInfos, Length(FPackageJSFileInfos)+1);
FPackageJSFileInfos[High(FPackageJSFileInfos)].Filename := Zip.FileNames[j];
// Apply JS keyboard only to mobile targets, because web is not supported
// in a package. If a package does not support mobile, it should not include
// the .js.
// Not using GetKeyboardInfo because that only handles kmx files
FPackageJSFileInfos[High(FPackageJSFileInfos)].Info.Targets := 'mobile';
end;
end;
end;
end
// Handle keyboard packages which may not have [Keyboard] sections defined in kmp.inf.
// Note: They will never include any js keyboards.
else if FKMPInfFile.Keyboards.Count = 0 then
begin
for i := 0 to High(Zip.FileNames) do
begin
if IsKeyboardFile(Zip.FileName[i]) then
begin
SetLength(FPackageKMXFileInfos, Length(FPackageKMXFileInfos)+1);
SaveMemberToFile(Zip.FileNames[i], FKMXTempFile.Name);
FPackageKMXFileInfos[High(FPackageKMXFileInfos)].Filename := Zip.FileNames[i];
GetKeyboardInfo(FKMXTempFile.Name, False, FPackageKMXFileInfos[High(FPackageKMXFileInfos)].Info, False);
end;
end;
end;
finally
FKMXTempFile.Free;
end;
finally
FreeAndNil(Zip);
end;
except
on E:EZipException do
Exit(Failed(E.Message));
end;
Result := True;
end;
function TMergeKeyboardInfo.LoadJSFile: Boolean;
begin
if FJsFile = '' then
Exit(True);
with TStringStream.Create('', TEncoding.UTF8) do
try
LoadFromFile(FJsFile);
FJSFileInfo.Filename := FJsFile;
FJSFileInfo.Data := DataString;
finally
Free;
end;
FJSFileInfo.Standalone := True;
Result := True;
end;
function TMergeKeyboardInfo.SaveJsonFile: Boolean;
var
str: TStringList;
begin
str := TStringList.Create;
try
PrettyPrintJSON(json, str);
with TStringStream.Create(str.Text, TEncoding.UTF8) do
try
// Use TStringStream so we don't get a default BOM prolog
SaveToFile(FJsonFile);
finally
Free;
end;
finally
str.Free;
end;
Result := True;
end;
function TMergeKeyboardInfo.Failed(message: string): Boolean;
begin
FCallback(plsError, FJsonFile, message, 0, 0);
Result := False;
end;
//
// id -- from .keyboard_info name
//
procedure TMergeKeyboardInfo.CheckOrAddID;
var
v: TJSONValue;
FID: string;
begin
FID := ExtractFileName(ChangeFileExt(FJsonFile, ''));
v := json.GetValue('id');
if v <> nil then
begin
if v.Value <> FID then
raise EInvalidKeyboardInfo.CreateFmt('id field is "%s" but should be "%s"', [v.Value, FID]);
end
else
json.AddPair('id', FID);
end;
procedure TMergeKeyboardInfo.AddSubtagNames(id: String; o: TJSONObject);
var
displayName, languageName, scriptName, regionName: String;
v: TJSONValue;
bcp47Tag: TBCP47Tag;
begin
if id = '' then
Exit;
bcp47Tag := TBCP47Tag.Create(id);
try
TLanguageCodeUtils.BCP47Languages.TryGetValue(bcp47Tag.Language, languageName);
TLanguageCodeUtils.BCP47Scripts.TryGetValue(bcp47Tag.Script, scriptName);
TLanguageCodeUtils.BCP47Regions.TryGetValue(bcp47Tag.Region, regionName);
finally
bcp47Tag.Free;
end;
displayName := TLanguageCodeUtils.LanguageName(languageName, scriptName, regionName);
v := o.Values[TKeyboardInfoFile.SDisplayName];
if not Assigned(v) then
o.AddPair(TKeyboardInfoFile.SDisplayName, displayName);
v := o.Values[TKeyboardInfoFile.SLanguageName];
if not Assigned(v) then
o.AddPair(TKeyboardInfoFile.SLanguageName, languageName);
if scriptName <> '' then
begin
v := o.Values[TKeyboardInfoFile.SScriptName];
if not Assigned(v) then
o.AddPair(TKeyboardInfoFile.SScriptName, scriptName);
end;
if regionName <> '' then
begin
v := o.Values[TKeyboardInfoFile.SRegionName];
if not Assigned(v) then
o.AddPair(TKeyboardInfoFile.SRegionName, regionName);
end;
end;
procedure TMergeKeyboardInfo.CheckOrMigrateLanguages;
var
v: TJSONValue;
alangs: TJSONArray;
olangs, o: TJSONObject;
pair: TJSONPair;
i: Integer;
id: string;
begin
v := json.GetValue(TKeyboardInfoFile.SLanguages);
if v is TJSONArray then
begin
// Migrate languages[] array to Object
alangs := v as TJSONArray;
olangs := TJSONObject.Create;
for i := 0 to alangs.Count - 1 do
begin
id := alangs.Items[i].Value;
if id = '' then
continue;
// Populate subtag names
o := TJSONObject.Create;
olangs.AddPair(id, o);
AddSubtagNames(id, o);
end;
json.RemovePair(TKeyboardInfoFile.SLanguages);
json.AddPair(TKeyboardInfoFile.SLanguages, olangs);
end
else if v is TJSONObject then
begin
olangs := v as TJsonObject;
for i := 0 to olangs.Count - 1 do
begin
pair := olangs.Pairs[i];
id := pair.JSONString.Value;
if id = '' then
continue;
// Populate subtag names
o := pair.JsonValue as TJSONObject;
AddSubtagNames(id, o);
end;
end;
end;
//
// name -- from kmp.inf, js
//
procedure TMergeKeyboardInfo.AddName;
var
FName: string;
begin
if json.GetValue('name') <> nil then Exit;
if Assigned(FKMPInfFile) then
FName := FKMPInfFile.Info.Desc[PackageInfo_Name]
else if not FJSFileInfo.Data.IsEmpty then
begin
with TRegEx.Match(FJSFileInfo.Data, 'this\.KN="([^"]+)"') do
begin
if Success
then FName := TGroupHelperRSP19902.Create(Groups[1], FJSFileInfo.Data).FixedValue
else Exit;
end;
end
else
Exit;
json.AddPair('name', FName);
end;
//
// isRTL -- from js; if more than one, just first one
//
procedure TMergeKeyboardInfo.AddIsRTL;
begin
if json.GetValue('isRTL') <> nil then Exit;
if (FJSFileInfo.Data <> '') then
begin
with TRegEx.Match(FJSFileInfo.Data, 'this\.KRTL=1') do
begin
if not Success then Exit;
end;
end
else
Exit;
json.AddPair('isRTL', TJsonTrue.Create);
end;
//
// authorName -- from kmp.inf
//
procedure TMergeKeyboardInfo.AddAuthor;
var
FName: string;
begin
if json.GetValue('authorName') <> nil then Exit;
if not Assigned(FKMPInfFile) then Exit;
FName := Trim(FKMPInfFile.Info.Desc[PackageInfo_Author]);
if FName = '' then Exit;
json.AddPair('authorName', FName);
end;
//
// authorEmail -- from kmp.inf
//
procedure TMergeKeyboardInfo.AddAuthorEmail;
var
FEmail: string;
begin
if json.GetValue('authorEmail') <> nil then Exit;
if not Assigned(FKMPInfFile) then Exit;
FEmail := FKMPInfFile.Info.URL[PackageInfo_Author];
if Copy(FEmail, 1, 7) <> 'mailto:' then Exit;
json.AddPair('authorEmail', Copy(FEMail,8,MaxInt));
end;
//
// lastModifiedDate -- build time? Is this
// good enough, and if not, how can we solve this?
//
procedure TMergeKeyboardInfo.AddLastModifiedDate;
var
FDateTime: string;
begin
if json.GetValue('lastModifiedDate') <> nil then Exit;
with TXSDateTime.Create do
try
AsDateTime := Now;
AsUTCDateTime := AsUTCDateTime; // Converts to UTC
FDateTime := NativeToXS;
finally
Free;
end;
json.AddPair('lastModifiedDate', FDateTime);
end;
//
// packageFilename -- from $keyboard_info_packageFilename
//
procedure TMergeKeyboardInfo.CheckOrAddPackageFilename;
var
FFilename: string;
v: TJSONValue;
begin
FFilename := ExtractFileName(FKmpFile);
v := json.GetValue('packageFilename');
if v <> nil then
begin
if FKmpFile = '' then
raise EInvalidKeyboardInfo.CreateFmt('packageFilename field is "%s" but that package is not present.', [v.Value])
else if v.Value <> FFilename then
raise EInvalidKeyboardInfo.CreateFmt('packageFilename field is "%s" but should be "%s"', [v.Value, FFilename]);
end
else
begin
if FKmpFile = '' then Exit;
json.AddPair('packageFilename', FFilename);
end;
// Check that the id of the keyboard matches the filename; used only for release/ keyboards
// in the keyboards repository
if FMergingValidateIds then
begin
if ChangeFileExt(FFilename, '') <> FBaseName then
raise EInvalidKeyboardInfo.CreateFmt('packageFilename field is "%s" but should be "%s.kmp"',
[FFilename, FBaseName]);
end;
end;
//
// jsFilename -- from $keyboard_info_jsFilename
//
procedure TMergeKeyboardInfo.CheckOrAddJsFilename;
var
FFilename: string;
v: TJSONValue;
begin
FFilename := ExtractFileName(FJsFile);
v := json.GetValue('jsFilename');
if v <> nil then
begin
if FJsFile = '' then
raise EInvalidKeyboardInfo.CreateFmt('jsFilename field is "%s" but that file is not present.', [v.Value])
else if v.Value <> FFilename then
raise EInvalidKeyboardInfo.CreateFmt('jsFilename field is "%s" but should be "%s"', [v.Value, FFilename]);
end
else
begin
if FJsFile = '' then Exit;
json.AddPair('jsFilename', FFilename);
end;
// Check that the id of the keyboard matches the filename; used only for release/ keyboards
// in the keyboards repository
if FMergingValidateIds then
begin
if ChangeFileExt(FFilename, '') <> FBaseName then
raise EInvalidKeyboardInfo.CreateFmt('jsFilename field is "%s" but should be "%s.js"',
[FFilename, FBaseName]);
end;
end;
//
// encodings -- from .kmx (existence of .js implies unicode)
//
procedure TMergeKeyboardInfo.CheckOrAddEncodings;
var
i: Integer;
encodingsc, encodings: TKIEncodings;
v: TJSONArray;
vc: TJSONArray;
begin
encodings := [];
// For each .kmx, get encodings and add to the result
for i := 0 to High(FPackageKMXFileInfos) do
encodings := encodings + FPackageKMXFileInfos[i].Info.Encodings;
if FJsFile <> '' then Include(encodings, keUnicode);
v := TJSONArray.Create;
if keANSI in encodings then
v.Add('ansi');
if keUnicode in encodings then
v.Add('unicode');
vc := json.GetValue('encodings') as TJSONArray;
if vc <> nil then
begin
encodingsc := [];
for i := 0 to vc.Count - 1 do
if vc.Items[i].Value = 'ansi' then Include(encodingsc, keANSI)
else if vc.Items[i].Value = 'unicode' then Include(encodingsc, keUnicode);
if encodingsc <> encodings then
raise EInvalidKeyboardInfo.CreateFmt('encodings field is "%s" but should be "%s"', [vc.ToJSON, v.ToJSON]);
end
else
json.AddPair('encodings', v);
end;
//
// packageFileSize, jsFileSize, all from the actual files
//
procedure TMergeKeyboardInfo.CheckOrAddFileSizes;
procedure DoFileSize(prefix: string);
var
vs, v: TJSONValue;
f: TSearchRec;
begin
v := json.GetValue(prefix+'Filename');
if v <> nil then
begin
if FindFirst(ExtractFilePath(FJsonFile)+v.Value, 0, f) <> 0 then
raise EInvalidKeyboardInfo.CreateFmt('Unable to locate file %s to check its size', [v.Value]);
FindClose(f);
vs := json.GetValue(prefix+'FileSize');
if vs = nil then
json.AddPair(prefix+'FileSize', TJSONNumber.Create(f.Size))
else
begin
if f.Size <> (vs as TJSONNumber).AsInt64 then
raise EInvalidKeyboardInfo.CreateFmt('File size for %s is recorded as %d but should be %d.',
[v.Value, (vs as TJSONNumber).AsInt64, f.Size]);
end;
end;
end;
begin
DoFileSize('js');
DoFileSize('package');
end;
//
// packageIncludes -- from kmp.inf?
//
procedure TMergeKeyboardInfo.AddPackageIncludes;
var
i: Integer;
id, ext, name: string;
v: TJSONArray;
j: Integer;
Found: Boolean;
begin
if json.GetValue('packageIncludes') <> nil then Exit;
if not Assigned(FKMPInfFile) then Exit;
//"welcome", "documentation", "fonts", "visualKeyboard"
v := TJSONArray.Create;
for i := 0 to FKMPInfFile.Files.Count - 1 do
begin
name := FKMPInffile.Files[i].FileName;
ext := ExtractFileExt(name);
id := '';
if SameText(name, 'welcome.htm') then id := 'welcome'
else if SameText(ext, '.kvk') then id := 'visualKeyboard'
else if SameText(ext, '.rtf') then id := 'documentation'
else if SameText(ext, '.html') then id := 'documentation'
else if SameText(ext, '.htm') then id := 'documentation'
else if SameText(ext, '.pdf') then id := 'documentation'
else if SameText(ext, '.ttf') then id := 'fonts'
else if SameText(ext, '.otf') then id := 'fonts'
else if SameText(ext, '.ttc') then id := 'fonts';
if (id <> '') then
begin
Found := False;
for j := 0 to v.Count - 1 do
if v.Items[j].Value = id then
begin
Found := True;
Break;
end;
if not Found then
v.Add(id);
end;
end;
json.AddPair('packageIncludes', v);
end;
//
// version -- from kmp.inf, js (if more than 1, then just first)
//
procedure TMergeKeyboardInfo.CheckOrAddVersion;
var
v: TJSONValue;
begin
if Assigned(FKMPInfFile) then
begin
FVersion := FKMPInfFile.Info.Desc[PackageInfo_Version];
end
else if (FJSFileInfo.Data <> '') then
begin
with TRegEx.Match(FJSFileInfo.Data, 'this\.KBVER=([''"])([^''"]+)(\1)') do
if Success then FVersion := TGroupHelperRSP19902.Create(Groups[2], FJSFileInfo.Data).FixedValue;
end;
if FVersion = '' then
FVersion := '1.0';
v := json.GetValue('version');
if v <> nil then
begin
if v.Value <> FVersion then
raise EInvalidKeyboardInfo.CreateFmt('version field is "%s" but should be "%s"', [v.Value, FVersion]);
end
else
json.AddPair('version', FVersion);
end;
//
// minKeymanVersion -- from kmp.inf, kmx, js
//
procedure TMergeKeyboardInfo.CheckOrAddMinKeymanVersion;
var
i: Integer;
MinVersion: Cardinal;
MinVersionString: string;
FJSMinVersionString: string;
v: TJSONValue;
s: string;
begin
MinVersion := $0500;
// For each .kmx, get minimum version and add to the result
for i := 0 to High(FPackageKMXFileInfos) do
if FPackageKMXFileInfos[i].Info.FileVersion > MinVersion then
MinVersion := FPackageKMXFileInfos[i].Info.FileVersion;
FJSMinVersionString := '';
// See also Keyman.System.KeyboardJSInfo.pas, CompilePackage.pas
with TRegEx.Match(FJSFileInfo.Data, 'this.KMINVER\s*=\s*([''"])(.*?)\1') do
begin
if Success then
begin
s := TGroupHelperRSP19902.Create(Groups[2], FJSFileInfo.Data).FixedValue;
if (FJSMinVersionString = '') or (CompareVersions(FJSMinVersionString, s) > 0) then
FJSMinVersionString := s;
end;
end;
MinVersionString := Format('%d.%d', [(MinVersion and VERSION_MASK_MAJOR) shr 8, (MinVersion and VERSION_MASK_MINOR)]);
if FJSMinVersionString <> '' then
if CompareVersions(MinVersionString, FJSMinVersionString) > 0 then
MinVersionString := FJSMinVersionString;
v := json.GetValue('minKeymanVersion');
if v <> nil then
begin
if v.Value <> MinVersionString then
raise EInvalidKeyboardInfo.CreateFmt('minKeymanVersion field is "%s" but should be "%s"', [v.Value, MinVersionString]);
end
else
json.AddPair('minKeymanVersion', MinVersionString);
end;
//
// Add helpLink, from commandline parameter
//
procedure TMergeKeyboardInfo.AddHelpLink;
var
v: TJSONValue;
begin
// Validate pre-existing helpLink
v := json.GetValue('helpLink');
if v <> nil then
begin
if v.Value.IndexOf('https://help.keyman.com/keyboard/') < 0 then
raise EInvalidKeyboardInfo.Create('helpLink should be on https://help.keyman.com/keyboard/');
end
else
begin
if FHelpLink = '' then
Exit;
json.AddPair('helpLink', FHelpLink);
end;
end;
//
// platformSupport -- deduce from whether kmp exists, js exists
//
procedure TMergeKeyboardInfo.AddPlatformSupport;
var
keyboardFile: TKeyboardInfoMap;
target: TKeymanTarget;
targets: TKeymanTargets;
p, v: TJSONObject;
numberKMXFiles, numberJSFiles : Integer;
procedure AddNewPair(PairName: string; value: string);
begin
try
if (v.GetValue(PairName) = nil) then
v.AddPair(PairName, value);
finally
end;
end;
function packageContainsKeyboardJs(const id: string): Boolean;
var
kf: TKeyboardInfoMap;
begin
for kf in FPackageJSFileInfos do
if SameText(TKeyboardUtils.KeyboardFileNameToID(kf.Filename), id) then
Exit(True);
Result := False;
end;
begin
try
// Validate pre-existing platformSupport
p := json.GetValue('platformSupport') as TJSONObject;
if p <> nil then
begin
numberKMXFiles := Length(FPackageKMXFileInfos);
if FJsFile <> '' then
begin
numberJSFiles := 1;
end
else
begin
numberJSFiles := Length(FPackageJSFileInfos);
end;
// Validate any target
if (p.GetValue('any') <> nil) and ((numberKMXFiles = 0) or (numberJSFiles = 0)) then
raise EInvalidKeyboardInfo.Create(
'"Any" target should have at least 1 .kmx and 1 .js keyboard file in the package');
// Validate desktop targets
if ((p.GetValue('windows') <> nil) or (p.GetValue('macos') <> nil) or
(p.GetValue('linux') <> nil) or (p.GetValue('desktop') <> nil)) and
(numberKMXFiles = 0) then
raise EInvalidKeyboardInfo.Create(
'Desktop targets should have at least 1 .kmx keyboard file in the package');
// Validate web/mobile targets. No kmx implies web/mobile targets
if ((numberKMXFiles = 0) or (p.GetValue('web') <> nil) or
(p.GetValue('iphone') <> nil) or (p.GetValue('ipad') <> nil) or
(p.GetValue('androidphone') <> nil) or (p.GetValue('androidtablet') <> nil) or
(p.GetValue('mobile') <> nil) or (p.GetValue('tablet') <> nil)) and
(numberJSFiles = 0) then
raise EInvalidKeyboardInfo.Create(
'Web/mobile targets should have at least 1 .js keyboard file in the package');
Exit;
end;
finally
end;
// Parse targets to populate platformSuppport
v := TJSONObject.Create;
for keyboardFile in FPackageKMXFileInfos do
begin
targets := StringToKeymanTargets(keyboardFile.Info.Targets);
if ktAny in targets then
begin
AddNewPair('windows', 'full');
AddNewPair('macos', 'full');
AddNewPair('linux', 'full');
if packageContainsKeyboardJs(TKeyboardUtils.KeyboardFileNameToId(keyboardFile.Filename)) then
begin
AddNewPair('android', 'full');
AddNewPair('ios', 'full');
end;
end
else
begin
// If targets empty, then include all desktop targets
if (targets = []) then
begin
AddNewPair('windows', 'full');
AddNewPair('macos', 'full');
AddNewPair('linux', 'full');
end;
for target in targets do
begin
if target = ktDesktop then
begin
AddNewPair('windows', 'full');
AddNewPair('macos', 'full');
AddNewPair('linux', 'full');
end;
if target = ktWindows then
AddNewPair('windows', 'full');
if target = ktMacosx then
AddNewPair('macos', 'full');
if target = ktLinux then
AddNewPair('linux', 'full');
if packageContainsKeyboardJs(TKeyboardUtils.KeyboardFileNameToId(keyboardFile.Filename)) then
begin
// FPackageKMXFileInfos can contain target information for web/mobile targets.
// This is a current limitation of FPackageJSFileInfos if there's no kmx files
if (target = ktMobile) then
begin
AddNewPair('android', 'full');
AddNewPair('ios', 'full');
end;
if (target = ktIphone) or (target = ktIpad) then
begin
AddNewPair('ios', 'full');
end;
if (target = ktAndroidphone) or (target = ktAndroidtablet) then
begin
AddNewPair('android', 'full');
end;
end;
end;
end;
end;
for keyboardFile in FPackageJSFileInfos do
begin
targets := StringToKeymanTargets(keyboardFile.Info.Targets);
for target in targets do
begin
if (target = ktMobile) then
begin
AddNewPair('android', 'full');
AddNewPair('ios', 'full');
end;
if (target = ktIphone) or (target = ktIpad) then
begin
AddNewPair('ios', 'full');
end;
if (target = ktAndroidphone) or (target = ktAndroidtablet) then
begin
AddNewPair('android', 'full');
end;
end;
end;
// Handle JS file not in kmp. Because it is isolated, we cannot detect
// whether it supports mobile vs desktop web because that is not included
// in the .js. So, for now we assume both.
//
// We no longer assume that the presence of a .js means support for
// native mobile apps. These apps now work on the basis of having a
// .kmp file available
if (FJsFile <> '') then
begin
AddNewPair('desktopWeb', 'full');
AddNewPair('mobileWeb', 'full');
end;
json.AddPair('platformSupport', v);
end;
//
// Add sourcePath field, from commandline parameter
//
procedure TMergeKeyboardInfo.AddSourcePath;
begin
if FSourcePath = '' then
Exit;
if not TRegEx.IsMatch(FSourcePath, '^(release|legacy|experimental)\/.+\/.+$') then
raise EInvalidKeyboardInfo.CreateFmt('The source path "%s" is an invalid format, '+
'expecting "(release|legacy|experimental)/n/name".', [FSourcePath]);
json.AddPair('sourcePath', FSourcePath);
end;
procedure TMergeKeyboardInfo.CheckPackageKeyboardFilenames;
var
numberKMXFiles, numberJSFiles : Integer;
begin
// Check that the id of the kmx/js files matches the base filename; used only for release/ keyboards
// in the keyboards repository. This implies there should be only 1 kmx and/or 1 js in release/ keyboard
// packages; this check would not be done in the packages folder.
if FMergingValidateIds and Assigned(FKMPInfFile) then
begin
numberKMXFiles := Length(FPackageKMXFileInfos);
numberJSFiles := Length(FPackageJSFileInfos);
if (numberKMXFiles = 0) and (numberJSFiles = 0) then
raise EInvalidKeyboardInfo.Create('There should be at least 1 .kmx or .js keyboard file in the package.');
if numberKMXFiles > 1 then
begin
raise EInvalidKeyboardInfo.Create('There should be at most 1 .kmx keyboard file in the packge.');
end
else if (numberKMXFiles = 1) and (ChangeFileExt(FPackageKMXFileInfos[0].Filename, '') <> FBaseName) then
begin
raise EInvalidKeyboardInfo.CreateFmt('The file "%s" file in the package has the wrong filename. It should be "%s.kmx"',
[FPackageKMXFileInfos[0].Filename, FBaseName]);
end;
if numberJSFiles > 1 then
begin
raise EInvalidKeyboardInfo.Create('There should be at most 1 .js keyboard file in the package.');
end
else if (numberJSFiles = 1) and (ChangeFileExt(FPackageJSFileInfos[0].Filename, '') <> FBaseName) then
begin
raise EInvalidKeyboardInfo.CreateFmt('The file "%s" file in the package has the wrong filename. It should be "%s.js"',
[FPackageJSFileInfos[0].Filename, FBaseName]);
end;
end;
end;
end.