chore(deps): replace cli-highlight (#4468)
This commit is contained in:
158
package-lock.json
generated
158
package-lock.json
generated
@@ -3967,10 +3967,6 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/any-promise": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -4159,75 +4155,6 @@
|
|||||||
"url": "https://polar.sh/cva"
|
"url": "https://polar.sh/cva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cli-highlight": {
|
|
||||||
"version": "2.1.11",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^4.0.0",
|
|
||||||
"highlight.js": "^10.7.1",
|
|
||||||
"mz": "^2.4.0",
|
|
||||||
"parse5": "^5.1.1",
|
|
||||||
"parse5-htmlparser2-tree-adapter": "^6.0.0",
|
|
||||||
"yargs": "^16.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"highlight": "bin/highlight"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0.0",
|
|
||||||
"npm": ">=5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cli-highlight/node_modules/chalk": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cli-highlight/node_modules/supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cliui": {
|
|
||||||
"version": "7.0.4",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"string-width": "^4.2.0",
|
|
||||||
"strip-ansi": "^6.0.0",
|
|
||||||
"wrap-ansi": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cliui/node_modules/ansi-regex": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cliui/node_modules/strip-ansi": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -4485,6 +4412,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
@@ -4562,6 +4490,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -4818,6 +4747,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/get-caller-file": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
@@ -5073,6 +5003,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -5640,15 +5571,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mz": {
|
|
||||||
"version": "2.7.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"any-promise": "^1.0.0",
|
|
||||||
"object-assign": "^4.0.1",
|
|
||||||
"thenify-all": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.12",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5743,13 +5665,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ollama": {
|
"node_modules/ollama": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -5839,21 +5754,6 @@
|
|||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"license": "(MIT AND Zlib)"
|
"license": "(MIT AND Zlib)"
|
||||||
},
|
},
|
||||||
"node_modules/parse5": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"parse5": "^6.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/partial-json": {
|
"node_modules/partial-json": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -6146,6 +6046,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -6488,6 +6389,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
@@ -6500,6 +6402,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/string-width/node_modules/ansi-regex": {
|
"node_modules/string-width/node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -6507,6 +6410,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/string-width/node_modules/strip-ansi": {
|
"node_modules/string-width/node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
@@ -6644,23 +6548,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/thenify": {
|
|
||||||
"version": "3.3.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"any-promise": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/thenify-all": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"thenify": ">= 3.1.0 < 4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7073,6 +6960,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
@@ -7088,6 +6976,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/wrap-ansi/node_modules/ansi-regex": {
|
"node_modules/wrap-ansi/node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -7095,6 +6984,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/wrap-ansi/node_modules/strip-ansi": {
|
"node_modules/wrap-ansi/node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
@@ -7141,6 +7031,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -7159,29 +7050,6 @@
|
|||||||
"url": "https://github.com/sponsors/eemeli"
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yargs": {
|
|
||||||
"version": "16.2.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cliui": "^7.0.2",
|
|
||||||
"escalade": "^3.1.1",
|
|
||||||
"get-caller-file": "^2.0.5",
|
|
||||||
"require-directory": "^2.1.1",
|
|
||||||
"string-width": "^4.2.0",
|
|
||||||
"y18n": "^5.0.5",
|
|
||||||
"yargs-parser": "^20.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs-parser": {
|
|
||||||
"version": "20.2.9",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -7278,9 +7146,9 @@
|
|||||||
"@earendil-works/pi-tui": "^0.74.0",
|
"@earendil-works/pi-tui": "^0.74.0",
|
||||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"cli-highlight": "^2.1.11",
|
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"glob": "^13.0.1",
|
"glob": "^13.0.1",
|
||||||
|
"highlight.js": "^10.7.3",
|
||||||
"hosted-git-info": "^9.0.2",
|
"hosted-git-info": "^9.0.2",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"jiti": "^2.7.0",
|
"jiti": "^2.7.0",
|
||||||
|
|||||||
@@ -43,9 +43,9 @@
|
|||||||
"@earendil-works/pi-tui": "^0.74.0",
|
"@earendil-works/pi-tui": "^0.74.0",
|
||||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||||
"chalk": "^5.5.0",
|
"chalk": "^5.5.0",
|
||||||
"cli-highlight": "^2.1.11",
|
|
||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"glob": "^13.0.1",
|
"glob": "^13.0.1",
|
||||||
|
"highlight.js": "^10.7.3",
|
||||||
"hosted-git-info": "^9.0.2",
|
"hosted-git-info": "^9.0.2",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"jiti": "^2.7.0",
|
"jiti": "^2.7.0",
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import * as fs from "node:fs";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
|
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { highlight, supportsLanguage } from "cli-highlight";
|
|
||||||
import { type Static, Type } from "typebox";
|
import { type Static, Type } from "typebox";
|
||||||
import { Compile } from "typebox/compile";
|
import { Compile } from "typebox/compile";
|
||||||
import { getCustomThemesDir, getThemesDir } from "../../../config.js";
|
import { getCustomThemesDir, getThemesDir } from "../../../config.js";
|
||||||
import type { SourceInfo } from "../../../core/source-info.js";
|
import type { SourceInfo } from "../../../core/source-info.js";
|
||||||
import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.js";
|
import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.js";
|
||||||
|
import { highlight, supportsLanguage } from "../../../utils/syntax-highlight.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types & Schema
|
// Types & Schema
|
||||||
|
|||||||
19
packages/coding-agent/src/utils/highlight-js-lib-index.d.ts
vendored
Normal file
19
packages/coding-agent/src/utils/highlight-js-lib-index.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
declare module "highlight.js/lib/index.js" {
|
||||||
|
interface HighlightResult {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HighlightOptions {
|
||||||
|
language: string;
|
||||||
|
ignoreIllegals?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HighlightJs {
|
||||||
|
highlight(code: string, options: HighlightOptions): HighlightResult;
|
||||||
|
highlightAuto(code: string, languageSubset?: string[]): HighlightResult;
|
||||||
|
getLanguage(name: string): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hljs: HighlightJs;
|
||||||
|
export default hljs;
|
||||||
|
}
|
||||||
51
packages/coding-agent/src/utils/html.ts
Normal file
51
packages/coding-agent/src/utils/html.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export interface DecodedHtmlEntity {
|
||||||
|
text: string;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeCodePoint(codePoint: number): string | undefined {
|
||||||
|
if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return String.fromCodePoint(codePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeHtmlEntity(entity: string): string | undefined {
|
||||||
|
switch (entity) {
|
||||||
|
case "amp":
|
||||||
|
return "&";
|
||||||
|
case "lt":
|
||||||
|
return "<";
|
||||||
|
case "gt":
|
||||||
|
return ">";
|
||||||
|
case "quot":
|
||||||
|
return '"';
|
||||||
|
case "apos":
|
||||||
|
return "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.startsWith("#x") || entity.startsWith("#X")) {
|
||||||
|
return decodeCodePoint(Number.parseInt(entity.slice(2), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.startsWith("#")) {
|
||||||
|
return decodeCodePoint(Number.parseInt(entity.slice(1), 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeHtmlEntityAt(html: string, index: number): DecodedHtmlEntity | undefined {
|
||||||
|
const semicolonIndex = html.indexOf(";", index + 1);
|
||||||
|
if (semicolonIndex === -1 || semicolonIndex - index > 16) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = html.slice(index + 1, semicolonIndex);
|
||||||
|
const decoded = decodeHtmlEntity(entity);
|
||||||
|
if (decoded === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: decoded, length: semicolonIndex - index + 1 };
|
||||||
|
}
|
||||||
146
packages/coding-agent/src/utils/syntax-highlight.ts
Normal file
146
packages/coding-agent/src/utils/syntax-highlight.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import hljs from "highlight.js/lib/index.js";
|
||||||
|
import { decodeHtmlEntityAt } from "./html.js";
|
||||||
|
|
||||||
|
export type HighlightFormatter = (text: string) => string;
|
||||||
|
export type HighlightTheme = Partial<Record<string, HighlightFormatter>>;
|
||||||
|
|
||||||
|
export interface HighlightOptions {
|
||||||
|
language?: string;
|
||||||
|
ignoreIllegals?: boolean;
|
||||||
|
languageSubset?: string[];
|
||||||
|
theme?: HighlightTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPAN_CLOSE = "</span>";
|
||||||
|
const HIGHLIGHT_CLASS_PREFIX = "hljs-";
|
||||||
|
|
||||||
|
function getScopeFromSpanTag(tag: string): string | undefined {
|
||||||
|
const match = /\sclass\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(tag);
|
||||||
|
const classValue = match?.[1] ?? match?.[2];
|
||||||
|
if (!classValue) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const className of classValue.split(/\s+/)) {
|
||||||
|
if (className.startsWith(HIGHLIGHT_CLASS_PREFIX)) {
|
||||||
|
return className.slice(HIGHLIGHT_CLASS_PREFIX.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScopeFormatter(scope: string, theme: HighlightTheme): HighlightFormatter | undefined {
|
||||||
|
const exact = theme[scope];
|
||||||
|
if (exact) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotIndex = scope.indexOf(".");
|
||||||
|
if (dotIndex !== -1) {
|
||||||
|
const prefixFormatter = theme[scope.slice(0, dotIndex)];
|
||||||
|
if (prefixFormatter) {
|
||||||
|
return prefixFormatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashIndex = scope.indexOf("-");
|
||||||
|
if (dashIndex !== -1) {
|
||||||
|
const prefixFormatter = theme[scope.slice(0, dashIndex)];
|
||||||
|
if (prefixFormatter) {
|
||||||
|
return prefixFormatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveFormatter(scopes: Array<string | undefined>, theme: HighlightTheme): HighlightFormatter | undefined {
|
||||||
|
for (let i = scopes.length - 1; i >= 0; i--) {
|
||||||
|
const scope = scopes[i];
|
||||||
|
if (!scope) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const formatter = getScopeFormatter(scope, theme);
|
||||||
|
if (formatter) {
|
||||||
|
return formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return theme.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpanOpenTagStart(html: string, index: number): boolean {
|
||||||
|
if (!html.startsWith("<span", index)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nextChar = html[index + "<span".length];
|
||||||
|
return nextChar === ">" || nextChar === " " || nextChar === "\t" || nextChar === "\n" || nextChar === "\r";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderHighlightedHtml(html: string, theme: HighlightTheme = {}): string {
|
||||||
|
let output = "";
|
||||||
|
let textBuffer = "";
|
||||||
|
const scopes: Array<string | undefined> = [];
|
||||||
|
|
||||||
|
const flushText = () => {
|
||||||
|
if (!textBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formatter = getActiveFormatter(scopes, theme);
|
||||||
|
output += formatter ? formatter(textBuffer) : textBuffer;
|
||||||
|
textBuffer = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
while (index < html.length) {
|
||||||
|
if (isSpanOpenTagStart(html, index)) {
|
||||||
|
const tagEndIndex = html.indexOf(">", index + 5);
|
||||||
|
if (tagEndIndex !== -1) {
|
||||||
|
flushText();
|
||||||
|
const tag = html.slice(index, tagEndIndex + 1);
|
||||||
|
const scope = getScopeFromSpanTag(tag);
|
||||||
|
scopes.push(scope);
|
||||||
|
index = tagEndIndex + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html.startsWith(SPAN_CLOSE, index)) {
|
||||||
|
flushText();
|
||||||
|
if (scopes.length > 0) {
|
||||||
|
scopes.pop();
|
||||||
|
}
|
||||||
|
index += SPAN_CLOSE.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html[index] === "&") {
|
||||||
|
const decoded = decodeHtmlEntityAt(html, index);
|
||||||
|
if (decoded) {
|
||||||
|
textBuffer += decoded.text;
|
||||||
|
index += decoded.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textBuffer += html[index];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushText();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlight(code: string, options: HighlightOptions = {}): string {
|
||||||
|
const html = options.language
|
||||||
|
? hljs.highlight(code, {
|
||||||
|
language: options.language,
|
||||||
|
ignoreIllegals: options.ignoreIllegals,
|
||||||
|
}).value
|
||||||
|
: hljs.highlightAuto(code, options.languageSubset).value;
|
||||||
|
return renderHighlightedHtml(html, options.theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsLanguage(name: string): boolean {
|
||||||
|
return hljs.getLanguage(name) !== undefined;
|
||||||
|
}
|
||||||
48
packages/coding-agent/test/syntax-highlight.test.ts
Normal file
48
packages/coding-agent/test/syntax-highlight.test.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { highlight, renderHighlightedHtml, supportsLanguage } from "../src/utils/syntax-highlight.js";
|
||||||
|
|
||||||
|
describe("syntax highlight renderer", () => {
|
||||||
|
it("renders highlighted spans with the provided theme", () => {
|
||||||
|
const rendered = renderHighlightedHtml('<span class="hljs-keyword">const</span> value', {
|
||||||
|
keyword: (text) => `[keyword:${text}]`,
|
||||||
|
});
|
||||||
|
expect(rendered).toBe("[keyword:const] value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes HTML entities emitted by highlight.js", () => {
|
||||||
|
const rendered = renderHighlightedHtml("<tag attr="value">&#x41;A</tag>");
|
||||||
|
expect(rendered).toBe('<tag attr="value">AA</tag>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inherits parent formatting for unmapped nested scopes", () => {
|
||||||
|
const interpolation = "$" + "{x}";
|
||||||
|
const rendered = renderHighlightedHtml(
|
||||||
|
`<span class="hljs-string">a<span class="hljs-subst">${interpolation}</span>b</span>`,
|
||||||
|
{
|
||||||
|
string: (text) => `[string:${text}]`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(rendered).toBe(`[string:a][string:${interpolation}][string:b]`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps parent formatting across unscoped nested spans", () => {
|
||||||
|
const rendered = renderHighlightedHtml('<span class="hljs-string">a<span class="language-xml">b</span>c</span>', {
|
||||||
|
string: (text) => `[string:${text}]`,
|
||||||
|
});
|
||||||
|
expect(rendered).toBe("[string:a][string:b][string:c]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("highlights code through highlight.js", () => {
|
||||||
|
expect(supportsLanguage("typescript")).toBe(true);
|
||||||
|
const rendered = highlight("const value = 1", {
|
||||||
|
language: "typescript",
|
||||||
|
ignoreIllegals: true,
|
||||||
|
theme: {
|
||||||
|
keyword: (text) => `[keyword:${text}]`,
|
||||||
|
number: (text) => `[number:${text}]`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(rendered).toContain("[keyword:const]");
|
||||||
|
expect(rendered).toContain("[number:1]");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src"
|
"rootDir": "./src"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts", "src/**/*.d.ts"],
|
||||||
"exclude": ["node_modules", "dist", "**/*.d.ts", "src/**/*.d.ts"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user