import { parse, ParseError } from 'jsonc-parser'; import { parse as parseYaml } from 'yaml'; export const DIAGRAM_TYPES = ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'] as const; export type DiagramType = (typeof DIAGRAM_TYPES)[number]; const FILENAME_RE = /\.(architecture|workflow|sequence|dataflow|lifecycle)\.json$/i; const ARCHIFY_EXT_RE = /\.archify$/i; const MAX_SNIFF_BYTES = 8 * 1024 * 1024; export function isDiagramType(value: unknown): value is DiagramType { return typeof value === 'string' && (DIAGRAM_TYPES as readonly string[]).includes(value); } export function typeFromFilename(fileName: string): DiagramType | undefined { const match = FILENAME_RE.exec(fileName); return match ? (match[1].toLowerCase() as DiagramType) : undefined; } /** `name.archify`: an Archify diagram whose type comes from `diagram_type`. */ export function isArchifyFile(fileName: string): boolean { return ARCHIFY_EXT_RE.test(fileName); } /** File names that are diagram sources regardless of their content. */ export function isDiagramFileName(fileName: string): boolean { return isArchifyFile(fileName) || FILENAME_RE.test(fileName); } /** Base name without the diagram source extension (`web.archify` → `web`). */ export function diagramBaseName(fileName: string): string { const base = fileName.split(/[\\/]/).pop() ?? fileName; return base.replace(ARCHIFY_EXT_RE, '').replace(/\.json$/i, ''); } export interface DiagramSource { type: DiagramType; diagram: Record; /** Source syntax: `.archify` files are YAML, Archify's own sources JSON. */ format: 'yaml' | 'json'; /** JSON text for the Archify renderer, which only reads JSON. */ json: string; } /** Parses `.archify` (YAML) or JSON text; undefined when it does not parse. */ export function parseDiagramText(text: string, fileName = ''): unknown { if (isArchifyFile(fileName)) { try { // YAML 1.2 also accepts the JSON form of older .archify files. return parseYaml(text, { prettyErrors: false, uniqueKeys: false }); } catch { return undefined; } } const errors: ParseError[] = []; return parse(text, errors, { allowTrailingComma: true }); } /** * Returns the diagram when the text declares an Archify `diagram_type`. * Falls back to the file name for documents that do not parse yet, so a * half-typed source still counts as a diagram. */ export function readDiagramSource(text: string, fileName = ''): DiagramSource | undefined { if (text.length > MAX_SNIFF_BYTES) return undefined; const format = isArchifyFile(fileName) ? 'yaml' : 'json'; const value = parseDiagramText(text, fileName) as Record | undefined; const isObject = value !== null && typeof value === 'object' && !Array.isArray(value); const json = (diagram: unknown) => (format === 'json' ? text : JSON.stringify(diagram ?? {}, null, 2)); if (isObject && isDiagramType(value.diagram_type)) { return { type: value.diagram_type, diagram: value, format, json: json(value) }; } const byName = typeFromFilename(fileName); if (byName) { const diagram = isObject ? value : {}; return { type: byName, diagram, format, json: json(diagram) }; } return undefined; } /** True when the HTML was produced by the Archify renderer. */ export function isArchifyHtml(text: string): boolean { const head = text.slice(0, 4096); return /