@qnroa/qtype 0.1.0 → 0.1.1
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 +31 -0
- package/CHANGELOG.zh.md +23 -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-BY0yuQoY.js → index--xiys9ma.js} +35 -35
- package/dist/view/assets/{index-Bp7wq7S1.js → index-CpOQ9x3V.js} +1 -1
- package/dist/view/assets/{index-D6FqmGLJ.js → index-y4VkSsUN.js} +2 -2
- package/dist/view/index.html +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,37 @@ can land in any minor bump (`0.x.0`).
|
|
|
13
13
|
|
|
14
14
|
## [Unreleased]
|
|
15
15
|
|
|
16
|
+
## [0.1.1] — 2026-08-28
|
|
17
|
+
|
|
18
|
+
Fixes typing comparison for equivalent-but-different characters that
|
|
19
|
+
the OS/IME hands out (fullwidth space in Chinese input mode, smart
|
|
20
|
+
quotes on macOS/iOS, CJK punctuation vs ASCII, etc.). Display side is
|
|
21
|
+
unchanged; equivalence only runs when comparing a keystroke against
|
|
22
|
+
the expected character.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **Fullwidth / half-width space now match on input.** Typing ` `
|
|
26
|
+
(Chinese IME space) against a card whose target is ` ` no
|
|
27
|
+
longer misses. Also covers non-breaking space ` `. Mobile users
|
|
28
|
+
on Chinese keyboards used to have to switch to English just to hit
|
|
29
|
+
space; that friction is gone.
|
|
30
|
+
- **Smart quotes match straight quotes on input.** iOS / macOS auto-
|
|
31
|
+
substituted `“`/`”`/`‘`/`’` now compare equal to
|
|
32
|
+
ASCII `"` and `'`. Cards typed on a phone stop failing on quotes.
|
|
33
|
+
- **CJK punctuation matches ASCII punctuation on input.** `,。?!:
|
|
34
|
+
;()【】《》/+=~` and friends are compared equivalent to their
|
|
35
|
+
ASCII forms. Users don't need to swap IME state mid-card.
|
|
36
|
+
- **En-dash / em-dash / fullwidth hyphen match ASCII `-` on input.**
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- **Display is honest about the source.** `normalizeText` no longer
|
|
40
|
+
rewrites user-visible characters at import time (previously
|
|
41
|
+
` ` were folded to ASCII space, `—` was expanded
|
|
42
|
+
to `--`, etc.). Cards render exactly what the author wrote; the
|
|
43
|
+
equivalence table lives in `typing/engine/normalize.ts` and only
|
|
44
|
+
runs at keystroke comparison time. Untypable characters (control,
|
|
45
|
+
zero-width, emoji) are still stripped.
|
|
46
|
+
|
|
16
47
|
## [0.1.0] — 2026-08-27
|
|
17
48
|
|
|
18
49
|
Breaking dist format. Source `material/*.md` unchanged — the source
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -12,6 +12,29 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.1.1] — 2026-08-28
|
|
16
|
+
|
|
17
|
+
修复输入法/终端产出的等价字符(中文输入法下的全角空格、macOS/iOS
|
|
18
|
+
自动替换的智能引号、中文标点 vs 英文标点等)造成的误判。显示层
|
|
19
|
+
不变,等价比较只在"按键 vs 期望字符"的那一步生效。
|
|
20
|
+
|
|
21
|
+
### 修复
|
|
22
|
+
- **全角/半角空格视为等价**。中文输入法下敲 ` `(U+3000) 匹配卡片
|
|
23
|
+
里的半角 ` ` 不再算错;` ` 不间断空格同样等价。手机上中文
|
|
24
|
+
输入法用户不再需要切英文只为了打个空格。
|
|
25
|
+
- **智能引号 = 直引号**。iOS/macOS 自动替换的 `""''` 现在跟 ASCII
|
|
26
|
+
`"` `'` 比较相等。手机上打卡不会因为引号变形而卡住。
|
|
27
|
+
- **中文标点 = 英文标点**。`,。?!:;()【】《》/+=~` 等在
|
|
28
|
+
比较时等价于对应的 ASCII 标点,无需切换输入法状态。
|
|
29
|
+
- **En-dash / em-dash / 全角减号 = ASCII `-`**。
|
|
30
|
+
|
|
31
|
+
### 变更
|
|
32
|
+
- **显示忠实于源码**。`normalizeText` 不再在 import 时改写用户可见
|
|
33
|
+
字符(之前 ` ` 会被折叠成 ASCII 空格,`—` 被扩展成 `--`)。
|
|
34
|
+
卡片显示什么就是作者写的什么;等价表放在
|
|
35
|
+
`typing/engine/normalize.ts`,只在打字比较时生效。控制字符 / 零
|
|
36
|
+
宽字符 / emoji 依然会剔除。
|
|
37
|
+
|
|
15
38
|
## [0.1.0] — 2026-08-27
|
|
16
39
|
|
|
17
40
|
破坏性 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
|
+
}
|