openpencil/packages/dom-css/src/runtime/headless.ts
2026-06-30 10:47:04 +03:00

68 lines
1.9 KiB
TypeScript

import { parseFragment, type DefaultTreeAdapterTypes } from 'parse5'
import { serializeHTML } from '../serialize'
import { parseStyleAttribute } from '../style-attribute'
import type { CssRuntime, DesignDocument, DesignElement, DesignNode } from '../types'
function attrsToRecord(attrs: DefaultTreeAdapterTypes.Element['attrs']): Record<string, string> {
const result: Record<string, string> = {}
for (const attr of attrs) result[attr.name] = attr.value
return result
}
function isTextNode(
node: DefaultTreeAdapterTypes.ChildNode
): node is DefaultTreeAdapterTypes.TextNode {
return node.nodeName === '#text'
}
function childToDesignNode(node: DefaultTreeAdapterTypes.ChildNode): DesignNode | null {
if (isTextNode(node)) {
return node.value.length > 0 ? { type: 'text', text: node.value } : null
}
if (!('tagName' in node)) return null
const attrs = attrsToRecord(node.attrs)
const element: DesignElement = {
type: 'element',
tagName: node.tagName.toLowerCase(),
attrs,
children: node.childNodes
.map(childToDesignNode)
.filter((child): child is DesignNode => child !== null)
}
const inlineStyle = parseStyleAttribute(attrs.style)
if (inlineStyle) element.inlineStyle = inlineStyle
return element
}
function parseHTML(html: string): DesignDocument {
const fragment = parseFragment(html)
return {
type: 'document',
children: fragment.childNodes
.map(childToDesignNode)
.filter((node): node is DesignNode => node !== null)
}
}
function unavailableError(feature: string): Error {
return new Error(`${feature} requires a headless CSSOM adapter.`)
}
export function createHeadlessCssRuntime(): CssRuntime {
return {
kind: 'headless',
parseHTML,
serializeHTML(document: DesignDocument) {
return serializeHTML(document)
},
computeStyles() {
return Promise.reject(unavailableError('computeStyles'))
}
}
}