@noobdemon/noob-cli 1.12.2 → 1.12.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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
4
4
 
5
+ ## [1.12.4] - 2026-06-12
6
+
7
+ ### Fixed
8
+ - **Bảng markdown không render khi stream** (`src/repl.js`): fix `1.12.3` chỉ áp dụng cho path NON-STREAM (`printAnswer` → `renderMarkdown`). Khi stream từng dòng qua `makeStreamPrinter`, mỗi dòng render độc lập qua `renderStreamLine` (chỉ hiểu heading + inline) → bảng `| ... |` đi raw ra terminal. Giờ thêm `renderStreamBlock(lines, fenceState)` export, detect table bằng cặp header-row + separator regex `TABLE_SEP_RE` (`/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/`), gom các row liên tiếp → 1 call `renderMarkdown`. `flushCompleteLines` HOLD các dòng cuối là table-row/sep candidate (max 64 dòng) để chờ đủ data — nếu flush ngay khi gặp `\n` thì header bị emit trước, separator đến sau không gom được. Cả `push(beforeTool)` và `flush()` cũng route qua `renderStreamBlock`. Code fence vẫn bypass ưu tiên cao nhất. Smoke `scripts/smoke-table-stream2.mjs` 5/5 PASS (table đơn, prose+table+prose, no-table regression, code fence bypass, false-positive pipe in prose).
9
+
10
+ ## [1.12.3] - 2026-06-12
11
+
12
+ ### Fixed
13
+ - **Bảng markdown không render khi user paste có indent** (`src/ui.js`): `renderMarkdown` cũ pass thẳng input qua `marked.parse()` — nếu mọi dòng bị indent ≥4 space (case thường gặp khi copy/paste từ list item hoặc reply), CommonMark treat là indented code block → bảng/heading/list không parse, in ra dạng raw pipe `| A | B |`. Fix: thêm hàm `dedent(md)` strip common leading whitespace từ mọi dòng non-empty TRƯỚC khi gọi `marked.parse()`. Đồng thời gỡ regex `.replace(/^ {4}(.*)$/gm, '▎ ')` trong `prettify` — handler cũ cho code-block-by-indent, dư thừa (marked đã render code fence riêng) và còn nuốt nhầm dòng output bảng nếu lọt qua. Verify: bảng indent 2/4 space + bảng giữa prose + regression heading/list/code fence/blockquote đều OK.
14
+
5
15
  ## [1.12.2] - 2026-06-12
6
16
 
7
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.12.2",
3
+ "version": "1.12.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/repl.js CHANGED
@@ -2138,12 +2138,69 @@ function printAnswer(text, name, color) {
2138
2138
  // - Trên dòng đang dở (chưa thấy \n), giữ trong buffer; cuối cùng `flush()` xử lý.
2139
2139
  // - Block code fence (```lang ... ```) bypass renderer: in nguyên xi giữa cặp ```.
2140
2140
  // - ```tool trở đi: nuốt sạch (sẽ render riêng qua tool dispatcher).
2141
+ // - Bảng markdown (header + separator |---|) cần parse đa dòng → gom thành
2142
+ // table block, render qua marked.parse() 1 lần (renderStreamBlock).
2143
+ // QUAN TRỌNG: stream line-by-line không tương thích với block elements
2144
+ // đa dòng — flushCompleteLines phải HOLD các dòng cuối là table-row/sep
2145
+ // candidate để chờ đủ data trước khi parse.
2141
2146
  function renderStreamLine(line, inCodeFence) {
2142
2147
  if (inCodeFence) return line; // code fence body: in raw, không parse markdown.
2143
2148
  const heading = renderHeadingLine(line);
2144
2149
  if (heading !== null) return heading;
2145
2150
  return renderInline(renderBulletPrefix(line));
2146
2151
  }
2152
+ // Dòng separator của bảng markdown: |---|---| hoặc |:---:|---:| v.v.
2153
+ const TABLE_SEP_RE = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/;
2154
+ // Dòng có ít nhất 1 pipe ở giữa (không tính pipe đầu/cuối) → ứng viên row của bảng.
2155
+ function looksLikeTableRow(line) {
2156
+ const trimmed = line.trim();
2157
+ if (!trimmed.startsWith('|') && !trimmed.includes('|')) return false;
2158
+ // Cần ít nhất 1 ký tự `|` thực sự phân tách cột.
2159
+ const stripped = trimmed.replace(/^\|/, '').replace(/\|$/, '');
2160
+ return stripped.includes('|');
2161
+ }
2162
+ // Render block lines (mảng dòng KHÔNG có \n cuối) thành ANSI. Detect table block
2163
+ // và parse qua marked; phần còn lại render line-by-line như cũ.
2164
+ export function renderStreamBlock(lines, fenceState) {
2165
+ const out = [];
2166
+ let i = 0;
2167
+ while (i < lines.length) {
2168
+ const ln = lines[i];
2169
+ // Code fence ưu tiên cao nhất — không đụng vào nội dung fence.
2170
+ const fenceMatch = ln.match(/^\s*```/);
2171
+ if (fenceMatch) {
2172
+ out.push(renderStreamLine(ln, fenceState.inCodeFence));
2173
+ fenceState.inCodeFence = !fenceState.inCodeFence;
2174
+ i++;
2175
+ continue;
2176
+ }
2177
+ if (fenceState.inCodeFence) {
2178
+ out.push(ln);
2179
+ i++;
2180
+ continue;
2181
+ }
2182
+ // Detect table: dòng hiện tại là row + dòng kế tiếp là separator.
2183
+ if (
2184
+ looksLikeTableRow(ln) &&
2185
+ i + 1 < lines.length &&
2186
+ TABLE_SEP_RE.test(lines[i + 1])
2187
+ ) {
2188
+ // Gom hết các dòng row liên tiếp.
2189
+ const tableLines = [ln, lines[i + 1]];
2190
+ let j = i + 2;
2191
+ while (j < lines.length && looksLikeTableRow(lines[j])) {
2192
+ tableLines.push(lines[j]);
2193
+ j++;
2194
+ }
2195
+ out.push(renderMarkdown(tableLines.join('\n')));
2196
+ i = j;
2197
+ continue;
2198
+ }
2199
+ out.push(renderStreamLine(ln, false));
2200
+ i++;
2201
+ }
2202
+ return out.join('\n');
2203
+ }
2147
2204
  function makeStreamPrinter(name, color) {
2148
2205
  let buf = ''; // toàn bộ delta đã nhận (chưa cắt)
2149
2206
  let printed = 0; // offset đã in trong buf
@@ -2164,28 +2221,36 @@ function makeStreamPrinter(name, color) {
2164
2221
  };
2165
2222
  // Render & emit từ printed → end. Chỉ flush các dòng đã HOÀN CHỈNH (có \n).
2166
2223
  // Dòng cuối chưa có \n: chừa lại trong buf cho lần push tiếp theo.
2224
+ // Lùi mốc flush nếu các dòng cuối là table-row/separator candidate — chờ đủ
2225
+ // data để gom thành table block parse 1 lần qua marked (xem renderStreamBlock).
2167
2226
  const flushCompleteLines = (end) => {
2168
2227
  const slice = buf.slice(printed, end);
2169
2228
  if (!slice) return;
2170
2229
  const lastNl = slice.lastIndexOf('\n');
2171
- if (lastNl === -1) return; // chưa có dòng nào hoàn chỉnh trong khoảng này
2172
- const complete = slice.slice(0, lastNl + 1); // bao gồm \n cuối
2173
- printed += lastNl + 1;
2174
- // Render từng dòng (split giữ \n)
2175
- const lines = complete.split('\n');
2176
- // split với chuỗi kết thúc \n → mảng cuối là '' — bỏ qua
2177
- const rendered = lines.map((ln, i) => {
2178
- if (i === lines.length - 1 && ln === '') return '';
2179
- // Toggle code fence khi gặp ``` ở đầu dòng (allow indent).
2180
- const fenceMatch = ln.match(/^\s*```/);
2181
- if (fenceMatch) {
2182
- const out = renderStreamLine(ln, inCodeFence);
2183
- inCodeFence = !inCodeFence;
2184
- return out;
2185
- }
2186
- return renderStreamLine(ln, inCodeFence);
2187
- }).join('\n');
2188
- emit(rendered);
2230
+ if (lastNl === -1) return;
2231
+ let complete = slice.slice(0, lastNl + 1);
2232
+ let completeLines = complete.split('\n');
2233
+ if (completeLines[completeLines.length - 1] === '') completeLines.pop();
2234
+ let hold = 0;
2235
+ while (
2236
+ completeLines.length - hold > 0 &&
2237
+ (looksLikeTableRow(completeLines[completeLines.length - 1 - hold]) ||
2238
+ TABLE_SEP_RE.test(completeLines[completeLines.length - 1 - hold]))
2239
+ ) {
2240
+ hold++;
2241
+ if (hold > 64) break;
2242
+ }
2243
+ if (hold > 0) {
2244
+ const keptLines = completeLines.slice(0, completeLines.length - hold);
2245
+ if (keptLines.length === 0) return; // toàn bộ slice là table candidate → chờ
2246
+ complete = keptLines.join('\n') + '\n';
2247
+ completeLines = keptLines;
2248
+ }
2249
+ printed += complete.length;
2250
+ const fenceState = { inCodeFence };
2251
+ const rendered = renderStreamBlock(completeLines, fenceState);
2252
+ inCodeFence = fenceState.inCodeFence;
2253
+ emit(rendered + '\n');
2189
2254
  };
2190
2255
  return {
2191
2256
  get started() {
@@ -2203,17 +2268,12 @@ function makeStreamPrinter(name, color) {
2203
2268
  // vì sắp suppress, không cần chừa buffer nữa).
2204
2269
  const beforeTool = buf.slice(printed, f);
2205
2270
  if (beforeTool) {
2206
- // Render từng dòng kể cả dòng dở cuối
2207
2271
  const parts = beforeTool.split('\n');
2208
- const rendered = parts.map((ln) => {
2209
- const fenceMatch = ln.match(/^\s*```/);
2210
- if (fenceMatch) {
2211
- const out = renderStreamLine(ln, inCodeFence);
2212
- inCodeFence = !inCodeFence;
2213
- return out;
2214
- }
2215
- return renderStreamLine(ln, inCodeFence);
2216
- }).join('\n');
2272
+ // bỏ '' cuối nếu kết thúc bằng \n
2273
+ if (parts[parts.length - 1] === '') parts.pop();
2274
+ const fenceState = { inCodeFence };
2275
+ const rendered = renderStreamBlock(parts, fenceState);
2276
+ inCodeFence = fenceState.inCodeFence;
2217
2277
  emit(rendered);
2218
2278
  }
2219
2279
  printed = buf.length;
@@ -2229,16 +2289,10 @@ function makeStreamPrinter(name, color) {
2229
2289
  // Render phần còn lại (kể cả dòng cuối chưa có \n).
2230
2290
  const tail = buf.slice(printed);
2231
2291
  const parts = tail.split('\n');
2232
- const rendered = parts.map((ln, i) => {
2233
- // Toggle fence
2234
- const fenceMatch = ln.match(/^\s*```/);
2235
- if (fenceMatch) {
2236
- const out = renderStreamLine(ln, inCodeFence);
2237
- inCodeFence = !inCodeFence;
2238
- return out;
2239
- }
2240
- return renderStreamLine(ln, inCodeFence);
2241
- }).join('\n');
2292
+ if (parts[parts.length - 1] === '') parts.pop();
2293
+ const fenceState = { inCodeFence };
2294
+ const rendered = renderStreamBlock(parts, fenceState);
2295
+ inCodeFence = fenceState.inCodeFence;
2242
2296
  emit(rendered);
2243
2297
  printed = buf.length;
2244
2298
  }
package/src/ui.js CHANGED
@@ -148,14 +148,32 @@ export function renderBulletPrefix(line) {
148
148
  function prettify(s) {
149
149
  return s
150
150
  .replace(/^( *)\* /gm, (_, sp) => sp + BULLET + ' ')
151
- .replace(/^ {4}(.*)$/gm, (_, rest) => c.dim('▎ ') + rest)
152
151
  .split('\n')
153
152
  .map(renderInline)
154
153
  .join('\n');
155
154
  }
156
155
 
156
+ // Strip common leading whitespace từ mọi dòng non-empty. Cần thiết vì user hay
157
+ // paste markdown bị indent (vd copy từ list item, từ code block) → CommonMark
158
+ // treat indent ≥4 space là code block → bảng/heading/list không parse.
159
+ function dedent(md) {
160
+ if (!md) return md;
161
+ const lines = md.split('\n');
162
+ let min = Infinity;
163
+ for (const line of lines) {
164
+ if (!line.trim()) continue;
165
+ const m = line.match(/^( *)/);
166
+ const indent = m ? m[1].length : 0;
167
+ if (indent < min) min = indent;
168
+ if (min === 0) break;
169
+ }
170
+ if (!Number.isFinite(min) || min === 0) return md;
171
+ const re = new RegExp(`^ {${min}}`, 'gm');
172
+ return md.replace(re, '');
173
+ }
174
+
157
175
  export function renderMarkdown(md) {
158
- return prettify(marked.parse(md || '')).trimEnd();
176
+ return prettify(marked.parse(dedent(md || ''))).trimEnd();
159
177
  }
160
178
 
161
179
  // ── formatQuota ────────────────────────────────────────────────────────────