dsh-code 0.6.1 → 0.8.0
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.en.md +20 -6
- package/README.md +20 -6
- package/lib/index.mjs +3952 -1315
- package/lib/startup.mjs +21 -9
- package/lib/theme-BEi4i_aN.mjs +624 -0
- package/lib/types/app.d.ts +108 -4
- package/lib/types/history.d.ts +15 -4
- package/lib/types/index.d.ts +49 -0
- package/lib/types/kernel-panels.d.ts +28 -0
- package/lib/types/mentions.d.ts +29 -12
- package/lib/types/models.d.ts +66 -0
- package/lib/types/permissions.d.ts +37 -0
- package/lib/types/presets.d.ts +2 -0
- package/lib/types/provider-settings.d.ts +144 -0
- package/lib/types/questions.d.ts +2 -0
- package/lib/types/render/animations.d.ts +177 -2
- package/lib/types/render/lines.d.ts +6 -0
- package/lib/types/render/markdown.d.ts +3 -3
- package/lib/types/render/projection.d.ts +123 -3
- package/lib/types/render/status.d.ts +35 -24
- package/lib/types/render/text.d.ts +14 -7
- package/lib/types/render/tool-detail.d.ts +3 -1
- package/lib/types/render/tool-preview.d.ts +4 -1
- package/lib/types/session-directory.d.ts +15 -0
- package/lib/types/startup.d.ts +12 -4
- package/lib/types/store.d.ts +13 -2
- package/lib/types/theme-panel.d.ts +24 -0
- package/lib/types/theme.d.ts +158 -2
- package/lib/types/version.d.ts +5 -0
- package/package.json +1 -1
- package/src/app.ts +1283 -206
- package/src/approval.ts +11 -2
- package/src/history.ts +20 -5
- package/src/index.ts +1207 -905
- package/src/kernel-panels.ts +518 -419
- package/src/mentions.ts +57 -27
- package/src/models.ts +200 -66
- package/src/permissions.ts +85 -0
- package/src/presets.ts +12 -0
- package/src/provider-settings.ts +520 -0
- package/src/questions.ts +15 -5
- package/src/render/animations.ts +373 -2
- package/src/render/lines.ts +21 -6
- package/src/render/markdown.ts +302 -4
- package/src/render/projection.ts +1419 -659
- package/src/render/status.ts +650 -603
- package/src/render/text.ts +28 -9
- package/src/render/tool-detail.ts +81 -40
- package/src/render/tool-preview.ts +18 -2
- package/src/session-directory.ts +44 -5
- package/src/skills.ts +8 -4
- package/src/startup.ts +119 -109
- package/src/store.ts +26 -8
- package/src/theme-panel.ts +72 -0
- package/src/theme.ts +206 -70
- package/src/version.ts +16 -0
package/src/render/markdown.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* block/inline parser producing styled line segments the Ink renderer maps
|
|
4
4
|
* to colored text. No ANSI here — the app owns color mapping, tests own the
|
|
5
5
|
* structure. The subset mirrors what agent replies actually emit: headings,
|
|
6
|
-
* emphasis, inline/fenced code, flat lists, blockquotes, links, rules,
|
|
7
|
-
* wrapped paragraphs. Unknown syntax degrades to plain text
|
|
6
|
+
* emphasis, inline/fenced code, flat lists, blockquotes, links, rules, GFM
|
|
7
|
+
* tables, and wrapped paragraphs. Unknown syntax degrades to plain text.
|
|
8
8
|
*
|
|
9
9
|
* @module @deepseek-ai/dsh-code/render/markdown
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/** Style classes the renderer emits; the app maps them to colors/props. */
|
|
13
|
-
export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'dim' | 'strike'
|
|
13
|
+
export type MdStyle = 'plain' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'accent' | 'accentBold' | 'dim' | 'strike'
|
|
14
14
|
|
|
15
15
|
/** One styled run of text. */
|
|
16
16
|
export interface MdSegment {
|
|
@@ -162,6 +162,290 @@ const RULE = /^(?:---|\*\*\*|___)\s*$/u
|
|
|
162
162
|
const QUOTE = /^>\s?(.*)$/u
|
|
163
163
|
const UNORDERED = /^\s*[-*+]\s+(.*)$/u
|
|
164
164
|
const ORDERED = /^\s*(\d+)[.)]\s+(.*)$/u
|
|
165
|
+
const TABLE_DELIMITER = /^(:)?-+(:)?$/u
|
|
166
|
+
const TABLE_COLUMN_GAP = 2
|
|
167
|
+
const TABLE_CELL_PADDING = 1
|
|
168
|
+
const TABLE_MIN_COLUMN_WIDTH = 3
|
|
169
|
+
const TABLE_MIN_ALIGNED_VALUE_WIDTH = 16
|
|
170
|
+
const MAX_TABLE_COLUMNS = 12
|
|
171
|
+
|
|
172
|
+
type TableAlignment = 'left' | 'center' | 'right'
|
|
173
|
+
|
|
174
|
+
interface ParsedTable {
|
|
175
|
+
headers: readonly string[]
|
|
176
|
+
alignments: readonly TableAlignment[]
|
|
177
|
+
rows: readonly (readonly string[])[]
|
|
178
|
+
nextIndex: number
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Split one pipe row without treating escaped or inline-code pipes as cells. */
|
|
182
|
+
function splitTableRow(line: string): string[] | undefined {
|
|
183
|
+
const source = line.trim()
|
|
184
|
+
if (!source.includes('|')) return undefined
|
|
185
|
+
const cells: string[] = []
|
|
186
|
+
let current = ''
|
|
187
|
+
let inCode = false
|
|
188
|
+
let sawSeparator = false
|
|
189
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
190
|
+
const char = source[index] ?? ''
|
|
191
|
+
if (char === '\\' && source[index + 1] === '|') {
|
|
192
|
+
current += '|'
|
|
193
|
+
index += 1
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
if (char === '`') {
|
|
197
|
+
inCode = !inCode
|
|
198
|
+
current += char
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
if (char === '|' && !inCode) {
|
|
202
|
+
cells.push(current.trim())
|
|
203
|
+
current = ''
|
|
204
|
+
sawSeparator = true
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
current += char
|
|
208
|
+
}
|
|
209
|
+
if (!sawSeparator) return undefined
|
|
210
|
+
cells.push(current.trim())
|
|
211
|
+
if (source.startsWith('|') && cells[0] === '') cells.shift()
|
|
212
|
+
if (source.endsWith('|') && cells.at(-1) === '') cells.pop()
|
|
213
|
+
return cells.length === 0 ? undefined : cells
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Parse the alignment marker carried by one GFM delimiter cell. */
|
|
217
|
+
function tableAlignment(cell: string): TableAlignment | undefined {
|
|
218
|
+
const match = TABLE_DELIMITER.exec(cell)
|
|
219
|
+
if (match === null) return undefined
|
|
220
|
+
if (match[1] !== undefined && match[2] !== undefined) return 'center'
|
|
221
|
+
if (match[2] !== undefined) return 'right'
|
|
222
|
+
return 'left'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Recognize one complete GFM pipe table at `startIndex`. */
|
|
226
|
+
function parseTable(source: readonly string[], startIndex: number): ParsedTable | undefined {
|
|
227
|
+
const headers = splitTableRow(source[startIndex] ?? '')
|
|
228
|
+
const delimiters = splitTableRow(source[startIndex + 1] ?? '')
|
|
229
|
+
if (headers === undefined || delimiters === undefined || headers.length !== delimiters.length) return undefined
|
|
230
|
+
if (headers.length === 0 || headers.length > MAX_TABLE_COLUMNS) return undefined
|
|
231
|
+
const alignments = delimiters.map(tableAlignment)
|
|
232
|
+
if (alignments.some(alignment => alignment === undefined)) return undefined
|
|
233
|
+
|
|
234
|
+
const rows: string[][] = []
|
|
235
|
+
let nextIndex = startIndex + 2
|
|
236
|
+
while (nextIndex < source.length) {
|
|
237
|
+
const cells = splitTableRow(source[nextIndex] ?? '')
|
|
238
|
+
if (cells === undefined) break
|
|
239
|
+
rows.push(Array.from({ length: headers.length }, (_, column) => cells[column] ?? ''))
|
|
240
|
+
nextIndex += 1
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
headers,
|
|
244
|
+
alignments: alignments as TableAlignment[],
|
|
245
|
+
rows,
|
|
246
|
+
nextIndex,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Visible width of one styled cell line. */
|
|
251
|
+
function segmentsWidth(segments: readonly MdSegment[]): number {
|
|
252
|
+
return segments.reduce((total, segment) => total + visibleColumns(segment.text), 0)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Drop wrapping-only leading spaces while preserving styles. */
|
|
256
|
+
function trimLeadingSpaces(segments: readonly MdSegment[]): readonly MdSegment[] {
|
|
257
|
+
const trimmed = segments.map(segment => ({ ...segment }))
|
|
258
|
+
while (trimmed[0]?.text.startsWith(' ') === true) {
|
|
259
|
+
const first = trimmed[0]!
|
|
260
|
+
const text = first.text.replace(/^ +/u, '')
|
|
261
|
+
if (text === '') trimmed.shift()
|
|
262
|
+
else trimmed[0] = { ...first, text }
|
|
263
|
+
}
|
|
264
|
+
return trimmed
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Hard-split an oversized soft-wrapped row while retaining inline styles. */
|
|
268
|
+
function hardWrapSegments(segments: readonly MdSegment[], width: number): readonly MdSegment[][] {
|
|
269
|
+
const out: MdSegment[][] = []
|
|
270
|
+
let current: MdSegment[] = []
|
|
271
|
+
let used = 0
|
|
272
|
+
const flush = (): void => {
|
|
273
|
+
out.push(current)
|
|
274
|
+
current = []
|
|
275
|
+
used = 0
|
|
276
|
+
}
|
|
277
|
+
for (const segment of segments) {
|
|
278
|
+
for (const char of segment.text) {
|
|
279
|
+
const cells = visibleColumns(char)
|
|
280
|
+
if (used > 0 && used + cells > width) flush()
|
|
281
|
+
const previous = current.at(-1)
|
|
282
|
+
if (previous?.style === segment.style) previous.text += char
|
|
283
|
+
else current.push({ text: char, style: segment.style })
|
|
284
|
+
used += cells
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (current.length > 0 || out.length === 0) flush()
|
|
288
|
+
return out
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Wrap one table cell to its allocated content width. */
|
|
292
|
+
function wrapTableCell(runs: readonly InlineRun[], width: number): readonly MdSegment[][] {
|
|
293
|
+
const segments = runs.map(run => seg(run.text, run.style))
|
|
294
|
+
const soft = wrapSegments(segments, Math.max(1, width))
|
|
295
|
+
if (soft.length === 0) return [[]]
|
|
296
|
+
const wrapped: MdSegment[][] = []
|
|
297
|
+
for (const line of soft) {
|
|
298
|
+
const trimmed = trimLeadingSpaces(line)
|
|
299
|
+
if (segmentsWidth(trimmed) <= width) wrapped.push(trimmed.map(segment => ({ ...segment })))
|
|
300
|
+
else wrapped.push(...hardWrapSegments(trimmed, width))
|
|
301
|
+
}
|
|
302
|
+
return wrapped
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Visible width after removing inline Markdown delimiters. */
|
|
306
|
+
function tableCellWidth(cell: string): number {
|
|
307
|
+
return segmentsWidth(parseInline(cell).map(run => seg(run.text, run.style)))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Allocate readable grid widths or request the vertical record fallback. */
|
|
311
|
+
function tableColumnWidths(table: ParsedTable, width: number): readonly number[] | undefined {
|
|
312
|
+
const columnCount = table.headers.length
|
|
313
|
+
const reserved = (columnCount * TABLE_CELL_PADDING) + ((columnCount - 1) * TABLE_COLUMN_GAP)
|
|
314
|
+
const available = width - reserved
|
|
315
|
+
if (available < columnCount * TABLE_MIN_COLUMN_WIDTH) return undefined
|
|
316
|
+
const widths = table.headers.map((header, column) => Math.max(
|
|
317
|
+
TABLE_MIN_COLUMN_WIDTH,
|
|
318
|
+
tableCellWidth(header),
|
|
319
|
+
...table.rows.map(row => tableCellWidth(row[column] ?? '')),
|
|
320
|
+
))
|
|
321
|
+
let overflow = widths.reduce((total, value) => total + value, 0) - available
|
|
322
|
+
while (overflow > 0) {
|
|
323
|
+
let widest = -1
|
|
324
|
+
for (let column = 0; column < widths.length; column += 1) {
|
|
325
|
+
if ((widths[column] ?? 0) <= TABLE_MIN_COLUMN_WIDTH) continue
|
|
326
|
+
if (widest < 0 || (widths[column] ?? 0) > (widths[widest] ?? 0)) widest = column
|
|
327
|
+
}
|
|
328
|
+
if (widest < 0) return undefined
|
|
329
|
+
widths[widest] = (widths[widest] ?? TABLE_MIN_COLUMN_WIDTH) - 1
|
|
330
|
+
overflow -= 1
|
|
331
|
+
}
|
|
332
|
+
return widths
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Reject grids whose headers or body values become fragmented vertical strips.
|
|
337
|
+
* This mirrors Codex's readability fallback without importing its larger table
|
|
338
|
+
* classification machinery: systemic long-token breaks or a seven-line prose
|
|
339
|
+
* cell are clearer as vertical field records.
|
|
340
|
+
*/
|
|
341
|
+
function tableGridIsReadable(table: ParsedTable, widths: readonly number[]): boolean {
|
|
342
|
+
const wrappedHeaders = table.headers.filter((header, column) => tableCellWidth(header) > (widths[column] ?? 0)).length
|
|
343
|
+
if (wrappedHeaders >= 2) return false
|
|
344
|
+
let affectedRows = 0
|
|
345
|
+
for (const row of table.rows) {
|
|
346
|
+
let affected = false
|
|
347
|
+
for (let column = 0; column < table.headers.length; column += 1) {
|
|
348
|
+
const width = widths[column] ?? TABLE_MIN_COLUMN_WIDTH
|
|
349
|
+
const runs = parseInline(row[column] ?? '')
|
|
350
|
+
const plain = runs.map(run => run.text).join('')
|
|
351
|
+
const fragmentedToken = plain.split(/\s+/u).some(token => visibleColumns(token) > width)
|
|
352
|
+
const wrappedHeight = wrapTableCell(runs, width).length
|
|
353
|
+
const catastrophicProse = plain.trim().split(/\s+/u).length >= 4 && width < 12 && wrappedHeight >= 7
|
|
354
|
+
if (fragmentedToken || catastrophicProse) {
|
|
355
|
+
affected = true
|
|
356
|
+
break
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (affected) affectedRows += 1
|
|
360
|
+
}
|
|
361
|
+
const threshold = table.rows.length <= 1 ? 1 : Math.max(2, Math.ceil(table.rows.length / 3))
|
|
362
|
+
return affectedRows < threshold
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Apply one table-cell alignment to a wrapped content row. */
|
|
366
|
+
function alignedCell(
|
|
367
|
+
segments: readonly MdSegment[],
|
|
368
|
+
width: number,
|
|
369
|
+
alignment: TableAlignment,
|
|
370
|
+
): { left: number; right: number } {
|
|
371
|
+
const remaining = Math.max(0, width - segmentsWidth(segments))
|
|
372
|
+
if (alignment === 'right') return { left: remaining, right: 0 }
|
|
373
|
+
if (alignment === 'center') return { left: Math.floor(remaining / 2), right: Math.ceil(remaining / 2) }
|
|
374
|
+
return { left: 0, right: remaining }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Render one logical grid row, including wrapped cell continuations. */
|
|
378
|
+
function renderTableGridRow(
|
|
379
|
+
cells: readonly string[],
|
|
380
|
+
widths: readonly number[],
|
|
381
|
+
alignments: readonly TableAlignment[],
|
|
382
|
+
header: boolean,
|
|
383
|
+
): readonly MdLine[] {
|
|
384
|
+
const wrapped = cells.map((cell, column) => wrapTableCell(parseInline(cell), widths[column] ?? TABLE_MIN_COLUMN_WIDTH))
|
|
385
|
+
const height = Math.max(1, ...wrapped.map(lines => lines.length))
|
|
386
|
+
return Array.from({ length: height }, (_, rowIndex) => {
|
|
387
|
+
const segments: MdSegment[] = []
|
|
388
|
+
for (let column = 0; column < cells.length; column += 1) {
|
|
389
|
+
const line = wrapped[column]?.[rowIndex] ?? []
|
|
390
|
+
const styled = header
|
|
391
|
+
? line.map(segment => ({ ...segment, style: 'accentBold' as const }))
|
|
392
|
+
: line
|
|
393
|
+
const alignment = alignedCell(styled, widths[column] ?? TABLE_MIN_COLUMN_WIDTH, alignments[column] ?? 'left')
|
|
394
|
+
segments.push(seg(' '.repeat(TABLE_CELL_PADDING + alignment.left)))
|
|
395
|
+
segments.push(...styled)
|
|
396
|
+
if (column + 1 < cells.length) segments.push(seg(' '.repeat(alignment.right + TABLE_COLUMN_GAP)))
|
|
397
|
+
}
|
|
398
|
+
return { segments: merge(segments) }
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Render a table as Codex-style borderless rows with measured separators. */
|
|
403
|
+
function renderTableGrid(table: ParsedTable, widths: readonly number[]): readonly MdLine[] {
|
|
404
|
+
const separator = (char: string): MdLine => ({
|
|
405
|
+
segments: [seg(widths.map(width => char.repeat(width + TABLE_CELL_PADDING)).join(' '.repeat(TABLE_COLUMN_GAP)), 'dim')],
|
|
406
|
+
})
|
|
407
|
+
const lines: MdLine[] = [
|
|
408
|
+
...renderTableGridRow(table.headers, widths, table.alignments, true),
|
|
409
|
+
separator('━'),
|
|
410
|
+
]
|
|
411
|
+
for (let row = 0; row < table.rows.length; row += 1) {
|
|
412
|
+
lines.push(...renderTableGridRow(table.rows[row] ?? [], widths, table.alignments, false))
|
|
413
|
+
if (row + 1 < table.rows.length) lines.push(separator('─'))
|
|
414
|
+
}
|
|
415
|
+
return lines
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Render an unreadably narrow grid as vertically scannable field records. */
|
|
419
|
+
function renderTableRecords(table: ParsedTable, width: number): readonly MdLine[] {
|
|
420
|
+
const labelWidth = Math.max(...table.headers.map(tableCellWidth))
|
|
421
|
+
const prefixWidth = TABLE_CELL_PADDING + labelWidth + TABLE_COLUMN_GAP
|
|
422
|
+
const aligned = prefixWidth + TABLE_MIN_ALIGNED_VALUE_WIDTH <= width
|
|
423
|
+
const lines: MdLine[] = []
|
|
424
|
+
for (let rowIndex = 0; rowIndex < table.rows.length; rowIndex += 1) {
|
|
425
|
+
const row = table.rows[rowIndex] ?? []
|
|
426
|
+
for (let column = 0; column < table.headers.length; column += 1) {
|
|
427
|
+
const label = table.headers[column] ?? ''
|
|
428
|
+
const value = row[column] ?? ''
|
|
429
|
+
const labelRuns = parseInline(label).map(run => seg(run.text, 'accentBold'))
|
|
430
|
+
if (aligned) {
|
|
431
|
+
const valueLines = wrapTableCell(parseInline(value), Math.max(1, width - prefixWidth))
|
|
432
|
+
for (let lineIndex = 0; lineIndex < valueLines.length; lineIndex += 1) {
|
|
433
|
+
const prefix = lineIndex === 0
|
|
434
|
+
? [seg(' '), ...labelRuns, seg(' '.repeat(labelWidth - tableCellWidth(label) + TABLE_COLUMN_GAP))]
|
|
435
|
+
: [seg(' '.repeat(prefixWidth))]
|
|
436
|
+
lines.push({ segments: merge([...prefix, ...(valueLines[lineIndex] ?? [])]) })
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
lines.push({ segments: merge([seg(' '), ...labelRuns]) })
|
|
440
|
+
for (const valueLine of wrapTableCell(parseInline(value), Math.max(1, width - 2))) {
|
|
441
|
+
lines.push({ segments: merge([seg(' '), ...valueLine]) })
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (rowIndex + 1 < table.rows.length) lines.push({ segments: [seg('─'.repeat(width), 'dim')] })
|
|
446
|
+
}
|
|
447
|
+
return lines
|
|
448
|
+
}
|
|
165
449
|
|
|
166
450
|
/** Render markdown text into styled lines of at most `width` columns. */
|
|
167
451
|
export function renderMarkdown(text: string, width: number): readonly MdLine[] {
|
|
@@ -178,7 +462,10 @@ export function renderMarkdown(text: string, width: number): readonly MdLine[] {
|
|
|
178
462
|
}
|
|
179
463
|
separatorPending = false
|
|
180
464
|
}
|
|
181
|
-
|
|
465
|
+
// Tabs become two visible spaces: terminal tab stops are contextual and
|
|
466
|
+
// cannot participate in a deterministic column budget (the same rule the
|
|
467
|
+
// styled-row path applies in lines.ts).
|
|
468
|
+
const raw = text.replaceAll('\r', '').replaceAll('\t', ' ')
|
|
182
469
|
const source = raw.split('\n')
|
|
183
470
|
let index = 0
|
|
184
471
|
while (index < source.length) {
|
|
@@ -193,6 +480,17 @@ export function renderMarkdown(text: string, width: number): readonly MdLine[] {
|
|
|
193
480
|
}
|
|
194
481
|
startBlock()
|
|
195
482
|
|
|
483
|
+
const table = parseTable(source, index - 1)
|
|
484
|
+
if (table !== undefined) {
|
|
485
|
+
const tableWidth = Math.max(10, Math.floor(width))
|
|
486
|
+
const columnWidths = tableColumnWidths(table, tableWidth)
|
|
487
|
+
lines.push(...(columnWidths === undefined || !tableGridIsReadable(table, columnWidths)
|
|
488
|
+
? renderTableRecords(table, tableWidth)
|
|
489
|
+
: renderTableGrid(table, columnWidths)))
|
|
490
|
+
index = table.nextIndex
|
|
491
|
+
continue
|
|
492
|
+
}
|
|
493
|
+
|
|
196
494
|
// Fenced code block: verbatim lines in code style, language label first.
|
|
197
495
|
const fence = FENCE.exec(line)
|
|
198
496
|
if (fence !== null) {
|