export type Item = Emittable | string; export class Formatter { width = 80; indentDelta = ' '; currentIndent = '\n'; buffer: Array = []; get indentSize(): number { return this.indentDelta.length; } set indentSize(n: number) { this.indentDelta = new Array(n + 1).join(' '); } write(i: Item) { if (typeof i === 'string') { this.buffer.push(i); } else { i.writeOn(this); } } newline() { this.write(this.currentIndent); } toString(): string { return this.buffer.join(''); } withIndent(f: () => void): void { const oldIndent = this.currentIndent; try { this.currentIndent = this.currentIndent + this.indentDelta; f(); } finally { this.currentIndent = oldIndent; } } clone(): Formatter { const f = Object.assign(new Formatter(), this); f.buffer = []; return f; } } export abstract class Emittable { abstract writeOn(f: Formatter): void; } export class Sequence extends Emittable { items: Array; constructor(items: Array) { super(); this.items = items; } get separator(): string { return ''; } get terminator(): string { return ''; } writeOn(f: Formatter): void { let needSeparator = false; this.items.forEach(i => { if (needSeparator) { f.write(this.separator); } else { needSeparator = true; } f.write(i); }); f.write(this.terminator); } } export class CommaSequence extends Sequence { get separator(): string { return ', '; } } export abstract class Grouping extends CommaSequence { abstract get open(): string; abstract get close(): string; writeHorizontally(f: Formatter): void { f.write(this.open); super.writeOn(f); f.write(this.close); } writeVertically(f: Formatter): void { f.write(this.open); if (this.items.length > 0) { f.withIndent(() => { this.items.forEach((i, index) => { f.newline(); f.write(i); const delim = index === this.items.length - 1 ? this.terminator : this.separator; f.write(delim.trimRight()); }); }); f.newline(); } f.write(this.close); } writeOn(f: Formatter): void { const g = f.clone(); this.writeHorizontally(g); const s = g.toString(); if (s.length <= f.width) { f.write(s); } else { this.writeVertically(f); } } } export class Parens extends Grouping { get open(): string { return '('; } get close(): string { return ')'; } } export class OperatorSequence extends Parens { operator: string; constructor(operator: string, items: Array) { super(items); this.operator = operator; } get separator(): string { return this.operator; } } export class Brackets extends Grouping { get open(): string { return '['; } get close(): string { return ']'; } } export class AngleBrackets extends Grouping { get open(): string { return '<'; } get close(): string { return '>'; } } export class Braces extends Grouping { get open(): string { return '{'; } get close(): string { return '}'; } } export class Block extends Braces { get separator(): string { return '; ' } get terminator(): string { return ';' } } export const seq = (... items: Item[]) => new Sequence(items); export const commas = (... items: Item[]) => new CommaSequence(items); export const parens = (... items: Item[]) => new Parens(items); export const opseq = (zero: string, op: string, ... items: Item[]) => (items.length === 0) ? zero : new OperatorSequence(op, items); export const brackets = (... items: Item[]) => new Brackets(items); export const anglebrackets = (... items: Item[]) => new AngleBrackets(items); export const braces = (... items: Item[]) => new Braces(items); export const block = (... items: Item[]) => new Block(items);