UNPKG

dropflow

Version:

A small CSS2 document renderer built from specifications

203 lines (202 loc) 6.82 kB
import '#register-default-environment'; import { HTMLElement, TextNode } from './dom.js'; import { getOriginStyle, computeElementStyle } from './style.js'; import { fonts, FontFace, loadFonts, loadFontsSync, createFaceFromTables, createFaceFromTablesSync } from './text-font.js'; import { generateBlockContainer, layoutBlockBox, BlockFormattingContext } from './layout-flow.js'; import HtmlPaintBackend from './paint-html.js'; import SvgPaintBackend from './paint-svg.js'; import CanvasPaintBackend from './paint-canvas.js'; import paint from './paint.js'; import { BoxArea, prelayout, postlayout } from './layout-box.js'; import { id } from './util.js'; export { environment } from './environment.js'; export { createDeclaredStyle as style, setOriginStyle } from './style.js'; export { fonts, FontFace, createFaceFromTables, createFaceFromTablesSync }; export function generate(rootElement) { if (rootElement.style === getOriginStyle()) { throw new Error('To use the hyperscript API, pass the element tree to dom() and use ' + 'the return value as the argument to generate().'); } return generateBlockContainer(rootElement); } export function layout(root, width = 640, height = 480) { const initialContainingBlock = new BoxArea(root, 0, 0, width, height); root.containingBlock = initialContainingBlock; prelayout(root); layoutBlockBox(root, { bfc: new BlockFormattingContext(0), lastBlockContainerArea: initialContainingBlock, lastPositionedArea: initialContainingBlock, mode: 'normal' }); postlayout(root); } /** * Old paint target for testing, not maintained much anymore */ export function paintToHtml(root) { const backend = new HtmlPaintBackend(); paint(root, backend); return backend.s; } export function paintToSvg(root) { const backend = new SvgPaintBackend(); const { width, height } = root.containingBlock; let cssFonts = ''; paint(root, backend); for (const [src, face] of backend.usedFonts) { cssFonts += `@font-face { font-family: "${face.uniqueFamily}"; src: url("${src}") format("opentype"); }\n`; } return ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"> <style type="text/css"> ${cssFonts} </style> ${backend.body()} </svg> `.trim(); } export function paintToSvgElements(root) { const backend = new SvgPaintBackend(); paint(root, backend); return backend.main; } export { eachRegisteredFont } from './text-font.js'; export function paintToCanvas(root, ctx) { const backend = new CanvasPaintBackend(ctx); paint(root, backend); } export async function renderToCanvasContext(rootElement, ctx, width, height) { await load(rootElement); const root = generate(rootElement); layout(root, width, height); paintToCanvas(root, ctx); } export async function renderToCanvas(rootElement, canvas) { const ctx = canvas.getContext('2d'); await renderToCanvasContext(rootElement, ctx, canvas.width, canvas.height); } function toDomChild(child) { if (typeof child === 'string') { return new TextNode(id(), child); } else { return child; } } export function dom(el) { let rootElement; if (el instanceof HTMLElement && el.tagName === 'html') { rootElement = el; if (rootElement.children.length === 1) { const [child] = rootElement.children; if (child instanceof TextNode) { // fast path: saves something like 0.4µs, so no need to keep... child.parent = rootElement; computeElementStyle(rootElement); computeElementStyle(child); return rootElement; } } } else { rootElement = new HTMLElement('root', 'html'); rootElement.children = Array.isArray(el) ? el.map(toDomChild) : [toDomChild(el)]; } // Assign parents const stack = [rootElement]; const parents = []; while (stack.length) { const el = stack.pop(); const parent = parents.at(-1); if ('sentinel' in el) { parents.pop(); } else { el.parent = parent || null; computeElementStyle(el); if (el instanceof HTMLElement) { parents.push(el); stack.push({ sentinel: true }); for (const child of el.children) stack.push(child); } } } return rootElement; } export function h(tagName, arg2, arg3) { let data; let children; if (typeof arg2 === 'string') { children = [new TextNode(id(), arg2)]; } else if (Array.isArray(arg2)) { children = arg2.map(toDomChild); } else { data = arg2; } if (Array.isArray(arg3)) { children = arg3.map(toDomChild); } else if (typeof arg3 === 'string') { children = [new TextNode(id(), arg3)]; } if (!children) children = []; if (!data) data = {}; const el = new HTMLElement(id(), tagName, null, data.attrs, data.style); el.children = children; return el; } export function t(text) { return new TextNode(id(), text); } export function staticLayoutContribution(box) { let intrinsicSize = 0; const definiteSize = box.getDefiniteInlineSize(); if (definiteSize !== undefined) return definiteSize; if (box.isBlockContainerOfInlines()) { const [ifc] = box.children; for (const line of ifc.paragraph.lineboxes) { intrinsicSize = Math.max(intrinsicSize, line.width); } // TODO: floats } else if (box.isBlockContainerOfBlockContainers()) { for (const child of box.children) { intrinsicSize = Math.max(intrinsicSize, staticLayoutContribution(child)); } } else { throw new Error(`Unknown box type: ${box.id}`); } const marginLineLeft = box.style.getMarginLineLeft(box); const marginLineRight = box.style.getMarginLineRight(box); const borderLineLeftWidth = box.style.getBorderLineLeftWidth(box); const paddingLineLeft = box.style.getPaddingLineLeft(box); const paddingLineRight = box.style.getPaddingLineRight(box); const borderLineRightWidth = box.style.getBorderLineRightWidth(box); intrinsicSize += (marginLineLeft === 'auto' ? 0 : marginLineLeft) + borderLineLeftWidth + paddingLineLeft + paddingLineRight + borderLineRightWidth + (marginLineRight === 'auto' ? 0 : marginLineRight); return intrinsicSize; } export async function load(root) { await loadFonts(root); // TODO: images too } export async function loadSync(root) { await loadFontsSync(root); // TODO: images too }