syntax-tree / hast-util-from-parse5

Compare d43f3ef ... +24 ... c5c6819

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,340 @@
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').Parent} Parent
7 +
 * @typedef {import('hast').Element} Element
8 +
 * @typedef {import('hast').Root} Root
9 +
 * @typedef {import('hast').Text} Text
10 +
 * @typedef {import('hast').Comment} Comment
11 +
 * @typedef {import('hast').DocType} Doctype
12 +
 * @typedef {Parent['children'][number]} Child
13 +
 * @typedef {Element['children'][number]} ElementChild
14 +
 * @typedef {Child|Root} Node
15 +
 * @typedef {import('parse5').Document} P5Document
16 +
 * @typedef {import('parse5').DocumentType} P5Doctype
17 +
 * @typedef {import('parse5').CommentNode} P5Comment
18 +
 * @typedef {import('parse5').TextNode} P5Text
19 +
 * @typedef {import('parse5').Element} P5Element
20 +
 * @typedef {import('parse5').ElementLocation} P5ElementLocation
21 +
 * @typedef {import('parse5').Location} P5Location
22 +
 * @typedef {import('parse5').Attribute} P5Attribute
23 +
 * @typedef {import('parse5').Node} P5Node
24 +
 *
25 +
 * @typedef {'html'|'svg'} Space
26 +
 *
27 +
 * @callback Handler
28 +
 * @param {Context} ctx
29 +
 * @param {P5Node} node
30 +
 * @param {Array.<Child>} [children]
31 +
 * @returns {Node}
32 +
 *
33 +
 * @typedef Options
34 +
 * @property {Space} [space='html'] Whether the root of the tree is in the `'html'` or `'svg'` space. If an element in with the SVG namespace is found in `ast`, `fromParse5` automatically switches to the SVG space when entering the element, and switches back when leaving
35 +
 * @property {VFile} [file] `VFile`, used to add positional information to nodes. If given, the file should have the original HTML source as its contents
36 +
 * @property {boolean} [verbose=false] Whether to add extra positional information about starting tags, closing tags, and attributes to elements. Note: not used without `file`
37 +
 *
38 +
 * @typedef Context
39 +
 * @property {Schema} schema
40 +
 * @property {VFile|undefined} file
41 +
 * @property {boolean|undefined} verbose
42 +
 * @property {boolean} location
43 +
 */
44 +
45 +
import {h, s} from 'hastscript'
46 +
import {html, svg, find} from 'property-information'
47 +
import {location} from 'vfile-location'
48 +
import {webNamespaces} from 'web-namespaces'
49 +
50 +
const own = {}.hasOwnProperty
51 +
52 +
// Handlers.
53 +
const map = {
54 +
  '#document': root,
55 +
  '#document-fragment': root,
56 +
  '#text': text,
57 +
  '#comment': comment,
58 +
  '#documentType': doctype
59 +
}
60 +
61 +
/**
62 +
 * Transform Parse5’s AST to a hast tree.
63 +
 *
64 +
 * @param {P5Node} ast
65 +
 * @param {Options|VFile} [options]
66 +
 */
67 +
export function fromParse5(ast, options = {}) {
68 +
  /** @type {Options} */
69 +
  let settings
70 +
  /** @type {VFile|undefined} */
71 +
  let file
72 +
73 +
  if (isFile(options)) {
74 +
    file = options
75 +
    settings = {}
76 +
  } else {
77 +
    file = options.file
78 +
    settings = options
79 +
  }
80 +
81 +
  return transform(
82 +
    {
83 +
      schema: settings.space === 'svg' ? svg : html,
84 +
      file,
85 +
      verbose: settings.verbose,
86 +
      location: false
87 +
    },
88 +
    ast
89 +
  )
90 +
}
91 +
92 +
/**
93 +
 * Transform children.
94 +
 *
95 +
 * @param {Context} ctx
96 +
 * @param {P5Node} ast
97 +
 * @returns {Node}
98 +
 */
99 +
function transform(ctx, ast) {
100 +
  const schema = ctx.schema
101 +
  /** @type {Handler} */
102 +
  // @ts-expect-error: index is fine.
103 +
  const fn = own.call(map, ast.nodeName) ? map[ast.nodeName] : element
104 +
  /** @type {Array.<Child>|undefined} */
105 +
  let children
106 +
107 +
  // Element.
108 +
  if ('tagName' in ast) {
109 +
    ctx.schema = ast.namespaceURI === webNamespaces.svg ? svg : html
110 +
  }
111 +
112 +
  if ('childNodes' in ast) {
113 +
    children = nodes(ctx, ast.childNodes)
114 +
  }
115 +
116 +
  const result = fn(ctx, ast, children)
117 +
118 +
  if ('sourceCodeLocation' in ast && ast.sourceCodeLocation && ctx.file) {
119 +
    // @ts-expect-error It’s fine.
120 +
    const position = createLocation(ctx, result, ast.sourceCodeLocation)
121 +
122 +
    if (position) {
123 +
      ctx.location = true
124 +
      result.position = position
125 +
    }
126 +
  }
127 +
128 +
  ctx.schema = schema
129 +
130 +
  return result
131 +
}
132 +
133 +
/**
134 +
 * Transform children.
135 +
 *
136 +
 * @param {Context} ctx
137 +
 * @param {Array.<P5Node>} children
138 +
 * @returns {Array.<Child>}
139 +
 */
140 +
function nodes(ctx, children) {
141 +
  let index = -1
142 +
  /** @type {Array.<Child>} */
143 +
  const result = []
144 +
145 +
  while (++index < children.length) {
146 +
    // @ts-expect-error Assume no roots in children.
147 +
    result[index] = transform(ctx, children[index])
148 +
  }
149 +
150 +
  return result
151 +
}
152 +
153 +
/**
154 +
 * Transform a document.
155 +
 * Stores `ast.quirksMode` in `node.data.quirksMode`.
156 +
 *
157 +
 * @type {Handler}
158 +
 * @param {P5Document} ast
159 +
 * @param {Array.<Child>} children
160 +
 * @returns {Root}
161 +
 */
162 +
function root(ctx, ast, children) {
163 +
  /** @type {Root} */
164 +
  const result = {
165 +
    type: 'root',
166 +
    children,
167 +
    data: {quirksMode: ast.mode === 'quirks' || ast.mode === 'limited-quirks'}
168 +
  }
169 +
170 +
  if (ctx.file && ctx.location) {
171 +
    const doc = String(ctx.file)
172 +
    const loc = location(doc)
173 +
    result.position = {
174 +
      start: loc.toPoint(0),
175 +
      end: loc.toPoint(doc.length)
176 +
    }
177 +
  }
178 +
179 +
  return result
180 +
}
181 +
182 +
/**
183 +
 * Transform a doctype.
184 +
 *
185 +
 * @type {Handler}
186 +
 * @returns {Doctype}
187 +
 */
188 +
function doctype() {
189 +
  // @ts-expect-error Types are out of date.
190 +
  return {type: 'doctype'}
191 +
}
192 +
193 +
/**
194 +
 * Transform a text.
195 +
 *
196 +
 * @type {Handler}
197 +
 * @param {P5Text} ast
198 +
 * @returns {Text}
199 +
 */
200 +
function text(_, ast) {
201 +
  return {type: 'text', value: ast.value}
202 +
}
203 +
204 +
/**
205 +
 * Transform a comment.
206 +
 *
207 +
 * @type {Handler}
208 +
 * @param {P5Comment} ast
209 +
 * @returns {Comment}
210 +
 */
211 +
function comment(_, ast) {
212 +
  return {type: 'comment', value: ast.data}
213 +
}
214 +
215 +
/**
216 +
 * Transform an element.
217 +
 *
218 +
 * @type {Handler}
219 +
 * @param {P5Element} ast
220 +
 * @param {Array.<ElementChild>} children
221 +
 * @returns {Element}
222 +
 */
223 +
function element(ctx, ast, children) {
224 +
  const fn = ctx.schema.space === 'svg' ? s : h
225 +
  let index = -1
226 +
  /** @type {Object.<string, string>} */
227 +
  const props = {}
228 +
229 +
  while (++index < ast.attrs.length) {
230 +
    const attribute = ast.attrs[index]
231 +
    props[(attribute.prefix ? attribute.prefix + ':' : '') + attribute.name] =
232 +
      attribute.value
233 +
  }
234 +
235 +
  const result = fn(ast.tagName, props, children)
236 +
237 +
  if (result.tagName === 'template' && 'content' in ast) {
238 +
    const pos = ast.sourceCodeLocation
239 +
    const startTag = pos && pos.startTag && position(pos.startTag)
240 +
    const endTag = pos && pos.endTag && position(pos.endTag)
241 +
242 +
    /** @type {Root} */
243 +
    // @ts-expect-error Types are wrong.
244 +
    const content = transform(ctx, ast.content)
245 +
246 +
    if (startTag && endTag && ctx.file) {
247 +
      content.position = {start: startTag.end, end: endTag.start}
248 +
    }
249 +
250 +
    result.content = content
251 +
  }
252 +
253 +
  return result
254 +
}
255 +
256 +
/**
257 +
 * Create clean positional information.
258 +
 *
259 +
 * @param {Context} ctx
260 +
 * @param {Node} node
261 +
 * @param {P5ElementLocation} location
262 +
 * @returns {Position|null}
263 +
 */
264 +
function createLocation(ctx, node, location) {
265 +
  const result = position(location)
266 +
267 +
  if (node.type === 'element') {
268 +
    const tail = node.children[node.children.length - 1]
269 +
270 +
    // Bug for unclosed with children.
271 +
    // See: <https://github.com/inikulin/parse5/issues/109>.
272 +
    if (
273 +
      result &&
274 +
      !location.endTag &&
275 +
      tail &&
276 +
      tail.position &&
277 +
      tail.position.end
278 +
    ) {
279 +
      result.end = Object.assign({}, tail.position.end)
280 +
    }
281 +
282 +
    if (ctx.verbose) {
283 +
      /** @type {Object.<string, Position|null>} */
284 +
      const props = {}
285 +
      /** @type {string} */
286 +
      let key
287 +
288 +
      for (key in location.attrs) {
289 +
        if (own.call(location.attrs, key)) {
290 +
          props[find(ctx.schema, key).property] = position(location.attrs[key])
291 +
        }
292 +
      }
293 +
294 +
      node.data = {
295 +
        position: {
296 +
          opening: position(location.startTag),
297 +
          closing: location.endTag ? position(location.endTag) : null,
298 +
          properties: props
299 +
        }
300 +
      }
301 +
    }
302 +
  }
303 +
304 +
  return result
305 +
}
306 +
307 +
/**
308 +
 * @param {P5Location} loc
309 +
 * @returns {Position|null}
310 +
 */
311 +
function position(loc) {
312 +
  const start = point({
313 +
    line: loc.startLine,
314 +
    column: loc.startCol,
315 +
    offset: loc.startOffset
316 +
  })
317 +
  const end = point({
318 +
    line: loc.endLine,
319 +
    column: loc.endCol,
320 +
    offset: loc.endOffset
321 +
  })
322 +
  // @ts-expect-error `null` is fine.
323 +
  return start || end ? {start, end} : null
324 +
}
325 +
326 +
/**
327 +
 * @param {Point} point
328 +
 * @returns {Point|null}
329 +
 */
330 +
function point(point) {
331 +
  return point.line && point.column ? point : null
332 +
}
333 +
334 +
/**
335 +
 * @param {VFile|Options} value
336 +
 * @returns {value is VFile}
337 +
 */
338 +
function isFile(value) {
339 +
  return 'messages' in value
340 +
}

@@ -1,234 +1,5 @@
Loading
1 -
'use strict'
1 +
/**
2 +
 * @typedef {import('./lib/index.js').Options} Options
3 +
 */
2 4
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 -
}
5 +
export {fromParse5} from './lib/index.js'

Learn more Showing 1 files with coverage changes found.

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

26 Commits

Files Coverage
index.js 100.00%
lib/index.js 100.00%
Project Totals (2 files) 100.00%
Loading