From 19fb3e77898232ca46c9b15ed8dd16aecfaa6a3a Mon Sep 17 00:00:00 2001 From: mitermayer Date: Sun, 19 Nov 2017 23:58:17 -0800 Subject: [PATCH] Exploration - Feature tree / Convert from html tree data support Summary: This PR is part of a series of PR's that will be exploring **tree data block support** in Draft. **Convert from html tree data support** This PR adds support for converting from HTML to tree data, as a follow up we will then hook the paste processor with this too *** **Note:** This is unstable and not part of the public API and should not be used by production systems. Closes https://github.com/facebook/draft-js/pull/1518 Differential Revision: D6372235 fbshipit-source-id: fdfe81efcb81e31aa4bb61f5cfe7b21bf11317a4 --- ...onvertFromHTMLToContentBlocks-test.js.snap | 849 +++++++++++++++++- .../convertFromHTMLToContentBlocks-test.js | 142 ++- .../convertFromHTMLToContentBlocks.js | 443 +++++---- .../DraftPasteProcessor-test.js.snap | 90 +- 4 files changed, 1255 insertions(+), 269 deletions(-) diff --git a/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap b/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap index 6b63a7d5c8..37795ef7ef 100644 --- a/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap +++ b/src/model/encoding/__tests__/__snapshots__/convertFromHTMLToContentBlocks-test.js.snap @@ -1,10 +1,568 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`converts deeply nested html blocks when experimentalTreeDataSupport is enabled 1`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key58", + "nextSibling": null, + "parent": undefined, + "prevSibling": null, + "text": " ", + "type": "unstyled", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key53", + "nextSibling": "key54", + "parent": null, + "prevSibling": null, + "text": "Some quote ", + "type": "ordered-list-item", + }, + Object { + "characterList": Array [], + "children": Array [ + "key55", + ], + "data": Object {}, + "depth": 0, + "key": "key54", + "nextSibling": null, + "parent": null, + "prevSibling": "key53", + "text": "", + "type": "ordered-list-item", + }, + Object { + "characterList": Array [], + "children": Array [ + "key56", + "key57", + ], + "data": Object {}, + "depth": 0, + "key": "key55", + "nextSibling": null, + "parent": "key54", + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key56", + "nextSibling": "key57", + "parent": "key55", + "prevSibling": null, + "text": "Hello World! ", + "type": "header-one", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key57", + "nextSibling": null, + "parent": "key55", + "prevSibling": "key56", + "text": "lorem ipsum + ", + "type": "unstyled", + }, +] +`; + +exports[`converts nested html blocks when experimentalTreeDataSupport is enabled 1`] = ` +Array [ + Object { + "characterList": Array [], + "children": Array [ + "key51", + "key52", + ], + "data": Object {}, + "depth": 0, + "key": "key50", + "nextSibling": null, + "parent": null, + "prevSibling": null, + "text": "", + "type": "blockquote", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key51", + "nextSibling": "key52", + "parent": "key50", + "prevSibling": null, + "text": "Hello World! + ", + "type": "header-one", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + Object { + "entity": null, + "style": Array [], + }, + ], + "children": Array [], + "data": Object {}, + "depth": 0, + "key": "key52", + "nextSibling": null, + "parent": "key50", + "prevSibling": "key51", + "text": "lorem ipsum + ", + "type": "unstyled", + }, +] +`; + exports[`img with data protocol should be correctly parsed 1`] = `"📷"`; exports[`img with http protocol should have camera emoji content 1`] = `"📷"`; -exports[`must not merge tags when converting adjacent
1`] = ` +exports[`must not merge tags when converting adjacent
1`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key0", + "text": "a", + "type": "blockquote", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key2", + "text": "b", + "type": "blockquote", + }, +] +`; + +exports[`must not merge tags when converting adjacent
2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key59", + "text": "a", + "type": "blockquote", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key61", + "text": "b", + "type": "blockquote", + }, +] +`; + +exports[`must not merge tags when converting adjacent
1`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key4", + "text": "a", + "type": "unstyled", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key6", + "text": "b", + "type": "unstyled", + }, +] +`; + +exports[`must not merge tags when converting adjacent
2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key63", + "text": "a", + "type": "unstyled", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key65", + "text": "b", + "type": "unstyled", + }, +] +`; + +exports[`must not merge tags when converting adjacent
1`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key8", + "text": "a", + "type": "atomic", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key10", + "text": "b", + "type": "atomic", + }, +] +`; + +exports[`must not merge tags when converting adjacent
2`] = ` Array [ Object { "characterList": Array [ @@ -15,9 +573,9 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key0", + "key": "key67", "text": "a", - "type": "blockquote", + "type": "atomic", }, Object { "characterList": Array [ @@ -28,14 +586,14 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key1", + "key": "key69", "text": "b", - "type": "blockquote", + "type": "atomic", }, ] `; -exports[`must not merge tags when converting adjacent
1`] = ` +exports[`must not merge tags when converting adjacent

1`] = ` Array [ Object { "characterList": Array [ @@ -46,9 +604,9 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key2", + "key": "key12", "text": "a", - "type": "unstyled", + "type": "header-one", }, Object { "characterList": Array [ @@ -59,14 +617,14 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key3", + "key": "key14", "text": "b", - "type": "unstyled", + "type": "header-one", }, ] `; -exports[`must not merge tags when converting adjacent
1`] = ` +exports[`must not merge tags when converting adjacent

2`] = ` Array [ Object { "characterList": Array [ @@ -77,9 +635,9 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key4", + "key": "key71", "text": "a", - "type": "atomic", + "type": "header-one", }, Object { "characterList": Array [ @@ -90,14 +648,14 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key5", + "key": "key73", "text": "b", - "type": "atomic", + "type": "header-one", }, ] `; -exports[`must not merge tags when converting adjacent

1`] = ` +exports[`must not merge tags when converting adjacent

1`] = ` Array [ Object { "characterList": Array [ @@ -108,9 +666,9 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key6", + "key": "key16", "text": "a", - "type": "header-one", + "type": "header-two", }, Object { "characterList": Array [ @@ -121,14 +679,14 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key7", + "key": "key18", "text": "b", - "type": "header-one", + "type": "header-two", }, ] `; -exports[`must not merge tags when converting adjacent

1`] = ` +exports[`must not merge tags when converting adjacent

2`] = ` Array [ Object { "characterList": Array [ @@ -139,7 +697,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key8", + "key": "key75", "text": "a", "type": "header-two", }, @@ -152,7 +710,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key9", + "key": "key77", "text": "b", "type": "header-two", }, @@ -170,7 +728,38 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key10", + "key": "key20", + "text": "a", + "type": "header-three", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key22", + "text": "b", + "type": "header-three", + }, +] +`; + +exports[`must not merge tags when converting adjacent

2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key79", "text": "a", "type": "header-three", }, @@ -183,7 +772,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key11", + "key": "key81", "text": "b", "type": "header-three", }, @@ -201,7 +790,38 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key12", + "key": "key24", + "text": "a", + "type": "header-four", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key26", + "text": "b", + "type": "header-four", + }, +] +`; + +exports[`must not merge tags when converting adjacent

2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key83", "text": "a", "type": "header-four", }, @@ -214,7 +834,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key13", + "key": "key85", "text": "b", "type": "header-four", }, @@ -232,7 +852,38 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key14", + "key": "key28", + "text": "a", + "type": "header-five", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key30", + "text": "b", + "type": "header-five", + }, +] +`; + +exports[`must not merge tags when converting adjacent

2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key87", "text": "a", "type": "header-five", }, @@ -245,7 +896,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key15", + "key": "key89", "text": "b", "type": "header-five", }, @@ -263,7 +914,38 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key16", + "key": "key32", + "text": "a", + "type": "header-six", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key34", + "text": "b", + "type": "header-six", + }, +] +`; + +exports[`must not merge tags when converting adjacent
2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key91", "text": "a", "type": "header-six", }, @@ -276,7 +958,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key17", + "key": "key93", "text": "b", "type": "header-six", }, @@ -294,7 +976,38 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key18", + "key": "key36", + "text": "a", + "type": "unordered-list-item", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key38", + "text": "b", + "type": "unordered-list-item", + }, +] +`; + +exports[`must not merge tags when converting adjacent
  • 2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key95", "text": "a", "type": "unordered-list-item", }, @@ -307,7 +1020,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key19", + "key": "key97", "text": "b", "type": "unordered-list-item", }, @@ -325,7 +1038,38 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key20", + "key": "key40", + "text": "a", + "type": "unstyled", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key42", + "text": "b", + "type": "unstyled", + }, +] +`; + +exports[`must not merge tags when converting adjacent

    2`] = ` +Array [ + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key99", "text": "a", "type": "unstyled", }, @@ -338,7 +1082,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key21", + "key": "key101", "text": "b", "type": "unstyled", }, @@ -356,7 +1100,38 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key22", + "key": "key44", + "text": "a", + "type": "code-block", + }, + Object { + "characterList": Array [ + Object { + "entity": null, + "style": Array [], + }, + ], + "data": Object {}, + "depth": 0, + "key": "key46", + "text": "b", + "type": "code-block", + }, +] +`; + +exports[`must not merge tags when converting adjacent

     2`] = `
    +Array [
    +  Object {
    +    "characterList": Array [
    +      Object {
    +        "entity": null,
    +        "style": Array [],
    +      },
    +    ],
    +    "data": Object {},
    +    "depth": 0,
    +    "key": "key103",
         "text": "a",
         "type": "code-block",
       },
    @@ -369,7 +1144,7 @@ Array [
         ],
         "data": Object {},
         "depth": 0,
    -    "key": "key23",
    +    "key": "key105",
         "text": "b",
         "type": "code-block",
       },
    diff --git a/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js b/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js
    index 5840520164..8b7ad8eba1 100644
    --- a/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js
    +++ b/src/model/encoding/__tests__/convertFromHTMLToContentBlocks-test.js
    @@ -16,32 +16,22 @@ jest.disableAutomock();
     
     jest.mock('generateRandomKey');
     
    +const DefaultDraftBlockRenderMap = require('DefaultDraftBlockRenderMap');
    +
     const convertFromHTMLToContentBlocks = require('convertFromHTMLToContentBlocks');
    +const getSafeBodyFromHTML = require('getSafeBodyFromHTML');
     
    -const assertConvertFromHTMLToContentBlocks = html_string => {
    -  expect(
    -    convertFromHTMLToContentBlocks(html_string).contentBlocks.map(block =>
    -      block.toJS(),
    -    ),
    -  ).toMatchSnapshot();
    +const DEFAULT_CONFIG = {
    +  DOMBuilder: getSafeBodyFromHTML,
    +  blockRenderMap: DefaultDraftBlockRenderMap,
    +  experimentalTreeDataSupport: false,
     };
     
     const IMAGE_DATA_URL =
       'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///' +
       'yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
     
    -const testConvertingAdjacentHtmlElementsToContentBlocks = (tag: string) => {
    -  it(`must not merge tags when converting adjacent <${tag} />`, () => {
    -    const html_string = `
    -      <${tag}>a
    -      <${tag}>b
    -    `;
    -
    -    assertConvertFromHTMLToContentBlocks(html_string);
    -  });
    -};
    -
    -[
    +const SUPPORTED_TAGS = [
       'blockquote',
       'div',
       'figure',
    @@ -54,7 +44,81 @@ const testConvertingAdjacentHtmlElementsToContentBlocks = (tag: string) => {
       'li',
       'p',
       'pre',
    -].forEach(tag => testConvertingAdjacentHtmlElementsToContentBlocks(tag));
    +];
    +
    +const assertConvertFromHTMLToContentBlocks = (html_string, config = {}) => {
    +  const options = {
    +    ...DEFAULT_CONFIG,
    +    ...config,
    +  };
    +
    +  const {DOMBuilder, blockRenderMap, experimentalTreeDataSupport} = options;
    +
    +  expect(
    +    convertFromHTMLToContentBlocks(
    +      html_string,
    +      DOMBuilder,
    +      blockRenderMap,
    +      experimentalTreeDataSupport,
    +    ).contentBlocks.map(block => block.toJS()),
    +  ).toMatchSnapshot();
    +};
    +
    +const testConvertingAdjacentHtmlElementsToContentBlocks = (
    +  tag: string,
    +  experimentalTreeDataSupport?: boolean = false,
    +) => {
    +  test(`must not merge tags when converting adjacent <${tag} />`, () => {
    +    const html_string = `
    +      <${tag}>a
    +      <${tag}>b
    +    `;
    +
    +    assertConvertFromHTMLToContentBlocks(
    +      html_string,
    +      experimentalTreeDataSupport,
    +    );
    +  });
    +};
    +
    +const normalizeBlock = block => {
    +  const {type, depth, textBlock, characterList} = block;
    +
    +  return {
    +    type,
    +    depth,
    +    textBlock,
    +    characterList,
    +  };
    +};
    +
    +const testConvertingHtmlElementsToContentBlocksAndRootContentBlockNodesMatch = (
    +  tag: string,
    +) => {
    +  test(`must convert root ContentBlockNodes to matching ContentBlock nodes for <${tag} />`, () => {
    +    const {DOMBuilder, blockRenderMap} = DEFAULT_CONFIG;
    +    const html_string = `<${tag}>a `;
    +    expect(
    +      convertFromHTMLToContentBlocks(
    +        html_string,
    +        DOMBuilder,
    +        blockRenderMap,
    +        false,
    +      ).contentBlocks.map(block => normalizeBlock(block.toJS())),
    +    ).toEqual(
    +      convertFromHTMLToContentBlocks(
    +        html_string,
    +        DOMBuilder,
    +        blockRenderMap,
    +        true,
    +      ).contentBlocks.map(block => normalizeBlock(block.toJS())),
    +    );
    +  });
    +};
    +
    +SUPPORTED_TAGS.forEach(tag =>
    +  testConvertingAdjacentHtmlElementsToContentBlocks(tag),
    +);
     
     test('img with http protocol should have camera emoji content', () => {
       const blocks = convertFromHTMLToContentBlocks(
    @@ -69,3 +133,43 @@ test('img with data protocol should be correctly parsed', () => {
       );
       expect(blocks.contentBlocks[0].text).toMatchSnapshot();
     });
    +
    +test('converts nested html blocks when experimentalTreeDataSupport is enabled', () => {
    +  const html_string = `
    +    
    +

    Hello World!

    +

    lorem ipsum

    +
    + `; + + assertConvertFromHTMLToContentBlocks(html_string, { + experimentalTreeDataSupport: true, + }); +}); + +test('converts deeply nested html blocks when experimentalTreeDataSupport is enabled', () => { + const html_string = ` +
      +
    1. Some quote
    2. +
    3. +
      +

      Hello World!

      +

      lorem ipsum

      +
      +
    4. +
    + `; + + assertConvertFromHTMLToContentBlocks(html_string, { + experimentalTreeDataSupport: true, + }); +}); + +SUPPORTED_TAGS.forEach(tag => + testConvertingAdjacentHtmlElementsToContentBlocks(tag, true), +); + +// assert that using tree blocks and root content block nodes are equivalent +SUPPORTED_TAGS.forEach(tag => + testConvertingHtmlElementsToContentBlocksAndRootContentBlockNodesMatch(tag), +); diff --git a/src/model/encoding/convertFromHTMLToContentBlocks.js b/src/model/encoding/convertFromHTMLToContentBlocks.js index 610d857ff6..9608d57acd 100644 --- a/src/model/encoding/convertFromHTMLToContentBlocks.js +++ b/src/model/encoding/convertFromHTMLToContentBlocks.js @@ -22,6 +22,7 @@ import type {EntityMap} from 'EntityMap'; const CharacterMetadata = require('CharacterMetadata'); const ContentBlock = require('ContentBlock'); +const ContentBlockNode = require('ContentBlockNode'); const DefaultDraftBlockRenderMap = require('DefaultDraftBlockRenderMap'); const DraftEntity = require('DraftEntity'); const Immutable = require('immutable'); @@ -31,23 +32,36 @@ const URI = require('URI'); const generateRandomKey = require('generateRandomKey'); const getSafeBodyFromHTML = require('getSafeBodyFromHTML'); const invariant = require('invariant'); -const nullthrows = require('nullthrows'); const sanitizeDraftText = require('sanitizeDraftText'); -var {List, OrderedSet} = Immutable; +type Block = { + type: DraftBlockType, + depth: number, + key?: string, + parent?: string, +}; -var NBSP = ' '; -var SPACE = ' '; +type Chunk = { + text: string, + inlines: Array, + entities: Array, + blocks: Array, +}; + +const {List, OrderedSet} = Immutable; + +const NBSP = ' '; +const SPACE = ' '; // Arbitrary max indent -var MAX_DEPTH = 4; +const MAX_DEPTH = 4; // used for replacing characters in HTML -var REGEX_CR = new RegExp('\r', 'g'); -var REGEX_LF = new RegExp('\n', 'g'); -var REGEX_NBSP = new RegExp(NBSP, 'g'); -var REGEX_CARRIAGE = new RegExp(' ?', 'g'); -var REGEX_ZWS = new RegExp('​?', 'g'); +const REGEX_CR = new RegExp('\r', 'g'); +const REGEX_LF = new RegExp('\n', 'g'); +const REGEX_NBSP = new RegExp(NBSP, 'g'); +const REGEX_CARRIAGE = new RegExp(' ?', 'g'); +const REGEX_ZWS = new RegExp('​?', 'g'); // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight const boldValues = ['bold', 'bolder', '500', '600', '700', '800', '900']; @@ -55,7 +69,7 @@ const notBoldValues = ['light', 'lighter', '100', '200', '300', '400']; // Block tag flow is different because LIs do not have // a deterministic style ;_; -var inlineTags = { +const inlineTags = { b: 'BOLD', code: 'CODE', del: 'STRIKETHROUGH', @@ -67,79 +81,36 @@ var inlineTags = { u: 'UNDERLINE', }; -var anchorAttr = ['className', 'href', 'rel', 'target', 'title']; +const anchorAttr = ['className', 'href', 'rel', 'target', 'title']; const imgAttr = ['alt', 'className', 'height', 'src', 'width']; -var lastBlock; +let lastBlock; -type Block = { - type: DraftBlockType, - depth: number, +const EMPTY_CHUNK = { + text: '', + inlines: [], + entities: [], + blocks: [], }; -type Chunk = { - text: string, - inlines: Array, - entities: Array, - blocks: Array, +const EMPTY_BLOCK = { + children: List(), + depth: 0, + key: '', + type: '', }; -function getEmptyChunk(): Chunk { - return { - text: '', - inlines: [], - entities: [], - blocks: [], - }; -} - -function getWhitespaceChunk(inEntity: ?string): Chunk { - var entities = new Array(1); - if (inEntity) { - entities[0] = inEntity; - } - return { - text: SPACE, - inlines: [OrderedSet()], - entities, - blocks: [], - }; -} - -function getSoftNewlineChunk(): Chunk { - return { - text: '\n', - inlines: [OrderedSet()], - entities: new Array(1), - blocks: [], - }; -} - -function getBlockDividerChunk(block: DraftBlockType, depth: number): Chunk { - return { - text: '\r', - inlines: [OrderedSet()], - entities: new Array(1), - blocks: [ - { - type: block, - depth: Math.max(0, Math.min(MAX_DEPTH, depth)), - }, - ], - }; -} - -function getListBlockType(tag: string, lastList: ?string): ?DraftBlockType { +const getListBlockType = (tag: string, lastList: ?string): ?DraftBlockType => { if (tag === 'li') { return lastList === 'ol' ? 'ordered-list-item' : 'unordered-list-item'; } return null; -} +}; -function getBlockMapSupportedTags( +const getBlockMapSupportedTags = ( blockRenderMap: DraftBlockRenderMap, -): Array { +): Array => { const unstyledElement = blockRenderMap.get('unstyled').element; let tags = Set([]); @@ -157,14 +128,14 @@ function getBlockMapSupportedTags( .filter(tag => tag && tag !== unstyledElement) .toArray() .sort(); -} +}; // custom element conversions -function getMultiMatchedType( +const getMultiMatchedType = ( tag: string, lastList: ?string, multiMatchExtractor: Array, -): ?DraftBlockType { +): ?DraftBlockType => { for (let ii = 0; ii < multiMatchExtractor.length; ii++) { const matchType = multiMatchExtractor[ii](tag, lastList); if (matchType) { @@ -172,13 +143,13 @@ function getMultiMatchedType( } } return null; -} +}; -function getBlockTypeForTag( +const getBlockTypeForTag = ( tag: string, lastList: ?string, blockRenderMap: DraftBlockRenderMap, -): DraftBlockType { +): DraftBlockType => { const matchedTypes = blockRenderMap .filter( (draftBlock: DraftBlockRenderConfig) => @@ -205,14 +176,14 @@ function getBlockTypeForTag( getMultiMatchedType(tag, lastList, [getListBlockType]) || 'unstyled' ); } -} +}; -function processInlineTag( +const processInlineTag = ( tag: string, node: Node, currentStyle: DraftInlineStyle, -): DraftInlineStyle { - var styleToCheck = inlineTags[tag]; +): DraftInlineStyle => { + const styleToCheck = inlineTags[tag]; if (styleToCheck) { currentStyle = currentStyle.add(styleToCheck).toOrderedSet(); } else if (node instanceof HTMLElement) { @@ -249,15 +220,19 @@ function processInlineTag( .toOrderedSet(); } return currentStyle; -} +}; -function joinChunks(A: Chunk, B: Chunk): Chunk { +const joinChunks = ( + A: Chunk, + B: Chunk, + experimentalHasNestedBlocks?: boolean, +): Chunk => { // Sometimes two blocks will touch in the DOM and we need to strip the // extra delimiter to preserve niceness. - var lastInA = A.text.slice(-1); - var firstInB = B.text.slice(0, 1); + const lastInA = A.text.slice(-1); + const firstInB = B.text.slice(0, 1); - if (lastInA === '\r' && firstInB === '\r') { + if (lastInA === '\r' && firstInB === '\r' && !experimentalHasNestedBlocks) { A.text = A.text.slice(0, -1); A.inlines.pop(); A.entities.pop(); @@ -281,32 +256,77 @@ function joinChunks(A: Chunk, B: Chunk): Chunk { entities: A.entities.concat(B.entities), blocks: A.blocks.concat(B.blocks), }; -} +}; /** * Check to see if we have anything like

    ... to create * block tags from. If we do, we can use those and ignore
    tags. If we * don't, we can treat
    tags as meaningful (unstyled) blocks. */ -function containsSemanticBlockMarkup( +const containsSemanticBlockMarkup = ( html: string, blockTags: Array, -): boolean { +): boolean => { return blockTags.some(tag => html.indexOf('<' + tag) !== -1); -} +}; -function hasValidLinkText(link: Node): boolean { +const hasValidLinkText = (link: Node): boolean => { invariant( link instanceof HTMLAnchorElement, 'Link must be an HTMLAnchorElement.', ); - var protocol = link.protocol; + const protocol = link.protocol; return ( protocol === 'http:' || protocol === 'https:' || protocol === 'mailto:' ); -} +}; + +const getWhitespaceChunk = (inEntity: ?string): Chunk => { + return { + ...EMPTY_CHUNK, + text: SPACE, + inlines: [OrderedSet()], + entities: new Array(1).map(none => (inEntity ? inEntity : none)), + }; +}; + +const getSoftNewlineChunk = (): Chunk => { + return { + ...EMPTY_CHUNK, + text: '\n', + inlines: [OrderedSet()], + entities: new Array(1), + }; +}; -function genFragment( +const getChunkedBlock = (props: Object = {}): Block => { + return { + ...EMPTY_BLOCK, + ...props, + }; +}; + +const getBlockDividerChunk = ( + block: DraftBlockType, + depth: number, + parentKey: ?string = null, +): Chunk => { + return { + text: '\r', + inlines: [OrderedSet()], + entities: new Array(1), + blocks: [ + getChunkedBlock({ + parent: parentKey, + key: generateRandomKey(), + type: block, + depth: Math.max(0, Math.min(MAX_DEPTH, depth)), + }), + ], + }; +}; + +const genFragment = ( entityMap: EntityMap, node: Node, inlineStyle: DraftInlineStyle, @@ -315,17 +335,24 @@ function genFragment( blockTags: Array, depth: number, blockRenderMap: DraftBlockRenderMap, - inEntity?: string, -): {chunk: Chunk, entityMap: EntityMap} { - var nodeName = node.nodeName.toLowerCase(); - var newBlock = false; - var nextBlockType = 'unstyled'; - var lastLastBlock = lastBlock; + inEntity?: ?string, + experimentalTreeDataSupport?: boolean, + parentKey?: string, +): {chunk: Chunk, entityMap: EntityMap} => { + const lastLastBlock = lastBlock; + let nodeName = node.nodeName.toLowerCase(); let newEntityMap = entityMap; + let nextBlockType = 'unstyled'; + let newBlock = false; + const inBlockType = + inBlock && getBlockTypeForTag(inBlock, lastList, blockRenderMap); + let chunk = {...EMPTY_CHUNK}; + let newChunk: ?Chunk = null; + let blockKey; // Base Case if (nodeName === '#text') { - var text = node.textContent; + let text = node.textContent; if (text.trim() === '' && inBlock !== 'pre') { return {chunk: getWhitespaceChunk(inEntity), entityMap}; } @@ -353,12 +380,11 @@ function genFragment( // BR tags if (nodeName === 'br') { - if ( - lastLastBlock === 'br' && - (!inBlock || - getBlockTypeForTag(inBlock, lastList, blockRenderMap) === 'unstyled') - ) { - return {chunk: getBlockDividerChunk('unstyled', depth), entityMap}; + if (lastLastBlock === 'br' && (!inBlock || inBlockType === 'unstyled')) { + return { + chunk: getBlockDividerChunk('unstyled', depth, parentKey), + entityMap, + }; } return {chunk: getSoftNewlineChunk(), entityMap}; } @@ -390,9 +416,6 @@ function genFragment( inEntity = DraftEntity.__create('IMAGE', 'MUTABLE', entityConfig || {}); } - var chunk = getEmptyChunk(); - var newChunk: ?Chunk = null; - // Inline tags inlineStyle = processInlineTag(nodeName, node, inlineStyle); @@ -404,32 +427,33 @@ function genFragment( lastList = nodeName; } + const blockType = getBlockTypeForTag(nodeName, lastList, blockRenderMap); + const inListBlock = lastList && inBlock === 'li' && nodeName === 'li'; + const inBlockOrHasNestedBlocks = + (!inBlock || experimentalTreeDataSupport) && + blockTags.indexOf(nodeName) !== -1; + // Block Tags - if (!inBlock && blockTags.indexOf(nodeName) !== -1) { - chunk = getBlockDividerChunk( - getBlockTypeForTag(nodeName, lastList, blockRenderMap), - depth, - ); - inBlock = nodeName; - newBlock = true; - } else if (lastList && inBlock === 'li' && nodeName === 'li') { - chunk = getBlockDividerChunk( - getBlockTypeForTag(nodeName, lastList, blockRenderMap), - depth, - ); + if (inListBlock || inBlockOrHasNestedBlocks) { + chunk = getBlockDividerChunk(blockType, depth, parentKey); + blockKey = chunk.blocks[0].key; inBlock = nodeName; - newBlock = true; + newBlock = !experimentalTreeDataSupport; + } + + // this is required so that we can handle 'ul' and 'ol' + if (inListBlock) { nextBlockType = lastList === 'ul' ? 'unordered-list-item' : 'ordered-list-item'; } // Recurse through children - var child: ?Node = node.firstChild; + let child: ?Node = node.firstChild; if (child != null) { nodeName = child.nodeName.toLowerCase(); } - var entityId: ?string = null; + let entityId: ?string = null; while (child) { if ( @@ -467,16 +491,18 @@ function genFragment( depth, blockRenderMap, entityId || inEntity, + experimentalTreeDataSupport, + blockKey, ); newChunk = generatedChunk; newEntityMap = maybeUpdatedEntityMap; - chunk = joinChunks(chunk, newChunk); - var sibling: ?Node = child.nextSibling; + chunk = joinChunks(chunk, newChunk, experimentalTreeDataSupport); + const sibling: ?Node = child.nextSibling; // Put in a newline to break up blocks inside blocks - if (sibling && blockTags.indexOf(nodeName) >= 0 && inBlock) { + if (!parentKey && sibling && blockTags.indexOf(nodeName) >= 0 && inBlock) { chunk = joinChunks(chunk, getSoftNewlineChunk()); } if (sibling) { @@ -486,18 +512,22 @@ function genFragment( } if (newBlock) { - chunk = joinChunks(chunk, getBlockDividerChunk(nextBlockType, depth)); + chunk = joinChunks( + chunk, + getBlockDividerChunk(nextBlockType, depth, parentKey), + ); } return {chunk, entityMap: newEntityMap}; -} +}; -function getChunkForHTML( +const getChunkForHTML = ( html: string, DOMBuilder: Function, blockRenderMap: DraftBlockRenderMap, entityMap: EntityMap, -): ?{chunk: Chunk, entityMap: EntityMap} { + experimentalTreeDataSupport?: boolean, +): ?{chunk: Chunk, entityMap: EntityMap} => { html = html .trim() .replace(REGEX_CR, '') @@ -507,7 +537,7 @@ function getChunkForHTML( const supportedBlockTags = getBlockMapSupportedTags(blockRenderMap); - var safeBody = DOMBuilder(html); + const safeBody = DOMBuilder(html); if (!safeBody) { return null; } @@ -516,13 +546,13 @@ function getChunkForHTML( // Sometimes we aren't dealing with content that contains nice semantic // tags. In this case, use divs to separate everything out into paragraphs // and hope for the best. - var workingBlocks = containsSemanticBlockMarkup(html, supportedBlockTags) + const workingBlocks = containsSemanticBlockMarkup(html, supportedBlockTags) ? supportedBlockTags : ['div']; // Start with -1 block depth to offset the fact that we are passing in a fake // UL block to start with. - var {chunk, entityMap: newEntityMap} = genFragment( + const fragment = genFragment( entityMap, safeBody, OrderedSet(), @@ -531,8 +561,13 @@ function getChunkForHTML( workingBlocks, -1, blockRenderMap, + undefined, + experimentalTreeDataSupport, ); + let chunk = fragment.chunk; + const newEntityMap = fragment.entityMap; + // join with previous block to prevent weirdness on paste if (chunk.text.indexOf('\r') === 0) { chunk = { @@ -553,7 +588,11 @@ function getChunkForHTML( // If we saw no block tags, put an unstyled one in if (chunk.blocks.length === 0) { - chunk.blocks.push({type: 'unstyled', depth: 0}); + chunk.blocks.push({ + ...EMPTY_CHUNK, + type: 'unstyled', + depth: 0, + }); } // Sometimes we start with text that isn't in a block, which is then @@ -564,60 +603,128 @@ function getChunkForHTML( } return {chunk, entityMap: newEntityMap}; -} +}; -function convertFromHTMLtoContentBlocks( +const convertChunkToContentBlocks = ( + chunk: Chunk, + experimentalTreeDataSupport: boolean, +): ?Array => { + if (!chunk || !chunk.text || !Array.isArray(chunk.blocks)) { + return null; + } + + const initialState = { + cacheRef: {}, + contentBlocks: [], + }; + + let start = 0; + + const {blocks: rawBlocks, inlines: rawInlines, entities: rawEntities} = chunk; + + const BlockNodeRecord = experimentalTreeDataSupport + ? ContentBlockNode + : ContentBlock; + + return chunk.text.split('\r').reduce((acc, textBlock, index) => { + // Make absolutely certain that our text is acceptable. + textBlock = sanitizeDraftText(textBlock); + + const block = rawBlocks[index]; + const end = start + textBlock.length; + const inlines = rawInlines.slice(start, end); + const entities = rawEntities.slice(start, end); + const characterList = List( + inlines.map((style, index) => { + const data = {style, entity: (null: ?string)}; + if (entities[index]) { + data.entity = entities[index]; + } + return CharacterMetadata.create(data); + }), + ); + start = end + 1; + + const {depth, type, parent} = block; + + const key = block.key || generateRandomKey(); + + const blockNode = new BlockNodeRecord({ + key, + parent, + type, + depth, + text: textBlock, + characterList, + prevSibling: + index === 0 || rawBlocks[index - 1].parent !== parent + ? null + : rawBlocks[index - 1].key, + nextSibling: + index === rawBlocks.length - 1 || rawBlocks[index + 1].parent !== parent + ? null + : rawBlocks[index + 1].key, + }); + + // childrens add themselves to their parents since we are iterating in order + if (parent) { + const parentIndex = acc.cacheRef[parent]; + const parentRecord = acc.contentBlocks[parentIndex]; + acc.contentBlocks[parentIndex] = parentRecord.set( + 'children', + parentRecord.children.push(key), + ); + } + + // insert node + acc.contentBlocks.push(blockNode); + + // cache ref for building links + acc.cacheRef[blockNode.key] = index; + + return acc; + }, initialState).contentBlocks; +}; + +/** + * Note for `experimentalTreeDataSupport`: + * + * This is unstable and not part of the public API and should not be used by + * production systems. This functionality may be update/removed without notice. + */ +const convertFromHTMLtoContentBlocks = ( html: string, DOMBuilder: Function = getSafeBodyFromHTML, blockRenderMap?: DraftBlockRenderMap = DefaultDraftBlockRenderMap, -): ?{contentBlocks: ?Array, entityMap: EntityMap} { + experimentalTreeDataSupport?: boolean = false, +): ?{contentBlocks: ?Array, entityMap: EntityMap} => { // Be ABSOLUTELY SURE that the dom builder you pass here won't execute // arbitrary code in whatever environment you're running this in. For an // example of how we try to do this in-browser, see getSafeBodyFromHTML. // TODO: replace DraftEntity with an OrderedMap here - var chunkData = getChunkForHTML( + const chunkData = getChunkForHTML( html, DOMBuilder, blockRenderMap, DraftEntity, + experimentalTreeDataSupport, ); if (chunkData == null) { return null; } - const {chunk, entityMap: newEntityMap} = chunkData; + const {chunk, entityMap} = chunkData; + const contentBlocks = convertChunkToContentBlocks( + chunk, + experimentalTreeDataSupport, + ); - var start = 0; return { - contentBlocks: chunk.text.split('\r').map((textBlock, ii) => { - // Make absolutely certain that our text is acceptable. - textBlock = sanitizeDraftText(textBlock); - var end = start + textBlock.length; - var inlines = nullthrows(chunk).inlines.slice(start, end); - var entities = nullthrows(chunk).entities.slice(start, end); - var characterList = List( - inlines.map((style, ii) => { - var data = {style, entity: (null: ?string)}; - if (entities[ii]) { - data.entity = entities[ii]; - } - return CharacterMetadata.create(data); - }), - ); - start = end + 1; - - return new ContentBlock({ - key: generateRandomKey(), - type: nullthrows(chunk).blocks[ii].type, - depth: nullthrows(chunk).blocks[ii].depth, - text: textBlock, - characterList, - }); - }), - entityMap: newEntityMap, + contentBlocks, + entityMap, }; -} +}; module.exports = convertFromHTMLtoContentBlocks; diff --git a/src/model/paste/__tests__/__snapshots__/DraftPasteProcessor-test.js.snap b/src/model/paste/__tests__/__snapshots__/DraftPasteProcessor-test.js.snap index eef732d1d6..403369796a 100644 --- a/src/model/paste/__tests__/__snapshots__/DraftPasteProcessor-test.js.snap +++ b/src/model/paste/__tests__/__snapshots__/DraftPasteProcessor-test.js.snap @@ -103,7 +103,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key39", + "key": "key58", "text": " hellothere ", "type": "unstyled", }, @@ -213,7 +213,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key40", + "key": "key59", "text": " hellothere ", "type": "unstyled", }, @@ -267,7 +267,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key41", + "key": "key60", "text": "hellothere", "type": "unstyled", }, @@ -301,7 +301,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key37", + "key": "key54", "text": "hello", "type": "paragraph", }, @@ -326,7 +326,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key38", + "key": "key56", "text": "what", "type": "paragraph", }, @@ -356,7 +356,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key35", + "key": "key53", "text": "hi ", "type": "unstyled", @@ -386,7 +386,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key36", + "key": "key52", "text": "hello", "type": "unstyled", }, @@ -476,7 +476,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key25", + "key": "key38", "text": " hi hello @@ -497,7 +497,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key18", + "key": "key33", "text": " ", "type": "unstyled", }, @@ -514,7 +514,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key19", + "key": "key29", "text": "hi", "type": "paragraph", }, @@ -543,7 +543,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key20", + "key": "key31", "text": "hello", "type": "paragraph", }, @@ -561,7 +561,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key5", + "key": "key9", "text": " ", "type": "unstyled", }, @@ -594,7 +594,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key6", + "key": "key7", "text": "what ", "type": "unordered-list-item", @@ -689,7 +689,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key30", + "key": "key47", "text": "This is a link, yep.", "type": "unstyled", }, @@ -711,7 +711,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key7", + "key": "key10", "text": "hi", "type": "header-one", }, @@ -728,7 +728,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key8", + "key": "key12", "text": "hi", "type": "header-two", }, @@ -746,7 +746,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key2", + "key": "key6", "text": " ", "type": "unstyled", }, @@ -763,7 +763,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key3", + "key": "key2", "text": "hi", "type": "ordered-list-item", }, @@ -1058,7 +1058,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key33", + "key": "key50", "text": "This is a link, yep.", "type": "unstyled", }, @@ -1152,7 +1152,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key32", + "key": "key49", "text": "This is a link, yep.", "type": "unstyled", }, @@ -1178,7 +1178,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key9", + "key": "key14", "text": "hi ", "type": "header-one", }, @@ -1227,7 +1227,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key10", + "key": "key17", "text": "whatever ", "type": "unstyled", }, @@ -1248,7 +1248,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key11", + "key": "key18", "text": "hi ", "type": "header-two", }, @@ -1310,7 +1310,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key12", + "key": "key20", "text": " Word , ", "type": "paragraph", }, @@ -1453,7 +1453,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key29", + "key": "key46", "text": " Bold Italic .", "type": "unstyled", }, @@ -1507,7 +1507,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key42", + "key": "key71", "text": "what ", "type": "unstyled", }, @@ -1532,7 +1532,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key43", + "key": "key61", "text": "what", "type": "unordered-list-item", }, @@ -1629,7 +1629,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key44", + "key": "key63", "text": " what ", "type": "unordered-list-item", }, @@ -1650,7 +1650,7 @@ Array [ ], "data": Object {}, "depth": 1, - "key": "key45", + "key": "key64", "text": "one", "type": "ordered-list-item", }, @@ -1671,7 +1671,7 @@ Array [ ], "data": Object {}, "depth": 1, - "key": "key46", + "key": "key66", "text": "two", "type": "ordered-list-item", }, @@ -1696,7 +1696,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key47", + "key": "key69", "text": "what", "type": "unordered-list-item", }, @@ -1790,7 +1790,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key34", + "key": "key51", "text": "This is a link, yep.", "type": "unstyled", }, @@ -1888,7 +1888,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key27", + "key": "key42", "text": " hello there ", @@ -1988,7 +1988,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key28", + "key": "key44", "text": " hello there ", @@ -2036,7 +2036,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key13", + "key": "key22", "text": "hello hi", "type": "unstyled", }, @@ -2082,7 +2082,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key14", + "key": "key23", "text": "hello hi", "type": "unstyled", }, @@ -2128,7 +2128,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key15", + "key": "key24", "text": "hello hi", "type": "unstyled", }, @@ -2218,7 +2218,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key31", + "key": "key48", "text": "A cool link, yep.", "type": "unstyled", }, @@ -2264,7 +2264,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key21", + "key": "key34", "text": "hi hello", "type": "unstyled", @@ -2367,7 +2367,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key26", + "key": "key40", "text": " hello there ", "type": "unstyled", }, @@ -2393,7 +2393,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key23", + "key": "key37", "text": "hi ", "type": "unstyled", @@ -2423,7 +2423,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key24", + "key": "key36", "text": "hello", "type": "unstyled", }, @@ -2465,7 +2465,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key22", + "key": "key35", "text": "hihello", "type": "unstyled", }, @@ -2487,7 +2487,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key16", + "key": "key25", "text": "hi", "type": "unstyled", }, @@ -2516,7 +2516,7 @@ Array [ ], "data": Object {}, "depth": 0, - "key": "key17", + "key": "key27", "text": "hello", "type": "unstyled", },