feat(editor): auto-detect JSON and XML formatting

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-02 09:01:48 +08:00
1 parent 3e779d24d7
commit 0bf2475804
15 files changed
+407 -2

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
import { formatJsonSource } from "@/lib/common/safeJsonFormat";
import { formatXmlSource } from "@/lib/common/xmlFormat";
/** Structured (non-SQL) content types the auto-format can recognise. */
export type AutoDetectedStructuredType = "json" | "xml";
/**
* Result of {@link detectAndFormatStructured}.
*
* - `{ kind: "sql" }` — not a JSON/XML document; hand off to the SQL formatter.
* - `{ kind: "json" | "xml", formatted }` — a valid structured document.
* - `{ kind: "unsupported", detectedType }` — structured-looking invalid text.
* The caller must keep the original text unchanged and must never pass it to
* the SQL formatter.
*/
export type StructuredFormatResult = { kind: "sql" } | { kind: "json" | "xml"; formatted: string } | { kind: "unsupported"; detectedType: AutoDetectedStructuredType };
/** First non-whitespace character of `text`, or null when it is all whitespace. */
export function firstNonWhitespaceChar(text: string): string | null {
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c !== " " && c !== "\t" && c !== "\n" && c !== "\r" && c !== "\f" && c !== "\v") return c;
}
return null;
}
/**
* Detects whether an editor selection is a JSON/XML document and formats it.
*
* A valid JSON document must start with `{` or `[`, but an editor selection can
* also be a SQL fragment such as a SQL Server bracket-quoted identifier. Only a
* successful JSON source parse is therefore classified as JSON; all other text
* continues through the existing SQL path.
*
* Safety contract: valid JSON/XML is formatted before reaching the SQL formatter.
* Invalid XML is returned as unsupported because sql-formatter silently rewrites
* XML-looking input into corrupted output.
*/
export function detectAndFormatStructured(text: string, options: { indentSize: number; useTabs?: boolean }): StructuredFormatResult {
const first = firstNonWhitespaceChar(text);
if (first === "{" || first === "[") {
try {
return { kind: "json", formatted: formatJsonSource(text, options.indentSize) };
} catch {
return { kind: "sql" };
}
}
if (looksLikeXml(text)) {
try {
return { kind: "xml", formatted: formatXmlSource(text, options.useTabs ? "\t" : " ".repeat(options.indentSize)) };
} catch {
return { kind: "unsupported", detectedType: "xml" };
}
}
return { kind: "sql" };
}
export function looksLikeXml(text: string): boolean {
const trimmed = text.trimStart();
return trimmed.startsWith("<?") || trimmed.startsWith("<!--") || /^<!DOCTYPE\b/i.test(trimmed) || /^<\/?[A-Za-z_:]/.test(trimmed);
}
+20
View File
@@ -1,9 +1,25 @@
import { DEFAULT_SQL_FORMATTER_SETTINGS, sqlFormatterOptions, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
import { looksLikeXml } from "@/lib/sql/autoFormat";
export type SqlFormatDialect = "mysql" | "postgres" | "sqlite" | "sqlserver" | "clickhouse" | "generic";
export const MAX_SQL_FORMAT_CHARS = 1_000_000;
/**
* Thrown by {@link formatSqlText} when the input is XML-looking and must never
* be run through the SQL formatter. sql-formatter silently rewrites well-formed
* XML into corrupted output, so this guard keeps any caller (including future
* ones) from corrupting structured text. Callers that can format XML should
* route before calling (see {@link detectAndFormatStructured}); this is
* defense-in-depth.
*/
export class UnsupportedStructuredInputError extends Error {
constructor(readonly detectedType: "xml") {
super(`Cannot format ${detectedType} content as SQL.`);
this.name = "UnsupportedStructuredInputError";
}
}
/**
* Maps a connection's database type to the SQL-formatter dialect to use.
*
@@ -65,6 +81,10 @@ export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "ge
throw new Error("SQL is too large to format safely.");
}
if (looksLikeXml(sql)) {
throw new UnsupportedStructuredInputError("xml");
}
const { format } = await import("sql-formatter");
const options = sqlFormatterOptions(settings);
const language = formatterLanguage(dialect);