@qnroa/qtype 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -13,6 +13,62 @@ can land in any minor bump (`0.x.0`).
13
13
 
14
14
  ## [Unreleased]
15
15
 
16
+ ## [0.1.2] — 2026-08-28
17
+
18
+ Fixes iOS Safari (and some Android IMEs) where soft-keyboard keys
19
+ (Space, Enter, Backspace) silently did nothing. The engine received
20
+ no event because these browsers don't fire `keydown` for keys
21
+ committed outside an active composition — they only arrive via
22
+ `beforeinput`, which the old handler `preventDefault`'d without
23
+ dispatching.
24
+
25
+ ### Fixed
26
+ - **Space key works on iPhone / iPad Chinese keyboards.** SPACE and
27
+ any printable char arriving as `beforeinput insertText` outside of
28
+ an active composition is now dispatched to the engine. Previously
29
+ the key felt "dead" — users had to swap to the English keyboard
30
+ just to type a space between words.
31
+ - **Enter / Return key works on iPhone / iPad.** `insertLineBreak`
32
+ and `insertParagraph` from `beforeinput` now dispatch as `enter`.
33
+ - **Backspace works on iPhone / iPad.** `deleteContentBackward`,
34
+ `deleteWordBackward`, and `deleteSoftLineBackward` from
35
+ `beforeinput` now dispatch as `backspace`.
36
+ - **De-duplication for browsers that fire both events.** Desktop
37
+ browsers fire `keydown` AND a follow-up `beforeinput` for the same
38
+ keystroke. A 50ms guard prevents a double-emit across all three
39
+ paths (char, enter, backspace).
40
+
41
+ ## [0.1.1] — 2026-08-28
42
+
43
+ Fixes typing comparison for equivalent-but-different characters that
44
+ the OS/IME hands out (fullwidth space in Chinese input mode, smart
45
+ quotes on macOS/iOS, CJK punctuation vs ASCII, etc.). Display side is
46
+ unchanged; equivalence only runs when comparing a keystroke against
47
+ the expected character.
48
+
49
+ ### Fixed
50
+ - **Fullwidth / half-width space now match on input.** Typing ` `
51
+ (Chinese IME space) against a card whose target is ` ` no
52
+ longer misses. Also covers non-breaking space ` `. Mobile users
53
+ on Chinese keyboards used to have to switch to English just to hit
54
+ space; that friction is gone.
55
+ - **Smart quotes match straight quotes on input.** iOS / macOS auto-
56
+ substituted `“`/`”`/`‘`/`’` now compare equal to
57
+ ASCII `"` and `'`. Cards typed on a phone stop failing on quotes.
58
+ - **CJK punctuation matches ASCII punctuation on input.** `,。?!:
59
+ ;()【】《》/+=~` and friends are compared equivalent to their
60
+ ASCII forms. Users don't need to swap IME state mid-card.
61
+ - **En-dash / em-dash / fullwidth hyphen match ASCII `-` on input.**
62
+
63
+ ### Changed
64
+ - **Display is honest about the source.** `normalizeText` no longer
65
+ rewrites user-visible characters at import time (previously
66
+ `   ` were folded to ASCII space, `—` was expanded
67
+ to `--`, etc.). Cards render exactly what the author wrote; the
68
+ equivalence table lives in `typing/engine/normalize.ts` and only
69
+ runs at keystroke comparison time. Untypable characters (control,
70
+ zero-width, emoji) are still stripped.
71
+
16
72
  ## [0.1.0] — 2026-08-27
17
73
 
18
74
  Breaking dist format. Source `material/*.md` unchanged — the source
package/CHANGELOG.zh.md CHANGED
@@ -12,6 +12,48 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.1.2] — 2026-08-28
16
+
17
+ 修复 iOS Safari(以及部分 Android 输入法)上软键盘的 Space / Enter /
18
+ Backspace 键"没反应"的问题。这些键在 composition 结束外按下时
19
+ **不触发** `keydown`,只通过 `beforeinput` 到达;旧代码只
20
+ `preventDefault` 没派发给引擎,用户以为键坏了。
21
+
22
+ ### 修复
23
+ - **iPhone / iPad 上空格键可用**。软键盘 `insertText` 现在会派发
24
+ 给引擎。之前必须切到英文键盘才能在单词间打空格。
25
+ - **iPhone / iPad 上 Enter/Return 可用**。`insertLineBreak` /
26
+ `insertParagraph` 现在派发为 `enter`。
27
+ - **iPhone / iPad 上 Backspace 可用**。`deleteContentBackward` /
28
+ `deleteWordBackward` / `deleteSoftLineBackward` 现在派发为
29
+ `backspace`。
30
+ - **桌面浏览器双触发去重**。桌面 `keydown` + `beforeinput` 同一次按
31
+ 键会双触发,加了 50ms 时间戳去重,char/enter/backspace 三条路径
32
+ 都覆盖。
33
+
34
+ ## [0.1.1] — 2026-08-28
35
+
36
+ 修复输入法/终端产出的等价字符(中文输入法下的全角空格、macOS/iOS
37
+ 自动替换的智能引号、中文标点 vs 英文标点等)造成的误判。显示层
38
+ 不变,等价比较只在"按键 vs 期望字符"的那一步生效。
39
+
40
+ ### 修复
41
+ - **全角/半角空格视为等价**。中文输入法下敲 ` `(U+3000) 匹配卡片
42
+ 里的半角 ` ` 不再算错;` ` 不间断空格同样等价。手机上中文
43
+ 输入法用户不再需要切英文只为了打个空格。
44
+ - **智能引号 = 直引号**。iOS/macOS 自动替换的 `""''` 现在跟 ASCII
45
+ `"` `'` 比较相等。手机上打卡不会因为引号变形而卡住。
46
+ - **中文标点 = 英文标点**。`,。?!:;()【】《》/+=~` 等在
47
+ 比较时等价于对应的 ASCII 标点,无需切换输入法状态。
48
+ - **En-dash / em-dash / 全角减号 = ASCII `-`**。
49
+
50
+ ### 变更
51
+ - **显示忠实于源码**。`normalizeText` 不再在 import 时改写用户可见
52
+ 字符(之前 `  ` 会被折叠成 ASCII 空格,`—` 被扩展成 `--`)。
53
+ 卡片显示什么就是作者写的什么;等价表放在
54
+ `typing/engine/normalize.ts`,只在打字比较时生效。控制字符 / 零
55
+ 宽字符 / emoji 依然会剔除。
56
+
15
57
  ## [0.1.0] — 2026-08-27
16
58
 
17
59
  破坏性 dist 格式变更。源 `material/*.md` 未变 —— 语法和语义端到端
@@ -1,15 +1,18 @@
1
1
  // Import-time text sanitization + validation.
2
- // Goal: guarantee that every character surviving normalization has a valid
3
- // keystroke plan, so runtime never has to skip or bail on unknown chars.
2
+ //
3
+ // This layer used to rewrite user-visible characters (smart quotes to
4
+ // straight, fullwidth space to half, em-dash to double hyphen, etc.) at
5
+ // import time — which meant the *displayed* text no longer matched what
6
+ // the author wrote. That's the wrong layer: display should be honest
7
+ // about the source; runtime equivalence for typing lives in
8
+ // `typing/engine/normalize.ts` and only affects comparison.
9
+ //
10
+ // So the rewrite table is empty. We still strip characters that are
11
+ // truly untypable (control chars, zero-width, emoji), because those
12
+ // have no keyboard input at all.
4
13
  import { charToKeystrokes } from './index.js';
5
- // Character rewrites: smart quotes, unusual dashes, etc.
6
- const REWRITE = {
7
- '—': '——', // em-dash → double hyphen (matches keystroke plan for 中文 —)
8
- '–': '-', // en-dash → hyphen
9
- '…': '……', // horizontal ellipsis → double per convention (chineseMap uses ……)
10
- ' ': ' ', // non-breaking space → regular space
11
- ' ': ' ', // ideographic space → regular space
12
- };
14
+ // Rewrites are intentionally empty. See file-level comment.
15
+ const REWRITE = {};
13
16
  // Ranges of code points to strip entirely (they can't be typed).
14
17
  const STRIP_RANGES = [
15
18
  [0x0000, 0x0008], // C0 controls before \t
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { CharAttr } from './chars.js';
21
21
  import { Feedback } from './feedback.js';
22
+ import { normalizeForCompare } from './normalize.js';
22
23
  import { defaultTypingSettings, } from './settings.js';
23
24
  import { computeStats } from './stats.js';
24
25
  export class TypingInput {
@@ -73,9 +74,14 @@ export class TypingInput {
73
74
  return Feedback.Correct;
74
75
  expected = this.#targetCodepoints[this.#position];
75
76
  }
77
+ // Normalize both sides through the same equivalence table so
78
+ // fullwidth/half-width / smart-quote / CJK-punctuation variants
79
+ // don't cause false misses. Display remains untouched.
80
+ const nCp = normalizeForCompare(codePoint);
81
+ const nExp = normalizeForCompare(expected);
76
82
  const isHit = this.settings.ignoreCase
77
- ? sameLetter(codePoint, expected)
78
- : codePoint === expected;
83
+ ? sameLetter(nCp, nExp)
84
+ : nCp === nExp;
79
85
  // Clone the array so subscribers see a new reference. Only the touched
80
86
  // Char is a new object — the rest are shared.
81
87
  const next = this.#chars.slice();
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Runtime input normalization for typing comparison.
3
+ *
4
+ * The engine used to compare `codePoint === expected` strictly, which broke
5
+ * for equivalent-but-different characters that the OS/IME hands out
6
+ * depending on state:
7
+ *
8
+ * - Fullwidth space U+3000 vs regular space U+0020 (Chinese IME)
9
+ * - Non-breaking space U+00A0 vs U+0020
10
+ * - Smart quotes U+201C/U+201D/U+2018/U+2019 vs straight quotes
11
+ * - Fullwidth CJK punctuation (U+FF0C etc.) vs ASCII punctuation
12
+ * - En-dash U+2013 vs hyphen U+002D
13
+ * - Horizontal ellipsis U+2026 vs three dots
14
+ *
15
+ * These are all pairs where the two forms carry the same *intent* — the
16
+ * user is trying to type the character they see, and shouldn't be
17
+ * penalised because their input method emitted a different codepoint.
18
+ *
19
+ * The rule: normalize BOTH the user's keystroke and the expected
20
+ * character through the same table, then compare. Display is untouched.
21
+ *
22
+ * Character classes we do NOT normalize:
23
+ * - Fullwidth Latin letters (U+FF21..U+FF3A / U+FF41..U+FF5A) — if a
24
+ * card contains "A" that's the author's explicit intent; ignoreCase
25
+ * handles the ASCII case elsewhere.
26
+ * - Fullwidth digits (U+FF10..U+FF19) — same reasoning.
27
+ * - Curly braces / angles that carry semantic meaning in Chinese text.
28
+ */
29
+ /** Char → equivalent char used for typing comparison. */
30
+ const EQUIV = {
31
+ // ─── Spaces ───────────────────────────────────────────
32
+ 0x3000: 0x0020, // U+3000 IDEOGRAPHIC SPACE
33
+ 0x00A0: 0x0020, // U+00A0 NO-BREAK SPACE
34
+ // ─── Quotes ────────────────────────────────────────────
35
+ 0x201C: 0x0022, // U+201C LEFT DOUBLE QUOTATION MARK
36
+ 0x201D: 0x0022, // U+201D RIGHT DOUBLE QUOTATION MARK
37
+ 0x201E: 0x0022, // U+201E DOUBLE LOW-9
38
+ 0x2033: 0x0022, // U+2033 DOUBLE PRIME
39
+ 0x2018: 0x0027, // U+2018 LEFT SINGLE QUOTATION MARK
40
+ 0x2019: 0x0027, // U+2019 RIGHT SINGLE QUOTATION MARK
41
+ 0x2032: 0x0027, // U+2032 PRIME
42
+ // ─── Dashes ────────────────────────────────────────────
43
+ 0x2013: 0x002D, // U+2013 EN DASH
44
+ 0x2014: 0x002D, // U+2014 EM DASH (kept: single "-" match; author's "——"
45
+ // double-hyphen convention still works because
46
+ // it's already two ASCII "-" in storage)
47
+ 0x2212: 0x002D, // U+2212 MINUS SIGN
48
+ 0xFF0D: 0x002D, // U+FF0D FULLWIDTH HYPHEN-MINUS
49
+ // ─── CJK punctuation → ASCII ───────────────────────────
50
+ 0xFF0C: 0x002C, // ,
51
+ 0x3002: 0x002E, // 。 (map to `.`; author writing 句号 the user types `.`)
52
+ 0xFF1F: 0x003F, // ?
53
+ 0xFF01: 0x0021, // !
54
+ 0xFF1A: 0x003A, // :
55
+ 0xFF1B: 0x003B, // ;
56
+ 0xFF08: 0x0028, // (
57
+ 0xFF09: 0x0029, // )
58
+ 0x3010: 0x005B, // 【
59
+ 0x3011: 0x005D, // 】
60
+ 0x300A: 0x003C, // 《
61
+ 0x300B: 0x003E, // 》
62
+ 0xFF0F: 0x002F, // /
63
+ 0xFF0B: 0x002B, // +
64
+ 0xFF1D: 0x003D, // =
65
+ 0xFF5E: 0x007E, // ~
66
+ 0xFF03: 0x0023, // #
67
+ 0xFF04: 0x0024, // $
68
+ 0xFF05: 0x0025, // %
69
+ 0xFF06: 0x0026, // &
70
+ 0xFF0A: 0x002A, // *
71
+ 0xFF20: 0x0040, // @
72
+ 0xFF3B: 0x005B, // [
73
+ 0xFF3D: 0x005D, // ]
74
+ 0xFF5B: 0x007B, // {
75
+ 0xFF5D: 0x007D, // }
76
+ // ─── Middle dot / interpunct ───────────────────────────
77
+ 0x00B7: 0x00B7, // U+00B7 keep (rendered as ·)
78
+ 0x2027: 0x00B7, // U+2027 HYPHENATION POINT
79
+ 0x30FB: 0x00B7, // U+30FB KATAKANA MIDDLE DOT
80
+ // ─── Ellipsis ──────────────────────────────────────────
81
+ 0x2026: 0x002E, // U+2026 HORIZONTAL ELLIPSIS -> "." (matched char-by-char
82
+ // if the card stores "..." literally)
83
+ };
84
+ /** Return the canonical form of `codePoint` for typing comparison. */
85
+ export function normalizeForCompare(codePoint) {
86
+ return EQUIV[codePoint] ?? codePoint;
87
+ }