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">&nbsp;</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);
+}