preserves/implementations/javascript/packages/schema/src/compiler/block.ts

178 lines
4.7 KiB
TypeScript

export type Item = Emittable | string;
export const DEFAULT_WIDTH = 80;
export class Formatter {
width = DEFAULT_WIDTH;
indentDelta = ' ';
currentIndent = '\n';
buffer: Array<string> = [];
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 function formatItems(i: Item[], width = DEFAULT_WIDTH): string {
const f = new Formatter();
f.width = width;
i.forEach(i => f.write(i));
return f.toString();
}
export interface Emittable {
writeOn(f: Formatter): void;
}
export class Sequence implements Emittable {
items: Array<Item>;
constructor(items: Array<Item>) {
if (items.some(i => i === void 0)) throw new Error('aiee');
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<Item>) {
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[]) => {
if (items.length === 1 && items[0] instanceof Block) {
return items[0];
} else {
return new Block(items);
}
}
export const fnblock = (... items: Item[]) => seq('((() => ', block(... items), ')())');
export const keyvalue = (k: string, v: Item) => seq(JSON.stringify(k), ': ', v);