Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
10 kB ยท 333 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334import { parseThemeVarsText, validateVarEntry } from "./custom-theme";import type { Theme, ThemeDefinition, ThemeInfo, ThemeMode, ThemeVarName, ThemeVars,} from "./types";
export type ThemeExportFormat = "ts" | "json" | "css";
export type ParsedThemeInput = { info: Partial<ThemeInfo>; definition: ThemeDefinition; /** Merge into the current theme instead of replacing it (bare declarations, a lone CSS block). */ partial: boolean; /** Dropped vars / values โ the rest of the input still applied. */ warnings: string[];};
export type ParseThemeInputResult = | ({ ok: true } & ParsedThemeInput) | { ok: false; error: string };
/** * Parses anything a builder, an agent or a theme file hands over: a `Theme` * (or `{ light, dark }`) as JSON or as a TS/JS object literal, CSS with * `:root { }` / `.dark { }` blocks, or bare `--name: value;` declarations * (applied to `fallbackMode`). */export function parseThemeInput( text: string, fallbackMode: ThemeMode = "light",): ParseThemeInputResult { const source = text.trim(); if (source.length === 0) return { ok: false, error: "Nothing to paste." };
const light = cssBlock(source, [":root", ".light"]); const dark = cssBlock(source, [".dark"]); const parsed = light !== undefined || dark !== undefined ? parseCssBlocks(light, dark) : source.includes("{") ? parseObjectLiteral(source, fallbackMode) : parseDeclarations(source, fallbackMode);
if (!parsed.ok) return parsed;
const hasVars = Object.keys(parsed.definition.light).length > 0 || Object.keys(parsed.definition.dark).length > 0; if (!hasVars) { const detail = parsed.warnings[0] ? ` ${parsed.warnings[0]}` : ""; return { ok: false, error: `No supported CSS variables found.${detail}` }; } return parsed;}
function parseDeclarations( source: string, mode: ThemeMode,): ParseThemeInputResult { const { vars, errors } = parseThemeVarsText(source); const definition: ThemeDefinition = { light: {}, dark: {} }; definition[mode] = vars; return { ok: true, info: {}, definition, partial: true, warnings: errors };}
/** Body of the first `selector { โฆ }`; indexOf instead of a regex (CodeQL js/polynomial-redos). */function cssBlock(source: string, selectors: string[]) { for (const selector of selectors) { let from = 0; let start = source.indexOf(selector, from); while (start !== -1) { from = start + selector.length; const open = source.indexOf("{", from); if (open === -1) break; // `.light-theme {` is not `.light {` if (source.slice(from, open).trim().length === 0) { const close = source.indexOf("}", open); return close === -1 ? undefined : source.slice(open + 1, close); } start = source.indexOf(selector, from); } } return undefined;}
function parseCssBlocks( light: string | undefined, dark: string | undefined,): ParseThemeInputResult { const lightResult = parseThemeVarsText(light ?? ""); const darkResult = parseThemeVarsText(dark ?? ""); return { ok: true, info: {}, definition: { light: lightResult.vars, dark: darkResult.vars }, // a lone block must not wipe the other mode partial: light === undefined || dark === undefined, warnings: [...lightResult.errors, ...darkResult.errors], };}
function parseObjectLiteral( source: string, fallbackMode: ThemeMode,): ParseThemeInputResult { // `import type { Theme }` carries a brace before the literal; everything // else ahead of it (comments, fences, prose) is skipped by objectStart const stripped = source.replace(/^\s*import\b[^\n]*$/gm, ""); const from = objectStart(stripped); if (from === -1) { return { ok: false, error: "Expected an object with light / dark vars." }; } const json = toJson(stripped.slice(from).replace(/\bas const\b/g, "")); const start = json.indexOf("{"); const end = json.lastIndexOf("}"); if (start === -1 || end <= start) { return { ok: false, error: "Expected an object with light / dark vars." }; }
let value: unknown; try { value = JSON.parse(json.slice(start, end + 1)); } catch { return { ok: false, error: "Could not parse the configuration. Paste the JSON or the theme file as is.", }; } if (!isRecord(value)) { return { ok: false, error: "Expected an object with light / dark vars." }; }
const warnings: string[] = []; const definition: ThemeDefinition = { light: {}, dark: {} }; let partial = false;
if (isRecord(value.light) || isRecord(value.dark)) { // `{ light }` alone must not wipe dark (and vice versa) partial = value.light === undefined || value.dark === undefined; for (const mode of ["light", "dark"] as const) { const vars = value[mode]; if (vars === undefined) continue; if (!isRecord(vars)) { warnings.push(`"${mode}" must be an object of CSS variables.`); continue; } definition[mode] = pickVars(vars, warnings); } } else if (Object.keys(value).some((key) => key.startsWith("--"))) { definition[fallbackMode] = pickVars(value, warnings); partial = true; } else { return { ok: false, error: "Expected an object with light / dark vars." }; }
const info: Partial<ThemeInfo> = {}; if (typeof value.id === "string" && value.id.trim()) info.id = value.id.trim(); if (typeof value.name === "string" && value.name.trim()) { info.name = value.name.trim(); } if ( isRecord(value.author) && typeof value.author.name === "string" && typeof value.author.url === "string" ) { info.author = { name: value.author.name, url: value.author.url }; }
return { ok: true, info, definition, partial, warnings };}
function pickVars( input: Record<string, unknown>, warnings: string[],): ThemeVars { const vars: ThemeVars = {}; for (const [name, raw] of Object.entries(input)) { if (typeof raw !== "string") { warnings.push(`Value of "${name}" must be a string.`); continue; } const errors = validateVarEntry(name, raw); if (errors.length > 0) { warnings.push(...errors); continue; } // safe: validateVarEntry checked membership in THEME_VAR_NAMES vars[name as ThemeVarName] = raw.trim(); } return vars;}
/** Index of the first `{` outside a comment; the body itself is left to the string-aware toJson. */function objectStart(source: string) { let i = 0; while (i < source.length) { if (source.startsWith("//", i)) { const end = source.indexOf("\n", i); if (end === -1) return -1; i = end + 1; } else if (source.startsWith("/*", i)) { const end = source.indexOf("*/", i + 2); if (end === -1) return -1; i = end + 2; } else if (source.charAt(i) === "{") { return i; } else { i++; } } return -1;}
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);}
/** * Rewrites a TS/JS object literal into strict JSON: strips comments, quotes * bare keys, normalizes quotes and drops trailing commas. String-aware, so * `//` inside an author url survives. */function toJson(source: string): string { let out = ""; let i = 0; while (i < source.length) { const ch = source.charAt(i);
if (ch === '"' || ch === "'" || ch === "`") { let j = i + 1; let str = ""; while (j < source.length && source[j] !== ch) { if (source[j] === "\\") { str += source[j + 1] ?? ""; j += 2; continue; } str += source.charAt(j); j++; } out += JSON.stringify(str); i = j + 1; continue; }
if (ch === "/" && source[i + 1] === "/") { while (i < source.length && source[i] !== "\n") i++; continue; } if (ch === "/" && source[i + 1] === "*") { const end = source.indexOf("*/", i + 2); i = end === -1 ? source.length : end + 2; continue; }
if (/[A-Za-z_$]/.test(ch)) { let j = i; while (j < source.length && /[\w$]/.test(source.charAt(j))) j++; const word = source.slice(i, j); let k = j; while (k < source.length && /\s/.test(source.charAt(k))) k++; if (source[k] === ":") { out += `${JSON.stringify(word)}:`; i = k + 1; } else { out += word; i = j; } continue; }
// comments are already gone from `out`, so a trailing comma is whatever // precedes the closing bracket if (ch === "}" || ch === "]") out = out.replace(/,\s*$/, "");
out += ch; i++; } return out;}
/** Serializes a theme for the clipboard: theme file, JSON or CSS blocks. */export function serializeTheme(theme: Theme, format: ThemeExportFormat) { switch (format) { case "json": return JSON.stringify(theme, null, 2); case "css": return `:root {\n${formatBlock(theme.light)}\n}\n.dark {\n${formatBlock(theme.dark)}\n}`; case "ts": return [ `// packages/theme-store/src/${theme.id}.ts`, `import type { Theme } from "./types";`, "", `export const ${constName(theme.id)} = {`, ` id: ${JSON.stringify(theme.id)},`, ` name: ${JSON.stringify(theme.name)},`, ` author: { name: ${JSON.stringify(theme.author.name)}, url: ${JSON.stringify(theme.author.url)} },`, ` light: {\n${formatObject(theme.light)}\n },`, ` dark: {\n${formatObject(theme.dark)}\n },`, "} as const satisfies Theme;", ].join("\n"); }}
function constName(id: string) { const base = id .toUpperCase() .replace(/[^A-Z0-9]+/g, "_") .replace(/^_|_$/g, "") .replace(/_?THEME$/, ""); return `${base || "MY"}_THEME`;}
function formatBlock(vars: ThemeVars) { return Object.entries(vars) .map(([name, value]) => ` ${name}: ${value};`) .join("\n");}
function formatObject(vars: ThemeVars) { return Object.entries(vars) .map( ([name, value]) => ` ${JSON.stringify(name)}: ${JSON.stringify(value)},`, ) .join("\n");}