UNPKG

builddocs

Version:

Generate HTML documentation from TypeScript types and doc comments

317 lines (316 loc) 10.8 kB
const HTMLspecial = { "<": "&lt;", "&": "&amp;", "\"": "&quot;" }; 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 }; }