import { parse, ParseError } from 'jsonc-parser'; export const DIAGRAM_TYPES = ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'] as const; export type DiagramType = (typeof DIAGRAM_TYPES)[number]; /** Node collection that may carry `sources` evidence, per diagram type. */ export const EVIDENCE_COLLECTIONS: Record = { architecture: 'components', workflow: 'nodes', sequence: 'participants', dataflow: 'nodes', lifecycle: 'states', }; 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; } /** * Parses JSON text and returns the diagram when it 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 errors: ParseError[] = []; const value = parse(text, errors, { allowTrailingComma: true }); if (value && typeof value === 'object' && !Array.isArray(value) && isDiagramType(value.diagram_type)) { return { type: value.diagram_type, diagram: value }; } const byName = typeFromFilename(fileName); if (byName) { const diagram = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; return { type: byName, 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 /): boolean { const meta = diagram.meta as Record | undefined; if (meta && meta.repository) return true; const nodes = diagram[EVIDENCE_COLLECTIONS[type]]; return Array.isArray(nodes) && nodes.some((node) => Array.isArray(node?.sources) && node.sources.length > 0); } /** Returns a copy of the diagram with repository evidence removed. */ export function stripSourceEvidence(type: DiagramType, diagram: Record): Record { const copy = structuredClone(diagram); const meta = copy.meta as Record | undefined; if (meta) delete meta.repository; const nodes = copy[EVIDENCE_COLLECTIONS[type]]; if (Array.isArray(nodes)) { for (const node of nodes) { if (node && typeof node === 'object') delete node.sources; } } return copy; }