diff --git a/lib/index.js b/lib/index.js index bbd7a8b..d33d67b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,20 @@ * @typedef {import('hast').Comment} HastComment * @typedef {HastParent['children'][number]} HastChild * @typedef {HastChild|HastRoot} HastNode + * + * @callback AfterTransform + * Function called when a DOM node is transformed into a hast node + * @param {Node} domNode + * The DOM node that was handled + * @param {HastNode|undefined} hastNode + * The corresponding hast node + * @returns {void} + * + * @typedef Options + * @property {AfterTransform} [afterTransform] + * + * @typedef Context + * @property {AfterTransform} [afterTransform] */ import {webNamespaces} from 'web-namespaces' @@ -21,17 +35,29 @@ const DOCUMENT_FRAGMENT_NODE = 11 /** * @param {Node} node + * @param {Context} ctx + * @returns {HastNode|undefined} + */ +function transform(node, ctx) { + const transformed = one(node, ctx) + if (ctx.afterTransform) ctx.afterTransform(node, transformed) + return transformed +} + +/** + * @param {Node} node + * @param {Context} ctx * @returns {HastNode|undefined} */ -function transform(node) { +function one(node, ctx) { switch (node.nodeType) { case ELEMENT_NODE: // @ts-expect-error TypeScript is wrong. - return element(node) + return element(node, ctx) case DOCUMENT_NODE: case DOCUMENT_FRAGMENT_NODE: // @ts-expect-error TypeScript is wrong. - return root(node) + return root(node, ctx) case TEXT_NODE: // @ts-expect-error TypeScript is wrong. return text(node) @@ -49,10 +75,11 @@ function transform(node) { * Transform a document. * * @param {Document|DocumentFragment} node + * @param {Context} ctx * @returns {HastRoot} */ -function root(node) { - return {type: 'root', children: all(node)} +function root(node, ctx) { + return {type: 'root', children: all(node, ctx)} } /** @@ -89,9 +116,10 @@ function comment(node) { * Transform an element. * * @param {Element} node + * @param {Context} ctx * @returns {HastElement} */ -function element(node) { +function element(node, ctx) { const space = node.namespaceURI const fn = space === webNamespaces.svg ? s : h const tagName = @@ -109,23 +137,24 @@ function element(node) { props[attributes[index]] = node.getAttribute(attributes[index]) || '' } - return fn(tagName, props, all(content)) + return fn(tagName, props, all(content, ctx)) } /** * Transform an element. * * @param {Document|DocumentFragment|Element} node + * @param {Context} ctx * @returns {Array.} */ -function all(node) { +function all(node, ctx) { const nodes = node.childNodes /** @type {Array.} */ const children = [] let index = -1 while (++index < nodes.length) { - const child = transform(nodes[index]) + const child = transform(nodes[index], ctx) if (child !== undefined) { // @ts-expect-error Assume no document inside document. @@ -138,8 +167,9 @@ function all(node) { /** * @param {Node} node + * @param {Options} [options] * @returns {HastNode} */ -export function fromDom(node) { - return transform(node || {}) || {type: 'root', children: []} +export function fromDom(node, options = {}) { + return transform(node || {}, options) || {type: 'root', children: []} } diff --git a/readme.md b/readme.md index 4fef0fc..79646c5 100644 --- a/readme.md +++ b/readme.md @@ -73,6 +73,14 @@ This works in a similar way to the [`parse5`][hast-util-from-parse5] version except that it works directly from the DOM rather than a string of HTML. Consequently, it does not maintain [positional info][positional-information]. +##### `options` + +###### `options.afterTransform` + +Function called when a DOM node is transformed into a hast node (`Function?`). +Given the DOM node that was handled as the first parameter and the +corresponding hast node as the second parameter. + ## Security Use of `hast-util-from-dom` can open you up to a diff --git a/test/index.js b/test/index.js index e1ee383..d5b00b7 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('../lib/index.js').HastNode} HastNode + */ + /* eslint-env browser */ import fs from 'node:fs' @@ -144,6 +148,40 @@ test('hast-util-from-dom', (t) => { 'should support an attribute w/o value' ) + const heading = document.createElement('h2') + const text = document.createTextNode('Hello') + heading.append(text) + + t.deepEqual( + (() => { + /** @type {Array<[Node, HastNode|undefined]>} */ + const calls = [] + fromDom(heading, { + /** + * @param {Node} node + * @param {HastNode|undefined} transformed + */ + afterTransform: (node, transformed) => { + calls.push([node, transformed]) + } + }) + return calls + })(), + [ + [text, {type: 'text', value: 'Hello'}], + [ + heading, + { + type: 'element', + tagName: 'h2', + properties: {}, + children: [{type: 'text', value: 'Hello'}] + } + ] + ], + 'should invoke afterTransform' + ) + t.end() })