Copy upstream release-44-1
Bug: 327164201
Test: n/a
Change-Id: Ie7a53c59a8d5619027dd14a0047a51b8da87051b
diff --git a/docs/charts/keyboard/.gitignore b/docs/charts/keyboard/.gitignore
new file mode 100644
index 0000000..fd7d0e5
--- /dev/null
+++ b/docs/charts/keyboard/.gitignore
@@ -0,0 +1,2 @@
+/node_modules
+/static/data
diff --git a/docs/charts/keyboard/build.mjs b/docs/charts/keyboard/build.mjs
new file mode 100644
index 0000000..73c4f29
--- /dev/null
+++ b/docs/charts/keyboard/build.mjs
@@ -0,0 +1,102 @@
+// do the XML parsing and fs access in a build step
+
+import { promises as fs } from "node:fs";
+import * as path from "node:path";
+import { XMLParser } from "fast-xml-parser";
+
+const KEYBOARD_PATH = "../../../keyboards/3.0";
+const IMPORT_PATH = "../../../keyboards/import";
+const DATA_PATH = "static/data";
+
+async function xmlList(basepath) {
+ const dir = await fs.opendir(basepath);
+ const xmls = [];
+ for await (const ent of dir) {
+ if (!ent.isFile() || !/\.xml$/.test(ent.name)) {
+ continue;
+ }
+ xmls.push(ent.name);
+ }
+ return xmls;
+}
+
+/**
+ * List of elements that are always arrays
+ */
+const alwaysArray = [
+ "keyboard3.transforms",
+ "keyboard3.transforms.transformGroup",
+ "keyboard3.transforms.transformGroup.transform",
+];
+
+/**
+ * Loading helper for isArray
+ * @param name
+ * @param jpath
+ * @param isLeafNode
+ * @param isAttribute
+ * @returns
+ */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const isArray = (name, jpath, isLeafNode, isAttribute) => {
+ if (alwaysArray.indexOf(jpath) !== -1) return true;
+ return false;
+};
+
+/**
+ * Do the XML Transform given raw XML source
+ * @param xml XML source for transforms. entire keyboard file.
+ * @param source source text
+ * @returns target text
+ */
+export function parseXml(xml) {
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ isArray,
+ });
+ const j = parser.parse(xml);
+ return j;
+}
+
+async function readFile(path) {
+ return fs.readFile(path, "utf-8");
+}
+
+async function main() {
+ const xmls = await xmlList(KEYBOARD_PATH);
+ const keyboards = await packXmls(KEYBOARD_PATH, xmls);
+ const importFiles = await xmlList(IMPORT_PATH);
+ const imports = await packXmls(IMPORT_PATH, importFiles);
+
+ const allData = {
+ keyboards,
+ imports,
+ };
+
+ const outPath = path.join(DATA_PATH, "keyboard-data.json");
+ const outJsPath = path.join(DATA_PATH, "keyboard-data.js");
+ await fs.mkdir(DATA_PATH, { recursive: true });
+ const json = JSON.stringify(allData, null, " "); // indent, in case we need to read it
+ await fs.writeFile(outPath, json, "utf-8");
+ await fs.writeFile(outJsPath, `const _KeyboardData = \n` + json);
+ return { xmls, importFiles, outPath, outJsPath };
+}
+
+main().then(
+ (done) => console.dir({ done }),
+ (err) => {
+ console.error(err);
+ process.exitCode = 1;
+ }
+);
+
+async function packXmls(basepath, xmls) {
+ const allData = {};
+ for (const fn of xmls) {
+ const fp = path.join(basepath, fn);
+ const data = await readFile(fp);
+ const parsed = parseXml(data);
+ allData[fn] = parsed;
+ }
+ return allData;
+}
diff --git a/docs/charts/keyboard/index.html b/docs/charts/keyboard/index.html
new file mode 100644
index 0000000..bcb7c7f
--- /dev/null
+++ b/docs/charts/keyboard/index.html
@@ -0,0 +1,2 @@
+<h1>You're almost there</h1>
+<a href="./static/index.html">Click here for the keyboard charts</a>
diff --git a/docs/charts/keyboard/package-lock.json b/docs/charts/keyboard/package-lock.json
new file mode 100644
index 0000000..b889a24
--- /dev/null
+++ b/docs/charts/keyboard/package-lock.json
@@ -0,0 +1,42 @@
+{
+ "name": "@unicode-org/keyboard-charts",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@unicode-org/keyboard-charts",
+ "version": "1.0.0",
+ "license": "Unicode-DFS-2016",
+ "dependencies": {
+ "fast-xml-parser": "^4.2.5"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz",
+ "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==",
+ "funding": [
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/naturalintelligence"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "dependencies": {
+ "strnum": "^1.0.5"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/strnum": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
+ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
+ }
+ }
+}
diff --git a/docs/charts/keyboard/package.json b/docs/charts/keyboard/package.json
new file mode 100644
index 0000000..5dccb3b
--- /dev/null
+++ b/docs/charts/keyboard/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@unicode-org/keyboard-charts",
+ "version": "1.0.0",
+ "description": "Keyboard Charts app",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "serve": "npx serve static",
+ "build": "node build.mjs"
+ },
+ "keywords": [],
+ "author": "Steven R. Loomis <[email protected]>",
+ "license": "Unicode-DFS-2016",
+ "bugs": {
+ "url": "https://github.com/unicode-org/cldr/issues"
+ },
+ "homepage": "https://github.com/unicode-org/cldr#readme",
+ "private": true,
+ "dependencies": {
+ "fast-xml-parser": "^4.2.5"
+ }
+}
diff --git a/docs/charts/keyboard/static/index.html b/docs/charts/keyboard/static/index.html
new file mode 100644
index 0000000..afb92e5
--- /dev/null
+++ b/docs/charts/keyboard/static/index.html
@@ -0,0 +1,138 @@
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>CLDR | Proposed Keyboard 3.0 Chart</title>
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+ <script src="./keyboard-chart.js"></script>
+ <script src="./data/keyboard-data.js"></script>
+ <link href="./keyboard-chart.css" rel="stylesheet" />
+ <link rel="stylesheet" type="text/css" href="https://www.unicode.org/webscripts/standard_styles.css">
+ </head>
+ <body>
+ <div id="app">
+ <!-- standard unicode header-->
+ <table width="100%" cellpadding="0" cellspacing="0" border="0">
+ <!-- BEGIN HEADER BAR -->
+ <tr>
+ <td colspan="2">
+ <table width="100%" border="0" cellpadding="0" cellspacing="0">
+ <tr>
+
+ <td class="icon" style="width:38px; height:35px">
+ <a href="https://www.unicode.org/">
+ <img border="0" src="https://www.unicode.org/webscripts/logo60s2.gif" align="middle"
+ alt="[Unicode]" width="34" height="33"></a>
+ </td>
+
+ <td class="icon" style="vertical-align:middle">
+ <a class="bar"> </a>
+ <a class="bar" href="https://cldr.unicode.org/index/keyboard-workgroup"><font size="3">CLDR | Keyboard-SC | Charts</font></a>
+ </td>
+
+ <td class="bar">
+ <a href="https://www.unicode.org/main.html" class="bar">Tech Site</a>
+ | <a href="https://www.unicode.org/sitemap/" class="bar">Site Map</a> |
+ <a href="https://www.unicode.org/search" class="bar">Search </a>
+ </td>
+
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" class="gray"> </td>
+ </tr>
+ <!-- END HEADER BAR -->
+ <!-- BEGIN CONTENTS -->
+ <tr>
+ <td class="contents" valign="top">
+
+
+ <i
+ >Note: This is a very preliminary chart. For feedback on this chart or
+ contents, please comment on:
+ <a href="https://unicode-org.atlassian.net/browse/CLDR-17205"
+ >https://unicode-org.atlassian.net/browse/CLDR-17205</a
+ ></i
+ >
+ <!-- {{ message }} -->
+ <hr />
+ <div>
+ <span v-for="file of files" :key="file">
+ <a :href="'#'+file">{{file}}</a> |
+ </span>
+ </div>
+ <hr />
+ <ol>
+ <li v-for="file of files" :key="file">
+ <h2 :id="file"><code>{{file}}</code></h2>
+ <ul>
+ <li v-for="layers of getLayers(file)">
+ <h3 v-if="layers.formId">Form: {{ layers.formId }}</h3>
+ <h3 v-if="layers.id">ID: {{ layers.id }}</h3>
+ <h4 v-if="layers.minDeviceWidth">
+ minDeviceWidth: {{ layers.minDeviceWidth }}mm
+ </h4>
+ <ul>
+ <li v-for="layer of layers.layer">
+ <h4 v-if="layer.modifiers">Modifier: {{ layer.modifiers }}</h4>
+ <h4 v-if="layer.id">{{ layer.id }}</h4>
+ <div class="rows">
+ <div class="row" v-for="row of layer.row">
+ <span
+ :title="key.id"
+ :class="getKeyClass(key)"
+ v-for="key of row.keys"
+ >
+ {{key.output}}
+ <b title="Switch" v-if="key.layerId">☞ {{key.layerId}}</b>
+ </span>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <hr />
+ </li>
+ </ol>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <script>
+ const { createApp } = Vue;
+
+ createApp({
+ data() {
+ return {};
+ },
+ computed: {
+ files() {
+ return getIds();
+ },
+ },
+ methods: {
+ getLayers(id) {
+ return getKeyboardLayers(id);
+ },
+ getKeys(id) {
+ return getKeyboardKeys(id);
+ },
+ getKeyClass(key) {
+ if (key.gap) {
+ return "gap-key key";
+ } else if (key.to) {
+ return "to-key key";
+ } else if (key.switch) {
+ return "switch-key key";
+ } else {
+ return "key";
+ }
+ },
+ },
+ }).mount("#app");
+ </script>
+
+ </body>
+</html>
diff --git a/docs/charts/keyboard/static/keyboard-chart.css b/docs/charts/keyboard/static/keyboard-chart.css
new file mode 100644
index 0000000..bcdc503
--- /dev/null
+++ b/docs/charts/keyboard/static/keyboard-chart.css
@@ -0,0 +1,34 @@
+
+.rows {
+ display: table;
+ margin-top: 2em;
+}
+
+.row {
+ display: table-row;
+}
+
+.key {
+ border: 1px solid gray;
+ padding: 0.25em;
+ margin-right: 0.5em;
+ height: 2em;
+ display: table-cell;
+ font-size: small;
+}
+
+.gap-key {
+ background-color: gray;
+}
+
+.to-key {
+ background-color: beige;
+}
+
+.switch-key {
+ background-color: lime;
+}
+
+.contents {
+ padding: 1em;
+}
diff --git a/docs/charts/keyboard/static/keyboard-chart.js b/docs/charts/keyboard/static/keyboard-chart.js
new file mode 100644
index 0000000..1f8be16
--- /dev/null
+++ b/docs/charts/keyboard/static/keyboard-chart.js
@@ -0,0 +1,114 @@
+// helper functions for keyboard
+
+/**
+ * Unescape an escaped string
+ * @param str input string such as '\u017c'
+ * @returns
+ */
+function unescapeStr(str) {
+ str = str.replace(/\\u{([0-9a-fA-F]+)}/g, (a, b) =>
+ String.fromCodePoint(Number.parseInt(b, 16))
+ );
+ return str;
+}
+
+function getKeyboardLayers(id) {
+ let q = _KeyboardData.keyboards[id].keyboard3.layers;
+ if (!Array.isArray(q)) {
+ q = [q];
+ }
+ mogrifyAttrs(q);
+ const keybag = getKeyboardKeys(id);
+ mogrifyLayerList(q, keybag);
+ return q;
+}
+
+function mogrifyLayerList(layerList, keybag) {
+ layerList.forEach(({ layer }) => {
+ layer.forEach(({ row }) => {
+ row.forEach((r) => {
+ r.keys = r.keys.split(" ").map((id) =>
+ Object.assign(
+ {
+ id,
+ },
+ keybag[id]
+ )
+ );
+ });
+ });
+ });
+}
+
+function getImportFile(id) {
+ return _KeyboardData.imports[id["@_path"].split("/")[1]];
+}
+
+function getImportKeys(id) {
+ const imp = getImportFile(id);
+ if (!imp) {
+ throw Error(`Could not load import ${JSON.stringify(id)}`);
+ }
+ return imp.keys.key;
+}
+
+function mogrifyKeys(keys) {
+ // drop @'
+ if (!keys) {
+ return [];
+ }
+ return keys.reduce((p, v) => {
+ // TODO: any other swapping
+ mogrifyAttrs(v);
+ const { id, output } = v;
+ if (output) {
+ v.output = unescapeStr(output);
+ }
+ p[id] = v;
+ return p;
+ }, {});
+}
+
+function mogrifyAttrs(o) {
+ for (const k of Object.keys(o)) {
+ const ok = o[k];
+ if (/^@_/.test(k)) {
+ const attr = k.substring(2);
+ o[attr] = ok;
+ delete o[k];
+ } else if (Array.isArray(ok)) {
+ ok.forEach((e) => mogrifyAttrs(e));
+ } else if (typeof ok === "object") {
+ mogrifyAttrs(ok);
+ }
+ }
+ return o;
+}
+
+function getKeyboardKeys(id) {
+ const keys = _KeyboardData.keyboards[id].keyboard3.keys.key || [];
+ if (!keys) {
+ throw Error(`No keys for ${id}`);
+ }
+ let imports = [
+ {
+ // add implied import
+ "@_base": "cldr",
+ "@_path": "techpreview/keys-Latn-implied.xml",
+ },
+ ...(_KeyboardData.keyboards[id].keyboard3.keys.import || []),
+ ];
+
+ const importedKeys = [];
+ for (const fn of imports) {
+ for (const k of getImportKeys(fn)) {
+ importedKeys.push(k);
+ }
+ }
+
+ return mogrifyKeys([...importedKeys, ...keys]);
+}
+
+function getIds() {
+ return Object.keys(_KeyboardData.keyboards);
+}