chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
212
third_party/pi/packages/coding-agent/src/utils/syntax-highlight.ts
vendored
Normal file
212
third_party/pi/packages/coding-agent/src/utils/syntax-highlight.ts
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
import hljs from "highlight.js/lib/core.js";
|
||||
import bash from "highlight.js/lib/languages/bash.js";
|
||||
import c from "highlight.js/lib/languages/c.js";
|
||||
import cpp from "highlight.js/lib/languages/cpp.js";
|
||||
import csharp from "highlight.js/lib/languages/csharp.js";
|
||||
import dart from "highlight.js/lib/languages/dart.js";
|
||||
import go from "highlight.js/lib/languages/go.js";
|
||||
import groovy from "highlight.js/lib/languages/groovy.js";
|
||||
import java from "highlight.js/lib/languages/java.js";
|
||||
import javascript from "highlight.js/lib/languages/javascript.js";
|
||||
import kotlin from "highlight.js/lib/languages/kotlin.js";
|
||||
import lua from "highlight.js/lib/languages/lua.js";
|
||||
import nix from "highlight.js/lib/languages/nix.js";
|
||||
import perl from "highlight.js/lib/languages/perl.js";
|
||||
import php from "highlight.js/lib/languages/php.js";
|
||||
import python from "highlight.js/lib/languages/python.js";
|
||||
import ruby from "highlight.js/lib/languages/ruby.js";
|
||||
import rust from "highlight.js/lib/languages/rust.js";
|
||||
import scala from "highlight.js/lib/languages/scala.js";
|
||||
import swift from "highlight.js/lib/languages/swift.js";
|
||||
import typescript from "highlight.js/lib/languages/typescript.js";
|
||||
import { decodeHtmlEntityAt } from "./html.ts";
|
||||
|
||||
const eagerLanguages = {
|
||||
python,
|
||||
java,
|
||||
go,
|
||||
javascript,
|
||||
cpp,
|
||||
typescript,
|
||||
php,
|
||||
ruby,
|
||||
c,
|
||||
csharp,
|
||||
nix,
|
||||
bash,
|
||||
rust,
|
||||
scala,
|
||||
kotlin,
|
||||
swift,
|
||||
dart,
|
||||
groovy,
|
||||
perl,
|
||||
lua,
|
||||
};
|
||||
|
||||
for (const [name, language] of Object.entries(eagerLanguages)) {
|
||||
hljs.registerLanguage(name, language);
|
||||
}
|
||||
|
||||
let allLanguagesPromise: Promise<void> | undefined;
|
||||
|
||||
export function loadAllHighlightLanguages(): Promise<void> {
|
||||
if (!allLanguagesPromise) {
|
||||
allLanguagesPromise = new Promise((resolve) => {
|
||||
setImmediate(() => {
|
||||
void import("highlight.js/lib/index.js").then(
|
||||
() => resolve(),
|
||||
() => {
|
||||
// Eager languages and plaintext fallback remain available.
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
return allLanguagesPromise;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user