builddocs
Version:
Generate HTML documentation from TypeScript types and doc comments
317 lines (316 loc) • 10.8 kB
JavaScript
const HTMLspecial = { "<": "<", "&": "&", "\"": """ };
function esc(text) {
return String(text).replace(/[<&\"]/g, ch => HTMLspecial[ch]);
}
const noAttrs = {};
class Text {
text;
attrs;
constructor(text, attrs = noAttrs) {
this.text = text;
this.attrs = attrs;
}
get length() { return this.text.length; }
}
class Seq {
parts;
length;
constructor(parts) {
this.parts = parts;
this.length = parts.reduce((l, p) => l + p.length, 0);
}
}
class Break {
parts;
length;
constructor(parts) {
this.parts = parts;
this.length = parts.reduce((l, p) => l + p.length, 0);
}
}
class Delim {
open;
close;
inner;
length;
mustBreak;
constructor(open, close, inner, mustBreak) {
this.open = open;
this.close = close;
this.inner = inner;
this.length = open.length + close.length + inner.reduce((l, p) => l + p.length, 0);
this.mustBreak = mustBreak;
}
}
class Ref {
content;
constructor(content) { this.content = content; }
get length() { return 0; }
}
export function seq(...content) { return new Seq(content); }
export function seqA(content) { return new Seq(content); }
export function maybeBreak(content) { return new Break(content); }
export function delim(open, inner, close, mustBreak = false) {
return new Delim(open, close, inner, mustBreak);
}
export function text(text, attrs) { return new Text(text, attrs); }
export function ref(value) { return new Ref(value); }
export const space = new Text(" ");
export const nbsp = new Text(" ");
export function sep(parts, separator = ",") {
let sep = separator == "," ? seq(text(separator), space) : seq(nbsp, text(separator), space);
return parts.map((p, i) => i == parts.length - 1 ? p : seq(p, sep));
}
// Formatting System
function formatAttrs(attrs) {
let result = "";
for (let attr in attrs) {
let value = attrs[attr];
if (value != null)
result += ` ${attr}="${esc(value)}"`;
}
return result;
}
class Line {
text;
textLength;
addIndent;
extraIndent;
// Resolved by resolveLayout
length = 0;
indent = 0;
space;
constructor(text, addIndent, textLength, extraIndent = 0, space = false) {
this.text = text;
this.textLength = textLength ?? this.text.reduce((l, t) => l + t.length, 0);
this.addIndent = addIndent;
this.extraIndent = extraIndent;
this.space = space;
}
render() {
let code = " ".repeat(this.indent);
let upto = this.text.length;
while (upto && this.text[upto - 1] == space)
upto--;
for (let i = 0; i < upto; i++) {
let text = this.text[i], attrs = formatAttrs(text.attrs), html = esc(text.text);
if (attrs) {
let tag = text.attrs.href ? "a" : "span";
code += `<${tag}${attrs}>${html}</${tag}>`;
}
else {
code += html;
}
}
return code;
}
}
function resolveLayout(layout) {
let base = 0;
for (let line of layout) {
base += line.addIndent;
line.indent = base + line.extraIndent;
if (line.indent < 0)
throw new Error("bad layout " + layout.map(l => l.addIndent));
line.length = line.indent + line.textLength;
}
return layout;
}
function findPos(layout, pos) {
for (let i = 0; i < layout.length; i++) {
let line = layout[i];
if (pos <= line.text.length)
return { line: i, off: pos };
pos -= line.text.length;
}
throw new Error("Position out of range: " + pos);
}
class LayoutDelim {
breakPoints;
score;
mustBreak;
constructor(breakPoints, mustBreak, score) {
this.breakPoints = breakPoints;
this.score = score;
this.mustBreak = mustBreak;
}
}
class LayoutBreakable {
breakPoints;
constructor(breakPoints) {
this.breakPoints = breakPoints;
}
get score() { return 0; }
}
function breakBy(layout, brk, width, indentUnit, must) {
let result = [], points = brk.breakPoints.map(p => findPos(layout, p));
if (!must && points.every(p => layout[p.line].length <= width))
return layout;
let j = 0, jOff = 0, isDelim = brk instanceof LayoutDelim, addIndent = 0, extraIndent = 0;
for (let i = 0;; i++) {
let last = i == points.length;
let { line, off } = last ? { line: layout.length, off: 0 } : points[i];
while (j < line) {
let from = layout[j++];
if (jOff || addIndent || extraIndent)
result.push(new Line(jOff ? from.text.slice(jOff) : from.text, addIndent + (jOff ? 0 : from.addIndent), jOff ? undefined : from.textLength, Math.max(from.extraIndent, extraIndent), from.space));
else
result.push(from);
jOff = addIndent = extraIndent = 0;
}
if (last)
break;
if (jOff < off) {
let from = layout[line];
result.push(new Line(from.text.slice(jOff, off), addIndent + (jOff ? 0 : from.addIndent), undefined, Math.max(from.extraIndent, extraIndent)));
jOff = off;
}
if (must)
result[result.length - 1].space = true;
addIndent = !isDelim || points.length == 1 ? 0 : i == 0 ? indentUnit : i == points.length - 1 ? -indentUnit : 0;
if (!isDelim)
extraIndent = indentUnit;
}
return resolveLayout(result);
}
function breakOnSpaces(line, width, indentUnit) {
let result = [], used = 0;
for (let lastSpace = -1, lastSpaceCol = -1, col = line.indent, j = 0; j < line.text.length; j++) {
let token = line.text[j];
col += token.length;
if (token == space) {
lastSpace = j + 1;
lastSpaceCol = col;
}
if (col > width && lastSpace > -1 && lastSpaceCol > (width / 5)) {
result.push(new Line(line.text.slice(used, lastSpace), used ? 0 : line.addIndent, undefined, used ? indentUnit : line.extraIndent));
used = lastSpace;
lastSpace = lastSpaceCol = -1;
col = line.indent + indentUnit;
}
}
if (used < line.text.length)
result.push(new Line(line.text.slice(used), used ? 0 : line.addIndent, undefined, used ? indentUnit : line.extraIndent, line.space));
return result;
}
function maybeUndo(layout, brk, width) {
let points = brk.breakPoints.map(p => findPos(layout, p));
let curLine = -1, curLen = 0;
// Skip out if points aren't at line end or joining would create a too-long line
for (let p of points) {
let before = layout[p.line], after = layout[p.line + 1];
if (p.off < before.textLength || !after)
return layout;
if (p.line == curLine)
curLen += after.textLength;
else
curLen = before.indent + before.textLength + after.textLength;
curLine = p.line + 1;
if (curLen > width)
return layout;
}
let result = [], pointI = 0;
for (let i = 0; i < layout.length; i++) {
let line = layout[i];
while (pointI < points.length && points[pointI].line == i) {
let next = layout[++i];
line = new Line(line.text.concat(next.text), line.addIndent, line.textLength + next.textLength, line.extraIndent, line.space || next.space);
}
result.push(line);
}
return resolveLayout(result);
}
function scoreDelim(delim, width) {
if (delim.mustBreak)
return 1e9;
let score = 0, innerLen = delim.inner.reduce((l, c) => l + c.length, 0);
if (delim.inner.length == 0) {
return -1e9;
}
else if (delim.inner.length == 1) {
if (delim.inner[0].length < 16)
return -1e9;
score -= 10;
}
else if (innerLen < 20) {
score -= 20 - innerLen;
}
else {
let biggest = delim.inner.reduce((a, b) => a.length > b.length ? a : b);
let average = (innerLen - delim.open.length - delim.close.length) / delim.inner.length;
let ratio = biggest.length / average;
if (ratio < 3)
score += (3 - ratio) * 5;
if (innerLen > width)
score += (innerLen / width) * 3;
}
return score;
}
export function format(content, width, indentUnit = 2) {
let text = [];
let breakable = [];
let refs = [];
let scan = (content) => {
if (content instanceof Text) {
text.push(content);
}
else if (content instanceof Seq) {
content.parts.forEach(scan);
}
else if (content instanceof Break) {
let breakPoints = [];
for (let i = 0; i < content.parts.length; i++) {
scan(content.parts[i]);
if (i < content.parts.length - 1)
breakPoints.push(text.length);
}
breakable.push(new LayoutBreakable(breakPoints));
}
else if (content instanceof Delim) {
scan(content.open);
let breakPoints = [text.length];
for (let inner of content.inner) {
scan(inner);
breakPoints.push(text.length);
}
scan(content.close);
let score = scoreDelim(content, width);
if (score > -1e9)
breakable.push(new LayoutDelim(breakPoints, content.mustBreak, score));
}
else if (content instanceof Ref) {
refs.push({ ref: content.content, line: text.length });
}
};
scan(content);
let layout = resolveLayout([new Line(text, 0)]);
breakable.sort((a, b) => b.score - a.score);
for (let brk of breakable) {
let mustBreak = brk instanceof LayoutDelim && brk.mustBreak;
if (!mustBreak && layout.every(line => line.length <= width))
break;
layout = breakBy(layout, brk, width, indentUnit, mustBreak);
}
for (let i = breakable.length - 1; i >= 0; i--) {
let brk = breakable[i];
if (brk.score > 0)
break;
layout = maybeUndo(layout, brk, width);
}
let didBreak = false;
for (let i = layout.length - 1; i >= 0; i--) {
let line = layout[i];
if (line.length > width) {
let broken = breakOnSpaces(line, width, indentUnit);
if (broken.length > 1) {
layout.splice(i, 1, ...broken);
didBreak = true;
}
}
}
if (didBreak)
layout = resolveLayout(layout);
for (let ref of refs)
ref.line = findPos(layout, ref.line).line;
return { layout, refs };
}