syntax-tree / hast-util-from-parse5

Compare d43f3ef ... +43 ... 5b70c18

Coverage Reach
lib/index.js index.js

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.


@@ -0,0 +1,375 @@
Loading
1 +
/**
2 +
 * @typedef {import('vfile').VFile} VFile
3 +
 * @typedef {import('property-information').Schema} Schema
4 +
 * @typedef {import('unist').Position} Position
5 +
 * @typedef {import('unist').Point} Point
6 +
 * @typedef {import('hast').Element} Element
7 +
 * @typedef {import('hast').Root} Root
8 +
 * @typedef {import('hast').Content} Content
9 +
 * @typedef {import('parse5').DefaultTreeAdapterMap} DefaultTreeAdapterMap
10 +
 * @typedef {import('parse5').Token.ElementLocation} P5ElementLocation
11 +
 * @typedef {import('parse5').Token.Location} P5Location
12 +
 */
13 +
14 +
/**
15 +
 * @typedef {Content | Root} Node
16 +
 * @typedef {DefaultTreeAdapterMap['document']} P5Document
17 +
 * @typedef {DefaultTreeAdapterMap['documentFragment']} P5DocumentFragment
18 +
 * @typedef {DefaultTreeAdapterMap['documentType']} P5DocumentType
19 +
 * @typedef {DefaultTreeAdapterMap['commentNode']} P5Comment
20 +
 * @typedef {DefaultTreeAdapterMap['textNode']} P5Text
21 +
 * @typedef {DefaultTreeAdapterMap['element']} P5Element
22 +
 * @typedef {DefaultTreeAdapterMap['node']} P5Node
23 +
 * @typedef {DefaultTreeAdapterMap['template']} P5Template
24 +
 *
25 +
 * @typedef {'html' | 'svg'} Space
26 +
 *   Namespace.
27 +
 *
28 +
 * @typedef Options
29 +
 *   Configuration.
30 +
 * @property {Space | null | undefined} [space='html']
31 +
 *   Which space the document is in.
32 +
 *
33 +
 *   When an `<svg>` element is found in the HTML space, this package already
34 +
 *   automatically switches to and from the SVG space when entering and exiting
35 +
 *   it.
36 +
 * @property {VFile | null | undefined} [file]
37 +
 *   File used to add positional info to nodes.
38 +
 *
39 +
 *   If given, the file should represent the original HTML source.
40 +
 * @property {boolean} [verbose=false]
41 +
 *   Whether to add extra positional info about starting tags, closing tags,
42 +
 *   and attributes to elements.
43 +
 *
44 +
 *   > 👉 **Note**: only used when `file` is given.
45 +
 *
46 +
 * @typedef State
47 +
 *   Info passed around about the current state.
48 +
 * @property {Schema} schema
49 +
 *   Current schema.
50 +
 * @property {VFile | undefined} file
51 +
 *   Corresponding file.
52 +
 * @property {boolean | undefined} verbose
53 +
 *   Add extra positional info.
54 +
 * @property {boolean} location
55 +
 *   Whether location info was found.
56 +
 */
57 +
58 +
import {h, s} from 'hastscript'
59 +
import {html, svg, find} from 'property-information'
60 +
import {location} from 'vfile-location'
61 +
import {webNamespaces} from 'web-namespaces'
62 +
63 +
const own = {}.hasOwnProperty
64 +
65 +
/**
66 +
 * Transform a `parse5` AST to hast.
67 +
 *
68 +
 * @param {P5Node} tree
69 +
 *   `parse5` tree to transform.
70 +
 * @param {Options | VFile | null | undefined} [options]
71 +
 *   Configuration.
72 +
 * @returns {Node}
73 +
 *   hast tree.
74 +
 */
75 +
export function fromParse5(tree, options) {
76 +
  const options_ = options || {}
77 +
  /** @type {Options} */
78 +
  let settings
79 +
  /** @type {VFile | undefined} */
80 +
  let file
81 +
82 +
  if (isFile(options_)) {
83 +
    file = options_
84 +
    settings = {}
85 +
  } else {
86 +
    file = options_.file || undefined
87 +
    settings = options_
88 +
  }
89 +
90 +
  return one(
91 +
    {
92 +
      schema: settings.space === 'svg' ? svg : html,
93 +
      file,
94 +
      verbose: settings.verbose,
95 +
      location: false
96 +
    },
97 +
    tree
98 +
  )
99 +
}
100 +
101 +
/**
102 +
 * Transform a node.
103 +
 *
104 +
 * @param {State} state
105 +
 *   Info passed around about the current state.
106 +
 * @param {P5Node} node
107 +
 *   p5 node.
108 +
 * @returns {Node}
109 +
 *   hast node.
110 +
 */
111 +
function one(state, node) {
112 +
  /** @type {Node} */
113 +
  let result
114 +
115 +
  switch (node.nodeName) {
116 +
    case '#comment': {
117 +
      const reference = /** @type {P5Comment} */ (node)
118 +
      result = {type: 'comment', value: reference.data}
119 +
      patch(state, reference, result)
120 +
      return result
121 +
    }
122 +
123 +
    case '#document':
124 +
    case '#document-fragment': {
125 +
      const reference = /** @type {P5Document | P5DocumentFragment} */ (node)
126 +
      const quirksMode =
127 +
        'mode' in reference
128 +
          ? reference.mode === 'quirks' || reference.mode === 'limited-quirks'
129 +
          : false
130 +
131 +
      result = {
132 +
        type: 'root',
133 +
        children: all(state, node.childNodes),
134 +
        data: {quirksMode}
135 +
      }
136 +
137 +
      if (state.file && state.location) {
138 +
        const doc = String(state.file)
139 +
        const loc = location(doc)
140 +
        result.position = {start: loc.toPoint(0), end: loc.toPoint(doc.length)}
141 +
      }
142 +
143 +
      return result
144 +
    }
145 +
146 +
    case '#documentType': {
147 +
      const reference = /** @type {P5DocumentType} */ (node)
148 +
      // @ts-expect-error Types are out of date.
149 +
      result = {type: 'doctype'}
150 +
      patch(state, reference, result)
151 +
      return result
152 +
    }
153 +
154 +
    case '#text': {
155 +
      const reference = /** @type {P5Text} */ (node)
156 +
      result = {type: 'text', value: reference.value}
157 +
      patch(state, reference, result)
158 +
      return result
159 +
    }
160 +
161 +
    // Element.
162 +
    default: {
163 +
      const reference = /** @type {P5Element} */ (node)
164 +
      result = element(state, reference)
165 +
      return result
166 +
    }
167 +
  }
168 +
}
169 +
170 +
/**
171 +
 * Transform children.
172 +
 *
173 +
 * @param {State} state
174 +
 *   Info passed around about the current state.
175 +
 * @param {Array<P5Node>} nodes
176 +
 *   Nodes.
177 +
 * @returns {Array<Content>}
178 +
 *   hast nodes.
179 +
 */
180 +
function all(state, nodes) {
181 +
  let index = -1
182 +
  /** @type {Array<Content>} */
183 +
  const result = []
184 +
185 +
  while (++index < nodes.length) {
186 +
    // @ts-expect-error Assume no roots in `nodes`.
187 +
    result[index] = one(state, nodes[index])
188 +
  }
189 +
190 +
  return result
191 +
}
192 +
193 +
/**
194 +
 * Transform an element.
195 +
 *
196 +
 * @param {State} state
197 +
 *   Info passed around about the current state.
198 +
 * @param {P5Element} node
199 +
 *   `parse5` node to transform.
200 +
 * @returns {Element}
201 +
 *   hast node.
202 +
 */
203 +
function element(state, node) {
204 +
  const schema = state.schema
205 +
206 +
  state.schema = node.namespaceURI === webNamespaces.svg ? svg : html
207 +
208 +
  // Props.
209 +
  let index = -1
210 +
  /** @type {Record<string, string>} */
211 +
  const props = {}
212 +
213 +
  while (++index < node.attrs.length) {
214 +
    const attribute = node.attrs[index]
215 +
    props[(attribute.prefix ? attribute.prefix + ':' : '') + attribute.name] =
216 +
      attribute.value
217 +
  }
218 +
219 +
  // Build.
220 +
  const fn = state.schema.space === 'svg' ? s : h
221 +
  const result = fn(node.tagName, props, all(state, node.childNodes))
222 +
  patch(state, node, result)
223 +
224 +
  // Switch content.
225 +
  if (result.tagName === 'template') {
226 +
    const reference = /** @type {P5Template} */ (node)
227 +
    const pos = reference.sourceCodeLocation
228 +
    const startTag = pos && pos.startTag && position(pos.startTag)
229 +
    const endTag = pos && pos.endTag && position(pos.endTag)
230 +
231 +
    /** @type {Root} */
232 +
    // @ts-expect-error Types are wrong.
233 +
    const content = one(state, reference.content)
234 +
235 +
    if (startTag && endTag && state.file) {
236 +
      content.position = {start: startTag.end, end: endTag.start}
237 +
    }
238 +
239 +
    result.content = content
240 +
  }
241 +
242 +
  state.schema = schema
243 +
244 +
  return result
245 +
}
246 +
247 +
/**
248 +
 * Patch positional info from `from` onto `to`.
249 +
 *
250 +
 * @param {State} state
251 +
 *   Info passed around about the current state.
252 +
 * @param {P5Node} from
253 +
 *   p5 node.
254 +
 * @param {Node} to
255 +
 *   hast node.
256 +
 * @returns {void}
257 +
 *   Nothing.
258 +
 */
259 +
function patch(state, from, to) {
260 +
  if ('sourceCodeLocation' in from && from.sourceCodeLocation && state.file) {
261 +
    const position = createLocation(state, to, from.sourceCodeLocation)
262 +
263 +
    if (position) {
264 +
      state.location = true
265 +
      to.position = position
266 +
    }
267 +
  }
268 +
}
269 +
270 +
/**
271 +
 * Create clean positional information.
272 +
 *
273 +
 * @param {State} state
274 +
 *   Info passed around about the current state.
275 +
 * @param {Node} node
276 +
 *   hast node.
277 +
 * @param {P5ElementLocation} location
278 +
 *   p5 location info.
279 +
 * @returns {Position | undefined}
280 +
 *   Position, or nothing.
281 +
 */
282 +
function createLocation(state, node, location) {
283 +
  const result = position(location)
284 +
285 +
  if (node.type === 'element') {
286 +
    const tail = node.children[node.children.length - 1]
287 +
288 +
    // Bug for unclosed with children.
289 +
    // See: <https://github.com/inikulin/parse5/issues/109>.
290 +
    if (
291 +
      result &&
292 +
      !location.endTag &&
293 +
      tail &&
294 +
      tail.position &&
295 +
      tail.position.end
296 +
    ) {
297 +
      result.end = Object.assign({}, tail.position.end)
298 +
    }
299 +
300 +
    if (state.verbose) {
301 +
      /** @type {Record<string, Position | undefined>} */
302 +
      const props = {}
303 +
      /** @type {string} */
304 +
      let key
305 +
306 +
      if (location.attrs) {
307 +
        for (key in location.attrs) {
308 +
          if (own.call(location.attrs, key)) {
309 +
            props[find(state.schema, key).property] = position(
310 +
              location.attrs[key]
311 +
            )
312 +
          }
313 +
        }
314 +
      }
315 +
316 +
      node.data = {
317 +
        position: {
318 +
          // @ts-expect-error: assume not `undefined`.
319 +
          opening: position(location.startTag),
320 +
          closing: location.endTag ? position(location.endTag) : null,
321 +
          properties: props
322 +
        }
323 +
      }
324 +
    }
325 +
  }
326 +
327 +
  return result
328 +
}
329 +
330 +
/**
331 +
 * Turn a p5 location into a position.
332 +
 *
333 +
 * @param {P5Location} loc
334 +
 *   Location.
335 +
 * @returns {Position | undefined}
336 +
 *   Position or nothing.
337 +
 */
338 +
function position(loc) {
339 +
  const start = point({
340 +
    line: loc.startLine,
341 +
    column: loc.startCol,
342 +
    offset: loc.startOffset
343 +
  })
344 +
  const end = point({
345 +
    line: loc.endLine,
346 +
    column: loc.endCol,
347 +
    offset: loc.endOffset
348 +
  })
349 +
  // @ts-expect-error `undefined` is fine.
350 +
  return start || end ? {start, end} : undefined
351 +
}
352 +
353 +
/**
354 +
 * Filter out invalid points.
355 +
 *
356 +
 * @param {Point} point
357 +
 *   Point with potentially `undefined` values.
358 +
 * @returns {Point | undefined}
359 +
 *   Point or nothing.
360 +
 */
361 +
function point(point) {
362 +
  return point.line && point.column ? point : undefined
363 +
}
364 +
365 +
/**
366 +
 * Check if something is a file.
367 +
 *
368 +
 * @param {VFile | Options} value
369 +
 *   File or options.
370 +
 * @returns {value is VFile}
371 +
 *   Whether `value` is a file.
372 +
 */
373 +
function isFile(value) {
374 +
  return 'messages' in value
375 +
}

@@ -1,234 +1,6 @@
Loading
1 -
'use strict'
1 +
/**
2 +
 * @typedef {import('./lib/index.js').Options} Options
3 +
 * @typedef {import('./lib/index.js').Space} Space
4 +
 */
2 5
3 -
var html = require('property-information/html')
4 -
var svg = require('property-information/svg')
5 -
var find = require('property-information/find')
6 -
var ns = require('web-namespaces')
7 -
var s = require('hastscript/svg')
8 -
var h = require('hastscript')
9 -
var count = require('ccount')
10 -
11 -
module.exports = wrapper
12 -
13 -
var own = {}.hasOwnProperty
14 -
15 -
// Handlers.
16 -
var map = {
17 -
  '#document': root,
18 -
  '#document-fragment': root,
19 -
  '#text': text,
20 -
  '#comment': comment,
21 -
  '#documentType': doctype
22 -
}
23 -
24 -
// Wrapper to normalise options.
25 -
function wrapper(ast, options) {
26 -
  var settings = options || {}
27 -
  var file
28 -
29 -
  if (settings.messages) {
30 -
    file = settings
31 -
    settings = {}
32 -
  } else {
33 -
    file = settings.file
34 -
  }
35 -
36 -
  return transform(ast, {
37 -
    schema: settings.space === 'svg' ? svg : html,
38 -
    file: file,
39 -
    verbose: settings.verbose,
40 -
    location: false
41 -
  })
42 -
}
43 -
44 -
// Transform a node.
45 -
function transform(ast, config) {
46 -
  var schema = config.schema
47 -
  var fn = own.call(map, ast.nodeName) ? map[ast.nodeName] : element
48 -
  var children
49 -
  var node
50 -
  var pos
51 -
52 -
  if (fn === element) {
53 -
    config.schema = ast.namespaceURI === ns.svg ? svg : html
54 -
  }
55 -
56 -
  if (ast.childNodes) {
57 -
    children = nodes(ast.childNodes, config)
58 -
  }
59 -
60 -
  node = fn(ast, children, config)
61 -
62 -
  if (ast.sourceCodeLocation && config.file) {
63 -
    pos = location(node, ast.sourceCodeLocation, config)
64 -
65 -
    if (pos) {
66 -
      config.location = true
67 -
      node.position = pos
68 -
    }
69 -
  }
70 -
71 -
  config.schema = schema
72 -
73 -
  return node
74 -
}
75 -
76 -
// Transform children.
77 -
function nodes(children, config) {
78 -
  var length = children.length
79 -
  var index = -1
80 -
  var result = []
81 -
82 -
  while (++index < length) {
83 -
    result[index] = transform(children[index], config)
84 -
  }
85 -
86 -
  return result
87 -
}
88 -
89 -
// Transform a document.
90 -
// Stores `ast.quirksMode` in `node.data.quirksMode`.
91 -
function root(ast, children, config) {
92 -
  var node = {type: 'root', children: children, data: {}}
93 -
  var doc
94 -
95 -
  node.data.quirksMode = ast.mode === 'quirks' || ast.mode === 'limited-quirks'
96 -
97 -
  if (config.file && config.location) {
98 -
    doc = String(config.file)
99 -
100 -
    node.position = {
101 -
      start: {line: 1, column: 1, offset: 0},
102 -
      end: {
103 -
        line: count(doc, '\n') + 1,
104 -
        column: doc.length - doc.lastIndexOf('\n'),
105 -
        offset: doc.length
106 -
      }
107 -
    }
108 -
  }
109 -
110 -
  return node
111 -
}
112 -
113 -
// Transform a doctype.
114 -
function doctype(ast) {
115 -
  return {
116 -
    type: 'doctype',
117 -
    name: ast.name || '',
118 -
    public: ast.publicId || null,
119 -
    system: ast.systemId || null
120 -
  }
121 -
}
122 -
123 -
// Transform a text.
124 -
function text(ast) {
125 -
  return {type: 'text', value: ast.value}
126 -
}
127 -
128 -
// Transform a comment.
129 -
function comment(ast) {
130 -
  return {type: 'comment', value: ast.data}
131 -
}
132 -
133 -
// Transform an element.
134 -
function element(ast, children, config) {
135 -
  var fn = config.schema.space === 'svg' ? s : h
136 -
  var name = ast.tagName
137 -
  var attributes = ast.attrs
138 -
  var length = attributes.length
139 -
  var props = {}
140 -
  var index = -1
141 -
  var attribute
142 -
  var prop
143 -
  var node
144 -
  var pos
145 -
  var start
146 -
  var end
147 -
148 -
  while (++index < length) {
149 -
    attribute = attributes[index]
150 -
    prop = (attribute.prefix ? attribute.prefix + ':' : '') + attribute.name
151 -
    props[prop] = attribute.value
152 -
  }
153 -
154 -
  node = fn(name, props, children)
155 -
156 -
  if (name === 'template' && 'content' in ast) {
157 -
    pos = ast.sourceCodeLocation
158 -
    start = pos && pos.startTag && position(pos.startTag).end
159 -
    end = pos && pos.endTag && position(pos.endTag).start
160 -
161 -
    node.content = transform(ast.content, config)
162 -
163 -
    if ((start || end) && config.file) {
164 -
      node.content.position = {start: start, end: end}
165 -
    }
166 -
  }
167 -
168 -
  return node
169 -
}
170 -
171 -
// Create clean positional information.
172 -
function location(node, location, config) {
173 -
  var schema = config.schema
174 -
  var verbose = config.verbose
175 -
  var pos = position(location)
176 -
  var reference
177 -
  var attributes
178 -
  var attribute
179 -
  var props
180 -
  var prop
181 -
182 -
  if (node.type === 'element') {
183 -
    reference = node.children[node.children.length - 1]
184 -
185 -
    // Bug for unclosed with children.
186 -
    // See: <https://github.com/inikulin/parse5/issues/109>.
187 -
    if (
188 -
      !location.endTag &&
189 -
      reference &&
190 -
      reference.position &&
191 -
      reference.position.end
192 -
    ) {
193 -
      pos.end = Object.assign({}, reference.position.end)
194 -
    }
195 -
196 -
    if (verbose) {
197 -
      attributes = location.attrs
198 -
      props = {}
199 -
200 -
      for (attribute in attributes) {
201 -
        prop = find(schema, attribute).property
202 -
        props[prop] = position(attributes[attribute])
203 -
      }
204 -
205 -
      node.data = {
206 -
        position: {
207 -
          opening: position(location.startTag),
208 -
          closing: location.endTag ? position(location.endTag) : null,
209 -
          properties: props
210 -
        }
211 -
      }
212 -
    }
213 -
  }
214 -
215 -
  return pos
216 -
}
217 -
218 -
function position(loc) {
219 -
  var start = point({
220 -
    line: loc.startLine,
221 -
    column: loc.startCol,
222 -
    offset: loc.startOffset
223 -
  })
224 -
  var end = point({
225 -
    line: loc.endLine,
226 -
    column: loc.endCol,
227 -
    offset: loc.endOffset
228 -
  })
229 -
  return start || end ? {start: start, end: end} : null
230 -
}
231 -
232 -
function point(point) {
233 -
  return point.line && point.column ? point : null
234 -
}
6 +
export {fromParse5} from './lib/index.js'

Learn more Showing 1 files with coverage changes found.

New file lib/index.js
New
Loading file...

45 Commits

Hiding 9 contexual commits
+31
+31
Hiding 1 contexual commits Hiding 4 contexual commits
+5
+5
Hiding 4 contexual commits
-12
-12
Hiding 5 contexual commits
+1 Files
+283
+283
Hiding 7 contexual commits
-9
-9
Hiding 1 contexual commits
Files Coverage
index.js 100.00%
lib/index.js 100.00%
Project Totals (2 files) 100.00%
Loading