toolcraft 0.0.78 → 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.
- package/node_modules/toolcraft-design/dist/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/terminal-markdown/ast.d.ts +6 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/html-renderer.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/html-renderer.js +13 -3
- package/node_modules/toolcraft-design/dist/terminal-markdown/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/code-highlight.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/parser/code-highlight.js +618 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/renderer.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/terminal-markdown/renderer.js +63 -1
- package/package.json +2 -2
|
@@ -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 {
|
|
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;
|
|
@@ -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
|
-
|
|
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,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
|
+
}
|
|
@@ -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
|
|
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.
|
|
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.
|
|
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",
|