@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 +56 -0
- package/CHANGELOG.zh.md +42 -0
- package/dist/core/keystroke/textNormalize.js +13 -10
- package/dist/core/typing/engine/TypingInput.js +8 -2
- package/dist/core/typing/engine/normalize.js +87 -0
- package/dist/view/assets/index-CeSHvqfR.js +182 -0
- package/dist/view/assets/{index-D6FqmGLJ.js → index-TskEzyRz.js} +2 -2
- package/dist/view/assets/{index-Bp7wq7S1.js → index-nAVje4CH.js} +1 -1
- package/dist/view/index.html +1 -1
- package/package.json +1 -1
- package/dist/view/assets/index-BY0yuQoY.js +0 -182
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
|
-
//
|
|
3
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
78
|
-
:
|
|
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
|
+
}
|