Published
Edited
Jul 12, 2021
Importers
9 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Doc = ({
// An empty document
nil: {type: 'nil'},

// A text document (optional color for syntax highlighting)
text: (text, color='black') => ({type: 'text', text, color}),

// A newline separator that flattens to a space by default
line: (reducedText=' ') => ({type: 'line', reducedText}),

// An indented document
nest: (indent, doc) => ({type: 'nest', indent, doc}),
// The concatenation, or joining, of multiple documents
concat: (docs) => ({type: 'concat', docs}),

// A union of documents that favors `a` if there's room
union: (a, b) => ({type: 'union', a, b}),
})
Insert cell
Insert cell
Insert cell
Insert cell
display(
Doc.concat([
Doc.text("Hello"),
Doc.line(),
Doc.text("world"),
]),
)
Insert cell
Insert cell
display(
Doc.concat([
Doc.text('['),
Doc.nest(2, Doc.concat([
Doc.line(),
Doc.text('arrayElement,'),
])),
Doc.line(),
Doc.text(']'),
])
)
Insert cell
Insert cell
Insert cell
Insert cell
// Take in a function and wrap it so that the function is cached
// on first invocation and returns results instantly thereafter.
function lazy(closure) {
let cached = null;
return () => cached == null ? (cached = closure()) : cached;
}
Insert cell
function layout(type, text=null, next=null, color='black') {
// Type can be "text", "line", or "nil"
return {type, text, color, next};
}
Insert cell
function fits(remainingWidth, layoutDoc) {
if (remainingWidth < 0) return false;
if (layoutDoc.type == 'text') {
return fits(remainingWidth - layoutDoc.text.length, layoutDoc.next());
}
// If the layout is of type `line` or `nil` it always fits.
return true;
}
Insert cell
Insert cell
function best(maxLineWidth, lineWidth, documentPairs) {
if (documentPairs.length == 0) return layout('null'); // end of layout chain
// Grab current indentation level and document from head of list
const [indent, doc] = documentPairs[0];
const nextDocs = documentPairs.slice(1);
// Convenience function to recurse
const _best = (newDocs=[], lw=lineWidth) => {
return best(maxLineWidth, lw, newDocs.concat(nextDocs));
};
if (doc.type == 'nil') return _best();
if (doc.type == 'concat') return _best(doc.docs.map(doc => [indent, doc]));
if (doc.type == 'nest') return _best([[indent + doc.indent, doc.doc]]);
if (doc.type == 'union') {
// See if first doc in union fits; if not, use second
const doc1 = _best([[indent, doc.a()]]);
if (fits(maxLineWidth - lineWidth, doc1)) return doc1;
return _best([[indent, doc.b()]]);
}
// Text and lines output layout chains
if (doc.type == 'text') {
const newLineWidth = lineWidth + doc.text.length;
return layout('text', doc.text, lazy(() => _best([], newLineWidth)), doc.color);
}
if (doc.type == 'line') {
return layout('line', `\n${' '.repeat(indent)}`, lazy(() => _best([], indent)));
}
}
Insert cell
Insert cell
function unravelLayout(layoutDoc) {
const results = [];
while (layoutDoc.type != 'null') {
results.push({text: layoutDoc.text, color: layoutDoc.color});
layoutDoc = layoutDoc.next();
}
return results;
}
Insert cell
function render(unraveledLayout) {
const pre = document.createElement('pre');
unraveledLayout.forEach(({color, text}) => {
const span = document.createElement('span');
span.style.color = color;
span.textContent = text;
pre.appendChild(span);
});
return pre;
}
Insert cell
Insert cell
function pretty(lineWidth, doc) {
return unravelLayout(best(lineWidth, 0, [[0, doc]]));
}
Insert cell
Insert cell
hiThere = Doc.union(() => Doc.concat([Doc.text("Hi"), Doc.text(' '), Doc.text("there")]),
() => Doc.concat([Doc.text("Hi"), Doc.line(), Doc.text("there")]))
Insert cell
md`Essentially, this union will display the text "Hi" and "there" separated by a space if there's room and a newline if there's not.`
Insert cell
render(pretty(80, hiThere))
Insert cell
render(pretty(5, hiThere))
Insert cell
Insert cell
function flatten(doc) {
// Flatten always picks the first option in a union
while (doc.type == 'union') doc = doc.a();

if (doc.type == 'concat') return Doc.concat(doc.docs.map(flatten));
if (doc.type == 'nest') return Doc.nest(doc.indent, flatten(doc.doc));
// Convert lines to their reduced text
if (doc.type == 'line') return Doc.text(doc.reducedText);

// If doc is text or nil, it's unchanged
return doc;
}
Insert cell
function group(doc) {
// Unions are evaluated lazily
return Doc.union(lazy(() => flatten(doc)), lazy(() => doc));
}
Insert cell
Insert cell
hiThere2 = group(Doc.concat([Doc.text("Hi"), Doc.line(), Doc.text("there")]))
Insert cell
render(pretty(80, hiThere2))
Insert cell
render(pretty(5, hiThere2))
Insert cell
Insert cell
Insert cell
HTML = ({
element: (tag, attrs={}, children=[]) => {
const attributes = Object.entries(attrs);
return {type: 'element', tag, attributes, children};
},
text: (text) => ({type: 'text', text}),
})
Insert cell
Insert cell
// HTML syntax-highlighting colors
Color = ({
tag: 'green',
property: '#ae39e8',
attribute: 'blue',
})
Insert cell
function showHTML(html) {
if (html.type == 'element') {
if (html.children.length == 0) {
return [showTag(html, "/>")];
} else {
return [
Doc.concat([showTag(html, ">"),
showList(showHTML, html.children),
Doc.text(`</${html.tag}>`, Color.tag)])
];
}
} else {
return html.text.split(/\s/).map(word => Doc.text(word));
}
}
Insert cell
function showTag(html, endTag) {
const space = html.attributes.length > 0 ? ' ' : '';
return Doc.concat([
Doc.text(`<${html.tag}${space}`, Color.tag),
showList(showAttribute, html.attributes),
Doc.text(endTag, Color.tag),
]);
}
Insert cell
function showAttribute(attr) {
return [
Doc.concat([
Doc.text(`${attr[0]}=`, Color.property),
Doc.text(JSON.stringify(attr[1]), Color.attribute),
]),
];
}
Insert cell
function showList(fn, l) {
if (l.length == 0) return Doc.nil;
const mapped = l.map(fn).reduce((x, y) => x.concat(y), []);
return group(Doc.concat([
Doc.nest(2, Doc.concat([Doc.line(''), show(mapped)])),
Doc.line(''),
]));
}
Insert cell
function show(l) {
// method called "fill" in Wadler's paper. He describes:
// "[fill] collapses a list of documents into a
// document. It puts a space between two documents when
// this leads to reasonable layout, and a newline
// otherwise.
// ... Note the use of flatten to ensure that a space
// is only inserted between documents which occupy a
// single line."
if (l.length == 0) return Doc.nil;
if (l.length == 1) return l[0];
return Doc.union(
lazy(() => Doc.concat([flatten(l[0]), Doc.text(' '),
show([flatten(l[1]), ...l.slice(2)])])),
lazy(() => Doc.concat([l[0], Doc.line(''), show(l.slice(1))])),
);
}
Insert cell
Insert cell
prettyHtml = (lineWidth, element) => pretty(lineWidth, Doc.concat(showHTML(element)))
Insert cell
Insert cell
testHtml =
// example adapted from Wadler
HTML.element('p', {color: 'blue', font: 'Times', size: '10'}, [
HTML.text('Here is some'),
HTML.element('em', {}, [HTML.text('emphasized')]),
HTML.text('text.'),
HTML.text('Here is a'),
HTML.element('a', {href: 'https://example.com'}, [HTML.text('link')]),
HTML.text('elsewhere.')
])
Insert cell
render(prettyHtml(80, testHtml))
Insert cell
render(prettyHtml(10, testHtml))
Insert cell
Insert cell
function htmlFromDom(element) {
if (element.nodeType == Node.TEXT_NODE) return HTML.text(element.textContent.trim());
return HTML.element(
element.tagName.toLowerCase(),
Object.fromEntries(Array.from(element.attributes).map(a => [a.name, a.value])),
Array.from(element.childNodes).map(htmlFromDom),
);
}
Insert cell
Insert cell
documentNodes = htmlFromDom(doc.parentElement.parentElement)
Insert cell
Insert cell
Insert cell
Insert cell
draw(printWidth, prettyHtml(printWidth, documentNodes))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more