UNPKG

dropflow

Version:

A small CSS2 document renderer built from specifications

97 lines (96 loc) 3.51 kB
export * from './api.js'; import { Parser, isWhitespace } from './parse-html.js'; import { TextNode, HTMLElement } from './dom.js'; import { parse as StyleParser } from './parse-css.js'; import { EMPTY_STYLE, computeElementStyle, createDeclaredStyle } from './style.js'; import { id } from './util.js'; export default function parse(str) { const parents = []; let rootElement = null; // afterHtml is like "after after body"; inHtml is like "in body" // https://html.spec.whatwg.org/multipage/parsing.html let insertionMode = 'beforeHtml'; function addText(text) { const parent = (parents.at(-1) || rootElement); let lastChild = parent.children.at(-1); if (lastChild && lastChild instanceof TextNode) { lastChild.text += text; } else { lastChild = new TextNode(id(), text, parent); parent.children.push(lastChild); lastChild.parent = parent; computeElementStyle(lastChild); } } function parentChild(child) { const parent = (parents.at(-1) || rootElement); parent.children.push(child); child.parent = parent; computeElementStyle(child); } function forceRoot(child) { rootElement = new HTMLElement('root', 'html'); parents.push(rootElement); computeElementStyle(rootElement); if (child) parentChild(child); } const parser = new Parser({ onopentag(tagName, attrs) { let parent = parents.at(-1); let declaredStyle = EMPTY_STYLE; // Just ignore invalid styles so the parser can continue if (attrs.style) { try { declaredStyle = createDeclaredStyle(StyleParser(attrs.style)); } catch { } } const element = new HTMLElement(id(), tagName, parent, attrs, declaredStyle); if (insertionMode === 'beforeHtml') { if (tagName === 'html') { rootElement = element; } else { forceRoot(element); } insertionMode = 'inHtml'; } else { // insertionMode === 'inHtml' is ok. 'afterHtml' is a parse error. // Chrome appends to the body, and that's the easiest thing to do. parentChild(element); } computeElementStyle(element); parents.push(element); }, onclosetag(tagName) { if (tagName === 'html') insertionMode = 'afterHtml'; parents.pop(); }, ontext(text) { if (insertionMode === 'inHtml' || insertionMode === 'afterHtml') { addText(text); } else if (insertionMode === 'beforeHtml') { let startInk = 0; while (startInk < text.length) { const code = text.charCodeAt(startInk); if (!isWhitespace(code)) break; startInk += 1; } if (startInk < text.length) { forceRoot(); addText(text.slice(startInk)); insertionMode = 'inHtml'; } } } }); parser.write(str); parser.end(); return rootElement || new HTMLElement('root', 'html'); }