Skip to content
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ function createDAG<T extends Identifiable>(): DAGraph<T> {
return new DAGraph<T>();
}

export * from './lib/formatVisitors';
export type { DAGraph, Identifiable, DAGVisitor, TraversalState };
export default createDAG;
export { createDAG };
47 changes: 47 additions & 0 deletions lib/formatVisitors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Identifiable, DAGVisitor } from '../index';

/**
* Creates a visitor that accumulates a string representation of the graph structure using indentation.
*
* @param labelFn optional function to generate a label for each node. Defaults to node.id.
* @param indent optional string to use for indentation. Defaults to 2 spaces.
* @returns a DAGVisitor that pushes lines to the context array.
*/
export function createIndentFormatter<T extends Identifiable>(
labelFn: (n: T) => string = n => n.id,
indent = ' '
): DAGVisitor<T, string[]> {
return (node, { depth }, lines) => {
lines.push(`${indent.repeat(depth)}${labelFn(node)}`);
};
}

/**
* Creates a visitor that accumulates a tree-like string representation of the graph structure
* using unicode box-drawing characters (├──, └──, │).
*
* @param labelFn optional function to generate a label for each node. Defaults to node.id.
* @returns a DAGVisitor that pushes lines to the context array.
*/
export function createTreeAsciiFormatter<T extends Identifiable>(
labelFn: (n: T) => string = n => n.id
): DAGVisitor<T, string[]> {
const isLastChild: boolean[] = [];

return (node, { depth, index, total }, lines) => {
const isLast = index === total - 1;
isLastChild[depth] = isLast;

let prefix = '';
if (depth > 0) {
for (let i = 1; i < depth; i++) {
prefix += isLastChild[i] ? ' ' : '│ ';
}

const connector = isLast ? '└── ' : '├── ';
lines.push(`${prefix}${connector}${labelFn(node)}`);
} else {
lines.push(labelFn(node));
}
};
}
102 changes: 102 additions & 0 deletions test/visitors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import createDAG, { createIndentFormatter, createTreeAsciiFormatter } from '..';

describe('visitors', () => {
describe('createPrintVisitor', () => {
test('should print graph with default indentation', () => {
const dag = createDAG();
const a = { id: 'A' };
const b = { id: 'B' };
const c = { id: 'C' };

// A -> B -> C
dag.addEdge(a, b);
dag.addEdge(b, c);

const lines: string[] = [];
dag.traverse(createIndentFormatter(), lines);

expect(lines).toEqual(['A', ' B', ' C']);
});

test('should support custom indentation', () => {
const dag = createDAG();
const a = { id: 'A' };
const b = { id: 'B' };

dag.addEdge(a, b);

const lines: string[] = [];
dag.traverse(
createIndentFormatter(n => n.id, '----'),
lines
);

expect(lines).toEqual(['A', '----B']);
});

test('should support custom label function', () => {
const dag = createDAG<{ id: string; val: number }>();
const a = { id: 'A', val: 1 };
const b = { id: 'B', val: 2 };

dag.addEdge(a, b);

const lines: string[] = [];
dag.traverse(
createIndentFormatter(n => `Value: ${n.val}`),
lines
);

expect(lines).toEqual(['Value: 1', ' Value: 2']);
});
});

describe('createTreeVisitor', () => {
test('should print graph with tree structure', () => {
const dag = createDAG();
const a = { id: 'A' };
const b = { id: 'B' };
const c = { id: 'C' };
const d = { id: 'D' };
const e = { id: 'E' };

// A -> B -> C
// A -> D -> E
dag.addEdge(a, b);
dag.addEdge(b, c);
dag.addEdge(a, d);
dag.addEdge(d, e);

// Roots: A
// Children of A: B, D (in that order because B added first)

const lines: string[] = [];
dag.traverse(createTreeAsciiFormatter(), lines);

const expected = ['A', '├── B', '│ └── C', '└── D', ' └── E'];

expect(lines).toEqual(expected);
});

test('should handle multiple roots', () => {
const dag = createDAG();
const a = { id: 'A' };
const b = { id: 'B' };
const c = { id: 'C' };

dag.addNode(a);
dag.addNode(b);
dag.addEdge(b, c);

// A
// B -> C

const lines: string[] = [];
dag.traverse(createTreeAsciiFormatter(), lines);

const expected = ['A', 'B', '└── C'];

expect(lines).toEqual(expected);
});
});
});