index.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. * @typedef State
  11. * Info passed around about the current state.
  12. * @property {VFile | undefined} file
  13. * Corresponding file.
  14. * @property {boolean} location
  15. * Whether location info was found.
  16. * @property {Schema} schema
  17. * Current schema.
  18. * @property {boolean | undefined} verbose
  19. * Add extra positional info.
  20. */
  21. import {ok as assert} from 'devlop'
  22. import {h, s} from 'hastscript'
  23. import {find, html, svg} from 'property-information'
  24. import {location} from 'vfile-location'
  25. import {webNamespaces} from 'web-namespaces'
  26. const own = {}.hasOwnProperty
  27. /** @type {unknown} */
  28. // type-coverage:ignore-next-line
  29. const proto = Object.prototype
  30. /**
  31. * Transform a `parse5` AST to hast.
  32. *
  33. * @param {DefaultTreeAdapterMap['node']} tree
  34. * `parse5` tree to transform.
  35. * @param {Options | null | undefined} [options]
  36. * Configuration (optional).
  37. * @returns {Nodes}
  38. * hast tree.
  39. */
  40. export function fromParse5(tree, options) {
  41. const settings = options || {}
  42. return one(
  43. {
  44. file: settings.file || undefined,
  45. location: false,
  46. schema: settings.space === 'svg' ? svg : html,
  47. verbose: settings.verbose || false
  48. },
  49. tree
  50. )
  51. }
  52. /**
  53. * Transform a node.
  54. *
  55. * @param {State} state
  56. * Info passed around about the current state.
  57. * @param {DefaultTreeAdapterMap['node']} node
  58. * p5 node.
  59. * @returns {Nodes}
  60. * hast node.
  61. */
  62. function one(state, node) {
  63. /** @type {Nodes} */
  64. let result
  65. switch (node.nodeName) {
  66. case '#comment': {
  67. const reference = /** @type {DefaultTreeAdapterMap['commentNode']} */ (
  68. node
  69. )
  70. result = {type: 'comment', value: reference.data}
  71. patch(state, reference, result)
  72. return result
  73. }
  74. case '#document':
  75. case '#document-fragment': {
  76. const reference =
  77. /** @type {DefaultTreeAdapterMap['document'] | DefaultTreeAdapterMap['documentFragment']} */ (
  78. node
  79. )
  80. const quirksMode =
  81. 'mode' in reference
  82. ? reference.mode === 'quirks' || reference.mode === 'limited-quirks'
  83. : false
  84. result = {
  85. type: 'root',
  86. children: all(state, node.childNodes),
  87. data: {quirksMode}
  88. }
  89. if (state.file && state.location) {
  90. const document = String(state.file)
  91. const loc = location(document)
  92. const start = loc.toPoint(0)
  93. const end = loc.toPoint(document.length)
  94. // Always defined as we give valid input.
  95. assert(start, 'expected `start`')
  96. assert(end, 'expected `end`')
  97. result.position = {start, end}
  98. }
  99. return result
  100. }
  101. case '#documentType': {
  102. const reference = /** @type {DefaultTreeAdapterMap['documentType']} */ (
  103. node
  104. )
  105. result = {type: 'doctype'}
  106. patch(state, reference, result)
  107. return result
  108. }
  109. case '#text': {
  110. const reference = /** @type {DefaultTreeAdapterMap['textNode']} */ (node)
  111. result = {type: 'text', value: reference.value}
  112. patch(state, reference, result)
  113. return result
  114. }
  115. // Element.
  116. default: {
  117. const reference = /** @type {DefaultTreeAdapterMap['element']} */ (node)
  118. result = element(state, reference)
  119. return result
  120. }
  121. }
  122. }
  123. /**
  124. * Transform children.
  125. *
  126. * @param {State} state
  127. * Info passed around about the current state.
  128. * @param {Array<DefaultTreeAdapterMap['node']>} nodes
  129. * Nodes.
  130. * @returns {Array<RootContent>}
  131. * hast nodes.
  132. */
  133. function all(state, nodes) {
  134. let index = -1
  135. /** @type {Array<RootContent>} */
  136. const results = []
  137. while (++index < nodes.length) {
  138. // Assume no roots in `nodes`.
  139. const result = /** @type {RootContent} */ (one(state, nodes[index]))
  140. results.push(result)
  141. }
  142. return results
  143. }
  144. /**
  145. * Transform an element.
  146. *
  147. * @param {State} state
  148. * Info passed around about the current state.
  149. * @param {DefaultTreeAdapterMap['element']} node
  150. * `parse5` node to transform.
  151. * @returns {Element}
  152. * hast node.
  153. */
  154. function element(state, node) {
  155. const schema = state.schema
  156. state.schema = node.namespaceURI === webNamespaces.svg ? svg : html
  157. // Props.
  158. let index = -1
  159. /** @type {Record<string, string>} */
  160. const properties = {}
  161. while (++index < node.attrs.length) {
  162. const attribute = node.attrs[index]
  163. const name =
  164. (attribute.prefix ? attribute.prefix + ':' : '') + attribute.name
  165. if (!own.call(proto, name)) {
  166. properties[name] = attribute.value
  167. }
  168. }
  169. // Build.
  170. const x = state.schema.space === 'svg' ? s : h
  171. const result = x(node.tagName, properties, all(state, node.childNodes))
  172. patch(state, node, result)
  173. // Switch content.
  174. if (result.tagName === 'template') {
  175. const reference = /** @type {DefaultTreeAdapterMap['template']} */ (node)
  176. const pos = reference.sourceCodeLocation
  177. const startTag = pos && pos.startTag && position(pos.startTag)
  178. const endTag = pos && pos.endTag && position(pos.endTag)
  179. // Root in, root out.
  180. const content = /** @type {Root} */ (one(state, reference.content))
  181. if (startTag && endTag && state.file) {
  182. content.position = {start: startTag.end, end: endTag.start}
  183. }
  184. result.content = content
  185. }
  186. state.schema = schema
  187. return result
  188. }
  189. /**
  190. * Patch positional info from `from` onto `to`.
  191. *
  192. * @param {State} state
  193. * Info passed around about the current state.
  194. * @param {DefaultTreeAdapterMap['node']} from
  195. * p5 node.
  196. * @param {Nodes} to
  197. * hast node.
  198. * @returns {undefined}
  199. * Nothing.
  200. */
  201. function patch(state, from, to) {
  202. if ('sourceCodeLocation' in from && from.sourceCodeLocation && state.file) {
  203. const position = createLocation(state, to, from.sourceCodeLocation)
  204. if (position) {
  205. state.location = true
  206. to.position = position
  207. }
  208. }
  209. }
  210. /**
  211. * Create clean positional information.
  212. *
  213. * @param {State} state
  214. * Info passed around about the current state.
  215. * @param {Nodes} node
  216. * hast node.
  217. * @param {Token.ElementLocation} location
  218. * p5 location info.
  219. * @returns {Position | undefined}
  220. * Position, or nothing.
  221. */
  222. function createLocation(state, node, location) {
  223. const result = position(location)
  224. if (node.type === 'element') {
  225. const tail = node.children[node.children.length - 1]
  226. // Bug for unclosed with children.
  227. // See: <https://github.com/inikulin/parse5/issues/109>.
  228. if (
  229. result &&
  230. !location.endTag &&
  231. tail &&
  232. tail.position &&
  233. tail.position.end
  234. ) {
  235. result.end = Object.assign({}, tail.position.end)
  236. }
  237. if (state.verbose) {
  238. /** @type {Record<string, Position | undefined>} */
  239. const properties = {}
  240. /** @type {string} */
  241. let key
  242. if (location.attrs) {
  243. for (key in location.attrs) {
  244. if (own.call(location.attrs, key)) {
  245. properties[find(state.schema, key).property] = position(
  246. location.attrs[key]
  247. )
  248. }
  249. }
  250. }
  251. assert(location.startTag, 'a start tag should exist')
  252. const opening = position(location.startTag)
  253. const closing = location.endTag ? position(location.endTag) : undefined
  254. /** @type {ElementData['position']} */
  255. const data = {opening}
  256. if (closing) data.closing = closing
  257. data.properties = properties
  258. node.data = {position: data}
  259. }
  260. }
  261. return result
  262. }
  263. /**
  264. * Turn a p5 location into a position.
  265. *
  266. * @param {Token.Location} loc
  267. * Location.
  268. * @returns {Position | undefined}
  269. * Position or nothing.
  270. */
  271. function position(loc) {
  272. const start = point({
  273. line: loc.startLine,
  274. column: loc.startCol,
  275. offset: loc.startOffset
  276. })
  277. const end = point({
  278. line: loc.endLine,
  279. column: loc.endCol,
  280. offset: loc.endOffset
  281. })
  282. // @ts-expect-error: we do use `undefined` for points if one or the other
  283. // exists.
  284. return start || end ? {start, end} : undefined
  285. }
  286. /**
  287. * Filter out invalid points.
  288. *
  289. * @param {Point} point
  290. * Point with potentially `undefined` values.
  291. * @returns {Point | undefined}
  292. * Point or nothing.
  293. */
  294. function point(point) {
  295. return point.line && point.column ? point : undefined
  296. }