@qnroa/qtype 0.2.1 → 0.2.3
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 +94 -0
- package/CHANGELOG.zh.md +72 -0
- package/dist/cli/commands/publish/build/action.js +63 -9
- package/dist/cli/commands/repo/new/action.js +14 -0
- package/dist/core/typing/engine/TypingInput.js +22 -39
- package/dist/view/assets/{index-B_HnQEsb.js → index-B_3g67jY.js} +2 -2
- package/dist/view/assets/index-CeFBpbIw.js +194 -0
- package/dist/view/assets/{index-BbG90BH8.js → index-Cj70GP_V.js} +1 -1
- package/dist/view/assets/index-VPadwWzO.css +1 -0
- package/dist/view/index.html +3 -3
- package/dist/view/logo.svg +21 -0
- package/package.json +1 -1
- package/templates/en/.qtype/assets/favicon.svg +21 -0
- package/templates/en/.qtype/assets/logo.svg +21 -0
- package/templates/zh/.qtype/assets/favicon.svg +21 -0
- package/templates/zh/.qtype/assets/logo.svg +21 -0
- package/dist/view/assets/index-B8531CqV.css +0 -1
- package/dist/view/assets/index-eEQRUOZD.js +0 -194
- /package/dist/view/{qtype-mark.svg → favicon.svg} +0 -0
- /package/dist/view/{qtype-logo.svg → wordmark.svg} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,100 @@ can land in any minor bump (`0.x.0`).
|
|
|
13
13
|
|
|
14
14
|
## [Unreleased]
|
|
15
15
|
|
|
16
|
+
## [0.2.3] — 2026-09-07
|
|
17
|
+
|
|
18
|
+
Fixes long-standing bugs around Chinese IME input on the typing arena
|
|
19
|
+
and cleans up how "ignore punctuation" behaves. The invisible editor
|
|
20
|
+
is now a `<input>` instead of a contenteditable div, matching the
|
|
21
|
+
pattern every mainstream Chinese typing site uses; character capture
|
|
22
|
+
reads `el.value` on the `input` event, so IME commits arrive as the
|
|
23
|
+
final glyph — no more races between keydown and beforeinput.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Miss-slot display setting** (Appearance → Miss display). Default
|
|
27
|
+
keeps the target glyph visible in red (matches Monkeytype / keybr
|
|
28
|
+
/ ohMyType / ptype conventions — user sees *what should have been
|
|
29
|
+
there*). Flip to "Typed" to render the user's actual keystroke in
|
|
30
|
+
red at the miss slot instead — useful when reviewing exactly what
|
|
31
|
+
went wrong.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **Editor element is now a hidden `<input>`, not `<div
|
|
35
|
+
contentEditable>`.** IME commits and macOS smart-punctuation
|
|
36
|
+
substitutions land in `el.value` as the final glyph, which the
|
|
37
|
+
`input` event reads and emits to the engine. Composition (pinyin
|
|
38
|
+
preedit) is gated on the event's own `isComposing` /
|
|
39
|
+
`inputType === 'insertCompositionText'` signals rather than the
|
|
40
|
+
hook's state machine, so browsers that fire `input` before
|
|
41
|
+
`compositionstart` can't leak preedit letters through as
|
|
42
|
+
characters.
|
|
43
|
+
- **`Ignore punctuation` now rewrites punctuation to space at engine
|
|
44
|
+
construction.** Previously the engine fast-forwarded through
|
|
45
|
+
punctuation slots inside `onKeyDown`, which left the arena still
|
|
46
|
+
showing punctuation and the on-screen keyboard still hinting the
|
|
47
|
+
punctuation key. Now every punctuation codepoint in the target
|
|
48
|
+
becomes a regular space up-front — the arena, the keyboard hint
|
|
49
|
+
and the engine all see the same target sequence, no divergence.
|
|
50
|
+
Toggling the setting mid-card rebuilds the engine via the
|
|
51
|
+
`useTypingEngine` memo.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- **Chinese full-width punctuation shown as ASCII.** The old
|
|
55
|
+
keydown-based capture path picked up whichever of `keydown.key` /
|
|
56
|
+
`beforeinput.data` fired first and could substitute a half-width
|
|
57
|
+
glyph. Moving to `<input>` + `input` event and always rendering
|
|
58
|
+
the target glyph (default) puts an end to this class of bug.
|
|
59
|
+
- **Dead `softInsertText` handler removed** from `INPUT_TYPE_HANDLERS`
|
|
60
|
+
and the `handleBeforeInput` fast-path; character emission has a
|
|
61
|
+
single owner now (the `input` event).
|
|
62
|
+
|
|
63
|
+
## [0.2.2] — 2026-09-01
|
|
64
|
+
|
|
65
|
+
A responsive footer, a convention-based way to swap the site logo
|
|
66
|
+
and favicon without touching code, and a housecleaning rename of the
|
|
67
|
+
public-facing brand assets to drop the redundant `qtype-` prefix.
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
- **Custom site logo via `.qtype/assets/logo.{svg,png}`.** `qtype repo
|
|
71
|
+
new` seeds `<repo>/.qtype/assets/logo.svg` from the language template,
|
|
72
|
+
and `qtype publish build` copies that file over the default
|
|
73
|
+
`logo.<ext>` in `dist/` when it's present. Users edit the file in
|
|
74
|
+
place (keeping the filename `logo.<ext>`) — no config key, no CLI
|
|
75
|
+
flag. Missing file → the built-in default keeps working. Used by the
|
|
76
|
+
Header brand mark; the browser tab now reads a separate `favicon`
|
|
77
|
+
(see below).
|
|
78
|
+
- **Custom favicon via `.qtype/assets/favicon.{svg,png,ico}`.** Same
|
|
79
|
+
convention as the logo — drop a file at this path, `publish build`
|
|
80
|
+
ships it as `dist/favicon.<ext>` and rewrites `index.html`'s
|
|
81
|
+
`<link rel="icon">` to point at the resolved format. Separating
|
|
82
|
+
favicon from logo lets users keep a wide Header brand mark while
|
|
83
|
+
still having a square browser-tab icon.
|
|
84
|
+
- **`data-qtype-logo-ext` on `<html>`** — new boot attribute so the
|
|
85
|
+
Header's runtime `<img>` can point at `/logo.<ext>` with the right
|
|
86
|
+
extension after a build.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- **Public brand assets renamed** to drop the `qtype-` prefix: files
|
|
90
|
+
in `src/view/public/` are now `logo.svg`, `favicon.svg`,
|
|
91
|
+
`wordmark.svg`; the built site emits `dist/logo.<ext>`,
|
|
92
|
+
`dist/favicon.<ext>`, and `dist/wordmark.svg` (unused for now, kept
|
|
93
|
+
around for future brand surfaces). Any external consumer referencing
|
|
94
|
+
`qtype-mark.svg` / `qtype-logo.svg` (the old file names) in
|
|
95
|
+
hard-coded URLs will need to update — but the runtime `<link rel="icon">`
|
|
96
|
+
and Header `<img>` are both regenerated on build so a fresh
|
|
97
|
+
`publish build` in an existing repo is enough.
|
|
98
|
+
- **Footer material name is now responsive.** The left-side title
|
|
99
|
+
truncates with an ellipsis on all viewports (max width 240px on
|
|
100
|
+
desktop, 80px on narrow screens, hidden below 400px) so an unusually
|
|
101
|
+
long material title can't crowd the absolute-centered CardNav or
|
|
102
|
+
push the theme/settings buttons around.
|
|
103
|
+
- **Footer layout wraps the title in an inner span**, which lets the
|
|
104
|
+
outer flex slot keep claiming the remaining space (so the right-side
|
|
105
|
+
buttons stay pinned to the edge) while the inner span alone handles
|
|
106
|
+
the ellipsis. Fixes a mobile-only regression where the settings /
|
|
107
|
+
theme buttons drifted next to the title instead of hugging the right
|
|
108
|
+
edge.
|
|
109
|
+
|
|
16
110
|
## [0.2.1] — 2026-09-01
|
|
17
111
|
|
|
18
112
|
Discoverability pass for `qtype config`. Users no longer have to grep
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -12,6 +12,78 @@ English: [CHANGELOG.md](https://www.npmjs.com/package/@qnroa/qtype?activeTab=cod
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.2.3] — 2026-09-07
|
|
16
|
+
|
|
17
|
+
修复打字界面在中文输入法下的一系列老问题,并把"忽略标点"的行为
|
|
18
|
+
理顺。隐藏的输入编辑器从 contenteditable 换成 `<input>`,和主流
|
|
19
|
+
中文打字站的做法一致;字符捕获改为在 `input` 事件里读 `el.value`,
|
|
20
|
+
IME 提交后的最终字符直接进引擎 —— 不再有 keydown 和 beforeinput
|
|
21
|
+
的竞态。
|
|
22
|
+
|
|
23
|
+
### 新增
|
|
24
|
+
- **错误显示设置**(外观 → 错误显示)。默认保持目标字符可见(红色),
|
|
25
|
+
和 Monkeytype / keybr / ohMyType / ptype 一致 —— 用户看到的是
|
|
26
|
+
"本来应该是什么"。切到"实际输入"则在错误位置显示用户实际敲入
|
|
27
|
+
的字符(仍然红色),方便复盘到底敲错在哪。
|
|
28
|
+
|
|
29
|
+
### 变更
|
|
30
|
+
- **编辑器元素从 `<div contentEditable>` 改为隐藏 `<input>`。** IME
|
|
31
|
+
提交和 macOS 智能标点替换都会把最终字符写进 `el.value`,由
|
|
32
|
+
`input` 事件读到再喂给引擎。组词状态(拼音预输入)靠事件自身的
|
|
33
|
+
`isComposing` / `inputType === 'insertCompositionText'` 判断,
|
|
34
|
+
不再依赖 hook 状态机的时序;这样即便浏览器先 `input` 后
|
|
35
|
+
`compositionstart`,预输入的拼音字母也不会以字符形式泄漏出去。
|
|
36
|
+
- **`忽略标点` 改为在引擎构造时把标点重写成空格。** 之前是引擎在
|
|
37
|
+
`onKeyDown` 里 fast-forward 跳过标点位置,但界面上标点还在显示,
|
|
38
|
+
键盘还在提示打标点键。现在所有标点 codepoint 上来就变空格 ——
|
|
39
|
+
arena、键盘提示、引擎看到的目标序列完全一致,不会跑偏。中途切换
|
|
40
|
+
设置时通过 `useTypingEngine` 的 memo 重建引擎。
|
|
41
|
+
|
|
42
|
+
### 修复
|
|
43
|
+
- **中文全角标点显示成 ASCII。** 老的 keydown 捕获路径会挑
|
|
44
|
+
`keydown.key` 或 `beforeinput.data` 里先来的那个,可能替换成半角。
|
|
45
|
+
改用 `<input>` + `input` 事件、并默认在打错位置显示目标字符,
|
|
46
|
+
这类问题一次性解决。
|
|
47
|
+
- **删除死代码 `softInsertText`。** `INPUT_TYPE_HANDLERS` 和
|
|
48
|
+
`handleBeforeInput` 里的 `insertText` 特殊分支一起删掉,字符发射
|
|
49
|
+
只由 `input` 事件负责。
|
|
50
|
+
|
|
51
|
+
## [0.2.2] — 2026-09-01
|
|
52
|
+
|
|
53
|
+
响应式页脚,约定式自定义 logo 和 favicon(不用改代码),以及把 view
|
|
54
|
+
的公用品牌资源改名去掉 `qtype-` 前缀。
|
|
55
|
+
|
|
56
|
+
### 新增
|
|
57
|
+
- **通过 `.qtype/assets/logo.{svg,png}` 自定义站点 logo。** `qtype repo
|
|
58
|
+
new` 会从语言模板复制 `logo.svg` 到 `<repo>/.qtype/assets/`;
|
|
59
|
+
`publish build` 时若该文件存在,就用它覆盖 dist 里的 `logo.<ext>`。
|
|
60
|
+
用户直接编辑该文件(保留文件名 `logo.<ext>`) —— 不用 config key,
|
|
61
|
+
不用 CLI 参数,文件不存在就用内置默认。用于 Header 品牌图标;浏
|
|
62
|
+
览器 tab 用独立的 favicon(见下)。
|
|
63
|
+
- **通过 `.qtype/assets/favicon.{svg,png,ico}` 自定义浏览器 tab 图标。**
|
|
64
|
+
和 logo 同样的约定式覆盖机制,`publish build` 会把用户的 favicon
|
|
65
|
+
复制成 `dist/favicon.<ext>` 并重写 `index.html` 里的
|
|
66
|
+
`<link rel="icon">`。favicon 和 logo 分开是为了让用户可以放宽横
|
|
67
|
+
的 logo,同时保留方形的 tab 图标。
|
|
68
|
+
- **`<html data-qtype-logo-ext>` 新 boot 属性** —— 让 Header 的
|
|
69
|
+
`<img src="/logo.<ext>">` 在运行时挑对扩展名。
|
|
70
|
+
|
|
71
|
+
### 变更
|
|
72
|
+
- **view 的公用品牌资源改名去掉 `qtype-` 前缀。** `src/view/public/`
|
|
73
|
+
下现在叫 `logo.svg`、`favicon.svg`、`wordmark.svg`;build 出来的
|
|
74
|
+
站点里也是 `dist/logo.<ext>`、`dist/favicon.<ext>` 和
|
|
75
|
+
`dist/wordmark.svg`(暂时没被引用,保留给以后用)。外部如果硬编码
|
|
76
|
+
引用了旧文件名(`qtype-mark.svg` / `qtype-logo.svg`)需要更新
|
|
77
|
+
—— 但运行时 `<link rel="icon">` 和 Header 的 `<img>` 都是构建时
|
|
78
|
+
重新生成的,已有 repo 只要跑一次 `publish build` 就同步了。
|
|
79
|
+
- **页脚材料名做了响应式截断。** 左边的材料标题在所有断点上都会
|
|
80
|
+
ellipsis(桌面 240px 上限,窄屏 80px,超窄 400px 以下隐藏),避免过
|
|
81
|
+
长的材料标题挤占 CardNav 绝对居中区域或推走右侧的主题 / 设置按钮。
|
|
82
|
+
- **标题外面包了一层 inner span**,让外层 flex 槽继续占满剩余空间
|
|
83
|
+
(右侧按钮才能贴到最右边),ellipsis 只作用在 inner span 上。修复
|
|
84
|
+
了窄屏上出现的一个视觉回归 —— 设置 / 主题按钮会飘到标题旁边,而
|
|
85
|
+
不是紧贴右侧。
|
|
86
|
+
|
|
15
87
|
## [0.2.1] — 2026-09-01
|
|
16
88
|
|
|
17
89
|
`qtype config` 的可发现性升级。用户不再需要翻源码或 wiki 才知道能设
|
|
@@ -80,11 +80,20 @@ export async function publishBuildAction(opts = {}) {
|
|
|
80
80
|
fs.rmSync(outDir, { recursive: true, force: true });
|
|
81
81
|
fs.mkdirSync(outDir, { recursive: true });
|
|
82
82
|
copyDir(viewBundle, outDir);
|
|
83
|
+
// Optional brand overrides: user may drop custom assets under
|
|
84
|
+
// `<repo>/.qtype/assets/`:
|
|
85
|
+
// - logo.{svg,png} → Header brand mark (dist/logo.<ext>)
|
|
86
|
+
// - favicon.{svg,png,ico} → browser tab (dist/favicon.<ext>)
|
|
87
|
+
// Whichever the user provides overrides the view bundle default;
|
|
88
|
+
// the losing sibling extensions are removed so we don't ship stale
|
|
89
|
+
// duplicates.
|
|
90
|
+
const logoExt = resolveCustomAsset(cwd, outDir, 'logo', ['svg', 'png']);
|
|
91
|
+
const faviconExt = resolveCustomAsset(cwd, outDir, 'favicon', ['svg', 'png', 'ico']);
|
|
83
92
|
// Rewrite dist/index.html to carry the site's name — both in the
|
|
84
93
|
// <title> (browser tab) and as a `data-qtype-repo-name` attribute on
|
|
85
94
|
// <html> (read by the view's `readBootConfig()` for the header
|
|
86
95
|
// brand label and anywhere else the repo name is shown).
|
|
87
|
-
rewriteIndexHtml(path.join(outDir, 'index.html'), name);
|
|
96
|
+
rewriteIndexHtml(path.join(outDir, 'index.html'), name, logoExt, faviconExt);
|
|
88
97
|
// Defensive: clear any stale material/ that might have snuck into the
|
|
89
98
|
// view bundle. Since dev-seed content lives at repo root's examples/
|
|
90
99
|
// (served only by the vite dev middleware, not the bundle), this is a
|
|
@@ -304,7 +313,43 @@ async function resolveRepoName(cwd) {
|
|
|
304
313
|
}
|
|
305
314
|
return path.basename(cwd);
|
|
306
315
|
}
|
|
307
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Resolve a user-provided brand asset (`logo` or `favicon`) by walking
|
|
318
|
+
* the accepted extensions in preference order and copying the first
|
|
319
|
+
* hit to `dist/<name>.<ext>`. Losing sibling extensions in the
|
|
320
|
+
* output are cleaned up so the site never carries stale defaults
|
|
321
|
+
* alongside the user's override. Returns the winning extension so
|
|
322
|
+
* downstream code (index.html rewrite, Header runtime) can build the
|
|
323
|
+
* right URL. Missing files → keep the view bundle's default (svg).
|
|
324
|
+
*/
|
|
325
|
+
function resolveCustomAsset(cwd, outDir, name, order) {
|
|
326
|
+
const candidates = order.map((ext) => ({
|
|
327
|
+
ext,
|
|
328
|
+
src: path.join(cwd, '.qtype', 'assets', `${name}.${ext}`),
|
|
329
|
+
}));
|
|
330
|
+
for (const c of candidates) {
|
|
331
|
+
if (!fs.existsSync(c.src))
|
|
332
|
+
continue;
|
|
333
|
+
fs.copyFileSync(c.src, path.join(outDir, `${name}.${c.ext}`));
|
|
334
|
+
for (const other of candidates) {
|
|
335
|
+
if (other.ext === c.ext)
|
|
336
|
+
continue;
|
|
337
|
+
const stale = path.join(outDir, `${name}.${other.ext}`);
|
|
338
|
+
if (fs.existsSync(stale))
|
|
339
|
+
fs.rmSync(stale, { force: true });
|
|
340
|
+
}
|
|
341
|
+
return c.ext;
|
|
342
|
+
}
|
|
343
|
+
return 'svg';
|
|
344
|
+
}
|
|
345
|
+
function extToMime(ext) {
|
|
346
|
+
if (ext === 'png')
|
|
347
|
+
return 'image/png';
|
|
348
|
+
if (ext === 'ico')
|
|
349
|
+
return 'image/x-icon';
|
|
350
|
+
return 'image/svg+xml';
|
|
351
|
+
}
|
|
352
|
+
function rewriteIndexHtml(indexPath, name, logoExt, faviconExt) {
|
|
308
353
|
if (!fs.existsSync(indexPath))
|
|
309
354
|
return;
|
|
310
355
|
const html = fs.readFileSync(indexPath, 'utf8');
|
|
@@ -314,13 +359,22 @@ function rewriteIndexHtml(indexPath, name) {
|
|
|
314
359
|
.replace(/>/g, '>')
|
|
315
360
|
.replace(/"/g, '"');
|
|
316
361
|
const withTitle = html.replace(/<title>[^<]*<\/title>/, `<title>${escaped}</title>`);
|
|
317
|
-
// Add or replace data-qtype-repo-name on the
|
|
318
|
-
// Two branches so a re-run cleanly
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
362
|
+
// Add or replace data-qtype-repo-name / data-qtype-logo-ext on the
|
|
363
|
+
// root <html> tag. Two branches per attribute so a re-run cleanly
|
|
364
|
+
// overwrites the existing value.
|
|
365
|
+
let out = withTitle;
|
|
366
|
+
out = /<html\b[^>]*\bdata-qtype-repo-name=/.test(out)
|
|
367
|
+
? out.replace(/(<html\b[^>]*\bdata-qtype-repo-name=)"[^"]*"/, `$1"${escaped}"`)
|
|
368
|
+
: out.replace(/<html\b/, `<html data-qtype-repo-name="${escaped}"`);
|
|
369
|
+
out = /<html\b[^>]*\bdata-qtype-logo-ext=/.test(out)
|
|
370
|
+
? out.replace(/(<html\b[^>]*\bdata-qtype-logo-ext=)"[^"]*"/, `$1"${logoExt}"`)
|
|
371
|
+
: out.replace(/<html\b/, `<html data-qtype-logo-ext="${logoExt}"`);
|
|
372
|
+
// Rewrite the favicon <link> to point at the resolved favicon file
|
|
373
|
+
// (SVG / PNG / ICO). Kept independent of the logo so the Header
|
|
374
|
+
// and the browser tab can carry different aspect ratios.
|
|
375
|
+
out = out.replace(/<link\s+rel="icon"[^>]*>/, `<link rel="icon" type="${extToMime(faviconExt)}" href="/favicon.${faviconExt}" />`);
|
|
376
|
+
if (out !== html)
|
|
377
|
+
fs.writeFileSync(indexPath, out);
|
|
324
378
|
}
|
|
325
379
|
async function obtainPassword() {
|
|
326
380
|
const envPwd = process.env.QTYPE_PUBLISH_PASSWORD;
|
|
@@ -93,6 +93,20 @@ export async function repoNewAction(opts) {
|
|
|
93
93
|
fs.mkdirSync(qtypeDir, { recursive: true });
|
|
94
94
|
fs.writeFileSync(path.join(qtypeDir, 'config.json'), JSON.stringify({ repo: { lang: canonicalLang } }, null, 2) + '\n');
|
|
95
95
|
console.log(` + .qtype/config.json`);
|
|
96
|
+
// 5b. Seed brand assets from the language template so users can
|
|
97
|
+
// rebrand by editing files in place. `publish build` picks them
|
|
98
|
+
// up over the defaults that ship in the view bundle.
|
|
99
|
+
// - logo.svg → Header brand mark
|
|
100
|
+
// - favicon.svg → browser tab icon
|
|
101
|
+
for (const rel of ['logo.svg', 'favicon.svg']) {
|
|
102
|
+
const src = path.join(templatesDir, '.qtype', 'assets', rel);
|
|
103
|
+
if (!fs.existsSync(src))
|
|
104
|
+
continue;
|
|
105
|
+
const dest = path.join(repoDir, '.qtype', 'assets', rel);
|
|
106
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
107
|
+
fs.copyFileSync(src, dest);
|
|
108
|
+
console.log(` + .qtype/assets/${rel}`);
|
|
109
|
+
}
|
|
96
110
|
// 6. git init (optional; requires explicit --git)
|
|
97
111
|
if (opts.git === true) {
|
|
98
112
|
try {
|
|
@@ -31,10 +31,28 @@ export class TypingInput {
|
|
|
31
31
|
#position;
|
|
32
32
|
#listeners = new Set();
|
|
33
33
|
constructor(text, settings) {
|
|
34
|
-
this.#target = text;
|
|
35
|
-
this.#targetCodepoints = Array.from(text, (ch) => ch.codePointAt(0) ?? 0);
|
|
36
|
-
this.#length = this.#targetCodepoints.length;
|
|
37
34
|
this.settings = { ...defaultTypingSettings, ...settings };
|
|
35
|
+
// ignorePunctuation replaces every punctuation codepoint with a
|
|
36
|
+
// regular space in the target sequence. The engine, the arena
|
|
37
|
+
// renderer, and the keyboard hint all just see a space — no
|
|
38
|
+
// fast-forward machinery, no special-case display, no divergence
|
|
39
|
+
// between what the user sees and what they need to type.
|
|
40
|
+
const raw = Array.from(text, (ch) => ch.codePointAt(0) ?? 0);
|
|
41
|
+
this.#targetCodepoints = this.settings.ignorePunctuation
|
|
42
|
+
? raw.map((cp) => (isPunctuation(cp) ? 0x20 : cp))
|
|
43
|
+
: raw;
|
|
44
|
+
// The effective target string: rewritten if ignorePunctuation is
|
|
45
|
+
// on, else identical to `text`. Anything that consumes
|
|
46
|
+
// `engine.target` (keyboard-hint plans, mini-arena mirrors, etc.)
|
|
47
|
+
// sees exactly the sequence the engine expects the user to type.
|
|
48
|
+
// We avoid `String.fromCodePoint(...arr)` here — spreading a large
|
|
49
|
+
// codepoint array can blow the JS engine's argument-count ceiling
|
|
50
|
+
// on unusually long targets. Building char-by-char is O(n) with no
|
|
51
|
+
// fixed upper bound.
|
|
52
|
+
this.#target = this.settings.ignorePunctuation
|
|
53
|
+
? this.#targetCodepoints.map((cp) => String.fromCodePoint(cp)).join('')
|
|
54
|
+
: text;
|
|
55
|
+
this.#length = this.#targetCodepoints.length;
|
|
38
56
|
this.#chars = this.#buildInitialChars();
|
|
39
57
|
this.#position = 0;
|
|
40
58
|
}
|
|
@@ -60,20 +78,7 @@ export class TypingInput {
|
|
|
60
78
|
if (this.#position >= this.#length) {
|
|
61
79
|
return Feedback.Correct;
|
|
62
80
|
}
|
|
63
|
-
|
|
64
|
-
// ignorePunctuation: fast-forward through any punctuation slots
|
|
65
|
-
// before matching the user's keystroke against the next
|
|
66
|
-
// typable character. Uses a broad Unicode range so it works
|
|
67
|
-
// for both ASCII and CJK punctuation.
|
|
68
|
-
if (this.settings.ignorePunctuation) {
|
|
69
|
-
while (this.#position < this.#length &&
|
|
70
|
-
isPunctuation(this.#targetCodepoints[this.#position])) {
|
|
71
|
-
this.#autoAdvance(timestamp);
|
|
72
|
-
}
|
|
73
|
-
if (this.#position >= this.#length)
|
|
74
|
-
return Feedback.Correct;
|
|
75
|
-
expected = this.#targetCodepoints[this.#position];
|
|
76
|
-
}
|
|
81
|
+
const expected = this.#targetCodepoints[this.#position];
|
|
77
82
|
// Normalize both sides through the same equivalence table so
|
|
78
83
|
// fullwidth/half-width / smart-quote / CJK-punctuation variants
|
|
79
84
|
// don't cause false misses. Display remains untouched.
|
|
@@ -195,28 +200,6 @@ export class TypingInput {
|
|
|
195
200
|
for (const l of this.#listeners)
|
|
196
201
|
l();
|
|
197
202
|
}
|
|
198
|
-
/**
|
|
199
|
-
* Mark the current slot as auto-advanced Hit (used by
|
|
200
|
-
* `ignorePunctuation`). Records the timestamp so stats include the
|
|
201
|
-
* auto-advance in `startedAt` timing, but doesn't count it toward
|
|
202
|
-
* user keystroke totals (still shows as Hit — the punctuation was
|
|
203
|
-
* "correctly typed" via the auto-advance rule).
|
|
204
|
-
*/
|
|
205
|
-
#autoAdvance(timestamp) {
|
|
206
|
-
if (this.#position >= this.#length)
|
|
207
|
-
return;
|
|
208
|
-
const expected = this.#targetCodepoints[this.#position];
|
|
209
|
-
const next = this.#chars.slice();
|
|
210
|
-
const prev = next[this.#position];
|
|
211
|
-
next[this.#position] = {
|
|
212
|
-
target: expected,
|
|
213
|
-
typed: expected,
|
|
214
|
-
attrs: (prev.attrs & ~(CharAttr.Miss | CharAttr.Cursor)) | CharAttr.Hit,
|
|
215
|
-
timestamp,
|
|
216
|
-
};
|
|
217
|
-
this.#chars = next;
|
|
218
|
-
this.#position++;
|
|
219
|
-
}
|
|
220
203
|
}
|
|
221
204
|
/**
|
|
222
205
|
* ASCII case-insensitive comparison. Non-ASCII (CJK, accented Latin,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-
|
|
2
|
-
import{r as l,_ as h,u as g,j as f,C as p,M as m,T as y,a as w}from"./index-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-Cj70GP_V.js","assets/index-CeFBpbIw.js","assets/index-VPadwWzO.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{r as l,_ as h,u as g,j as f,C as p,M as m,T as y,a as w}from"./index-CeFBpbIw.js";let o=null,c=null;async function L(){return o||c||(c=h(()=>import("./index-Cj70GP_V.js"),__vite__mapDeps([0,1,2])).then(async e=>(o=await e.createHighlighter({themes:["github-dark","github-light"],langs:[]}),o)),c)}async function _(e,t){if(!e.getLoadedLanguages().includes(t))try{await e.loadLanguage(t)}catch{}}function x(e){const[t,n]=l.useState(o);return l.useEffect(()=>{if(!e)return;let r=!1;return(async()=>{const s=await L();await _(s,e),r||n(s)})(),()=>{r=!0}},[e]),t}const A=/```(\w+)/;function E(e){const t=e.toLowerCase();return t.includes("typescript")||t.endsWith(".ts")?"typescript":t.includes("python")||t.endsWith(".py")?"python":t.includes("cpp")||t.includes("c++")?"cpp":t.includes("java")&&!t.includes("script")?"java":t.includes("go")?"go":t.includes("rust")||t.endsWith(".rs")?"rust":t.includes("code-c")||t.endsWith(".c")?"c":"javascript"}function j(e,t){if(e){const n=e.match(A);if(n)return n[1]}return E(t)}function k(e,t,n,r){if(!r||!r.getLoadedLanguages().includes(t))return new Array(Array.from(e).length).fill(null);try{const s=r.codeToTokens(e,{lang:t,theme:n}),i=[];for(const u of s.tokens){for(const a of u)for(const d of a.content)i.push(a.color??null);i.push(null)}return!e.endsWith(`
|
|
3
3
|
`)&&i.length>Array.from(e).length&&i.pop(),i}catch{return new Array(Array.from(e).length).fill(null)}}function v(e){var d;const{material:t,card:n}=e,[r]=g(),s=l.useMemo(()=>r?j(n.question,t.filePath):"",[n.question,t.filePath,r]),i=typeof document<"u"&&((d=document.documentElement.getAttribute("data-theme"))!=null&&d.includes("latte"))?"github-light":"github-dark",u=x(r?s:""),a=l.useMemo(()=>r?k(n.answer,s,i,u):void 0,[n.answer,s,i,u,r]);return f.jsx(p,{type:"code",variant:"flow",slots:[{role:"title",content:n.title},n.question?{role:"question",content:f.jsx(m,{source:n.question})}:{role:"question",content:null},{role:"arena",content:f.jsx(y,{...w(e),charColors:a},n.id)}]})}export{v as CodeCard};
|