Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

120
gaia/apps/keyboard/build/build.js Executable file
View file

@ -0,0 +1,120 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
var KeyboardLayoutConfigurator =
require('./layout_configurator').KeyboardLayoutConfigurator;
var settingsConfig = require('./settings-config');
var KeyboardAppBuilder = function() {
};
// set options
KeyboardAppBuilder.prototype.setOptions = function(options) {
this.enabledLayouts = options.GAIA_KEYBOARD_LAYOUTS.split(',');
this.downloadableLayouts =
options.GAIA_KEYBOARD_DOWNLOADABLE_LAYOUTS.split(',');
this.userDictEnabled = options.GAIA_KEYBOARD_ENABLE_USER_DICT === '1';
this.distDir = utils.getFile(options.STAGE_APP_DIR);
this.appDir = utils.getFile(options.APP_DIR);
};
// Copy static files to build_stage.
KeyboardAppBuilder.prototype.copyStaticFiles = function() {
// Files to blindly copy to build_stage regardless of versions
var filenames = ['resources'];
var dirs = [];
dirs = dirs.concat('js', 'js/imes');
// Unfortunately we have to explicitly list many files here
// because the whitelist must not include optional layout
// specific files.
filenames = filenames.concat('index.html',
'locales',
'settings.html',
'style',
'js/settings',
'js/shared',
'js/keyboard',
'js/views');
dirs.forEach(function(dirName) {
var dir = utils.getFile.apply(utils,
[this.distDir.path].concat(dirName.split('/')));
utils.ensureFolderExists(dir);
}.bind(this));
filenames.forEach(function(filename) {
var filenameArr = filename.split('/');
var file = utils.getFile.apply(utils,
[this.appDir.path].concat(filenameArr));
var distSubDir = utils.getFile.apply(utils,
[this.distDir.path].concat(filenameArr.slice(0, filenameArr.length - 1)));
var targetFile = utils.getFile(distSubDir.path, file.leafName);
if (targetFile.exists()) {
targetFile.remove(true);
}
utils.copyFileTo(file.path, distSubDir.path, file.leafName);
}.bind(this));
};
KeyboardAppBuilder.prototype.copyLayouts = function() {
this.layoutConfigurator.copyFiles(this.distDir);
};
KeyboardAppBuilder.prototype.generateLayoutsJSON = function() {
// Write a dictionary list file into keyboard/js/settings/
// This file is noly used in keyboard settings page.
// (That's why we annotate latin.js too.)
var configFileDesc = utils.getFile(
this.distDir.path, 'js', 'settings', 'layouts.json');
var layouts = this.layoutConfigurator.getLayoutsJSON();
utils.writeContent(
configFileDesc, JSON.stringify(layouts, null, 2));
};
KeyboardAppBuilder.prototype.generateManifest = function() {
var manifest =
utils.getJSON(utils.getFile(this.appDir.path, 'manifest.webapp'));
this.layoutConfigurator.addInputsToManifest(manifest);
// Write content to build_stage
utils.writeContent(utils.getFile(this.distDir.path, 'manifest.webapp'),
JSON.stringify(manifest, null, 2));
};
KeyboardAppBuilder.prototype.modifySettings = function() {
var enabledFeatures = {
handwriting: settingsConfig.checkHandwriting(this.enabledLayouts),
userDict: this.userDictEnabled
};
settingsConfig.addSettings(
this.appDir.path, this.distDir.path, enabledFeatures);
};
KeyboardAppBuilder.prototype.execute = function(options) {
this.setOptions(options);
this.layoutConfigurator =
new KeyboardLayoutConfigurator(this.appDir);
this.layoutConfigurator
.loadLayouts(this.enabledLayouts, this.downloadableLayouts);
this.copyStaticFiles();
this.copyLayouts();
this.generateLayoutsJSON();
this.generateManifest();
this.modifySettings();
};
exports.execute = function(options) {
// We cannot export prototype functions out :(
// so we run execute() this way.
(new KeyboardAppBuilder()).execute(options);
};

View file

@ -0,0 +1,412 @@
'use strict';
/* jshint node: true */
let utils = require('utils');
var KeyboardLayoutDetail = function(id) {
this.id = id;
};
// A KeyboardLayoutDetail instance contains the following properties.
// They are intentionally named to avoid the word "name".
// Filenames are "id"s and the label for humans are "label"s.
// The actual nsIFile are "file" or "dir"s.
KeyboardLayoutDetail.prototype.id = undefined;
KeyboardLayoutDetail.prototype.label = undefined;
KeyboardLayoutDetail.prototype.labelL10nId = undefined;
KeyboardLayoutDetail.prototype.layoutFile = null;
KeyboardLayoutDetail.prototype.types = null;
KeyboardLayoutDetail.prototype.imEngineId = undefined;
KeyboardLayoutDetail.prototype.imEngineDir = null;
KeyboardLayoutDetail.prototype.noIncludeInExpandLayoutIdSet = undefined;
// How size is counted is imEngine dependent.
KeyboardLayoutDetail.prototype.fileSize = null;
KeyboardLayoutDetail.prototype.preloadDictRequired = undefined;
KeyboardLayoutDetail.prototype.dictPreloaded = false;
// The nsIFile instance points to the dictionary file in the tree.
KeyboardLayoutDetail.prototype.dictFile = null;
// The filename of the dictionary; use for construting CDN URL.
// Must be unique for a given resource.
KeyboardLayoutDetail.prototype.dictFilename = undefined;
// The path within the packaged app, relative to the imEngine dir root.
// Must be unique for a given resource.
// This is also used to construct database ID.
KeyboardLayoutDetail.prototype.dictFilePath = undefined;
// Read the .js file for the named keyboard layout and extract
// its information.
KeyboardLayoutDetail.prototype.load = function(appDir) {
var id = this.id;
var layoutFile = this.layoutFile =
utils.getFile(appDir.path, 'js', 'layouts' , id + '.js');
if (!layoutFile.exists()) {
throw new Error('KeyboardLayoutDetail: Keyboard layout ' + id + '.js' +
' specified by GAIA_KEYBOARD_LAYOUTS not found in ' + appDir.path);
}
// The keyboard layout files are JavaScript files that add properties
// to the Keybords object. They are not clean JSON, so we have to use
// use the scriptloader to load them. That also gives stacktraces for
// errors inside the keyboard file.
// They reference globals KeyEvent and KeyboardEvent, so we
// have to define those on the context object.
var win = { Keyboards: {},
KeyEvent: {},
KeyboardEvent: {} };
utils.scriptLoader.load(layoutFile.path, win, true);
if (!(id in win.Keyboards)) {
throw new Error('KeyboardLayoutDetail: Keyboard layout ' + id + '.js' +
' did not expose itself correctly.');
}
// These properties exists in all layouts.
this.label = win.Keyboards[id].menuLabel;
if (win.Keyboards[id].menuLabelL10nId) {
this.labelL10nId = 'layout-' + win.Keyboards[id].menuLabelL10nId;
}
this.types = win.Keyboards[id].types.sort();
this.imEngineId = win.Keyboards[id].imEngine;
this.noIncludeInExpandLayoutIdSet =
win.Keyboards[id].noIncludeInExpandLayoutIdSet;
if (this.imEngineId) {
this.imEngineDir =
utils.getFile(appDir.path, 'js', 'imes', this.imEngineId);
if (!this.imEngineDir.exists()) {
throw new Error('KeyboardLayoutDetail: Keyboard layout ' + id + '.js' +
' specified a non-exist ime.');
}
}
// Handle its dictionary declarations, and count the size, if any.
var lang;
switch (this.imEngineId) {
case 'latin':
this.preloadDictRequired = false;
lang = win.Keyboards[id].autoCorrectLanguage;
if (lang) {
this.dictFilename = lang + '.dict';
this.dictFilePath = 'dictionaries/' + this.dictFilename;
this.dictFile = utils.getFile(appDir.path, 'js', 'imes', 'latin',
'dictionaries', this.dictFilename);
if (!this.dictFile.exists()) {
throw new Error('KeyboardLayoutDetail: ' +
'Keyboard layout ' + id + '.js' +
' specified a non-exist dictionary for latin engine.');
}
// Consider the file size of the layout as the size of the dinctionary.
this.fileSize = this.dictFile.fileSize;
} else {
// To prevent this to be 0, set the size to it's layoutFile.
this.fileSize = this.layoutFile.fileSize;
}
break;
case 'handwriting':
case 'jskanji':
case 'jspinyin':
case 'jszhuyin':
// These IMs come with dictionary data in the tree. They must be
// excluded in a "noPreloadDictRequired" build to conserve build size.
this.preloadDictRequired = true;
// The file size of the layout would be the size of the engine dir.
this.fileSize = utils.ls(this.imEngineDir, true)
.map(function(file) {
return file.fileSize;
})
.reduceRight(function(prev, fileSize) {
return prev + fileSize;
}, 0);
break;
case 'jsavrophonetic':
case 'jstelex':
case 'india':
case 'emoji':
case 'jshangul':
case 'vietnamese':
case 'myanmar':
// These IMs only come with some logic in JavaScript with no preload
// dictionaries -- we can safely include them w/o taking too much size.
this.preloadDictRequired = false;
// The file size of the layout would be the size of the engine dir.
this.fileSize = utils.ls(this.imEngineDir, true)
.map(function(file) {
return file.fileSize;
})
.reduceRight(function(prev, fileSize) {
return prev + fileSize;
}, 0);
break;
default:
// It's possible the layout doesn't have an IMEngine, and it's acceptible.
// Other than that, throw so people are forced to place their IMEngine
// correctly here.
if (this.imEngineId) {
throw new Error('KeyboardLayoutDetail: Found an unknown imEngine: "' +
this.imEngineId + '".');
}
// To prevent this to be 0, set the size to it's layoutFile.
this.fileSize = this.layoutFile.fileSize;
// This is purposely left unset.
this.preloadDictRequired = undefined;
}
};
var KeyboardLayoutConfigurator = function(appDir) {
this.appDir = appDir;
};
KeyboardLayoutConfigurator.prototype.loadLayouts =
function(enabledLayoutIds, downloadableLayoutIds) {
var enabledLayoutIdSet = this._expandLayoutIdSet(enabledLayoutIds);
var downloadableLayoutIdSet = this._expandLayoutIdSet(downloadableLayoutIds);
utils.log('keyboard-load-layouts', 'The enabled layouts are set to: ' +
[...enabledLayoutIdSet].join(', '));
utils.log('keyboard-load-layouts', 'The downloadable layouts are set to: ' +
[...downloadableLayoutIdSet].join(', '));
if (enabledLayoutIdSet.size === 0) {
throw new Error('KeyboardLayoutConfigurator: No layout specified?');
}
this.layoutDetails = [];
// We need to track the dict separately here --
// because we want to set all layouts using the same dictionary to be
// considered preloaded.
var imEngineDictIdSet = new Set();
enabledLayoutIdSet.forEach(function(layoutId) {
var detail = new KeyboardLayoutDetail(layoutId);
detail.load(this.appDir);
if (detail.dictFile) {
imEngineDictIdSet.add(detail.imEngineId + '/' + detail.dictFilePath);
}
this.layoutDetails.push(detail);
}, this);
downloadableLayoutIdSet.forEach(function(layoutId) {
if (enabledLayoutIdSet.has(layoutId)) {
return;
}
var detail = new KeyboardLayoutDetail(layoutId);
detail.load(this.appDir);
this.layoutDetails.push(detail);
}, this);
this.layoutDetails.sort(function(a, b) {
if (a.id > b.id) {
return 1;
} else if (a.id < b.id) {
return -1;
}
throw new Error('KeyboardLayoutConfigurator: ' +
'Found two identical layout id: ' + a.id);
});
this.layoutDetails.forEach(function(detail) {
if (imEngineDictIdSet.has(detail.imEngineId + '/' + detail.dictFilePath)) {
detail.dictPreloaded = true;
}
});
};
KeyboardLayoutConfigurator.prototype._expandLayoutIdSet = function(layoutIds) {
var layoutIdSet = new Set(layoutIds);
if (layoutIdSet.has('*')) {
// Overwrite the set with all the layouts in the tree.
layoutIdSet = new Set(this._listAllLayouts());
} else if (layoutIdSet.has('noPreloadDictRequired')) {
// Get all the layouts that does not require preload dictionary.
this._listAllLayouts(function isPreloadDictNotRequired(detail) {
return !detail.preloadDictRequired;
}).forEach(function(layoutId) {
layoutIdSet.add(layoutId);
}, this);
layoutIdSet.delete('noPreloadDictRequired');
}
layoutIdSet.delete('');
return layoutIdSet;
};
KeyboardLayoutConfigurator.prototype._listAllLayouts = function(testFunc) {
var layoutDir = utils.getFile(this.appDir.path, 'js', 'layouts');
return utils.ls(layoutDir, false).map(function(file) {
if (!file.leafName.endsWith('.js')) {
return '';
}
return file.leafName.substr(0, file.leafName.length - 3);
}).filter(function(layoutId) {
if (!layoutId) {
return false;
}
var detail = new KeyboardLayoutDetail(layoutId);
detail.load(this.appDir);
if (detail.noIncludeInExpandLayoutIdSet) {
return false;
}
if (typeof testFunc === 'function' && !testFunc(detail)) {
return false;
}
return true;
}, this);
};
KeyboardLayoutConfigurator.prototype.copyFiles = function(distDir) {
// Copy the layout file to where it needs to go
let layoutDest = utils.getFile(distDir.path, 'js', 'layouts');
utils.ensureFolderExists(layoutDest);
this.layoutDetails.forEach(function(layoutDetail) {
var file = layoutDetail.layoutFile;
utils.copyFileTo(file.path, layoutDest.path, file.leafName);
}, this);
// Copy the entire imEngineDir or selected files if applicable.
this._copyIMEngineDirs(distDir);
// Copy the dictFile if applicable.
this._copyDicts(distDir);
};
KeyboardLayoutConfigurator.prototype._copyIMEngineDirs = function(distDir) {
let imeDest = utils.getFile(distDir.path, 'js', 'imes');
this.layoutDetails.forEach(function(layoutDetail) {
if (!layoutDetail.imEngineId) {
return;
}
var imEngineDirDest = utils.getFile(imeDest.path, layoutDetail.imEngineId);
// Don't try to copy the dir again.
if (imEngineDirDest.exists()) {
return;
}
// Copy the entire imEngineDir or selected files if applicable.
switch (layoutDetail.imEngineId) {
case 'latin':
utils.ensureFolderExists(imEngineDirDest);
utils.ls(layoutDetail.imEngineDir, false).forEach(function(file) {
if (file.leafName === 'dictionaries') {
return;
}
utils.copyFileTo(file.path, imEngineDirDest.path, file.leafName);
});
break;
default:
let file = layoutDetail.imEngineDir;
utils.copyFileTo(file.path, imeDest.path, file.leafName);
break;
}
}, this);
};
KeyboardLayoutConfigurator.prototype._copyDicts = function(distDir) {
let dictDests = {
latin: utils.getFile(distDir.path, 'js', 'imes', 'latin', 'dictionaries')
};
this.layoutDetails.forEach(function(layoutDetail) {
// Copy the dictFile if applicable.
switch (layoutDetail.imEngineId) {
case 'latin':
if (!layoutDetail.dictPreloaded) {
return;
}
utils.ensureFolderExists(dictDests.latin);
let file = layoutDetail.dictFile;
utils.copyFileTo(file.path, dictDests.latin.path, file.leafName);
break;
}
}, this);
};
KeyboardLayoutConfigurator.prototype.getLayoutsJSON = function() {
var layouts = [];
this.layoutDetails.forEach(function(layoutDetail) {
var layout = {
id: layoutDetail.id,
name: layoutDetail.label,
imEngineId: layoutDetail.imEngineId,
types: layoutDetail.types,
dictFileSize: layoutDetail.fileSize
};
if (layoutDetail.labelL10nId) {
layout.nameL10nId = layoutDetail.labelL10nId;
}
if (layoutDetail.dictFile) {
layout.preloaded = layoutDetail.dictPreloaded;
layout.dictFilename = layoutDetail.dictFilename;
layout.dictFilePath = layoutDetail.dictFilePath;
} else {
layout.preloaded = true;
}
layouts.push(layout);
});
return layouts;
};
KeyboardLayoutConfigurator.prototype.addInputsToManifest = function(manifest) {
this.layoutDetails.forEach(function(layoutDetail) {
// Layout does not get declared statically
// if its dictionary is not preloaded.
if (layoutDetail.dictFile && !layoutDetail.dictPreloaded) {
return;
}
manifest.inputs[layoutDetail.id] = {
launch_path: '/index.html#' + layoutDetail.id,
name: layoutDetail.label,
description: layoutDetail.label,
types: layoutDetail.types
};
});
};
exports.KeyboardLayoutConfigurator = KeyboardLayoutConfigurator;

View file

@ -0,0 +1,71 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
// All layouts for handwriting should be contained.
// When adding a new handwriting IME, add its layout here.
const allHandwritingLayouts = ['zh-Hans-Handwriting'];
// Check whether pre-installed layouts contain layout for handwriting.
function checkHandwriting(allLayouts) {
for (var i = 0; i < allHandwritingLayouts.length; i++) {
for (var j = 0; j < allLayouts.length; j++) {
if (allHandwritingLayouts[i] == allLayouts[j]) {
return true;
}
}
}
return false;
}
function addSettings(appDirPath, distDirPath, enabledFeatures) {
var settings = utils.getFile(appDirPath, 'settings.html');
var content = utils.getFileContent(settings);
var domDoc = utils.getDocument(content);
// Add scripts and modify html for handwriting and user dictionary settings.
// If parse handwriting-settings.html to dom element, and
// add it to container with container.appendChild(), we met
// an error:
// "Exception: HierarchyRequestError: Node cannot be inserted at
// the specified point in the hierarchy"
// Is it a bug?
var baseViewScriptElemInHead =
domDoc.head.querySelector('script[src="js/settings/base_view.js"]');
var insertScript = function (src) {
var script = domDoc.createElement('script');
script.defer = 'defer';
script.src = src;
domDoc.head.insertBefore(script, baseViewScriptElemInHead.nextSibling);
};
if (enabledFeatures.handwriting) {
insertScript('js/settings/handwriting_settings_view.js');
var hwSettings = utils.getFile(appDirPath, 'handwriting-settings.html');
var hwContent = utils.getFileContent(hwSettings);
domDoc.getElementById('general-container').innerHTML += hwContent;
}
if (enabledFeatures.userDict) {
insertScript('js/settings/user_dictionary_edit_dialog.js');
insertScript('js/settings/user_dictionary_list_panel.js');
insertScript('js/settings/word_list_converter.js');
insertScript('js/settings/user_dictionary.js');
var udSettings = utils.getFile(appDirPath, 'user-dictionary-settings.html');
var udContent = utils.getFileContent(udSettings);
domDoc.querySelector('#general-container #general-settings ul').innerHTML +=
udContent;
}
var sDoc = utils.serializeDocument(domDoc);
utils.writeContent(utils.getFile(distDirPath, 'settings.html'), sDoc);
}
exports.checkHandwriting = checkHandwriting;
exports.addSettings = addSettings;