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}> - <${tag}>b${tag}> - `; - - 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}> + <${tag}>b${tag}> + `; + + 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${tag}> `; + 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 = ` +++ `; + + assertConvertFromHTMLToContentBlocks(html_string, { + experimentalTreeDataSupport: true, + }); +}); + +test('converts deeply nested html blocks when experimentalTreeDataSupport is enabled', () => { + const html_string = ` +Hello World!
+lorem ipsum
+
++Hello World!
+lorem ipsum
+
... to create * block tags from. If we do, we can use those and ignore
tags. If we * don't, we can treattags 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", },