@toddzheng024/dscode-bundle 0.7.11 → 0.7.13
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/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +1 -1
- package/package.json +5 -4
- package/plugins/i18n/messages.mjs +6 -6
- package/plugins/session-metrics/view.mjs +16 -5
- package/plugins/tui-tools/index.mjs +14 -1
- package/vendor/tui/lib/app.mjs +6674 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +64 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2198 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1862 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44483
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal markdown renderer for assistant replies: a pure GFM-subset
|
|
3
|
+
* block/inline parser producing styled line segments the Ink renderer maps
|
|
4
|
+
* to colored text. No ANSI here — the app owns color mapping, tests own the
|
|
5
|
+
* structure. The subset mirrors what agent replies actually emit: headings,
|
|
6
|
+
* emphasis, inline/fenced code, flat lists, blockquotes, links, rules, GFM
|
|
7
|
+
* tables, and wrapped paragraphs. Unknown syntax degrades to plain text.
|
|
8
|
+
*
|
|
9
|
+
* @module @deepseek-ai/dsh-code/render/markdown
|
|
10
|
+
*/
|
|
11
|
+
import { graphemeWidth, splitGraphemes, stringWidth } from './width.mjs';
|
|
12
|
+
/** Plain segment helper. */
|
|
13
|
+
function seg(text, style = 'plain') {
|
|
14
|
+
return { text, style };
|
|
15
|
+
}
|
|
16
|
+
/** Visible width of a run in columns (grapheme-cluster and emoji aware). */
|
|
17
|
+
export function visibleColumns(text) {
|
|
18
|
+
return stringWidth(text);
|
|
19
|
+
}
|
|
20
|
+
/** Punctuation that should not become the first visible glyph of a row. */
|
|
21
|
+
function isClosingPunctuation(text) {
|
|
22
|
+
return /^[,.;:!?%、。,.!?:;%)》」』】〕〉》”’]/u.test(text);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Split prose into soft-wrap units: ASCII words stay intact, while wide CJK
|
|
26
|
+
* glyphs become individual break opportunities. This keeps ordinary prose
|
|
27
|
+
* readable without allowing a Chinese paragraph to become one overlong word.
|
|
28
|
+
*/
|
|
29
|
+
function wrapUnits(segments) {
|
|
30
|
+
const units = [];
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
let word = '';
|
|
33
|
+
const flushWord = () => {
|
|
34
|
+
if (word !== '')
|
|
35
|
+
units.push({ text: word, style: segment.style });
|
|
36
|
+
word = '';
|
|
37
|
+
};
|
|
38
|
+
// Grapheme clusters keep ZWJ families, flags, and combining sequences
|
|
39
|
+
// whole; a cluster wider than one cell is its own break opportunity.
|
|
40
|
+
for (const cluster of splitGraphemes(segment.text)) {
|
|
41
|
+
if (cluster === ' ') {
|
|
42
|
+
flushWord();
|
|
43
|
+
units.push({ text: cluster, style: segment.style });
|
|
44
|
+
}
|
|
45
|
+
else if (graphemeWidth(cluster) > 1) {
|
|
46
|
+
flushWord();
|
|
47
|
+
units.push({ text: cluster, style: segment.style });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
word += cluster;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
flushWord();
|
|
54
|
+
}
|
|
55
|
+
return units;
|
|
56
|
+
}
|
|
57
|
+
/** Walk styled units, keeping logical rows within the physical column budget. */
|
|
58
|
+
function wrapSegments(segments, width) {
|
|
59
|
+
const limit = Math.max(1, Math.floor(width));
|
|
60
|
+
const lines = [];
|
|
61
|
+
let current = [];
|
|
62
|
+
let used = 0;
|
|
63
|
+
const flush = () => {
|
|
64
|
+
while (current.at(-1)?.text === ' ') {
|
|
65
|
+
used -= visibleColumns(current.pop().text);
|
|
66
|
+
}
|
|
67
|
+
if (current.length > 0)
|
|
68
|
+
lines.push(merge(current));
|
|
69
|
+
current = [];
|
|
70
|
+
used = 0;
|
|
71
|
+
};
|
|
72
|
+
const appendAtom = (unit) => {
|
|
73
|
+
const columns = visibleColumns(unit.text);
|
|
74
|
+
if (used > 0 && used + columns > limit)
|
|
75
|
+
flush();
|
|
76
|
+
current.push(unit);
|
|
77
|
+
used += columns;
|
|
78
|
+
};
|
|
79
|
+
const append = (unit) => {
|
|
80
|
+
const columns = visibleColumns(unit.text);
|
|
81
|
+
if (used === 0 && columns > limit) {
|
|
82
|
+
for (const cluster of splitGraphemes(unit.text))
|
|
83
|
+
appendAtom({ text: cluster, style: unit.style });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (used + columns <= limit || current.length === 0) {
|
|
87
|
+
current.push(unit);
|
|
88
|
+
used += columns;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Keep full-width punctuation attached to the preceding CJK glyph. If
|
|
92
|
+
// the row is full, move that glyph with the punctuation instead of
|
|
93
|
+
// producing a visually orphaned line beginning with ':' or '。'.
|
|
94
|
+
const previous = current.at(-1);
|
|
95
|
+
if (isClosingPunctuation(unit.text) && previous !== undefined && visibleColumns(previous.text) > 1) {
|
|
96
|
+
current.pop();
|
|
97
|
+
used -= visibleColumns(previous.text);
|
|
98
|
+
flush();
|
|
99
|
+
appendAtom(previous);
|
|
100
|
+
appendAtom(unit);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
flush();
|
|
104
|
+
if (columns > limit) {
|
|
105
|
+
for (const cluster of splitGraphemes(unit.text))
|
|
106
|
+
appendAtom({ text: cluster, style: unit.style });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
appendAtom(unit);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
for (const unit of wrapUnits(segments))
|
|
113
|
+
append(unit);
|
|
114
|
+
flush();
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
/** Join adjacent same-style runs so the app renders fewer elements. */
|
|
118
|
+
function merge(segments) {
|
|
119
|
+
const merged = [];
|
|
120
|
+
for (const segment of segments) {
|
|
121
|
+
const last = merged[merged.length - 1];
|
|
122
|
+
if (last !== undefined && last.style === segment.style) {
|
|
123
|
+
merged[merged.length - 1] = { text: last.text + segment.text, style: last.style };
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
merged.push({ ...segment });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return merged;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse inline markdown in one line of text. Link destinations render as a
|
|
133
|
+
* dim `(url)` suffix — the visible text keeps the accent.
|
|
134
|
+
*/
|
|
135
|
+
function parseInline(text) {
|
|
136
|
+
const runs = [];
|
|
137
|
+
let rest = text;
|
|
138
|
+
while (rest !== '') {
|
|
139
|
+
const code = /^`([^`]+)`/u.exec(rest);
|
|
140
|
+
if (code !== null) {
|
|
141
|
+
runs.push({ text: code[1] ?? '', style: 'code' });
|
|
142
|
+
rest = rest.slice(code[0].length);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const boldItalic = /^\*\*\*([^*]+)\*\*\*/u.exec(rest);
|
|
146
|
+
if (boldItalic !== null) {
|
|
147
|
+
runs.push({ text: boldItalic[1] ?? '', style: 'boldItalic' });
|
|
148
|
+
rest = rest.slice(boldItalic[0].length);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const bold = /^\*\*([^*]+)\*\*/u.exec(rest);
|
|
152
|
+
if (bold !== null) {
|
|
153
|
+
runs.push({ text: bold[1] ?? '', style: 'bold' });
|
|
154
|
+
rest = rest.slice(bold[0].length);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const italic = /^\*([^*]+)\*/u.exec(rest) ?? /^_([^_]+)_/u.exec(rest);
|
|
158
|
+
if (italic !== null) {
|
|
159
|
+
runs.push({ text: italic[1] ?? '', style: 'italic' });
|
|
160
|
+
rest = rest.slice(italic[0].length);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const strike = /^~~([^~]+)~~/u.exec(rest);
|
|
164
|
+
if (strike !== null) {
|
|
165
|
+
runs.push({ text: strike[1] ?? '', style: 'strike' });
|
|
166
|
+
rest = rest.slice(strike[0].length);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const link = /^\[([^\]]+)\]\(([^)\s]+)\)/u.exec(rest);
|
|
170
|
+
if (link !== null) {
|
|
171
|
+
const label = link[1] ?? '';
|
|
172
|
+
const url = link[2] ?? '';
|
|
173
|
+
runs.push({ text: label, style: 'accent' });
|
|
174
|
+
runs.push({ text: ` (${url})`, style: 'dim' });
|
|
175
|
+
rest = rest.slice(link[0].length);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// Plain run up to the next special opener.
|
|
179
|
+
const next = rest.search(/[*_`~[]/u);
|
|
180
|
+
if (next === -1) {
|
|
181
|
+
runs.push({ text: rest, style: 'plain' });
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (next > 0) {
|
|
185
|
+
runs.push({ text: rest.slice(0, next), style: 'plain' });
|
|
186
|
+
rest = rest.slice(next);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// A special opener at position 0 that no pattern consumed: emit it
|
|
190
|
+
// literally and advance, so unbalanced syntax never loops.
|
|
191
|
+
runs.push({ text: rest.slice(0, 1), style: 'plain' });
|
|
192
|
+
rest = rest.slice(1);
|
|
193
|
+
}
|
|
194
|
+
return runs;
|
|
195
|
+
}
|
|
196
|
+
const HEADING = /^(#{1,6})\s+(.*)$/u;
|
|
197
|
+
const FENCE = /^```([^\s`]*)\s*$/u;
|
|
198
|
+
const RULE = /^(?:---|\*\*\*|___)\s*$/u;
|
|
199
|
+
const QUOTE = /^>\s?(.*)$/u;
|
|
200
|
+
const UNORDERED = /^\s*[-*+]\s+(.*)$/u;
|
|
201
|
+
const ORDERED = /^\s*(\d+)[.)]\s+(.*)$/u;
|
|
202
|
+
const TABLE_DELIMITER = /^(:)?-+(:)?$/u;
|
|
203
|
+
const TABLE_COLUMN_GAP = 2;
|
|
204
|
+
const TABLE_CELL_PADDING = 1;
|
|
205
|
+
const TABLE_MIN_COLUMN_WIDTH = 3;
|
|
206
|
+
const TABLE_MIN_ALIGNED_VALUE_WIDTH = 16;
|
|
207
|
+
const MAX_TABLE_COLUMNS = 12;
|
|
208
|
+
/** Split one pipe row without treating escaped or inline-code pipes as cells. */
|
|
209
|
+
function splitTableRow(line) {
|
|
210
|
+
const source = line.trim();
|
|
211
|
+
if (!source.includes('|'))
|
|
212
|
+
return undefined;
|
|
213
|
+
const cells = [];
|
|
214
|
+
let current = '';
|
|
215
|
+
let inCode = false;
|
|
216
|
+
let sawSeparator = false;
|
|
217
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
218
|
+
const char = source[index] ?? '';
|
|
219
|
+
if (char === '\\' && source[index + 1] === '|') {
|
|
220
|
+
current += '|';
|
|
221
|
+
index += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (char === '`') {
|
|
225
|
+
inCode = !inCode;
|
|
226
|
+
current += char;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (char === '|' && !inCode) {
|
|
230
|
+
cells.push(current.trim());
|
|
231
|
+
current = '';
|
|
232
|
+
sawSeparator = true;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
current += char;
|
|
236
|
+
}
|
|
237
|
+
if (!sawSeparator)
|
|
238
|
+
return undefined;
|
|
239
|
+
cells.push(current.trim());
|
|
240
|
+
if (source.startsWith('|') && cells[0] === '')
|
|
241
|
+
cells.shift();
|
|
242
|
+
if (source.endsWith('|') && cells.at(-1) === '')
|
|
243
|
+
cells.pop();
|
|
244
|
+
return cells.length === 0 ? undefined : cells;
|
|
245
|
+
}
|
|
246
|
+
/** Parse the alignment marker carried by one GFM delimiter cell. */
|
|
247
|
+
function tableAlignment(cell) {
|
|
248
|
+
const match = TABLE_DELIMITER.exec(cell);
|
|
249
|
+
if (match === null)
|
|
250
|
+
return undefined;
|
|
251
|
+
if (match[1] !== undefined && match[2] !== undefined)
|
|
252
|
+
return 'center';
|
|
253
|
+
if (match[2] !== undefined)
|
|
254
|
+
return 'right';
|
|
255
|
+
return 'left';
|
|
256
|
+
}
|
|
257
|
+
/** Recognize one complete GFM pipe table at `startIndex`. */
|
|
258
|
+
function parseTable(source, startIndex) {
|
|
259
|
+
const headers = splitTableRow(source[startIndex] ?? '');
|
|
260
|
+
const delimiters = splitTableRow(source[startIndex + 1] ?? '');
|
|
261
|
+
if (headers === undefined || delimiters === undefined || headers.length !== delimiters.length)
|
|
262
|
+
return undefined;
|
|
263
|
+
if (headers.length === 0 || headers.length > MAX_TABLE_COLUMNS)
|
|
264
|
+
return undefined;
|
|
265
|
+
const alignments = delimiters.map(tableAlignment);
|
|
266
|
+
if (alignments.some(alignment => alignment === undefined))
|
|
267
|
+
return undefined;
|
|
268
|
+
const rows = [];
|
|
269
|
+
let nextIndex = startIndex + 2;
|
|
270
|
+
while (nextIndex < source.length) {
|
|
271
|
+
const cells = splitTableRow(source[nextIndex] ?? '');
|
|
272
|
+
if (cells === undefined)
|
|
273
|
+
break;
|
|
274
|
+
rows.push(Array.from({ length: headers.length }, (_, column) => cells[column] ?? ''));
|
|
275
|
+
nextIndex += 1;
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
headers,
|
|
279
|
+
alignments: alignments,
|
|
280
|
+
rows,
|
|
281
|
+
nextIndex,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/** Visible width of one styled cell line. */
|
|
285
|
+
function segmentsWidth(segments) {
|
|
286
|
+
return segments.reduce((total, segment) => total + visibleColumns(segment.text), 0);
|
|
287
|
+
}
|
|
288
|
+
/** Drop wrapping-only leading spaces while preserving styles. */
|
|
289
|
+
function trimLeadingSpaces(segments) {
|
|
290
|
+
const trimmed = segments.map(segment => ({ ...segment }));
|
|
291
|
+
while (trimmed[0]?.text.startsWith(' ') === true) {
|
|
292
|
+
const first = trimmed[0];
|
|
293
|
+
const text = first.text.replace(/^ +/u, '');
|
|
294
|
+
if (text === '')
|
|
295
|
+
trimmed.shift();
|
|
296
|
+
else
|
|
297
|
+
trimmed[0] = { ...first, text };
|
|
298
|
+
}
|
|
299
|
+
return trimmed;
|
|
300
|
+
}
|
|
301
|
+
/** Hard-split an oversized soft-wrapped row while retaining inline styles. */
|
|
302
|
+
function hardWrapSegments(segments, width) {
|
|
303
|
+
const out = [];
|
|
304
|
+
let current = [];
|
|
305
|
+
let used = 0;
|
|
306
|
+
const flush = () => {
|
|
307
|
+
out.push(current);
|
|
308
|
+
current = [];
|
|
309
|
+
used = 0;
|
|
310
|
+
};
|
|
311
|
+
for (const segment of segments) {
|
|
312
|
+
for (const cluster of splitGraphemes(segment.text)) {
|
|
313
|
+
const cells = graphemeWidth(cluster);
|
|
314
|
+
if (used > 0 && used + cells > width)
|
|
315
|
+
flush();
|
|
316
|
+
const previous = current.at(-1);
|
|
317
|
+
if (previous?.style === segment.style)
|
|
318
|
+
previous.text += cluster;
|
|
319
|
+
else
|
|
320
|
+
current.push({ text: cluster, style: segment.style });
|
|
321
|
+
used += cells;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (current.length > 0 || out.length === 0)
|
|
325
|
+
flush();
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
/** Wrap one table cell to its allocated content width. */
|
|
329
|
+
function wrapTableCell(runs, width) {
|
|
330
|
+
const segments = runs.map(run => seg(run.text, run.style));
|
|
331
|
+
const soft = wrapSegments(segments, Math.max(1, width));
|
|
332
|
+
if (soft.length === 0)
|
|
333
|
+
return [[]];
|
|
334
|
+
const wrapped = [];
|
|
335
|
+
for (const line of soft) {
|
|
336
|
+
const trimmed = trimLeadingSpaces(line);
|
|
337
|
+
if (segmentsWidth(trimmed) <= width)
|
|
338
|
+
wrapped.push(trimmed.map(segment => ({ ...segment })));
|
|
339
|
+
else
|
|
340
|
+
wrapped.push(...hardWrapSegments(trimmed, width));
|
|
341
|
+
}
|
|
342
|
+
return wrapped;
|
|
343
|
+
}
|
|
344
|
+
/** Visible width after removing inline Markdown delimiters. */
|
|
345
|
+
function tableCellWidth(cell) {
|
|
346
|
+
return segmentsWidth(parseInline(cell).map(run => seg(run.text, run.style)));
|
|
347
|
+
}
|
|
348
|
+
/** Allocate readable grid widths or request the vertical record fallback. */
|
|
349
|
+
function tableColumnWidths(table, width) {
|
|
350
|
+
const columnCount = table.headers.length;
|
|
351
|
+
const reserved = (columnCount * TABLE_CELL_PADDING) + ((columnCount - 1) * TABLE_COLUMN_GAP);
|
|
352
|
+
const available = width - reserved;
|
|
353
|
+
if (available < columnCount * TABLE_MIN_COLUMN_WIDTH)
|
|
354
|
+
return undefined;
|
|
355
|
+
const widths = table.headers.map((header, column) => Math.max(TABLE_MIN_COLUMN_WIDTH, tableCellWidth(header), ...table.rows.map(row => tableCellWidth(row[column] ?? ''))));
|
|
356
|
+
let overflow = widths.reduce((total, value) => total + value, 0) - available;
|
|
357
|
+
while (overflow > 0) {
|
|
358
|
+
let widest = -1;
|
|
359
|
+
for (let column = 0; column < widths.length; column += 1) {
|
|
360
|
+
if ((widths[column] ?? 0) <= TABLE_MIN_COLUMN_WIDTH)
|
|
361
|
+
continue;
|
|
362
|
+
if (widest < 0 || (widths[column] ?? 0) > (widths[widest] ?? 0))
|
|
363
|
+
widest = column;
|
|
364
|
+
}
|
|
365
|
+
if (widest < 0)
|
|
366
|
+
return undefined;
|
|
367
|
+
widths[widest] = (widths[widest] ?? TABLE_MIN_COLUMN_WIDTH) - 1;
|
|
368
|
+
overflow -= 1;
|
|
369
|
+
}
|
|
370
|
+
return widths;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Reject grids whose headers or body values become fragmented vertical strips.
|
|
374
|
+
* This mirrors Codex's readability fallback without importing its larger table
|
|
375
|
+
* classification machinery: systemic long-token breaks or a seven-line prose
|
|
376
|
+
* cell are clearer as vertical field records.
|
|
377
|
+
*/
|
|
378
|
+
function tableGridIsReadable(table, widths) {
|
|
379
|
+
const wrappedHeaders = table.headers.filter((header, column) => tableCellWidth(header) > (widths[column] ?? 0)).length;
|
|
380
|
+
if (wrappedHeaders >= 2)
|
|
381
|
+
return false;
|
|
382
|
+
let affectedRows = 0;
|
|
383
|
+
for (const row of table.rows) {
|
|
384
|
+
let affected = false;
|
|
385
|
+
for (let column = 0; column < table.headers.length; column += 1) {
|
|
386
|
+
const width = widths[column] ?? TABLE_MIN_COLUMN_WIDTH;
|
|
387
|
+
const runs = parseInline(row[column] ?? '');
|
|
388
|
+
const plain = runs.map(run => run.text).join('');
|
|
389
|
+
const fragmentedToken = plain.split(/\s+/u).some(token => visibleColumns(token) > width);
|
|
390
|
+
const wrappedHeight = wrapTableCell(runs, width).length;
|
|
391
|
+
const catastrophicProse = plain.trim().split(/\s+/u).length >= 4 && width < 12 && wrappedHeight >= 7;
|
|
392
|
+
if (fragmentedToken || catastrophicProse) {
|
|
393
|
+
affected = true;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (affected)
|
|
398
|
+
affectedRows += 1;
|
|
399
|
+
}
|
|
400
|
+
const threshold = table.rows.length <= 1 ? 1 : Math.max(2, Math.ceil(table.rows.length / 3));
|
|
401
|
+
return affectedRows < threshold;
|
|
402
|
+
}
|
|
403
|
+
/** Apply one table-cell alignment to a wrapped content row. */
|
|
404
|
+
function alignedCell(segments, width, alignment) {
|
|
405
|
+
const remaining = Math.max(0, width - segmentsWidth(segments));
|
|
406
|
+
if (alignment === 'right')
|
|
407
|
+
return { left: remaining, right: 0 };
|
|
408
|
+
if (alignment === 'center')
|
|
409
|
+
return { left: Math.floor(remaining / 2), right: Math.ceil(remaining / 2) };
|
|
410
|
+
return { left: 0, right: remaining };
|
|
411
|
+
}
|
|
412
|
+
/** Render one logical grid row, including wrapped cell continuations. */
|
|
413
|
+
function renderTableGridRow(cells, widths, alignments, header) {
|
|
414
|
+
const wrapped = cells.map((cell, column) => wrapTableCell(parseInline(cell), widths[column] ?? TABLE_MIN_COLUMN_WIDTH));
|
|
415
|
+
const height = Math.max(1, ...wrapped.map(lines => lines.length));
|
|
416
|
+
return Array.from({ length: height }, (_, rowIndex) => {
|
|
417
|
+
const segments = [];
|
|
418
|
+
for (let column = 0; column < cells.length; column += 1) {
|
|
419
|
+
const line = wrapped[column]?.[rowIndex] ?? [];
|
|
420
|
+
const styled = header
|
|
421
|
+
? line.map(segment => ({ ...segment, style: 'accentBold' }))
|
|
422
|
+
: line;
|
|
423
|
+
const alignment = alignedCell(styled, widths[column] ?? TABLE_MIN_COLUMN_WIDTH, alignments[column] ?? 'left');
|
|
424
|
+
segments.push(seg(' '.repeat(TABLE_CELL_PADDING + alignment.left)));
|
|
425
|
+
segments.push(...styled);
|
|
426
|
+
if (column + 1 < cells.length)
|
|
427
|
+
segments.push(seg(' '.repeat(alignment.right + TABLE_COLUMN_GAP)));
|
|
428
|
+
}
|
|
429
|
+
return { segments: merge(segments) };
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/** Render a table as Codex-style borderless rows with measured separators. */
|
|
433
|
+
function renderTableGrid(table, widths) {
|
|
434
|
+
const separator = (char) => ({
|
|
435
|
+
segments: [seg(widths.map(width => char.repeat(width + TABLE_CELL_PADDING)).join(' '.repeat(TABLE_COLUMN_GAP)), 'dim')],
|
|
436
|
+
});
|
|
437
|
+
const lines = [
|
|
438
|
+
...renderTableGridRow(table.headers, widths, table.alignments, true),
|
|
439
|
+
separator('━'),
|
|
440
|
+
];
|
|
441
|
+
for (let row = 0; row < table.rows.length; row += 1) {
|
|
442
|
+
lines.push(...renderTableGridRow(table.rows[row] ?? [], widths, table.alignments, false));
|
|
443
|
+
if (row + 1 < table.rows.length)
|
|
444
|
+
lines.push(separator('─'));
|
|
445
|
+
}
|
|
446
|
+
return lines;
|
|
447
|
+
}
|
|
448
|
+
/** Render an unreadably narrow grid as vertically scannable field records. */
|
|
449
|
+
function renderTableRecords(table, width) {
|
|
450
|
+
const labelWidth = Math.max(...table.headers.map(tableCellWidth));
|
|
451
|
+
const prefixWidth = TABLE_CELL_PADDING + labelWidth + TABLE_COLUMN_GAP;
|
|
452
|
+
const aligned = prefixWidth + TABLE_MIN_ALIGNED_VALUE_WIDTH <= width;
|
|
453
|
+
const lines = [];
|
|
454
|
+
for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) {
|
|
455
|
+
const row = table.rows[rowIndex] ?? [];
|
|
456
|
+
for (let column = 0; column < table.headers.length; column += 1) {
|
|
457
|
+
const label = table.headers[column] ?? '';
|
|
458
|
+
const value = row[column] ?? '';
|
|
459
|
+
const labelRuns = parseInline(label).map(run => seg(run.text, 'accentBold'));
|
|
460
|
+
if (aligned) {
|
|
461
|
+
const valueLines = wrapTableCell(parseInline(value), Math.max(1, width - prefixWidth));
|
|
462
|
+
for (let lineIndex = 0; lineIndex < valueLines.length; lineIndex += 1) {
|
|
463
|
+
const prefix = lineIndex === 0
|
|
464
|
+
? [seg(' '), ...labelRuns, seg(' '.repeat(labelWidth - tableCellWidth(label) + TABLE_COLUMN_GAP))]
|
|
465
|
+
: [seg(' '.repeat(prefixWidth))];
|
|
466
|
+
lines.push({ segments: merge([...prefix, ...(valueLines[lineIndex] ?? [])]) });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
lines.push({ segments: merge([seg(' '), ...labelRuns]) });
|
|
471
|
+
for (const valueLine of wrapTableCell(parseInline(value), Math.max(1, width - 2))) {
|
|
472
|
+
lines.push({ segments: merge([seg(' '), ...valueLine]) });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (rowIndex + 1 < table.rows.length)
|
|
477
|
+
lines.push({ segments: [seg('─'.repeat(width), 'dim')] });
|
|
478
|
+
}
|
|
479
|
+
return lines;
|
|
480
|
+
}
|
|
481
|
+
/** Render markdown text into styled lines of at most `width` columns. */
|
|
482
|
+
export function renderMarkdown(text, width, options = {}) {
|
|
483
|
+
const lines = [];
|
|
484
|
+
let separatorPending = false;
|
|
485
|
+
const push = (segments) => {
|
|
486
|
+
if (options.physicalWrap === false) {
|
|
487
|
+
lines.push({ segments: merge(segments) });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
for (const wrapped of wrapSegments(segments, Math.max(10, width))) {
|
|
491
|
+
lines.push({ segments: merge(wrapped) });
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const startBlock = () => {
|
|
495
|
+
if (separatorPending && lines.length > 0 && lines.at(-1)?.segments.length !== 0) {
|
|
496
|
+
lines.push({ segments: [] });
|
|
497
|
+
}
|
|
498
|
+
separatorPending = false;
|
|
499
|
+
};
|
|
500
|
+
// Tabs become two visible spaces: terminal tab stops are contextual and
|
|
501
|
+
// cannot participate in a deterministic column budget (the same rule the
|
|
502
|
+
// styled-row path applies in lines.ts).
|
|
503
|
+
const raw = text.replaceAll('\r', '').replaceAll('\t', ' ');
|
|
504
|
+
const source = raw.split('\n');
|
|
505
|
+
let index = 0;
|
|
506
|
+
while (index < source.length) {
|
|
507
|
+
const line = source[index] ?? '';
|
|
508
|
+
index += 1;
|
|
509
|
+
// Preserve one deliberate row between source blocks. Repeated blank
|
|
510
|
+
// lines collapse, and leading/trailing whitespace never grows output.
|
|
511
|
+
if (line.trim() === '') {
|
|
512
|
+
separatorPending = lines.length > 0;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
startBlock();
|
|
516
|
+
const table = parseTable(source, index - 1);
|
|
517
|
+
if (table !== undefined) {
|
|
518
|
+
const tableWidth = Math.max(10, Math.floor(width));
|
|
519
|
+
const columnWidths = tableColumnWidths(table, tableWidth);
|
|
520
|
+
lines.push(...(columnWidths === undefined || !tableGridIsReadable(table, columnWidths)
|
|
521
|
+
? renderTableRecords(table, tableWidth)
|
|
522
|
+
: renderTableGrid(table, columnWidths)));
|
|
523
|
+
index = table.nextIndex;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
// Fenced code block: verbatim lines in code style, language label first.
|
|
527
|
+
// A ```diff fence keeps the same classification the /diff panel uses
|
|
528
|
+
// (shared rule: lines.ts diffLineStyle) so pasted review diffs render
|
|
529
|
+
// with the added/removed tints everywhere markdown does.
|
|
530
|
+
const fence = FENCE.exec(line);
|
|
531
|
+
if (fence !== null) {
|
|
532
|
+
const language = fence[1] ?? '';
|
|
533
|
+
const diffFence = language === 'diff' || language === 'patch';
|
|
534
|
+
if (language !== '')
|
|
535
|
+
push([seg(` ${language}`, 'dim')]);
|
|
536
|
+
while (index < source.length && !FENCE.test(source[index] ?? '')) {
|
|
537
|
+
const source_ = source[index] ?? '';
|
|
538
|
+
const style = !diffFence
|
|
539
|
+
? 'code'
|
|
540
|
+
: source_.startsWith('+') && !source_.startsWith('+++')
|
|
541
|
+
? 'diffAdd'
|
|
542
|
+
: source_.startsWith('-') && !source_.startsWith('---')
|
|
543
|
+
? 'diffDel'
|
|
544
|
+
: 'code';
|
|
545
|
+
push([seg(` ${source_}`, style)]);
|
|
546
|
+
index += 1;
|
|
547
|
+
}
|
|
548
|
+
index += 1; // closing fence
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (RULE.test(line.trim())) {
|
|
552
|
+
push([seg(` ${'─'.repeat(Math.max(1, Math.floor(width / 4)))}`, 'dim')]);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const heading = HEADING.exec(line);
|
|
556
|
+
if (heading !== null) {
|
|
557
|
+
push([seg(heading[2] ?? '', 'accent')]);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const quote = QUOTE.exec(line);
|
|
561
|
+
if (quote !== null) {
|
|
562
|
+
push([seg(' │ ', 'accent'), ...parseInline(quote[1] ?? '').map(run => seg(run.text, run.style === 'plain' ? 'dim' : run.style))]);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const ordered = ORDERED.exec(line);
|
|
566
|
+
if (ordered !== null) {
|
|
567
|
+
const prefix = options.physicalWrap === false ? `${ordered[1] ?? ''}. ` : ` ${ordered[1] ?? ''}. `;
|
|
568
|
+
push([seg(prefix, 'accent'), ...parseInline(ordered[2] ?? '').map(run => seg(run.text, run.style))]);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const unordered = UNORDERED.exec(line);
|
|
572
|
+
if (unordered !== null) {
|
|
573
|
+
push([seg(options.physicalWrap === false ? '• ' : ' • ', 'accent'), ...parseInline(unordered[1] ?? '').map(run => seg(run.text, run.style))]);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
// Paragraph: gather until a blank line, then wrap as one flow. Line
|
|
577
|
+
// breaks inside a paragraph join as a single space (GFM soft breaks).
|
|
578
|
+
const paragraph = [line];
|
|
579
|
+
while (index < source.length && (source[index] ?? '').trim() !== '') {
|
|
580
|
+
paragraph.push(source[index] ?? '');
|
|
581
|
+
index += 1;
|
|
582
|
+
}
|
|
583
|
+
const runs = [];
|
|
584
|
+
for (let at = 0; at < paragraph.length; at += 1) {
|
|
585
|
+
if (at > 0)
|
|
586
|
+
runs.push({ text: ' ', style: 'plain' });
|
|
587
|
+
runs.push(...parseInline(paragraph[at] ?? ''));
|
|
588
|
+
}
|
|
589
|
+
push(runs.map(run => seg(run.text, run.style)));
|
|
590
|
+
}
|
|
591
|
+
return lines;
|
|
592
|
+
}
|