toolcraft 0.0.77 → 0.0.79

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.
@@ -8,7 +8,7 @@ Shared terminal design system for Toolcraft applications. It provides design tok
8
8
  npm install toolcraft
9
9
  ```
10
10
 
11
- `toolcraft-design` is currently distributed through `toolcraft`. Import the design system from the bundled `toolcraft/design` entrypoint:
11
+ `toolcraft-design` is currently distributed through `toolcraft`, not installed directly from npm as a standalone package. Import the design system from the bundled `toolcraft/design` entrypoint:
12
12
 
13
13
  ```ts
14
14
  import {
@@ -45,7 +45,7 @@ export * as staticRender from "./static/index.js";
45
45
  export { SPINNER_FRAMES, renderSpinnerFrame, renderSpinnerStopped, renderMenu } from "./static/index.js";
46
46
  export type { SpinnerFrameOptions, SpinnerStoppedOptions, MenuOption, RenderMenuOptions } from "./static/index.js";
47
47
  export { parse, render, renderHtml, renderMarkdown, renderMarkdownHtml } from "./terminal-markdown/index.js";
48
- export type { MdNode, RenderOptions, HtmlRenderOptions } from "./terminal-markdown/index.js";
48
+ export type { CodeToken, CodeTokenKind, HtmlRenderOptions, MdNode, RenderOptions } from "./terminal-markdown/index.js";
49
49
  export { getTheme, resolveThemeName, resetThemeCache } from "./internal/theme-detect.js";
50
50
  export type { ThemeEnv } from "./internal/theme-detect.js";
51
51
  export { configureTheme, getThemeConfig, resetTheme } from "./internal/theme-state.js";
@@ -2,6 +2,11 @@ export type MdRange = {
2
2
  start: number;
3
3
  end: number;
4
4
  };
5
+ export type CodeTokenKind = "anchor" | "at-rule" | "attribute" | "boolean" | "color" | "command" | "comment" | "decorator" | "directive" | "flag" | "function" | "identifier" | "important" | "invalid" | "key" | "keyword" | "label" | "null" | "number" | "operator" | "parameter" | "plain" | "property" | "punctuation" | "regex" | "selector" | "string" | "tag" | "template" | "type" | "variable";
6
+ export type CodeToken = {
7
+ kind: CodeTokenKind;
8
+ value: string;
9
+ };
5
10
  type MdNodeWithRange = {
6
11
  range?: MdRange;
7
12
  };
@@ -23,6 +28,7 @@ export type MdNode = MdNodeWithRange & ({
23
28
  lang?: string;
24
29
  meta?: string;
25
30
  value: string;
31
+ tokens?: CodeToken[];
26
32
  } | {
27
33
  type: "list";
28
34
  ordered: boolean;
@@ -2,5 +2,6 @@ import type { MdNode } from "./ast.js";
2
2
  export interface HtmlRenderOptions {
3
3
  showFrontmatter?: boolean;
4
4
  allowRawHtml?: boolean;
5
+ syntaxHighlight?: boolean;
5
6
  }
6
7
  export declare function renderHtml(ast: MdNode, options?: HtmlRenderOptions): string;
@@ -1,7 +1,9 @@
1
+ import { highlightCodeBlock } from "./parser/code-highlight.js";
1
2
  export function renderHtml(ast, options = {}) {
2
3
  const context = {
3
4
  showFrontmatter: options.showFrontmatter ?? false,
4
5
  allowRawHtml: options.allowRawHtml ?? false,
6
+ syntaxHighlight: options.syntaxHighlight ?? false,
5
7
  footnotes: ast.type === "root" ? createFootnoteState(ast.children) : undefined
6
8
  };
7
9
  return renderNode(ast, context);
@@ -19,7 +21,7 @@ function renderNode(node, context) {
19
21
  case "alert":
20
22
  return renderAlert(node, context);
21
23
  case "code":
22
- return renderCodeBlock(node);
24
+ return renderCodeBlock(node, context);
23
25
  case "list":
24
26
  return renderList(node, context);
25
27
  case "table":
@@ -79,11 +81,19 @@ function renderAlert(node, context) {
79
81
  const content = renderChildren(node.children, context);
80
82
  return `<blockquote data-alert="${escapeAttribute(node.kind)}">${content}</blockquote>`;
81
83
  }
82
- function renderCodeBlock(node) {
84
+ function renderCodeBlock(node, context) {
83
85
  const classAttribute = node.lang === undefined || node.lang.length === 0
84
86
  ? ""
85
87
  : ` class="language-${escapeAttribute(node.lang)}"`;
86
- return `<pre><code${classAttribute}>${escapeHtml(node.value)}</code></pre>`;
88
+ const tokens = context.syntaxHighlight ? highlightCodeBlock(node) : undefined;
89
+ const content = tokens === undefined
90
+ ? escapeHtml(node.value)
91
+ : tokens
92
+ .map((token) => token.kind === "plain"
93
+ ? escapeHtml(token.value)
94
+ : `<span class="tc-token-${escapeAttribute(token.kind)}">${escapeHtml(token.value)}</span>`)
95
+ .join("");
96
+ return `<pre><code${classAttribute}>${content}</code></pre>`;
87
97
  }
88
98
  function renderList(node, context) {
89
99
  const tag = node.ordered ? "ol" : "ul";
@@ -1,6 +1,6 @@
1
1
  import { type HtmlRenderOptions } from "./html-renderer.js";
2
2
  import { type RenderOptions } from "./renderer.js";
3
- export type { MdNode } from "./ast.js";
3
+ export type { CodeToken, CodeTokenKind, MdNode } from "./ast.js";
4
4
  export { renderHtml } from "./html-renderer.js";
5
5
  export type { HtmlRenderOptions } from "./html-renderer.js";
6
6
  export { parse } from "./parser.js";
@@ -0,0 +1,4 @@
1
+ import type { CodeToken, MdNode } from "../ast.js";
2
+ export declare function highlightCodeBlock(node: Pick<Extract<MdNode, {
3
+ type: "code";
4
+ }>, "lang" | "value" | "tokens">): CodeToken[] | undefined;
@@ -0,0 +1,618 @@
1
+ const codeLanguages = [
2
+ {
3
+ id: "javascript",
4
+ aliases: ["js", "javascript", "mjs", "cjs", "es6"],
5
+ family: "lexical",
6
+ spec: "javascript"
7
+ },
8
+ { id: "javascriptreact", aliases: ["jsx"], family: "lexical", spec: "javascript" },
9
+ {
10
+ id: "typescript",
11
+ aliases: ["ts", "typescript", "mts", "cts"],
12
+ family: "lexical",
13
+ spec: "typescript"
14
+ },
15
+ { id: "typescriptreact", aliases: ["tsx"], family: "lexical", spec: "typescript" },
16
+ { id: "json", aliases: ["json"], family: "data", spec: "json" },
17
+ { id: "jsonc", aliases: ["jsonc"], family: "data", spec: "jsonc" },
18
+ { id: "jsonl", aliases: ["jsonl"], family: "data", spec: "json" },
19
+ { id: "yaml", aliases: ["yaml", "yml"], family: "data", spec: "yaml" },
20
+ { id: "css", aliases: ["css"], family: "style", spec: "css" },
21
+ { id: "scss", aliases: ["scss"] },
22
+ { id: "sass", aliases: ["sass"] },
23
+ { id: "less", aliases: ["less"] },
24
+ { id: "postcss", aliases: ["postcss"] },
25
+ { id: "shellscript", aliases: ["sh", "bash", "shell", "shellscript", "zsh", "fish"] },
26
+ { id: "python", aliases: ["py", "python"] },
27
+ { id: "sql", aliases: ["sql", "ddl", "dml"] },
28
+ { id: "html", aliases: ["html"] },
29
+ { id: "xml", aliases: ["xml", "svg"] },
30
+ { id: "markdown", aliases: ["md", "markdown"] },
31
+ { id: "diff", aliases: ["diff", "patch"] },
32
+ { id: "dockerfile", aliases: ["dockerfile", "docker"] },
33
+ { id: "ini", aliases: ["ini", "properties"] },
34
+ { id: "toml", aliases: ["toml"] },
35
+ { id: "plaintext", aliases: ["text", "txt", "plain", "plaintext"], plain: true },
36
+ { id: "ruby", aliases: ["rb", "ruby"] },
37
+ { id: "go", aliases: ["go", "golang"] },
38
+ { id: "java", aliases: ["java"] },
39
+ { id: "c", aliases: ["c"] },
40
+ { id: "cpp", aliases: ["cpp", "c++", "cc", "cxx"] },
41
+ { id: "csharp", aliases: ["cs", "csharp", "c#"] },
42
+ { id: "rust", aliases: ["rs", "rust"] },
43
+ { id: "php", aliases: ["php"] }
44
+ ];
45
+ const languageByAlias = new Map();
46
+ for (const language of codeLanguages) {
47
+ languageByAlias.set(language.id.toLowerCase(), language);
48
+ for (const alias of language.aliases) {
49
+ languageByAlias.set(alias.toLowerCase(), language);
50
+ }
51
+ }
52
+ const jsKeywords = new Set([
53
+ "as",
54
+ "async",
55
+ "await",
56
+ "break",
57
+ "case",
58
+ "catch",
59
+ "class",
60
+ "const",
61
+ "continue",
62
+ "debugger",
63
+ "default",
64
+ "delete",
65
+ "do",
66
+ "else",
67
+ "export",
68
+ "extends",
69
+ "finally",
70
+ "for",
71
+ "from",
72
+ "function",
73
+ "get",
74
+ "if",
75
+ "import",
76
+ "in",
77
+ "instanceof",
78
+ "let",
79
+ "new",
80
+ "of",
81
+ "return",
82
+ "set",
83
+ "static",
84
+ "super",
85
+ "switch",
86
+ "throw",
87
+ "try",
88
+ "typeof",
89
+ "var",
90
+ "void",
91
+ "while",
92
+ "with",
93
+ "yield"
94
+ ]);
95
+ const tsKeywords = new Set([
96
+ ...jsKeywords,
97
+ "abstract",
98
+ "declare",
99
+ "enum",
100
+ "implements",
101
+ "interface",
102
+ "keyof",
103
+ "namespace",
104
+ "private",
105
+ "protected",
106
+ "public",
107
+ "readonly",
108
+ "satisfies",
109
+ "type"
110
+ ]);
111
+ const tsTypes = new Set([
112
+ "any",
113
+ "bigint",
114
+ "boolean",
115
+ "never",
116
+ "number",
117
+ "object",
118
+ "string",
119
+ "symbol",
120
+ "unknown",
121
+ "void"
122
+ ]);
123
+ const jsConstants = new Set(["true", "false", "null", "undefined", "NaN", "Infinity"]);
124
+ const lexicalSpecs = {
125
+ javascript: {
126
+ keywords: jsKeywords,
127
+ constants: jsConstants,
128
+ lineComments: ["//"],
129
+ blockComments: true,
130
+ stringQuotes: ['"', "'"],
131
+ templateQuotes: true,
132
+ decorators: true
133
+ },
134
+ typescript: {
135
+ keywords: tsKeywords,
136
+ types: tsTypes,
137
+ constants: jsConstants,
138
+ lineComments: ["//"],
139
+ blockComments: true,
140
+ stringQuotes: ['"', "'"],
141
+ templateQuotes: true,
142
+ decorators: true
143
+ }
144
+ };
145
+ const tokenizers = {
146
+ lexical: tokenizeLexical,
147
+ data: tokenizeData,
148
+ style: tokenizeStyle,
149
+ line: tokenizeLine
150
+ };
151
+ export function highlightCodeBlock(node) {
152
+ if (node.tokens !== undefined) {
153
+ return node.tokens;
154
+ }
155
+ const language = resolveCodeLanguage(node.lang);
156
+ if (language === undefined ||
157
+ language.plain === true ||
158
+ language.family === undefined ||
159
+ node.value.length === 0) {
160
+ return undefined;
161
+ }
162
+ const tokenize = tokenizers[language.family];
163
+ const tokens = tokenize(node.value, language);
164
+ return tokens.some((token) => token.kind !== "plain") ? tokens : undefined;
165
+ }
166
+ function resolveCodeLanguage(lang) {
167
+ if (lang === undefined || lang.length === 0) {
168
+ return undefined;
169
+ }
170
+ return languageByAlias.get(lang.toLowerCase());
171
+ }
172
+ function tokenizeLexical(source, language) {
173
+ const spec = lexicalSpecs[language.spec ?? ""];
174
+ if (spec === undefined) {
175
+ return [{ kind: "plain", value: source }];
176
+ }
177
+ const emitter = createEmitter(source);
178
+ let index = 0;
179
+ while (index < source.length) {
180
+ const start = index;
181
+ const char = source[index];
182
+ index = readWhitespace(source, index);
183
+ if (index > start) {
184
+ emitter.pushPlain(start, index);
185
+ continue;
186
+ }
187
+ const lineCommentEnd = readAnyLineComment(source, index, spec.lineComments ?? []);
188
+ if (lineCommentEnd > index) {
189
+ emitter.push("comment", index, lineCommentEnd);
190
+ index = lineCommentEnd;
191
+ continue;
192
+ }
193
+ const blockCommentEnd = spec.blockComments === true ? readBlockComment(source, index) : index;
194
+ if (blockCommentEnd > index) {
195
+ emitter.push("comment", index, blockCommentEnd);
196
+ index = blockCommentEnd;
197
+ continue;
198
+ }
199
+ if (spec.decorators === true && char === "@" && isIdentifierStart(source[index + 1] ?? "")) {
200
+ index = readIdentifier(source, index + 1);
201
+ emitter.push("decorator", start, index);
202
+ continue;
203
+ }
204
+ if ((spec.stringQuotes ?? []).includes(char)) {
205
+ index = readQuotedString(source, index, char);
206
+ emitter.push("string", start, index);
207
+ continue;
208
+ }
209
+ if (spec.templateQuotes === true && char === "`") {
210
+ index = readQuotedString(source, index, "`");
211
+ emitter.push("template", start, index);
212
+ continue;
213
+ }
214
+ index = readNumber(source, index);
215
+ if (index > start) {
216
+ emitter.push("number", start, index);
217
+ continue;
218
+ }
219
+ index = readIdentifier(source, index);
220
+ if (index > start) {
221
+ emitter.push(classifyLexicalWord(source.slice(start, index), spec), start, index);
222
+ continue;
223
+ }
224
+ emitter.pushPlain(start, start + 1);
225
+ index = start + 1;
226
+ }
227
+ return emitter.tokens;
228
+ }
229
+ function tokenizeData(source, language) {
230
+ return language.spec === "yaml" ? tokenizeYaml(source) : tokenizeJsonLike(source, language.spec === "jsonc");
231
+ }
232
+ function tokenizeJsonLike(source, allowComments) {
233
+ const emitter = createEmitter(source);
234
+ let index = 0;
235
+ while (index < source.length) {
236
+ const start = index;
237
+ index = readWhitespace(source, index);
238
+ if (index > start) {
239
+ emitter.pushPlain(start, index);
240
+ continue;
241
+ }
242
+ if (allowComments) {
243
+ const lineCommentEnd = readAnyLineComment(source, index, ["//"]);
244
+ if (lineCommentEnd > index) {
245
+ emitter.push("comment", index, lineCommentEnd);
246
+ index = lineCommentEnd;
247
+ continue;
248
+ }
249
+ const blockCommentEnd = readBlockComment(source, index);
250
+ if (blockCommentEnd > index) {
251
+ emitter.push("comment", index, blockCommentEnd);
252
+ index = blockCommentEnd;
253
+ continue;
254
+ }
255
+ }
256
+ if (source[index] === '"') {
257
+ index = readQuotedString(source, index, '"');
258
+ emitter.push(isJsonKey(source, index) ? "key" : "string", start, index);
259
+ continue;
260
+ }
261
+ index = readNumber(source, index);
262
+ if (index > start) {
263
+ emitter.push("number", start, index);
264
+ continue;
265
+ }
266
+ index = readIdentifier(source, index);
267
+ if (index > start) {
268
+ emitter.push(classifyDataWord(source.slice(start, index)), start, index);
269
+ continue;
270
+ }
271
+ emitter.pushPlain(start, start + 1);
272
+ index = start + 1;
273
+ }
274
+ return emitter.tokens;
275
+ }
276
+ function tokenizeYaml(source) {
277
+ const emitter = createEmitter(source);
278
+ let index = 0;
279
+ let atLineStart = true;
280
+ while (index < source.length) {
281
+ const start = index;
282
+ if (source[index] === "\n") {
283
+ emitter.pushPlain(index, index + 1);
284
+ index += 1;
285
+ atLineStart = true;
286
+ continue;
287
+ }
288
+ const whitespaceEnd = readSpacesAndTabs(source, index);
289
+ if (whitespaceEnd > index) {
290
+ emitter.pushPlain(index, whitespaceEnd);
291
+ index = whitespaceEnd;
292
+ continue;
293
+ }
294
+ if (source[index] === "#") {
295
+ index = readUntilLineEnd(source, index);
296
+ emitter.push("comment", start, index);
297
+ atLineStart = false;
298
+ continue;
299
+ }
300
+ if (source[index] === '"' || source[index] === "'") {
301
+ const quote = source[index];
302
+ index = readQuotedString(source, index, quote);
303
+ emitter.push("string", start, index);
304
+ atLineStart = false;
305
+ continue;
306
+ }
307
+ if (atLineStart) {
308
+ const keyEnd = readYamlKey(source, index);
309
+ if (keyEnd > index) {
310
+ emitter.push("key", index, keyEnd);
311
+ index = keyEnd;
312
+ atLineStart = false;
313
+ continue;
314
+ }
315
+ }
316
+ index = readNumber(source, index);
317
+ if (index > start) {
318
+ emitter.push("number", start, index);
319
+ atLineStart = false;
320
+ continue;
321
+ }
322
+ index = readIdentifier(source, index);
323
+ if (index > start) {
324
+ emitter.push(classifyDataWord(source.slice(start, index)), start, index);
325
+ atLineStart = false;
326
+ continue;
327
+ }
328
+ emitter.pushPlain(start, start + 1);
329
+ index = start + 1;
330
+ atLineStart = false;
331
+ }
332
+ return emitter.tokens;
333
+ }
334
+ function tokenizeStyle(source) {
335
+ const emitter = createEmitter(source);
336
+ let index = 0;
337
+ while (index < source.length) {
338
+ const start = index;
339
+ index = readWhitespace(source, index);
340
+ if (index > start) {
341
+ emitter.pushPlain(start, index);
342
+ continue;
343
+ }
344
+ const blockCommentEnd = readBlockComment(source, index);
345
+ if (blockCommentEnd > index) {
346
+ emitter.push("comment", index, blockCommentEnd);
347
+ index = blockCommentEnd;
348
+ continue;
349
+ }
350
+ if (source[index] === "@") {
351
+ index = readCssName(source, index + 1);
352
+ if (index > start + 1) {
353
+ emitter.push("at-rule", start, index);
354
+ continue;
355
+ }
356
+ }
357
+ if (source[index] === "#" && isHex(source[index + 1] ?? "")) {
358
+ index = readCssColor(source, index + 1);
359
+ emitter.push("color", start, index);
360
+ continue;
361
+ }
362
+ if (source.startsWith("!important", index)) {
363
+ index += "!important".length;
364
+ emitter.push("important", start, index);
365
+ continue;
366
+ }
367
+ if (source[index] === '"' || source[index] === "'") {
368
+ const quote = source[index];
369
+ index = readQuotedString(source, index, quote);
370
+ emitter.push("string", start, index);
371
+ continue;
372
+ }
373
+ index = readNumber(source, index);
374
+ if (index > start) {
375
+ emitter.push("number", start, index);
376
+ continue;
377
+ }
378
+ index = readCssName(source, index);
379
+ if (index > start) {
380
+ emitter.push(isCssProperty(source, index) ? "property" : "selector", start, index);
381
+ continue;
382
+ }
383
+ emitter.pushPlain(start, start + 1);
384
+ index = start + 1;
385
+ }
386
+ return emitter.tokens;
387
+ }
388
+ function tokenizeLine(source) {
389
+ return [{ kind: "plain", value: source }];
390
+ }
391
+ function createEmitter(source) {
392
+ const tokens = [];
393
+ return {
394
+ tokens,
395
+ push(kind, start, end) {
396
+ pushToken(tokens, source, kind, start, end);
397
+ },
398
+ pushPlain(start, end) {
399
+ pushToken(tokens, source, "plain", start, end);
400
+ }
401
+ };
402
+ }
403
+ function pushToken(tokens, source, kind, start, end) {
404
+ if (end <= start) {
405
+ return;
406
+ }
407
+ const value = source.slice(start, end);
408
+ const previous = tokens[tokens.length - 1];
409
+ if (previous?.kind === kind) {
410
+ previous.value += value;
411
+ return;
412
+ }
413
+ tokens.push({ kind, value });
414
+ }
415
+ function classifyLexicalWord(word, spec) {
416
+ if (word === "true" || word === "false") {
417
+ return "boolean";
418
+ }
419
+ if (word === "null") {
420
+ return "null";
421
+ }
422
+ if (spec.keywords?.has(word) === true) {
423
+ return "keyword";
424
+ }
425
+ if (spec.types?.has(word) === true) {
426
+ return "type";
427
+ }
428
+ if (spec.constants?.has(word) === true) {
429
+ return "number";
430
+ }
431
+ return "plain";
432
+ }
433
+ function classifyDataWord(word) {
434
+ switch (word) {
435
+ case "true":
436
+ case "false":
437
+ return "boolean";
438
+ case "null":
439
+ case "Null":
440
+ case "NULL":
441
+ case "~":
442
+ return "null";
443
+ default:
444
+ return "plain";
445
+ }
446
+ }
447
+ function readWhitespace(source, index) {
448
+ while (index < source.length && isWhitespace(source[index])) {
449
+ index += 1;
450
+ }
451
+ return index;
452
+ }
453
+ function readSpacesAndTabs(source, index) {
454
+ while (index < source.length && (source[index] === " " || source[index] === "\t")) {
455
+ index += 1;
456
+ }
457
+ return index;
458
+ }
459
+ function readIdentifier(source, index) {
460
+ if (!isIdentifierStart(source[index] ?? "")) {
461
+ return index;
462
+ }
463
+ index += 1;
464
+ while (index < source.length && isIdentifierPart(source[index])) {
465
+ index += 1;
466
+ }
467
+ return index;
468
+ }
469
+ function readCssName(source, index) {
470
+ if (!isCssNameStart(source[index] ?? "")) {
471
+ return index;
472
+ }
473
+ index += 1;
474
+ while (index < source.length && isCssNamePart(source[index])) {
475
+ index += 1;
476
+ }
477
+ return index;
478
+ }
479
+ function readNumber(source, index) {
480
+ const start = index;
481
+ if (source[index] === "-") {
482
+ index += 1;
483
+ }
484
+ let hasDigit = false;
485
+ while (index < source.length && isDigit(source[index])) {
486
+ index += 1;
487
+ hasDigit = true;
488
+ }
489
+ if (source[index] === "." && isDigit(source[index + 1] ?? "")) {
490
+ index += 1;
491
+ while (index < source.length && isDigit(source[index])) {
492
+ index += 1;
493
+ hasDigit = true;
494
+ }
495
+ }
496
+ if (!hasDigit) {
497
+ return start;
498
+ }
499
+ if ((source[index] === "e" || source[index] === "E") && isExponentStart(source[index + 1] ?? "")) {
500
+ const exponentStart = index;
501
+ index += 1;
502
+ if (source[index] === "+" || source[index] === "-") {
503
+ index += 1;
504
+ }
505
+ const digitsStart = index;
506
+ while (index < source.length && isDigit(source[index])) {
507
+ index += 1;
508
+ }
509
+ if (index === digitsStart) {
510
+ return exponentStart;
511
+ }
512
+ }
513
+ return index;
514
+ }
515
+ function readQuotedString(source, index, quote) {
516
+ index += 1;
517
+ while (index < source.length) {
518
+ const char = source[index];
519
+ index += 1;
520
+ if (char === "\\") {
521
+ index = Math.min(source.length, index + 1);
522
+ continue;
523
+ }
524
+ if (char === quote) {
525
+ return index;
526
+ }
527
+ }
528
+ return index;
529
+ }
530
+ function readAnyLineComment(source, index, markers) {
531
+ for (const marker of markers) {
532
+ if (source.startsWith(marker, index)) {
533
+ return readUntilLineEnd(source, index);
534
+ }
535
+ }
536
+ return index;
537
+ }
538
+ function readBlockComment(source, index) {
539
+ if (!source.startsWith("/*", index)) {
540
+ return index;
541
+ }
542
+ index += 2;
543
+ while (index < source.length) {
544
+ if (source.startsWith("*/", index)) {
545
+ return index + 2;
546
+ }
547
+ index += 1;
548
+ }
549
+ return source.length;
550
+ }
551
+ function readUntilLineEnd(source, index) {
552
+ while (index < source.length && source[index] !== "\n") {
553
+ index += 1;
554
+ }
555
+ return index;
556
+ }
557
+ function readYamlKey(source, index) {
558
+ const start = index;
559
+ while (index < source.length) {
560
+ const char = source[index];
561
+ if (char === ":") {
562
+ return index > start ? index : start;
563
+ }
564
+ if (char === "\n" || char === "#" || char === "{" || char === "}" || char === "[" || char === "]") {
565
+ return start;
566
+ }
567
+ index += 1;
568
+ }
569
+ return start;
570
+ }
571
+ function readCssColor(source, index) {
572
+ let count = 0;
573
+ while (index < source.length && isHex(source[index]) && count < 8) {
574
+ index += 1;
575
+ count += 1;
576
+ }
577
+ return index;
578
+ }
579
+ function isJsonKey(source, index) {
580
+ index = readWhitespace(source, index);
581
+ return source[index] === ":";
582
+ }
583
+ function isCssProperty(source, index) {
584
+ index = readWhitespace(source, index);
585
+ return source[index] === ":";
586
+ }
587
+ function isWhitespace(char) {
588
+ return char === " " || char === "\n" || char === "\r" || char === "\t";
589
+ }
590
+ function isIdentifierStart(char) {
591
+ return isAlpha(char) || char === "_" || char === "$";
592
+ }
593
+ function isIdentifierPart(char) {
594
+ return isIdentifierStart(char) || isDigit(char);
595
+ }
596
+ function isCssNameStart(char) {
597
+ return isAlpha(char) || char === "_" || char === "-" || char === ".";
598
+ }
599
+ function isCssNamePart(char) {
600
+ return isCssNameStart(char) || isDigit(char);
601
+ }
602
+ function isExponentStart(char) {
603
+ return isDigit(char) || char === "+" || char === "-";
604
+ }
605
+ function isAlpha(char) {
606
+ const code = char.charCodeAt(0);
607
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
608
+ }
609
+ function isDigit(char) {
610
+ const code = char.charCodeAt(0);
611
+ return code >= 48 && code <= 57;
612
+ }
613
+ function isHex(char) {
614
+ const code = char.charCodeAt(0);
615
+ return ((code >= 48 && code <= 57) ||
616
+ (code >= 65 && code <= 70) ||
617
+ (code >= 97 && code <= 102));
618
+ }
@@ -2,5 +2,6 @@ import type { MdNode } from "./ast.js";
2
2
  export interface RenderOptions {
3
3
  width?: number;
4
4
  showFrontmatter?: boolean;
5
+ syntaxHighlight?: boolean;
5
6
  }
6
7
  export declare function render(ast: MdNode, options?: RenderOptions): string;
@@ -5,6 +5,7 @@ import { stripAnsi } from "../internal/strip-ansi.js";
5
5
  import { spacing } from "../tokens/spacing.js";
6
6
  import { typography } from "../tokens/typography.js";
7
7
  import { widths } from "../tokens/widths.js";
8
+ import { highlightCodeBlock } from "./parser/code-highlight.js";
8
9
  const lineChar = "─";
9
10
  export function render(ast, options = {}) {
10
11
  const requestedWidth = options.width ?? process.stdout.columns ?? widths.maxLine;
@@ -15,6 +16,7 @@ export function render(ast, options = {}) {
15
16
  const context = {
16
17
  width,
17
18
  showFrontmatter: options.showFrontmatter ?? false,
19
+ syntaxHighlight: options.syntaxHighlight ?? false,
18
20
  theme: getTheme(),
19
21
  footnotes: ast.type === "root" ? createFootnoteState(ast.children) : undefined
20
22
  };
@@ -113,12 +115,72 @@ function renderAlert(node, context) {
113
115
  function renderCodeBlock(node, context) {
114
116
  const indent = " ".repeat(spacing.sm);
115
117
  const lines = node.value.split("\n").map((line) => stripAnsi(line));
118
+ const strippedSource = lines.join("\n");
116
119
  const longestLine = lines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
117
120
  const borderWidth = Math.max(3, Math.min(context.width - indent.length, longestLine));
118
121
  const border = context.theme.muted(`${indent}${lineChar.repeat(borderWidth)}`);
119
- const content = lines.map((line) => `${indent}${line}`).join("\n");
122
+ const highlightedLines = context.syntaxHighlight === true
123
+ ? renderCodeTokens(highlightCodeBlock({ lang: node.lang, value: strippedSource }), context)?.split("\n")
124
+ : undefined;
125
+ const content = (highlightedLines ?? lines).map((line) => `${indent}${line}`).join("\n");
120
126
  return `${border}\n${content}\n${border}\n\n`;
121
127
  }
128
+ function renderCodeTokens(tokens, context) {
129
+ if (tokens === undefined) {
130
+ return undefined;
131
+ }
132
+ return tokens.map((token) => styleCodeToken(token, context)).join("");
133
+ }
134
+ function styleCodeToken(token, context) {
135
+ if (token.kind === "plain") {
136
+ return token.value;
137
+ }
138
+ return getCodeTokenFormatter(token.kind, context)(token.value);
139
+ }
140
+ function getCodeTokenFormatter(kind, context) {
141
+ switch (kind) {
142
+ case "keyword":
143
+ case "type":
144
+ case "tag":
145
+ case "command":
146
+ case "decorator":
147
+ case "directive":
148
+ case "at-rule":
149
+ return (value) => context.theme.accent(typography.bold(value));
150
+ case "string":
151
+ case "template":
152
+ return context.theme.success;
153
+ case "number":
154
+ case "boolean":
155
+ case "null":
156
+ case "parameter":
157
+ return context.theme.number;
158
+ case "comment":
159
+ return context.theme.muted;
160
+ case "property":
161
+ case "key":
162
+ case "attribute":
163
+ case "variable":
164
+ case "function":
165
+ case "anchor":
166
+ case "label":
167
+ return context.theme.info;
168
+ case "regex":
169
+ case "color":
170
+ case "important":
171
+ case "flag":
172
+ return context.theme.warning;
173
+ case "invalid":
174
+ return context.theme.error;
175
+ case "operator":
176
+ case "punctuation":
177
+ case "selector":
178
+ return context.theme.muted;
179
+ case "identifier":
180
+ case "plain":
181
+ return (value) => value;
182
+ }
183
+ }
122
184
  function renderList(node, context) {
123
185
  const items = node.children
124
186
  .map((child, index) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft",
3
- "version": "0.0.77",
3
+ "version": "0.0.79",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -51,7 +51,7 @@
51
51
  "postpack": "node ../../scripts/manage-bundled-workspace-deps.mjs cleanup . toolcraft-design @poe-code/frontmatter @poe-code/agent-mcp-config @poe-code/agent-human-in-loop @poe-code/task-list @poe-code/agent-defs @poe-code/config-mutations @poe-code/process-runner tiny-mcp-client mcp-oauth auth-store"
52
52
  },
53
53
  "dependencies": {
54
- "toolcraft-schema": "0.0.77",
54
+ "toolcraft-schema": "0.0.79",
55
55
  "commander": "^13.1.0",
56
56
  "fast-string-width": "^3.0.2",
57
57
  "fast-wrap-ansi": "^0.2.0",