import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
import { cn } from "@/lib/utils";
const KEYWORDS = new Set([
"SELECT",
"FROM",
"WHERE",
"JOIN",
"LEFT",
"RIGHT",
"INNER",
"OUTER",
"FULL",
"CROSS",
"ON",
"AND",
"OR",
"NOT",
"IN",
"LIKE",
"ILIKE",
"IS",
"NULL",
"TRUE",
"FALSE",
"GROUP",
"BY",
"ORDER",
"HAVING",
"LIMIT",
"OFFSET",
"WITH",
"AS",
"CASE",
"WHEN",
"THEN",
"ELSE",
"END",
"DISTINCT",
"UNION",
"ALL",
"INTERSECT",
"EXCEPT",
"INSERT",
"INTO",
"VALUES",
"UPDATE",
"SET",
"DELETE",
"CREATE",
"TABLE",
"VIEW",
"INDEX",
"DROP",
"ALTER",
"ADD",
"COLUMN",
"PRIMARY",
"KEY",
"FOREIGN",
"REFERENCES",
"CONSTRAINT",
"DEFAULT",
"UNIQUE",
"CHECK",
"EXISTS",
"BETWEEN",
"ASC",
"DESC",
"NULLS",
"FIRST",
"LAST",
"OVER",
"PARTITION",
"ROWS",
"RANGE",
"UNBOUNDED",
"PRECEDING",
"FOLLOWING",
"CURRENT",
"ROW",
"WINDOW",
"LATERAL",
"ARRAY",
"STRUCT",
"UNNEST",
"TABLESAMPLE",
"SYSTEM",
"BERNOULLI",
"PERCENT",
"FETCH",
"NEXT",
"ONLY",
"TIES",
"RECURSIVE",
"TEMPORARY",
"TEMP",
"IF",
"IFNULL",
"IFF",
"QUALIFY",
"PIVOT",
"UNPIVOT",
"MATCH",
"AGAINST",
"NATURAL",
"USING",
"REPLACE",
"IGNORE",
"ROLLUP",
"CUBE",
"GROUPING",
"SETS",
"DATE",
"DATETIME",
"TIMESTAMP",
"TIME",
"INTERVAL",
"CAST",
"CONVERT",
"TRY_CAST",
"SAFE_CAST",
"EXTRACT",
"EPOCH",
"YEAR",
"MONTH",
"DAY",
"HOUR",
"MINUTE",
"SECOND",
"WEEK",
"QUARTER",
"DOW",
"DOY",
]);
type TokenType =
| "keyword"
| "function"
| "string"
| "comment"
| "number"
| "operator"
| "punctuation"
| "variable"
| "plain";
interface Token {
type: TokenType;
value: string;
}
function tokenize(sql: string): Token[] {
const tokens: Token[] = [];
let i = 0;
while (i < sql.length) {
if (sql[i] === "-" && sql[i + 1] === "-") {
const end = sql.indexOf("\n", i);
const value = end === -1 ? sql.slice(i) : sql.slice(i, end);
tokens.push({ type: "comment", value });
i += value.length;
continue;
}
if (sql[i] === "/" && sql[i + 1] === "*") {
const end = sql.indexOf("*/", i + 2);
const value = end === -1 ? sql.slice(i) : sql.slice(i, end + 2);
tokens.push({ type: "comment", value });
i += value.length;
continue;
}
if (sql[i] === "'") {
let j = i + 1;
while (j < sql.length) {
if (sql[j] === "'" && sql[j + 1] === "'") {
j += 2;
} else if (sql[j] === "'") {
j++;
break;
} else {
j++;
}
}
tokens.push({ type: "string", value: sql.slice(i, j) });
i = j;
continue;
}
if (sql[i] === '"') {
let j = i + 1;
while (j < sql.length && sql[j] !== '"') j++;
if (j < sql.length) j++;
tokens.push({ type: "plain", value: sql.slice(i, j) });
i = j;
continue;
}
if (sql[i] === "`") {
let j = i + 1;
while (j < sql.length && sql[j] !== "`") j++;
if (j < sql.length) j++;
tokens.push({ type: "plain", value: sql.slice(i, j) });
i = j;
continue;
}
if (sql[i] === "{" && sql[i + 1] === "{") {
const end = sql.indexOf("}}", i + 2);
const value = end === -1 ? sql.slice(i) : sql.slice(i, end + 2);
tokens.push({ type: "variable", value });
i += value.length;
continue;
}
if (
/[0-9]/.test(sql[i]) ||
(sql[i] === "." && /[0-9]/.test(sql[i + 1] ?? ""))
) {
let j = i;
while (j < sql.length && /[0-9._eExX]/.test(sql[j])) j++;
tokens.push({ type: "number", value: sql.slice(i, j) });
i = j;
continue;
}
if (/[A-Za-z_]/.test(sql[i])) {
let j = i;
while (j < sql.length && /[A-Za-z0-9_]/.test(sql[j])) j++;
const word = sql.slice(i, j);
const upper = word.toUpperCase();
let k = j;
while (k < sql.length && sql[k] === " ") k++;
const isFunction = sql[k] === "(";
if (isFunction && !KEYWORDS.has(upper)) {
tokens.push({ type: "function", value: word });
} else if (KEYWORDS.has(upper)) {
tokens.push({ type: "keyword", value: word });
} else {
tokens.push({ type: "plain", value: word });
}
i = j;
continue;
}
const twoChar = sql.slice(i, i + 2);
if (["<>", "!=", "<=", ">=", "::", "||"].includes(twoChar)) {
tokens.push({ type: "operator", value: twoChar });
i += 2;
continue;
}
const ch = sql[i];
if ("=<>+-*/%!".includes(ch)) {
tokens.push({ type: "operator", value: ch });
} else if ("(),;.[]".includes(ch)) {
tokens.push({ type: "punctuation", value: ch });
} else {
tokens.push({ type: "plain", value: ch });
}
i++;
}
return tokens;
}
const TOKEN_CLASS: Record = {
keyword: "text-blue-600 dark:text-blue-400 font-semibold",
function: "text-violet-600 dark:text-violet-400",
string: "text-green-700 dark:text-green-400",
comment: "text-slate-400 dark:text-slate-500 italic",
number: "text-amber-600 dark:text-amber-400",
operator: "text-rose-500 dark:text-rose-400",
punctuation: "text-slate-500 dark:text-slate-400",
variable: "text-orange-600 dark:text-orange-400 font-medium",
plain: "",
};
interface SqlHighlightProps extends HTMLAttributes {
sql: string;
preClassName?: string;
}
export const SqlHighlight = forwardRef(
function SqlHighlight({ sql, className, preClassName, ...props }, ref) {
const tokens = tokenize(sql);
const nodes: ReactNode[] = tokens.map((tok, idx) => {
const cls = TOKEN_CLASS[tok.type];
if (!cls) return tok.value;
return (
{tok.value}
);
});
return (
{nodes}
);
},
);