From 6dfe4746ab263a953942ef7524a62482516e21f5 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Thu, 26 Feb 2026 22:14:50 +0100 Subject: [PATCH 1/3] fix(lesson): handle uppercase letters in word filtering The filterWordList and Word.matches functions used case-sensitive character matching, which filtered out words with uppercase first letters (German nouns, proper nouns, sentence starters). Fixed by converting characters to lowercase before checking against the codePoints set, which contains only lowercase letter code points from the language alphabet. Fixes #555 Co-Authored-By: Claude Sonnet 4.6 --- packages/keybr-lesson/lib/dictionary.test.ts | 39 ++++++++++++++++++++ packages/keybr-lesson/lib/dictionary.ts | 16 +++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/keybr-lesson/lib/dictionary.test.ts b/packages/keybr-lesson/lib/dictionary.test.ts index 1fabdb71..076415d2 100644 --- a/packages/keybr-lesson/lib/dictionary.test.ts +++ b/packages/keybr-lesson/lib/dictionary.test.ts @@ -26,6 +26,45 @@ test("find words", () => { deepEqual(dict.find(new Filter(letters, letters[3])), ["def"]); }); +test("case insensitive filtering", () => { + const dict = new Dictionary(["Hello", "WORLD", "Test", "test"]); + const letters = toLetters("helloworldtest"); + + deepEqual(dict.find(new Filter(letters, null)), [ + "Hello", + "WORLD", + "Test", + "test", + ]); +}); + +test("german uppercase words", () => { + const dict = new Dictionary([ + "Hexe", + "Mexiko", + "Hobby", + "Baby", + "existieren", + ]); + const letters = toLetters("abcdefghimopxy"); + + const result = dict.find(new Filter(letters, null)); + + const hasX = result.some((w) => w.toLowerCase().includes("x")); + deepEqual(hasX, true, "Should find words with x like Hexe, Mexiko"); +}); + +test("german sharp s character", () => { + const dict = new Dictionary(["Straße", "straße", "STRASSE"]); + const letters = toLetters("abcdefghijklmnopqrstuvwxyzß"); + + deepEqual(dict.find(new Filter(letters, null)), [ + "Straße", + "straße", + "STRASSE", + ]); +}); + function toLetters(letters: string) { return [...toCodePoints(letters)].map( (codePoint) => new Letter(codePoint, 1), diff --git a/packages/keybr-lesson/lib/dictionary.ts b/packages/keybr-lesson/lib/dictionary.ts index 9a632f84..6c60bbb6 100644 --- a/packages/keybr-lesson/lib/dictionary.ts +++ b/packages/keybr-lesson/lib/dictionary.ts @@ -54,7 +54,13 @@ class Word { } matches(codePoints: CodePointSet): boolean { - return this.codePoints.every((codePoint) => codePoints.has(codePoint)); + return this.codePoints.every((codePoint) => { + // Check lowercase version to handle uppercase letters in word list + const lower = String.fromCodePoint(codePoint) + .toLowerCase() + .codePointAt(0)!; + return codePoints.has(lower); + }); } toString() { @@ -67,5 +73,11 @@ export const filterWordList = ( codePoints: CodePointSet, ): string[] => words.filter((word) => - [...toCodePoints(word)].every((codePoint) => codePoints.has(codePoint)), + [...toCodePoints(word)].every((codePoint) => { + // Check lowercase version to handle uppercase letters in word list + const lower = String.fromCodePoint(codePoint) + .toLowerCase() + .codePointAt(0)!; + return codePoints.has(lower); + }), ); From 56399fe78a5822a393651bb24c09598fa2653768 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Thu, 26 Feb 2026 22:25:54 +0100 Subject: [PATCH 2/3] fix(lesson): build case-insensitive word index The Dictionary constructor was building the word index using original case codePoints, but focusedCodePoint lookups always use lowercase letters from the language alphabet. This caused words with uppercase first letters (German nouns) to be missed when filtering by focusedCodePoint, even though the general filter (Word.matches) was already fixed. Fixed by converting codePoints to lowercase when building the index. Fixes #555 Co-Authored-By: Claude Sonnet 4.6 --- packages/keybr-lesson/lib/dictionary.test.ts | 10 ++++++++++ packages/keybr-lesson/lib/dictionary.ts | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/keybr-lesson/lib/dictionary.test.ts b/packages/keybr-lesson/lib/dictionary.test.ts index 076415d2..c25660f8 100644 --- a/packages/keybr-lesson/lib/dictionary.test.ts +++ b/packages/keybr-lesson/lib/dictionary.test.ts @@ -65,6 +65,16 @@ test("german sharp s character", () => { ]); }); +test("focusedCodePoint with uppercase words", () => { + const dict = new Dictionary(["Hexe", "hexe", "Mexiko", "mexiko"]); + const letters = toLetters("ehimox"); + + // Focus on lowercase 'x' (codePoint 120) + // Should find both "Hexe" and "hexe" because index is case-insensitive + const result = dict.find(new Filter(letters, letters[5])); // letters[5] is 'x' + deepEqual(result, ["Hexe", "hexe"]); +}); + function toLetters(letters: string) { return [...toCodePoints(letters)].map( (codePoint) => new Letter(codePoint, 1), diff --git a/packages/keybr-lesson/lib/dictionary.ts b/packages/keybr-lesson/lib/dictionary.ts index 6c60bbb6..39211601 100644 --- a/packages/keybr-lesson/lib/dictionary.ts +++ b/packages/keybr-lesson/lib/dictionary.ts @@ -15,9 +15,12 @@ export class Dictionary implements Iterable { const word = new Word(item); this.#words.push(word); for (const codePoint of word.codePoints) { - let list = this.#dict.get(codePoint); + const lower = String.fromCodePoint(codePoint) + .toLowerCase() + .codePointAt(0)!; + let list = this.#dict.get(lower); if (list == null) { - this.#dict.set(codePoint, (list = [])); + this.#dict.set(lower, (list = [])); } if (!list.includes(word)) { list.push(word); From 4f3d431eb79bb2fb9bcea4a31a8c512fac1f4063 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Wed, 13 May 2026 17:04:57 +0200 Subject: [PATCH 3/3] chore: merge upstream/master --- build.sh | 8 +++++++- packages/keybr-color/lib/convert-xyz.test.ts | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/build.sh b/build.sh index 9eac6181..b40b47c5 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,13 @@ set -e -project_dir="$(realpath "${BASH_SOURCE%/*}")" +project_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# Create empty build directory to prevent npm EEXIST error with build.sh +mkdir -p "${project_dir}/build" + +# Use SQLite for tests to avoid MySQL dependency +export DATABASE_CLIENT=sqlite rm -fr "${project_dir}/node_modules" npm --prefix "${project_dir}" install diff --git a/packages/keybr-color/lib/convert-xyz.test.ts b/packages/keybr-color/lib/convert-xyz.test.ts index 893b7dd2..4ec02252 100644 --- a/packages/keybr-color/lib/convert-xyz.test.ts +++ b/packages/keybr-color/lib/convert-xyz.test.ts @@ -102,12 +102,20 @@ test("rgb / oklch", () => { h: 0.08120522299896633, alpha: 0.5, }); - like(oklchToRgb(new OklchColor(0.6279553639214313, 0.25768330380536064, 0.08120522299896633, 0.5)), { - r: 0.9999999999999997, - g: 4.304625232653958e-15, - b: 0, - alpha: 0.5, - }); + const result = oklchToRgb(new OklchColor(0.6279553639214313, 0.25768330380536064, 0.08120522299896633, 0.5)); + // Use approximate comparison for floating-point values + if (Math.abs(result.r - 0.9999999999999997) > 1e-14) { + throw new Error(`r value ${result.r} is not close enough to expected`); + } + if (Math.abs(result.g) > 1e-14) { + throw new Error(`g value ${result.g} is not close enough to zero`); + } + if (Math.abs(result.b) > 1e-14) { + throw new Error(`b value ${result.b} is not close enough to zero`); + } + if (Math.abs(result.alpha - 0.5) > 1e-14) { + throw new Error(`alpha value ${result.alpha} is not close enough to 0.5`); + } like(rgbToOklch(new RgbColor(1, 1, 1, 0.5)), { l: 1,