mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 11:29:02 +00:00
Add shadcn, tailwind and react detection & templates to bun create. Also: bun install --analyze <files...> (#17035)
This commit is contained in:
796
src/bake/client/JavaScriptSyntaxHighlighter.ts
Normal file
796
src/bake/client/JavaScriptSyntaxHighlighter.ts
Normal file
@@ -0,0 +1,796 @@
|
||||
interface HighlighterOptions {
|
||||
enableColors: boolean;
|
||||
redactSensitiveInformation: boolean;
|
||||
languageName?: string;
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
const enum TokenClass {
|
||||
Keyword = "syntax-pink",
|
||||
Type = "syntax-cyan italic",
|
||||
Parameter = "syntax-orange italic",
|
||||
Error = "syntax-red",
|
||||
Operator = "syntax-pink",
|
||||
Function = "syntax-green",
|
||||
String = "syntax-yellow",
|
||||
Comment = "syntax-gray",
|
||||
Constant = "syntax-purple",
|
||||
Variable = "syntax-fg",
|
||||
Generic = "syntax-orange italic",
|
||||
KeywordNew = "syntax-pink bold",
|
||||
Decorator = "syntax-green italic",
|
||||
JSXTag = "syntax-cyan",
|
||||
JSXComponent = "syntax-green",
|
||||
JSXAttribute = "syntax-orange",
|
||||
JSXString = "syntax-yellow",
|
||||
JSXText = "syntax-fg",
|
||||
JSXPunctuation = "syntax-pink",
|
||||
JSXExpression = "syntax-pink",
|
||||
}
|
||||
|
||||
// Pre-compile keyword maps for Dracula-compliant highlighting
|
||||
const keywordColorMap = new Map<string, TokenClass>([
|
||||
// Flow control keywords
|
||||
["if", TokenClass.Keyword],
|
||||
["else", TokenClass.Keyword],
|
||||
["for", TokenClass.Keyword],
|
||||
["while", TokenClass.Keyword],
|
||||
["do", TokenClass.Keyword],
|
||||
["switch", TokenClass.Keyword],
|
||||
["case", TokenClass.Keyword],
|
||||
["break", TokenClass.Keyword],
|
||||
["continue", TokenClass.Keyword],
|
||||
["return", TokenClass.Keyword],
|
||||
["try", TokenClass.Keyword],
|
||||
["catch", TokenClass.Keyword],
|
||||
["finally", TokenClass.Keyword],
|
||||
["throw", TokenClass.Keyword],
|
||||
|
||||
// Declaration keywords
|
||||
["const", TokenClass.Keyword],
|
||||
["let", TokenClass.Keyword],
|
||||
["var", TokenClass.Keyword],
|
||||
["function", TokenClass.Keyword],
|
||||
["class", TokenClass.Keyword],
|
||||
|
||||
// TypeScript specific
|
||||
["interface", TokenClass.Type],
|
||||
["type", TokenClass.Type],
|
||||
["enum", TokenClass.Type],
|
||||
["namespace", TokenClass.Type],
|
||||
["abstract", TokenClass.Type],
|
||||
["implements", TokenClass.Type],
|
||||
["readonly", TokenClass.Type],
|
||||
["private", TokenClass.Type],
|
||||
["protected", TokenClass.Type],
|
||||
["public", TokenClass.Type],
|
||||
["static", TokenClass.Type],
|
||||
["declare", TokenClass.Type],
|
||||
["extends", TokenClass.Type],
|
||||
|
||||
// Values
|
||||
["true", TokenClass.Constant],
|
||||
["false", TokenClass.Constant],
|
||||
["null", TokenClass.Constant],
|
||||
["undefined", TokenClass.Constant],
|
||||
["this", TokenClass.Parameter],
|
||||
|
||||
// Modules
|
||||
["import", TokenClass.Keyword],
|
||||
["export", TokenClass.Keyword],
|
||||
["from", TokenClass.Keyword],
|
||||
["as", TokenClass.Type],
|
||||
["default", TokenClass.Keyword],
|
||||
|
||||
// Async
|
||||
["async", TokenClass.Keyword],
|
||||
["await", TokenClass.Keyword],
|
||||
|
||||
// Special keywords
|
||||
["new", TokenClass.KeywordNew],
|
||||
]);
|
||||
|
||||
// Add TypeScript modifiers
|
||||
const typeModifiers = new Set(["private", "protected", "public", "readonly", "abstract", "static", "declare"]);
|
||||
|
||||
// Add JSX-specific tokens
|
||||
const htmlTags = new Set([
|
||||
"a",
|
||||
"abbr",
|
||||
"address",
|
||||
"area",
|
||||
"article",
|
||||
"aside",
|
||||
"audio",
|
||||
"b",
|
||||
"base",
|
||||
"bdi",
|
||||
"bdo",
|
||||
"blockquote",
|
||||
"body",
|
||||
"br",
|
||||
"button",
|
||||
"canvas",
|
||||
"caption",
|
||||
"cite",
|
||||
"code",
|
||||
"col",
|
||||
"colgroup",
|
||||
"data",
|
||||
"datalist",
|
||||
"dd",
|
||||
"del",
|
||||
"details",
|
||||
"dfn",
|
||||
"dialog",
|
||||
"div",
|
||||
"dl",
|
||||
"dt",
|
||||
"em",
|
||||
"embed",
|
||||
"fieldset",
|
||||
"figcaption",
|
||||
"figure",
|
||||
"footer",
|
||||
"form",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"head",
|
||||
"header",
|
||||
"hr",
|
||||
"html",
|
||||
"i",
|
||||
"iframe",
|
||||
"img",
|
||||
"input",
|
||||
"ins",
|
||||
"kbd",
|
||||
"label",
|
||||
"legend",
|
||||
"li",
|
||||
"link",
|
||||
"main",
|
||||
"map",
|
||||
"mark",
|
||||
"meta",
|
||||
"meter",
|
||||
"nav",
|
||||
"noscript",
|
||||
"object",
|
||||
"ol",
|
||||
"optgroup",
|
||||
"option",
|
||||
"output",
|
||||
"p",
|
||||
"param",
|
||||
"picture",
|
||||
"pre",
|
||||
"progress",
|
||||
"q",
|
||||
"rp",
|
||||
"rt",
|
||||
"ruby",
|
||||
"s",
|
||||
"samp",
|
||||
"script",
|
||||
"section",
|
||||
"select",
|
||||
"small",
|
||||
"source",
|
||||
"span",
|
||||
"strong",
|
||||
"style",
|
||||
"sub",
|
||||
"summary",
|
||||
"sup",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"template",
|
||||
"textarea",
|
||||
"tfoot",
|
||||
"th",
|
||||
"thead",
|
||||
"time",
|
||||
"title",
|
||||
"tr",
|
||||
"track",
|
||||
"u",
|
||||
"ul",
|
||||
"var",
|
||||
"video",
|
||||
"wbr",
|
||||
]);
|
||||
|
||||
const sensitivePatterns = new Set(["_auth", "_authToken", "token", "_password", "email"]);
|
||||
|
||||
// Character sets for lexing
|
||||
const IDENTIFIER_START = new Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_");
|
||||
const IDENTIFIER_PART = new Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$_");
|
||||
const WHITESPACE = new Set(" \t\n\r");
|
||||
const DIGITS = new Set("0123456789");
|
||||
const HEX_DIGITS = new Set("0123456789abcdefABCDEF");
|
||||
const OPERATORS = new Set("+-*/%=<>!&|^~?:");
|
||||
|
||||
enum TokenType {
|
||||
Whitespace = "Whitespace",
|
||||
Newline = "Newline",
|
||||
Identifier = "Identifier",
|
||||
Keyword = "Keyword",
|
||||
String = "String",
|
||||
Number = "Number",
|
||||
Operator = "Operator",
|
||||
Comment = "Comment",
|
||||
TemplateString = "TemplateString",
|
||||
TemplateInterpolation = "TemplateInterpolation",
|
||||
Punctuator = "Punctuator",
|
||||
JSXTag = "JSXTag",
|
||||
JSXComponent = "JSXComponent",
|
||||
JSXAttribute = "JSXAttribute",
|
||||
JSXString = "JSXString",
|
||||
JSXText = "JSXText",
|
||||
JSXPunctuation = "JSXPunctuation",
|
||||
JSXExpression = "JSXExpression",
|
||||
Type = "Type",
|
||||
TypeParameter = "TypeParameter",
|
||||
Interface = "Interface",
|
||||
Enum = "Enum",
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
line: number;
|
||||
column: number;
|
||||
tokenClass?: TokenClass;
|
||||
}
|
||||
|
||||
export class DraculaSyntaxHighlighter {
|
||||
private text: string;
|
||||
private pos: number = 0;
|
||||
private line: number = 1;
|
||||
private column: number = 0;
|
||||
private readonly options: HighlighterOptions;
|
||||
|
||||
// Add JSX state tracking
|
||||
private isInJSXTag: boolean = false;
|
||||
private isJSXTagStart: boolean = false;
|
||||
|
||||
// Add TypeScript state tracking
|
||||
private isExpectingTypeName: boolean = false;
|
||||
private isInGenericType: boolean = false;
|
||||
private isInDestructuring: boolean = false;
|
||||
private isAfterExtendsOrImplements: boolean = false;
|
||||
|
||||
constructor(text: string, options: Partial<HighlighterOptions> = {}) {
|
||||
this.text = text;
|
||||
this.options = {
|
||||
enableColors: true,
|
||||
redactSensitiveInformation: false,
|
||||
languageName: "javascript",
|
||||
showLineNumbers: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
// Initialize state
|
||||
this.pos = 0;
|
||||
this.line = 1;
|
||||
this.column = 0;
|
||||
this.isInJSXTag = false;
|
||||
this.isJSXTagStart = false;
|
||||
this.isExpectingTypeName = false;
|
||||
this.isInGenericType = false;
|
||||
this.isInDestructuring = false;
|
||||
this.isAfterExtendsOrImplements = false;
|
||||
}
|
||||
|
||||
private peek(offset: number = 0): string {
|
||||
return this.text[this.pos + offset] || "";
|
||||
}
|
||||
|
||||
private consume(length: number = 1): string {
|
||||
const value = this.text.slice(this.pos, this.pos + length);
|
||||
for (const char of value) {
|
||||
if (char === "\n") {
|
||||
this.line++;
|
||||
this.column = 0;
|
||||
} else {
|
||||
this.column++;
|
||||
}
|
||||
}
|
||||
this.pos += length;
|
||||
return value;
|
||||
}
|
||||
|
||||
private createToken(type: TokenType, value: string, tokenClass?: TokenClass): Token {
|
||||
return {
|
||||
type,
|
||||
value,
|
||||
line: this.line,
|
||||
column: this.column - value.length,
|
||||
tokenClass,
|
||||
};
|
||||
}
|
||||
|
||||
private lexWhitespace(): Token | null {
|
||||
let value = "";
|
||||
while (this.pos < this.text.length && this.isWhitespace(this.peek()) && this.peek() !== "\n") {
|
||||
value += this.consume();
|
||||
}
|
||||
return value ? this.createToken(TokenType.Whitespace, value) : null;
|
||||
}
|
||||
|
||||
private lexNewline(): Token | null {
|
||||
return this.peek() === "\n" ? this.createToken(TokenType.Newline, this.consume()) : null;
|
||||
}
|
||||
|
||||
private lexIdentifierOrKeyword(): Token | null {
|
||||
if (!this.isIdentifierStart(this.peek())) return null;
|
||||
|
||||
const value = this.consumeIdentifier();
|
||||
const tokenClass = keywordColorMap.get(value);
|
||||
|
||||
// Handle JSX tags and components
|
||||
if (this.isInJSXTag) {
|
||||
if (this.isJSXTagStart) {
|
||||
this.isJSXTagStart = false;
|
||||
return this.createToken(
|
||||
TokenType.JSXTag,
|
||||
value,
|
||||
htmlTags.has(value.toLowerCase()) ? TokenClass.JSXTag : TokenClass.JSXComponent,
|
||||
);
|
||||
}
|
||||
return this.createToken(TokenType.JSXAttribute, value, TokenClass.JSXAttribute);
|
||||
}
|
||||
|
||||
// Handle TypeScript keywords and types
|
||||
if (tokenClass) {
|
||||
// Special handling for TypeScript modifiers and type keywords
|
||||
if (typeModifiers.has(value)) {
|
||||
return this.createToken(TokenType.Keyword, value, TokenClass.Type);
|
||||
}
|
||||
if (value === "interface" || value === "type" || value === "enum") {
|
||||
this.isExpectingTypeName = true;
|
||||
return this.createToken(TokenType.Keyword, value, TokenClass.Type);
|
||||
}
|
||||
if (value === "extends" || value === "implements") {
|
||||
this.isAfterExtendsOrImplements = true;
|
||||
}
|
||||
return this.createToken(TokenType.Keyword, value, tokenClass);
|
||||
}
|
||||
|
||||
// Handle type names and references
|
||||
if (this.isExpectingTypeName) {
|
||||
this.isExpectingTypeName = false;
|
||||
return this.createToken(TokenType.Identifier, value, TokenClass.Type);
|
||||
}
|
||||
|
||||
// Check if this is a type reference
|
||||
const nextChar = this.peek();
|
||||
const prevChar = this.pos > 0 ? this.text[this.pos - 1] : "";
|
||||
if (
|
||||
(prevChar === ":" && !this.isInDestructuring) ||
|
||||
this.isAfterExtendsOrImplements ||
|
||||
(prevChar === "<" && this.isInGenericType) ||
|
||||
(prevChar === "<" && nextChar !== "=" && !this.isInJSXTag)
|
||||
) {
|
||||
return this.createToken(TokenType.Identifier, value, TokenClass.Type);
|
||||
}
|
||||
|
||||
if (this.peek() === "(") {
|
||||
return this.createToken(TokenType.Identifier, value, TokenClass.Function);
|
||||
}
|
||||
|
||||
return this.createToken(TokenType.Identifier, value, TokenClass.Variable);
|
||||
}
|
||||
|
||||
private lexNumber(): Token | null {
|
||||
if (!this.isDigit(this.peek())) return null;
|
||||
|
||||
const value = this.consumeNumber();
|
||||
return this.createToken(TokenType.Number, value, TokenClass.Constant);
|
||||
}
|
||||
|
||||
private lexString(): Token | null {
|
||||
const quote = this.peek();
|
||||
if (quote !== '"' && quote !== "'" && quote !== "`") return null;
|
||||
|
||||
if (quote === "`") {
|
||||
return this.lexTemplateString();
|
||||
}
|
||||
|
||||
const value = this.consumeString(quote);
|
||||
return this.createToken(TokenType.String, value, TokenClass.String);
|
||||
}
|
||||
|
||||
private lexTemplateString(): Token | null {
|
||||
const tokens: Token[] = [];
|
||||
let str = this.consume(); // Initial backtick
|
||||
|
||||
while (this.pos < this.text.length) {
|
||||
const char = this.peek();
|
||||
const prevChar = this.peek(-1);
|
||||
|
||||
if (char === "`" && prevChar !== "\\") {
|
||||
str += this.consume();
|
||||
tokens.push(this.createToken(TokenType.TemplateString, str, TokenClass.String));
|
||||
break;
|
||||
}
|
||||
|
||||
if (char === "$" && this.peek(1) === "{" && prevChar !== "\\") {
|
||||
if (str) {
|
||||
tokens.push(this.createToken(TokenType.TemplateString, str, TokenClass.String));
|
||||
str = "";
|
||||
}
|
||||
|
||||
const interpStart = this.consume(2);
|
||||
tokens.push(this.createToken(TokenType.TemplateInterpolation, interpStart, TokenClass.Operator));
|
||||
|
||||
let braceCount = 1;
|
||||
while (this.pos < this.text.length && braceCount > 0) {
|
||||
const c = this.peek();
|
||||
if (c === "{") braceCount++;
|
||||
if (c === "}") braceCount--;
|
||||
|
||||
if (braceCount === 0) {
|
||||
tokens.push(this.createToken(TokenType.TemplateInterpolation, this.consume(), TokenClass.Operator));
|
||||
} else {
|
||||
const token = this.nextToken();
|
||||
if (token) tokens.push(token);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "\\") {
|
||||
str += this.consume(2);
|
||||
} else {
|
||||
str += this.consume();
|
||||
}
|
||||
}
|
||||
|
||||
return tokens[0]; // Return first token, others will be picked up in next iterations
|
||||
}
|
||||
|
||||
private lexComment(): Token | null {
|
||||
if (this.peek() !== "/" || (this.peek(1) !== "/" && this.peek(1) !== "*")) return null;
|
||||
|
||||
const value = this.consumeComment();
|
||||
return this.createToken(TokenType.Comment, value, TokenClass.Comment);
|
||||
}
|
||||
|
||||
private lexOperator(): Token | null {
|
||||
if (!this.isOperator(this.peek())) return null;
|
||||
|
||||
const value = this.consumeOperator();
|
||||
return this.createToken(TokenType.Operator, value, TokenClass.Operator);
|
||||
}
|
||||
|
||||
private lexPunctuator(): Token | null {
|
||||
const char = this.peek();
|
||||
if ("[](){}.,;".includes(char)) {
|
||||
if (char === "<") {
|
||||
const next = this.peek(1);
|
||||
if (this.isIdentifierStart(next) || next === "/") {
|
||||
this.isInJSXTag = true;
|
||||
this.isJSXTagStart = true;
|
||||
} else if (this.isIdentifierStart(this.peek(2))) {
|
||||
this.isInGenericType = true;
|
||||
}
|
||||
} else if (char === ">") {
|
||||
this.isInJSXTag = false;
|
||||
this.isInGenericType = false;
|
||||
} else if (char === "{") {
|
||||
this.isInDestructuring = true;
|
||||
} else if (char === "}") {
|
||||
this.isInDestructuring = false;
|
||||
}
|
||||
return this.createToken(TokenType.Punctuator, this.consume(), TokenClass.Operator);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private nextToken(): Token | null {
|
||||
if (this.pos >= this.text.length) return null;
|
||||
|
||||
const token =
|
||||
this.lexWhitespace() ||
|
||||
this.lexNewline() ||
|
||||
this.lexComment() ||
|
||||
this.lexString() ||
|
||||
this.lexIdentifierOrKeyword() ||
|
||||
this.lexNumber() ||
|
||||
this.lexOperator() ||
|
||||
this.lexPunctuator() ||
|
||||
this.createToken(TokenType.Operator, this.consume(), TokenClass.Operator);
|
||||
|
||||
// Reset extends/implements state after non-whitespace tokens
|
||||
if (token?.type !== TokenType.Whitespace && token?.type !== TokenType.Newline) {
|
||||
this.isAfterExtendsOrImplements = false;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private *tokenize(): Generator<Token> {
|
||||
while (this.pos < this.text.length) {
|
||||
const token = this.nextToken();
|
||||
if (token) yield token;
|
||||
}
|
||||
}
|
||||
|
||||
public highlight(): string {
|
||||
const containerClass = this.options.languageName
|
||||
? `dracula-theme language-${this.options.languageName}`
|
||||
: "dracula-theme";
|
||||
|
||||
const classAttr = this.options.showLineNumbers ? `${containerClass} with-line-numbers` : containerClass;
|
||||
|
||||
let result = "";
|
||||
let lineContent = "";
|
||||
let currentLine = 1;
|
||||
|
||||
const startNewLine = () => {
|
||||
if (lineContent) {
|
||||
result += this.buildHtmlElement("div", { "class": "line" }, lineContent);
|
||||
lineContent = "";
|
||||
}
|
||||
};
|
||||
|
||||
for (const token of this.tokenize()) {
|
||||
if (token.type === TokenType.Newline) {
|
||||
startNewLine();
|
||||
currentLine++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.tokenClass) {
|
||||
lineContent += this.wrap(token.value, token.tokenClass);
|
||||
} else {
|
||||
lineContent += this.escapeHtml(token.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any remaining content
|
||||
startNewLine();
|
||||
|
||||
// Wrap everything in pre tag
|
||||
return this.buildHtmlElement("pre", { "class": classAttr }, result);
|
||||
}
|
||||
|
||||
private escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
private buildHtmlElement(tag: string, attributes: Record<string, string>, content: string): string {
|
||||
const attrs = Object.entries(attributes)
|
||||
.map(([key, value]) => `${key}="${this.escapeHtml(value)}"`)
|
||||
.join(" ");
|
||||
|
||||
return `<${tag}${attrs ? " " + attrs : ""}>${content}</${tag}>`;
|
||||
}
|
||||
|
||||
private wrap(content: string, tokenClass: string): string {
|
||||
if (!this.options.enableColors) return this.escapeHtml(content);
|
||||
|
||||
// Handle multiple classes (e.g., "syntax-pink bold")
|
||||
const classes = tokenClass
|
||||
.split(" ")
|
||||
.map(cls => cls.trim())
|
||||
.join(" ");
|
||||
return `<span class="${classes}">${this.escapeHtml(content)}</span>`;
|
||||
}
|
||||
|
||||
private isIdentifierStart(char: string): boolean {
|
||||
return IDENTIFIER_START.has(char);
|
||||
}
|
||||
|
||||
private isIdentifierPart(char: string): boolean {
|
||||
return IDENTIFIER_PART.has(char);
|
||||
}
|
||||
|
||||
private isWhitespace(char: string): boolean {
|
||||
return WHITESPACE.has(char);
|
||||
}
|
||||
|
||||
private isDigit(char: string): boolean {
|
||||
return DIGITS.has(char);
|
||||
}
|
||||
|
||||
private isHexDigit(char: string): boolean {
|
||||
return HEX_DIGITS.has(char);
|
||||
}
|
||||
|
||||
private isOperator(char: string): boolean {
|
||||
return OPERATORS.has(char);
|
||||
}
|
||||
|
||||
private consumeIdentifier(): string {
|
||||
let identifier = this.consume();
|
||||
while (this.pos < this.text.length && this.isIdentifierPart(this.peek())) {
|
||||
identifier += this.consume();
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private consumeString(quote: string): string {
|
||||
let str = "";
|
||||
let pos = this.pos;
|
||||
let isEscaped = false;
|
||||
|
||||
// Consume initial quote
|
||||
str += this.consume();
|
||||
|
||||
while (this.pos < this.text.length) {
|
||||
const char = this.peek();
|
||||
|
||||
if (isEscaped) {
|
||||
str += this.consume();
|
||||
isEscaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "\\") {
|
||||
str += this.consume();
|
||||
isEscaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === quote) {
|
||||
str += this.consume();
|
||||
break;
|
||||
}
|
||||
|
||||
str += this.consume();
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private consumeTemplateString(): string {
|
||||
let str = "";
|
||||
let pos = this.pos;
|
||||
let isEscaped = false;
|
||||
|
||||
// Consume initial backtick
|
||||
str += this.consume();
|
||||
|
||||
while (this.pos < this.text.length) {
|
||||
const char = this.peek();
|
||||
|
||||
if (isEscaped) {
|
||||
str += this.consume();
|
||||
isEscaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "\\") {
|
||||
str += this.consume();
|
||||
isEscaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "`") {
|
||||
str += this.consume();
|
||||
break;
|
||||
}
|
||||
|
||||
if (char === "$" && this.peek(1) === "{") {
|
||||
return str;
|
||||
}
|
||||
|
||||
str += this.consume();
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private consumeNumber(): string {
|
||||
let num = "";
|
||||
// Handle hex
|
||||
if (this.peek() === "0") {
|
||||
const next = this.peek(1);
|
||||
if (next === "x" || next === "X") {
|
||||
num = this.consume(2);
|
||||
while (this.pos < this.text.length && this.isHexDigit(this.peek())) {
|
||||
num += this.consume();
|
||||
}
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular number
|
||||
while (this.pos < this.text.length) {
|
||||
const char = this.peek();
|
||||
if (this.isDigit(char) || char === "." || char === "e" || char === "E") {
|
||||
num += this.consume();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
private consumeComment(): string {
|
||||
const commentStart = this.consume(2); // Consume // or /*
|
||||
let comment = commentStart;
|
||||
const isLineComment = commentStart === "//";
|
||||
|
||||
if (isLineComment) {
|
||||
// Consume until newline or end of file
|
||||
while (this.pos < this.text.length) {
|
||||
const char = this.peek();
|
||||
if (char === "\n") {
|
||||
// Don't consume the newline as part of the comment
|
||||
break;
|
||||
}
|
||||
comment += this.consume();
|
||||
}
|
||||
} else {
|
||||
// Handle block comments
|
||||
let foundEnd = false;
|
||||
while (this.pos < this.text.length && !foundEnd) {
|
||||
if (this.peek() === "*" && this.peek(1) === "/") {
|
||||
comment += this.consume(2);
|
||||
foundEnd = true;
|
||||
} else {
|
||||
comment += this.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
return comment;
|
||||
}
|
||||
|
||||
private consumeOperator(): string {
|
||||
let operator = this.consume();
|
||||
|
||||
// Handle multi-character operators
|
||||
const next = this.peek();
|
||||
if (this.isOperator(next)) {
|
||||
const combined = operator + next;
|
||||
// Handle common compound operators
|
||||
if (["==", "===", "!=", "!==", ">=", "<=", "++", "--", "&&", "||", "??", ">>", "<<", "=>"].includes(combined)) {
|
||||
operator += this.consume();
|
||||
// Handle triple operators
|
||||
if (
|
||||
(combined === "==" || combined === "!=" || combined === "<<" || combined === ">>") &&
|
||||
this.peek() === combined[0]
|
||||
) {
|
||||
operator += this.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
return operator;
|
||||
}
|
||||
|
||||
private shouldRedactSensitive(str: string): boolean {
|
||||
// Simple string matching without regex
|
||||
// Check for UUID-like pattern
|
||||
if (str.length === 36 && str[8] === "-" && str[13] === "-" && str[18] === "-" && str[23] === "-") {
|
||||
const isValidChar = (c: string) => this.isHexDigit(c) || c === "-";
|
||||
return [...str].every(isValidChar);
|
||||
}
|
||||
|
||||
// Check for URL with credentials
|
||||
if (str.includes("@") && (str.startsWith("http://") || str.startsWith("https://") || str.startsWith("ftp://"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for NPM token
|
||||
if (str.startsWith("npm_") && str.length === 68) {
|
||||
const isValidTokenChar = (c: string) => this.isIdentifierPart(c);
|
||||
return [...str.slice(4)].every(isValidTokenChar);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user