Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

762 строки
23 KiB

  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. * @flow
  9. * @emails oncall+draft_js
  10. */
  11. 'use strict';
  12. import type { BlockNodeRecord } from "./BlockNodeRecord";
  13. import type { DraftBlockRenderMap } from "./DraftBlockRenderMap";
  14. import type { DraftInlineStyle } from "./DraftInlineStyle";
  15. import type { EntityMap } from "./EntityMap";
  16. const CharacterMetadata = require("./CharacterMetadata");
  17. const ContentBlock = require("./ContentBlock");
  18. const ContentBlockNode = require("./ContentBlockNode");
  19. const DefaultDraftBlockRenderMap = require("./DefaultDraftBlockRenderMap");
  20. const DraftEntity = require("./DraftEntity");
  21. const URI = require("fbjs/lib/URI");
  22. const cx = require("fbjs/lib/cx");
  23. const generateRandomKey = require("./generateRandomKey");
  24. const getSafeBodyFromHTML = require("./getSafeBodyFromHTML");
  25. const gkx = require("./gkx");
  26. const {
  27. List,
  28. Map,
  29. OrderedSet
  30. } = require("immutable");
  31. const isHTMLAnchorElement = require("./isHTMLAnchorElement");
  32. const isHTMLBRElement = require("./isHTMLBRElement");
  33. const isHTMLElement = require("./isHTMLElement");
  34. const isHTMLImageElement = require("./isHTMLImageElement");
  35. const experimentalTreeDataSupport = gkx('draft_tree_data_support');
  36. const NBSP = ' ';
  37. const SPACE = ' '; // used for replacing characters in HTML
  38. const REGEX_CR = new RegExp('\r', 'g');
  39. const REGEX_LF = new RegExp('\n', 'g');
  40. const REGEX_LEADING_LF = new RegExp('^\n', 'g');
  41. const REGEX_NBSP = new RegExp(NBSP, 'g');
  42. const REGEX_CARRIAGE = new RegExp('
?', 'g');
  43. const REGEX_ZWS = new RegExp('​?', 'g'); // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
  44. const boldValues = ['bold', 'bolder', '500', '600', '700', '800', '900'];
  45. const notBoldValues = ['light', 'lighter', 'normal', '100', '200', '300', '400'];
  46. const anchorAttr = ['className', 'href', 'rel', 'target', 'title'];
  47. const imgAttr = ['alt', 'className', 'height', 'src', 'width'];
  48. const knownListItemDepthClasses = {
  49. [cx('public/DraftStyleDefault/depth0')]: 0,
  50. [cx('public/DraftStyleDefault/depth1')]: 1,
  51. [cx('public/DraftStyleDefault/depth2')]: 2,
  52. [cx('public/DraftStyleDefault/depth3')]: 3,
  53. [cx('public/DraftStyleDefault/depth4')]: 4
  54. };
  55. const HTMLTagToRawInlineStyleMap: Map<string, string> = Map({
  56. b: 'BOLD',
  57. code: 'CODE',
  58. del: 'STRIKETHROUGH',
  59. em: 'ITALIC',
  60. i: 'ITALIC',
  61. s: 'STRIKETHROUGH',
  62. strike: 'STRIKETHROUGH',
  63. strong: 'BOLD',
  64. u: 'UNDERLINE',
  65. mark: 'HIGHLIGHT'
  66. });
  67. type BlockTypeMap = Map<string, string | Array<string>>;
  68. /**
  69. * Build a mapping from HTML tags to draftjs block types
  70. * out of a BlockRenderMap.
  71. *
  72. * The BlockTypeMap for the default BlockRenderMap looks like this:
  73. * Map({
  74. * h1: 'header-one',
  75. * h2: 'header-two',
  76. * h3: 'header-three',
  77. * h4: 'header-four',
  78. * h5: 'header-five',
  79. * h6: 'header-six',
  80. * blockquote: 'blockquote',
  81. * figure: 'atomic',
  82. * pre: ['code-block'],
  83. * div: 'unstyled',
  84. * p: 'unstyled',
  85. * li: ['ordered-list-item', 'unordered-list-item'],
  86. * })
  87. */
  88. const buildBlockTypeMap = (blockRenderMap: DraftBlockRenderMap): BlockTypeMap => {
  89. const blockTypeMap = {};
  90. blockRenderMap.mapKeys((blockType, desc) => {
  91. const elements = [desc.element];
  92. if (desc.aliasedElements !== undefined) {
  93. elements.push(...desc.aliasedElements);
  94. }
  95. elements.forEach(element => {
  96. if (blockTypeMap[element] === undefined) {
  97. blockTypeMap[element] = blockType;
  98. } else if (typeof blockTypeMap[element] === 'string') {
  99. blockTypeMap[element] = [blockTypeMap[element], blockType];
  100. } else {
  101. blockTypeMap[element].push(blockType);
  102. }
  103. });
  104. });
  105. return Map(blockTypeMap);
  106. };
  107. /**
  108. * If we're pasting from one DraftEditor to another we can check to see if
  109. * existing list item depth classes are being used and preserve this style
  110. */
  111. const getListItemDepth = (node: HTMLElement, depth: number = 0): number => {
  112. Object.keys(knownListItemDepthClasses).some(depthClass => {
  113. if (node.classList.contains(depthClass)) {
  114. depth = knownListItemDepthClasses[depthClass];
  115. }
  116. });
  117. return depth;
  118. };
  119. /**
  120. * Return true if the provided HTML Element can be used to build a
  121. * Draftjs-compatible link.
  122. */
  123. const isValidAnchor = (node: Node) => {
  124. if (!isHTMLAnchorElement(node)) {
  125. return false;
  126. }
  127. const anchorNode: HTMLAnchorElement = (node: any);
  128. return !!(anchorNode.href && (anchorNode.protocol === 'http:' || anchorNode.protocol === 'https:' || anchorNode.protocol === 'mailto:'));
  129. };
  130. /**
  131. * Return true if the provided HTML Element can be used to build a
  132. * Draftjs-compatible image.
  133. */
  134. const isValidImage = (node: Node): boolean => {
  135. if (!isHTMLImageElement(node)) {
  136. return false;
  137. }
  138. const imageNode: HTMLImageElement = (node: any);
  139. return !!(imageNode.attributes.getNamedItem('src') && imageNode.attributes.getNamedItem('src').value);
  140. };
  141. /**
  142. * Try to guess the inline style of an HTML element based on its css
  143. * styles (font-weight, font-style and text-decoration).
  144. */
  145. const styleFromNodeAttributes = (node: Node, style: DraftInlineStyle): DraftInlineStyle => {
  146. if (!isHTMLElement(node)) {
  147. return style;
  148. }
  149. const htmlElement: HTMLElement = (node: any);
  150. const fontWeight = htmlElement.style.fontWeight;
  151. const fontStyle = htmlElement.style.fontStyle;
  152. const textDecoration = htmlElement.style.textDecoration;
  153. return style.withMutations(style => {
  154. if (boldValues.indexOf(fontWeight) >= 0) {
  155. style.add('BOLD');
  156. } else if (notBoldValues.indexOf(fontWeight) >= 0) {
  157. style.remove('BOLD');
  158. }
  159. if (fontStyle === 'italic') {
  160. style.add('ITALIC');
  161. } else if (fontStyle === 'normal') {
  162. style.remove('ITALIC');
  163. }
  164. if (textDecoration === 'underline') {
  165. style.add('UNDERLINE');
  166. }
  167. if (textDecoration === 'line-through') {
  168. style.add('STRIKETHROUGH');
  169. }
  170. if (textDecoration === 'none') {
  171. style.remove('UNDERLINE');
  172. style.remove('STRIKETHROUGH');
  173. }
  174. });
  175. };
  176. /**
  177. * Determine if a nodeName is a list type, 'ul' or 'ol'
  178. */
  179. const isListNode = (nodeName: ?string): boolean => nodeName === 'ul' || nodeName === 'ol';
  180. /**
  181. * ContentBlockConfig is a mutable data structure that holds all
  182. * the information required to build a ContentBlock and an array of
  183. * all the child nodes (childConfigs).
  184. * It is being used a temporary data structure by the
  185. * ContentBlocksBuilder class.
  186. */
  187. type ContentBlockConfig = {
  188. characterList: List<CharacterMetadata>,
  189. data?: Map<any, any>,
  190. depth?: number,
  191. key: string,
  192. text: string,
  193. type: string,
  194. children: List<string>,
  195. parent: ?string,
  196. prevSibling: ?string,
  197. nextSibling: ?string,
  198. childConfigs: Array<ContentBlockConfig>,
  199. ...
  200. };
  201. /**
  202. * ContentBlocksBuilder builds a list of ContentBlocks and an Entity Map
  203. * out of one (or several) HTMLElement(s).
  204. *
  205. * The algorithm has two passes: first it builds a tree of ContentBlockConfigs
  206. * by walking through the HTML nodes and their children, then it walks the
  207. * ContentBlockConfigs tree to compute parents/siblings and create
  208. * the actual ContentBlocks.
  209. *
  210. * Typical usage is:
  211. * new ContentBlocksBuilder()
  212. * .addDOMNode(someHTMLNode)
  213. * .addDOMNode(someOtherHTMLNode)
  214. * .getContentBlocks();
  215. *
  216. */
  217. class ContentBlocksBuilder {
  218. // Most of the method in the class depend on the state of the content builder
  219. // (i.e. currentBlockType, currentDepth, currentEntity etc.). Though it may
  220. // be confusing at first, it made the code simpler than the alternative which
  221. // is to pass those values around in every call.
  222. // The following attributes are used to accumulate text and styles
  223. // as we are walking the HTML node tree.
  224. characterList: List<CharacterMetadata> = List();
  225. currentBlockType: string = 'unstyled';
  226. currentDepth: number = 0;
  227. currentEntity: ?string = null;
  228. currentText: string = '';
  229. wrapper: ?string = null; // Describes the future ContentState as a tree of content blocks
  230. blockConfigs: Array<ContentBlockConfig> = []; // The content blocks generated from the blockConfigs
  231. contentBlocks: Array<BlockNodeRecord> = []; // Entity map use to store links and images found in the HTML nodes
  232. entityMap: EntityMap = DraftEntity; // Map HTML tags to draftjs block types and disambiguation function
  233. blockTypeMap: BlockTypeMap;
  234. disambiguate: (string, ?string) => ?string;
  235. constructor(blockTypeMap: BlockTypeMap, disambiguate: (string, ?string) => ?string): void {
  236. this.clear();
  237. this.blockTypeMap = blockTypeMap;
  238. this.disambiguate = disambiguate;
  239. }
  240. /**
  241. * Clear the internal state of the ContentBlocksBuilder
  242. */
  243. clear(): void {
  244. this.characterList = List();
  245. this.blockConfigs = [];
  246. this.currentBlockType = 'unstyled';
  247. this.currentDepth = 0;
  248. this.currentEntity = null;
  249. this.currentText = '';
  250. this.entityMap = DraftEntity;
  251. this.wrapper = null;
  252. this.contentBlocks = [];
  253. }
  254. /**
  255. * Add an HTMLElement to the ContentBlocksBuilder
  256. */
  257. addDOMNode(node: Node): ContentBlocksBuilder {
  258. this.contentBlocks = [];
  259. this.currentDepth = 0; // Converts the HTML node to block config
  260. this.blockConfigs.push(...this._toBlockConfigs([node], OrderedSet())); // There might be some left over text in the builder's
  261. // internal state, if so make a ContentBlock out of it.
  262. this._trimCurrentText();
  263. if (this.currentText !== '') {
  264. this.blockConfigs.push(this._makeBlockConfig());
  265. } // for chaining
  266. return this;
  267. }
  268. /**
  269. * Return the ContentBlocks and the EntityMap that corresponds
  270. * to the previously added HTML nodes.
  271. */
  272. getContentBlocks(): {
  273. contentBlocks: ?Array<BlockNodeRecord>,
  274. entityMap: EntityMap,
  275. ...
  276. } {
  277. if (this.contentBlocks.length === 0) {
  278. if (experimentalTreeDataSupport) {
  279. this._toContentBlocks(this.blockConfigs);
  280. } else {
  281. this._toFlatContentBlocks(this.blockConfigs);
  282. }
  283. }
  284. return {
  285. contentBlocks: this.contentBlocks,
  286. entityMap: this.entityMap
  287. };
  288. } // ***********************************WARNING******************************
  289. // The methods below this line are private - don't call them directly.
  290. /**
  291. * Generate a new ContentBlockConfig out of the current internal state
  292. * of the builder, then clears the internal state.
  293. */
  294. _makeBlockConfig(config: Object = {}): ContentBlockConfig {
  295. const key = config.key || generateRandomKey();
  296. const block = {
  297. key,
  298. type: this.currentBlockType,
  299. text: this.currentText,
  300. characterList: this.characterList,
  301. depth: this.currentDepth,
  302. parent: null,
  303. children: List(),
  304. prevSibling: null,
  305. nextSibling: null,
  306. childConfigs: [],
  307. ...config
  308. };
  309. this.characterList = List();
  310. this.currentBlockType = 'unstyled';
  311. this.currentText = '';
  312. return block;
  313. }
  314. /**
  315. * Converts an array of HTML elements to a multi-root tree of content
  316. * block configs. Some text content may be left in the builders internal
  317. * state to enable chaining sucessive calls.
  318. */
  319. _toBlockConfigs(nodes: Array<Node>, style: DraftInlineStyle): Array<ContentBlockConfig> {
  320. const blockConfigs = [];
  321. for (let i = 0; i < nodes.length; i++) {
  322. const node = nodes[i];
  323. const nodeName = node.nodeName.toLowerCase();
  324. if (nodeName === 'body' || isListNode(nodeName)) {
  325. // body, ol and ul are 'block' type nodes so create a block config
  326. // with the text accumulated so far (if any)
  327. this._trimCurrentText();
  328. if (this.currentText !== '') {
  329. blockConfigs.push(this._makeBlockConfig());
  330. } // body, ol and ul nodes are ignored, but their children are inlined in
  331. // the parent block config.
  332. const wasCurrentDepth = this.currentDepth;
  333. const wasWrapper = this.wrapper;
  334. if (isListNode(nodeName)) {
  335. this.wrapper = nodeName;
  336. if (isListNode(wasWrapper)) {
  337. this.currentDepth++;
  338. }
  339. }
  340. blockConfigs.push(...this._toBlockConfigs(Array.from(node.childNodes), style));
  341. this.currentDepth = wasCurrentDepth;
  342. this.wrapper = wasWrapper;
  343. continue;
  344. }
  345. let blockType = this.blockTypeMap.get(nodeName);
  346. if (blockType !== undefined) {
  347. // 'block' type node means we need to create a block config
  348. // with the text accumulated so far (if any)
  349. this._trimCurrentText();
  350. if (this.currentText !== '') {
  351. blockConfigs.push(this._makeBlockConfig());
  352. }
  353. const wasCurrentDepth = this.currentDepth;
  354. const wasWrapper = this.wrapper;
  355. this.wrapper = nodeName === 'pre' ? 'pre' : this.wrapper;
  356. if (typeof blockType !== 'string') {
  357. blockType = this.disambiguate(nodeName, this.wrapper) || blockType[0] || 'unstyled';
  358. }
  359. if (!experimentalTreeDataSupport && isHTMLElement(node) && (blockType === 'unordered-list-item' || blockType === 'ordered-list-item')) {
  360. const htmlElement: HTMLElement = (node: any);
  361. this.currentDepth = getListItemDepth(htmlElement, this.currentDepth);
  362. }
  363. const key = generateRandomKey();
  364. const childConfigs = this._toBlockConfigs(Array.from(node.childNodes), style);
  365. this._trimCurrentText();
  366. blockConfigs.push(this._makeBlockConfig({
  367. key,
  368. childConfigs,
  369. type: blockType
  370. }));
  371. this.currentDepth = wasCurrentDepth;
  372. this.wrapper = wasWrapper;
  373. continue;
  374. }
  375. if (nodeName === '#text') {
  376. this._addTextNode(node, style);
  377. continue;
  378. }
  379. if (nodeName === 'br') {
  380. this._addBreakNode(node, style);
  381. continue;
  382. }
  383. if (isValidImage(node)) {
  384. this._addImgNode(node, style);
  385. continue;
  386. }
  387. if (isValidAnchor(node)) {
  388. this._addAnchorNode(node, blockConfigs, style);
  389. continue;
  390. }
  391. let newStyle = style;
  392. if (HTMLTagToRawInlineStyleMap.has(nodeName)) {
  393. newStyle = newStyle.add(HTMLTagToRawInlineStyleMap.get(nodeName));
  394. }
  395. newStyle = styleFromNodeAttributes(node, newStyle);
  396. blockConfigs.push(...this._toBlockConfigs(Array.from(node.childNodes), newStyle));
  397. }
  398. return blockConfigs;
  399. }
  400. /**
  401. * Append a string of text to the internal buffer.
  402. */
  403. _appendText(text: string, style: DraftInlineStyle) {
  404. this.currentText += text;
  405. const characterMetadata = CharacterMetadata.create({
  406. style,
  407. entity: this.currentEntity
  408. });
  409. this.characterList = this.characterList.push(...Array(text.length).fill(characterMetadata));
  410. }
  411. /**
  412. * Trim the text in the internal buffer.
  413. */
  414. _trimCurrentText() {
  415. const l = this.currentText.length;
  416. let begin = l - this.currentText.trimLeft().length;
  417. let end = this.currentText.trimRight().length; // We should not trim whitespaces for which an entity is defined.
  418. let entity = this.characterList.findEntry(characterMetadata => characterMetadata.getEntity() !== null);
  419. begin = entity !== undefined ? Math.min(begin, entity[0]) : begin;
  420. entity = this.characterList.reverse().findEntry(characterMetadata => characterMetadata.getEntity() !== null);
  421. end = entity !== undefined ? Math.max(end, l - entity[0]) : end;
  422. if (begin > end) {
  423. this.currentText = '';
  424. this.characterList = List();
  425. } else {
  426. this.currentText = this.currentText.slice(begin, end);
  427. this.characterList = this.characterList.slice(begin, end);
  428. }
  429. }
  430. /**
  431. * Add the content of an HTML text node to the internal state
  432. */
  433. _addTextNode(node: Node, style: DraftInlineStyle) {
  434. let text = node.textContent;
  435. const trimmedText = text.trim(); // If we are not in a pre block and the trimmed content is empty,
  436. // normalize to a single space.
  437. if (trimmedText === '' && this.wrapper !== 'pre') {
  438. text = ' ';
  439. }
  440. if (this.wrapper !== 'pre') {
  441. // Trim leading line feed, which is invisible in HTML
  442. text = text.replace(REGEX_LEADING_LF, ''); // Can't use empty string because MSWord
  443. text = text.replace(REGEX_LF, SPACE);
  444. }
  445. this._appendText(text, style);
  446. }
  447. _addBreakNode(node: Node, style: DraftInlineStyle) {
  448. if (!isHTMLBRElement(node)) {
  449. return;
  450. }
  451. this._appendText('\n', style);
  452. }
  453. /**
  454. * Add the content of an HTML img node to the internal state
  455. */
  456. _addImgNode(node: Node, style: DraftInlineStyle) {
  457. if (!isHTMLImageElement(node)) {
  458. return;
  459. }
  460. const image: HTMLImageElement = (node: any);
  461. const entityConfig = {};
  462. imgAttr.forEach(attr => {
  463. const imageAttribute = image.getAttribute(attr);
  464. if (imageAttribute) {
  465. entityConfig[attr] = imageAttribute;
  466. }
  467. }); // TODO: T15530363 update this when we remove DraftEntity entirely
  468. this.currentEntity = this.entityMap.__create('IMAGE', 'IMMUTABLE', entityConfig); // The child text node cannot just have a space or return as content (since
  469. // we strip those out), unless the image is for presentation only.
  470. // See https://github.com/facebook/draft-js/issues/231 for some context.
  471. if (gkx('draftjs_fix_paste_for_img')) {
  472. if (image.getAttribute('role') !== 'presentation') {
  473. this._appendText('\ud83d\udcf7', style);
  474. }
  475. } else {
  476. this._appendText('\ud83d\udcf7', style);
  477. }
  478. this.currentEntity = null;
  479. }
  480. /**
  481. * Add the content of an HTML 'a' node to the internal state. Child nodes
  482. * (if any) are converted to Block Configs and appended to the provided
  483. * blockConfig array.
  484. */
  485. _addAnchorNode(node: Node, blockConfigs: Array<ContentBlockConfig>, style: DraftInlineStyle) {
  486. // The check has already been made by isValidAnchor but
  487. // we have to do it again to keep flow happy.
  488. if (!isHTMLAnchorElement(node)) {
  489. return;
  490. }
  491. const anchor: HTMLAnchorElement = (node: any);
  492. const entityConfig = {};
  493. anchorAttr.forEach(attr => {
  494. const anchorAttribute = anchor.getAttribute(attr);
  495. if (anchorAttribute) {
  496. entityConfig[attr] = anchorAttribute;
  497. }
  498. });
  499. entityConfig.url = new URI(anchor.href).toString(); // TODO: T15530363 update this when we remove DraftEntity completely
  500. this.currentEntity = this.entityMap.__create('LINK', 'MUTABLE', entityConfig || {});
  501. blockConfigs.push(...this._toBlockConfigs(Array.from(node.childNodes), style));
  502. this.currentEntity = null;
  503. }
  504. /**
  505. * Walk the BlockConfig tree, compute parent/children/siblings,
  506. * and generate the corresponding ContentBlockNode
  507. */
  508. _toContentBlocks(blockConfigs: Array<ContentBlockConfig>, parent: ?string = null) {
  509. const l = blockConfigs.length - 1;
  510. for (let i = 0; i <= l; i++) {
  511. const config = blockConfigs[i];
  512. config.parent = parent;
  513. config.prevSibling = i > 0 ? blockConfigs[i - 1].key : null;
  514. config.nextSibling = i < l ? blockConfigs[i + 1].key : null;
  515. config.children = List(config.childConfigs.map(child => child.key));
  516. this.contentBlocks.push(new ContentBlockNode({ ...config
  517. }));
  518. this._toContentBlocks(config.childConfigs, config.key);
  519. }
  520. }
  521. /**
  522. * Remove 'useless' container nodes from the block config hierarchy, by
  523. * replacing them with their children.
  524. */
  525. _hoistContainersInBlockConfigs(blockConfigs: Array<ContentBlockConfig>): List<ContentBlockConfig> {
  526. const hoisted = List(blockConfigs).flatMap(blockConfig => {
  527. // Don't mess with useful blocks
  528. if (blockConfig.type !== 'unstyled' || blockConfig.text !== '') {
  529. return [blockConfig];
  530. }
  531. return this._hoistContainersInBlockConfigs(blockConfig.childConfigs);
  532. });
  533. return hoisted;
  534. } // ***********************************************************************
  535. // The two methods below are used for backward compatibility when
  536. // experimentalTreeDataSupport is disabled.
  537. /**
  538. * Same as _toContentBlocks but replaces nested blocks by their
  539. * text content.
  540. */
  541. _toFlatContentBlocks(blockConfigs: Array<ContentBlockConfig>) {
  542. const cleanConfigs = this._hoistContainersInBlockConfigs(blockConfigs);
  543. cleanConfigs.forEach(config => {
  544. const {
  545. text,
  546. characterList
  547. } = this._extractTextFromBlockConfigs(config.childConfigs);
  548. this.contentBlocks.push(new ContentBlock({ ...config,
  549. text: config.text + text,
  550. characterList: config.characterList.concat(characterList)
  551. }));
  552. });
  553. }
  554. /**
  555. * Extract the text and the associated inline styles form an
  556. * array of content block configs.
  557. */
  558. _extractTextFromBlockConfigs(blockConfigs: Array<ContentBlockConfig>): {
  559. text: string,
  560. characterList: List<CharacterMetadata>,
  561. ...
  562. } {
  563. const l = blockConfigs.length - 1;
  564. let text = '';
  565. let characterList = List();
  566. for (let i = 0; i <= l; i++) {
  567. const config = blockConfigs[i];
  568. text += config.text;
  569. characterList = characterList.concat(config.characterList);
  570. if (text !== '' && config.type !== 'unstyled') {
  571. text += '\n';
  572. characterList = characterList.push(characterList.last());
  573. }
  574. const children = this._extractTextFromBlockConfigs(config.childConfigs);
  575. text += children.text;
  576. characterList = characterList.concat(children.characterList);
  577. }
  578. return {
  579. text,
  580. characterList
  581. };
  582. }
  583. }
  584. /**
  585. * Converts an HTML string to an array of ContentBlocks and an EntityMap
  586. * suitable to initialize the internal state of a Draftjs component.
  587. */
  588. const convertFromHTMLToContentBlocks = (html: string, DOMBuilder: Function = getSafeBodyFromHTML, blockRenderMap?: DraftBlockRenderMap = DefaultDraftBlockRenderMap): ?{
  589. contentBlocks: ?Array<BlockNodeRecord>,
  590. entityMap: EntityMap,
  591. ...
  592. } => {
  593. // Be ABSOLUTELY SURE that the dom builder you pass here won't execute
  594. // arbitrary code in whatever environment you're running this in. For an
  595. // example of how we try to do this in-browser, see getSafeBodyFromHTML.
  596. // Remove funky characters from the HTML string
  597. html = html.trim().replace(REGEX_CR, '').replace(REGEX_NBSP, SPACE).replace(REGEX_CARRIAGE, '').replace(REGEX_ZWS, ''); // Build a DOM tree out of the HTML string
  598. const safeBody = DOMBuilder(html);
  599. if (!safeBody) {
  600. return null;
  601. } // Build a BlockTypeMap out of the BlockRenderMap
  602. const blockTypeMap = buildBlockTypeMap(blockRenderMap); // Select the proper block type for the cases where the blockRenderMap
  603. // uses multiple block types for the same html tag.
  604. const disambiguate = (tag: string, wrapper: ?string): ?string => {
  605. if (tag === 'li') {
  606. return wrapper === 'ol' ? 'ordered-list-item' : 'unordered-list-item';
  607. }
  608. return null;
  609. };
  610. return new ContentBlocksBuilder(blockTypeMap, disambiguate).addDOMNode(safeBody).getContentBlocks();
  611. };
  612. module.exports = convertFromHTMLToContentBlocks;