Skip to content

Instantly share code, notes, and snippets.

@Gozala
Last active June 9, 2017 17:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gozala/3841ea4920113fdca31ecbf5a4e5b803 to your computer and use it in GitHub Desktop.
Save Gozala/3841ea4920113fdca31ecbf5a4e5b803 to your computer and use it in GitHub Desktop.
A prettier printer
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