┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ // Chain layout for generated diagrams: a linear pipeline renders as │
│ // horizontal boxes with labels over the wires, or stacks on a straight │
│ // vertical spine when it doesn't fit. Branching layout is delegated to │
│ // mermaid-ascii (see mermaid.ts). │
│ import { G, center, len } from "./ascii"; │
│ │
│ export interface FlowEdge { │
│ from: string; │
│ to: string; │
│ label: string; │
│ } │
│ │
│ export interface FlowGraph { │
│ labels: Map<string, string>; │
│ edges: FlowEdge[]; │
│ } │
│ │
│ // Orders the nodes if the edges form a single path, else null. │
│ function chainOf(g: FlowGraph): string[] | null { │
│ const nodes = new Set<string>(g.edges.flatMap((e) => [e.from, e.to])); │
│ const out = new Map<string, string>(); │
│ const hasIn = new Set<string>(); │
│ for (const e of g.edges) { │
│ if (out.has(e.from) || hasIn.has(e.to)) return null; │
│ out.set(e.from, e.to); │
│ hasIn.add(e.to); │
│ } │
│ const heads = [...nodes].filter((n) => !hasIn.has(n)); │
│ if (heads.length !== 1) return null; │
│ const chain = [heads[0]]; │
│ while (out.has(chain[chain.length - 1])) chain.push(out.get(chain[chain.length - │
│ 1])!); │
│ return chain.length === nodes.size ? chain : null; │
│ } │
│ │
│ function horizontal(chain: string[], g: FlowGraph, width: number): string | null { │
│ const disp = chain.map((id) => g.labels.get(id) ?? id); │
│ const bw = disp.map((d) => len(d) + 4); │
│ const ew = g.edges.map((e) => Math.max(len(e.label) + 2, 4)); │
│ const total = bw.reduce((a, b) => a + b, 0) + ew.reduce((a, b) => a + b, 0); │
│ if (total > width) return null; │
│ │
│ let labelRow = ""; │
│ let top = ""; │
│ let mid = ""; │
│ let bot = ""; │
│ chain.forEach((_, i) => { │
│ labelRow += " ".repeat(bw[i]); │
│ top += G.TL + G.H.repeat(bw[i] - 2) + G.TR; │
│ mid += G.V + center(disp[i], bw[i] - 2) + G.V; │
│ bot += G.BL + G.H.repeat(bw[i] - 2) + G.BR; │
│ if (i < g.edges.length) { │
│ labelRow += center(g.edges[i].label, ew[i]); │
│ top += " ".repeat(ew[i]); │
│ mid += G.H.repeat(ew[i] - 1) + ">"; │
│ bot += " ".repeat(ew[i]); │
│ } │
│ }); │
│ │
│ const rows = [top, mid, bot].map((r) => r.replace(/\s+$/, "")); │
│ if (g.edges.some((e) => e.label)) rows.unshift(labelRow.replace(/\s+$/, "")); │
│ return rows.join("\n"); │
│ } │
│ │
│ function vertical(chain: string[], g: FlowGraph): string { │
│ const disp = chain.map((id) => g.labels.get(id) ?? id); │
│ const bw = disp.map((d) => len(d) + 4); │
│ const spine = Math.floor(Math.max(...bw) / 2); │
│ │
│ const border = (l: string, r: string, w: number, left: number, junction: string │
│ | null) => { │
│ if (junction === null) return " ".repeat(left) + l + G.H.repeat(w - 2) + r; │
│ const before = spine - left - 1; │
│ return " ".repeat(left) + l + G.H.repeat(before) + junction + G.H.repeat(w - 2 │
│ - before - 1) + r; │
│ }; │
│ │
│ const rows: string[] = []; │
│ chain.forEach((_, i) => { │
│ const w = bw[i]; │
│ const left = spine - Math.floor(w / 2); │
│ rows.push(border(G.TL, G.TR, w, left, i > 0 ? G.BJ : null)); │
│ rows.push(" ".repeat(left) + G.V + center(disp[i], w - 2) + G.V); │
│ rows.push(border(G.BL, G.BR, w, left, i < chain.length - 1 ? G.TJ : null)); │
│ if (i < g.edges.length) { │
│ const label = g.edges[i].label; │
│ rows.push(" ".repeat(spine) + G.V + (label ? ` ${label}` : "")); │
│ rows.push(" ".repeat(spine) + "↓"); │
│ } │
│ }); │
│ return rows.join("\n"); │
│ } │
│ │
│ /** Renders g if its edges form one linear chain; null otherwise. */ │
│ export function renderChain(g: FlowGraph, width: number, preferHorizontal = true): │
│ string | null { │
│ const chain = chainOf(g); │
│ if (!chain) return null; │
│ │
│ // Edges may be authored in any order; realign them with the chain. │
│ const byFrom = new Map(g.edges.map((e) => [e.from, e])); │
│ const ordered: FlowGraph = { │
│ labels: g.labels, │
│ edges: chain.slice(0, -1).map((id) => byFrom.get(id)!), │
│ }; │
│ if (!preferHorizontal) return vertical(chain, ordered); │
│ return horizontal(chain, ordered, width) ?? vertical(chain, ordered); │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ // Chain layout for generated diagrams: a li │
│ near pipeline renders as │
│ // horizontal boxes with labels over the wir │
│ es, or stacks on a straight │
│ // vertical spine when it doesn't fit. Branc │
│ hing layout is delegated to │
│ // mermaid-ascii (see mermaid.ts). │
│ import { G, center, len } from "./ascii"; │
│ │
│ export interface FlowEdge { │
│ from: string; │
│ to: string; │
│ label: string; │
│ } │
│ │
│ export interface FlowGraph { │
│ labels: Map<string, string>; │
│ edges: FlowEdge[]; │
│ } │
│ │
│ // Orders the nodes if the edges form a sing │
│ le path, else null. │
│ function chainOf(g: FlowGraph): string[] | n │
│ ull { │
│ const nodes = new Set<string>(g.edges.flat │
│ Map((e) => [e.from, e.to])); │
│ const out = new Map<string, string>(); │
│ const hasIn = new Set<string>(); │
│ for (const e of g.edges) { │
│ if (out.has(e.from) || hasIn.has(e.to)) │
│ return null; │
│ out.set(e.from, e.to); │
│ hasIn.add(e.to); │
│ } │
│ const heads = [...nodes].filter((n) => !ha │
│ sIn.has(n)); │
│ if (heads.length !== 1) return null; │
│ const chain = [heads[0]]; │
│ while (out.has(chain[chain.length - 1])) c │
│ hain.push(out.get(chain[chain.length - 1])!) │
│ ; │
│ return chain.length === nodes.size ? chain │
│ : null; │
│ } │
│ │
│ function horizontal(chain: string[], g: Flow │
│ Graph, width: number): string | null { │
│ const disp = chain.map((id) => g.labels.ge │
│ t(id) ?? id); │
│ const bw = disp.map((d) => len(d) + 4); │
│ const ew = g.edges.map((e) => Math.max(len │
│ (e.label) + 2, 4)); │
│ const total = bw.reduce((a, b) => a + b, 0 │
│ ) + ew.reduce((a, b) => a + b, 0); │
│ if (total > width) return null; │
│ │
│ let labelRow = ""; │
│ let top = ""; │
│ let mid = ""; │
│ let bot = ""; │
│ chain.forEach((_, i) => { │
│ labelRow += " ".repeat(bw[i]); │
│ top += G.TL + G.H.repeat(bw[i] - 2) + G. │
│ TR; │
│ mid += G.V + center(disp[i], bw[i] - 2) │
│ + G.V; │
│ bot += G.BL + G.H.repeat(bw[i] - 2) + G. │
│ BR; │
│ if (i < g.edges.length) { │
│ labelRow += center(g.edges[i].label, e │
│ w[i]); │
│ top += " ".repeat(ew[i]); │
│ mid += G.H.repeat(ew[i] - 1) + ">"; │
│ bot += " ".repeat(ew[i]); │
│ } │
│ }); │
│ │
│ const rows = [top, mid, bot].map((r) => r. │
│ replace(/\s+$/, "")); │
│ if (g.edges.some((e) => e.label)) rows.uns │
│ hift(labelRow.replace(/\s+$/, "")); │
│ return rows.join("\n"); │
│ } │
│ │
│ function vertical(chain: string[], g: FlowGr │
│ aph): string { │
│ const disp = chain.map((id) => g.labels.ge │
│ t(id) ?? id); │
│ const bw = disp.map((d) => len(d) + 4); │
│ const spine = Math.floor(Math.max(...bw) / │
│ 2); │
│ │
│ const border = (l: string, r: string, w: n │
│ umber, left: number, junction: string | null │
│ ) => { │
│ if (junction === null) return " ".repeat │
│ (left) + l + G.H.repeat(w - 2) + r; │
│ const before = spine - left - 1; │
│ return " ".repeat(left) + l + G.H.repeat │
│ (before) + junction + G.H.repeat(w - 2 - bef │
│ ore - 1) + r; │
│ }; │
│ │
│ const rows: string[] = []; │
│ chain.forEach((_, i) => { │
│ const w = bw[i]; │
│ const left = spine - Math.floor(w / 2); │
│ rows.push(border(G.TL, G.TR, w, left, i │
│ > 0 ? G.BJ : null)); │
│ rows.push(" ".repeat(left) + G.V + cente │
│ r(disp[i], w - 2) + G.V); │
│ rows.push(border(G.BL, G.BR, w, left, i │
│ < chain.length - 1 ? G.TJ : null)); │
│ if (i < g.edges.length) { │
│ const label = g.edges[i].label; │
│ rows.push(" ".repeat(spine) + G.V + (l │
│ abel ? ` ${label}` : "")); │
│ rows.push(" ".repeat(spine) + "↓"); │
│ } │
│ }); │
│ return rows.join("\n"); │
│ } │
│ │
│ /** Renders g if its edges form one linear c │
│ hain; null otherwise. */ │
│ export function renderChain(g: FlowGraph, wi │
│ dth: number, preferHorizontal = true): strin │
│ g | null { │
│ const chain = chainOf(g); │
│ if (!chain) return null; │
│ │
│ // Edges may be authored in any order; rea │
│ lign them with the chain. │
│ const byFrom = new Map(g.edges.map((e) => │
│ [e.from, e])); │
│ const ordered: FlowGraph = { │
│ labels: g.labels, │
│ edges: chain.slice(0, -1).map((id) => by │
│ From.get(id)!), │
│ }; │
│ if (!preferHorizontal) return vertical(cha │
│ in, ordered); │
│ return horizontal(chain, ordered, width) ? │
│ ? vertical(chain, ordered); │
│ } │
└──────────────────────────────────────────────┘