diff --git a/apps/palette-generator/palette-generator.ts b/apps/palette-generator/palette-generator.ts index e76f210..fe02404 100644 --- a/apps/palette-generator/palette-generator.ts +++ b/apps/palette-generator/palette-generator.ts @@ -13,7 +13,7 @@ import { render } from "preact"; import { html } from "htm/preact"; import { Palette } from "../../src/client/Palette"; import { BlissSymbol } from "../../src/client/BlissSymbol"; -import { processPaletteLabels, fetchBlissGlossJson } from "./paletteJsonGenerator"; +import { processPaletteLabels, loadBciAvSymbolsDict } from "./paletteJsonGenerator"; import "../../src/client/index.scss"; import { initAdaptivePaletteGlobals, adaptivePaletteGlobals, cellTypeRegistry @@ -22,7 +22,7 @@ import { // Initialize any globals used elsewhere in the code. await initAdaptivePaletteGlobals("paletteDisplay"); -await fetchBlissGlossJson(); +await loadBciAvSymbolsDict(); let currentPaletteName = ""; const MAX_MATCHES_OUTPUT = 7; @@ -32,7 +32,7 @@ const MAX_MATCHES_OUTPUT = 7; * set up a handler to adjust the palette for changes in the cell type. */ function initCellTypesSelect () { - const cellTypesSelect = document.getElementById("cellTypes"); + const cellTypesSelect = document.getElementById("cellTypes") as HTMLSelectElement; Object.keys(cellTypeRegistry).forEach ((cellType) => { // The "cell" type `ContentBmwEncoding` is for an array of symbols within // a content area, not for cells within a palette. Avoid for now. @@ -58,7 +58,7 @@ function renderExamples() { isPresentation=false labelledBy="slashExampleLabel" /> - `, document.getElementById("slashExample")); + `, document.getElementById("slashExample") as HTMLElement); // Semi-colon example render(html` @@ -68,7 +68,7 @@ function renderExamples() { isPresentation=false labelledBy="semicolonExampleLabel" /> - `, document.getElementById("semicolonExample")); + `, document.getElementById("semicolonExample") as HTMLElement); // Kerning example (relative kerning) render(html` @@ -78,7 +78,7 @@ function renderExamples() { isPresentation=false labelledBy="kerningExampleLabel" /> - `, document.getElementById("kerningExample")); + `, document.getElementById("kerningExample") as HTMLElement); // X example render(html` @@ -88,7 +88,7 @@ function renderExamples() { isPresentation=false labelledBy="XExampleLabel" /> - `, document.getElementById("XExample")); + `, document.getElementById("XExample") as HTMLElement); } /** @@ -123,7 +123,7 @@ function handleGenerateDisplayButton () { if (glossesArray.length === 0) { paletteDisplay.innerText = "

Missing glosses ?

"; } - const lookupResults = processPaletteLabels( + const lookupResults = await processPaletteLabels( glossesArray, paletteName.value, parseInt(rowStart.value), diff --git a/apps/palette-generator/paletteJsonGenerator.ts b/apps/palette-generator/paletteJsonGenerator.ts index 0f4b879..fb641e2 100644 --- a/apps/palette-generator/paletteJsonGenerator.ts +++ b/apps/palette-generator/paletteJsonGenerator.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 Inclusive Design Research Centre, OCAD University + * Copyright 2024-2026 Inclusive Design Research Centre, OCAD University * All rights reserved. * * Licensed under the New BSD license. You may not use this file except in @@ -9,25 +9,55 @@ * https://github.com/inclusive-design/adaptive-palette/blob/main/LICENSE */ import { v4 as uuidv4 } from "uuid"; +import { bciAvSymbolsDictUrl, loadDataFromUrl } from "../../src/client/GlobalData"; import { makeBciAvIdType, BCIAV_PATTERN_KEY, BLISSARY_PATTERN_KEY, decomposeBciAvId } from "../../src/client/SvgUtils"; +import { BciAvSymbolsDict } from "../../src/client"; const BLANK_CELL = "BLANK"; const SVG_PREFIX = "SVG:"; const SVG_SUFFIX = ":SVG"; const LABEL_MARKER = "LABEL:"; -let bliss_gloss; -export async function fetchBlissGlossJson () { - // Read and parse the Bliss gloss JSON file - try { - const fetchResponse = await fetch("/data/bliss_symbol_explanations.json"); - bliss_gloss = await fetchResponse.json(); - } catch (error) { - console.error(`Error fetching 'bliss_symbol_explanations.json': ${error.message}`); - } - return bliss_gloss; +let bciAvSymbolsDict: BciAvSymbolsDict = []; + +// Load the BCI AV symbols dictionary from the URL +export async function loadBciAvSymbolsDict() { + bciAvSymbolsDict = await loadDataFromUrl(bciAvSymbolsDictUrl); +} + +// Define types used in the functions below +interface BlissGloss { + id: string | number; + description: string; + composition?: (string | number)[]; // Optional +} + +interface BciAvMatch { + bciAvId: number; + label: string; + composition?: (string | number)[]; // Optional + fullComposition?: (string | number)[]; // Optional +} + +type MatchByInfo = Record; + +interface Cell { + type: string; + options: { + label: string; + bciAvId?: number | (string | number)[]; + rowStart: number; + rowSpan: number; + columnStart: number; + columnSpan: number; + }; +} + +interface Palette { + name: string; + cells: Record; } /** @@ -36,7 +66,7 @@ export async function fetchBlissGlossJson () { * @param {string} - the string to test. * @returns {boolean} */ -function isSvgBuilderString (theString) { +function isSvgBuilderString (theString: string) { return theString.startsWith(SVG_PREFIX) && theString.endsWith(SVG_SUFFIX); } @@ -51,7 +81,7 @@ function isSvgBuilderString (theString) { * @return {Array} - An array of the specifiers required by the SvgUtils. * @throws {Error} - If the encoding is not well formed. */ -function convertSvgBuilderString (theString) { +function convertSvgBuilderString (theString: string) { let result; // Three forms, one with commas and one without: // - commas: @@ -96,43 +126,62 @@ function convertSvgBuilderString (theString) { * { id: {number}, description: {string}, ... } * @throws {Error} If no BCI AV ID is found for the label. */ -function findBciAvId(label, blissGlosses) { - const matches = []; - // Search for the label in the Bliss gloss + +/** + * Helper function: Normalizes polymorphic return from decomposeBciAvId + * (value | array | undefined) into (array | undefined) + */ +const normalizeToOptionalArray = (val: unknown): (string | number)[] | undefined => { + if (val === undefined || val === null) return undefined; + if (Array.isArray(val)) { + return val as (string | number)[]; + } + return [val as string | number]; +}; + +/** + * Helper function: Compares two arrays for equality safely + */ +const areArraysEqual = (a?: (string | number)[], b?: (string | number)[]): boolean => { + if (!a || !b) return a === b; // If both undefined, they are equal + if (a.length !== b.length) return false; + return a.every((val, index) => String(val) === String(b[index])); +}; + +function findBciAvId(label: string, blissGlosses: BlissGloss[]): BciAvMatch[] { + const matches: BciAvMatch[] = []; console.log(`For label ${label}:`); + for (const gloss of blissGlosses) { - // Try an exact match or a word match - const wordMatch = new RegExp("\\b" + `${label}` + "\\b"); - if ((label === gloss.description) || wordMatch.test(gloss.description)) { - // Get the composition of all the parts of the symbol's compostion or - // its ID. But if the `fullComposition` is the same as the original - // ignore it. - const glossId = parseInt(gloss.id); - let fullComposition = undefined; - let equalCompositions = false; - if (gloss.composition) { - fullComposition = decomposeBciAvId(gloss.composition); - equalCompositions = fullComposition.join("") === gloss.composition.join(""); - } - else { - fullComposition = decomposeBciAvId(glossId); - if (fullComposition && fullComposition.length === 1) { - equalCompositions = fullComposition[0] === glossId; - } - } + const wordMatch = new RegExp("\\b" + label + "\\b"); + + if (label === gloss.description || wordMatch.test(gloss.description)) { + const glossId = typeof gloss.id === "number" ? gloss.id : parseInt(gloss.id); + + // Determine the source for decomposition and what "original" state to compare against + const source = gloss.composition ?? glossId; + const originalAsArray = gloss.composition ?? [glossId]; + + const fullComp = normalizeToOptionalArray(decomposeBciAvId(source)); + + // Check if the decomposition resulted in exactly what we started with + const equalCompositions = areArraysEqual(fullComp, originalAsArray); + matches.push({ bciAvId: glossId, label: gloss.description, - composition: gloss.composition, - fullComposition: ( equalCompositions ? undefined : fullComposition ) + composition: gloss.composition, // Might be undefined + fullComposition: equalCompositions ? undefined : fullComp }); - console.log(`\tFound match: ${gloss.description}, bci-av-id: ${gloss.id}`); + + console.log(`\tFound match: ${gloss.description}, bci-av-id: ${glossId}`); } } - // If no BCI AV ID is found, throw an error + if (matches.length === 0) { throw new Error(`BciAvId not found for label: ${label}`); } + return matches; } @@ -145,7 +194,7 @@ function findBciAvId(label, blissGlosses) { * @returns {Object} The object that matches the given BCI AV ID * @throws {Error} If the given BCI AV ID is invalid (not in the gloss) */ -function findByBciAvId (bciAvId: string, blissGlosses: array) { +function findByBciAvId (bciAvId: string, blissGlosses: BlissGloss[]) { const theEntry = blissGlosses.find((entry) => (entry.id === bciAvId)); if (theEntry === undefined) { throw new Error(`BciAvId not found for BCI AV ID: ${bciAvId}`); @@ -197,17 +246,18 @@ function findByBciAvId (bciAvId: string, blissGlosses: array) { * was no match in the gloss * } */ -export function processPaletteLabels (paletteLabels, paletteName, startRow, startColumn, cellType) { + +export async function processPaletteLabels (paletteLabels: string[][], paletteName: string, startRow: number, startColumn: number, cellType: string) { // Initialize palette to return, the matches, and the error list - const finalJson = { + const finalJson: Palette = { "name": paletteName, "cells": {} }; - const matchByInfoArray = []; - const errors = []; + const matchByInfoArray: MatchByInfo[] = []; + const errors: string[] = []; - paletteLabels.forEach((row, rowIndex) => { - row.forEach((infoString, colIndex) => { + paletteLabels.forEach((row: string[], rowIndex: number) => { + row.forEach((infoString: string, colIndex: number) => { const current_row = startRow + rowIndex; const current_column = startColumn + colIndex; @@ -217,7 +267,7 @@ export function processPaletteLabels (paletteLabels, paletteName, startRow, star } // Create a cell object for the current `infoString`, leaving the // `bciAvId` field undefined for now. - const cell = { + const cell: Cell = { type: cellType, options: { label: infoString, @@ -243,27 +293,28 @@ export function processPaletteLabels (paletteLabels, paletteName, startRow, star } else { // If the `infoString` is a BCI AV ID (a number), just use it for the - // `bciAvId`. Even so, find its description from the `bliss_gloss` + // `bciAvId`. Even so, find its description from the `bciAvSymbolsDict` cell.options.bciAvId = parseInt(infoString); if (!isNaN(cell.options.bciAvId)) { - const glossEntry = findByBciAvId(infoString, bliss_gloss); + const glossEntry = findByBciAvId(infoString, bciAvSymbolsDict); cell.options.label = actualLabel || glossEntry["description"]; } else { // Find the BCI AV IDs for the current infoString. Use the first // match for the palette - const matches = findBciAvId(infoString, bliss_gloss); + const matches = findBciAvId(infoString, bciAvSymbolsDict); cell.options.bciAvId = matches[0].bciAvId; cell.options.label = actualLabel || infoString; - const inputMatches = {}; + const inputMatches: MatchByInfo = {}; inputMatches[infoString] = matches; matchByInfoArray.push(inputMatches); } } } - catch (error) { + catch (error: unknown) { // If an error occurs, add it to the errors array - errors.push(error.message); + const errorMessage = error instanceof Error ? error.message : String(error); + errors.push(errorMessage); // Change the cell label to indicate that this cell is not right yet. // The `bciAvId` encoding means "not found". diff --git a/src/client/GlobalData.ts b/src/client/GlobalData.ts index ed3250d..227130b 100644 --- a/src/client/GlobalData.ts +++ b/src/client/GlobalData.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Inclusive Design Research Centre, OCAD University + * Copyright 2023-2026 Inclusive Design Research Centre, OCAD University * All rights reserved. * * Licensed under the New BSD license. You may not use this file except in @@ -13,18 +13,7 @@ * Populate and export global data */ import { signal } from "@preact/signals"; - -// NOTE: this import causes a warning serving the application using the `vite` -// server. The warning suggests to *not* us the `public` folder but to use -// the `src` folder instead. However, this code is also served using node -// express and it is in the proper location for that envionment. A copy of the -// warning follows: -// "Assets in public directory cannot be imported from JavaScript. -// If you intend to import that asset, put the file in the src directory, and use /src/data/bliss_symbol_explanations.json instead of /public/data/bliss_symbol_explanations.json. -// If you intend to use the URL of that asset, use /data/bliss_symbol_explanations.json?url. -// Files in the public directory are served at the root path. -// Instead of /public/data/bliss_symbol_explanations.json, use /data/bliss_symbol_explanations.json." -import bliss_symbols from "../../public/data/bliss_symbol_explanations.json"; +import { BlissaryIdMap, BciAvSymbolsDict } from "./index.d"; /** * The map between cell types (string) and actual components that render corresponding cells @@ -65,12 +54,22 @@ export const cellTypeRegistry = { * Load the map between the BCI-AV IDs and the code consumed by the Bliss SVG * and create the PaletterStore and NavigationStack objects. */ -export const adaptivePaletteGlobals = { +interface AdaptivePaletteGlobals { + blissaryIdMap: BlissaryIdMap | null; + bciAvSymbols: BciAvSymbolsDict | null; + paletteStore: PaletteStore; + navigationStack: NavigationStack; + mainPaletteContainerId: string; +}; + +const blissaryIdMapUrl: string = "https://raw.githubusercontent.com/hlridge/Bliss-Blissary-BCI-ID-Map/main/blissary_to_bci_mapping.json"; +export const bciAvSymbolsDictUrl: string = "https://raw.githubusercontent.com/inclusive-design/adaptive-palette/main/public/data/bliss_symbol_explanations.json"; + +export const adaptivePaletteGlobals: AdaptivePaletteGlobals = { // The map between the BCI-AV IDs and the code consumed by the Bliss SVG // builder. The map itself is set asynchronously. - blissaryIdMapUrl: "https://raw.githubusercontent.com/hlridge/Bliss-Blissary-BCI-ID-Map/main/blissary_to_bci_mapping.json", blissaryIdMap: null, - bciAvSymbols: bliss_symbols, + bciAvSymbols: null, paletteStore: new PaletteStore(), navigationStack: new NavigationStack(), @@ -81,8 +80,8 @@ export const adaptivePaletteGlobals = { mainPaletteContainerId: "" }; -export async function loadBlissaryIdMap (): Promise { - const response = await fetch(adaptivePaletteGlobals.blissaryIdMapUrl); +export async function loadDataFromUrl(url: string): Promise { + const response = await fetch(url); return await response.json(); } @@ -94,10 +93,11 @@ export async function loadBlissaryIdMap (): Promise { * use for rendering the the * main paletted Defaults to the * empty string which denotes - * the `delement. + * the `` element. */ export async function initAdaptivePaletteGlobals (mainPaletteContainerId?:string): Promise { - adaptivePaletteGlobals.blissaryIdMap = await loadBlissaryIdMap(); + adaptivePaletteGlobals.blissaryIdMap = await loadDataFromUrl(blissaryIdMapUrl); + adaptivePaletteGlobals.bciAvSymbols = await loadDataFromUrl(bciAvSymbolsDictUrl); adaptivePaletteGlobals.mainPaletteContainerId = mainPaletteContainerId || ""; } diff --git a/src/client/index.d.ts b/src/client/index.d.ts index 28d561b..a20e0ed 100644 --- a/src/client/index.d.ts +++ b/src/client/index.d.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Inclusive Design Research Centre, OCAD University + * Copyright 2023-2026 Inclusive Design Research Centre, OCAD University * All rights reserved. * * Licensed under the New BSD license. You may not use this file except in @@ -75,3 +75,22 @@ export type ContentSignalDataType = { payloads: SymbolEncodingType[], caretPosition: number }; + +export type BlissaryIdMapEntry = { + blissaryId: number; + bciAvId: number; + blissSvgBuilderCode: string; +} + +export type BlissaryIdMap = BlissaryIdMapEntry[]; + +export type BciAvSymbolEntry = { + id: string; + description: string; + pos: string; + explanation: string; + isCharacter: boolean; + composition?: (string | number)[]; // Optional +} + +export type BciAvSymbolsDict = BciAvSymbolEntry[];