nex-code 0.3.5 → 0.3.7

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/cli/render.js DELETED
@@ -1,495 +0,0 @@
1
- /**
2
- * cli/render.js — Rich Terminal Rendering
3
- * Markdown rendering, syntax highlighting, table formatting
4
- * Zero dependencies — uses ANSI escape codes directly
5
- */
6
-
7
- const { C } = require('./ui');
8
-
9
- /**
10
- * Render markdown-like text for terminal output
11
- * Supports: headers, bold, italic, code, code blocks, lists, links
12
- * @param {string} text
13
- * @returns {string}
14
- */
15
- function renderMarkdown(text) {
16
- if (!text) return '';
17
-
18
- const lines = text.split('\n');
19
- const rendered = [];
20
- let inCodeBlock = false;
21
- let codeBlockLang = '';
22
-
23
- for (const line of lines) {
24
- // Code block toggle
25
- if (line.trim().startsWith('```')) {
26
- if (inCodeBlock) {
27
- rendered.push(`${C.dim}${'─'.repeat(40)}${C.reset}`);
28
- inCodeBlock = false;
29
- codeBlockLang = '';
30
- } else {
31
- inCodeBlock = true;
32
- codeBlockLang = line.trim().substring(3).trim();
33
- const label = codeBlockLang ? ` ${codeBlockLang} ` : '';
34
- rendered.push(`${C.dim}${'─'.repeat(3)}${label}${'─'.repeat(Math.max(0, 37 - label.length))}${C.reset}`);
35
- }
36
- continue;
37
- }
38
-
39
- if (inCodeBlock) {
40
- rendered.push(` ${highlightCode(line, codeBlockLang)}`);
41
- continue;
42
- }
43
-
44
- // Headers
45
- if (line.startsWith('### ')) {
46
- rendered.push(`${C.bold}${C.cyan} ${line.substring(4)}${C.reset}`);
47
- continue;
48
- }
49
- if (line.startsWith('## ')) {
50
- rendered.push(`${C.bold}${C.cyan} ${line.substring(3)}${C.reset}`);
51
- continue;
52
- }
53
- if (line.startsWith('# ')) {
54
- rendered.push(`${C.bold}${C.cyan}${line.substring(2)}${C.reset}`);
55
- continue;
56
- }
57
-
58
- // Lists
59
- if (/^\s*[-*]\s/.test(line)) {
60
- const indent = line.match(/^(\s*)/)[1];
61
- const content = line.replace(/^\s*[-*]\s/, '');
62
- rendered.push(`${indent}${C.cyan}•${C.reset} ${renderInline(content)}`);
63
- continue;
64
- }
65
-
66
- // Numbered lists
67
- if (/^\s*\d+\.\s/.test(line)) {
68
- const match = line.match(/^(\s*)(\d+)\.\s(.*)/);
69
- if (match) {
70
- rendered.push(`${match[1]}${C.cyan}${match[2]}.${C.reset} ${renderInline(match[3])}`);
71
- continue;
72
- }
73
- }
74
-
75
- // Regular line
76
- rendered.push(renderInline(line));
77
- }
78
-
79
- return rendered.join('\n');
80
- }
81
-
82
- /**
83
- * Render inline markdown (bold, italic, code, links)
84
- * @param {string} text
85
- * @returns {string}
86
- */
87
- function renderInline(text) {
88
- if (!text) return '';
89
-
90
- return text
91
- // Inline code `code`
92
- .replace(/`([^`]+)`/g, `${C.cyan}$1${C.reset}`)
93
- // Bold **text**
94
- .replace(/\*\*([^*]+)\*\*/g, `${C.bold}$1${C.reset}`)
95
- // Italic *text*
96
- .replace(/\*([^*]+)\*/g, `${C.dim}$1${C.reset}`)
97
- // Links [text](url)
98
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, `${C.cyan}$1${C.reset} ${C.dim}($2)${C.reset}`);
99
- }
100
-
101
- /**
102
- * Basic syntax highlighting for code
103
- * @param {string} line
104
- * @param {string} lang
105
- * @returns {string}
106
- */
107
- function highlightCode(line, lang) {
108
- if (!line) return '';
109
-
110
- const jsLangs = ['js', 'javascript', 'ts', 'typescript', 'jsx', 'tsx'];
111
- if (jsLangs.includes(lang) || !lang) {
112
- return highlightJS(line);
113
- }
114
- if (lang === 'bash' || lang === 'sh' || lang === 'shell' || lang === 'zsh') {
115
- return highlightBash(line);
116
- }
117
- if (lang === 'json' || lang === 'jsonc') {
118
- return highlightJSON(line);
119
- }
120
- if (lang === 'python' || lang === 'py') {
121
- return highlightPython(line);
122
- }
123
- if (lang === 'go' || lang === 'golang') {
124
- return highlightGo(line);
125
- }
126
- if (lang === 'rust' || lang === 'rs') {
127
- return highlightRust(line);
128
- }
129
- if (lang === 'css' || lang === 'scss' || lang === 'less') {
130
- return highlightCSS(line);
131
- }
132
- if (lang === 'html' || lang === 'xml' || lang === 'svg' || lang === 'htm') {
133
- return highlightHTML(line);
134
- }
135
-
136
- // Default: no highlighting
137
- return line;
138
- }
139
-
140
- function highlightJS(line) {
141
- const keywords = /\b(const|let|var|function|return|if|else|for|while|class|import|export|from|require|async|await|new|this|throw|try|catch|switch|case|break|default|typeof|instanceof)\b/g;
142
- const strings = /(["'`])(?:(?=(\\?))\2.)*?\1/g;
143
- const comments = /(\/\/.*$)/;
144
- const numbers = /\b(\d+\.?\d*)\b/g;
145
-
146
- let result = line;
147
- // Order matters: comments last (they override everything)
148
- result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
149
- result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
150
- result = result.replace(strings, `${C.green}$&${C.reset}`);
151
- result = result.replace(comments, `${C.dim}$1${C.reset}`);
152
-
153
- return result;
154
- }
155
-
156
- function highlightBash(line) {
157
- const commands = /^(\s*)([\w-]+)/;
158
- const flags = /(--?\w[\w-]*)/g;
159
- const strings = /(["'])(?:(?=(\\?))\2.)*?\1/g;
160
- const comments = /(#.*$)/;
161
-
162
- let result = line;
163
- result = result.replace(flags, `${C.cyan}$1${C.reset}`);
164
- result = result.replace(commands, `$1${C.green}$2${C.reset}`);
165
- result = result.replace(strings, `${C.yellow}$&${C.reset}`);
166
- result = result.replace(comments, `${C.dim}$1${C.reset}`);
167
-
168
- return result;
169
- }
170
-
171
- function highlightJSON(line) {
172
- const keys = /("[\w-]+")\s*:/g;
173
- const strings = /:\s*("(?:[^"\\]|\\.)*")/g;
174
- const numbers = /:\s*(\d+\.?\d*)/g;
175
- const booleans = /:\s*(true|false|null)/g;
176
-
177
- let result = line;
178
- result = result.replace(keys, `${C.cyan}$1${C.reset}:`);
179
- result = result.replace(strings, `: ${C.green}$1${C.reset}`);
180
- result = result.replace(numbers, `: ${C.yellow}$1${C.reset}`);
181
- result = result.replace(booleans, `: ${C.magenta}$1${C.reset}`);
182
-
183
- return result;
184
- }
185
-
186
- function highlightPython(line) {
187
- const keywords = /\b(def|class|if|elif|else|for|while|return|import|from|as|try|except|finally|raise|with|yield|lambda|pass|break|continue|and|or|not|in|is|None|True|False|self|async|await|nonlocal|global)\b/g;
188
- const strings = /("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g;
189
- const comments = /(#.*$)/;
190
- const numbers = /\b(\d+\.?\d*)\b/g;
191
- const decorators = /^(\s*@\w+)/;
192
-
193
- let result = line;
194
- result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
195
- result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
196
- result = result.replace(decorators, `${C.cyan}$1${C.reset}`);
197
- result = result.replace(strings, `${C.green}$&${C.reset}`);
198
- result = result.replace(comments, `${C.dim}$1${C.reset}`);
199
-
200
- return result;
201
- }
202
-
203
- function highlightGo(line) {
204
- const keywords = /\b(func|package|import|var|const|type|struct|interface|map|chan|go|defer|return|if|else|for|range|switch|case|default|break|continue|select|fallthrough|nil|true|false|make|new|len|cap|append|copy|delete|panic|recover)\b/g;
205
- const types = /\b(string|int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float32|float64|bool|byte|rune|error|any)\b/g;
206
- const strings = /(["'`])(?:(?=(\\?))\2.)*?\1/g;
207
- const comments = /(\/\/.*$)/;
208
- const numbers = /\b(\d+\.?\d*)\b/g;
209
-
210
- let result = line;
211
- result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
212
- result = result.replace(types, `${C.cyan}$1${C.reset}`);
213
- result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
214
- result = result.replace(strings, `${C.green}$&${C.reset}`);
215
- result = result.replace(comments, `${C.dim}$1${C.reset}`);
216
-
217
- return result;
218
- }
219
-
220
- function highlightRust(line) {
221
- const keywords = /\b(fn|let|mut|const|struct|enum|impl|trait|pub|use|mod|crate|self|super|match|if|else|for|while|loop|return|break|continue|where|as|in|ref|move|async|await|unsafe|extern|type|static|dyn|macro_rules)\b/g;
222
- const types = /\b(i8|i16|i32|i64|i128|u8|u16|u32|u64|u128|f32|f64|bool|char|str|String|Vec|Option|Result|Box|Rc|Arc|Self|Some|None|Ok|Err|true|false)\b/g;
223
- const strings = /(["'])(?:(?=(\\?))\2.)*?\1/g;
224
- const comments = /(\/\/.*$)/;
225
- const numbers = /\b(\d+\.?\d*)\b/g;
226
- const macros = /\b(\w+!)/g;
227
-
228
- let result = line;
229
- result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
230
- result = result.replace(types, `${C.cyan}$1${C.reset}`);
231
- result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
232
- result = result.replace(macros, `${C.yellow}$1${C.reset}`);
233
- result = result.replace(strings, `${C.green}$&${C.reset}`);
234
- result = result.replace(comments, `${C.dim}$1${C.reset}`);
235
-
236
- return result;
237
- }
238
-
239
- function highlightCSS(line) {
240
- const properties = /^(\s*)([\w-]+)\s*:/;
241
- const values = /:\s*([^;]+)/;
242
- const selectors = /^(\s*[.#@][\w-]+)/;
243
- const numbers = /\b(\d+\.?\d*(px|em|rem|%|vh|vw|s|ms|deg|fr)?)\b/g;
244
- const comments = /(\/\*.*?\*\/|\/\/.*$)/;
245
- const colors = /(#[0-9a-fA-F]{3,8})\b/g;
246
-
247
- let result = line;
248
- result = result.replace(colors, `${C.yellow}$1${C.reset}`);
249
- result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
250
- result = result.replace(properties, `$1${C.cyan}$2${C.reset}:`);
251
- result = result.replace(selectors, `$1${C.magenta}$&${C.reset}`);
252
- result = result.replace(comments, `${C.dim}$1${C.reset}`);
253
-
254
- return result;
255
- }
256
-
257
- function highlightHTML(line) {
258
- const tags = /<\/?(\w[\w-]*)/g;
259
- const attrs = /\s([\w-]+)=/g;
260
- const strings = /(["'])(?:(?=(\\?))\2.)*?\1/g;
261
- const comments = /(<!--.*?-->)/g;
262
- const entities = /(&\w+;)/g;
263
-
264
- let result = line;
265
- result = result.replace(comments, `${C.dim}$1${C.reset}`);
266
- result = result.replace(strings, `${C.green}$&${C.reset}`);
267
- result = result.replace(tags, `<${C.magenta}$1${C.reset}`);
268
- result = result.replace(attrs, ` ${C.cyan}$1${C.reset}=`);
269
- result = result.replace(entities, `${C.yellow}$1${C.reset}`);
270
-
271
- return result;
272
- }
273
-
274
- /**
275
- * Render a table in the terminal
276
- * @param {string[]} headers
277
- * @param {string[][]} rows
278
- * @returns {string}
279
- */
280
- function renderTable(headers, rows) {
281
- if (!headers || headers.length === 0) return '';
282
-
283
- // Calculate column widths
284
- const widths = headers.map((h, i) => {
285
- const maxRow = rows.reduce((max, row) => Math.max(max, (row[i] || '').length), 0);
286
- return Math.max(h.length, maxRow);
287
- });
288
-
289
- const sep = widths.map((w) => '─'.repeat(w + 2)).join('┼');
290
- const headerLine = headers.map((h, i) => ` ${C.bold}${h.padEnd(widths[i])}${C.reset} `).join('│');
291
-
292
- const lines = [];
293
- lines.push(`${C.dim}┌${sep.replace(/┼/g, '┬')}┐${C.reset}`);
294
- lines.push(`${C.dim}│${C.reset}${headerLine}${C.dim}│${C.reset}`);
295
- lines.push(`${C.dim}├${sep}┤${C.reset}`);
296
-
297
- for (const row of rows) {
298
- const rowLine = headers.map((_, i) => ` ${(row[i] || '').padEnd(widths[i])} `).join(`${C.dim}│${C.reset}`);
299
- lines.push(`${C.dim}│${C.reset}${rowLine}${C.dim}│${C.reset}`);
300
- }
301
-
302
- lines.push(`${C.dim}└${sep.replace(/┼/g, '┴')}┘${C.reset}`);
303
- return lines.join('\n');
304
- }
305
-
306
- /**
307
- * Render a progress bar
308
- * @param {string} label
309
- * @param {number} current
310
- * @param {number} total
311
- * @param {number} width
312
- * @returns {string}
313
- */
314
- function renderProgress(label, current, total, width = 30) {
315
- const pct = total > 0 ? Math.round((current / total) * 100) : 0;
316
- const filled = Math.round((pct / 100) * width);
317
- const empty = width - filled;
318
- const color = pct >= 100 ? C.green : pct > 50 ? C.yellow : C.cyan;
319
-
320
- return ` ${label} ${color}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset} ${pct}% (${current}/${total})`;
321
- }
322
-
323
- /**
324
- * StreamRenderer — renders markdown line-by-line as tokens arrive.
325
- * Buffers partial lines, flushes complete lines with rendering.
326
- */
327
- class StreamRenderer {
328
- constructor() {
329
- this.buffer = '';
330
- this.inCodeBlock = false;
331
- this.codeBlockLang = '';
332
- this.lineCount = 0;
333
- // Streaming cursor state
334
- this._cursorTimer = null;
335
- this._cursorFrame = 0;
336
- this._cursorActive = false;
337
- }
338
-
339
- /** Write to stdout, silently ignoring EPIPE errors after abort */
340
- _safeWrite(data) {
341
- try { process.stdout.write(data); } catch (e) { if (e.code !== 'EPIPE') throw e; }
342
- }
343
-
344
- /** Write to stderr (same stream as Spinner) for cursor animation */
345
- _cursorWrite(data) {
346
- try { process.stderr.write(data); } catch (e) { if (e.code !== 'EPIPE') throw e; }
347
- }
348
-
349
- startCursor() {
350
- this._cursorActive = true;
351
- this._cursorFrame = 0;
352
- this._cursorWrite('\x1b[?25l'); // hide terminal cursor
353
- this._renderCursor();
354
- this._cursorTimer = setInterval(() => this._renderCursor(), 80);
355
- }
356
-
357
- _renderCursor() {
358
- // Same braille spinner as Thinking... — seamless continuation on stderr
359
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
360
- const f = frames[this._cursorFrame % frames.length];
361
- this._cursorWrite(`\x1b[2K\r\x1b[36m${f}\x1b[0m`);
362
- this._cursorFrame++;
363
- }
364
-
365
- _clearCursorLine() {
366
- if (this._cursorActive) {
367
- this._cursorWrite('\x1b[2K\r');
368
- }
369
- }
370
-
371
- stopCursor() {
372
- if (this._cursorTimer) {
373
- clearInterval(this._cursorTimer);
374
- this._cursorTimer = null;
375
- }
376
- if (this._cursorActive) {
377
- this._cursorWrite('\x1b[2K\r\x1b[?25h'); // clear line + show terminal cursor
378
- this._cursorActive = false;
379
- }
380
- }
381
-
382
- /**
383
- * Push a token chunk into the stream renderer.
384
- * Renders complete lines immediately; buffers partial lines.
385
- */
386
- push(text) {
387
- if (!text) return;
388
- this._clearCursorLine();
389
- this.buffer += text;
390
-
391
- // Process all complete lines
392
- let nlIdx;
393
- while ((nlIdx = this.buffer.indexOf('\n')) !== -1) {
394
- const line = this.buffer.substring(0, nlIdx);
395
- this.buffer = this.buffer.substring(nlIdx + 1);
396
- this._renderLine(line);
397
- }
398
-
399
- if (this._cursorActive) {
400
- this._renderCursor();
401
- if (this._cursorTimer) clearInterval(this._cursorTimer);
402
- this._cursorTimer = setInterval(() => this._renderCursor(), 120);
403
- }
404
- }
405
-
406
- /**
407
- * Flush remaining buffer content (call at end of stream).
408
- */
409
- flush() {
410
- this.stopCursor();
411
- if (this.buffer) {
412
- this._renderLine(this.buffer);
413
- this.buffer = '';
414
- }
415
- // Reset state
416
- if (this.inCodeBlock) {
417
- this._safeWrite(`${C.dim}${'─'.repeat(40)}${C.reset}\n`);
418
- this.inCodeBlock = false;
419
- this.codeBlockLang = '';
420
- }
421
- }
422
-
423
- _renderLine(line) {
424
- // Code block toggle
425
- if (line.trim().startsWith('```')) {
426
- if (this.inCodeBlock) {
427
- this._safeWrite(`${C.dim}${'─'.repeat(40)}${C.reset}\n`);
428
- this.inCodeBlock = false;
429
- this.codeBlockLang = '';
430
- } else {
431
- this.inCodeBlock = true;
432
- this.codeBlockLang = line.trim().substring(3).trim();
433
- const label = this.codeBlockLang ? ` ${this.codeBlockLang} ` : '';
434
- this._safeWrite(`${C.dim}${'─'.repeat(3)}${label}${'─'.repeat(Math.max(0, 37 - label.length))}${C.reset}\n`);
435
- }
436
- return;
437
- }
438
-
439
- if (this.inCodeBlock) {
440
- this._safeWrite(` ${highlightCode(line, this.codeBlockLang)}\n`);
441
- return;
442
- }
443
-
444
- // Headers
445
- if (line.startsWith('### ')) {
446
- this._safeWrite(`${C.bold}${C.cyan} ${line.substring(4)}${C.reset}\n`);
447
- return;
448
- }
449
- if (line.startsWith('## ')) {
450
- this._safeWrite(`${C.bold}${C.cyan} ${line.substring(3)}${C.reset}\n`);
451
- return;
452
- }
453
- if (line.startsWith('# ')) {
454
- this._safeWrite(`${C.bold}${C.cyan}${line.substring(2)}${C.reset}\n`);
455
- return;
456
- }
457
-
458
- // Lists
459
- if (/^\s*[-*]\s/.test(line)) {
460
- const indent = line.match(/^(\s*)/)[1];
461
- const content = line.replace(/^\s*[-*]\s/, '');
462
- this._safeWrite(`${indent}${C.cyan}•${C.reset} ${renderInline(content)}\n`);
463
- return;
464
- }
465
-
466
- // Numbered lists
467
- if (/^\s*\d+\.\s/.test(line)) {
468
- const match = line.match(/^(\s*)(\d+)\.\s(.*)/);
469
- if (match) {
470
- this._safeWrite(`${match[1]}${C.cyan}${match[2]}.${C.reset} ${renderInline(match[3])}\n`);
471
- return;
472
- }
473
- }
474
-
475
- // Regular line
476
- this._safeWrite(`${renderInline(line)}\n`);
477
- }
478
- }
479
-
480
- module.exports = {
481
- renderMarkdown,
482
- renderInline,
483
- highlightCode,
484
- highlightJS,
485
- highlightBash,
486
- highlightJSON,
487
- highlightPython,
488
- highlightGo,
489
- highlightRust,
490
- highlightCSS,
491
- highlightHTML,
492
- renderTable,
493
- renderProgress,
494
- StreamRenderer,
495
- };