dropflow
Version:
A small CSS2 document renderer built from specifications
203 lines (202 loc) • 6.82 kB
JavaScript
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
}