From f64e56f46a24fccf0202553e252cd115ea678164 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 5 Mar 2021 11:56:37 +0100 Subject: [PATCH] Add support for `options.passThrough` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for options * Add `passThrough` to pass through otherwise unknown nodes from the “malformed” tree to the well formed tree Related to wooorm/xdm#17. --- index.js | 59 +++++++++++++++++++++++++++++++---- package.json | 1 + readme.md | 9 +++++- test.js | 62 +++++++++++++++++++++++++++++++++++++ types/hast-util-raw-test.ts | 7 +++++ types/index.d.ts | 15 ++++++++- 6 files changed, 145 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 50ac015..0665512 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ var Parser = require('parse5/lib/parser') var pos = require('unist-util-position') +var visit = require('unist-util-visit') var fromParse5 = require('hast-util-from-parse5') var toParse5 = require('hast-util-to-parse5') var voids = require('html-void-elements') @@ -19,12 +20,9 @@ var endTagToken = 'END_TAG_TOKEN' var commentToken = 'COMMENT_TOKEN' var doctypeToken = 'DOCTYPE_TOKEN' -var parseOptions = { - sourceCodeLocationInfo: true, - scriptingEnabled: false -} +var parseOptions = {sourceCodeLocationInfo: true, scriptingEnabled: false} -function wrap(tree, file) { +function wrap(tree, file, options) { var parser = new Parser(parseOptions) var one = zwitch('type', { handlers: { @@ -37,11 +35,32 @@ function wrap(tree, file) { }, unknown: unknown }) + var stitches var tokenizer var preprocessor var posTracker var locationTracker - var result = fromParse5(documentMode(tree) ? document() : fragment(), file) + var result + var index + + if (file && !('contents' in file)) { + options = file + file = undefined + } + + if (options && options.passThrough) { + index = -1 + + while (++index < options.passThrough.length) { + one.handlers[options.passThrough[index]] = stitch + } + } + + result = fromParse5(documentMode(tree) ? document() : fragment(), file) + + if (stitches) { + visit(result, 'comment', mend) + } // Unpack if possible and when not given a `root`. if (tree.type !== 'root' && result.children.length === 1) { @@ -50,6 +69,13 @@ function wrap(tree, file) { return result + function mend(node, index, parent) { + if (node.value.stitch) { + parent.children[index] = node.value.stitch + return index + } + } + function fragment() { var context = { nodeName: 'template', @@ -208,6 +234,27 @@ function wrap(tree, file) { } } + function stitch(node) { + var clone = Object.assign({}, node) + + stitches = true + + // Recurse, because to somewhat handle `[]` (where `[]` denotes the + // passed through node). + if (node.children) { + clone.children = wrap( + {type: 'root', children: node.children}, + file, + options + ).children + } + + // Hack: `value` is supposed to be a string, but as none of the tools + // (`parse5` or `hast-util-from-parse5`) looks at it, we can pass nodes + // through. + comment({value: {stitch: clone}}) + } + function resetTokenizer() { // Reset tokenizer: // See: . diff --git a/package.json b/package.json index 7b4baaf..e99cd74 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "html-void-elements": "^1.0.0", "parse5": "^6.0.0", "unist-util-position": "^3.0.0", + "unist-util-visit": "^2.0.0", "vfile": "^4.0.0", "web-namespaces": "^1.0.0", "xtend": "^4.0.0", diff --git a/readme.md b/readme.md index 72b7281..0cab987 100644 --- a/readme.md +++ b/readme.md @@ -61,12 +61,19 @@ Yields: ## API -### `raw(tree[, file])` +### `raw(tree[, file][, options])` Given a [**hast**][hast] [*tree*][tree] and an optional [vfile][] (for [positional info][position-information]), return a new parsed-again [**hast**][hast] [*tree*][tree]. +###### `options.passThrough` + +List of custom hast node types to pass through (keep) in hast +(`Array.`, default: `[]`). +If the passed through nodes have children, those children are expected to be +hast and will be handled. + ## Security Use of `hast-util-raw` can open you up to a [cross-site scripting (XSS)][xss] diff --git a/test.js b/test.js index 7e769e7..5070972 100644 --- a/test.js +++ b/test.js @@ -185,6 +185,68 @@ test('raw', function (t) { 'should not discard HTML broken over several raw nodes' ) + t.deepEqual( + raw(u('root', [u('custom', 'x')]), {passThrough: ['custom']}), + u('root', {data: {quirksMode: false}}, [u('custom', 'x')]), + 'should support passing through nodes w/o children' + ) + + t.deepEqual( + raw(u('root', [u('custom', [u('raw', 'j')])]), { + passThrough: ['custom'] + }), + u('root', {data: {quirksMode: false}}, [u('custom', [h('i', 'j')])]), + 'should support passing through nodes w/ `raw` children' + ) + + t.deepEqual( + raw(u('root', [u('custom', [u('comment', 'x')])]), { + passThrough: ['custom'] + }), + u('root', {data: {quirksMode: false}}, [u('custom', [u('comment', 'x')])]), + 'should support passing through nodes w/ `comment` children' + ) + + t.deepEqual( + raw(u('root', [u('custom', [])]), { + passThrough: ['custom'] + }), + u('root', {data: {quirksMode: false}}, [u('custom', [])]), + 'should support passing through nodes w/ `0` children' + ) + + t.deepEqual( + raw(u('root', [u('custom', [u('raw', '')])]), { + passThrough: ['custom'] + }), + u('root', {data: {quirksMode: false}}, [u('custom', [h('x')])]), + 'should support passing through nodes w/ broken raw children (2)' + ) + + t.deepEqual( + raw(u('root', [u('custom', [u('raw', '')])]), { + passThrough: ['custom'] + }), + u('root', {data: {quirksMode: false}}, [u('custom', [])]), + 'should support passing through nodes w/ broken raw children (3)' + ) + + t.deepEqual( + raw(u('root', [u('custom', [u('raw', '')]), u('raw', '')]), { + passThrough: ['custom'] + }), + u('root', {data: {quirksMode: false}}, [u('custom', [h('x')])]), + 'should support passing through nodes w/ broken raw children (4)' + ) + t.deepEqual( raw(u('root', [u('raw', '')])), u('root', {data: {quirksMode: false}}, [ diff --git a/types/hast-util-raw-test.ts b/types/hast-util-raw-test.ts index dbc1b6b..b07ff8a 100644 --- a/types/hast-util-raw-test.ts +++ b/types/hast-util-raw-test.ts @@ -6,7 +6,14 @@ raw({type: 'element', tagName: 'div', properties: {}, children: []}) // $ExpectT // prettier-ignore raw({type: 'element', tagName: 'div', properties: {}, children: []}, vFile('test')) // $ExpectType Node +raw({type: 'raw'}, {}) // $ExpectType Node +raw({type: 'raw'}, {passThrough: []}) // $ExpectType Node +raw({type: 'raw'}, {passThrough: ['x']}) // $ExpectType Node +raw({type: 'raw'}, vFile(), {}) // $ExpectType Node + raw() // $ExpectError raw({}) // $ExpectError // prettier-ignore raw({type: 'element', tagName: 'div', properties: {}, children: []}, 'not a vFile') // $ExpectError +raw({type: 'raw'}, {x: 1}) // $ExpectError +raw({type: 'raw'}, {}, vFile()) // $ExpectError diff --git a/types/index.d.ts b/types/index.d.ts index 41c4e57..17758eb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,11 +3,24 @@ import {Node} from 'hast' import {VFile} from 'vfile' +declare namespace raw { + interface Options { + /** + * List of custom hast node types to pass through (keep) in hast. + * If the passed through nodes have children, those children are expected to + * be hast and will be handled. + */ + passThrough?: string[] + } +} + /** * Given a hast tree and an optional vfile (for positional info), return a new parsed-again hast tree. * @param tree original hast tree * @param file positional info + * @param options settings */ -declare function raw(tree: Node, file?: VFile): Node +declare function raw(tree: Node, file?: VFile, options?: raw.Options): Node +declare function raw(tree: Node, options?: raw.Options): Node export = raw