diff --git a/docs/reference/serializers/html.md b/docs/reference/serializers/html.md index 75bdf0748..93dc1d9b7 100644 --- a/docs/reference/serializers/html.md +++ b/docs/reference/serializers/html.md @@ -12,6 +12,8 @@ For an example of the `Html` serializer in action, check out the [`paste-html` e - [Example](#example) - [Properties](#properties) - [`rules`](#rules) + - [`defaultBlockType`](#defaultblocktype) + - [`parseHtml`](#parsehtml) - [Methods](#methods) - [`deserialize`](#deserialize) - [`serialize`](#serialize) @@ -42,6 +44,15 @@ new Html({ An array of rules to initialize the `Html` serializer with, defining your schema. +### `defaultBlockType` +`String|Object` + +A default block type for blocks which do not match any rule. Can be a string such as `paragraph` or an object with a `type` attribute such as `{ type: 'paragraph' }`. + +### `parseHtml` +`Function` + +A function to parse an HTML string and return a DOM object. Defaults to using the native `DOMParser` in browser environments that support it. For older browsers or server-side rendering, you can include the [parse5](https://www.npmjs.com/package/parse5) package and pass `parse5.parseFragment` as the `parseHtml` option. ## Methods @@ -75,9 +86,9 @@ Each rule must define two properties: #### `rule.deserialize` -`rule.deserialize(el: CheerioElement, next: Function) => Object || Void` +`rule.deserialize(el: Element, next: Function) => Object || Void` -The `deserialize` function should return a plain Javascript object representing the deserialized state, or nothing if the rule in question doesn't know how to deserialize the object, in which case the next rule in the stack will be attempted. +The `deserialize` function receives a DOM element and should return a plain Javascript object representing the deserialized state, or nothing if the rule in question doesn't know how to deserialize the object, in which case the next rule in the stack will be attempted. The returned object is almost exactly equivalent to the objects returned by the [`Raw`](./raw.md) serializer, except an extra `kind: 'mark'` is added to account for the ability to nest marks. diff --git a/docs/walkthroughs/saving-and-loading-html-content.md b/docs/walkthroughs/saving-and-loading-html-content.md index 69a1ebc39..7552d7efe 100644 --- a/docs/walkthroughs/saving-and-loading-html-content.md +++ b/docs/walkthroughs/saving-and-loading-html-content.md @@ -58,7 +58,7 @@ const rules = [ return { kind: 'block', type: 'paragraph', - nodes: next(el.children) + nodes: next(el.childNodes) } } } @@ -68,7 +68,7 @@ const rules = [ If you've worked with the [`Raw`](../reference/serializers/raw.md) serializer before, the return value of the `deserialize` should look familiar! It's just the same raw JSON format. -The `el` argument that the `deserialize` function receives is just a [`cheerio`](https://github.com/cheeriojs/cheerio) element object. And the `next` argument is a function that will deserialize any `cheerio` element(s) we pass it, which is how you recurse through each nodes children. +The `el` argument that the `deserialize` function receives is just a DOM element. And the `next` argument is a function that will deserialize any element(s) we pass it, which is how you recurse through each node's children. Okay, that's `deserialize`, now let's define the `serialize` property of the paragraph rule as well: @@ -80,7 +80,7 @@ const rules = [ return { kind: 'block', type: 'paragraph', - nodes: next(el.children) + nodes: next(el.childNodes) } } }, @@ -119,7 +119,7 @@ const rules = [ return { kind: 'block', type: type, - nodes: next(el.children) + nodes: next(el.childNodes) } }, // Switch serialize to handle more blocks... @@ -137,7 +137,7 @@ const rules = [ Now each of our block types is handled. -You'll notice that even though code blocks are nested in a `
` and a `` element, we don't need to specifically handle that case in our `deserialize` function, because the `Html` serializer will automatically recurse through `el.children` if no matching deserializer is found. This way, unknown tags will just be skipped over in the tree, instead of their contents omitted completely.
+You'll notice that even though code blocks are nested in a `
` and a `` element, we don't need to specifically handle that case in our `deserialize` function, because the `Html` serializer will automatically recurse through `el.childNodes` if no matching deserializer is found. This way, unknown tags will just be skipped over in the tree, instead of their contents omitted completely.
 
 Okay. So now our serializer can handle blocks, but we need to add our marks to it as well. Let's do that with a new rule...
 
@@ -164,7 +164,7 @@ const rules = [
       return {
         kind: 'block',
         type: type,
-        nodes: next(el.children)
+        nodes: next(el.childNodes)
       }
     },
     serialize(object, children) {
@@ -184,7 +184,7 @@ const rules = [
       return {
         kind: 'mark',
         type: type,
-        nodes: next(el.children)
+        nodes: next(el.childNodes)
       }
     },
     serialize(object, children) {
diff --git a/examples/paste-html/index.js b/examples/paste-html/index.js
index 30b9e938a..8db714b83 100644
--- a/examples/paste-html/index.js
+++ b/examples/paste-html/index.js
@@ -85,7 +85,7 @@ const RULES = [
       return {
         kind: 'block',
         type: block,
-        nodes: next(el.children)
+        nodes: next(el.childNodes)
       }
     }
   },
@@ -96,23 +96,23 @@ const RULES = [
       return {
         kind: 'mark',
         type: mark,
-        nodes: next(el.children)
+        nodes: next(el.childNodes)
       }
     }
   },
   {
-    // Special case for code blocks, which need to grab the nested children.
+    // Special case for code blocks, which need to grab the nested childNodes.
     deserialize(el, next) {
       if (el.tagName != 'pre') return
-      const code = el.children[0]
-      const children = code && code.tagName == 'code'
-        ? code.children
-        : el.children
+      const code = el.childNodes[0]
+      const childNodes = code && code.tagName == 'code'
+        ? code.childNodes
+        : el.childNodes
 
       return {
         kind: 'block',
         type: 'code',
-        nodes: next(children)
+        nodes: next(childNodes)
       }
     }
   },
@@ -123,9 +123,9 @@ const RULES = [
       return {
         kind: 'inline',
         type: 'link',
-        nodes: next(el.children),
+        nodes: next(el.childNodes),
         data: {
-          href: el.attribs.href
+          href: el.attrs.find(({ name }) => name == 'href').value
         }
       }
     }
diff --git a/package.json b/package.json
index cd9f24655..e7b755827 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,6 @@
   "repository": "git://github.com/ianstormtaylor/slate.git",
   "main": "./lib/index.js",
   "dependencies": {
-    "cheerio": "^0.22.0",
     "debug": "^2.3.2",
     "direction": "^0.1.5",
     "es6-map": "^0.1.4",
@@ -59,6 +58,7 @@
     "mocha": "^2.5.3",
     "np": "^2.9.0",
     "npm-run-all": "^2.3.0",
+    "parse5": "^3.0.2",
     "prismjs": "^1.5.1",
     "react": "^15.4.2",
     "react-addons-perf": "^15.4.2",
diff --git a/src/serializers/html.js b/src/serializers/html.js
index f6646bbe4..bf58971f7 100644
--- a/src/serializers/html.js
+++ b/src/serializers/html.js
@@ -2,7 +2,6 @@
 import Raw from './raw'
 import React from 'react'
 import ReactDOMServer from 'react-dom/server'
-import cheerio from 'cheerio'
 import typeOf from 'type-of'
 import { Record } from 'immutable'
 
@@ -34,12 +33,12 @@ const TEXT_RULE = {
       }
     }
 
-    if (el.type == 'text') {
-      if (el.data && el.data.match(//)) return
+    if (el.nodeName == '#text') {
+      if (el.value && el.value.match(//)) return
 
       return {
         kind: 'text',
-        text: el.data
+        text: el.value
       }
     }
   },
@@ -71,8 +70,8 @@ class Html {
    *
    * @param {Object} options
    *   @property {Array} rules
-   *   @property {String} defaultBlockType
    *   @property {String|Object} defaultBlockType
+   *   @property {Function} parseHtml
    */
 
   constructor(options = {}) {
@@ -82,6 +81,19 @@ class Html {
     ]
 
     this.defaultBlockType = options.defaultBlockType || 'paragraph'
+
+    // Set DOM parser function or fallback to native DOMParser if present.
+    if (options.parseHtml !== null) {
+      this.parseHtml = options.parseHtml
+    } else if (typeof DOMParser !== 'undefined') {
+      this.parseHtml = (html) => {
+        return new DOMParser().parseFromString(html, 'application/xml')
+      }
+    } else {
+      throw new Error(
+        'Native DOMParser is not present in this environment; you must supply a parse function via options.parseHtml'
+      )
+    }
   }
 
   /**
@@ -94,8 +106,7 @@ class Html {
    */
 
   deserialize = (html, options = {}) => {
-    const $ = cheerio.load(html).root()
-    const children = $.children().toArray()
+    const children = this.parseHtml(html).childNodes
     let nodes = this.deserializeElements(children)
 
     const { defaultBlockType } = this
@@ -151,7 +162,7 @@ class Html {
   }
 
   /**
-   * Deserialize an array of Cheerio `elements`.
+   * Deserialize an array of DOM elements.
    *
    * @param {Array} elements
    * @return {Array}
@@ -160,7 +171,7 @@ class Html {
   deserializeElements = (elements = []) => {
     let nodes = []
 
-    elements.forEach((element) => {
+    elements.filter(this.cruftNewline).forEach((element) => {
       const node = this.deserializeElement(element)
       switch (typeOf(node)) {
         case 'array':
@@ -176,7 +187,7 @@ class Html {
   }
 
   /**
-   * Deserialize a Cheerio `element`.
+   * Deserialize a DOM element.
    *
    * @param {Object} element
    * @return {Any}
@@ -215,7 +226,7 @@ class Html {
       break
     }
 
-    return node || next(element.children)
+    return node || next(element.childNodes)
   }
 
   /**
@@ -226,7 +237,7 @@ class Html {
    */
 
   deserializeMark = (mark) => {
-    const { type, data } = mark
+    const { type, value } = mark
 
     const applyMark = (node) => {
       if (node.kind == 'mark') {
@@ -237,7 +248,7 @@ class Html {
         if (!node.ranges) node.ranges = [{ text: node.text }]
         node.ranges = node.ranges.map((range) => {
           range.marks = range.marks || []
-          range.marks.push({ type, data })
+          range.marks.push({ type, value })
           return range
         })
       }
@@ -337,6 +348,17 @@ class Html {
     }
   }
 
+  /**
+   * Filter out cruft newline nodes inserted by the DOM parser.
+   *
+   * @param {Object} element
+   * @return {Boolean}
+   */
+
+  cruftNewline = (element) => {
+    return !(element.nodeName == '#text' && element.value == '\n')
+  }
+
 }
 
 /**
diff --git a/test/helpers/clean.js b/test/helpers/clean.js
new file mode 100644
index 000000000..ef937132e
--- /dev/null
+++ b/test/helpers/clean.js
@@ -0,0 +1,50 @@
+import parse5 from 'parse5'
+
+const UNWANTED_ATTRS = [
+  'data-key',
+  'data-offset-key'
+]
+
+const UNWANTED_TOP_LEVEL_ATTRS = [
+  'autocorrect',
+  'spellcheck',
+  'style',
+  'data-gramm'
+]
+
+/**
+ * Clean an element of unwanted attributes
+ *
+ * @param {Element} element
+ * @return {Element}
+ */
+
+function stripUnwantedAttrs(element) {
+  if(Array.isArray(element.attrs)) {
+    element.attrs = element.attrs.filter(({ name }) => { return !UNWANTED_ATTRS.includes(name) })
+
+    if(element.parentNode.nodeName === '#document-fragment') {
+      element.attrs = element.attrs.filter(({ name }) => { return !UNWANTED_TOP_LEVEL_ATTRS.includes(name) })      
+    }
+  }
+  if(Array.isArray(element.childNodes)) {
+    element.childNodes.forEach(stripUnwantedAttrs)
+  }
+  if(element.nodeName === '#text') {
+    element.value = element.value.trim()
+  }
+  return element
+}
+
+/**
+ * Clean a renderer `html` string, removing dynamic attributes.
+ *
+ * @param {String} html
+ * @return {String}
+ */
+
+export default function clean(html) {
+  const $ = parse5.parseFragment(html)
+  $.childNodes.forEach(stripUnwantedAttrs)
+  return parse5.serialize($)
+}
diff --git a/test/plugins/index.js b/test/plugins/index.js
index ee477476a..30ae51335 100644
--- a/test/plugins/index.js
+++ b/test/plugins/index.js
@@ -2,11 +2,11 @@
 import React from 'react'
 import ReactDOM from 'react-dom/server'
 import assert from 'assert'
-import cheerio from 'cheerio'
 import fs from 'fs-promise'
 import readYaml from 'read-yaml-promise'
 import { Editor, Raw } from '../..'
 import { resolve } from 'path'
+import clean from '../helpers/clean'
 
 /**
  * Tests.
@@ -29,9 +29,7 @@ describe('plugins', () => {
       }
 
       const string = ReactDOM.renderToStaticMarkup()
-      const expected = cheerio
-        .load(output)
-        .html()
+      const expected = output
         .trim()
         .replace(/\n/gm, '')
         .replace(/>\s*<')
@@ -40,26 +38,3 @@ describe('plugins', () => {
     })
   }
 })
-
-/**
- * Clean a renderer `html` string, removing dynamic attributes.
- *
- * @param {String} html
- * @return {String}
- */
-
-function clean(html) {
-  const $ = cheerio.load(html)
-
-  $('*').each((i, el) => {
-    $(el).removeAttr('data-key')
-    $(el).removeAttr('data-offset-key')
-  })
-
-  $.root().children().removeAttr('autocorrect')
-  $.root().children().removeAttr('spellcheck')
-  $.root().children().removeAttr('style')
-  $.root().children().removeAttr('data-gramm')
-
-  return $.html()
-}
diff --git a/test/rendering/index.js b/test/rendering/index.js
index 1b0108698..8a61ada3d 100644
--- a/test/rendering/index.js
+++ b/test/rendering/index.js
@@ -2,11 +2,12 @@
 import React from 'react'
 import ReactDOM from 'react-dom/server'
 import assert from 'assert'
-import cheerio from 'cheerio'
+import parse5 from 'parse5'
 import fs from 'fs-promise'
 import readYaml from 'read-yaml-promise'
 import { Editor, Raw } from '../..'
 import { resolve } from 'path'
+import clean from '../helpers/clean'
 
 /**
  * Tests.
@@ -29,9 +30,7 @@ describe('rendering', () => {
       }
 
       const string = ReactDOM.renderToStaticMarkup()
-      const expected = cheerio
-        .load(output)
-        .html()
+      const expected = parse5.serialize(parse5.parseFragment(output))
         .trim()
         .replace(/\n/gm, '')
         .replace(/>\s*<')
@@ -40,26 +39,3 @@ describe('rendering', () => {
     })
   }
 })
-
-/**
- * Clean a renderer `html` string, removing dynamic attributes.
- *
- * @param {String} html
- * @return {String}
- */
-
-function clean(html) {
-  const $ = cheerio.load(html)
-
-  $('*').each((i, el) => {
-    $(el).removeAttr('data-key')
-    $(el).removeAttr('data-offset-key')
-  })
-
-  $.root().children().removeAttr('autocorrect')
-  $.root().children().removeAttr('spellcheck')
-  $.root().children().removeAttr('style')
-  $.root().children().removeAttr('data-gramm')
-
-  return $.html()
-}
diff --git a/test/serializers/fixtures/html/deserialize/block-nested/index.js b/test/serializers/fixtures/html/deserialize/block-nested/index.js
index 9580667fd..5bf54b16c 100644
--- a/test/serializers/fixtures/html/deserialize/block-nested/index.js
+++ b/test/serializers/fixtures/html/deserialize/block-nested/index.js
@@ -8,14 +8,14 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'blockquote': {
             return {
               kind: 'block',
               type: 'quote',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/block-no-children/index.js b/test/serializers/fixtures/html/deserialize/block-no-children/index.js
index 4f362bef4..99840a59a 100644
--- a/test/serializers/fixtures/html/deserialize/block-no-children/index.js
+++ b/test/serializers/fixtures/html/deserialize/block-no-children/index.js
@@ -8,14 +8,14 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'a': {
             return {
               kind: 'inline',
               type: 'link',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/block-with-data/index.js b/test/serializers/fixtures/html/deserialize/block-with-data/index.js
index c4ac2c5e6..27c9795e0 100644
--- a/test/serializers/fixtures/html/deserialize/block-with-data/index.js
+++ b/test/serializers/fixtures/html/deserialize/block-with-data/index.js
@@ -9,7 +9,7 @@ export default {
               kind: 'block',
               type: 'paragraph',
               data: { key: 'value' },
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/block/index.js b/test/serializers/fixtures/html/deserialize/block/index.js
index 52e38b12d..cd58f29f1 100644
--- a/test/serializers/fixtures/html/deserialize/block/index.js
+++ b/test/serializers/fixtures/html/deserialize/block/index.js
@@ -8,7 +8,7 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/default-block/index.js b/test/serializers/fixtures/html/deserialize/default-block/index.js
index 830d64ab7..eb4df3feb 100644
--- a/test/serializers/fixtures/html/deserialize/default-block/index.js
+++ b/test/serializers/fixtures/html/deserialize/default-block/index.js
@@ -8,7 +8,7 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/html-comment/index.js b/test/serializers/fixtures/html/deserialize/html-comment/index.js
index 52e38b12d..cd58f29f1 100644
--- a/test/serializers/fixtures/html/deserialize/html-comment/index.js
+++ b/test/serializers/fixtures/html/deserialize/html-comment/index.js
@@ -8,7 +8,7 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/inline-nested/index.js b/test/serializers/fixtures/html/deserialize/inline-nested/index.js
index 44303a100..38ea4dba5 100644
--- a/test/serializers/fixtures/html/deserialize/inline-nested/index.js
+++ b/test/serializers/fixtures/html/deserialize/inline-nested/index.js
@@ -8,21 +8,21 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'a': {
             return {
               kind: 'inline',
               type: 'link',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'b': {
             return {
               kind: 'inline',
               type: 'hashtag',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/inline-no-children/index.js b/test/serializers/fixtures/html/deserialize/inline-no-children/index.js
index 4f362bef4..99840a59a 100644
--- a/test/serializers/fixtures/html/deserialize/inline-no-children/index.js
+++ b/test/serializers/fixtures/html/deserialize/inline-no-children/index.js
@@ -8,14 +8,14 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'a': {
             return {
               kind: 'inline',
               type: 'link',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/inline-with-data/index.js b/test/serializers/fixtures/html/deserialize/inline-with-data/index.js
index 629c3bee3..da3ad984d 100644
--- a/test/serializers/fixtures/html/deserialize/inline-with-data/index.js
+++ b/test/serializers/fixtures/html/deserialize/inline-with-data/index.js
@@ -8,16 +8,16 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'a': {
             return {
               kind: 'inline',
               type: 'link',
-              nodes: next(el.children),
+              nodes: next(el.childNodes),
               data: {
-                href: el.attribs.href
+                href: el.attrs.find(({ name }) => name == 'href').value
               }
             }
           }
diff --git a/test/serializers/fixtures/html/deserialize/inline-with-is-void/index.js b/test/serializers/fixtures/html/deserialize/inline-with-is-void/index.js
index 66526ce8e..6eaa6a19f 100644
--- a/test/serializers/fixtures/html/deserialize/inline-with-is-void/index.js
+++ b/test/serializers/fixtures/html/deserialize/inline-with-is-void/index.js
@@ -8,7 +8,7 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'a': {
diff --git a/test/serializers/fixtures/html/deserialize/inline/index.js b/test/serializers/fixtures/html/deserialize/inline/index.js
index 4f362bef4..99840a59a 100644
--- a/test/serializers/fixtures/html/deserialize/inline/index.js
+++ b/test/serializers/fixtures/html/deserialize/inline/index.js
@@ -8,14 +8,14 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'a': {
             return {
               kind: 'inline',
               type: 'link',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/mark-interleaved/index.js b/test/serializers/fixtures/html/deserialize/mark-interleaved/index.js
index 2866022dc..c07bd4303 100644
--- a/test/serializers/fixtures/html/deserialize/mark-interleaved/index.js
+++ b/test/serializers/fixtures/html/deserialize/mark-interleaved/index.js
@@ -8,21 +8,21 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'em': {
             return {
               kind: 'mark',
               type: 'italic',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'strong': {
             return {
               kind: 'mark',
               type: 'bold',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/mark-nested/index.js b/test/serializers/fixtures/html/deserialize/mark-nested/index.js
index 2866022dc..c07bd4303 100644
--- a/test/serializers/fixtures/html/deserialize/mark-nested/index.js
+++ b/test/serializers/fixtures/html/deserialize/mark-nested/index.js
@@ -8,21 +8,21 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'em': {
             return {
               kind: 'mark',
               type: 'italic',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'strong': {
             return {
               kind: 'mark',
               type: 'bold',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/mark/index.js b/test/serializers/fixtures/html/deserialize/mark/index.js
index fc363e085..a63641657 100644
--- a/test/serializers/fixtures/html/deserialize/mark/index.js
+++ b/test/serializers/fixtures/html/deserialize/mark/index.js
@@ -8,14 +8,14 @@ export default {
             return {
               kind: 'block',
               type: 'paragraph',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
           case 'em': {
             return {
               kind: 'mark',
               type: 'italic',
-              nodes: next(el.children)
+              nodes: next(el.childNodes)
             }
           }
         }
diff --git a/test/serializers/fixtures/html/deserialize/skip-element/index.js b/test/serializers/fixtures/html/deserialize/skip-element/index.js
index a215e3e23..d56247802 100644
--- a/test/serializers/fixtures/html/deserialize/skip-element/index.js
+++ b/test/serializers/fixtures/html/deserialize/skip-element/index.js
@@ -24,7 +24,7 @@ export default {
         return {
           kind: 'block',
           type: 'paragraph',
-          nodes: next(el.children),
+          nodes: next(el.childNodes),
         }
       }
     },
diff --git a/test/serializers/fixtures/html/deserialize/skip-element/output.yaml b/test/serializers/fixtures/html/deserialize/skip-element/output.yaml
index 2a6e04bc8..9dd86e13f 100644
--- a/test/serializers/fixtures/html/deserialize/skip-element/output.yaml
+++ b/test/serializers/fixtures/html/deserialize/skip-element/output.yaml
@@ -5,10 +5,16 @@ nodes:
     isVoid: false
     data: {}
     nodes:
-      - type: divider
-        isVoid: true
-        data: {}
-        nodes:
-          - characters:
-              - marks: []
-                text: " "
+    - characters: []
+  - type: divider
+    isVoid: true
+    data: {}
+    nodes:
+    - characters:
+        - text: " "
+          marks: []
+  - type: paragraph
+    isVoid: false
+    data: {}
+    nodes:
+    - characters: []
diff --git a/test/serializers/index.js b/test/serializers/index.js
index 7af9f4806..93a1d3d28 100644
--- a/test/serializers/index.js
+++ b/test/serializers/index.js
@@ -7,6 +7,7 @@ import { Html, Plain, Raw } from '../..'
 import { resolve } from 'path'
 import React from 'react'
 import { Iterable } from 'immutable'
+import parse5 from 'parse5'
 
 /**
  * Tests.
@@ -22,7 +23,8 @@ describe('serializers', () => {
         if (test[0] === '.') continue
         it(test, async () => {
           const innerDir = resolve(dir, test)
-          const html = new Html(require(innerDir).default)
+          const htmlOpts = Object.assign({}, require(innerDir).default, { parseHtml: parse5.parseFragment })
+          const html = new Html(htmlOpts)
           const expected = await readYaml(resolve(innerDir, 'output.yaml'))
           const input = fs.readFileSync(resolve(innerDir, 'input.html'), 'utf8')
           const state = html.deserialize(input)
@@ -32,7 +34,9 @@ describe('serializers', () => {
       }
 
       it('optionally returns a raw representation', () => {
-        const html = new Html(require('./fixtures/html/deserialize/block').default)
+        const fixture = require('./fixtures/html/deserialize/block').default
+        const htmlOpts = Object.assign({}, fixture, { parseHtml: parse5.parseFragment })
+        const html = new Html(htmlOpts)
         const input = fs.readFileSync(resolve(__dirname, './fixtures/html/deserialize/block/input.html'), 'utf8')
         const serialized = html.deserialize(input, { toRaw: true })
         assert.deepEqual(serialized, {
@@ -56,7 +60,9 @@ describe('serializers', () => {
       })
 
       it('optionally does not normalize', () => {
-        const html = new Html(require('./fixtures/html/deserialize/inline-with-is-void').default)
+        const fixture = require('./fixtures/html/deserialize/inline-with-is-void').default
+        const htmlOpts = Object.assign({}, fixture, { parseHtml: parse5.parseFragment })
+        const html = new Html(htmlOpts)
         const input = fs.readFileSync(resolve(__dirname, './fixtures/html/deserialize/inline-with-is-void/input.html'), 'utf8')
         const serialized = html.deserialize(input, { toRaw: true, normalize: false })
         assert.deepEqual(serialized, {
@@ -89,7 +95,8 @@ describe('serializers', () => {
         if (test[0] === '.') continue
         it(test, async () => {
           const innerDir = resolve(dir, test)
-          const html = new Html(require(innerDir).default)
+          const htmlOpts = Object.assign({}, require(innerDir).default, { parseHtml: parse5.parseFragment })
+          const html = new Html(htmlOpts)
           const input = require(resolve(innerDir, 'input.js')).default
           const expected = fs.readFileSync(resolve(innerDir, 'output.html'), 'utf8')
           const serialized = html.serialize(input)
@@ -98,7 +105,9 @@ describe('serializers', () => {
       }
 
       it('optionally returns an iterable list of React elements', () => {
-        const html = new Html(require('./fixtures/html/serialize/block-nested').default)
+        const fixture = require('./fixtures/html/serialize/block-nested').default
+        const htmlOpts = Object.assign({}, fixture, { parseHtml: parse5.parseFragment })
+        const html = new Html(htmlOpts)
         const input = require('./fixtures/html/serialize/block-nested/input.js').default
         const serialized = html.serialize(input, { render: false })
         assert(Iterable.isIterable(serialized), 'did not return an interable list')
diff --git a/yarn.lock b/yarn.lock
index a521532c1..2d8d8239d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,10 @@
 # yarn lockfile v1
 
 
+"@types/node@^6.0.46":
+  version "6.0.83"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.83.tgz#dd022db01ac2c01c1057775e88ccffce96d1d6fe"
+
 JSONStream@^1.0.3:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
@@ -1073,10 +1077,6 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
   version "4.11.6"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
 
-boolbase@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
-
 boom@2.x.x:
   version "2.10.1"
   resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
@@ -1437,27 +1437,6 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-cheerio@^0.22.0:
-  version "0.22.0"
-  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
-  dependencies:
-    css-select "~1.2.0"
-    dom-serializer "~0.1.0"
-    entities "~1.1.1"
-    htmlparser2 "^3.9.1"
-    lodash.assignin "^4.0.9"
-    lodash.bind "^4.1.4"
-    lodash.defaults "^4.0.1"
-    lodash.filter "^4.4.0"
-    lodash.flatten "^4.2.0"
-    lodash.foreach "^4.3.0"
-    lodash.map "^4.4.0"
-    lodash.merge "^4.4.0"
-    lodash.pick "^4.2.1"
-    lodash.reduce "^4.4.0"
-    lodash.reject "^4.4.0"
-    lodash.some "^4.4.0"
-
 chokidar@^1.0.0, chokidar@^1.6.1:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
@@ -1775,19 +1754,6 @@ crypto-random-string@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
 
-css-select@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
-  dependencies:
-    boolbase "~1.0.0"
-    css-what "2.1"
-    domutils "1.5.1"
-    nth-check "~1.0.1"
-
-css-what@2.1:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
-
 cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0":
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b"
@@ -1986,38 +1952,10 @@ doctrine@^2.0.0:
     esutils "^2.0.2"
     isarray "^1.0.0"
 
-dom-serializer@0, dom-serializer@~0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
-  dependencies:
-    domelementtype "~1.1.1"
-    entities "~1.1.1"
-
 domain-browser@~1.1.0:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
 
-domelementtype@1, domelementtype@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
-
-domelementtype@~1.1.1:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
-
-domhandler@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
-  dependencies:
-    domelementtype "1"
-
-domutils@1.5.1, domutils@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
-  dependencies:
-    dom-serializer "0"
-    domelementtype "1"
-
 dot-parts@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/dot-parts/-/dot-parts-1.0.1.tgz#884bd7bcfc3082ffad2fe5db53e494d8f3e0743f"
@@ -2091,10 +2029,6 @@ encoding@^0.1.11:
   dependencies:
     iconv-lite "~0.4.13"
 
-entities@^1.1.1, entities@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
-
 envify@^3.4.1:
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8"
@@ -3020,17 +2954,6 @@ htmlescape@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
 
-htmlparser2@^3.9.1:
-  version "3.9.2"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
-  dependencies:
-    domelementtype "^1.3.0"
-    domhandler "^2.3.0"
-    domutils "^1.5.1"
-    entities "^1.1.1"
-    inherits "^2.0.1"
-    readable-stream "^2.0.2"
-
 http-proxy@^1.8.1:
   version "1.16.2"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742"
@@ -4005,14 +3928,6 @@ lodash.assign@^4.0.0, lodash.assign@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
 
-lodash.assignin@^4.0.9:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
-
-lodash.bind@^4.1.4:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
-
 lodash.clonedeep@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db"
@@ -4024,22 +3939,6 @@ lodash.cond@^4.3.0:
   version "4.5.2"
   resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
 
-lodash.defaults@^4.0.1:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
-
-lodash.filter@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
-
-lodash.flatten@^4.2.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
-
-lodash.foreach@^4.3.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
-
 lodash.isarguments@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@@ -4056,38 +3955,14 @@ lodash.keys@^3.0.0:
     lodash.isarguments "^3.0.0"
     lodash.isarray "^3.0.0"
 
-lodash.map@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
-
 lodash.memoize@~3.0.3:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
 
-lodash.merge@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
-
-lodash.pick@^4.2.1:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
-
 lodash.pickby@^4.0.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff"
 
-lodash.reduce@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
-
-lodash.reject@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
-
-lodash.some@^4.4.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
-
 lodash@^4.0.0, lodash@^4.14.0, lodash@^4.2.0, lodash@^4.3.0:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -4501,12 +4376,6 @@ npmlog@^4.0.2:
     gauge "~2.7.1"
     set-blocking "~2.0.0"
 
-nth-check@~1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4"
-  dependencies:
-    boolbase "~1.0.0"
-
 number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -4698,6 +4567,12 @@ parse5@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
 
+parse5@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510"
+  dependencies:
+    "@types/node" "^6.0.46"
+
 patch-text@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/patch-text/-/patch-text-1.0.2.tgz#4bf36e65e51733d6e98f0cf62e09034daa0348ac"