A prettier printer implementation in TS based on Philip Wadler paper.
Last active
June 9, 2017 17:41
-
-
Save Gozala/3841ea4920113fdca31ecbf5a4e5b803 to your computer and use it in GitHub Desktop.
A prettier printer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface Flatten { | |
flatten(): Doc | |
} | |
export interface Fits { | |
fits(wihth: number): boolean | |
} | |
export type Options = Array<[number, Doc]> | |
export interface Best { | |
best(position: number, width: number, offset: number, rest: Options): Document | |
} | |
export interface Layout { | |
layout(): string | |
} | |
export interface Pretty { | |
toString(width?: number): string | |
} | |
export type Text = string & Pretty & { tag: String } | |
const isText = (text: any): text is Text => typeof text === "string" | |
export class Nil implements Flatten, Best, Fits, Layout, Pretty { | |
type: "Nil" = "Nil" | |
flatten(): Doc { | |
return this | |
} | |
best(_: number, width: number, offset: number, options: Options): Document { | |
return best(width, offset, options) | |
} | |
fits(width: number): boolean { | |
return width >= 0 | |
} | |
layout(): string { | |
return "" | |
} | |
toString(_width?: number) { | |
return "" | |
} | |
} | |
export class Concat implements Flatten, Best, Pretty { | |
type: "<>" = "<>" | |
constructor(public left: Doc, public right: Doc) {} | |
flatten(): Doc { | |
const left = flatten(this.left) | |
const right = flatten(this.right) | |
if (this.left === left && this.right === right) { | |
return this | |
} else { | |
return new Concat(left, right) | |
} | |
} | |
best(i: number, width: number, offset: number, options: Options): Document { | |
return best(width, offset, [[i, this.left], [i, this.right], ...options]) | |
} | |
toString(width?: number) { | |
return pretty(width || 80, this) | |
} | |
} | |
export class Nest implements Flatten, Best { | |
type: "nest" = "nest" | |
constructor(public n: number, public content: Doc) {} | |
flatten(): Doc { | |
const content = flatten(this.content) | |
if (content === this.content) { | |
return this | |
} else { | |
return new Nest(this.n, content) | |
} | |
} | |
best(i: number, width: number, offset: number, options: Options): Document { | |
return best(width, offset, [[i + this.n, this.content], ...options]) | |
} | |
toString(width?: number) { | |
return pretty(width || 80, this) | |
} | |
} | |
export class Line { | |
type: "line" = "line" | |
flatten(): Doc { | |
return space | |
} | |
best(i: number, width: number, _offset: number, options: Options): Document { | |
return new LineDocument(i, best(width, i, options)) | |
} | |
toString(width?: number): string { | |
return pretty(width || 80, this) | |
} | |
} | |
export class Union implements Flatten, Best { | |
type: "<|>" = "<|>" | |
constructor(public left: Doc, public right: Doc) {} | |
flatten(): Doc { | |
return flatten(this.left) | |
} | |
best(i: number, width: number, offset: number, options: Options): Document { | |
const left = best(width, offset, [[i, this.left], ...options]) | |
if (left.fits(width - offset)) { | |
return left | |
} else { | |
return best(width, offset, [[i, this.right], ...options]) | |
} | |
} | |
toString(width?: number): string { | |
return pretty(width || 80, this) | |
} | |
} | |
export type Doc = Nil | Text | Concat | Nest | Line | Union | |
export class TextDocument implements Fits, Layout { | |
type: "Text" = "Text" | |
constructor(public text: string, public content: Document) {} | |
fits(width: number): boolean { | |
return width >= 0 && this.content.fits(width - this.text.length) | |
} | |
layout(): string { | |
return `${this.text}${this.content.layout()}` | |
} | |
} | |
export class LineDocument implements Fits, Layout { | |
type: "Line" = "Line" | |
constructor(public n: number, public content: Document) {} | |
fits(width: number): boolean { | |
return width >= 0 | |
} | |
layout(): string { | |
return `\n${" ".repeat(this.n)}${this.content.layout()}` | |
} | |
} | |
export type Document = Nil | TextDocument | LineDocument | |
export const nil = new Nil() | |
export const line = new Line() | |
export const space = <Text>" " | |
export const concat = (...contents: Array<Doc>): Doc => { | |
switch (contents.length) { | |
case 0: | |
return nil | |
case 1: | |
return contents[0] | |
default: | |
const last = contents.pop() | |
return contents.reduceRight( | |
(right, left) => new Concat(left, right), | |
last | |
) | |
} | |
} | |
export const nest = (n: number, doc: Doc): Doc => new Nest(n, doc) | |
export const text = (content: string): Doc => <Text>content | |
export const group = (doc: Doc): Doc => new Union(flatten(doc), doc) | |
const flatten = (content: Doc): Doc => { | |
if (isText(content)) { | |
return content | |
} else { | |
return content.flatten() | |
} | |
} | |
const best = (width: number, offset: number, options: Options): Document => { | |
if (options.length === 0) { | |
return nil | |
} else { | |
const [[n, document], ...rest] = options | |
if (isText(document)) { | |
const text = document | |
return new TextDocument(text, best(width, offset + text.length, rest)) | |
} else { | |
return document.best(n, width, offset, rest) | |
} | |
} | |
} | |
export const pretty = (width: number, content: Doc): string => | |
best(width, 0, [[0, content]]).layout() | |
export const joinWithSpace = (left: Doc, right: Doc): Doc => | |
concat(left, space, right) | |
export const joinWithLine = (left: Doc, right: Doc): Doc => | |
concat(left, line, right) | |
type Foldr<state, input> = (model: state, item: input) => state | |
export const fold = (f: Foldr<Doc, Doc>, contents: Array<Doc>): Doc => { | |
switch (contents.length) { | |
case 0: | |
return nil | |
case 1: | |
return contents[0] | |
default: { | |
const [head, ...tail] = contents | |
return tail.reduce(f, head) | |
} | |
} | |
} | |
export const spread = (...contents: Array<Doc>): Doc => | |
fold(joinWithSpace, contents) | |
export const stack = (...contents: Array<Doc>): Doc => | |
fold(joinWithLine, contents) | |
export const bracket = (open: string, content: Doc, close: string): Doc => | |
group(concat(text(open), nest(2, concat(line, content)), line, text(close))) | |
export const joinFill = (left: Doc, right: Doc): Doc => | |
concat(left, group(line), right) | |
export const fillwords = (text: string): Doc => | |
fold(joinFill, <Array<Text>>text.split(/\s+/)) | |
export const fill = (...contents: Array<Doc>): Doc => { | |
switch (contents.length) { | |
case 0: | |
return nil | |
case 1: | |
return contents[0] | |
default: { | |
const [first, second, ...rest] = contents | |
const left = flatten(joinWithSpace(first, fill(flatten(second), ...rest))) | |
const right = joinWithLine(first, fill(second, ...rest)) | |
return new Union(left, right) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment