@deepseek-ai/dsh-client-ui-primitives 0.1.2-alpha.2 → 0.1.2-alpha.4
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/README.i18n.yaml +2 -2
- package/README.md +5 -2
- package/README.zh.md +5 -2
- package/lib/Button.module.css +1 -1
- package/lib/Input.module.css +1 -1
- package/lib/Menu.module.css +7 -6
- package/lib/Modal.module.css +3 -3
- package/lib/StateDot.module.css +2 -0
- package/lib/TerminalBlock.module.css +1 -1
- package/lib/index.js +404 -57
- package/lib/markdown/JsonBlock.module.css +1 -1
- package/lib/markdown/MarkdownText.module.css +3 -3
- package/lib/types/markdown/CodeBlock.d.ts +4 -3
- package/lib/types/markdown/highlight.d.ts +29 -4
- package/lib/types/markdown/incremental.d.ts +23 -12
- package/lib/types/markdown/useViewportHighlighting.d.ts +11 -0
- package/package.json +1 -7
- package/lib/invariant.js +0 -26
- package/lib/types/invariant.d.ts +0 -16
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 78cb6caec665687feb1c18c65e1afe47c6e01ea1
|
|
6
|
+
README.zh.md: 1450de83f88e4e33be4616ebd1f70588dd19e8d2
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Compose feature UI from these atoms whenever the web client needs a standard con
|
|
|
33
33
|
|
|
34
34
|
### Rendering agent output
|
|
35
35
|
|
|
36
|
-
`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. While a reply streams, it freezes completed blocks and highlights
|
|
36
|
+
`MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. While a reply streams, it freezes completed blocks, advances a top-level open fence by completed lines, and highlights that fence from saved Shiki grammar state. Completed token lines enter fixed-size React groups, so later chunks reconcile only the growing group; an unchanged fence retains that DOM when the final full parse resolves cross-document syntax ([incremental renderer](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md), [streaming fence highlighting](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.md)). `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `MessageText` remains the literal-text primitive for user-authored content.
|
|
37
37
|
|
|
38
38
|
### Localizing copy
|
|
39
39
|
|
|
@@ -63,7 +63,7 @@ The package is one separation: presentational React atoms with zero Cordis and z
|
|
|
63
63
|
|
|
64
64
|
### Streaming markdown
|
|
65
65
|
|
|
66
|
-
While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply. A
|
|
66
|
+
While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply. A final unclosed top-level fence keeps its parsed code node and sends only the last completed line plus the current partial line through the same GFM grammar; a closing fence or ambiguous parse returns to the ordinary tail path. Highlighting likewise resumes from saved Shiki grammar state and publishes only newly completed lines plus the mutable tail. `CodeBlock` seals completed lines into fixed-size React groups, reuses earlier groups, and retains the whole highlighted tree across settlement when code and language are unchanged. The settled full parse still resolves references that crossed the freeze boundary ([incremental renderer](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md), [streaming fence highlighting](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.md)).
|
|
67
67
|
|
|
68
68
|
### Geometry and overflow
|
|
69
69
|
|
|
@@ -103,6 +103,7 @@ None; this package neither assembles nor sends a provider request.
|
|
|
103
103
|
These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap.
|
|
104
104
|
|
|
105
105
|
- **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.
|
|
106
|
+
- **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.
|
|
106
107
|
- **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists.
|
|
107
108
|
- **`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.
|
|
108
109
|
- **No `Active` `StateDot` variant** — the supported states are done, warning, ongoing, and error.
|
|
@@ -118,3 +119,5 @@ These limits define how the atoms behave at the edges; they are current package
|
|
|
118
119
|
None.
|
|
119
120
|
|
|
120
121
|
</details>
|
|
122
|
+
|
|
123
|
+
**Runtime invariant:** No companion is published. Pure props-in React atoms with no Cordis API — no events, no services, no mutable cross-plugin state; rendering contracts are asserted directly by this package's component specs.
|
package/README.zh.md
CHANGED
|
@@ -33,7 +33,7 @@ kind: "package-library"
|
|
|
33
33
|
|
|
34
34
|
### 渲染 agent 输出
|
|
35
35
|
|
|
36
|
-
`MarkdownText` 渲染不可信的 GFM 与 TeX
|
|
36
|
+
`MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。回复流式输出时,它冻结已完成的块、按已完成行推进顶层未闭合 fence,并从保存的 Shiki grammar state 为该 fence 增量高亮。已完成的 token 行进入固定大小的 React 分组,后续分片只 reconcile 正在增长的分组;最终全量解析解决跨文档语法时,未变化的 fence 会保留该 DOM([增量渲染器](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md)、[流式 fence 高亮](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.zh.md))。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`MessageText` 仍是用户创作内容的字面文本原语。
|
|
37
37
|
|
|
38
38
|
### 本地化文案
|
|
39
39
|
|
|
@@ -63,7 +63,7 @@ kind: "package-library"
|
|
|
63
63
|
|
|
64
64
|
### 流式 markdown
|
|
65
65
|
|
|
66
|
-
回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React
|
|
66
|
+
回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复。末尾的顶层未闭合 fence 会保留已解析的 code node,只把最后一个已完成行与当前未完成行交给同一套 GFM grammar;闭合 fence 或有歧义的解析会回到普通尾部路径。高亮同样从保存的 Shiki grammar state 续接,并只发布新完成行与可变尾部。`CodeBlock` 把已完成行封入固定大小的 React 分组、复用更早的分组,并在代码与语言未变化时跨定稿保留整棵高亮树。定稿时的全量解析仍会解析跨过冻结边界的引用([增量渲染器](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md)、[流式 fence 高亮](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.zh.md))。
|
|
67
67
|
|
|
68
68
|
### 几何与溢出
|
|
69
69
|
|
|
@@ -103,6 +103,7 @@ kind: "package-library"
|
|
|
103
103
|
这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。
|
|
104
104
|
|
|
105
105
|
- **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。
|
|
106
|
+
- **长高亮 fence 会保留完整 token DOM**:流式路径避免重新解析、重新 tokenize 和 reconcile 已完成前缀,但不会丢弃旧颜色或虚拟化 token span。因此最终 DOM 数量仍随 fence 的 token 数增长;嵌套/容器内 fence 与病态的单个超长行仍走通用尾部路径。
|
|
106
107
|
- **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
|
|
107
108
|
- **`Pill` 与 `Input` 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。
|
|
108
109
|
- **`StateDot` 没有 `Active` 变体**:支持的状态为 done、warning、ongoing 和 error。
|
|
@@ -118,3 +119,5 @@ kind: "package-library"
|
|
|
118
119
|
无。
|
|
119
120
|
|
|
120
121
|
</details>
|
|
122
|
+
|
|
123
|
+
**运行时不变式:** 不发布伴生入口。这些是纯 props-in React atom,没有 Cordis API、事件、service 或跨插件可变状态;渲染约定由组件测试覆盖。
|
package/lib/Button.module.css
CHANGED
package/lib/Input.module.css
CHANGED
package/lib/Menu.module.css
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
/* Dropdown card (figma MenuDropdown 122:9481 / 419:16920): menu surface,
|
|
7
|
-
* r12,
|
|
7
|
+
* r12, elevation-prominent (hairline stroke in shadow), 4px inset padding. */
|
|
8
8
|
.list,
|
|
9
9
|
.submenu {
|
|
10
10
|
/* min-widths below are the design's outer card widths — include the pad. */
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
display: flex;
|
|
14
14
|
flex-direction: column;
|
|
15
15
|
gap: 0;
|
|
16
|
-
border:
|
|
17
|
-
border-radius:
|
|
16
|
+
border: 0;
|
|
17
|
+
border-radius: 20px;
|
|
18
18
|
background: var(--dsw-specific-menu);
|
|
19
|
-
|
|
19
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
20
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
20
21
|
/* Elevated surface: the scrollbar thumb takes the l2 elevation tokens. The
|
|
21
22
|
declaration sits on the card rather than on `.scrollable .viewport`
|
|
22
23
|
because the elevation is a property of this surface, and the custom
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
flex-direction: column;
|
|
86
87
|
margin-top: 4px;
|
|
87
88
|
padding-top: 4px;
|
|
88
|
-
border-top:
|
|
89
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
.itemWrap {
|
|
@@ -212,7 +213,7 @@
|
|
|
212
213
|
|
|
213
214
|
/* Separator cell (figma 122:9481): py 4 / px 2 around the hairline. */
|
|
214
215
|
.separator {
|
|
215
|
-
height:
|
|
216
|
+
height: 0.5px;
|
|
216
217
|
margin: 4px 2px;
|
|
217
218
|
background: var(--dsw-alias-border-l1);
|
|
218
219
|
}
|
package/lib/Modal.module.css
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
backdrop-filter: var(--dsw-mask-blur);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/* Dialog card: r24,
|
|
21
|
+
/* Dialog card: r24, elevation-prominent, layer-2 fill, pb 24. */
|
|
22
22
|
.dialog {
|
|
23
23
|
position: relative;
|
|
24
24
|
z-index: 1;
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
width: min(380px, 100%);
|
|
29
29
|
padding: 0 0 24px;
|
|
30
30
|
overflow: hidden;
|
|
31
|
-
border:
|
|
31
|
+
border: 0;
|
|
32
32
|
border-radius: 24px;
|
|
33
33
|
background: var(--dsw-alias-bg-layer-2);
|
|
34
|
-
box-shadow: var(--dsw-
|
|
34
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.content {
|
package/lib/StateDot.module.css
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
position: absolute;
|
|
19
19
|
inset: 0;
|
|
20
20
|
border-radius: 50%;
|
|
21
|
+
corner-shape: round;
|
|
21
22
|
background: currentColor;
|
|
22
23
|
opacity: 0.1;
|
|
23
24
|
}
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
position: absolute;
|
|
28
29
|
inset: 20%;
|
|
29
30
|
border-radius: 50%;
|
|
31
|
+
corner-shape: round;
|
|
30
32
|
background: currentColor;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
divider the IN/OUT card draws between its sections. A running card is
|
|
63
63
|
banner-only, so it draws none. */
|
|
64
64
|
.block:not([data-running]) .header {
|
|
65
|
-
border-bottom:
|
|
65
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/* One row per command line. The prompt column is the only element allowed to
|
package/lib/index.js
CHANGED
|
@@ -4014,7 +4014,7 @@ function runState(running, exitCode, signal, labels) {
|
|
|
4014
4014
|
* @param line - the line's styled runs.
|
|
4015
4015
|
* @returns the line's children.
|
|
4016
4016
|
*/
|
|
4017
|
-
function renderLine(line) {
|
|
4017
|
+
function renderLine$1(line) {
|
|
4018
4018
|
return line.map((span, index) => span.style === void 0 ? span.text : jsx("span", {
|
|
4019
4019
|
style: span.style,
|
|
4020
4020
|
children: span.text
|
|
@@ -4094,7 +4094,7 @@ function TerminalBlock({ command, cwd, home, output, exitCode, signal, running =
|
|
|
4094
4094
|
children: [
|
|
4095
4095
|
(capped ? lines.slice(0, headLines) : lines).map((line, index) => jsx("div", {
|
|
4096
4096
|
className: css$15.line,
|
|
4097
|
-
children: renderLine(line)
|
|
4097
|
+
children: renderLine$1(line)
|
|
4098
4098
|
}, index)),
|
|
4099
4099
|
hidden > 0 && jsx("button", {
|
|
4100
4100
|
type: "button",
|
|
@@ -4106,7 +4106,7 @@ function TerminalBlock({ command, cwd, home, output, exitCode, signal, running =
|
|
|
4106
4106
|
}),
|
|
4107
4107
|
capped && lines.slice(lines.length - tailLines).map((line, index) => jsx("div", {
|
|
4108
4108
|
className: css$15.line,
|
|
4109
|
-
children: renderLine(line)
|
|
4109
|
+
children: renderLine$1(line)
|
|
4110
4110
|
}, index))
|
|
4111
4111
|
]
|
|
4112
4112
|
}))]
|
|
@@ -4253,6 +4253,14 @@ const LANG_ALIASES = new Map([
|
|
|
4253
4253
|
["xml", "xml"],
|
|
4254
4254
|
["lua", "lua"]
|
|
4255
4255
|
]);
|
|
4256
|
+
/**
|
|
4257
|
+
* Whether a language hint can use the shared syntax highlighter.
|
|
4258
|
+
* @param lang - Language hint from a code surface.
|
|
4259
|
+
* @returns Whether the hint resolves to a supported grammar.
|
|
4260
|
+
*/
|
|
4261
|
+
function supportsHighlighting(lang) {
|
|
4262
|
+
return lang !== void 0 && LANG_ALIASES.has(lang.toLowerCase());
|
|
4263
|
+
}
|
|
4256
4264
|
/** All token colors resolve through `--shiki-*` custom properties (theme package sheets). */
|
|
4257
4265
|
const cssVariablesTheme = createCssVariablesTheme({
|
|
4258
4266
|
name: "css-variables",
|
|
@@ -4435,10 +4443,11 @@ function lineSpans(line) {
|
|
|
4435
4443
|
* tokenization is line-based and forward-only — a line's tokens depend only on
|
|
4436
4444
|
* its own text and the grammar state entering it — so appended text never
|
|
4437
4445
|
* changes a completed line's tokens. The session caches the spans of every
|
|
4438
|
-
* completed line together with the grammar state after them;
|
|
4439
|
-
* {@link
|
|
4440
|
-
*
|
|
4441
|
-
*
|
|
4446
|
+
* completed line together with the grammar state after them;
|
|
4447
|
+
* {@link updateFrame} reports only newly completed lines plus the still-growing
|
|
4448
|
+
* last line, while {@link update} materializes the complete compatibility
|
|
4449
|
+
* result. Per-call tokenization cost therefore excludes the completed prefix,
|
|
4450
|
+
* and the result equals a from-scratch tokenization of the same code.
|
|
4442
4451
|
* Non-append input and a change of resolved grammar reset the cache and
|
|
4443
4452
|
* re-tokenize fully, so any input stays correct.
|
|
4444
4453
|
*/
|
|
@@ -4454,11 +4463,15 @@ var StreamingHighlightSession = class {
|
|
|
4454
4463
|
lastCode;
|
|
4455
4464
|
lastLang;
|
|
4456
4465
|
lastResult;
|
|
4466
|
+
generation = 0;
|
|
4467
|
+
lastFrame;
|
|
4457
4468
|
reset(resolved) {
|
|
4458
4469
|
this.resolved = resolved;
|
|
4459
4470
|
this.prefix = "";
|
|
4460
4471
|
this.spans = [];
|
|
4461
4472
|
this.state = void 0;
|
|
4473
|
+
this.generation += 1;
|
|
4474
|
+
this.lastFrame = void 0;
|
|
4462
4475
|
}
|
|
4463
4476
|
/** Tokenize `text` with `resolved`, resuming from the cached grammar state when one exists. */
|
|
4464
4477
|
tokenize(resolved, text) {
|
|
@@ -4469,28 +4482,23 @@ var StreamingHighlightSession = class {
|
|
|
4469
4482
|
});
|
|
4470
4483
|
}
|
|
4471
4484
|
/**
|
|
4472
|
-
* Tokenize
|
|
4473
|
-
*
|
|
4474
|
-
*
|
|
4475
|
-
*
|
|
4476
|
-
* a React caller can reuse cached line elements. A lazy grammar not yet
|
|
4477
|
-
* loaded returns `undefined` and loads in the background exactly as
|
|
4478
|
-
* {@link highlightToHtml} does; the next call after it registers highlights.
|
|
4479
|
-
* @param code - the fence text accumulated so far (display-trimmed, no synthetic trailing newline).
|
|
4480
|
-
* @param lang - the language hint (a markdown fence info string).
|
|
4481
|
-
* @returns one entry per line of `code` (each an array of runs), or `undefined` for unknown or not-yet-loaded languages.
|
|
4485
|
+
* Tokenize one update as a delta for a retained renderer.
|
|
4486
|
+
* @param code - the fence text accumulated so far.
|
|
4487
|
+
* @param lang - the language hint.
|
|
4488
|
+
* @returns Newly completed lines plus the current tail, or `undefined` for the plain arm.
|
|
4482
4489
|
*/
|
|
4483
|
-
|
|
4484
|
-
if (code === this.lastCode && lang === this.lastLang && this.
|
|
4490
|
+
updateFrame(code, lang) {
|
|
4491
|
+
if (code === this.lastCode && lang === this.lastLang && this.lastFrame !== void 0) return this.lastFrame;
|
|
4485
4492
|
this.lastCode = code;
|
|
4486
4493
|
this.lastLang = lang;
|
|
4494
|
+
this.lastResult = void 0;
|
|
4487
4495
|
const resolved = lang === void 0 ? void 0 : LANG_ALIASES.get(lang.toLowerCase());
|
|
4488
4496
|
if (resolved === void 0 || !ensureGrammar(resolved)) {
|
|
4489
4497
|
this.reset(void 0);
|
|
4490
|
-
this.lastResult = void 0;
|
|
4491
4498
|
return;
|
|
4492
4499
|
}
|
|
4493
4500
|
if (resolved !== this.resolved || !code.startsWith(this.prefix)) this.reset(resolved);
|
|
4501
|
+
const firstNewLine = this.spans.length;
|
|
4494
4502
|
const rest = code.slice(this.prefix.length);
|
|
4495
4503
|
const lastNewline = rest.lastIndexOf("\n");
|
|
4496
4504
|
if (lastNewline >= 0) {
|
|
@@ -4500,7 +4508,30 @@ var StreamingHighlightSession = class {
|
|
|
4500
4508
|
this.state = highlighter().getLastGrammarState(tokens);
|
|
4501
4509
|
this.prefix = code.slice(0, this.prefix.length + lastNewline + 1);
|
|
4502
4510
|
}
|
|
4503
|
-
this.
|
|
4511
|
+
this.lastFrame = {
|
|
4512
|
+
generation: this.generation,
|
|
4513
|
+
appended: this.spans.slice(firstNewLine),
|
|
4514
|
+
tail: this.tokenize(resolved, rest.slice(lastNewline + 1)).map(lineSpans)
|
|
4515
|
+
};
|
|
4516
|
+
return this.lastFrame;
|
|
4517
|
+
}
|
|
4518
|
+
/**
|
|
4519
|
+
* Tokenize the fence's current text into per-line highlighted runs;
|
|
4520
|
+
* `undefined` means the caller renders its plain fallback. Idempotent per
|
|
4521
|
+
* (`code`, `lang`) input — repeated calls return the identical result array —
|
|
4522
|
+
* and a retained line keeps its span-array identity across growing calls, so
|
|
4523
|
+
* a React caller can reuse cached line elements. A lazy grammar not yet
|
|
4524
|
+
* loaded returns `undefined` and loads in the background exactly as
|
|
4525
|
+
* {@link highlightToHtml} does; the next call after it registers highlights.
|
|
4526
|
+
* @param code - the fence text accumulated so far (display-trimmed, no synthetic trailing newline).
|
|
4527
|
+
* @param lang - the language hint (a markdown fence info string).
|
|
4528
|
+
* @returns one entry per line of `code` (each an array of runs), or `undefined` for unknown or not-yet-loaded languages.
|
|
4529
|
+
*/
|
|
4530
|
+
update(code, lang) {
|
|
4531
|
+
if (code === this.lastCode && lang === this.lastLang && this.lastResult !== void 0) return this.lastResult;
|
|
4532
|
+
const frame = this.updateFrame(code, lang);
|
|
4533
|
+
if (frame === void 0) return void 0;
|
|
4534
|
+
this.lastResult = [...this.spans, ...frame.tail];
|
|
4504
4535
|
return this.lastResult;
|
|
4505
4536
|
}
|
|
4506
4537
|
};
|
|
@@ -4535,6 +4566,73 @@ function highlightLines(code, lang) {
|
|
|
4535
4566
|
})));
|
|
4536
4567
|
}
|
|
4537
4568
|
//#endregion
|
|
4569
|
+
//#region lib/types/markdown/useViewportHighlighting.js
|
|
4570
|
+
const noop = () => {};
|
|
4571
|
+
/** One document-wide observer; activated elements leave it permanently. */
|
|
4572
|
+
var HighlightViewport = class {
|
|
4573
|
+
observer;
|
|
4574
|
+
activators = /* @__PURE__ */ new Map();
|
|
4575
|
+
observe(element, activate) {
|
|
4576
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
4577
|
+
activate();
|
|
4578
|
+
return noop;
|
|
4579
|
+
}
|
|
4580
|
+
this.observer ??= new IntersectionObserver((entries) => {
|
|
4581
|
+
for (const entry of entries) {
|
|
4582
|
+
if (!entry.isIntersecting) continue;
|
|
4583
|
+
const current = this.activators.get(entry.target);
|
|
4584
|
+
/* v8 ignore next -- the observer reports only elements still registered with it. */
|
|
4585
|
+
if (current === void 0) continue;
|
|
4586
|
+
this.activators.delete(entry.target);
|
|
4587
|
+
this.observer?.unobserve(entry.target);
|
|
4588
|
+
current();
|
|
4589
|
+
}
|
|
4590
|
+
this.releaseEmptyObserver();
|
|
4591
|
+
});
|
|
4592
|
+
this.activators.set(element, activate);
|
|
4593
|
+
this.observer.observe(element);
|
|
4594
|
+
return () => {
|
|
4595
|
+
this.activators.delete(element);
|
|
4596
|
+
this.observer?.unobserve(element);
|
|
4597
|
+
this.releaseEmptyObserver();
|
|
4598
|
+
};
|
|
4599
|
+
}
|
|
4600
|
+
releaseEmptyObserver() {
|
|
4601
|
+
if (this.activators.size > 0) return;
|
|
4602
|
+
this.observer?.disconnect();
|
|
4603
|
+
this.observer = void 0;
|
|
4604
|
+
}
|
|
4605
|
+
};
|
|
4606
|
+
const highlightViewport = new HighlightViewport();
|
|
4607
|
+
/**
|
|
4608
|
+
* Activate one supported code surface when it first intersects the viewport.
|
|
4609
|
+
* Activation lasts for the component lifetime; browsers without
|
|
4610
|
+
* IntersectionObserver activate immediately.
|
|
4611
|
+
* @param target - Code surface whose plain rendering reserves its geometry.
|
|
4612
|
+
* @param lang - Optional language hint.
|
|
4613
|
+
* @returns Whether this component may build highlighted output.
|
|
4614
|
+
*/
|
|
4615
|
+
function useViewportHighlighting(target, lang) {
|
|
4616
|
+
const supported = supportsHighlighting(lang);
|
|
4617
|
+
const [activated, setActivated] = useState(false);
|
|
4618
|
+
const activate = useCallback(() => {
|
|
4619
|
+
setActivated(true);
|
|
4620
|
+
}, []);
|
|
4621
|
+
useEffect(() => {
|
|
4622
|
+
if (activated || !supported) return;
|
|
4623
|
+
const element = target.current;
|
|
4624
|
+
/* v8 ignore next -- React attaches the host ref before running effects. */
|
|
4625
|
+
if (element === null) return;
|
|
4626
|
+
return highlightViewport.observe(element, activate);
|
|
4627
|
+
}, [
|
|
4628
|
+
activate,
|
|
4629
|
+
activated,
|
|
4630
|
+
supported,
|
|
4631
|
+
target
|
|
4632
|
+
]);
|
|
4633
|
+
return activated && supported;
|
|
4634
|
+
}
|
|
4635
|
+
//#endregion
|
|
4538
4636
|
//#region lib/types/ReadBlock.js
|
|
4539
4637
|
/**
|
|
4540
4638
|
* Content lines shown before the height cap collapses the middle. Matches
|
|
@@ -4555,8 +4653,11 @@ function renderSpans(spans) {
|
|
|
4555
4653
|
* @returns the read block element.
|
|
4556
4654
|
*/
|
|
4557
4655
|
function ReadBlock({ label, labels, lines, totalLines, lang, maxLines = 16, className }) {
|
|
4656
|
+
const rootRef = useRef(null);
|
|
4657
|
+
const highlighting = useViewportHighlighting(rootRef, lang);
|
|
4558
4658
|
const raw = useMemo(() => lines.map((line) => line.text).join("\n"), [lines]);
|
|
4559
|
-
const highlighted = useMemo(() => highlightLines(raw, lang), [
|
|
4659
|
+
const highlighted = useMemo(() => highlighting ? highlightLines(raw, lang) : void 0, [
|
|
4660
|
+
highlighting,
|
|
4560
4661
|
raw,
|
|
4561
4662
|
lang,
|
|
4562
4663
|
useSyncExternalStore(subscribeGrammarLoaded, grammarLoadCount, grammarLoadCount)
|
|
@@ -4594,6 +4695,7 @@ function ReadBlock({ label, labels, lines, totalLines, lang, maxLines = 16, clas
|
|
|
4594
4695
|
}, line.number));
|
|
4595
4696
|
const paired = lines.map((line, index) => [line, highlighted?.[index]]);
|
|
4596
4697
|
return jsxs("div", {
|
|
4698
|
+
ref: rootRef,
|
|
4597
4699
|
className: clsx(css$16.block, className),
|
|
4598
4700
|
"data-read": "",
|
|
4599
4701
|
children: [jsxs("div", {
|
|
@@ -5009,19 +5111,23 @@ function SearchBlock(props) {
|
|
|
5009
5111
|
* Re-parsing the whole accumulated document on every streaming chunk is
|
|
5010
5112
|
* quadratic in the final reply length. CommonMark block parsing is line-based
|
|
5011
5113
|
* and appended text can only reshape the parse frontier — the last top-level
|
|
5012
|
-
* block (a paragraph becoming a setext heading or a table, a list
|
|
5013
|
-
* after a blank line
|
|
5014
|
-
*
|
|
5015
|
-
*
|
|
5016
|
-
*
|
|
5017
|
-
*
|
|
5114
|
+
* block (a paragraph becoming a setext heading or a table, or a list
|
|
5115
|
+
* continuing after a blank line) — so earlier blocks are final. This parser
|
|
5116
|
+
* therefore freezes all but the trailing {@link UNSTABLE_TAIL_BLOCKS} blocks
|
|
5117
|
+
* and re-parses only the source tail behind them. A final unclosed top-level
|
|
5118
|
+
* fence cannot freeze as a block, so its completed content lines use a second
|
|
5119
|
+
* frontier: only the last completed line and current partial line return
|
|
5120
|
+
* through the caller's grammar. Each source region is therefore parsed a
|
|
5121
|
+
* bounded number of times over the stream instead of once per chunk.
|
|
5018
5122
|
*
|
|
5019
|
-
* The freeze boundary comes from the parser's own `position` offsets
|
|
5020
|
-
*
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
5023
|
-
*
|
|
5024
|
-
*
|
|
5123
|
+
* The block freeze boundary comes from the parser's own `position` offsets.
|
|
5124
|
+
* The cut sits at the *end offset* of the last frozen block (not the next
|
|
5125
|
+
* block's start): a following block's start offset excludes up to three spaces
|
|
5126
|
+
* of insignificant leading indentation, which is harmless to drop, but
|
|
5127
|
+
* cutting at the previous end also keeps the inter-block blank lines in the
|
|
5128
|
+
* tail so the sliced source stays verbatim. Fence scanning only recognizes a
|
|
5129
|
+
* parser-confirmed code node and closing delimiter; ambiguous input returns to
|
|
5130
|
+
* the normal tail parse.
|
|
5025
5131
|
*
|
|
5026
5132
|
* Known deviation, shared with any prefix-freeze scheme: micromark resolves
|
|
5027
5133
|
* reference-style links and footnotes document-wide at parse time, so a
|
|
@@ -5046,6 +5152,84 @@ function blockKey(node, base, index) {
|
|
|
5046
5152
|
const offset = node.position?.start.offset;
|
|
5047
5153
|
return offset === void 0 ? -(index + 1) : base + offset;
|
|
5048
5154
|
}
|
|
5155
|
+
/** Return the first line terminator at or after `start`, including a CRLF pair. */
|
|
5156
|
+
function lineTerminatorEnd(text, start) {
|
|
5157
|
+
for (let index = start; index < text.length; index += 1) {
|
|
5158
|
+
const char = text[index];
|
|
5159
|
+
if (char === "\n") return index + 1;
|
|
5160
|
+
if (char === "\r") return text[index + 1] === "\n" ? index + 2 : index + 1;
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
/**
|
|
5164
|
+
* Source prefix before the last completed line. Keeping that line beside the
|
|
5165
|
+
* current partial line lets the grammar retain its trailing-newline semantics.
|
|
5166
|
+
*/
|
|
5167
|
+
function committableLinePrefixLength(text) {
|
|
5168
|
+
let previousEnd = 0;
|
|
5169
|
+
let end = 0;
|
|
5170
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
5171
|
+
const char = text[index];
|
|
5172
|
+
if (char === "\n") {
|
|
5173
|
+
previousEnd = end;
|
|
5174
|
+
end = index + 1;
|
|
5175
|
+
continue;
|
|
5176
|
+
}
|
|
5177
|
+
if (char !== "\r" || index + 1 >= text.length) continue;
|
|
5178
|
+
if (text[index + 1] === "\n") index += 1;
|
|
5179
|
+
previousEnd = end;
|
|
5180
|
+
end = index + 1;
|
|
5181
|
+
}
|
|
5182
|
+
return previousEnd;
|
|
5183
|
+
}
|
|
5184
|
+
/** Exact source terminator ending a non-empty committable prefix. */
|
|
5185
|
+
function trailingLineTerminator(text) {
|
|
5186
|
+
return text.endsWith("\r\n") ? "\r\n" : text.endsWith("\r") ? "\r" : "\n";
|
|
5187
|
+
}
|
|
5188
|
+
/** Whether `text` contains a CommonMark closing fence on one of its logical lines. */
|
|
5189
|
+
function containsClosingFence(text, marker, markerLength) {
|
|
5190
|
+
let start = 0;
|
|
5191
|
+
while (start <= text.length) {
|
|
5192
|
+
let end = start;
|
|
5193
|
+
while (end < text.length && text[end] !== "\n" && text[end] !== "\r") end += 1;
|
|
5194
|
+
const line = text.slice(start, end);
|
|
5195
|
+
let indent = 0;
|
|
5196
|
+
while (indent < 3 && line[indent] === " ") indent += 1;
|
|
5197
|
+
let run = indent;
|
|
5198
|
+
while (line[run] === marker) run += 1;
|
|
5199
|
+
if (run - indent >= markerLength && /^[ \t]*$/.test(line.slice(run))) return true;
|
|
5200
|
+
if (end === text.length) return false;
|
|
5201
|
+
start = text[end] === "\r" && text[end + 1] === "\n" ? end + 2 : end + 1;
|
|
5202
|
+
}
|
|
5203
|
+
/* v8 ignore next -- each loop iteration returns at EOF or advances past a line terminator. */
|
|
5204
|
+
return false;
|
|
5205
|
+
}
|
|
5206
|
+
/** Advance an mdast point across one append while treating a split CRLF as one line ending. */
|
|
5207
|
+
function advancePoint(point, appended, precededByCarriageReturn) {
|
|
5208
|
+
let line = point.line;
|
|
5209
|
+
let column = point.column;
|
|
5210
|
+
let afterCarriageReturn = precededByCarriageReturn;
|
|
5211
|
+
for (const char of appended) {
|
|
5212
|
+
if (char === "\n") {
|
|
5213
|
+
if (!afterCarriageReturn) line += 1;
|
|
5214
|
+
column = 1;
|
|
5215
|
+
afterCarriageReturn = false;
|
|
5216
|
+
continue;
|
|
5217
|
+
}
|
|
5218
|
+
if (char === "\r") {
|
|
5219
|
+
line += 1;
|
|
5220
|
+
column = 1;
|
|
5221
|
+
afterCarriageReturn = true;
|
|
5222
|
+
continue;
|
|
5223
|
+
}
|
|
5224
|
+
column += 1;
|
|
5225
|
+
afterCarriageReturn = false;
|
|
5226
|
+
}
|
|
5227
|
+
return {
|
|
5228
|
+
line,
|
|
5229
|
+
column,
|
|
5230
|
+
offset: point.offset + appended.length
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5049
5233
|
/**
|
|
5050
5234
|
* Append-only incremental parser over a caller-supplied grammar. One instance
|
|
5051
5235
|
* accumulates one streaming document; non-append input resets it.
|
|
@@ -5057,10 +5241,112 @@ var IncrementalMarkdownParser = class {
|
|
|
5057
5241
|
frozen = [];
|
|
5058
5242
|
generation = 0;
|
|
5059
5243
|
cached = null;
|
|
5244
|
+
openFence = null;
|
|
5060
5245
|
/** @param parse - Grammar shared with whatever renders the blocks, so boundaries agree. */
|
|
5061
5246
|
constructor(parse) {
|
|
5062
5247
|
this.parse = parse;
|
|
5063
5248
|
}
|
|
5249
|
+
/** Parse one unclosed-fence content slice through the caller's grammar. */
|
|
5250
|
+
fenceValue(state, text) {
|
|
5251
|
+
const root = this.parse(`${state.syntheticPrefix}${text}`);
|
|
5252
|
+
if (root.children.length !== 1) return void 0;
|
|
5253
|
+
const node = root.children[0];
|
|
5254
|
+
return node.type === "code" ? node.value : void 0;
|
|
5255
|
+
}
|
|
5256
|
+
/** Recognize the parsed tail's final unclosed fence and prepare its incremental content frontier. */
|
|
5257
|
+
openFenceState(text, base, tail, frozen) {
|
|
5258
|
+
const codeIndex = tail.length - 1;
|
|
5259
|
+
const block = tail[codeIndex];
|
|
5260
|
+
if (block?.node.type !== "code") return null;
|
|
5261
|
+
const node = block.node;
|
|
5262
|
+
const startOffset = node.position?.start.offset;
|
|
5263
|
+
const end = node.position?.end;
|
|
5264
|
+
if (startOffset === void 0 || end?.offset === void 0) return null;
|
|
5265
|
+
/* v8 ignore next -- the caller's parse slice ends at text.length, so its final node ends there. */
|
|
5266
|
+
if (base + end.offset !== text.length) return null;
|
|
5267
|
+
const source = text.slice(base);
|
|
5268
|
+
const previousLf = source.lastIndexOf("\n", startOffset - 1);
|
|
5269
|
+
const previousCr = source.lastIndexOf("\r", startOffset - 1);
|
|
5270
|
+
const lineStart = Math.max(previousLf, previousCr) + 1;
|
|
5271
|
+
const terminatorEnd = lineTerminatorEnd(source, startOffset);
|
|
5272
|
+
/* v8 ignore next -- a parser-confirmed fenced code node requires its opening line terminator. */
|
|
5273
|
+
if (terminatorEnd === void 0) return null;
|
|
5274
|
+
if (terminatorEnd === source.length && source.endsWith("\r")) return null;
|
|
5275
|
+
const openingLine = source.slice(lineStart, terminatorEnd).replace(/[\r\n]+$/, "");
|
|
5276
|
+
const opening = /^( {0,3})(`{3,}|~{3,})/.exec(openingLine);
|
|
5277
|
+
if (opening === null) return null;
|
|
5278
|
+
const indent = opening[1];
|
|
5279
|
+
const run = opening[2];
|
|
5280
|
+
/* v8 ignore next -- mdast positions a fenced code node at the matched delimiter after indentation. */
|
|
5281
|
+
if (lineStart + indent.length !== startOffset) return null;
|
|
5282
|
+
const marker = run[0];
|
|
5283
|
+
const contentStart = base + terminatorEnd;
|
|
5284
|
+
const content = text.slice(contentStart);
|
|
5285
|
+
if (containsClosingFence(content, marker, run.length)) return null;
|
|
5286
|
+
const syntheticPrefix = `${indent}${run}\n`;
|
|
5287
|
+
const stableLength = committableLinePrefixLength(content);
|
|
5288
|
+
const stableValue = stableLength === 0 ? "" : this.fenceValue({ syntheticPrefix }, content.slice(0, stableLength));
|
|
5289
|
+
if (stableValue === void 0) return null;
|
|
5290
|
+
const pendingStart = contentStart + stableLength;
|
|
5291
|
+
const stableSource = content.slice(0, stableLength);
|
|
5292
|
+
const valuePrefix = stableLength === 0 ? "" : `${stableValue}${trailingLineTerminator(stableSource)}`;
|
|
5293
|
+
const pendingValue = this.fenceValue({ syntheticPrefix }, text.slice(pendingStart));
|
|
5294
|
+
if (pendingValue === void 0 || `${valuePrefix}${pendingValue}` !== node.value) return null;
|
|
5295
|
+
return {
|
|
5296
|
+
marker,
|
|
5297
|
+
markerLength: run.length,
|
|
5298
|
+
syntheticPrefix,
|
|
5299
|
+
codeIndex,
|
|
5300
|
+
frozen,
|
|
5301
|
+
tail,
|
|
5302
|
+
pendingStart,
|
|
5303
|
+
valuePrefix,
|
|
5304
|
+
end: {
|
|
5305
|
+
line: end.line,
|
|
5306
|
+
column: end.column,
|
|
5307
|
+
offset: end.offset
|
|
5308
|
+
},
|
|
5309
|
+
endedWithCarriageReturn: text.endsWith("\r")
|
|
5310
|
+
};
|
|
5311
|
+
}
|
|
5312
|
+
/** Extend a recognized unclosed fence without parsing its completed content prefix again. */
|
|
5313
|
+
updateOpenFence(state, text, previousText) {
|
|
5314
|
+
const pending = text.slice(state.pendingStart);
|
|
5315
|
+
if (containsClosingFence(pending, state.marker, state.markerLength)) return void 0;
|
|
5316
|
+
const pendingValue = this.fenceValue(state, pending);
|
|
5317
|
+
if (pendingValue === void 0) return void 0;
|
|
5318
|
+
const stableLength = committableLinePrefixLength(pending);
|
|
5319
|
+
const stableValue = stableLength === 0 ? "" : this.fenceValue(state, pending.slice(0, stableLength));
|
|
5320
|
+
if (stableValue === void 0) return void 0;
|
|
5321
|
+
const previousNode = state.tail[state.codeIndex].node;
|
|
5322
|
+
const end = advancePoint(state.end, text.slice(previousText.length), state.endedWithCarriageReturn);
|
|
5323
|
+
const node = {
|
|
5324
|
+
...previousNode,
|
|
5325
|
+
value: `${state.valuePrefix}${pendingValue}`,
|
|
5326
|
+
position: {
|
|
5327
|
+
start: previousNode.position.start,
|
|
5328
|
+
end
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
5331
|
+
const tail = state.tail.map((entry, index) => index === state.codeIndex ? {
|
|
5332
|
+
...entry,
|
|
5333
|
+
node
|
|
5334
|
+
} : entry);
|
|
5335
|
+
const cached = {
|
|
5336
|
+
frozen: state.frozen,
|
|
5337
|
+
tail,
|
|
5338
|
+
generation: this.generation
|
|
5339
|
+
};
|
|
5340
|
+
this.openFence = {
|
|
5341
|
+
...state,
|
|
5342
|
+
tail,
|
|
5343
|
+
pendingStart: state.pendingStart + stableLength,
|
|
5344
|
+
valuePrefix: stableLength === 0 ? state.valuePrefix : `${state.valuePrefix}${stableValue}${trailingLineTerminator(pending.slice(0, stableLength))}`,
|
|
5345
|
+
end,
|
|
5346
|
+
endedWithCarriageReturn: text.endsWith("\r")
|
|
5347
|
+
};
|
|
5348
|
+
return cached;
|
|
5349
|
+
}
|
|
5064
5350
|
/**
|
|
5065
5351
|
* Fold the current accumulated text and return the frozen/tail split.
|
|
5066
5352
|
* Idempotent for identical input (the previous result is returned as-is),
|
|
@@ -5074,8 +5360,19 @@ var IncrementalMarkdownParser = class {
|
|
|
5074
5360
|
this.prevText = "";
|
|
5075
5361
|
this.tailStart = 0;
|
|
5076
5362
|
this.frozen = [];
|
|
5363
|
+
this.openFence = null;
|
|
5077
5364
|
this.generation += 1;
|
|
5078
5365
|
}
|
|
5366
|
+
const previousText = this.prevText;
|
|
5367
|
+
if (previousText !== "" && this.openFence !== null) {
|
|
5368
|
+
const incremental = this.updateOpenFence(this.openFence, text, previousText);
|
|
5369
|
+
if (incremental !== void 0) {
|
|
5370
|
+
this.prevText = text;
|
|
5371
|
+
this.cached = incremental;
|
|
5372
|
+
return incremental;
|
|
5373
|
+
}
|
|
5374
|
+
this.openFence = null;
|
|
5375
|
+
}
|
|
5079
5376
|
this.prevText = text;
|
|
5080
5377
|
const base = this.tailStart;
|
|
5081
5378
|
const blocks = this.parse(text.slice(base)).children;
|
|
@@ -5100,6 +5397,7 @@ var IncrementalMarkdownParser = class {
|
|
|
5100
5397
|
tail,
|
|
5101
5398
|
generation: this.generation
|
|
5102
5399
|
};
|
|
5400
|
+
this.openFence = this.openFenceState(text, base, tail, this.cached.frozen);
|
|
5103
5401
|
return this.cached;
|
|
5104
5402
|
}
|
|
5105
5403
|
};
|
|
@@ -5488,52 +5786,100 @@ const SHIKI_PRE_PROPS = {
|
|
|
5488
5786
|
},
|
|
5489
5787
|
tabIndex: 0
|
|
5490
5788
|
};
|
|
5789
|
+
/** Completed-line group size; React reconciles groups while the DOM remains line-for-line identical. */
|
|
5790
|
+
const STREAMING_LINE_GROUP_SIZE = 32;
|
|
5791
|
+
function renderLine(line, index) {
|
|
5792
|
+
return jsxs(Fragment$1, { children: [index > 0 && "\n", jsx("span", {
|
|
5793
|
+
className: "line",
|
|
5794
|
+
children: line.map((span, spanIndex) => jsx("span", {
|
|
5795
|
+
style: span.style,
|
|
5796
|
+
children: span.text
|
|
5797
|
+
}, spanIndex))
|
|
5798
|
+
})] }, index);
|
|
5799
|
+
}
|
|
5491
5800
|
function CodeBlock({ code, lang, streaming, className, copyLabel, copiedLabel }) {
|
|
5492
5801
|
const trimmed = code.endsWith("\n") ? code.slice(0, -1) : code;
|
|
5802
|
+
const rootRef = useRef(null);
|
|
5803
|
+
const highlighting = useViewportHighlighting(rootRef, lang);
|
|
5493
5804
|
const loaded = useSyncExternalStore(subscribeGrammarLoaded, grammarLoadCount, grammarLoadCount);
|
|
5494
|
-
const html = useMemo(() => streaming === true ? void 0 : highlightToHtml(trimmed, lang), [
|
|
5495
|
-
streaming,
|
|
5496
|
-
trimmed,
|
|
5497
|
-
lang,
|
|
5498
|
-
loaded
|
|
5499
|
-
]);
|
|
5500
5805
|
const sessionRef = useRef(null);
|
|
5501
5806
|
const lineCacheRef = useRef(null);
|
|
5807
|
+
const settledRef = useRef(false);
|
|
5502
5808
|
const streamedBody = useMemo(() => {
|
|
5809
|
+
if (!highlighting) {
|
|
5810
|
+
sessionRef.current = null;
|
|
5811
|
+
lineCacheRef.current = null;
|
|
5812
|
+
settledRef.current = false;
|
|
5813
|
+
return;
|
|
5814
|
+
}
|
|
5503
5815
|
if (streaming !== true) {
|
|
5816
|
+
const previous = lineCacheRef.current;
|
|
5817
|
+
if (previous !== null && previous.code === trimmed && previous.lang === lang) {
|
|
5818
|
+
settledRef.current = true;
|
|
5819
|
+
return previous.body;
|
|
5820
|
+
}
|
|
5504
5821
|
sessionRef.current = null;
|
|
5505
5822
|
lineCacheRef.current = null;
|
|
5823
|
+
settledRef.current = true;
|
|
5506
5824
|
return;
|
|
5507
5825
|
}
|
|
5826
|
+
if (settledRef.current) {
|
|
5827
|
+
sessionRef.current = null;
|
|
5828
|
+
lineCacheRef.current = null;
|
|
5829
|
+
settledRef.current = false;
|
|
5830
|
+
}
|
|
5508
5831
|
sessionRef.current ??= new StreamingHighlightSession();
|
|
5509
|
-
const
|
|
5510
|
-
if (
|
|
5832
|
+
const frame = sessionRef.current.updateFrame(trimmed, lang);
|
|
5833
|
+
if (frame === void 0) {
|
|
5511
5834
|
lineCacheRef.current = null;
|
|
5512
5835
|
return;
|
|
5513
5836
|
}
|
|
5514
5837
|
const previous = lineCacheRef.current;
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5838
|
+
if (previous?.frame === frame && previous.code === trimmed && previous.lang === lang) return previous.body;
|
|
5839
|
+
const sameGeneration = previous?.generation === frame.generation;
|
|
5840
|
+
const groups = sameGeneration ? [...previous.groups] : [];
|
|
5841
|
+
let pending = sameGeneration ? [...previous.pending] : [];
|
|
5842
|
+
let nextLine = sameGeneration ? previous.nextLine : 0;
|
|
5843
|
+
for (const line of frame.appended) {
|
|
5844
|
+
pending.push(renderLine(line, nextLine));
|
|
5845
|
+
nextLine += 1;
|
|
5846
|
+
if (pending.length !== STREAMING_LINE_GROUP_SIZE) continue;
|
|
5847
|
+
const start = nextLine - pending.length;
|
|
5848
|
+
groups.push(jsx(Fragment$1, { children: pending }, start));
|
|
5849
|
+
pending = [];
|
|
5850
|
+
}
|
|
5851
|
+
const tail = frame.tail.map((line, index) => renderLine(line, nextLine + index));
|
|
5852
|
+
const tailGroup = jsx(Fragment$1, { children: [...pending, ...tail] }, nextLine - pending.length);
|
|
5853
|
+
const body = jsx("pre", {
|
|
5527
5854
|
...SHIKI_PRE_PROPS,
|
|
5528
|
-
children:
|
|
5855
|
+
children: jsxs("code", { children: [groups, tailGroup] })
|
|
5529
5856
|
});
|
|
5857
|
+
lineCacheRef.current = {
|
|
5858
|
+
code: trimmed,
|
|
5859
|
+
lang,
|
|
5860
|
+
generation: frame.generation,
|
|
5861
|
+
frame,
|
|
5862
|
+
groups,
|
|
5863
|
+
pending,
|
|
5864
|
+
nextLine,
|
|
5865
|
+
body
|
|
5866
|
+
};
|
|
5867
|
+
return body;
|
|
5530
5868
|
}, [
|
|
5531
5869
|
streaming,
|
|
5870
|
+
highlighting,
|
|
5871
|
+
trimmed,
|
|
5872
|
+
lang,
|
|
5873
|
+
loaded
|
|
5874
|
+
]);
|
|
5875
|
+
const html = useMemo(() => highlighting && streaming !== true && streamedBody === void 0 ? highlightToHtml(trimmed, lang) : void 0, [
|
|
5876
|
+
streaming,
|
|
5877
|
+
highlighting,
|
|
5878
|
+
streamedBody,
|
|
5532
5879
|
trimmed,
|
|
5533
5880
|
lang,
|
|
5534
5881
|
loaded
|
|
5535
5882
|
]);
|
|
5536
|
-
const rootRef = useRef(null);
|
|
5537
5883
|
const [copied, setCopied] = useState(false);
|
|
5538
5884
|
const onCopy = useCallback(() => {
|
|
5539
5885
|
if (copied) return;
|
|
@@ -6039,7 +6385,8 @@ function renderSettled(text, labels, fileMentions) {
|
|
|
6039
6385
|
};
|
|
6040
6386
|
const blocks = wrapBlockChildren(renderBlocks(root.children.map((node, index) => ({
|
|
6041
6387
|
node,
|
|
6042
|
-
|
|
6388
|
+
/* v8 ignore next -- parseFull uses parseGfm, which stamps every top-level node. */
|
|
6389
|
+
key: node.position?.start.offset ?? -(index + 1)
|
|
6043
6390
|
})), context), false);
|
|
6044
6391
|
const section = renderFootnoteSection(context);
|
|
6045
6392
|
return section === null ? blocks : [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
max-height: 200px;
|
|
24
24
|
overflow: auto;
|
|
25
25
|
background: var(--dsw-alias-markdown-code-block);
|
|
26
|
-
border:
|
|
26
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
27
27
|
border-radius: 6px;
|
|
28
28
|
font-family: var(--ds-font-family-code);
|
|
29
29
|
font-size: 11px;
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
.markdown hr {
|
|
127
127
|
display: block;
|
|
128
128
|
border: none;
|
|
129
|
-
height:
|
|
129
|
+
height: 0.5px;
|
|
130
130
|
margin: 32px 0;
|
|
131
131
|
background: var(--dsw-alias-border-l2);
|
|
132
132
|
}
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
.tableScroll th {
|
|
226
226
|
text-align: start;
|
|
227
227
|
padding: 10px 16px;
|
|
228
|
-
border-bottom:
|
|
228
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l3);
|
|
229
229
|
border-top: none;
|
|
230
230
|
font: var(--dsw-font-markdown-table-head);
|
|
231
231
|
max-width: 320px;
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
|
|
236
236
|
.tableScroll td {
|
|
237
237
|
padding: 10px 16px;
|
|
238
|
-
border-bottom:
|
|
238
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
239
239
|
font: var(--dsw-font-markdown-table);
|
|
240
240
|
max-width: 320px;
|
|
241
241
|
max-width: min(30vw, 320px);
|
|
@@ -6,9 +6,10 @@ export interface CodeBlockProps {
|
|
|
6
6
|
/**
|
|
7
7
|
* The code is still growing (a streaming markdown fence): highlight through
|
|
8
8
|
* a per-instance {@link StreamingHighlightSession}, which re-tokenizes only
|
|
9
|
-
* appended text and keeps completed
|
|
10
|
-
*
|
|
11
|
-
* stream-stable React key);
|
|
9
|
+
* appended text and keeps completed line groups (and DOM) untouched. The
|
|
10
|
+
* caller must keep the component instance stable across growth (a
|
|
11
|
+
* stream-stable React key); an unchanged streamed fence also retains that
|
|
12
|
+
* tree when it settles. Cold settled callers get shiki's HTML.
|
|
12
13
|
*/
|
|
13
14
|
streaming?: boolean | undefined;
|
|
14
15
|
/** Extra class merged onto the wrapper (callers position; this component draws). */
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* highlighting, still monospace) — never an error.
|
|
19
19
|
*/
|
|
20
20
|
import type { CSSProperties } from 'react';
|
|
21
|
+
/**
|
|
22
|
+
* Whether a language hint can use the shared syntax highlighter.
|
|
23
|
+
* @param lang - Language hint from a code surface.
|
|
24
|
+
* @returns Whether the hint resolves to a supported grammar.
|
|
25
|
+
*/
|
|
26
|
+
export declare function supportsHighlighting(lang: string | undefined): boolean;
|
|
21
27
|
/**
|
|
22
28
|
* Subscribe to lazy-grammar load completions; `listener` fires after a
|
|
23
29
|
* {@link LAZY_GRAMMARS} grammar finishes registering on the singleton, so a
|
|
@@ -61,10 +67,11 @@ export interface HighlightSpan {
|
|
|
61
67
|
* tokenization is line-based and forward-only — a line's tokens depend only on
|
|
62
68
|
* its own text and the grammar state entering it — so appended text never
|
|
63
69
|
* changes a completed line's tokens. The session caches the spans of every
|
|
64
|
-
* completed line together with the grammar state after them;
|
|
65
|
-
* {@link
|
|
66
|
-
*
|
|
67
|
-
*
|
|
70
|
+
* completed line together with the grammar state after them;
|
|
71
|
+
* {@link updateFrame} reports only newly completed lines plus the still-growing
|
|
72
|
+
* last line, while {@link update} materializes the complete compatibility
|
|
73
|
+
* result. Per-call tokenization cost therefore excludes the completed prefix,
|
|
74
|
+
* and the result equals a from-scratch tokenization of the same code.
|
|
68
75
|
* Non-append input and a change of resolved grammar reset the cache and
|
|
69
76
|
* re-tokenize fully, so any input stays correct.
|
|
70
77
|
*/
|
|
@@ -80,9 +87,18 @@ export declare class StreamingHighlightSession {
|
|
|
80
87
|
private lastCode;
|
|
81
88
|
private lastLang;
|
|
82
89
|
private lastResult;
|
|
90
|
+
private generation;
|
|
91
|
+
private lastFrame;
|
|
83
92
|
private reset;
|
|
84
93
|
/** Tokenize `text` with `resolved`, resuming from the cached grammar state when one exists. */
|
|
85
94
|
private tokenize;
|
|
95
|
+
/**
|
|
96
|
+
* Tokenize one update as a delta for a retained renderer.
|
|
97
|
+
* @param code - the fence text accumulated so far.
|
|
98
|
+
* @param lang - the language hint.
|
|
99
|
+
* @returns Newly completed lines plus the current tail, or `undefined` for the plain arm.
|
|
100
|
+
*/
|
|
101
|
+
updateFrame(code: string, lang: string | undefined): StreamingHighlightFrame | undefined;
|
|
86
102
|
/**
|
|
87
103
|
* Tokenize the fence's current text into per-line highlighted runs;
|
|
88
104
|
* `undefined` means the caller renders its plain fallback. Idempotent per
|
|
@@ -97,6 +113,15 @@ export declare class StreamingHighlightSession {
|
|
|
97
113
|
*/
|
|
98
114
|
update(code: string, lang: string | undefined): readonly HighlightSpan[][] | undefined;
|
|
99
115
|
}
|
|
116
|
+
/** One retained-renderer update from {@link StreamingHighlightSession.updateFrame}. */
|
|
117
|
+
export interface StreamingHighlightFrame {
|
|
118
|
+
/** Changes whenever prior completed lines must be discarded. */
|
|
119
|
+
readonly generation: number;
|
|
120
|
+
/** Completed lines added since the preceding frame in this generation. */
|
|
121
|
+
readonly appended: readonly HighlightSpan[][];
|
|
122
|
+
/** The still-growing final line or lines, replaced by the next frame. */
|
|
123
|
+
readonly tail: readonly HighlightSpan[][];
|
|
124
|
+
}
|
|
100
125
|
/**
|
|
101
126
|
* Tokenize `code` into per-line highlighted runs when `lang` maps to a
|
|
102
127
|
* registered grammar; `undefined` means the caller renders its plain fallback.
|
|
@@ -4,19 +4,23 @@
|
|
|
4
4
|
* Re-parsing the whole accumulated document on every streaming chunk is
|
|
5
5
|
* quadratic in the final reply length. CommonMark block parsing is line-based
|
|
6
6
|
* and appended text can only reshape the parse frontier — the last top-level
|
|
7
|
-
* block (a paragraph becoming a setext heading or a table, a list
|
|
8
|
-
* after a blank line
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* block (a paragraph becoming a setext heading or a table, or a list
|
|
8
|
+
* continuing after a blank line) — so earlier blocks are final. This parser
|
|
9
|
+
* therefore freezes all but the trailing {@link UNSTABLE_TAIL_BLOCKS} blocks
|
|
10
|
+
* and re-parses only the source tail behind them. A final unclosed top-level
|
|
11
|
+
* fence cannot freeze as a block, so its completed content lines use a second
|
|
12
|
+
* frontier: only the last completed line and current partial line return
|
|
13
|
+
* through the caller's grammar. Each source region is therefore parsed a
|
|
14
|
+
* bounded number of times over the stream instead of once per chunk.
|
|
13
15
|
*
|
|
14
|
-
* The freeze boundary comes from the parser's own `position` offsets
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* The block freeze boundary comes from the parser's own `position` offsets.
|
|
17
|
+
* The cut sits at the *end offset* of the last frozen block (not the next
|
|
18
|
+
* block's start): a following block's start offset excludes up to three spaces
|
|
19
|
+
* of insignificant leading indentation, which is harmless to drop, but
|
|
20
|
+
* cutting at the previous end also keeps the inter-block blank lines in the
|
|
21
|
+
* tail so the sliced source stays verbatim. Fence scanning only recognizes a
|
|
22
|
+
* parser-confirmed code node and closing delimiter; ambiguous input returns to
|
|
23
|
+
* the normal tail parse.
|
|
20
24
|
*
|
|
21
25
|
* Known deviation, shared with any prefix-freeze scheme: micromark resolves
|
|
22
26
|
* reference-style links and footnotes document-wide at parse time, so a
|
|
@@ -55,8 +59,15 @@ export declare class IncrementalMarkdownParser {
|
|
|
55
59
|
private frozen;
|
|
56
60
|
private generation;
|
|
57
61
|
private cached;
|
|
62
|
+
private openFence;
|
|
58
63
|
/** @param parse - Grammar shared with whatever renders the blocks, so boundaries agree. */
|
|
59
64
|
constructor(parse: (text: string) => Root);
|
|
65
|
+
/** Parse one unclosed-fence content slice through the caller's grammar. */
|
|
66
|
+
private fenceValue;
|
|
67
|
+
/** Recognize the parsed tail's final unclosed fence and prepare its incremental content frontier. */
|
|
68
|
+
private openFenceState;
|
|
69
|
+
/** Extend a recognized unclosed fence without parsing its completed content prefix again. */
|
|
70
|
+
private updateOpenFence;
|
|
60
71
|
/**
|
|
61
72
|
* Fold the current accumulated text and return the frozen/tail split.
|
|
62
73
|
* Idempotent for identical input (the previous result is returned as-is),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Activate one supported code surface when it first intersects the viewport.
|
|
4
|
+
* Activation lasts for the component lifetime; browsers without
|
|
5
|
+
* IntersectionObserver activate immediately.
|
|
6
|
+
* @param target - Code surface whose plain rendering reserves its geometry.
|
|
7
|
+
* @param lang - Optional language hint.
|
|
8
|
+
* @returns Whether this component may build highlighted output.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useViewportHighlighting(target: RefObject<Element>, lang: string | undefined): boolean;
|
|
11
|
+
//# sourceMappingURL=useViewportHighlighting.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-primitives",
|
|
3
3
|
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
|
|
4
|
-
"version": "0.1.2-alpha.
|
|
4
|
+
"version": "0.1.2-alpha.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
"types": "./lib/types/index.d.ts",
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
|
-
"./invariant": {
|
|
22
|
-
"types": "./lib/types/invariant.d.ts",
|
|
23
|
-
"default": "./lib/invariant.js"
|
|
24
|
-
},
|
|
25
21
|
"./src/*": "./src/*",
|
|
26
22
|
"./package.json": "./package.json"
|
|
27
23
|
},
|
|
@@ -51,12 +47,10 @@
|
|
|
51
47
|
"devDependencies": {
|
|
52
48
|
"@types/react": "~18.3.1",
|
|
53
49
|
"@types/react-dom": "~18.3.0",
|
|
54
|
-
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
55
50
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
56
51
|
},
|
|
57
52
|
"files": [
|
|
58
53
|
"lib/index.js",
|
|
59
|
-
"lib/invariant.js",
|
|
60
54
|
"lib/**/*.css",
|
|
61
55
|
"lib/types/**/*.d.ts"
|
|
62
56
|
],
|
package/lib/invariant.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
//#region lib/types/invariant.js
|
|
2
|
-
/**
|
|
3
|
-
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-primitives`.
|
|
4
|
-
* @module @deepseek-ai/dsh-client-ui-primitives/invariant
|
|
5
|
-
*/
|
|
6
|
-
const PACKAGE_NAME = "@deepseek-ai/dsh-client-ui-primitives";
|
|
7
|
-
/** Cordis companion plugin name. */
|
|
8
|
-
const name = "client-ui-primitives-invariant";
|
|
9
|
-
/** Service required before the companion can reserve package ownership. */
|
|
10
|
-
const inject = ["invariants"];
|
|
11
|
-
/**
|
|
12
|
-
* No runtime invariant: pure props-in React atoms with no Cordis API —
|
|
13
|
-
* no events, no services, no mutable cross-plugin state; rendering contracts
|
|
14
|
-
* are asserted directly by this package's component specs.
|
|
15
|
-
*/
|
|
16
|
-
const install = () => {};
|
|
17
|
-
/**
|
|
18
|
-
* Register this package's invariant companion.
|
|
19
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
20
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
21
|
-
*/
|
|
22
|
-
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
23
|
-
//#endregion
|
|
24
|
-
export { apply, inject, name };
|
|
25
|
-
|
|
26
|
-
//# sourceMappingURL=invariant.js.map
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-primitives`.
|
|
3
|
-
* @module @deepseek-ai/dsh-client-ui-primitives/invariant
|
|
4
|
-
*/
|
|
5
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
-
/** Cordis companion plugin name. */
|
|
7
|
-
export declare const name = "client-ui-primitives-invariant";
|
|
8
|
-
/** Service required before the companion can reserve package ownership. */
|
|
9
|
-
export declare const inject: string[];
|
|
10
|
-
/**
|
|
11
|
-
* Register this package's invariant companion.
|
|
12
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
-
*/
|
|
15
|
-
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
-
//# sourceMappingURL=invariant.d.ts.map
|