index.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /**
  2. * @import {
  3. * Comment as HastComment,
  4. * Doctype as HastDoctype,
  5. * Element as HastElement,
  6. * Nodes as HastNodes,
  7. * RootContent as HastRootContent,
  8. * Root as HastRoot,
  9. * Text as HastText,
  10. * } from 'hast'
  11. */
  12. /**
  13. * @callback AfterTransform
  14. * Callback called when each node is transformed.
  15. * @param {Node} domNode
  16. * DOM node that was handled.
  17. * @param {HastNodes} hastNode
  18. * Corresponding hast node.
  19. * @returns {undefined | void}
  20. * Nothing.
  21. *
  22. * Note: `void` included until TS infers `undefined` nicely.
  23. *
  24. * @typedef Options
  25. * Configuration.
  26. * @property {AfterTransform | null | undefined} [afterTransform]
  27. * Callback called when each node is transformed (optional).
  28. */
  29. import {h, s} from 'hastscript'
  30. import {webNamespaces} from 'web-namespaces'
  31. /**
  32. * Transform a DOM tree to a hast tree.
  33. *
  34. * @param {Node} tree
  35. * DOM tree to transform.
  36. * @param {Options | null | undefined} [options]
  37. * Configuration (optional).
  38. * @returns {HastNodes}
  39. * Equivalent hast node.
  40. */
  41. export function fromDom(tree, options) {
  42. return transform(tree, options || {}) || {type: 'root', children: []}
  43. }
  44. /**
  45. * @param {Node} node
  46. * DOM node to transform.
  47. * @param {Options} options
  48. * Configuration.
  49. * @returns {HastNodes | undefined}
  50. * Equivalent hast node.
  51. *
  52. * Note that certain legacy DOM nodes (i.e., Attr nodes (2), CDATA, processing instructions)
  53. */
  54. function transform(node, options) {
  55. const transformed = one(node, options)
  56. if (transformed && options.afterTransform)
  57. options.afterTransform(node, transformed)
  58. return transformed
  59. }
  60. /**
  61. * @param {Node} node
  62. * DOM node to transform.
  63. * @param {Options} options
  64. * Configuration.
  65. * @returns {HastNodes | undefined}
  66. * Equivalent hast node.
  67. */
  68. function one(node, options) {
  69. switch (node.nodeType) {
  70. case 1 /* Element */: {
  71. const domNode = /** @type {Element} */ (node)
  72. return element(domNode, options)
  73. }
  74. // Ignore: Attr (2).
  75. case 3 /* Text */: {
  76. const domNode = /** @type {Text} */ (node)
  77. return text(domNode)
  78. }
  79. // Ignore: CDATA (4).
  80. // Removed: Entity reference (5)
  81. // Removed: Entity (6)
  82. // Ignore: Processing instruction (7).
  83. case 8 /* Comment */: {
  84. const domNode = /** @type {Comment} */ (node)
  85. return comment(domNode)
  86. }
  87. case 9 /* Document */: {
  88. const domNode = /** @type {Document} */ (node)
  89. return root(domNode, options)
  90. }
  91. case 10 /* Document type */: {
  92. return doctype()
  93. }
  94. case 11 /* Document fragment */: {
  95. const domNode = /** @type {DocumentFragment} */ (node)
  96. return root(domNode, options)
  97. }
  98. default: {
  99. return undefined
  100. }
  101. }
  102. }
  103. /**
  104. * Transform a document.
  105. *
  106. * @param {Document | DocumentFragment} node
  107. * DOM node to transform.
  108. * @param {Options} options
  109. * Configuration.
  110. * @returns {HastRoot}
  111. * Equivalent hast node.
  112. */
  113. function root(node, options) {
  114. return {type: 'root', children: all(node, options)}
  115. }
  116. /**
  117. * Transform a doctype.
  118. *
  119. * @returns {HastDoctype}
  120. * Equivalent hast node.
  121. */
  122. function doctype() {
  123. return {type: 'doctype'}
  124. }
  125. /**
  126. * Transform a text.
  127. *
  128. * @param {Text} node
  129. * DOM node to transform.
  130. * @returns {HastText}
  131. * Equivalent hast node.
  132. */
  133. function text(node) {
  134. return {type: 'text', value: node.nodeValue || ''}
  135. }
  136. /**
  137. * Transform a comment.
  138. *
  139. * @param {Comment} node
  140. * DOM node to transform.
  141. * @returns {HastComment}
  142. * Equivalent hast node.
  143. */
  144. function comment(node) {
  145. return {type: 'comment', value: node.nodeValue || ''}
  146. }
  147. /**
  148. * Transform an element.
  149. *
  150. * @param {Element} node
  151. * DOM node to transform.
  152. * @param {Options} options
  153. * Configuration.
  154. * @returns {HastElement}
  155. * Equivalent hast node.
  156. */
  157. function element(node, options) {
  158. const space = node.namespaceURI
  159. const x = space === webNamespaces.svg ? s : h
  160. const tagName =
  161. space === webNamespaces.html ? node.tagName.toLowerCase() : node.tagName
  162. /** @type {DocumentFragment | Element} */
  163. const content =
  164. // @ts-expect-error: DOM types are wrong, content can exist.
  165. space === webNamespaces.html && tagName === 'template' ? node.content : node
  166. const attributes = node.getAttributeNames()
  167. /** @type {Record<string, string>} */
  168. const properties = {}
  169. let index = -1
  170. while (++index < attributes.length) {
  171. properties[attributes[index]] = node.getAttribute(attributes[index]) || ''
  172. }
  173. return x(tagName, properties, all(content, options))
  174. }
  175. /**
  176. * Transform child nodes in a parent.
  177. *
  178. * @param {Document | DocumentFragment | Element} node
  179. * DOM node to transform.
  180. * @param {Options} options
  181. * Configuration.
  182. * @returns {Array<HastRootContent>}
  183. * Equivalent hast nodes.
  184. */
  185. function all(node, options) {
  186. const nodes = node.childNodes
  187. /** @type {Array<HastRootContent>} */
  188. const children = []
  189. let index = -1
  190. while (++index < nodes.length) {
  191. const child = transform(nodes[index], options)
  192. if (child !== undefined) {
  193. // @ts-expect-error Assume no document inside document.
  194. children.push(child)
  195. }
  196. }
  197. return children
  198. }