From ca40dc544e1734ea1da6984e4d06d75ed40d11d9 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 26 Oct 2026 19:10:55 +0800 Subject: [PATCH] fix(ui-primitives): end bare URLs before astral symbols and add Web snapshot --- .../web/tests/autolink-cjk-punctuation.e2e.ts | 50 +++++++ apps/web/tsconfig.json | 1 + .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 1 + packages/client/ui-primitives/README.zh.md | 1 + .../links-and-autolinks.settled.txt | 10 ++ .../links-and-autolinks.streaming.txt | 10 ++ .../markdown-autolink-literals.client.spec.ts | 9 ++ .../tests/markdown-dom-parity.client.spec.tsx | 2 + ...dast-util-gfm-autolink-literal@2.0.1.patch | 6 +- ...extension-gfm-autolink-literal@2.1.0.patch | 132 ++++++++++++++++-- pnpm-lock.yaml | 12 +- .../reply.expected.md | 5 + .../autolink-cjk-punctuation/session.v4.jsonl | 10 ++ .../web/autolink-cjk-punctuation/snapshot.yml | 7 + tsconfig.host.json | 1 + 16 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 apps/web/tests/autolink-cjk-punctuation.e2e.ts create mode 100644 snapshots/web/autolink-cjk-punctuation/reply.expected.md create mode 100644 snapshots/web/autolink-cjk-punctuation/session.v4.jsonl create mode 100644 snapshots/web/autolink-cjk-punctuation/snapshot.yml diff --git a/apps/web/tests/autolink-cjk-punctuation.e2e.ts b/apps/web/tests/autolink-cjk-punctuation.e2e.ts new file mode 100644 index 0000000000..a9e4e37de9 --- /dev/null +++ b/apps/web/tests/autolink-cjk-punctuation.e2e.ts @@ -0,0 +1,50 @@ +/** A bare URL in a settled CJK reply ends before the following full-width punctuation. */ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium, type Browser, type Page } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { + assertFixtureInventory, compareOrRefreshGolden, + launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/autolink-cjk-punctuation', import.meta.url)) +const MODE = webSnapshotMode() +const URL_TEXT = 'http://127.0.0.1:8790/' + +describe('web e2e: bare URL before CJK punctuation', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + await seedSession(scaffold, await readFile(join(SNAPSHOT_DIR, 'session.v4.jsonl'), 'utf8'), 'autolink-cjk-punctuation') + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.locator('[role="treeitem"]').first().click() + await page.locator('[role="treeitem"]').nth(1).click() + await page.getByRole('link', { name: URL_TEXT, exact: true }).waitFor() + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('links only the URL and renders the following sentence as text', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-autolink-cjk-punctuation')) + const link = page.getByRole('link', { name: URL_TEXT, exact: true }) + expect(await link.getAttribute('href')).toBe(URL_TEXT) + const prose = await link.locator('..').ariaSnapshot() + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'reply.expected.md'), prose, MODE) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + await assertFixtureInventory(SNAPSHOT_DIR, ['session.v4.jsonl', 'reply.expected.md']) + }) +}) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 62e0e67a61..c5855a507e 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -163,6 +163,7 @@ "tests/present.e2e.ts", "tests/present-svg.e2e.ts", "tests/present-after-closing-reply.e2e.ts", + "tests/autolink-cjk-punctuation.e2e.ts", "tests/composer-draft-scroll.e2e.ts", "tests/composer-focus.e2e.ts", "tests/composer-placeholder.e2e.ts", diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 3bf488202a..e3b1049c7d 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -54,8 +54,8 @@ en: ca75c51c89c2c9b0 zh: de54f3467ef3b9be /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work: - en: 9c5bc17e35af9d0b - zh: 4f1a142c7d56cf72 + en: 2f2db9565903b4e6 + zh: ca9d8fed836d857c /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index d15a9c8776..a1285641e2 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -220,6 +220,7 @@ These limits define how the atoms behave at the edges; they are current package - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. - **A long highlighted fence retains its complete token DOM** — streaming avoids re-parsing, re-tokenizing, and reconciling the completed prefix, but it does not discard old colors or virtualize token spans. Final DOM cardinality therefore still follows the fence's token count; nested/container fences and a pathological single long line remain on the general tail path. - **Overlong lines stay uncolored** — a source line of 1,000 or more UTF-16 units renders as one plain run in every highlighted surface, because TextMate scanning cost grows superlinearly with line length and a single generated line can otherwise block the page for seconds. The grammar state skips that line, so a comment or template literal that opens or closes on it mis-colors the following lines. Highlighting still runs synchronously during render, so a file with thousands of ordinary lines costs proportionally more. +- **Bare URLs end before non-ASCII punctuation** — the [tokenizer patch](../../../patches/micromark-extension-gfm-autolink-literal@2.1.0.patch) and the [transform patch](../../../patches/mdast-util-gfm-autolink-literal@2.0.1.patch) end a GFM autolink literal at non-ASCII punctuation, symbols, or whitespace, so a URL with an unencoded character such as `(` or `・` in its path links only up to it; write such a URL as `<…>` or `[…](…)`. A URL followed directly by CJK letters still links them, because paths such as `/wiki/中文` are valid. Both patches are keyed to exact versions: an upgrade of either package must re-apply or retire its patch, otherwise the boundary rule silently disappears. - **The Go struct-field rule is patched locally** — when the Go grammar loads, its repeated one-line struct-field rule is replaced with the rule [worlpaker/go-syntax#25](https://github.com/worlpaker/go-syntax/pull/25) merged, because the vendored rule backtracks exponentially on lines such as a brace-bearing comment after `struct {`. If a dependency upgrade removes that rule, loading the grammar throws and Go code renders as plain text; delete the patch once `@shikijs/langs` ships the upstream rule. - **The fish logo is a redrawn approximation** — its font glyph has no exportable vector geometry in the local design data, so a hand-authored recreation stands in until an exact export path exists. - **`Pill` and `Input` have no design source** — both atoms are self-defined; the sidebar search field and view-tab strip that resemble them are consumer-owned compositions, not these atoms. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index b10aa0135f..c5b9360995 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -220,6 +220,7 @@ kind: "package-library" - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 - **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。 - **超长行不着色**:所有高亮界面中,长度达到 1,000 个 UTF-16 单元的源码行渲染为一段纯文本,因为 TextMate 扫描成本随行长超线性增长,单个生成行否则会阻塞页面数秒。语法状态会跳过该行,因此在该行开始或结束的注释、模板字符串会使后续行着色错误。高亮仍在渲染期间同步执行,因此数千个普通行的文件仍按比例增加开销。 +- **裸 URL 在非 ASCII 标点前结束**:[tokenizer 补丁](../../../patches/micromark-extension-gfm-autolink-literal@2.1.0.patch)和[转换补丁](../../../patches/mdast-util-gfm-autolink-literal@2.0.1.patch)让 GFM 自动链接在非 ASCII 标点、符号或空白处结束,因此路径中含未编码 `(`、`・` 等字符的 URL 只链接到该字符之前;这类 URL 请写成 `<…>` 或 `[…](…)`。URL 后直接跟中日韩文字时仍会并入链接,因为 `/wiki/中文` 这类路径是合法的。两个补丁都绑定精确版本:升级任一依赖时必须重新应用或撤销对应补丁,否则该边界规则会悄然失效。 - **Go 结构体字段规则在本地打了补丁**:Go 语法加载时,重复匹配单行结构体字段的规则会替换为 [worlpaker/go-syntax#25](https://github.com/worlpaker/go-syntax/pull/25) 合入的规则,因为内置规则在 `struct {` 之后含花括号注释等行上会指数级回溯。若依赖升级后该规则消失,加载语法会抛错,Go 代码以纯文本显示;`@shikijs/langs` 发布上游规则后删除此补丁。 - **鱼形标志是重新绘制的近似版本**:它来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。 - **`Pill` 与 `Input` 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。 diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.settled.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.settled.txt index 7efb9bcaaa..0f674a580a 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.settled.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.settled.txt @@ -43,6 +43,16 @@ #text "https://autolink.example.com" #text " literal." +

+ #text "中文 " + +

#text "[spaces encoded](" diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.streaming.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.streaming.txt index 7efb9bcaaa..0f674a580a 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.streaming.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/links-and-autolinks.streaming.txt @@ -43,6 +43,16 @@ #text "https://autolink.example.com" #text " literal." +

+ #text "中文 " + +

#text "[spaces encoded](" diff --git a/packages/client/ui-primitives/tests/markdown-autolink-literals.client.spec.ts b/packages/client/ui-primitives/tests/markdown-autolink-literals.client.spec.ts index eba95b1434..467ac880b1 100644 --- a/packages/client/ui-primitives/tests/markdown-autolink-literals.client.spec.ts +++ b/packages/client/ui-primitives/tests/markdown-autolink-literals.client.spec.ts @@ -23,8 +23,17 @@ describe.each([parseGfm, parseGfmWithMath])('autolink literals in %o', (parse) = const qq = 'https://qm.qq.com/cgi-bin/qm/qr?k=Abc%2BDef%2Fghi%3D%3D&authKey=tok%252Fvalue%253D&noverify=0' expect(phrasing(parse(`加入群聊:${qq},欢迎。`))).toEqual(['加入群聊:', { link: qq }, ',欢迎。']) // After an unbalanced `[`, the literal comes from the text transform instead of the tokenizer. + // Its case-insensitive Unicode regex must keep ASCII `s` and `k`, which fold with `ſ` and `K`. expect(phrasing(parse('[x https://example.com/docs/Kit?s=K。后'))).toEqual( ['[x ', { link: 'https://example.com/docs/Kit?s=K' }, '。后']) + for (const prefix of ['', '[x ']) { + expect(phrasing(parse(`${prefix}https://example.com/a😀下一句`))).toEqual( + [`${prefix}`, { link: 'https://example.com/a' }, '😀下一句'].filter(part => part !== '')) + expect(phrasing(parse(`${prefix}https://example.com/a\u3000下一句`))).toEqual( + [`${prefix}`, { link: 'https://example.com/a' }, '\u3000下一句'].filter(part => part !== '')) + expect(phrasing(parse(`${prefix}https://example.com/a𝒳b ok`))).toEqual( + [`${prefix}`, { link: 'https://example.com/a𝒳b' }, ' ok'].filter(part => part !== '')) + } // Unencoded non-ASCII punctuation inside a URL also ends it. expect(phrasing(parse('https://zh.wikipedia.org/wiki/中文(消歧义)'))).toEqual( [{ link: 'https://zh.wikipedia.org/wiki/中文' }, '(消歧义)']) diff --git a/packages/client/ui-primitives/tests/markdown-dom-parity.client.spec.tsx b/packages/client/ui-primitives/tests/markdown-dom-parity.client.spec.tsx index 00af46ceab..9ff98f5817 100644 --- a/packages/client/ui-primitives/tests/markdown-dom-parity.client.spec.tsx +++ b/packages/client/ui-primitives/tests/markdown-dom-parity.client.spec.tsx @@ -171,6 +171,8 @@ const CORPUS: Record = { '', ' and bare autolink https://autolink.example.com literal.', '', + '中文 https://autolink.example.com/a。后文 ends before CJK punctuation.', + '', '[spaces encoded](https://example.com/a b)', '', '[![linked badge](https://example.com/badge.png)](https://example.com/target) image-only anchors carry no leading glyph.', diff --git a/patches/mdast-util-gfm-autolink-literal@2.0.1.patch b/patches/mdast-util-gfm-autolink-literal@2.0.1.patch index 6d1cd94dda..d984564d23 100644 --- a/patches/mdast-util-gfm-autolink-literal@2.0.1.patch +++ b/patches/mdast-util-gfm-autolink-literal@2.0.1.patch @@ -1,5 +1,5 @@ diff --git a/lib/index.js b/lib/index.js -index c5ca771c24dd914e342f791716a822431ee32b3a..11265b3fe41bb41985f149e2bd8f2fc2b6f85e2e 100644 +index c5ca771c24dd914e342f791716a822431ee32b3a..b6fb7f576043878d53d449f8bb6036566ecf89d2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -131,7 +131,8 @@ function transformGfmAutolinkLiterals(tree) { @@ -7,8 +7,8 @@ index c5ca771c24dd914e342f791716a822431ee32b3a..11265b3fe41bb41985f149e2bd8f2fc2 tree, [ - [/(https?:\/\/|www(?=\.))([-.\w]+)([^ \t\r\n]*)/gi, findUrl], -+ // Non-ASCII punctuation and symbols, such as `。` or `,`, end the path in CJK prose. -+ [/(https?:\/\/|www(?=\.))([-.\w]+)((?:[!-~]|(?![\p{P}\p{S}])[^ \t\r\n])*)/giu, findUrl], ++ // Non-ASCII punctuation, symbols, and whitespace, such as `。`, `,`, or ` `, end the path in CJK prose. ++ [/(https?:\/\/|www(?=\.))([-.\w]+)((?:[!-~]|(?![\p{P}\p{S}\s])[^ \t\r\n])*)/giu, findUrl], [/(?<=^|\s|\p{P}|\p{S})([-.\w+]+)@([-\w]+(?:\.[-\w]+)+)/gu, findEmail] ], {ignore: ['link', 'linkReference']} diff --git a/patches/micromark-extension-gfm-autolink-literal@2.1.0.patch b/patches/micromark-extension-gfm-autolink-literal@2.1.0.patch index be54305c35..097c429b67 100644 --- a/patches/micromark-extension-gfm-autolink-literal@2.1.0.patch +++ b/patches/micromark-extension-gfm-autolink-literal@2.1.0.patch @@ -1,8 +1,23 @@ diff --git a/dev/lib/syntax.js b/dev/lib/syntax.js -index 904e27200ae4980183beda525d33feef70c2e7f7..f05dff0e41b478e856b8419a306dfba163e9a9af 100644 +index 904e27200ae4980183beda525d33feef70c2e7f7..1e05880bc1e7df0e05137811cd182f10872985df 100644 --- a/dev/lib/syntax.js +++ b/dev/lib/syntax.js -@@ -662,7 +662,8 @@ function tokenizePath(effects, ok) { +@@ -16,6 +16,7 @@ const wwwPrefix = {tokenize: tokenizeWwwPrefix, partial: true} + const domain = {tokenize: tokenizeDomain, partial: true} + const path = {tokenize: tokenizePath, partial: true} + const trail = {tokenize: tokenizeTrail, partial: true} ++const astralPunctuation = {tokenize: tokenizeAstralPunctuation, partial: true} + const emailDomainDotTrail = { + tokenize: tokenizeEmailDomainDotTrail, + partial: true +@@ -659,10 +660,15 @@ function tokenizePath(effects, ok) { + return effects.check(trail, ok, pathAtPunctuation)(code) + } + ++ if (highSurrogate(code)) { ++ return effects.check(astralPunctuation, ok, pathAtPunctuation)(code) ++ } ++ if ( code === codes.eof || markdownLineEndingOrSpace(code) || @@ -12,7 +27,7 @@ index 904e27200ae4980183beda525d33feef70c2e7f7..f05dff0e41b478e856b8419a306dfba1 ) { return ok(code) } -@@ -762,7 +763,8 @@ function tokenizeTrail(effects, ok, nok) { +@@ -762,11 +768,16 @@ function tokenizeTrail(effects, ok, nok) { // So is whitespace. code === codes.eof || markdownLineEndingOrSpace(code) || @@ -22,7 +37,15 @@ index 904e27200ae4980183beda525d33feef70c2e7f7..f05dff0e41b478e856b8419a306dfba1 ) { return ok(code) } -@@ -932,6 +934,17 @@ function previousEmail(code) { + ++ if (highSurrogate(code)) { ++ return effects.check(astralPunctuation, ok, nok)(code) ++ } ++ + return nok(code) + } + +@@ -932,6 +943,56 @@ function previousEmail(code) { return !(code === codes.slash || gfmAtext(code)) } @@ -36,24 +59,77 @@ index 904e27200ae4980183beda525d33feef70c2e7f7..f05dff0e41b478e856b8419a306dfba1 +function nonAsciiPunctuation(code) { + return code !== codes.eof && code > 127 && unicodePunctuation(code) +} ++ ++/** ++ * Whether `code` is a UTF-16 high surrogate, which starts a character outside ++ * the BMP such as `😀`. ++ * ++ * @param {Code} code ++ * @returns {boolean} ++ */ ++function highSurrogate(code) { ++ return code !== codes.eof && code >= 0xd800 && code <= 0xdbff ++} ++ ++/** ++ * Check whether a surrogate pair is punctuation or a symbol. ++ * ++ * @this {TokenizeContext} ++ * @type {Tokenizer} ++ */ ++function tokenizeAstralPunctuation(effects, ok, nok) { ++ /** @type {number} */ ++ let high ++ ++ return start ++ ++ /** @type {State} */ ++ function start(code) { ++ high = /** @type {number} */ (code) ++ effects.consume(code) ++ return low ++ } ++ ++ /** @type {State} */ ++ function low(code) { ++ return code !== codes.eof && code >= 0xdc00 && code <= 0xdfff && ++ /[\p{P}\p{S}]/u.test(String.fromCharCode(high, code)) ++ ? ok(code) ++ : nok(code) ++ } ++} + /** * @param {Code} code * @returns {boolean} diff --git a/lib/syntax.js b/lib/syntax.js -index dffbaf15146b34d3b3f634286629ccb148866f4c..00c336f17c389929d58dab156a72d8961a45caf8 100644 +index dffbaf15146b34d3b3f634286629ccb148866f4c..60b1d5d43811b31592728b4539c5f4d43174dd35 100644 --- a/lib/syntax.js +++ b/lib/syntax.js -@@ -584,7 +584,7 @@ function tokenizePath(effects, ok) { +@@ -19,6 +19,10 @@ const trail = { + tokenize: tokenizeTrail, + partial: true + }; ++const astralPunctuation = { ++ tokenize: tokenizeAstralPunctuation, ++ partial: true ++}; + const emailDomainDotTrail = { + tokenize: tokenizeEmailDomainDotTrail, + partial: true +@@ -584,7 +588,10 @@ function tokenizePath(effects, ok) { if (code === 33 || code === 34 || code === 38 || code === 39 || code === 41 || code === 42 || code === 44 || code === 46 || code === 58 || code === 59 || code === 60 || code === 63 || code === 93 || code === 95 || code === 126) { return effects.check(trail, ok, pathAtPunctuation)(code); } - if (code === null || markdownLineEndingOrSpace(code) || unicodeWhitespace(code)) { ++ if (highSurrogate(code)) { ++ return effects.check(astralPunctuation, ok, pathAtPunctuation)(code); ++ } + if (code === null || markdownLineEndingOrSpace(code) || unicodeWhitespace(code) || nonAsciiPunctuation(code)) { return ok(code); } effects.consume(code); -@@ -665,7 +665,7 @@ function tokenizeTrail(effects, ok, nok) { +@@ -665,9 +672,12 @@ function tokenizeTrail(effects, ok, nok) { // `<` is an end. code === 60 || // So is whitespace. @@ -61,8 +137,13 @@ index dffbaf15146b34d3b3f634286629ccb148866f4c..00c336f17c389929d58dab156a72d896 + code === null || markdownLineEndingOrSpace(code) || unicodeWhitespace(code) || nonAsciiPunctuation(code)) { return ok(code); } ++ if (highSurrogate(code)) { ++ return effects.check(astralPunctuation, ok, nok)(code); ++ } return nok(code); -@@ -816,6 +816,17 @@ function previousEmail(code) { + } + +@@ -816,6 +826,52 @@ function previousEmail(code) { return !(code === 47 || gfmAtext(code)); } @@ -76,6 +157,41 @@ index dffbaf15146b34d3b3f634286629ccb148866f4c..00c336f17c389929d58dab156a72d896 +function nonAsciiPunctuation(code) { + return code !== null && code > 127 && unicodePunctuation(code); +} ++ ++/** ++ * Whether `code` is a UTF-16 high surrogate, which starts a character outside ++ * the BMP such as `😀`. ++ * ++ * @param {Code} code ++ * @returns {boolean} ++ */ ++function highSurrogate(code) { ++ return code !== null && code >= 0xd800 && code <= 0xdbff; ++} ++ ++/** ++ * Check whether a surrogate pair is punctuation or a symbol. ++ * ++ * @this {TokenizeContext} ++ * @type {Tokenizer} ++ */ ++function tokenizeAstralPunctuation(effects, ok, nok) { ++ /** @type {number} */ ++ let high; ++ return start; ++ ++ /** @type {State} */ ++ function start(code) { ++ high = /** @type {number} */code; ++ effects.consume(code); ++ return low; ++ } ++ ++ /** @type {State} */ ++ function low(code) { ++ return code !== null && code >= 0xdc00 && code <= 0xdfff && /[\p{P}\p{S}]/u.test(String.fromCharCode(high, code)) ? ok(code) : nok(code); ++ } ++} + /** * @param {Code} code diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96fad1e899..e7f719777f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ patchedDependencies: app-builder-lib@26.17.0: b70dc2a2a78ffabb80a4f14fafc7c103237f2b0a207e1f296fa1c1387ea29348 exceljs@4.4.0: c1bf8ac8f319dab382f08e3a669b715e691d46c703ac5a4163eb383fbedd1d48 lexical@0.49.0: 54c4413db208cf234cb68c6a837f2523d3b319aa17911f1da385af08362e1356 - mdast-util-gfm-autolink-literal@2.0.1: 52891f84fbc268311cc9229fc2447d0a5557010eb3faace21c5e72a086031e46 - micromark-extension-gfm-autolink-literal@2.1.0: d9ea8de22e086b2711ec954c5b76eaf93e7b8503854de02ca557632fa7ccb4d2 + mdast-util-gfm-autolink-literal@2.0.1: 7fa6fc7927128a45c3f148d31faf388806895e2de39c14a144200edbc1e7aaa0 + micromark-extension-gfm-autolink-literal@2.1.0: 906ae7c0cd1f69e7ee308a41bc0542e0085276ac7517e56e96f0ccaf5a89327d node-pty@1.2.0-beta.15: 9d30053266d0f39d3debe894c675867f6782da6bcadbc94a693b3cfb9eff8ed0 importers: @@ -31120,7 +31120,7 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-autolink-literal@2.0.1(patch_hash=52891f84fbc268311cc9229fc2447d0a5557010eb3faace21c5e72a086031e46): + mdast-util-gfm-autolink-literal@2.0.1(patch_hash=7fa6fc7927128a45c3f148d31faf388806895e2de39c14a144200edbc1e7aaa0): dependencies: '@types/mdast': 4.0.4 ccount: 2.0.1 @@ -31168,7 +31168,7 @@ snapshots: mdast-util-gfm@3.1.0(supports-color@9.4.0): dependencies: mdast-util-from-markdown: 2.0.3(supports-color@9.4.0) - mdast-util-gfm-autolink-literal: 2.0.1(patch_hash=52891f84fbc268311cc9229fc2447d0a5557010eb3faace21c5e72a086031e46) + mdast-util-gfm-autolink-literal: 2.0.1(patch_hash=7fa6fc7927128a45c3f148d31faf388806895e2de39c14a144200edbc1e7aaa0) mdast-util-gfm-footnote: 2.1.0(supports-color@9.4.0) mdast-util-gfm-strikethrough: 2.0.0(supports-color@9.4.0) mdast-util-gfm-table: 2.0.0(supports-color@9.4.0) @@ -31272,7 +31272,7 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-autolink-literal@2.1.0(patch_hash=d9ea8de22e086b2711ec954c5b76eaf93e7b8503854de02ca557632fa7ccb4d2): + micromark-extension-gfm-autolink-literal@2.1.0(patch_hash=906ae7c0cd1f69e7ee308a41bc0542e0085276ac7517e56e96f0ccaf5a89327d): dependencies: micromark-util-character: 2.1.1 micromark-util-sanitize-uri: 2.0.1 @@ -31321,7 +31321,7 @@ snapshots: micromark-extension-gfm@3.0.0: dependencies: - micromark-extension-gfm-autolink-literal: 2.1.0(patch_hash=d9ea8de22e086b2711ec954c5b76eaf93e7b8503854de02ca557632fa7ccb4d2) + micromark-extension-gfm-autolink-literal: 2.1.0(patch_hash=906ae7c0cd1f69e7ee308a41bc0542e0085276ac7517e56e96f0ccaf5a89327d) micromark-extension-gfm-footnote: 2.1.0 micromark-extension-gfm-strikethrough: 2.1.0 micromark-extension-gfm-table: 2.1.1 diff --git a/snapshots/web/autolink-cjk-punctuation/reply.expected.md b/snapshots/web/autolink-cjk-punctuation/reply.expected.md new file mode 100644 index 0000000000..9d7793fefd --- /dev/null +++ b/snapshots/web/autolink-cjk-punctuation/reply.expected.md @@ -0,0 +1,5 @@ +- paragraph: + - text: 如果组会要现场演示,工作台还在 + - link "http://127.0.0.1:8790/": + - /url: http://127.0.0.1:8790/ + - text: 。建议先按时间轴彩排一遍;如果超过 18 分钟,优先删 Step 6 和 Step 8 的备用段,不动第 11 节。 diff --git a/snapshots/web/autolink-cjk-punctuation/session.v4.jsonl b/snapshots/web/autolink-cjk-punctuation/session.v4.jsonl new file mode 100644 index 0000000000..d45033814c --- /dev/null +++ b/snapshots/web/autolink-cjk-punctuation/session.v4.jsonl @@ -0,0 +1,10 @@ +{"version":4,"isSeeded":false,"createdAt":0,"cwd":"{{cwd}}","delegationDepth":0,"type":"session","id":"{{session:1}}"} +{"type":"turn/start","data":{"turn":1}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:1}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"How should I prepare the meeting demo?"}],"source":{"kind":"user","rpcId":"{{rpc:1}}"},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Meeting demo","messageSeqs":[3],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"原综述顶部也加了讲稿版入口;完整版仍然保留,方便会后补细节。\n\n如果组会要现场演示,工作台还在 http://127.0.0.1:8790/。建议先按时间轴彩排一遍;如果超过 18 分钟,优先删 Step 6 和 Step 8 的备用段,不动第 11 节。"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"stream":[]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/web/autolink-cjk-punctuation/snapshot.yml b/snapshots/web/autolink-cjk-punctuation/snapshot.yml new file mode 100644 index 0000000000..8d1b58f039 --- /dev/null +++ b/snapshots/web/autolink-cjk-punctuation/snapshot.yml @@ -0,0 +1,7 @@ +version: 1 +scenario: autolink-cjk-punctuation +profile: web +composition: web-default +recording: authored +header: + class: web-default diff --git a/tsconfig.host.json b/tsconfig.host.json index c9d3036a66..a7046b1246 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -155,6 +155,7 @@ "apps/web/tests/present.e2e.ts", "apps/web/tests/present-svg.e2e.ts", "apps/web/tests/present-after-closing-reply.e2e.ts", + "apps/web/tests/autolink-cjk-punctuation.e2e.ts", "apps/web/tests/composer-draft-scroll.e2e.ts", "apps/web/tests/composer-focus.e2e.ts", "apps/web/tests/composer-placeholder.e2e.ts",