/** * @typedef {import('hast').Element} Element * @typedef {import('hast').ElementData} ElementData * @typedef {import('hast').Nodes} Nodes * @typedef {import('hast').Root} Root * @typedef {import('hast').RootContent} RootContent * * @typedef {import('parse5').DefaultTreeAdapterMap} DefaultTreeAdapterMap * @typedef {import('parse5').Token.ElementLocation} P5ElementLocation * @typedef {import('parse5').Token.Location} P5Location * * @typedef {import('property-information').Schema} Schema * * @typedef {import('unist').Point} Point * @typedef {import('unist').Position} Position * * @typedef {import('vfile').VFile} VFile */ /** * @typedef {DefaultTreeAdapterMap['document']} P5Document * @typedef {DefaultTreeAdapterMap['documentFragment']} P5DocumentFragment * @typedef {DefaultTreeAdapterMap['documentType']} P5DocumentType * @typedef {DefaultTreeAdapterMap['commentNode']} P5Comment * @typedef {DefaultTreeAdapterMap['textNode']} P5Text * @typedef {DefaultTreeAdapterMap['element']} P5Element * @typedef {DefaultTreeAdapterMap['node']} P5Node * @typedef {DefaultTreeAdapterMap['template']} P5Template */ /** * @typedef Options * Configuration. * @property {Space | null | undefined} [space='html'] * Which space the document is in (default: `'html'`). * * When an `` element is found in the HTML space, this package already * automatically switches to and from the SVG space when entering and exiting * it. * @property {VFile | null | undefined} [file] * File used to add positional info to nodes (optional). * * If given, the file should represent the original HTML source. * @property {boolean | null | undefined} [verbose=false] * Whether to add extra positional info about starting tags, closing tags, * and attributes to elements (default: `false`). * * > 👉 **Note**: only used when `file` is given. * * @typedef {'html' | 'svg'} Space * Namespace. * * @typedef State * Info passed around about the current state. * @property {VFile | undefined} file * Corresponding file. * @property {boolean} location * Whether location info was found. * @property {Schema} schema * Current schema. * @property {boolean | undefined} verbose * Add extra positional info. */ import {ok as assert} from 'devlop' import {h, s} from 'hastscript' import {find, html, svg} from 'property-information' import {location} from 'vfile-location' import {webNamespaces} from 'web-namespaces' const own = {}.hasOwnProperty /** @type {unknown} */ // type-coverage:ignore-next-line const proto = Object.prototype /** * Transform a `parse5` AST to hast. * * @param {P5Node} tree * `parse5` tree to transform. * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {Nodes} * hast tree. */ export function fromParse5(tree, options) { const settings = options || {} return one( { file: settings.file || undefined, location: false, schema: settings.space === 'svg' ? svg : html, verbose: settings.verbose || false }, tree ) } /** * Transform a node. * * @param {State} state * Info passed around about the current state. * @param {P5Node} node * p5 node. * @returns {Nodes} * hast node. */ function one(state, node) { /** @type {Nodes} */ let result switch (node.nodeName) { case '#comment': { const reference = /** @type {P5Comment} */ (node) result = {type: 'comment', value: reference.data} patch(state, reference, result) return result } case '#document': case '#document-fragment': { const reference = /** @type {P5Document | P5DocumentFragment} */ (node) const quirksMode = 'mode' in reference ? reference.mode === 'quirks' || reference.mode === 'limited-quirks' : false result = { type: 'root', children: all(state, node.childNodes), data: {quirksMode} } if (state.file && state.location) { const doc = String(state.file) const loc = location(doc) const start = loc.toPoint(0) const end = loc.toPoint(doc.length) // Always defined as we give valid input. assert(start, 'expected `start`') assert(end, 'expected `end`') result.position = {start, end} } return result } case '#documentType': { const reference = /** @type {P5DocumentType} */ (node) result = {type: 'doctype'} patch(state, reference, result) return result } case '#text': { const reference = /** @type {P5Text} */ (node) result = {type: 'text', value: reference.value} patch(state, reference, result) return result } // Element. default: { const reference = /** @type {P5Element} */ (node) result = element(state, reference) return result } } } /** * Transform children. * * @param {State} state * Info passed around about the current state. * @param {Array} nodes * Nodes. * @returns {Array} * hast nodes. */ function all(state, nodes) { let index = -1 /** @type {Array} */ const results = [] while (++index < nodes.length) { // Assume no roots in `nodes`. const result = /** @type {RootContent} */ (one(state, nodes[index])) results.push(result) } return results } /** * Transform an element. * * @param {State} state * Info passed around about the current state. * @param {P5Element} node * `parse5` node to transform. * @returns {Element} * hast node. */ function element(state, node) { const schema = state.schema state.schema = node.namespaceURI === webNamespaces.svg ? svg : html // Props. let index = -1 /** @type {Record} */ const props = {} while (++index < node.attrs.length) { const attribute = node.attrs[index] const name = (attribute.prefix ? attribute.prefix + ':' : '') + attribute.name if (!own.call(proto, name)) { props[name] = attribute.value } } // Build. const fn = state.schema.space === 'svg' ? s : h const result = fn(node.tagName, props, all(state, node.childNodes)) patch(state, node, result) // Switch content. if (result.tagName === 'template') { const reference = /** @type {P5Template} */ (node) const pos = reference.sourceCodeLocation const startTag = pos && pos.startTag && position(pos.startTag) const endTag = pos && pos.endTag && position(pos.endTag) // Root in, root out. const content = /** @type {Root} */ (one(state, reference.content)) if (startTag && endTag && state.file) { content.position = {start: startTag.end, end: endTag.start} } result.content = content } state.schema = schema return result } /** * Patch positional info from `from` onto `to`. * * @param {State} state * Info passed around about the current state. * @param {P5Node} from * p5 node. * @param {Nodes} to * hast node. * @returns {undefined} * Nothing. */ function patch(state, from, to) { if ('sourceCodeLocation' in from && from.sourceCodeLocation && state.file) { const position = createLocation(state, to, from.sourceCodeLocation) if (position) { state.location = true to.position = position } } } /** * Create clean positional information. * * @param {State} state * Info passed around about the current state. * @param {Nodes} node * hast node. * @param {P5ElementLocation} location * p5 location info. * @returns {Position | undefined} * Position, or nothing. */ function createLocation(state, node, location) { const result = position(location) if (node.type === 'element') { const tail = node.children[node.children.length - 1] // Bug for unclosed with children. // See: . if ( result && !location.endTag && tail && tail.position && tail.position.end ) { result.end = Object.assign({}, tail.position.end) } if (state.verbose) { /** @type {Record} */ const props = {} /** @type {string} */ let key if (location.attrs) { for (key in location.attrs) { if (own.call(location.attrs, key)) { props[find(state.schema, key).property] = position( location.attrs[key] ) } } } assert(location.startTag, 'a start tag should exist') const opening = position(location.startTag) const closing = location.endTag ? position(location.endTag) : undefined /** @type {ElementData['position']} */ const data = {opening} if (closing) data.closing = closing data.properties = props node.data = {position: data} } } return result } /** * Turn a p5 location into a position. * * @param {P5Location} loc * Location. * @returns {Position | undefined} * Position or nothing. */ function position(loc) { const start = point({ line: loc.startLine, column: loc.startCol, offset: loc.startOffset }) const end = point({ line: loc.endLine, column: loc.endCol, offset: loc.endOffset }) // @ts-expect-error: we do use `undefined` for points if one or the other // exists. return start || end ? {start, end} : undefined } /** * Filter out invalid points. * * @param {Point} point * Point with potentially `undefined` values. * @returns {Point | undefined} * Point or nothing. */ function point(point) { return point.line && point.column ? point : undefined }