Skip to content

Commit f64e56f

Browse files
committed
Add support for options.passThrough
* 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.
1 parent bee5dd9 commit f64e56f

File tree

6 files changed

+145
-8
lines changed

6 files changed

+145
-8
lines changed

index.js

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
var Parser = require('parse5/lib/parser')
44
var pos = require('unist-util-position')
5+
var visit = require('unist-util-visit')
56
var fromParse5 = require('hast-util-from-parse5')
67
var toParse5 = require('hast-util-to-parse5')
78
var voids = require('html-void-elements')
@@ -19,12 +20,9 @@ var endTagToken = 'END_TAG_TOKEN'
1920
var commentToken = 'COMMENT_TOKEN'
2021
var doctypeToken = 'DOCTYPE_TOKEN'
2122

22-
var parseOptions = {
23-
sourceCodeLocationInfo: true,
24-
scriptingEnabled: false
25-
}
23+
var parseOptions = {sourceCodeLocationInfo: true, scriptingEnabled: false}
2624

27-
function wrap(tree, file) {
25+
function wrap(tree, file, options) {
2826
var parser = new Parser(parseOptions)
2927
var one = zwitch('type', {
3028
handlers: {
@@ -37,11 +35,32 @@ function wrap(tree, file) {
3735
},
3836
unknown: unknown
3937
})
38+
var stitches
4039
var tokenizer
4140
var preprocessor
4241
var posTracker
4342
var locationTracker
44-
var result = fromParse5(documentMode(tree) ? document() : fragment(), file)
43+
var result
44+
var index
45+
46+
if (file && !('contents' in file)) {
47+
options = file
48+
file = undefined
49+
}
50+
51+
if (options && options.passThrough) {
52+
index = -1
53+
54+
while (++index < options.passThrough.length) {
55+
one.handlers[options.passThrough[index]] = stitch
56+
}
57+
}
58+
59+
result = fromParse5(documentMode(tree) ? document() : fragment(), file)
60+
61+
if (stitches) {
62+
visit(result, 'comment', mend)
63+
}
4564

4665
// Unpack if possible and when not given a `root`.
4766
if (tree.type !== 'root' && result.children.length === 1) {
@@ -50,6 +69,13 @@ function wrap(tree, file) {
5069

5170
return result
5271

72+
function mend(node, index, parent) {
73+
if (node.value.stitch) {
74+
parent.children[index] = node.value.stitch
75+
return index
76+
}
77+
}
78+
5379
function fragment() {
5480
var context = {
5581
nodeName: 'template',
@@ -208,6 +234,27 @@ function wrap(tree, file) {
208234
}
209235
}
210236

237+
function stitch(node) {
238+
var clone = Object.assign({}, node)
239+
240+
stitches = true
241+
242+
// Recurse, because to somewhat handle `[<x>]</x>` (where `[]` denotes the
243+
// passed through node).
244+
if (node.children) {
245+
clone.children = wrap(
246+
{type: 'root', children: node.children},
247+
file,
248+
options
249+
).children
250+
}
251+
252+
// Hack: `value` is supposed to be a string, but as none of the tools
253+
// (`parse5` or `hast-util-from-parse5`) looks at it, we can pass nodes
254+
// through.
255+
comment({value: {stitch: clone}})
256+
}
257+
211258
function resetTokenizer() {
212259
// Reset tokenizer:
213260
// See: <https://github.com/inikulin/parse5/blob/9c683e1/packages/parse5/lib/tokenizer/index.js#L218-L234>.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"html-void-elements": "^1.0.0",
3737
"parse5": "^6.0.0",
3838
"unist-util-position": "^3.0.0",
39+
"unist-util-visit": "^2.0.0",
3940
"vfile": "^4.0.0",
4041
"web-namespaces": "^1.0.0",
4142
"xtend": "^4.0.0",

readme.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,19 @@ Yields:
6161

6262
## API
6363

64-
### `raw(tree[, file])`
64+
### `raw(tree[, file][, options])`
6565

6666
Given a [**hast**][hast] [*tree*][tree] and an optional [vfile][] (for
6767
[positional info][position-information]), return a new parsed-again
6868
[**hast**][hast] [*tree*][tree].
6969

70+
###### `options.passThrough`
71+
72+
List of custom hast node types to pass through (keep) in hast
73+
(`Array.<string>`, default: `[]`).
74+
If the passed through nodes have children, those children are expected to be
75+
hast and will be handled.
76+
7077
## Security
7178

7279
Use of `hast-util-raw` can open you up to a [cross-site scripting (XSS)][xss]

test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,68 @@ test('raw', function (t) {
185185
'should not discard HTML broken over several raw nodes'
186186
)
187187

188+
t.deepEqual(
189+
raw(u('root', [u('custom', 'x')]), {passThrough: ['custom']}),
190+
u('root', {data: {quirksMode: false}}, [u('custom', 'x')]),
191+
'should support passing through nodes w/o children'
192+
)
193+
194+
t.deepEqual(
195+
raw(u('root', [u('custom', [u('raw', '<i>j</i>')])]), {
196+
passThrough: ['custom']
197+
}),
198+
u('root', {data: {quirksMode: false}}, [u('custom', [h('i', 'j')])]),
199+
'should support passing through nodes w/ `raw` children'
200+
)
201+
202+
t.deepEqual(
203+
raw(u('root', [u('custom', [u('comment', 'x')])]), {
204+
passThrough: ['custom']
205+
}),
206+
u('root', {data: {quirksMode: false}}, [u('custom', [u('comment', 'x')])]),
207+
'should support passing through nodes w/ `comment` children'
208+
)
209+
210+
t.deepEqual(
211+
raw(u('root', [u('custom', [])]), {
212+
passThrough: ['custom']
213+
}),
214+
u('root', {data: {quirksMode: false}}, [u('custom', [])]),
215+
'should support passing through nodes w/ `0` children'
216+
)
217+
218+
t.deepEqual(
219+
raw(u('root', [u('custom', [u('raw', '<x')])]), {
220+
passThrough: ['custom']
221+
}),
222+
u('root', {data: {quirksMode: false}}, [u('custom', [])]),
223+
'should support passing through nodes w/ broken raw children (1)'
224+
)
225+
226+
t.deepEqual(
227+
raw(u('root', [u('custom', [u('raw', '<x>')])]), {
228+
passThrough: ['custom']
229+
}),
230+
u('root', {data: {quirksMode: false}}, [u('custom', [h('x')])]),
231+
'should support passing through nodes w/ broken raw children (2)'
232+
)
233+
234+
t.deepEqual(
235+
raw(u('root', [u('custom', [u('raw', '</x>')])]), {
236+
passThrough: ['custom']
237+
}),
238+
u('root', {data: {quirksMode: false}}, [u('custom', [])]),
239+
'should support passing through nodes w/ broken raw children (3)'
240+
)
241+
242+
t.deepEqual(
243+
raw(u('root', [u('custom', [u('raw', '<x>')]), u('raw', '</x>')]), {
244+
passThrough: ['custom']
245+
}),
246+
u('root', {data: {quirksMode: false}}, [u('custom', [h('x')])]),
247+
'should support passing through nodes w/ broken raw children (4)'
248+
)
249+
188250
t.deepEqual(
189251
raw(u('root', [u('raw', '<script>alert(1)</script>')])),
190252
u('root', {data: {quirksMode: false}}, [

types/hast-util-raw-test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ raw({type: 'element', tagName: 'div', properties: {}, children: []}) // $ExpectT
66
// prettier-ignore
77
raw({type: 'element', tagName: 'div', properties: {}, children: []}, vFile('test')) // $ExpectType Node
88

9+
raw({type: 'raw'}, {}) // $ExpectType Node
10+
raw({type: 'raw'}, {passThrough: []}) // $ExpectType Node
11+
raw({type: 'raw'}, {passThrough: ['x']}) // $ExpectType Node
12+
raw({type: 'raw'}, vFile(), {}) // $ExpectType Node
13+
914
raw() // $ExpectError
1015
raw({}) // $ExpectError
1116
// prettier-ignore
1217
raw({type: 'element', tagName: 'div', properties: {}, children: []}, 'not a vFile') // $ExpectError
18+
raw({type: 'raw'}, {x: 1}) // $ExpectError
19+
raw({type: 'raw'}, {}, vFile()) // $ExpectError

types/index.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,24 @@
33
import {Node} from 'hast'
44
import {VFile} from 'vfile'
55

6+
declare namespace raw {
7+
interface Options {
8+
/**
9+
* List of custom hast node types to pass through (keep) in hast.
10+
* If the passed through nodes have children, those children are expected to
11+
* be hast and will be handled.
12+
*/
13+
passThrough?: string[]
14+
}
15+
}
16+
617
/**
718
* Given a hast tree and an optional vfile (for positional info), return a new parsed-again hast tree.
819
* @param tree original hast tree
920
* @param file positional info
21+
* @param options settings
1022
*/
11-
declare function raw(tree: Node, file?: VFile): Node
23+
declare function raw(tree: Node, file?: VFile, options?: raw.Options): Node
24+
declare function raw(tree: Node, options?: raw.Options): Node
1225

1326
export = raw

0 commit comments

Comments
 (0)