├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.d.ts ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── test ├── fixtures │ ├── attributes │ │ ├── index.html │ │ └── index.json │ ├── doctype-nameless │ │ ├── index.html │ │ └── index.json │ ├── doctype-quirksmode-ibm │ │ ├── index.html │ │ └── index.json │ ├── doctype-quirksmode-xml │ │ ├── index.html │ │ └── index.json │ ├── doctype │ │ ├── index.html │ │ └── index.json │ ├── element-void-close │ │ ├── index.html │ │ └── index.json │ ├── element-void │ │ ├── index.html │ │ └── index.json │ ├── simple │ │ ├── index.html │ │ └── index.json │ ├── svg │ │ ├── index.html │ │ └── index.json │ └── template │ │ ├── index.html │ │ └── index.json └── index.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: unifiedjs/beep-boop-beta@main 6 | with: 7 | repo-token: ${{secrets.GITHUB_TOKEN}} 8 | name: bb 9 | on: 10 | issues: 11 | types: [closed, edited, labeled, opened, reopened, unlabeled] 12 | pull_request_target: 13 | types: [closed, edited, labeled, opened, reopened, unlabeled] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v5 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.log 3 | *.map 4 | *.tsbuildinfo 5 | .DS_Store 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | !/index.d.ts 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.html 3 | *.md 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type {Position} from 'unist' 2 | import type {VFile} from 'vfile' 3 | 4 | export {fromParse5} from './lib/index.js' 5 | 6 | /** 7 | * Configuration. 8 | */ 9 | export interface Options { 10 | /** 11 | * File used to add positional info to nodes (optional). 12 | * 13 | * If given, the file should represent the original HTML source. 14 | */ 15 | file?: VFile | null | undefined 16 | 17 | /** 18 | * Which space the document is in (default: `'html'`). 19 | * 20 | * When an `` element is found in the HTML space, this package already 21 | * automatically switches to and from the SVG space when entering and exiting 22 | * it. 23 | */ 24 | space?: Space | null | undefined 25 | 26 | /** 27 | * Whether to add extra positional info about starting tags, closing tags, 28 | * and attributes to elements (default: `false`). 29 | * 30 | * > 👉 **Note**: only used when `file` is given. 31 | */ 32 | verbose?: boolean | null | undefined 33 | } 34 | 35 | /** 36 | * Namespace. 37 | */ 38 | export type Space = 'html' | 'svg' 39 | 40 | // Register data on hast. 41 | declare module 'hast' { 42 | interface ElementData { 43 | position: { 44 | /** 45 | * Positional info of the start tag of an element. 46 | * 47 | * Field added by `hast-util-from-parse5` (a utility used inside 48 | * `rehype-parse` responsible for parsing HTML), when passing 49 | * `verbose: true`. 50 | */ 51 | opening?: Position | undefined 52 | 53 | /** 54 | * Positional info of the end tag of an element. 55 | * 56 | * Field added by `hast-util-from-parse5` (a utility used inside 57 | * `rehype-parse` responsible for parsing HTML), when passing 58 | * `verbose: true`. 59 | */ 60 | closing?: Position | undefined 61 | 62 | /** 63 | * Positional info of the properties of an element. 64 | * 65 | * Field added by `hast-util-from-parse5` (a utility used inside 66 | * `rehype-parse` responsible for parsing HTML), when passing 67 | * `verbose: true`. 68 | */ 69 | properties?: Record | undefined 70 | } 71 | } 72 | 73 | interface RootData { 74 | /** 75 | * Whether the document was using quirksmode. 76 | * 77 | * Field added by `hast-util-from-parse5` (a utility used inside 78 | * `rehype-parse` responsible for parsing HTML). 79 | */ 80 | quirksMode?: boolean | undefined 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: extra types exposed from `index.d.ts`. 2 | export {fromParse5} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {ElementData, Element, Nodes, RootContent, Root} from 'hast' 3 | * @import {DefaultTreeAdapterMap, Token} from 'parse5' 4 | * @import {Schema} from 'property-information' 5 | * @import {Point, Position} from 'unist' 6 | * @import {VFile} from 'vfile' 7 | * @import {Options} from 'hast-util-from-parse5' 8 | */ 9 | 10 | /** 11 | * @typedef State 12 | * Info passed around about the current state. 13 | * @property {VFile | undefined} file 14 | * Corresponding file. 15 | * @property {boolean} location 16 | * Whether location info was found. 17 | * @property {Schema} schema 18 | * Current schema. 19 | * @property {boolean | undefined} verbose 20 | * Add extra positional info. 21 | */ 22 | 23 | import {ok as assert} from 'devlop' 24 | import {h, s} from 'hastscript' 25 | import {find, html, svg} from 'property-information' 26 | import {location} from 'vfile-location' 27 | import {webNamespaces} from 'web-namespaces' 28 | 29 | const own = {}.hasOwnProperty 30 | /** @type {unknown} */ 31 | // type-coverage:ignore-next-line 32 | const proto = Object.prototype 33 | 34 | /** 35 | * Transform a `parse5` AST to hast. 36 | * 37 | * @param {DefaultTreeAdapterMap['node']} tree 38 | * `parse5` tree to transform. 39 | * @param {Options | null | undefined} [options] 40 | * Configuration (optional). 41 | * @returns {Nodes} 42 | * hast tree. 43 | */ 44 | export function fromParse5(tree, options) { 45 | const settings = options || {} 46 | 47 | return one( 48 | { 49 | file: settings.file || undefined, 50 | location: false, 51 | schema: settings.space === 'svg' ? svg : html, 52 | verbose: settings.verbose || false 53 | }, 54 | tree 55 | ) 56 | } 57 | 58 | /** 59 | * Transform a node. 60 | * 61 | * @param {State} state 62 | * Info passed around about the current state. 63 | * @param {DefaultTreeAdapterMap['node']} node 64 | * p5 node. 65 | * @returns {Nodes} 66 | * hast node. 67 | */ 68 | function one(state, node) { 69 | /** @type {Nodes} */ 70 | let result 71 | 72 | switch (node.nodeName) { 73 | case '#comment': { 74 | const reference = /** @type {DefaultTreeAdapterMap['commentNode']} */ ( 75 | node 76 | ) 77 | result = {type: 'comment', value: reference.data} 78 | patch(state, reference, result) 79 | return result 80 | } 81 | 82 | case '#document': 83 | case '#document-fragment': { 84 | const reference = 85 | /** @type {DefaultTreeAdapterMap['document'] | DefaultTreeAdapterMap['documentFragment']} */ ( 86 | node 87 | ) 88 | const quirksMode = 89 | 'mode' in reference 90 | ? reference.mode === 'quirks' || reference.mode === 'limited-quirks' 91 | : false 92 | 93 | result = { 94 | type: 'root', 95 | children: all(state, node.childNodes), 96 | data: {quirksMode} 97 | } 98 | 99 | if (state.file && state.location) { 100 | const document = String(state.file) 101 | const loc = location(document) 102 | const start = loc.toPoint(0) 103 | const end = loc.toPoint(document.length) 104 | // Always defined as we give valid input. 105 | assert(start, 'expected `start`') 106 | assert(end, 'expected `end`') 107 | result.position = {start, end} 108 | } 109 | 110 | return result 111 | } 112 | 113 | case '#documentType': { 114 | const reference = /** @type {DefaultTreeAdapterMap['documentType']} */ ( 115 | node 116 | ) 117 | result = {type: 'doctype'} 118 | patch(state, reference, result) 119 | return result 120 | } 121 | 122 | case '#text': { 123 | const reference = /** @type {DefaultTreeAdapterMap['textNode']} */ (node) 124 | result = {type: 'text', value: reference.value} 125 | patch(state, reference, result) 126 | return result 127 | } 128 | 129 | // Element. 130 | default: { 131 | const reference = /** @type {DefaultTreeAdapterMap['element']} */ (node) 132 | result = element(state, reference) 133 | return result 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Transform children. 140 | * 141 | * @param {State} state 142 | * Info passed around about the current state. 143 | * @param {Array} nodes 144 | * Nodes. 145 | * @returns {Array} 146 | * hast nodes. 147 | */ 148 | function all(state, nodes) { 149 | let index = -1 150 | /** @type {Array} */ 151 | const results = [] 152 | 153 | while (++index < nodes.length) { 154 | // Assume no roots in `nodes`. 155 | const result = /** @type {RootContent} */ (one(state, nodes[index])) 156 | results.push(result) 157 | } 158 | 159 | return results 160 | } 161 | 162 | /** 163 | * Transform an element. 164 | * 165 | * @param {State} state 166 | * Info passed around about the current state. 167 | * @param {DefaultTreeAdapterMap['element']} node 168 | * `parse5` node to transform. 169 | * @returns {Element} 170 | * hast node. 171 | */ 172 | function element(state, node) { 173 | const schema = state.schema 174 | 175 | state.schema = node.namespaceURI === webNamespaces.svg ? svg : html 176 | 177 | // Props. 178 | let index = -1 179 | /** @type {Record} */ 180 | const properties = {} 181 | 182 | while (++index < node.attrs.length) { 183 | const attribute = node.attrs[index] 184 | const name = 185 | (attribute.prefix ? attribute.prefix + ':' : '') + attribute.name 186 | if (!own.call(proto, name)) { 187 | properties[name] = attribute.value 188 | } 189 | } 190 | 191 | // Build. 192 | const x = state.schema.space === 'svg' ? s : h 193 | const result = x(node.tagName, properties, all(state, node.childNodes)) 194 | patch(state, node, result) 195 | 196 | // Switch content. 197 | if (result.tagName === 'template') { 198 | const reference = /** @type {DefaultTreeAdapterMap['template']} */ (node) 199 | const pos = reference.sourceCodeLocation 200 | const startTag = pos && pos.startTag && position(pos.startTag) 201 | const endTag = pos && pos.endTag && position(pos.endTag) 202 | 203 | // Root in, root out. 204 | const content = /** @type {Root} */ (one(state, reference.content)) 205 | 206 | if (startTag && endTag && state.file) { 207 | content.position = {start: startTag.end, end: endTag.start} 208 | } 209 | 210 | result.content = content 211 | } 212 | 213 | state.schema = schema 214 | 215 | return result 216 | } 217 | 218 | /** 219 | * Patch positional info from `from` onto `to`. 220 | * 221 | * @param {State} state 222 | * Info passed around about the current state. 223 | * @param {DefaultTreeAdapterMap['node']} from 224 | * p5 node. 225 | * @param {Nodes} to 226 | * hast node. 227 | * @returns {undefined} 228 | * Nothing. 229 | */ 230 | function patch(state, from, to) { 231 | if ('sourceCodeLocation' in from && from.sourceCodeLocation && state.file) { 232 | const position = createLocation(state, to, from.sourceCodeLocation) 233 | 234 | if (position) { 235 | state.location = true 236 | to.position = position 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * Create clean positional information. 243 | * 244 | * @param {State} state 245 | * Info passed around about the current state. 246 | * @param {Nodes} node 247 | * hast node. 248 | * @param {Token.ElementLocation} location 249 | * p5 location info. 250 | * @returns {Position | undefined} 251 | * Position, or nothing. 252 | */ 253 | function createLocation(state, node, location) { 254 | const result = position(location) 255 | 256 | if (node.type === 'element') { 257 | const tail = node.children[node.children.length - 1] 258 | 259 | // Bug for unclosed with children. 260 | // See: . 261 | if ( 262 | result && 263 | !location.endTag && 264 | tail && 265 | tail.position && 266 | tail.position.end 267 | ) { 268 | result.end = Object.assign({}, tail.position.end) 269 | } 270 | 271 | if (state.verbose) { 272 | /** @type {Record} */ 273 | const properties = {} 274 | /** @type {string} */ 275 | let key 276 | 277 | if (location.attrs) { 278 | for (key in location.attrs) { 279 | if (own.call(location.attrs, key)) { 280 | properties[find(state.schema, key).property] = position( 281 | location.attrs[key] 282 | ) 283 | } 284 | } 285 | } 286 | 287 | assert(location.startTag, 'a start tag should exist') 288 | const opening = position(location.startTag) 289 | const closing = location.endTag ? position(location.endTag) : undefined 290 | /** @type {ElementData['position']} */ 291 | const data = {opening} 292 | if (closing) data.closing = closing 293 | data.properties = properties 294 | 295 | node.data = {position: data} 296 | } 297 | } 298 | 299 | return result 300 | } 301 | 302 | /** 303 | * Turn a p5 location into a position. 304 | * 305 | * @param {Token.Location} loc 306 | * Location. 307 | * @returns {Position | undefined} 308 | * Position or nothing. 309 | */ 310 | function position(loc) { 311 | const start = point({ 312 | line: loc.startLine, 313 | column: loc.startCol, 314 | offset: loc.startOffset 315 | }) 316 | const end = point({ 317 | line: loc.endLine, 318 | column: loc.endCol, 319 | offset: loc.endOffset 320 | }) 321 | 322 | // @ts-expect-error: we do use `undefined` for points if one or the other 323 | // exists. 324 | return start || end ? {start, end} : undefined 325 | } 326 | 327 | /** 328 | * Filter out invalid points. 329 | * 330 | * @param {Point} point 331 | * Point with potentially `undefined` values. 332 | * @returns {Point | undefined} 333 | * Point or nothing. 334 | */ 335 | function point(point) { 336 | return point.line && point.column ? point : undefined 337 | } 338 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/syntax-tree/hast-util-from-parse5/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/hast": "^3.0.0", 9 | "@types/unist": "^3.0.0", 10 | "devlop": "^1.0.0", 11 | "hastscript": "^9.0.0", 12 | "property-information": "^7.0.0", 13 | "vfile": "^6.0.0", 14 | "vfile-location": "^5.0.0", 15 | "web-namespaces": "^2.0.0" 16 | }, 17 | "description": "hast utility to transform from a `parse5` AST", 18 | "devDependencies": { 19 | "@types/node": "^22.0.0", 20 | "c8": "^10.0.0", 21 | "is-hidden": "^2.0.0", 22 | "parse5": "^7.0.0", 23 | "prettier": "^3.0.0", 24 | "remark-cli": "^12.0.0", 25 | "remark-preset-wooorm": "^11.0.0", 26 | "to-vfile": "^8.0.0", 27 | "type-coverage": "^2.0.0", 28 | "typescript": "^5.0.0", 29 | "unist-util-visit": "^5.0.0", 30 | "xo": "^0.60.0" 31 | }, 32 | "exports": "./index.js", 33 | "files": [ 34 | "index.d.ts", 35 | "index.js", 36 | "lib/" 37 | ], 38 | "funding": { 39 | "type": "opencollective", 40 | "url": "https://opencollective.com/unified" 41 | }, 42 | "keywords": [ 43 | "ast", 44 | "change", 45 | "hast-util", 46 | "hast", 47 | "transform", 48 | "unist", 49 | "utility", 50 | "util" 51 | ], 52 | "license": "MIT", 53 | "name": "hast-util-from-parse5", 54 | "prettier": { 55 | "bracketSpacing": false, 56 | "semi": false, 57 | "singleQuote": true, 58 | "tabWidth": 2, 59 | "trailingComma": "none", 60 | "useTabs": false 61 | }, 62 | "remarkConfig": { 63 | "plugins": [ 64 | "remark-preset-wooorm" 65 | ] 66 | }, 67 | "repository": "syntax-tree/hast-util-from-parse5", 68 | "scripts": { 69 | "build": "tsc --build --clean && tsc --build && type-coverage", 70 | "format": "remark --frail --quiet --output -- . && prettier --log-level warn --write -- . && xo --fix", 71 | "test-api": "node --conditions development test/index.js", 72 | "test-coverage": "c8 --100 --reporter lcov -- npm run test-api", 73 | "test": "npm run build && npm run format && npm run test-coverage" 74 | }, 75 | "sideEffects": false, 76 | "typeCoverage": { 77 | "atLeast": 100, 78 | "strict": true 79 | }, 80 | "type": "module", 81 | "version": "8.0.3", 82 | "xo": { 83 | "overrides": [ 84 | { 85 | "files": [ 86 | "**/*.d.ts" 87 | ], 88 | "rules": { 89 | "@typescript-eslint/array-type": [ 90 | "error", 91 | { 92 | "default": "generic" 93 | } 94 | ], 95 | "@typescript-eslint/ban-types": [ 96 | "error", 97 | { 98 | "extendDefaults": true 99 | } 100 | ], 101 | "@typescript-eslint/consistent-type-definitions": [ 102 | "error", 103 | "interface" 104 | ] 105 | } 106 | }, 107 | { 108 | "files": "test/**/*.js", 109 | "rules": { 110 | "no-await-in-loop": "off" 111 | } 112 | } 113 | ], 114 | "prettier": true, 115 | "rules": { 116 | "max-depth": "off", 117 | "unicorn/prefer-at": "off" 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # hast-util-from-parse5 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | 8 | [hast][github-hast] utility to transform from the 9 | [`parse5`][github-parse5] AST. 10 | 11 | ## Contents 12 | 13 | * [What is this?](#what-is-this) 14 | * [When should I use this?](#when-should-i-use-this) 15 | * [Install](#install) 16 | * [Use](#use) 17 | * [API](#api) 18 | * [`fromParse5(tree[, options])`](#fromparse5tree-options) 19 | * [`Options`](#options) 20 | * [`Space`](#space-1) 21 | * [Types](#types) 22 | * [Compatibility](#compatibility) 23 | * [Security](#security) 24 | * [Related](#related) 25 | * [Contribute](#contribute) 26 | * [License](#license) 27 | 28 | ## What is this? 29 | 30 | This package is a utility that can turn a parse5 tree into a hast tree. 31 | 32 | ## When should I use this? 33 | 34 | You can use this package when using `parse5` as an HTML parser and wanting to 35 | work with hast. 36 | 37 | The utility [`hast-util-to-parse5`][github-hast-util-to-parse5] does the 38 | inverse of this utility. 39 | It generates `parse5`s AST again. 40 | 41 | The utility [`hast-util-from-html`][github-hast-util-from-html] wraps this 42 | utility and `parse5` to both parse HTML and generate hast from it. 43 | 44 | ## Install 45 | 46 | This package is [ESM only][github-gist-esm]. 47 | In Node.js (version 16+), 48 | install with [npm][npmjs-install]: 49 | 50 | ```sh 51 | npm install hast-util-from-parse5 52 | ``` 53 | 54 | In Deno with [`esm.sh`][esmsh]: 55 | 56 | ```js 57 | import {fromParse5} from "https://esm.sh/hast-util-from-parse5@8" 58 | ``` 59 | 60 | In browsers with [`esm.sh`][esmsh]: 61 | 62 | ```html 63 | 66 | ``` 67 | 68 | ## Use 69 | 70 | Say our document `example.html` contains: 71 | 72 | ```html 73 | Hello!

World! 74 | ``` 75 | 76 | …and our module `example.js` looks as follows: 77 | 78 | ```js 79 | import {fromParse5} from 'hast-util-from-parse5' 80 | import {parse} from 'parse5' 81 | import {read} from 'to-vfile' 82 | import {inspect} from 'unist-util-inspect' 83 | 84 | const file = await read('example.html') 85 | const p5ast = parse(String(file), {sourceCodeLocationInfo: true}) 86 | const hast = fromParse5(p5ast, {file}) 87 | 88 | console.log(inspect(hast)) 89 | ``` 90 | 91 | …now running `node example.js` yields: 92 | 93 | ```text 94 | root[2] (1:1-2:1, 0-70) 95 | │ data: {"quirksMode":false} 96 | ├─0 doctype (1:1-1:16, 0-15) 97 | └─1 element[2] 98 | │ properties: {} 99 | ├─0 element[1] 100 | │ │ properties: {} 101 | │ └─0 element[1] (1:16-1:37, 15-36) 102 | │ │ properties: {} 103 | │ └─0 text "Hello!" (1:23-1:29, 22-28) 104 | └─1 element<body>[1] 105 | │ properties: {} 106 | └─0 element<h1>[3] (1:37-2:1, 36-70) 107 | │ properties: {"id":"world"} 108 | ├─0 text "World!" (1:52-1:58, 51-57) 109 | ├─1 comment "after" (1:58-1:70, 57-69) 110 | └─2 text "\n" (1:70-2:1, 69-70) 111 | ``` 112 | 113 | ## API 114 | 115 | This package exports the identifier [`fromParse5`][api-from-parse5]. 116 | There is no default export. 117 | 118 | ### `fromParse5(tree[, options])` 119 | 120 | Transform a `parse5` AST to hast. 121 | 122 | ###### Parameters 123 | 124 | * `tree` 125 | ([`Parse5Node`][github-parse5-node]) 126 | — `parse5` tree to transform 127 | * `options` 128 | ([`Options`][api-options], optional) 129 | — configuration 130 | 131 | ###### Returns 132 | 133 | hast tree ([`HastNode`][github-hast-nodes]). 134 | 135 | ### `Options` 136 | 137 | Configuration (TypeScript type). 138 | 139 | ##### Fields 140 | 141 | ###### `file` 142 | 143 | File used to add positional info to nodes 144 | ([`VFile`][github-vfile], optional). 145 | 146 | If given, 147 | the file should represent the original HTML source. 148 | 149 | ###### `space` 150 | 151 | Which space the document is in 152 | ([`Space`][api-space], default: `'html'`). 153 | 154 | When an `<svg>` element is found in the HTML space, 155 | this package already automatically switches to and from the SVG space when 156 | entering and exiting it. 157 | 158 | ###### `verbose` 159 | 160 | Whether to add extra positional info about starting tags, 161 | closing tags, 162 | and attributes to elements 163 | (`boolean`, default: `false`). 164 | 165 | > 👉 **Note**: 166 | > only used when `file` is given. 167 | 168 | For the following HTML: 169 | 170 | ```html 171 | <img src="http://example.com/fav.ico" alt="foo" title="bar"> 172 | ``` 173 | 174 | The verbose info would looks as follows: 175 | 176 | ```js 177 | { 178 | type: 'element', 179 | tagName: 'img', 180 | properties: {src: 'http://example.com/fav.ico', alt: 'foo', title: 'bar'}, 181 | children: [], 182 | data: { 183 | position: { 184 | opening: { 185 | start: {line: 1, column: 1, offset: 0}, 186 | end: {line: 1, column: 61, offset: 60} 187 | }, 188 | closing: null, 189 | properties: { 190 | src: { 191 | start: {line: 1, column: 6, offset: 5}, 192 | end: {line: 1, column: 38, offset: 37} 193 | }, 194 | alt: { 195 | start: {line: 1, column: 39, offset: 38}, 196 | end: {line: 1, column: 48, offset: 47} 197 | }, 198 | title: { 199 | start: {line: 1, column: 49, offset: 48}, 200 | end: {line: 1, column: 60, offset: 59} 201 | } 202 | } 203 | } 204 | }, 205 | position: { 206 | start: {line: 1, column: 1, offset: 0}, 207 | end: {line: 1, column: 61, offset: 60} 208 | } 209 | } 210 | ``` 211 | 212 | ### `Space` 213 | 214 | Namespace (TypeScript type). 215 | 216 | ###### Type 217 | 218 | ```ts 219 | type Space = 'html' | 'svg' 220 | ``` 221 | 222 | ## Types 223 | 224 | This package is fully typed with [TypeScript][]. 225 | It exports the additional types [`Options`][api-options] and 226 | [`Space`][api-space]. 227 | 228 | ## Compatibility 229 | 230 | Projects maintained by the unified collective are compatible with maintained 231 | versions of Node.js. 232 | 233 | When we cut a new major release, 234 | we drop support for unmaintained versions of Node. 235 | This means we try to keep the current release line, 236 | `hast-util-from-parse5@8`, 237 | compatible with Node.js 16. 238 | 239 | ## Security 240 | 241 | Use of `hast-util-from-parse5` can open you up to a 242 | [cross-site scripting (XSS)][wikipedia-xss] attack if Parse5’s AST is unsafe. 243 | 244 | ## Related 245 | 246 | * [`hast-util-to-parse5`][github-hast-util-to-parse5] 247 | — transform hast to Parse5’s AST 248 | * [`hast-util-to-nlcst`](https://github.com/syntax-tree/hast-util-to-nlcst) 249 | — transform hast to nlcst 250 | * [`hast-util-to-mdast`](https://github.com/syntax-tree/hast-util-to-mdast) 251 | — transform hast to mdast 252 | * [`hast-util-to-xast`](https://github.com/syntax-tree/hast-util-to-xast) 253 | — transform hast to xast 254 | * [`mdast-util-to-hast`](https://github.com/syntax-tree/mdast-util-to-hast) 255 | — transform mdast to hast 256 | * [`mdast-util-to-nlcst`](https://github.com/syntax-tree/mdast-util-to-nlcst) 257 | — transform mdast to nlcst 258 | 259 | ## Contribute 260 | 261 | See [`contributing.md`][health-contributing] 262 | in 263 | [`syntax-tree/.github`][health] 264 | for ways to get started. 265 | See [`support.md`][health-support] for ways to get help. 266 | 267 | This project has a [code of conduct][health-coc]. 268 | By interacting with this repository, 269 | organization, 270 | or community you agree to abide by its terms. 271 | 272 | ## License 273 | 274 | [MIT][file-license] © [Titus Wormer][wooorm] 275 | 276 | <!-- Definitions --> 277 | 278 | [api-from-parse5]: #fromparse5tree-options 279 | 280 | [api-options]: #options 281 | 282 | [api-space]: #space-1 283 | 284 | [badge-build-image]: https://github.com/syntax-tree/hast-util-from-parse5/workflows/main/badge.svg 285 | 286 | [badge-build-url]: https://github.com/syntax-tree/hast-util-from-parse5/actions 287 | 288 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/syntax-tree/hast-util-from-parse5.svg 289 | 290 | [badge-coverage-url]: https://codecov.io/github/syntax-tree/hast-util-from-parse5 291 | 292 | [badge-downloads-image]: https://img.shields.io/npm/dm/hast-util-from-parse5.svg 293 | 294 | [badge-downloads-url]: https://www.npmjs.com/package/hast-util-from-parse5 295 | 296 | [badge-size-image]: https://img.shields.io/bundlejs/size/hast-util-from-parse5 297 | 298 | [badge-size-url]: https://bundlejs.com/?q=hast-util-from-parse5 299 | 300 | [esmsh]: https://esm.sh 301 | 302 | [file-license]: license 303 | 304 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 305 | 306 | [github-hast]: https://github.com/syntax-tree/hast 307 | 308 | [github-hast-nodes]: https://github.com/syntax-tree/hast#nodes 309 | 310 | [github-hast-util-from-html]: https://github.com/syntax-tree/hast-util-from-html 311 | 312 | [github-hast-util-to-parse5]: https://github.com/syntax-tree/hast-util-to-parse5 313 | 314 | [github-parse5]: https://github.com/inikulin/parse5 315 | 316 | [github-parse5-node]: https://github.com/inikulin/parse5/blob/master/packages/parse5/lib/tree-adapters/default.ts 317 | 318 | [github-vfile]: https://github.com/vfile/vfile 319 | 320 | [health]: https://github.com/syntax-tree/.github 321 | 322 | [health-coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 323 | 324 | [health-contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 325 | 326 | [health-support]: https://github.com/syntax-tree/.github/blob/main/support.md 327 | 328 | [npmjs-install]: https://docs.npmjs.com/cli/install 329 | 330 | [typescript]: https://www.typescriptlang.org 331 | 332 | [wikipedia-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting 333 | 334 | [wooorm]: https://wooorm.com 335 | -------------------------------------------------------------------------------- /test/fixtures/attributes/index.html: -------------------------------------------------------------------------------- 1 | <p id="foo" class="bar baz" data-qux="quux"></p> 2 | 3 | <p data-123="456"></p> 4 | 5 | <img alt hidden> 6 | 7 | <a download></a> 8 | 9 | <a download=example.mp3></a> 10 | -------------------------------------------------------------------------------- /test/fixtures/attributes/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "element", 6 | "tagName": "html", 7 | "properties": {}, 8 | "children": [ 9 | { 10 | "type": "element", 11 | "tagName": "head", 12 | "properties": {}, 13 | "children": [] 14 | }, 15 | { 16 | "type": "element", 17 | "tagName": "body", 18 | "properties": {}, 19 | "children": [ 20 | { 21 | "type": "element", 22 | "tagName": "p", 23 | "properties": { 24 | "id": "foo", 25 | "className": ["bar", "baz"], 26 | "dataQux": "quux" 27 | }, 28 | "children": [], 29 | "data": { 30 | "position": { 31 | "opening": { 32 | "start": { 33 | "line": 1, 34 | "column": 1, 35 | "offset": 0 36 | }, 37 | "end": { 38 | "line": 1, 39 | "column": 45, 40 | "offset": 44 41 | } 42 | }, 43 | "closing": { 44 | "start": { 45 | "line": 1, 46 | "column": 45, 47 | "offset": 44 48 | }, 49 | "end": { 50 | "line": 1, 51 | "column": 49, 52 | "offset": 48 53 | } 54 | }, 55 | "properties": { 56 | "id": { 57 | "start": { 58 | "line": 1, 59 | "column": 4, 60 | "offset": 3 61 | }, 62 | "end": { 63 | "line": 1, 64 | "column": 12, 65 | "offset": 11 66 | } 67 | }, 68 | "className": { 69 | "start": { 70 | "line": 1, 71 | "column": 13, 72 | "offset": 12 73 | }, 74 | "end": { 75 | "line": 1, 76 | "column": 28, 77 | "offset": 27 78 | } 79 | }, 80 | "dataQux": { 81 | "start": { 82 | "line": 1, 83 | "column": 29, 84 | "offset": 28 85 | }, 86 | "end": { 87 | "line": 1, 88 | "column": 44, 89 | "offset": 43 90 | } 91 | } 92 | } 93 | } 94 | }, 95 | "position": { 96 | "start": { 97 | "line": 1, 98 | "column": 1, 99 | "offset": 0 100 | }, 101 | "end": { 102 | "line": 1, 103 | "column": 49, 104 | "offset": 48 105 | } 106 | } 107 | }, 108 | { 109 | "type": "text", 110 | "value": "\n\n", 111 | "position": { 112 | "start": { 113 | "line": 1, 114 | "column": 49, 115 | "offset": 48 116 | }, 117 | "end": { 118 | "line": 3, 119 | "column": 1, 120 | "offset": 50 121 | } 122 | } 123 | }, 124 | { 125 | "type": "element", 126 | "tagName": "p", 127 | "properties": { 128 | "data123": "456" 129 | }, 130 | "children": [], 131 | "data": { 132 | "position": { 133 | "opening": { 134 | "start": { 135 | "line": 3, 136 | "column": 1, 137 | "offset": 50 138 | }, 139 | "end": { 140 | "line": 3, 141 | "column": 19, 142 | "offset": 68 143 | } 144 | }, 145 | "closing": { 146 | "start": { 147 | "line": 3, 148 | "column": 19, 149 | "offset": 68 150 | }, 151 | "end": { 152 | "line": 3, 153 | "column": 23, 154 | "offset": 72 155 | } 156 | }, 157 | "properties": { 158 | "data123": { 159 | "start": { 160 | "line": 3, 161 | "column": 4, 162 | "offset": 53 163 | }, 164 | "end": { 165 | "line": 3, 166 | "column": 18, 167 | "offset": 67 168 | } 169 | } 170 | } 171 | } 172 | }, 173 | "position": { 174 | "start": { 175 | "line": 3, 176 | "column": 1, 177 | "offset": 50 178 | }, 179 | "end": { 180 | "line": 3, 181 | "column": 23, 182 | "offset": 72 183 | } 184 | } 185 | }, 186 | { 187 | "type": "text", 188 | "value": "\n\n", 189 | "position": { 190 | "start": { 191 | "line": 3, 192 | "column": 23, 193 | "offset": 72 194 | }, 195 | "end": { 196 | "line": 5, 197 | "column": 1, 198 | "offset": 74 199 | } 200 | } 201 | }, 202 | { 203 | "type": "element", 204 | "tagName": "img", 205 | "properties": { 206 | "alt": "", 207 | "hidden": true 208 | }, 209 | "children": [], 210 | "data": { 211 | "position": { 212 | "opening": { 213 | "start": { 214 | "line": 5, 215 | "column": 1, 216 | "offset": 74 217 | }, 218 | "end": { 219 | "line": 5, 220 | "column": 17, 221 | "offset": 90 222 | } 223 | }, 224 | "properties": { 225 | "alt": { 226 | "start": { 227 | "line": 5, 228 | "column": 6, 229 | "offset": 79 230 | }, 231 | "end": { 232 | "line": 5, 233 | "column": 9, 234 | "offset": 82 235 | } 236 | }, 237 | "hidden": { 238 | "start": { 239 | "line": 5, 240 | "column": 10, 241 | "offset": 83 242 | }, 243 | "end": { 244 | "line": 5, 245 | "column": 16, 246 | "offset": 89 247 | } 248 | } 249 | } 250 | } 251 | }, 252 | "position": { 253 | "start": { 254 | "line": 5, 255 | "column": 1, 256 | "offset": 74 257 | }, 258 | "end": { 259 | "line": 5, 260 | "column": 17, 261 | "offset": 90 262 | } 263 | } 264 | }, 265 | { 266 | "type": "text", 267 | "value": "\n\n", 268 | "position": { 269 | "start": { 270 | "line": 5, 271 | "column": 17, 272 | "offset": 90 273 | }, 274 | "end": { 275 | "line": 7, 276 | "column": 1, 277 | "offset": 92 278 | } 279 | } 280 | }, 281 | { 282 | "type": "element", 283 | "tagName": "a", 284 | "properties": { 285 | "download": true 286 | }, 287 | "children": [], 288 | "data": { 289 | "position": { 290 | "opening": { 291 | "start": { 292 | "line": 7, 293 | "column": 1, 294 | "offset": 92 295 | }, 296 | "end": { 297 | "line": 7, 298 | "column": 13, 299 | "offset": 104 300 | } 301 | }, 302 | "closing": { 303 | "start": { 304 | "line": 7, 305 | "column": 13, 306 | "offset": 104 307 | }, 308 | "end": { 309 | "line": 7, 310 | "column": 17, 311 | "offset": 108 312 | } 313 | }, 314 | "properties": { 315 | "download": { 316 | "start": { 317 | "line": 7, 318 | "column": 4, 319 | "offset": 95 320 | }, 321 | "end": { 322 | "line": 7, 323 | "column": 12, 324 | "offset": 103 325 | } 326 | } 327 | } 328 | } 329 | }, 330 | "position": { 331 | "start": { 332 | "line": 7, 333 | "column": 1, 334 | "offset": 92 335 | }, 336 | "end": { 337 | "line": 7, 338 | "column": 17, 339 | "offset": 108 340 | } 341 | } 342 | }, 343 | { 344 | "type": "text", 345 | "value": "\n\n", 346 | "position": { 347 | "start": { 348 | "line": 7, 349 | "column": 17, 350 | "offset": 108 351 | }, 352 | "end": { 353 | "line": 9, 354 | "column": 1, 355 | "offset": 110 356 | } 357 | } 358 | }, 359 | { 360 | "type": "element", 361 | "tagName": "a", 362 | "properties": { 363 | "download": "example.mp3" 364 | }, 365 | "children": [], 366 | "data": { 367 | "position": { 368 | "opening": { 369 | "start": { 370 | "line": 9, 371 | "column": 1, 372 | "offset": 110 373 | }, 374 | "end": { 375 | "line": 9, 376 | "column": 25, 377 | "offset": 134 378 | } 379 | }, 380 | "closing": { 381 | "start": { 382 | "line": 9, 383 | "column": 25, 384 | "offset": 134 385 | }, 386 | "end": { 387 | "line": 9, 388 | "column": 29, 389 | "offset": 138 390 | } 391 | }, 392 | "properties": { 393 | "download": { 394 | "start": { 395 | "line": 9, 396 | "column": 4, 397 | "offset": 113 398 | }, 399 | "end": { 400 | "line": 9, 401 | "column": 24, 402 | "offset": 133 403 | } 404 | } 405 | } 406 | } 407 | }, 408 | "position": { 409 | "start": { 410 | "line": 9, 411 | "column": 1, 412 | "offset": 110 413 | }, 414 | "end": { 415 | "line": 9, 416 | "column": 29, 417 | "offset": 138 418 | } 419 | } 420 | }, 421 | { 422 | "type": "text", 423 | "value": "\n", 424 | "position": { 425 | "start": { 426 | "line": 9, 427 | "column": 29, 428 | "offset": 138 429 | }, 430 | "end": { 431 | "line": 10, 432 | "column": 1, 433 | "offset": 139 434 | } 435 | } 436 | } 437 | ] 438 | } 439 | ] 440 | } 441 | ], 442 | "data": { 443 | "quirksMode": true 444 | }, 445 | "position": { 446 | "start": { 447 | "line": 1, 448 | "column": 1, 449 | "offset": 0 450 | }, 451 | "end": { 452 | "line": 10, 453 | "column": 1, 454 | "offset": 139 455 | } 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /test/fixtures/doctype-nameless/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE> 2 | -------------------------------------------------------------------------------- /test/fixtures/doctype-nameless/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "doctype", 6 | "position": { 7 | "start": { 8 | "line": 1, 9 | "column": 1, 10 | "offset": 0 11 | }, 12 | "end": { 13 | "line": 1, 14 | "column": 11, 15 | "offset": 10 16 | } 17 | } 18 | }, 19 | { 20 | "type": "element", 21 | "tagName": "html", 22 | "properties": {}, 23 | "children": [ 24 | { 25 | "type": "element", 26 | "tagName": "head", 27 | "properties": {}, 28 | "children": [] 29 | }, 30 | { 31 | "type": "element", 32 | "tagName": "body", 33 | "properties": {}, 34 | "children": [] 35 | } 36 | ] 37 | } 38 | ], 39 | "data": { 40 | "quirksMode": true 41 | }, 42 | "position": { 43 | "start": { 44 | "line": 1, 45 | "column": 1, 46 | "offset": 0 47 | }, 48 | "end": { 49 | "line": 2, 50 | "column": 1, 51 | "offset": 11 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/fixtures/doctype-quirksmode-ibm/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html SYSTEM "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"> 2 | -------------------------------------------------------------------------------- /test/fixtures/doctype-quirksmode-ibm/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "doctype", 6 | "position": { 7 | "start": { 8 | "line": 1, 9 | "column": 1, 10 | "offset": 0 11 | }, 12 | "end": { 13 | "line": 1, 14 | "column": 84, 15 | "offset": 83 16 | } 17 | } 18 | }, 19 | { 20 | "type": "element", 21 | "tagName": "html", 22 | "properties": {}, 23 | "children": [ 24 | { 25 | "type": "element", 26 | "tagName": "head", 27 | "properties": {}, 28 | "children": [] 29 | }, 30 | { 31 | "type": "element", 32 | "tagName": "body", 33 | "properties": {}, 34 | "children": [] 35 | } 36 | ] 37 | } 38 | ], 39 | "data": { 40 | "quirksMode": true 41 | }, 42 | "position": { 43 | "start": { 44 | "line": 1, 45 | "column": 1, 46 | "offset": 0 47 | }, 48 | "end": { 49 | "line": 2, 50 | "column": 1, 51 | "offset": 84 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/fixtures/doctype-quirksmode-xml/index.html: -------------------------------------------------------------------------------- 1 | <?xml version="1.0"?> 2 | -------------------------------------------------------------------------------- /test/fixtures/doctype-quirksmode-xml/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "comment", 6 | "value": "?xml version=\"1.0\"?", 7 | "position": { 8 | "start": { 9 | "line": 1, 10 | "column": 1, 11 | "offset": 0 12 | }, 13 | "end": { 14 | "line": 1, 15 | "column": 22, 16 | "offset": 21 17 | } 18 | } 19 | }, 20 | { 21 | "type": "element", 22 | "tagName": "html", 23 | "properties": {}, 24 | "children": [ 25 | { 26 | "type": "element", 27 | "tagName": "head", 28 | "properties": {}, 29 | "children": [] 30 | }, 31 | { 32 | "type": "element", 33 | "tagName": "body", 34 | "properties": {}, 35 | "children": [] 36 | } 37 | ] 38 | } 39 | ], 40 | "data": { 41 | "quirksMode": true 42 | }, 43 | "position": { 44 | "start": { 45 | "line": 1, 46 | "column": 1, 47 | "offset": 0 48 | }, 49 | "end": { 50 | "line": 2, 51 | "column": 1, 52 | "offset": 22 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/fixtures/doctype/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | -------------------------------------------------------------------------------- /test/fixtures/doctype/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "doctype", 6 | "position": { 7 | "start": { 8 | "line": 1, 9 | "column": 1, 10 | "offset": 0 11 | }, 12 | "end": { 13 | "line": 1, 14 | "column": 16, 15 | "offset": 15 16 | } 17 | } 18 | }, 19 | { 20 | "type": "element", 21 | "tagName": "html", 22 | "properties": {}, 23 | "children": [ 24 | { 25 | "type": "element", 26 | "tagName": "head", 27 | "properties": {}, 28 | "children": [] 29 | }, 30 | { 31 | "type": "element", 32 | "tagName": "body", 33 | "properties": {}, 34 | "children": [] 35 | } 36 | ] 37 | } 38 | ], 39 | "data": { 40 | "quirksMode": false 41 | }, 42 | "position": { 43 | "start": { 44 | "line": 1, 45 | "column": 1, 46 | "offset": 0 47 | }, 48 | "end": { 49 | "line": 2, 50 | "column": 1, 51 | "offset": 16 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/fixtures/element-void-close/index.html: -------------------------------------------------------------------------------- 1 | <br>text</br> 2 | <img>text</img> 3 | -------------------------------------------------------------------------------- /test/fixtures/element-void-close/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "element", 6 | "tagName": "html", 7 | "properties": {}, 8 | "children": [ 9 | { 10 | "type": "element", 11 | "tagName": "head", 12 | "properties": {}, 13 | "children": [] 14 | }, 15 | { 16 | "type": "element", 17 | "tagName": "body", 18 | "properties": {}, 19 | "children": [ 20 | { 21 | "type": "element", 22 | "tagName": "br", 23 | "properties": {}, 24 | "children": [], 25 | "data": { 26 | "position": { 27 | "opening": { 28 | "start": { 29 | "line": 1, 30 | "column": 1, 31 | "offset": 0 32 | }, 33 | "end": { 34 | "line": 1, 35 | "column": 5, 36 | "offset": 4 37 | } 38 | }, 39 | "properties": {} 40 | } 41 | }, 42 | "position": { 43 | "start": { 44 | "line": 1, 45 | "column": 1, 46 | "offset": 0 47 | }, 48 | "end": { 49 | "line": 1, 50 | "column": 5, 51 | "offset": 4 52 | } 53 | } 54 | }, 55 | { 56 | "type": "text", 57 | "value": "text", 58 | "position": { 59 | "start": { 60 | "line": 1, 61 | "column": 5, 62 | "offset": 4 63 | }, 64 | "end": { 65 | "line": 1, 66 | "column": 9, 67 | "offset": 8 68 | } 69 | } 70 | }, 71 | { 72 | "type": "element", 73 | "tagName": "br", 74 | "properties": {}, 75 | "children": [] 76 | }, 77 | { 78 | "type": "text", 79 | "value": "\n", 80 | "position": { 81 | "start": { 82 | "line": 1, 83 | "column": 14, 84 | "offset": 13 85 | }, 86 | "end": { 87 | "line": 2, 88 | "column": 1, 89 | "offset": 14 90 | } 91 | } 92 | }, 93 | { 94 | "type": "element", 95 | "tagName": "img", 96 | "properties": {}, 97 | "children": [], 98 | "data": { 99 | "position": { 100 | "opening": { 101 | "start": { 102 | "line": 2, 103 | "column": 1, 104 | "offset": 14 105 | }, 106 | "end": { 107 | "line": 2, 108 | "column": 6, 109 | "offset": 19 110 | } 111 | }, 112 | "properties": {} 113 | } 114 | }, 115 | "position": { 116 | "start": { 117 | "line": 2, 118 | "column": 1, 119 | "offset": 14 120 | }, 121 | "end": { 122 | "line": 2, 123 | "column": 6, 124 | "offset": 19 125 | } 126 | } 127 | }, 128 | { 129 | "type": "text", 130 | "value": "text\n", 131 | "position": { 132 | "start": { 133 | "line": 2, 134 | "column": 6, 135 | "offset": 19 136 | }, 137 | "end": { 138 | "line": 3, 139 | "column": 1, 140 | "offset": 30 141 | } 142 | } 143 | } 144 | ] 145 | } 146 | ] 147 | } 148 | ], 149 | "data": { 150 | "quirksMode": true 151 | }, 152 | "position": { 153 | "start": { 154 | "line": 1, 155 | "column": 1, 156 | "offset": 0 157 | }, 158 | "end": { 159 | "line": 3, 160 | "column": 1, 161 | "offset": 30 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /test/fixtures/element-void/index.html: -------------------------------------------------------------------------------- 1 | <img src="http://example.com/fav.ico" alt="foo" title="bar"> 2 | 3 | <hr> 4 | 5 | <p>this<br/>and that</p> 6 | 7 | <p>this<br>and that</p> 8 | 9 | <svg><path><circle><g><rect></svg> 10 | 11 | <svg><path/><circle/><g/><rect/></svg> 12 | 13 | <math><mglyph><mspace><malignmark></math> 14 | 15 | <math><mglyph/><mspace/><malignmark/></math> 16 | -------------------------------------------------------------------------------- /test/fixtures/element-void/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "element", 6 | "tagName": "html", 7 | "properties": {}, 8 | "children": [ 9 | { 10 | "type": "element", 11 | "tagName": "head", 12 | "properties": {}, 13 | "children": [] 14 | }, 15 | { 16 | "type": "element", 17 | "tagName": "body", 18 | "properties": {}, 19 | "children": [ 20 | { 21 | "type": "element", 22 | "tagName": "img", 23 | "properties": { 24 | "src": "http://example.com/fav.ico", 25 | "alt": "foo", 26 | "title": "bar" 27 | }, 28 | "children": [], 29 | "data": { 30 | "position": { 31 | "opening": { 32 | "start": { 33 | "line": 1, 34 | "column": 1, 35 | "offset": 0 36 | }, 37 | "end": { 38 | "line": 1, 39 | "column": 61, 40 | "offset": 60 41 | } 42 | }, 43 | "properties": { 44 | "src": { 45 | "start": { 46 | "line": 1, 47 | "column": 6, 48 | "offset": 5 49 | }, 50 | "end": { 51 | "line": 1, 52 | "column": 38, 53 | "offset": 37 54 | } 55 | }, 56 | "alt": { 57 | "start": { 58 | "line": 1, 59 | "column": 39, 60 | "offset": 38 61 | }, 62 | "end": { 63 | "line": 1, 64 | "column": 48, 65 | "offset": 47 66 | } 67 | }, 68 | "title": { 69 | "start": { 70 | "line": 1, 71 | "column": 49, 72 | "offset": 48 73 | }, 74 | "end": { 75 | "line": 1, 76 | "column": 60, 77 | "offset": 59 78 | } 79 | } 80 | } 81 | } 82 | }, 83 | "position": { 84 | "start": { 85 | "line": 1, 86 | "column": 1, 87 | "offset": 0 88 | }, 89 | "end": { 90 | "line": 1, 91 | "column": 61, 92 | "offset": 60 93 | } 94 | } 95 | }, 96 | { 97 | "type": "text", 98 | "value": "\n\n", 99 | "position": { 100 | "start": { 101 | "line": 1, 102 | "column": 61, 103 | "offset": 60 104 | }, 105 | "end": { 106 | "line": 3, 107 | "column": 1, 108 | "offset": 62 109 | } 110 | } 111 | }, 112 | { 113 | "type": "element", 114 | "tagName": "hr", 115 | "properties": {}, 116 | "children": [], 117 | "data": { 118 | "position": { 119 | "opening": { 120 | "start": { 121 | "line": 3, 122 | "column": 1, 123 | "offset": 62 124 | }, 125 | "end": { 126 | "line": 3, 127 | "column": 5, 128 | "offset": 66 129 | } 130 | }, 131 | "properties": {} 132 | } 133 | }, 134 | "position": { 135 | "start": { 136 | "line": 3, 137 | "column": 1, 138 | "offset": 62 139 | }, 140 | "end": { 141 | "line": 3, 142 | "column": 5, 143 | "offset": 66 144 | } 145 | } 146 | }, 147 | { 148 | "type": "text", 149 | "value": "\n\n", 150 | "position": { 151 | "start": { 152 | "line": 3, 153 | "column": 5, 154 | "offset": 66 155 | }, 156 | "end": { 157 | "line": 5, 158 | "column": 1, 159 | "offset": 68 160 | } 161 | } 162 | }, 163 | { 164 | "type": "element", 165 | "tagName": "p", 166 | "properties": {}, 167 | "children": [ 168 | { 169 | "type": "text", 170 | "value": "this", 171 | "position": { 172 | "start": { 173 | "line": 5, 174 | "column": 4, 175 | "offset": 71 176 | }, 177 | "end": { 178 | "line": 5, 179 | "column": 8, 180 | "offset": 75 181 | } 182 | } 183 | }, 184 | { 185 | "type": "element", 186 | "tagName": "br", 187 | "properties": {}, 188 | "children": [], 189 | "data": { 190 | "position": { 191 | "opening": { 192 | "start": { 193 | "line": 5, 194 | "column": 8, 195 | "offset": 75 196 | }, 197 | "end": { 198 | "line": 5, 199 | "column": 13, 200 | "offset": 80 201 | } 202 | }, 203 | "properties": {} 204 | } 205 | }, 206 | "position": { 207 | "start": { 208 | "line": 5, 209 | "column": 8, 210 | "offset": 75 211 | }, 212 | "end": { 213 | "line": 5, 214 | "column": 13, 215 | "offset": 80 216 | } 217 | } 218 | }, 219 | { 220 | "type": "text", 221 | "value": "and that", 222 | "position": { 223 | "start": { 224 | "line": 5, 225 | "column": 13, 226 | "offset": 80 227 | }, 228 | "end": { 229 | "line": 5, 230 | "column": 21, 231 | "offset": 88 232 | } 233 | } 234 | } 235 | ], 236 | "data": { 237 | "position": { 238 | "opening": { 239 | "start": { 240 | "line": 5, 241 | "column": 1, 242 | "offset": 68 243 | }, 244 | "end": { 245 | "line": 5, 246 | "column": 4, 247 | "offset": 71 248 | } 249 | }, 250 | "closing": { 251 | "start": { 252 | "line": 5, 253 | "column": 21, 254 | "offset": 88 255 | }, 256 | "end": { 257 | "line": 5, 258 | "column": 25, 259 | "offset": 92 260 | } 261 | }, 262 | "properties": {} 263 | } 264 | }, 265 | "position": { 266 | "start": { 267 | "line": 5, 268 | "column": 1, 269 | "offset": 68 270 | }, 271 | "end": { 272 | "line": 5, 273 | "column": 25, 274 | "offset": 92 275 | } 276 | } 277 | }, 278 | { 279 | "type": "text", 280 | "value": "\n\n", 281 | "position": { 282 | "start": { 283 | "line": 5, 284 | "column": 25, 285 | "offset": 92 286 | }, 287 | "end": { 288 | "line": 7, 289 | "column": 1, 290 | "offset": 94 291 | } 292 | } 293 | }, 294 | { 295 | "type": "element", 296 | "tagName": "p", 297 | "properties": {}, 298 | "children": [ 299 | { 300 | "type": "text", 301 | "value": "this", 302 | "position": { 303 | "start": { 304 | "line": 7, 305 | "column": 4, 306 | "offset": 97 307 | }, 308 | "end": { 309 | "line": 7, 310 | "column": 8, 311 | "offset": 101 312 | } 313 | } 314 | }, 315 | { 316 | "type": "element", 317 | "tagName": "br", 318 | "properties": {}, 319 | "children": [], 320 | "data": { 321 | "position": { 322 | "opening": { 323 | "start": { 324 | "line": 7, 325 | "column": 8, 326 | "offset": 101 327 | }, 328 | "end": { 329 | "line": 7, 330 | "column": 12, 331 | "offset": 105 332 | } 333 | }, 334 | "properties": {} 335 | } 336 | }, 337 | "position": { 338 | "start": { 339 | "line": 7, 340 | "column": 8, 341 | "offset": 101 342 | }, 343 | "end": { 344 | "line": 7, 345 | "column": 12, 346 | "offset": 105 347 | } 348 | } 349 | }, 350 | { 351 | "type": "text", 352 | "value": "and that", 353 | "position": { 354 | "start": { 355 | "line": 7, 356 | "column": 12, 357 | "offset": 105 358 | }, 359 | "end": { 360 | "line": 7, 361 | "column": 20, 362 | "offset": 113 363 | } 364 | } 365 | } 366 | ], 367 | "data": { 368 | "position": { 369 | "opening": { 370 | "start": { 371 | "line": 7, 372 | "column": 1, 373 | "offset": 94 374 | }, 375 | "end": { 376 | "line": 7, 377 | "column": 4, 378 | "offset": 97 379 | } 380 | }, 381 | "closing": { 382 | "start": { 383 | "line": 7, 384 | "column": 20, 385 | "offset": 113 386 | }, 387 | "end": { 388 | "line": 7, 389 | "column": 24, 390 | "offset": 117 391 | } 392 | }, 393 | "properties": {} 394 | } 395 | }, 396 | "position": { 397 | "start": { 398 | "line": 7, 399 | "column": 1, 400 | "offset": 94 401 | }, 402 | "end": { 403 | "line": 7, 404 | "column": 24, 405 | "offset": 117 406 | } 407 | } 408 | }, 409 | { 410 | "type": "text", 411 | "value": "\n\n", 412 | "position": { 413 | "start": { 414 | "line": 7, 415 | "column": 24, 416 | "offset": 117 417 | }, 418 | "end": { 419 | "line": 9, 420 | "column": 1, 421 | "offset": 119 422 | } 423 | } 424 | }, 425 | { 426 | "type": "element", 427 | "tagName": "svg", 428 | "properties": {}, 429 | "children": [ 430 | { 431 | "type": "element", 432 | "tagName": "path", 433 | "properties": {}, 434 | "children": [ 435 | { 436 | "type": "element", 437 | "tagName": "circle", 438 | "properties": {}, 439 | "children": [ 440 | { 441 | "type": "element", 442 | "tagName": "g", 443 | "properties": {}, 444 | "children": [ 445 | { 446 | "type": "element", 447 | "tagName": "rect", 448 | "properties": {}, 449 | "children": [], 450 | "data": { 451 | "position": { 452 | "opening": { 453 | "start": { 454 | "line": 9, 455 | "column": 23, 456 | "offset": 141 457 | }, 458 | "end": { 459 | "line": 9, 460 | "column": 29, 461 | "offset": 147 462 | } 463 | }, 464 | "properties": {} 465 | } 466 | }, 467 | "position": { 468 | "start": { 469 | "line": 9, 470 | "column": 23, 471 | "offset": 141 472 | }, 473 | "end": { 474 | "line": 9, 475 | "column": 29, 476 | "offset": 147 477 | } 478 | } 479 | } 480 | ], 481 | "data": { 482 | "position": { 483 | "opening": { 484 | "start": { 485 | "line": 9, 486 | "column": 20, 487 | "offset": 138 488 | }, 489 | "end": { 490 | "line": 9, 491 | "column": 23, 492 | "offset": 141 493 | } 494 | }, 495 | "properties": {} 496 | } 497 | }, 498 | "position": { 499 | "start": { 500 | "line": 9, 501 | "column": 20, 502 | "offset": 138 503 | }, 504 | "end": { 505 | "line": 9, 506 | "column": 29, 507 | "offset": 147 508 | } 509 | } 510 | } 511 | ], 512 | "data": { 513 | "position": { 514 | "opening": { 515 | "start": { 516 | "line": 9, 517 | "column": 12, 518 | "offset": 130 519 | }, 520 | "end": { 521 | "line": 9, 522 | "column": 20, 523 | "offset": 138 524 | } 525 | }, 526 | "properties": {} 527 | } 528 | }, 529 | "position": { 530 | "start": { 531 | "line": 9, 532 | "column": 12, 533 | "offset": 130 534 | }, 535 | "end": { 536 | "line": 9, 537 | "column": 29, 538 | "offset": 147 539 | } 540 | } 541 | } 542 | ], 543 | "data": { 544 | "position": { 545 | "opening": { 546 | "start": { 547 | "line": 9, 548 | "column": 6, 549 | "offset": 124 550 | }, 551 | "end": { 552 | "line": 9, 553 | "column": 12, 554 | "offset": 130 555 | } 556 | }, 557 | "properties": {} 558 | } 559 | }, 560 | "position": { 561 | "start": { 562 | "line": 9, 563 | "column": 6, 564 | "offset": 124 565 | }, 566 | "end": { 567 | "line": 9, 568 | "column": 29, 569 | "offset": 147 570 | } 571 | } 572 | } 573 | ], 574 | "data": { 575 | "position": { 576 | "opening": { 577 | "start": { 578 | "line": 9, 579 | "column": 1, 580 | "offset": 119 581 | }, 582 | "end": { 583 | "line": 9, 584 | "column": 6, 585 | "offset": 124 586 | } 587 | }, 588 | "closing": { 589 | "start": { 590 | "line": 9, 591 | "column": 29, 592 | "offset": 147 593 | }, 594 | "end": { 595 | "line": 9, 596 | "column": 35, 597 | "offset": 153 598 | } 599 | }, 600 | "properties": {} 601 | } 602 | }, 603 | "position": { 604 | "start": { 605 | "line": 9, 606 | "column": 1, 607 | "offset": 119 608 | }, 609 | "end": { 610 | "line": 9, 611 | "column": 35, 612 | "offset": 153 613 | } 614 | } 615 | }, 616 | { 617 | "type": "text", 618 | "value": "\n\n", 619 | "position": { 620 | "start": { 621 | "line": 9, 622 | "column": 35, 623 | "offset": 153 624 | }, 625 | "end": { 626 | "line": 11, 627 | "column": 1, 628 | "offset": 155 629 | } 630 | } 631 | }, 632 | { 633 | "type": "element", 634 | "tagName": "svg", 635 | "properties": {}, 636 | "children": [ 637 | { 638 | "type": "element", 639 | "tagName": "path", 640 | "properties": {}, 641 | "children": [], 642 | "data": { 643 | "position": { 644 | "opening": { 645 | "start": { 646 | "line": 11, 647 | "column": 6, 648 | "offset": 160 649 | }, 650 | "end": { 651 | "line": 11, 652 | "column": 13, 653 | "offset": 167 654 | } 655 | }, 656 | "properties": {} 657 | } 658 | }, 659 | "position": { 660 | "start": { 661 | "line": 11, 662 | "column": 6, 663 | "offset": 160 664 | }, 665 | "end": { 666 | "line": 11, 667 | "column": 13, 668 | "offset": 167 669 | } 670 | } 671 | }, 672 | { 673 | "type": "element", 674 | "tagName": "circle", 675 | "properties": {}, 676 | "children": [], 677 | "data": { 678 | "position": { 679 | "opening": { 680 | "start": { 681 | "line": 11, 682 | "column": 13, 683 | "offset": 167 684 | }, 685 | "end": { 686 | "line": 11, 687 | "column": 22, 688 | "offset": 176 689 | } 690 | }, 691 | "properties": {} 692 | } 693 | }, 694 | "position": { 695 | "start": { 696 | "line": 11, 697 | "column": 13, 698 | "offset": 167 699 | }, 700 | "end": { 701 | "line": 11, 702 | "column": 22, 703 | "offset": 176 704 | } 705 | } 706 | }, 707 | { 708 | "type": "element", 709 | "tagName": "g", 710 | "properties": {}, 711 | "children": [], 712 | "data": { 713 | "position": { 714 | "opening": { 715 | "start": { 716 | "line": 11, 717 | "column": 22, 718 | "offset": 176 719 | }, 720 | "end": { 721 | "line": 11, 722 | "column": 26, 723 | "offset": 180 724 | } 725 | }, 726 | "properties": {} 727 | } 728 | }, 729 | "position": { 730 | "start": { 731 | "line": 11, 732 | "column": 22, 733 | "offset": 176 734 | }, 735 | "end": { 736 | "line": 11, 737 | "column": 26, 738 | "offset": 180 739 | } 740 | } 741 | }, 742 | { 743 | "type": "element", 744 | "tagName": "rect", 745 | "properties": {}, 746 | "children": [], 747 | "data": { 748 | "position": { 749 | "opening": { 750 | "start": { 751 | "line": 11, 752 | "column": 26, 753 | "offset": 180 754 | }, 755 | "end": { 756 | "line": 11, 757 | "column": 33, 758 | "offset": 187 759 | } 760 | }, 761 | "properties": {} 762 | } 763 | }, 764 | "position": { 765 | "start": { 766 | "line": 11, 767 | "column": 26, 768 | "offset": 180 769 | }, 770 | "end": { 771 | "line": 11, 772 | "column": 33, 773 | "offset": 187 774 | } 775 | } 776 | } 777 | ], 778 | "data": { 779 | "position": { 780 | "opening": { 781 | "start": { 782 | "line": 11, 783 | "column": 1, 784 | "offset": 155 785 | }, 786 | "end": { 787 | "line": 11, 788 | "column": 6, 789 | "offset": 160 790 | } 791 | }, 792 | "closing": { 793 | "start": { 794 | "line": 11, 795 | "column": 33, 796 | "offset": 187 797 | }, 798 | "end": { 799 | "line": 11, 800 | "column": 39, 801 | "offset": 193 802 | } 803 | }, 804 | "properties": {} 805 | } 806 | }, 807 | "position": { 808 | "start": { 809 | "line": 11, 810 | "column": 1, 811 | "offset": 155 812 | }, 813 | "end": { 814 | "line": 11, 815 | "column": 39, 816 | "offset": 193 817 | } 818 | } 819 | }, 820 | { 821 | "type": "text", 822 | "value": "\n\n", 823 | "position": { 824 | "start": { 825 | "line": 11, 826 | "column": 39, 827 | "offset": 193 828 | }, 829 | "end": { 830 | "line": 13, 831 | "column": 1, 832 | "offset": 195 833 | } 834 | } 835 | }, 836 | { 837 | "type": "element", 838 | "tagName": "math", 839 | "properties": {}, 840 | "children": [ 841 | { 842 | "type": "element", 843 | "tagName": "mglyph", 844 | "properties": {}, 845 | "children": [ 846 | { 847 | "type": "element", 848 | "tagName": "mspace", 849 | "properties": {}, 850 | "children": [ 851 | { 852 | "type": "element", 853 | "tagName": "malignmark", 854 | "properties": {}, 855 | "children": [], 856 | "data": { 857 | "position": { 858 | "opening": { 859 | "start": { 860 | "line": 13, 861 | "column": 23, 862 | "offset": 217 863 | }, 864 | "end": { 865 | "line": 13, 866 | "column": 35, 867 | "offset": 229 868 | } 869 | }, 870 | "properties": {} 871 | } 872 | }, 873 | "position": { 874 | "start": { 875 | "line": 13, 876 | "column": 23, 877 | "offset": 217 878 | }, 879 | "end": { 880 | "line": 13, 881 | "column": 35, 882 | "offset": 229 883 | } 884 | } 885 | } 886 | ], 887 | "data": { 888 | "position": { 889 | "opening": { 890 | "start": { 891 | "line": 13, 892 | "column": 15, 893 | "offset": 209 894 | }, 895 | "end": { 896 | "line": 13, 897 | "column": 23, 898 | "offset": 217 899 | } 900 | }, 901 | "properties": {} 902 | } 903 | }, 904 | "position": { 905 | "start": { 906 | "line": 13, 907 | "column": 15, 908 | "offset": 209 909 | }, 910 | "end": { 911 | "line": 13, 912 | "column": 35, 913 | "offset": 229 914 | } 915 | } 916 | } 917 | ], 918 | "data": { 919 | "position": { 920 | "opening": { 921 | "start": { 922 | "line": 13, 923 | "column": 7, 924 | "offset": 201 925 | }, 926 | "end": { 927 | "line": 13, 928 | "column": 15, 929 | "offset": 209 930 | } 931 | }, 932 | "properties": {} 933 | } 934 | }, 935 | "position": { 936 | "start": { 937 | "line": 13, 938 | "column": 7, 939 | "offset": 201 940 | }, 941 | "end": { 942 | "line": 13, 943 | "column": 35, 944 | "offset": 229 945 | } 946 | } 947 | } 948 | ], 949 | "data": { 950 | "position": { 951 | "opening": { 952 | "start": { 953 | "line": 13, 954 | "column": 1, 955 | "offset": 195 956 | }, 957 | "end": { 958 | "line": 13, 959 | "column": 7, 960 | "offset": 201 961 | } 962 | }, 963 | "closing": { 964 | "start": { 965 | "line": 13, 966 | "column": 35, 967 | "offset": 229 968 | }, 969 | "end": { 970 | "line": 13, 971 | "column": 42, 972 | "offset": 236 973 | } 974 | }, 975 | "properties": {} 976 | } 977 | }, 978 | "position": { 979 | "start": { 980 | "line": 13, 981 | "column": 1, 982 | "offset": 195 983 | }, 984 | "end": { 985 | "line": 13, 986 | "column": 42, 987 | "offset": 236 988 | } 989 | } 990 | }, 991 | { 992 | "type": "text", 993 | "value": "\n\n", 994 | "position": { 995 | "start": { 996 | "line": 13, 997 | "column": 42, 998 | "offset": 236 999 | }, 1000 | "end": { 1001 | "line": 15, 1002 | "column": 1, 1003 | "offset": 238 1004 | } 1005 | } 1006 | }, 1007 | { 1008 | "type": "element", 1009 | "tagName": "math", 1010 | "properties": {}, 1011 | "children": [ 1012 | { 1013 | "type": "element", 1014 | "tagName": "mglyph", 1015 | "properties": {}, 1016 | "children": [], 1017 | "data": { 1018 | "position": { 1019 | "opening": { 1020 | "start": { 1021 | "line": 15, 1022 | "column": 7, 1023 | "offset": 244 1024 | }, 1025 | "end": { 1026 | "line": 15, 1027 | "column": 16, 1028 | "offset": 253 1029 | } 1030 | }, 1031 | "properties": {} 1032 | } 1033 | }, 1034 | "position": { 1035 | "start": { 1036 | "line": 15, 1037 | "column": 7, 1038 | "offset": 244 1039 | }, 1040 | "end": { 1041 | "line": 15, 1042 | "column": 16, 1043 | "offset": 253 1044 | } 1045 | } 1046 | }, 1047 | { 1048 | "type": "element", 1049 | "tagName": "mspace", 1050 | "properties": {}, 1051 | "children": [], 1052 | "data": { 1053 | "position": { 1054 | "opening": { 1055 | "start": { 1056 | "line": 15, 1057 | "column": 16, 1058 | "offset": 253 1059 | }, 1060 | "end": { 1061 | "line": 15, 1062 | "column": 25, 1063 | "offset": 262 1064 | } 1065 | }, 1066 | "properties": {} 1067 | } 1068 | }, 1069 | "position": { 1070 | "start": { 1071 | "line": 15, 1072 | "column": 16, 1073 | "offset": 253 1074 | }, 1075 | "end": { 1076 | "line": 15, 1077 | "column": 25, 1078 | "offset": 262 1079 | } 1080 | } 1081 | }, 1082 | { 1083 | "type": "element", 1084 | "tagName": "malignmark", 1085 | "properties": {}, 1086 | "children": [], 1087 | "data": { 1088 | "position": { 1089 | "opening": { 1090 | "start": { 1091 | "line": 15, 1092 | "column": 25, 1093 | "offset": 262 1094 | }, 1095 | "end": { 1096 | "line": 15, 1097 | "column": 38, 1098 | "offset": 275 1099 | } 1100 | }, 1101 | "properties": {} 1102 | } 1103 | }, 1104 | "position": { 1105 | "start": { 1106 | "line": 15, 1107 | "column": 25, 1108 | "offset": 262 1109 | }, 1110 | "end": { 1111 | "line": 15, 1112 | "column": 38, 1113 | "offset": 275 1114 | } 1115 | } 1116 | } 1117 | ], 1118 | "data": { 1119 | "position": { 1120 | "opening": { 1121 | "start": { 1122 | "line": 15, 1123 | "column": 1, 1124 | "offset": 238 1125 | }, 1126 | "end": { 1127 | "line": 15, 1128 | "column": 7, 1129 | "offset": 244 1130 | } 1131 | }, 1132 | "closing": { 1133 | "start": { 1134 | "line": 15, 1135 | "column": 38, 1136 | "offset": 275 1137 | }, 1138 | "end": { 1139 | "line": 15, 1140 | "column": 45, 1141 | "offset": 282 1142 | } 1143 | }, 1144 | "properties": {} 1145 | } 1146 | }, 1147 | "position": { 1148 | "start": { 1149 | "line": 15, 1150 | "column": 1, 1151 | "offset": 238 1152 | }, 1153 | "end": { 1154 | "line": 15, 1155 | "column": 45, 1156 | "offset": 282 1157 | } 1158 | } 1159 | }, 1160 | { 1161 | "type": "text", 1162 | "value": "\n", 1163 | "position": { 1164 | "start": { 1165 | "line": 15, 1166 | "column": 45, 1167 | "offset": 282 1168 | }, 1169 | "end": { 1170 | "line": 16, 1171 | "column": 1, 1172 | "offset": 283 1173 | } 1174 | } 1175 | } 1176 | ] 1177 | } 1178 | ] 1179 | } 1180 | ], 1181 | "data": { 1182 | "quirksMode": true 1183 | }, 1184 | "position": { 1185 | "start": { 1186 | "line": 1, 1187 | "column": 1, 1188 | "offset": 0 1189 | }, 1190 | "end": { 1191 | "line": 16, 1192 | "column": 1, 1193 | "offset": 283 1194 | } 1195 | } 1196 | } 1197 | -------------------------------------------------------------------------------- /test/fixtures/simple/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html><title>Hello!

World! 2 | -------------------------------------------------------------------------------- /test/fixtures/simple/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "doctype", 6 | "position": { 7 | "start": { 8 | "line": 1, 9 | "column": 1, 10 | "offset": 0 11 | }, 12 | "end": { 13 | "line": 1, 14 | "column": 16, 15 | "offset": 15 16 | } 17 | } 18 | }, 19 | { 20 | "type": "element", 21 | "tagName": "html", 22 | "properties": {}, 23 | "children": [ 24 | { 25 | "type": "element", 26 | "tagName": "head", 27 | "properties": {}, 28 | "children": [ 29 | { 30 | "type": "element", 31 | "tagName": "title", 32 | "properties": {}, 33 | "children": [ 34 | { 35 | "type": "text", 36 | "value": "Hello!", 37 | "position": { 38 | "start": { 39 | "line": 1, 40 | "column": 23, 41 | "offset": 22 42 | }, 43 | "end": { 44 | "line": 1, 45 | "column": 29, 46 | "offset": 28 47 | } 48 | } 49 | } 50 | ], 51 | "data": { 52 | "position": { 53 | "opening": { 54 | "start": { 55 | "line": 1, 56 | "column": 16, 57 | "offset": 15 58 | }, 59 | "end": { 60 | "line": 1, 61 | "column": 23, 62 | "offset": 22 63 | } 64 | }, 65 | "closing": { 66 | "start": { 67 | "line": 1, 68 | "column": 29, 69 | "offset": 28 70 | }, 71 | "end": { 72 | "line": 1, 73 | "column": 37, 74 | "offset": 36 75 | } 76 | }, 77 | "properties": {} 78 | } 79 | }, 80 | "position": { 81 | "start": { 82 | "line": 1, 83 | "column": 16, 84 | "offset": 15 85 | }, 86 | "end": { 87 | "line": 1, 88 | "column": 37, 89 | "offset": 36 90 | } 91 | } 92 | } 93 | ] 94 | }, 95 | { 96 | "type": "element", 97 | "tagName": "body", 98 | "properties": {}, 99 | "children": [ 100 | { 101 | "type": "element", 102 | "tagName": "h1", 103 | "properties": { 104 | "id": "world" 105 | }, 106 | "children": [ 107 | { 108 | "type": "text", 109 | "value": "World!", 110 | "position": { 111 | "start": { 112 | "line": 1, 113 | "column": 52, 114 | "offset": 51 115 | }, 116 | "end": { 117 | "line": 1, 118 | "column": 58, 119 | "offset": 57 120 | } 121 | } 122 | }, 123 | { 124 | "type": "comment", 125 | "value": "after", 126 | "position": { 127 | "start": { 128 | "line": 1, 129 | "column": 58, 130 | "offset": 57 131 | }, 132 | "end": { 133 | "line": 1, 134 | "column": 70, 135 | "offset": 69 136 | } 137 | } 138 | }, 139 | { 140 | "type": "text", 141 | "value": "\n", 142 | "position": { 143 | "start": { 144 | "line": 1, 145 | "column": 70, 146 | "offset": 69 147 | }, 148 | "end": { 149 | "line": 2, 150 | "column": 1, 151 | "offset": 70 152 | } 153 | } 154 | } 155 | ], 156 | "data": { 157 | "position": { 158 | "opening": { 159 | "start": { 160 | "line": 1, 161 | "column": 37, 162 | "offset": 36 163 | }, 164 | "end": { 165 | "line": 1, 166 | "column": 52, 167 | "offset": 51 168 | } 169 | }, 170 | "properties": { 171 | "id": { 172 | "start": { 173 | "line": 1, 174 | "column": 41, 175 | "offset": 40 176 | }, 177 | "end": { 178 | "line": 1, 179 | "column": 51, 180 | "offset": 50 181 | } 182 | } 183 | } 184 | } 185 | }, 186 | "position": { 187 | "start": { 188 | "line": 1, 189 | "column": 37, 190 | "offset": 36 191 | }, 192 | "end": { 193 | "line": 2, 194 | "column": 1, 195 | "offset": 70 196 | } 197 | } 198 | } 199 | ] 200 | } 201 | ] 202 | } 203 | ], 204 | "data": { 205 | "quirksMode": false 206 | }, 207 | "position": { 208 | "start": { 209 | "line": 1, 210 | "column": 1, 211 | "offset": 0 212 | }, 213 | "end": { 214 | "line": 2, 215 | "column": 1, 216 | "offset": 70 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /test/fixtures/svg/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /test/fixtures/svg/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "element", 6 | "tagName": "html", 7 | "properties": {}, 8 | "children": [ 9 | { 10 | "type": "element", 11 | "tagName": "head", 12 | "properties": {}, 13 | "children": [] 14 | }, 15 | { 16 | "type": "element", 17 | "tagName": "body", 18 | "properties": {}, 19 | "children": [ 20 | { 21 | "type": "element", 22 | "tagName": "div", 23 | "properties": {}, 24 | "children": [ 25 | { 26 | "type": "text", 27 | "value": "\n ", 28 | "position": { 29 | "start": { 30 | "line": 1, 31 | "column": 6, 32 | "offset": 5 33 | }, 34 | "end": { 35 | "line": 2, 36 | "column": 3, 37 | "offset": 8 38 | } 39 | } 40 | }, 41 | { 42 | "type": "element", 43 | "tagName": "svg", 44 | "properties": { 45 | "width": "230", 46 | "height": "120", 47 | "viewBox": "0 0 200 200", 48 | "xmlns": "http://www.w3.org/2000/svg", 49 | "xmlnsXLink": "http://www.w3.org/1999/xlink" 50 | }, 51 | "children": [ 52 | { 53 | "type": "text", 54 | "value": "\n ", 55 | "position": { 56 | "start": { 57 | "line": 2, 58 | "column": 133, 59 | "offset": 138 60 | }, 61 | "end": { 62 | "line": 3, 63 | "column": 5, 64 | "offset": 143 65 | } 66 | } 67 | }, 68 | { 69 | "type": "element", 70 | "tagName": "circle", 71 | "properties": { 72 | "cx": "60", 73 | "cy": "60", 74 | "r": "50", 75 | "fill": "red" 76 | }, 77 | "children": [], 78 | "data": { 79 | "position": { 80 | "opening": { 81 | "start": { 82 | "line": 3, 83 | "column": 5, 84 | "offset": 143 85 | }, 86 | "end": { 87 | "line": 3, 88 | "column": 49, 89 | "offset": 187 90 | } 91 | }, 92 | "properties": { 93 | "cx": { 94 | "start": { 95 | "line": 3, 96 | "column": 13, 97 | "offset": 151 98 | }, 99 | "end": { 100 | "line": 3, 101 | "column": 20, 102 | "offset": 158 103 | } 104 | }, 105 | "cy": { 106 | "start": { 107 | "line": 3, 108 | "column": 22, 109 | "offset": 160 110 | }, 111 | "end": { 112 | "line": 3, 113 | "column": 29, 114 | "offset": 167 115 | } 116 | }, 117 | "r": { 118 | "start": { 119 | "line": 3, 120 | "column": 30, 121 | "offset": 168 122 | }, 123 | "end": { 124 | "line": 3, 125 | "column": 36, 126 | "offset": 174 127 | } 128 | }, 129 | "fill": { 130 | "start": { 131 | "line": 3, 132 | "column": 37, 133 | "offset": 175 134 | }, 135 | "end": { 136 | "line": 3, 137 | "column": 47, 138 | "offset": 185 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "position": { 145 | "start": { 146 | "line": 3, 147 | "column": 5, 148 | "offset": 143 149 | }, 150 | "end": { 151 | "line": 3, 152 | "column": 49, 153 | "offset": 187 154 | } 155 | } 156 | }, 157 | { 158 | "type": "text", 159 | "value": "\n ", 160 | "position": { 161 | "start": { 162 | "line": 3, 163 | "column": 49, 164 | "offset": 187 165 | }, 166 | "end": { 167 | "line": 4, 168 | "column": 5, 169 | "offset": 192 170 | } 171 | } 172 | }, 173 | { 174 | "type": "element", 175 | "tagName": "circle", 176 | "properties": { 177 | "cx": "170", 178 | "cy": "60", 179 | "r": "50", 180 | "fill": "green" 181 | }, 182 | "children": [], 183 | "data": { 184 | "position": { 185 | "opening": { 186 | "start": { 187 | "line": 4, 188 | "column": 5, 189 | "offset": 192 190 | }, 191 | "end": { 192 | "line": 4, 193 | "column": 51, 194 | "offset": 238 195 | } 196 | }, 197 | "properties": { 198 | "cx": { 199 | "start": { 200 | "line": 4, 201 | "column": 13, 202 | "offset": 200 203 | }, 204 | "end": { 205 | "line": 4, 206 | "column": 21, 207 | "offset": 208 208 | } 209 | }, 210 | "cy": { 211 | "start": { 212 | "line": 4, 213 | "column": 22, 214 | "offset": 209 215 | }, 216 | "end": { 217 | "line": 4, 218 | "column": 29, 219 | "offset": 216 220 | } 221 | }, 222 | "r": { 223 | "start": { 224 | "line": 4, 225 | "column": 30, 226 | "offset": 217 227 | }, 228 | "end": { 229 | "line": 4, 230 | "column": 36, 231 | "offset": 223 232 | } 233 | }, 234 | "fill": { 235 | "start": { 236 | "line": 4, 237 | "column": 37, 238 | "offset": 224 239 | }, 240 | "end": { 241 | "line": 4, 242 | "column": 49, 243 | "offset": 236 244 | } 245 | } 246 | } 247 | } 248 | }, 249 | "position": { 250 | "start": { 251 | "line": 4, 252 | "column": 5, 253 | "offset": 192 254 | }, 255 | "end": { 256 | "line": 4, 257 | "column": 51, 258 | "offset": 238 259 | } 260 | } 261 | }, 262 | { 263 | "type": "text", 264 | "value": "\n ", 265 | "position": { 266 | "start": { 267 | "line": 4, 268 | "column": 51, 269 | "offset": 238 270 | }, 271 | "end": { 272 | "line": 5, 273 | "column": 3, 274 | "offset": 241 275 | } 276 | } 277 | } 278 | ], 279 | "data": { 280 | "position": { 281 | "opening": { 282 | "start": { 283 | "line": 2, 284 | "column": 3, 285 | "offset": 8 286 | }, 287 | "end": { 288 | "line": 2, 289 | "column": 133, 290 | "offset": 138 291 | } 292 | }, 293 | "closing": { 294 | "start": { 295 | "line": 5, 296 | "column": 3, 297 | "offset": 241 298 | }, 299 | "end": { 300 | "line": 5, 301 | "column": 9, 302 | "offset": 247 303 | } 304 | }, 305 | "properties": { 306 | "width": { 307 | "start": { 308 | "line": 2, 309 | "column": 8, 310 | "offset": 13 311 | }, 312 | "end": { 313 | "line": 2, 314 | "column": 19, 315 | "offset": 24 316 | } 317 | }, 318 | "height": { 319 | "start": { 320 | "line": 2, 321 | "column": 20, 322 | "offset": 25 323 | }, 324 | "end": { 325 | "line": 2, 326 | "column": 32, 327 | "offset": 37 328 | } 329 | }, 330 | "viewBox": { 331 | "start": { 332 | "line": 2, 333 | "column": 33, 334 | "offset": 38 335 | }, 336 | "end": { 337 | "line": 2, 338 | "column": 54, 339 | "offset": 59 340 | } 341 | }, 342 | "xmlns": { 343 | "start": { 344 | "line": 2, 345 | "column": 55, 346 | "offset": 60 347 | }, 348 | "end": { 349 | "line": 2, 350 | "column": 89, 351 | "offset": 94 352 | } 353 | }, 354 | "xmlnsXLink": { 355 | "start": { 356 | "line": 2, 357 | "column": 90, 358 | "offset": 95 359 | }, 360 | "end": { 361 | "line": 2, 362 | "column": 132, 363 | "offset": 137 364 | } 365 | } 366 | } 367 | } 368 | }, 369 | "position": { 370 | "start": { 371 | "line": 2, 372 | "column": 3, 373 | "offset": 8 374 | }, 375 | "end": { 376 | "line": 5, 377 | "column": 9, 378 | "offset": 247 379 | } 380 | } 381 | }, 382 | { 383 | "type": "text", 384 | "value": "\n", 385 | "position": { 386 | "start": { 387 | "line": 5, 388 | "column": 9, 389 | "offset": 247 390 | }, 391 | "end": { 392 | "line": 6, 393 | "column": 1, 394 | "offset": 248 395 | } 396 | } 397 | } 398 | ], 399 | "data": { 400 | "position": { 401 | "opening": { 402 | "start": { 403 | "line": 1, 404 | "column": 1, 405 | "offset": 0 406 | }, 407 | "end": { 408 | "line": 1, 409 | "column": 6, 410 | "offset": 5 411 | } 412 | }, 413 | "closing": { 414 | "start": { 415 | "line": 6, 416 | "column": 1, 417 | "offset": 248 418 | }, 419 | "end": { 420 | "line": 6, 421 | "column": 7, 422 | "offset": 254 423 | } 424 | }, 425 | "properties": {} 426 | } 427 | }, 428 | "position": { 429 | "start": { 430 | "line": 1, 431 | "column": 1, 432 | "offset": 0 433 | }, 434 | "end": { 435 | "line": 6, 436 | "column": 7, 437 | "offset": 254 438 | } 439 | } 440 | }, 441 | { 442 | "type": "text", 443 | "value": "\n", 444 | "position": { 445 | "start": { 446 | "line": 6, 447 | "column": 7, 448 | "offset": 254 449 | }, 450 | "end": { 451 | "line": 7, 452 | "column": 1, 453 | "offset": 255 454 | } 455 | } 456 | } 457 | ] 458 | } 459 | ] 460 | } 461 | ], 462 | "data": { 463 | "quirksMode": true 464 | }, 465 | "position": { 466 | "start": { 467 | "line": 1, 468 | "column": 1, 469 | "offset": 0 470 | }, 471 | "end": { 472 | "line": 7, 473 | "column": 1, 474 | "offset": 255 475 | } 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /test/fixtures/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | Templates and their content 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/template/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "doctype", 6 | "position": { 7 | "start": { 8 | "line": 1, 9 | "column": 1, 10 | "offset": 0 11 | }, 12 | "end": { 13 | "line": 1, 14 | "column": 16, 15 | "offset": 15 16 | } 17 | } 18 | }, 19 | { 20 | "type": "element", 21 | "tagName": "html", 22 | "properties": {}, 23 | "children": [ 24 | { 25 | "type": "element", 26 | "tagName": "head", 27 | "properties": {}, 28 | "children": [ 29 | { 30 | "type": "element", 31 | "tagName": "title", 32 | "properties": {}, 33 | "children": [ 34 | { 35 | "type": "text", 36 | "value": "Templates and their content", 37 | "position": { 38 | "start": { 39 | "line": 2, 40 | "column": 8, 41 | "offset": 23 42 | }, 43 | "end": { 44 | "line": 2, 45 | "column": 35, 46 | "offset": 50 47 | } 48 | } 49 | } 50 | ], 51 | "data": { 52 | "position": { 53 | "opening": { 54 | "start": { 55 | "line": 2, 56 | "column": 1, 57 | "offset": 16 58 | }, 59 | "end": { 60 | "line": 2, 61 | "column": 8, 62 | "offset": 23 63 | } 64 | }, 65 | "closing": { 66 | "start": { 67 | "line": 2, 68 | "column": 35, 69 | "offset": 50 70 | }, 71 | "end": { 72 | "line": 2, 73 | "column": 43, 74 | "offset": 58 75 | } 76 | }, 77 | "properties": {} 78 | } 79 | }, 80 | "position": { 81 | "start": { 82 | "line": 2, 83 | "column": 1, 84 | "offset": 16 85 | }, 86 | "end": { 87 | "line": 2, 88 | "column": 43, 89 | "offset": 58 90 | } 91 | } 92 | }, 93 | { 94 | "type": "text", 95 | "value": "\n", 96 | "position": { 97 | "start": { 98 | "line": 2, 99 | "column": 43, 100 | "offset": 58 101 | }, 102 | "end": { 103 | "line": 3, 104 | "column": 1, 105 | "offset": 59 106 | } 107 | } 108 | } 109 | ] 110 | }, 111 | { 112 | "type": "element", 113 | "tagName": "body", 114 | "properties": {}, 115 | "children": [ 116 | { 117 | "type": "text", 118 | "value": "\n", 119 | "position": { 120 | "start": { 121 | "line": 3, 122 | "column": 7, 123 | "offset": 65 124 | }, 125 | "end": { 126 | "line": 4, 127 | "column": 1, 128 | "offset": 66 129 | } 130 | } 131 | }, 132 | { 133 | "type": "element", 134 | "tagName": "template", 135 | "properties": { 136 | "id": "text" 137 | }, 138 | "children": [], 139 | "content": { 140 | "type": "root", 141 | "children": [ 142 | { 143 | "type": "text", 144 | "value": "!", 145 | "position": { 146 | "start": { 147 | "line": 4, 148 | "column": 21, 149 | "offset": 86 150 | }, 151 | "end": { 152 | "line": 4, 153 | "column": 22, 154 | "offset": 87 155 | } 156 | } 157 | } 158 | ], 159 | "data": { 160 | "quirksMode": false 161 | }, 162 | "position": { 163 | "start": { 164 | "line": 4, 165 | "column": 21, 166 | "offset": 86 167 | }, 168 | "end": { 169 | "line": 4, 170 | "column": 22, 171 | "offset": 87 172 | } 173 | } 174 | }, 175 | "data": { 176 | "position": { 177 | "opening": { 178 | "start": { 179 | "line": 4, 180 | "column": 1, 181 | "offset": 66 182 | }, 183 | "end": { 184 | "line": 4, 185 | "column": 21, 186 | "offset": 86 187 | } 188 | }, 189 | "closing": { 190 | "start": { 191 | "line": 4, 192 | "column": 22, 193 | "offset": 87 194 | }, 195 | "end": { 196 | "line": 4, 197 | "column": 33, 198 | "offset": 98 199 | } 200 | }, 201 | "properties": { 202 | "id": { 203 | "start": { 204 | "line": 4, 205 | "column": 11, 206 | "offset": 76 207 | }, 208 | "end": { 209 | "line": 4, 210 | "column": 20, 211 | "offset": 85 212 | } 213 | } 214 | } 215 | } 216 | }, 217 | "position": { 218 | "start": { 219 | "line": 4, 220 | "column": 1, 221 | "offset": 66 222 | }, 223 | "end": { 224 | "line": 4, 225 | "column": 33, 226 | "offset": 98 227 | } 228 | } 229 | }, 230 | { 231 | "type": "text", 232 | "value": "\n\n", 233 | "position": { 234 | "start": { 235 | "line": 4, 236 | "column": 33, 237 | "offset": 98 238 | }, 239 | "end": { 240 | "line": 6, 241 | "column": 1, 242 | "offset": 100 243 | } 244 | } 245 | }, 246 | { 247 | "type": "element", 248 | "tagName": "template", 249 | "properties": { 250 | "id": "html" 251 | }, 252 | "children": [], 253 | "content": { 254 | "type": "root", 255 | "children": [ 256 | { 257 | "type": "element", 258 | "tagName": "strong", 259 | "properties": {}, 260 | "children": [ 261 | { 262 | "type": "text", 263 | "value": "importance", 264 | "position": { 265 | "start": { 266 | "line": 6, 267 | "column": 29, 268 | "offset": 128 269 | }, 270 | "end": { 271 | "line": 6, 272 | "column": 39, 273 | "offset": 138 274 | } 275 | } 276 | } 277 | ], 278 | "data": { 279 | "position": { 280 | "opening": { 281 | "start": { 282 | "line": 6, 283 | "column": 21, 284 | "offset": 120 285 | }, 286 | "end": { 287 | "line": 6, 288 | "column": 29, 289 | "offset": 128 290 | } 291 | }, 292 | "closing": { 293 | "start": { 294 | "line": 6, 295 | "column": 39, 296 | "offset": 138 297 | }, 298 | "end": { 299 | "line": 6, 300 | "column": 48, 301 | "offset": 147 302 | } 303 | }, 304 | "properties": {} 305 | } 306 | }, 307 | "position": { 308 | "start": { 309 | "line": 6, 310 | "column": 21, 311 | "offset": 120 312 | }, 313 | "end": { 314 | "line": 6, 315 | "column": 48, 316 | "offset": 147 317 | } 318 | } 319 | }, 320 | { 321 | "type": "text", 322 | "value": " and ", 323 | "position": { 324 | "start": { 325 | "line": 6, 326 | "column": 48, 327 | "offset": 147 328 | }, 329 | "end": { 330 | "line": 6, 331 | "column": 53, 332 | "offset": 152 333 | } 334 | } 335 | }, 336 | { 337 | "type": "element", 338 | "tagName": "em", 339 | "properties": {}, 340 | "children": [ 341 | { 342 | "type": "text", 343 | "value": "emphasis", 344 | "position": { 345 | "start": { 346 | "line": 6, 347 | "column": 57, 348 | "offset": 156 349 | }, 350 | "end": { 351 | "line": 6, 352 | "column": 65, 353 | "offset": 164 354 | } 355 | } 356 | } 357 | ], 358 | "data": { 359 | "position": { 360 | "opening": { 361 | "start": { 362 | "line": 6, 363 | "column": 53, 364 | "offset": 152 365 | }, 366 | "end": { 367 | "line": 6, 368 | "column": 57, 369 | "offset": 156 370 | } 371 | }, 372 | "closing": { 373 | "start": { 374 | "line": 6, 375 | "column": 65, 376 | "offset": 164 377 | }, 378 | "end": { 379 | "line": 6, 380 | "column": 70, 381 | "offset": 169 382 | } 383 | }, 384 | "properties": {} 385 | } 386 | }, 387 | "position": { 388 | "start": { 389 | "line": 6, 390 | "column": 53, 391 | "offset": 152 392 | }, 393 | "end": { 394 | "line": 6, 395 | "column": 70, 396 | "offset": 169 397 | } 398 | } 399 | }, 400 | { 401 | "type": "text", 402 | "value": ".", 403 | "position": { 404 | "start": { 405 | "line": 6, 406 | "column": 70, 407 | "offset": 169 408 | }, 409 | "end": { 410 | "line": 6, 411 | "column": 71, 412 | "offset": 170 413 | } 414 | } 415 | } 416 | ], 417 | "data": { 418 | "quirksMode": false 419 | }, 420 | "position": { 421 | "start": { 422 | "line": 6, 423 | "column": 21, 424 | "offset": 120 425 | }, 426 | "end": { 427 | "line": 6, 428 | "column": 71, 429 | "offset": 170 430 | } 431 | } 432 | }, 433 | "data": { 434 | "position": { 435 | "opening": { 436 | "start": { 437 | "line": 6, 438 | "column": 1, 439 | "offset": 100 440 | }, 441 | "end": { 442 | "line": 6, 443 | "column": 21, 444 | "offset": 120 445 | } 446 | }, 447 | "closing": { 448 | "start": { 449 | "line": 6, 450 | "column": 71, 451 | "offset": 170 452 | }, 453 | "end": { 454 | "line": 6, 455 | "column": 82, 456 | "offset": 181 457 | } 458 | }, 459 | "properties": { 460 | "id": { 461 | "start": { 462 | "line": 6, 463 | "column": 11, 464 | "offset": 110 465 | }, 466 | "end": { 467 | "line": 6, 468 | "column": 20, 469 | "offset": 119 470 | } 471 | } 472 | } 473 | } 474 | }, 475 | "position": { 476 | "start": { 477 | "line": 6, 478 | "column": 1, 479 | "offset": 100 480 | }, 481 | "end": { 482 | "line": 6, 483 | "column": 82, 484 | "offset": 181 485 | } 486 | } 487 | }, 488 | { 489 | "type": "text", 490 | "value": "\n", 491 | "position": { 492 | "start": { 493 | "line": 6, 494 | "column": 82, 495 | "offset": 181 496 | }, 497 | "end": { 498 | "line": 7, 499 | "column": 1, 500 | "offset": 182 501 | } 502 | } 503 | } 504 | ], 505 | "data": { 506 | "position": { 507 | "opening": { 508 | "start": { 509 | "line": 3, 510 | "column": 1, 511 | "offset": 59 512 | }, 513 | "end": { 514 | "line": 3, 515 | "column": 7, 516 | "offset": 65 517 | } 518 | }, 519 | "properties": {} 520 | } 521 | }, 522 | "position": { 523 | "start": { 524 | "line": 3, 525 | "column": 1, 526 | "offset": 59 527 | }, 528 | "end": { 529 | "line": 7, 530 | "column": 1, 531 | "offset": 182 532 | } 533 | } 534 | } 535 | ] 536 | } 537 | ], 538 | "data": { 539 | "quirksMode": false 540 | }, 541 | "position": { 542 | "start": { 543 | "line": 1, 544 | "column": 1, 545 | "offset": 0 546 | }, 547 | "end": { 548 | "line": 7, 549 | "column": 1, 550 | "offset": 182 551 | } 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Nodes} from 'hast' 3 | * @import {html as Html} from 'parse5' 4 | * @import {VFile} from 'vfile' 5 | */ 6 | 7 | /** 8 | * @typedef Config 9 | * @property {VFile} file 10 | * @property {URL} out 11 | */ 12 | 13 | import assert from 'node:assert/strict' 14 | import fs from 'node:fs/promises' 15 | import test from 'node:test' 16 | import {fromParse5} from 'hast-util-from-parse5' 17 | import {isHidden} from 'is-hidden' 18 | import {parse, parseFragment} from 'parse5' 19 | import {read, toVFile} from 'to-vfile' 20 | import {visit} from 'unist-util-visit' 21 | 22 | test('fromParse5', async function (t) { 23 | const file = toVFile({value: 'Hello!

World!'}) 24 | 25 | await t.test('should expose the public api', async function () { 26 | assert.deepEqual( 27 | Object.keys(await import('hast-util-from-parse5')).sort(), 28 | ['fromParse5'] 29 | ) 30 | }) 31 | 32 | await t.test('should transform a complete document', async function () { 33 | assert.deepEqual(fromParse5(parse(String(file))), { 34 | type: 'root', 35 | children: [ 36 | { 37 | type: 'element', 38 | tagName: 'html', 39 | properties: {}, 40 | children: [ 41 | { 42 | type: 'element', 43 | tagName: 'head', 44 | properties: {}, 45 | children: [ 46 | { 47 | type: 'element', 48 | tagName: 'title', 49 | properties: {}, 50 | children: [{type: 'text', value: 'Hello!'}] 51 | } 52 | ] 53 | }, 54 | { 55 | type: 'element', 56 | tagName: 'body', 57 | properties: {}, 58 | children: [ 59 | { 60 | type: 'element', 61 | tagName: 'h1', 62 | properties: {}, 63 | children: [{type: 'text', value: 'World!'}] 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | ], 70 | data: {quirksMode: true} 71 | }) 72 | }) 73 | 74 | await t.test('should transform a fragment', async function () { 75 | assert.deepEqual(fromParse5(parseFragment(String(file))), { 76 | type: 'root', 77 | children: [ 78 | { 79 | type: 'element', 80 | tagName: 'title', 81 | properties: {}, 82 | children: [{type: 'text', value: 'Hello!'}] 83 | }, 84 | { 85 | type: 'element', 86 | tagName: 'h1', 87 | properties: {}, 88 | children: [{type: 'text', value: 'World!'}] 89 | } 90 | ], 91 | data: {quirksMode: false} 92 | }) 93 | }) 94 | 95 | await t.test('should support synthetic locations', async function () { 96 | assert.deepEqual( 97 | fromParse5( 98 | { 99 | nodeName: 'title', 100 | tagName: 'title', 101 | attrs: [], 102 | namespaceURI: /** @type {Html.NS} */ ('http://www.w3.org/1999/xhtml'), 103 | childNodes: [ 104 | { 105 | nodeName: '#text', 106 | value: 'Hello!', 107 | // @ts-expect-error: check how the runtime handles an empty object. 108 | sourceCodeLocation: {} 109 | } 110 | ], 111 | // @ts-expect-error: check how the runtime handles partial locations. 112 | sourceCodeLocation: { 113 | startLine: 1, 114 | startCol: 1, 115 | startOffset: 0 116 | } 117 | }, 118 | {file} 119 | ), 120 | { 121 | type: 'element', 122 | tagName: 'title', 123 | properties: {}, 124 | children: [ 125 | { 126 | type: 'text', 127 | value: 'Hello!' 128 | } 129 | ], 130 | position: {start: {line: 1, column: 1, offset: 0}, end: undefined} 131 | } 132 | ) 133 | }) 134 | 135 | await t.test( 136 | 'should support synthetic locations on unclosed elements', 137 | async function () { 138 | assert.deepEqual( 139 | fromParse5( 140 | { 141 | nodeName: 'p', 142 | tagName: 'p', 143 | attrs: [], 144 | namespaceURI: /** @type {Html.NS} */ ( 145 | 'http://www.w3.org/1999/xhtml' 146 | ), 147 | childNodes: [ 148 | { 149 | nodeName: '#text', 150 | value: 'Hello!', 151 | parentNode: null, 152 | sourceCodeLocation: { 153 | startLine: 1, 154 | startCol: 4, 155 | startOffset: 3, 156 | endLine: 1, 157 | endCol: 10, 158 | endOffset: 9 159 | } 160 | } 161 | ], 162 | // @ts-expect-error: check how the runtime handles partial locations. 163 | sourceCodeLocation: { 164 | startLine: 1, 165 | startCol: 1, 166 | startOffset: 0 167 | } 168 | }, 169 | {file} 170 | ), 171 | { 172 | type: 'element', 173 | tagName: 'p', 174 | properties: {}, 175 | children: [ 176 | { 177 | type: 'text', 178 | value: 'Hello!', 179 | position: { 180 | start: {line: 1, column: 4, offset: 3}, 181 | end: {line: 1, column: 10, offset: 9} 182 | } 183 | } 184 | ], 185 | position: { 186 | start: {line: 1, column: 1, offset: 0}, 187 | end: {line: 1, column: 10, offset: 9} 188 | } 189 | } 190 | ) 191 | } 192 | ) 193 | 194 | await t.test('should transform svg', async function () { 195 | assert.deepEqual( 196 | fromParse5( 197 | parseFragment( 198 | [ 199 | '', 200 | '', 201 | '' 202 | ].join('\n') 203 | ), 204 | {space: 'svg'} 205 | ), 206 | { 207 | type: 'root', 208 | children: [ 209 | { 210 | type: 'element', 211 | tagName: 'svg', 212 | properties: { 213 | width: '230', 214 | height: '120', 215 | viewBox: '0 0 200 200', 216 | xmlns: 'http://www.w3.org/2000/svg', 217 | xmlnsXLink: 'http://www.w3.org/1999/xlink' 218 | }, 219 | children: [ 220 | {type: 'text', value: '\n'}, 221 | { 222 | type: 'element', 223 | tagName: 'circle', 224 | properties: {cx: '60', cy: '60', r: '50', fill: 'red'}, 225 | children: [] 226 | }, 227 | {type: 'text', value: '\n'} 228 | ] 229 | } 230 | ], 231 | data: {quirksMode: false} 232 | } 233 | ) 234 | }) 235 | 236 | await t.test('should ignore prototypal props', async function () { 237 | assert.deepEqual( 238 | fromParse5(parseFragment(''), {space: 'svg'}), 239 | { 240 | type: 'root', 241 | children: [ 242 | {type: 'element', tagName: 'x', properties: {y: ''}, children: []} 243 | ], 244 | data: {quirksMode: false} 245 | } 246 | ) 247 | }) 248 | 249 | await t.test('should handle unknown attributes', async function () { 250 | assert.deepEqual( 251 | fromParse5(parseFragment('')), 252 | { 253 | type: 'root', 254 | children: [ 255 | { 256 | type: 'element', 257 | tagName: 'button', 258 | properties: { 259 | type: 'other', 260 | disabled: true 261 | }, 262 | children: [ 263 | { 264 | type: 'text', 265 | value: 'Hello' 266 | } 267 | ] 268 | } 269 | ], 270 | data: { 271 | quirksMode: false 272 | } 273 | } 274 | ) 275 | }) 276 | }) 277 | 278 | test('fixtures', async function (t) { 279 | const base = new URL('fixtures/', import.meta.url) 280 | const folders = await fs.readdir(base) 281 | let index = -1 282 | 283 | while (++index < folders.length) { 284 | const folder = folders[index] 285 | 286 | if (isHidden(folder)) { 287 | continue 288 | } 289 | 290 | await t.test(folder, async function () { 291 | const file = await read(new URL(folder + '/index.html', base)) 292 | const out = new URL(folder + '/index.json', base) 293 | const config = {file, out} 294 | await checkYesYes(config) 295 | await checkNoYes(config) 296 | await checkYesNo(config) 297 | await checkNoNo(config) 298 | }) 299 | } 300 | }) 301 | 302 | /** 303 | * @param {Config} options 304 | */ 305 | async function checkYesYes(options) { 306 | const input = parse(String(options.file), { 307 | sourceCodeLocationInfo: true 308 | }) 309 | const actual = fromParse5(input, {file: options.file, verbose: true}) 310 | /** @type {Nodes} */ 311 | let expected 312 | 313 | try { 314 | expected = JSON.parse(String(await fs.readFile(options.out))) 315 | } catch { 316 | // New fixture. 317 | await fs.writeFile(options.out, JSON.stringify(actual, null, 2) + '\n') 318 | return 319 | } 320 | 321 | assert.deepEqual( 322 | actual, 323 | expected, 324 | 'p5 w/ position, hast w/ intent of position' 325 | ) 326 | } 327 | 328 | /** 329 | * @param {Config} options 330 | */ 331 | async function checkNoYes(options) { 332 | const input = parse(String(options.file)) 333 | const actual = fromParse5(input, {file: options.file, verbose: true}) 334 | /** @type {Nodes} */ 335 | const expected = JSON.parse(String(await fs.readFile(options.out))) 336 | 337 | clean(expected) 338 | 339 | assert.deepEqual( 340 | actual, 341 | expected, 342 | 'p5 w/o position, hast w/ intent of position' 343 | ) 344 | } 345 | 346 | /** 347 | * @param {Config} options 348 | */ 349 | async function checkYesNo(options) { 350 | const input = parse(String(options.file), { 351 | sourceCodeLocationInfo: true 352 | }) 353 | const actual = fromParse5(input) 354 | /** @type {Nodes} */ 355 | const expected = JSON.parse(String(await fs.readFile(options.out))) 356 | 357 | clean(expected) 358 | 359 | assert.deepEqual( 360 | actual, 361 | expected, 362 | 'p5 w/ position, hast w/o intent of position' 363 | ) 364 | } 365 | 366 | /** 367 | * @param {Config} options 368 | */ 369 | async function checkNoNo(options) { 370 | const input = parse(String(options.file)) 371 | const actual = fromParse5(input) 372 | /** @type {Nodes} */ 373 | const expected = JSON.parse(String(await fs.readFile(options.out))) 374 | 375 | clean(expected) 376 | 377 | assert.deepEqual( 378 | actual, 379 | expected, 380 | 'p5 w/o position, hast w/o intent of position' 381 | ) 382 | } 383 | 384 | /** 385 | * @param {Nodes} tree 386 | */ 387 | function clean(tree) { 388 | visit(tree, function (node) { 389 | delete node.position 390 | 391 | // Remove verbose data. 392 | if (node.type === 'element') { 393 | delete node.data 394 | 395 | if (node.content) { 396 | clean(node.content) 397 | } 398 | } 399 | }) 400 | } 401 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js", "index.d.ts"] 16 | } 17 | --------------------------------------------------------------------------------