├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── config ├── test-compiler.js └── test-setup.js ├── js ├── __test__ │ ├── .eslintrc │ ├── blockTest.js │ ├── commonTest.js │ └── mainTest.js ├── block.js ├── common.js ├── index.js └── list.js ├── lib └── draftjs-to-html.js ├── package.json ├── readme.md ├── rollup.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | config 2 | node_modules 3 | lib 4 | interfaces 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "plugins": [ "mocha" ], 5 | "env": { 6 | "mocha": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*node_modules/fbjs.* 3 | .*node_modules 4 | .*/test/* 5 | 6 | [include] 7 | .*/js/* 8 | 9 | [libs] 10 | ./interfaces/chai.js 11 | ./interfaces/mocha.js 12 | ./interfaces/sinon.js 13 | ./interfaces/draft-js.js 14 | ./interfaces/immutable.js 15 | 16 | [options] 17 | esproposal.class_static_fields=enable 18 | esproposal.class_instance_fields=enable 19 | module.system=haste 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | lib 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | # Commenting this out is preferred by some people, see 3 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 4 | node_modules 5 | CHANGELOG.md 6 | config 7 | docs 8 | interfaces 9 | js 10 | readme.md 11 | scripts 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jyoti Puri 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /config/test-compiler.js: -------------------------------------------------------------------------------- 1 | require("@babel/core"); 2 | function noop() { 3 | return null; 4 | } 5 | require.extensions[".css"] = noop; 6 | require.extensions[".svg"] = noop; 7 | require.extensions[".png"] = noop; 8 | -------------------------------------------------------------------------------- /config/test-setup.js: -------------------------------------------------------------------------------- 1 | require("@babel/register")(); 2 | 3 | var jsdom = require("jsdom"); 4 | const { JSDOM } = jsdom; 5 | 6 | const { document } = new JSDOM({ 7 | url: "http://localhost" 8 | }).window; 9 | global.document = document; 10 | 11 | global.window = document.defaultView; 12 | global.HTMLElement = window.HTMLElement; 13 | global.HTMLAnchorElement = window.HTMLAnchorElement; 14 | 15 | global.navigator = { 16 | userAgent: "node.js" 17 | }; 18 | -------------------------------------------------------------------------------- /js/__test__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ "mocha" ], 4 | "extends": "airbnb", 5 | "globals": { 6 | "describe": true, 7 | "it": true 8 | }, 9 | "rules": { 10 | "import/no-extraneous-dependencies": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /js/__test__/blockTest.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { assert } from 'chai'; 4 | import { 5 | getBlockTag, 6 | trimLeadingZeros, 7 | trimTrailingZeros, 8 | getStylesAtOffset, 9 | sameStyleAsPrevious, 10 | addInlineStyleMarkup, 11 | addStylePropertyMarkup, 12 | } from '../block'; 13 | 14 | describe('getBlockTag test suite', () => { 15 | it('should return correct block tag when getBlockTag is called', () => { 16 | assert.equal(getBlockTag('header-one'), 'h1'); 17 | assert.equal(getBlockTag('unordered-list-item'), 'ul'); 18 | assert.equal(getBlockTag('unstyled'), 'p'); 19 | }); 20 | }); 21 | 22 | describe('trimLeadingZeros test suite', () => { 23 | it('should correctly replace leading blank spaces', () => { 24 | assert.equal(trimLeadingZeros(' testing'), '  testing'); 25 | assert.equal(trimLeadingZeros('tes ting'), 'tes ting'); 26 | assert.equal(trimLeadingZeros('testing '), 'testing '); 27 | assert.equal(trimLeadingZeros(''), ''); 28 | }); 29 | }); 30 | 31 | describe('trimTrailingZeros test suite', () => { 32 | it('should correctly replace trailing blank spaces', () => { 33 | assert.equal(trimTrailingZeros(' testing'), ' testing'); 34 | assert.equal(trimTrailingZeros('tes ting'), 'tes ting'); 35 | assert.equal(trimTrailingZeros('testing '), 'testing  '); 36 | assert.equal(trimTrailingZeros(''), ''); 37 | }); 38 | }); 39 | 40 | describe('getStylesAtOffset test suite', () => { 41 | it('should return correct styles at some offset', () => { 42 | const inlineStyles = { 43 | BOLD: [true, true], 44 | ITALIC: [false, false], 45 | UNDERLINE: [true, false], 46 | STRIKETHROUGH: [false, false], 47 | CODE: [true, false], 48 | SUBSCRIPT: [true, false], 49 | SUPERSCRIPT: [true, false], 50 | COLOR: ['rgb(97,189,109)', 'rgb(26,188,156)'], 51 | BGCOLOR: ['rgb(99,199,199)', 'rgb(28,189,176)'], 52 | FONTSIZE: [10, 20], 53 | FONTFAMILY: ['Arial', 'Georgia'], 54 | }; 55 | let styles = getStylesAtOffset(inlineStyles, 0); 56 | assert.equal(styles.COLOR, 'rgb(97,189,109)'); 57 | assert.equal(styles.BGCOLOR, 'rgb(99,199,199)'); 58 | assert.equal(styles.FONTSIZE, 10); 59 | assert.equal(styles.FONTFAMILY, 'Arial'); 60 | assert.equal(styles.ITALIC, undefined); 61 | assert.equal(styles.UNDERLINE, true); 62 | assert.equal(styles.SUBSCRIPT, true); 63 | assert.equal(styles.SUPERSCRIPT, true); 64 | assert.equal(styles.BOLD, true); 65 | styles = getStylesAtOffset(inlineStyles, 1); 66 | assert.equal(styles.COLOR, 'rgb(26,188,156)'); 67 | assert.equal(styles.BGCOLOR, 'rgb(28,189,176)'); 68 | assert.equal(styles.FONTSIZE, 20); 69 | assert.equal(styles.FONTFAMILY, 'Georgia'); 70 | assert.equal(styles.ITALIC, undefined); 71 | assert.equal(styles.UNDERLINE, undefined); 72 | assert.equal(styles.SUBSCRIPT, undefined); 73 | assert.equal(styles.SUPERSCRIPT, undefined); 74 | assert.equal(styles.BOLD, true); 75 | }); 76 | }); 77 | 78 | describe('sameStyleAsPrevious test suite', () => { 79 | it('should return true ifstyles at offset is same as style at previous offset', () => { 80 | const inlineStyles = { 81 | BOLD: [true, true, false], 82 | ITALIC: [false, false, true], 83 | UNDERLINE: [true, true, false], 84 | COLOR: ['rgb(97,189,109)', 'rgb(26,188,156)', 'rgb(26,188,156)'], 85 | BGCOLOR: ['rgb(97,189,109)', 'rgb(26,188,156)', 'rgb(26,188,156)'], 86 | FONTSIZE: [10, 10, 20], 87 | FONTFAMILY: ['Arial', 'Arial', 'Georgia'], 88 | length: 3, 89 | }; 90 | let sameStyled = sameStyleAsPrevious(inlineStyles, ['BOLD', 'ITALIC', 'UNDERLINE'], 1); 91 | assert.isTrue(sameStyled); 92 | sameStyled = sameStyleAsPrevious(inlineStyles, ['BOLD', 'ITALIC', 'COLOR', 'BGCOLOR'], 1); 93 | assert.isNotTrue(sameStyled); 94 | }); 95 | it('should return false if offset is 0', () => { 96 | const inlineStyles = { 97 | BOLD: [true, true, false], 98 | ITALIC: [false, false, true], 99 | UNDERLINE: [true, true, false], 100 | COLOR: ['rgb(97,189,109)', 'rgb(26,188,156)', 'rgb(26,188,156)'], 101 | BGCOLOR: ['rgb(97,189,109)', 'rgb(26,188,156)', 'rgb(26,188,156)'], 102 | FONTSIZE: [10, 10, 20], 103 | FONTFAMILY: ['Arial', 'Arial', 'Georgia'], 104 | }; 105 | const sameStyled = sameStyleAsPrevious(inlineStyles, ['BOLD', 'ITALIC', 'UNDERLINE'], 0); 106 | assert.isNotTrue(sameStyled); 107 | }); 108 | it('should return false if offset exceeds length', () => { 109 | const inlineStyles = { 110 | BOLD: [true, true, false], 111 | ITALIC: [false, false, true], 112 | UNDERLINE: [true, true, false], 113 | STRIKETHROUGH: [true, true, false], 114 | CODE: [true, true, false], 115 | COLOR: ['rgb(97,189,109)', 'rgb(26,188,156)', 'rgb(26,188,156)'], 116 | FONTSIZE: [10, 10, 20], 117 | FONTFAMILY: ['Arial', 'Arial', 'Georgia'], 118 | }; 119 | const sameStyled = sameStyleAsPrevious( 120 | inlineStyles, 121 | ['BOLD', 'ITALIC', 'UNDERLINE', 'STRIKETHROUGH', 'CODE'], 3, 122 | ); 123 | assert.isNotTrue(sameStyled); 124 | }); 125 | }); 126 | 127 | describe('addInlineStyleMarkup test suite', () => { 128 | let markup = addInlineStyleMarkup('BOLD', 'test'); 129 | assert.equal(markup, 'test'); 130 | markup = addInlineStyleMarkup('ITALIC', 'test'); 131 | assert.equal(markup, 'test'); 132 | markup = addInlineStyleMarkup('UNDERLINE', 'test'); 133 | assert.equal(markup, 'test'); 134 | markup = addInlineStyleMarkup('STRIKETHROUGH', 'test'); 135 | assert.equal(markup, 'test'); 136 | markup = addInlineStyleMarkup('CODE', 'test'); 137 | assert.equal(markup, 'test'); 138 | }); 139 | 140 | describe('addStylePropertyMarkup test suite', () => { 141 | it('should correctly add styles based on styles object', () => { 142 | let markup = addStylePropertyMarkup( 143 | { 144 | COLOR: 'red', 145 | BGCOLOR: 'pink', 146 | FONTSIZE: 10, 147 | FONTFAMILY: 'Arial', 148 | }, 149 | 'test', 150 | ); 151 | assert.equal( 152 | markup, 153 | 'test', 154 | ); 155 | markup = addStylePropertyMarkup({ COLOR: 'red' }, 'test'); 156 | assert.equal(markup, 'test'); 157 | markup = addStylePropertyMarkup({ BGCOLOR: 'pink' }, 'test'); 158 | assert.equal(markup, 'test'); 159 | markup = addStylePropertyMarkup({ FONTFAMILY: 'Arial' }, 'test'); 160 | assert.equal(markup, 'test'); 161 | markup = addStylePropertyMarkup({ FONTSIZE: 'medium' }, 'test'); 162 | assert.equal(markup, 'test'); 163 | markup = addStylePropertyMarkup({ FONTSIZE: '24' }, 'test'); 164 | assert.equal(markup, 'test'); 165 | markup = addStylePropertyMarkup({ BOLD: true }, 'test'); 166 | assert.equal(markup, 'test'); 167 | markup = addStylePropertyMarkup(undefined, 'test'); 168 | assert.equal(markup, 'test'); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /js/__test__/commonTest.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { spy } from 'sinon'; 3 | import { forEach, isEmptyString } from '../common'; 4 | 5 | describe('forEach test suite', () => { 6 | const obj = { 7 | 1: 1, 8 | 2: 2, 9 | 3: 3, 10 | }; 11 | const callback = spy(); 12 | it('should return without calling callback for undefined objects', () => { 13 | forEach(undefined, callback); 14 | assert.equal(callback.callCount, 0); 15 | assert.equal(callback.callCount, 0); 16 | }); 17 | it('should call forEach for each defined key in an object', () => { 18 | forEach(obj, callback); 19 | assert.equal(callback.callCount, 3); 20 | assert.equal(callback.callCount, 3); 21 | }); 22 | }); 23 | 24 | describe('isEmptyString test suite', () => { 25 | it('should return true if its 0 length string', () => { 26 | assert.isTrue(isEmptyString('')); 27 | }); 28 | it('should return false if the string has some content', () => { 29 | assert.isNotTrue(isEmptyString('abc')); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /js/__test__/mainTest.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { convertFromHTML, ContentState, convertToRaw } from 'draft-js'; 3 | import draftToHtml from '../index'; 4 | 5 | describe('draftToHtml test suite', () => { 6 | it('should return correct html', () => { 7 | const html = '

testing

\n'; 8 | const arrContentBlocks = convertFromHTML(html); 9 | const contentState = ContentState.createFromBlockArray(arrContentBlocks); 10 | const result = draftToHtml(convertToRaw(contentState)); 11 | assert.equal(html, result); 12 | }); 13 | 14 | it('should return empty string for undefined input', () => { 15 | const result = draftToHtml(undefined); 16 | assert.equal('', result); 17 | }); 18 | 19 | it('should return correct result for list', () => { 20 | let html = '\n'; 21 | let output = '\n'; 22 | let arrContentBlocks = convertFromHTML(html); 23 | let contentState = ContentState.createFromBlockArray(arrContentBlocks); 24 | let result = draftToHtml(convertToRaw(contentState)); 25 | assert.equal(output, result); 26 | 27 | html = '
  1. 1
  2. \n
  3. 2
  4. \n
  5. 3
  6. \n
\n'; 28 | output = '
    \n
  1. 1
  2. \n
  3. 2
  4. \n
  5. 3
  6. \n
\n'; 29 | arrContentBlocks = convertFromHTML(html); 30 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 31 | result = draftToHtml(convertToRaw(contentState)); 32 | assert.equal(output, result); 33 | 34 | html = '
  1. 1
  2. \n
    1. 2
    2. \n
    \n
  3. 3
  4. \n
\n'; 35 | output = '
    \n
  1. 1
  2. \n
      \n
    1. 2
    2. \n
    \n
  3. 3
  4. \n
\n'; 36 | arrContentBlocks = convertFromHTML(html); 37 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 38 | result = draftToHtml(convertToRaw(contentState)); 39 | assert.equal(output, result); 40 | 41 | html = '
  1. 1
  2. \n
    1. 2
    2. \n
    3. 3
    4. \n
    \n
  3. 4
  4. \n
\n'; 42 | output = '
    \n
  1. 1
  2. \n
      \n
    1. 2
    2. \n
    3. ' 43 | + '3
    4. \n
    \n
  3. 4
  4. \n
\n'; 44 | arrContentBlocks = convertFromHTML(html); 45 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 46 | result = draftToHtml(convertToRaw(contentState)); 47 | assert.equal(output, result); 48 | 49 | html = '
  1. 1
  2. \n
    1. 2
    2. \n
      1. 3
      2. \n
      ' 50 | + '\n
    \n
  3. 3
  4. \n
\n'; 51 | output = '
    \n
  1. 1
  2. \n
      \n
    1. 2
    2. \n
        \n
      1. 3' 52 | + '
      2. \n
      \n
    \n
  3. 3
  4. \n
\n'; 53 | arrContentBlocks = convertFromHTML(html); 54 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 55 | result = draftToHtml(convertToRaw(contentState)); 56 | assert.equal(output, result); 57 | }); 58 | 59 | it('should return correct result for inline styles color', () => { 60 | let html = '\n'; 61 | let output = '\n'; 62 | let arrContentBlocks = convertFromHTML(html); 63 | let contentState = ContentState.createFromBlockArray(arrContentBlocks); 64 | let result = draftToHtml(convertToRaw(contentState)); 65 | assert.equal(output, result); 66 | 67 | html = '
  1. 1
  2. \n
  3. 2
  4. \n
  5. 3
  6. \n
\n'; 68 | output = '
    \n
  1. 1
  2. \n
  3. 2
  4. \n
  5. 3
  6. \n
\n'; 69 | arrContentBlocks = convertFromHTML(html); 70 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 71 | result = draftToHtml(convertToRaw(contentState)); 72 | assert.equal(output, result); 73 | 74 | html = '
  1. 1
  2. \n
    1. 2
    2. \n
    \n
  3. 3
  4. \n
\n'; 75 | output = '
    \n
  1. 1
  2. \n
      \n
    1. 2
    2. \n
    \n
  3. 3
  4. \n
\n'; 76 | arrContentBlocks = convertFromHTML(html); 77 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 78 | result = draftToHtml(convertToRaw(contentState)); 79 | assert.equal(output, result); 80 | 81 | html = '
  1. 1
  2. \n
    1. 2
    2. \n
    3. 3
    4. \n
    \n
  3. 4
  4. \n
\n'; 82 | output = '
    \n
  1. 1
  2. \n
      \n
    1. 2
    2. \n
    3. 3' 83 | + '
    4. \n
    \n
  3. 4
  4. \n
\n'; 84 | arrContentBlocks = convertFromHTML(html); 85 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 86 | result = draftToHtml(convertToRaw(contentState)); 87 | assert.equal(output, result); 88 | 89 | html = '
  1. 1
  2. \n
    1. 2
    2. \n
      1. 3
      2. \n
      ' 90 | + '\n
    \n
  3. 3
  4. \n
\n'; 91 | output = '
    \n
  1. 1
  2. \n
      \n
    1. 2
    2. \n
        \n
      1. 3' 92 | + '
      2. \n
      \n
    \n
  3. 3
  4. \n
\n'; 93 | arrContentBlocks = convertFromHTML(html); 94 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 95 | result = draftToHtml(convertToRaw(contentState)); 96 | assert.equal(output, result); 97 | }); 98 | 99 | it('should return correct result for different heading styles', () => { 100 | let html = '

testing

\n'; 101 | let arrContentBlocks = convertFromHTML(html); 102 | let contentState = ContentState.createFromBlockArray(arrContentBlocks); 103 | let result = draftToHtml(convertToRaw(contentState)); 104 | assert.equal(html, result); 105 | 106 | html = '

testing

\n'; 107 | arrContentBlocks = convertFromHTML(html); 108 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 109 | result = draftToHtml(convertToRaw(contentState)); 110 | assert.equal(html, result); 111 | 112 | html = '
testing
\n'; 113 | arrContentBlocks = convertFromHTML(html); 114 | contentState = ContentState.createFromBlockArray(arrContentBlocks); 115 | result = draftToHtml(convertToRaw(contentState)); 116 | assert.equal(html, result); 117 | }); 118 | 119 | it('should return correct result when there are emojis', () => { 120 | const html = '

👈👈

\n'; 121 | const arrContentBlocks = convertFromHTML(html); 122 | const contentState = ContentState.createFromBlockArray(arrContentBlocks); 123 | const result = draftToHtml(convertToRaw(contentState)); 124 | assert.equal(html, result); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /js/block.js: -------------------------------------------------------------------------------- 1 | import { forEach, isEmptyString } from './common'; 2 | 3 | /** 4 | * Mapping block-type to corresponding html tag. 5 | */ 6 | const blockTypesMapping = { 7 | unstyled: 'p', 8 | 'header-one': 'h1', 9 | 'header-two': 'h2', 10 | 'header-three': 'h3', 11 | 'header-four': 'h4', 12 | 'header-five': 'h5', 13 | 'header-six': 'h6', 14 | 'unordered-list-item': 'ul', 15 | 'ordered-list-item': 'ol', 16 | blockquote: 'blockquote', 17 | code: 'pre', 18 | }; 19 | 20 | /** 21 | * Function will return HTML tag for a block. 22 | */ 23 | export function getBlockTag(type) { 24 | return type && blockTypesMapping[type]; 25 | } 26 | 27 | /** 28 | * Function will return style string for a block. 29 | */ 30 | export function getBlockStyle(data) { 31 | let styles = ''; 32 | forEach(data, (key, value) => { 33 | if (value) { 34 | styles += `${key}:${value};`; 35 | } 36 | }); 37 | return styles; 38 | } 39 | 40 | /** 41 | * The function returns an array of hashtag-sections in blocks. 42 | * These will be areas in block which have hashtags applicable to them. 43 | */ 44 | function getHashtagRanges(blockText, hashtagConfig) { 45 | const sections = []; 46 | if (hashtagConfig) { 47 | let counter = 0; 48 | let startIndex = 0; 49 | let text = blockText; 50 | const trigger = hashtagConfig.trigger || '#'; 51 | const separator = hashtagConfig.separator || ' '; 52 | for (;text.length > 0 && startIndex >= 0;) { 53 | if (text[0] === trigger) { 54 | startIndex = 0; 55 | counter = 0; 56 | text = text.substr(trigger.length); 57 | } else { 58 | startIndex = text.indexOf(separator + trigger); 59 | if (startIndex >= 0) { 60 | text = text.substr(startIndex + (separator + trigger).length); 61 | counter += startIndex + separator.length; 62 | } 63 | } 64 | if (startIndex >= 0) { 65 | const endIndex = text.indexOf(separator) >= 0 66 | ? text.indexOf(separator) 67 | : text.length; 68 | const hashtag = text.substr(0, endIndex); 69 | if (hashtag && hashtag.length > 0) { 70 | sections.push({ 71 | offset: counter, 72 | length: hashtag.length + trigger.length, 73 | type: 'HASHTAG', 74 | }); 75 | } 76 | counter += trigger.length; 77 | } 78 | } 79 | } 80 | return sections; 81 | } 82 | 83 | /** 84 | * The function returns an array of entity-sections in blocks. 85 | * These will be areas in block which have same entity or no entity applicable to them. 86 | */ 87 | function getSections( 88 | block, 89 | hashtagConfig, 90 | ) { 91 | const sections = []; 92 | let lastOffset = 0; 93 | let sectionRanges = block.entityRanges.map((range) => { 94 | const { offset, length, key } = range; 95 | return { 96 | offset, 97 | length, 98 | key, 99 | type: 'ENTITY', 100 | }; 101 | }); 102 | sectionRanges = sectionRanges.concat(getHashtagRanges(block.text, hashtagConfig)); 103 | sectionRanges = sectionRanges.sort((s1, s2) => s1.offset - s2.offset); 104 | sectionRanges.forEach((r) => { 105 | if (r.offset > lastOffset) { 106 | sections.push({ 107 | start: lastOffset, 108 | end: r.offset, 109 | }); 110 | } 111 | sections.push({ 112 | start: r.offset, 113 | end: r.offset + r.length, 114 | entityKey: r.key, 115 | type: r.type, 116 | }); 117 | lastOffset = r.offset + r.length; 118 | }); 119 | if (lastOffset < block.text.length) { 120 | sections.push({ 121 | start: lastOffset, 122 | end: block.text.length, 123 | }); 124 | } 125 | return sections; 126 | } 127 | 128 | /** 129 | * Function to check if the block is an atomic entity block. 130 | */ 131 | function isAtomicEntityBlock(block) { 132 | if (block.entityRanges.length > 0 && (isEmptyString(block.text) 133 | || block.type === 'atomic')) { 134 | return true; 135 | } 136 | return false; 137 | } 138 | 139 | /** 140 | * The function will return array of inline styles applicable to the block. 141 | */ 142 | function getStyleArrayForBlock(block) { 143 | const { text, inlineStyleRanges } = block; 144 | const inlineStyles = { 145 | BOLD: new Array(text.length), 146 | ITALIC: new Array(text.length), 147 | UNDERLINE: new Array(text.length), 148 | STRIKETHROUGH: new Array(text.length), 149 | CODE: new Array(text.length), 150 | SUPERSCRIPT: new Array(text.length), 151 | SUBSCRIPT: new Array(text.length), 152 | COLOR: new Array(text.length), 153 | BGCOLOR: new Array(text.length), 154 | FONTSIZE: new Array(text.length), 155 | FONTFAMILY: new Array(text.length), 156 | length: text.length, 157 | }; 158 | if (inlineStyleRanges && inlineStyleRanges.length > 0) { 159 | inlineStyleRanges.forEach((range) => { 160 | const { offset } = range; 161 | const length = offset + range.length; 162 | for (let i = offset; i < length; i += 1) { 163 | if (range.style.indexOf('color-') === 0) { 164 | inlineStyles.COLOR[i] = range.style.substring(6); 165 | } else if (range.style.indexOf('bgcolor-') === 0) { 166 | inlineStyles.BGCOLOR[i] = range.style.substring(8); 167 | } else if (range.style.indexOf('fontsize-') === 0) { 168 | inlineStyles.FONTSIZE[i] = range.style.substring(9); 169 | } else if (range.style.indexOf('fontfamily-') === 0) { 170 | inlineStyles.FONTFAMILY[i] = range.style.substring(11); 171 | } else if (inlineStyles[range.style]) { 172 | inlineStyles[range.style][i] = true; 173 | } 174 | } 175 | }); 176 | } 177 | return inlineStyles; 178 | } 179 | 180 | /** 181 | * The function will return inline style applicable at some offset within a block. 182 | */ 183 | export function getStylesAtOffset(inlineStyles, offset) { 184 | const styles = {}; 185 | if (inlineStyles.COLOR[offset]) { 186 | styles.COLOR = inlineStyles.COLOR[offset]; 187 | } 188 | if (inlineStyles.BGCOLOR[offset]) { 189 | styles.BGCOLOR = inlineStyles.BGCOLOR[offset]; 190 | } 191 | if (inlineStyles.FONTSIZE[offset]) { 192 | styles.FONTSIZE = inlineStyles.FONTSIZE[offset]; 193 | } 194 | if (inlineStyles.FONTFAMILY[offset]) { 195 | styles.FONTFAMILY = inlineStyles.FONTFAMILY[offset]; 196 | } 197 | if (inlineStyles.UNDERLINE[offset]) { 198 | styles.UNDERLINE = true; 199 | } 200 | if (inlineStyles.ITALIC[offset]) { 201 | styles.ITALIC = true; 202 | } 203 | if (inlineStyles.BOLD[offset]) { 204 | styles.BOLD = true; 205 | } 206 | if (inlineStyles.STRIKETHROUGH[offset]) { 207 | styles.STRIKETHROUGH = true; 208 | } 209 | if (inlineStyles.CODE[offset]) { 210 | styles.CODE = true; 211 | } 212 | if (inlineStyles.SUBSCRIPT[offset]) { 213 | styles.SUBSCRIPT = true; 214 | } 215 | if (inlineStyles.SUPERSCRIPT[offset]) { 216 | styles.SUPERSCRIPT = true; 217 | } 218 | return styles; 219 | } 220 | 221 | /** 222 | * Function returns true for a set of styles if the value of these styles at an offset 223 | * are same as that on the previous offset. 224 | */ 225 | export function sameStyleAsPrevious( 226 | inlineStyles, 227 | styles, 228 | index, 229 | ) { 230 | let sameStyled = true; 231 | if (index > 0 && index < inlineStyles.length) { 232 | styles.forEach((style) => { 233 | sameStyled = sameStyled && inlineStyles[style][index] === inlineStyles[style][index - 1]; 234 | }); 235 | } else { 236 | sameStyled = false; 237 | } 238 | return sameStyled; 239 | } 240 | 241 | /** 242 | * Function returns html for text depending on inline style tags applicable to it. 243 | */ 244 | export function addInlineStyleMarkup(style, content) { 245 | if (style === 'BOLD') { 246 | return `${content}`; 247 | } if (style === 'ITALIC') { 248 | return `${content}`; 249 | } if (style === 'UNDERLINE') { 250 | return `${content}`; 251 | } if (style === 'STRIKETHROUGH') { 252 | return `${content}`; 253 | } if (style === 'CODE') { 254 | return `${content}`; 255 | } if (style === 'SUPERSCRIPT') { 256 | return `${content}`; 257 | } if (style === 'SUBSCRIPT') { 258 | return `${content}`; 259 | } 260 | return content; 261 | } 262 | 263 | /** 264 | * The function returns text for given section of block after doing required character replacements. 265 | */ 266 | function getSectionText(text) { 267 | if (text && text.length > 0) { 268 | const chars = text.map((ch) => { 269 | switch (ch) { 270 | case '\n': 271 | return '
'; 272 | case '&': 273 | return '&'; 274 | case '<': 275 | return '<'; 276 | case '>': 277 | return '>'; 278 | default: 279 | return ch; 280 | } 281 | }); 282 | return chars.join(''); 283 | } 284 | return ''; 285 | } 286 | 287 | /** 288 | * Function returns html for text depending on inline style tags applicable to it. 289 | */ 290 | export function addStylePropertyMarkup(styles, text) { 291 | if (styles && (styles.COLOR || styles.BGCOLOR || styles.FONTSIZE || styles.FONTFAMILY)) { 292 | let styleString = 'style="'; 293 | if (styles.COLOR) { 294 | styleString += `color: ${styles.COLOR};`; 295 | } 296 | if (styles.BGCOLOR) { 297 | styleString += `background-color: ${styles.BGCOLOR};`; 298 | } 299 | if (styles.FONTSIZE) { 300 | styleString += `font-size: ${styles.FONTSIZE}${/^\d+$/.test(styles.FONTSIZE) ? 'px' : ''};`; 301 | } 302 | if (styles.FONTFAMILY) { 303 | styleString += `font-family: ${styles.FONTFAMILY};`; 304 | } 305 | styleString += '"'; 306 | return `${text}`; 307 | } 308 | return text; 309 | } 310 | 311 | /** 312 | * Function will return markup for Entity. 313 | */ 314 | function getEntityMarkup( 315 | entityMap, 316 | entityKey, 317 | text, 318 | customEntityTransform, 319 | ) { 320 | const entity = entityMap[entityKey]; 321 | if (typeof customEntityTransform === 'function') { 322 | const html = customEntityTransform(entity, text); 323 | if (html) { 324 | return html; 325 | } 326 | } 327 | if (entity.type === 'MENTION') { 328 | return `${text}`; 329 | } 330 | if (entity.type === 'LINK') { 331 | const targetOption = entity.data.targetOption || '_self'; 332 | return `${text}`; 333 | } 334 | if (entity.type === 'IMAGE') { 335 | const { alignment } = entity.data; 336 | if (alignment && alignment.length) { 337 | return `
${entity.data.alt}
`; 338 | } 339 | return `${entity.data.alt}`; 340 | } 341 | if (entity.type === 'EMBEDDED_LINK') { 342 | return ``; 343 | } 344 | return text; 345 | } 346 | 347 | /** 348 | * For a given section in a block the function will return a further list of sections, 349 | * with similar inline styles applicable to them. 350 | */ 351 | function getInlineStyleSections( 352 | block, 353 | styles, 354 | start, 355 | end, 356 | ) { 357 | const styleSections = []; 358 | const text = Array.from(block.text); 359 | if (text.length > 0) { 360 | const inlineStyles = getStyleArrayForBlock(block); 361 | let section; 362 | for (let i = start; i < end; i += 1) { 363 | if (i !== start && sameStyleAsPrevious(inlineStyles, styles, i)) { 364 | section.text.push(text[i]); 365 | section.end = i + 1; 366 | } else { 367 | section = { 368 | styles: getStylesAtOffset(inlineStyles, i), 369 | text: [text[i]], 370 | start: i, 371 | end: i + 1, 372 | }; 373 | styleSections.push(section); 374 | } 375 | } 376 | } 377 | return styleSections; 378 | } 379 | 380 | /** 381 | * Replace leading blank spaces by   382 | */ 383 | export function trimLeadingZeros(sectionText) { 384 | if (sectionText) { 385 | let replacedText = sectionText; 386 | for (let i = 0; i < replacedText.length; i += 1) { 387 | if (sectionText[i] === ' ') { 388 | replacedText = replacedText.replace(' ', ' '); 389 | } else { 390 | break; 391 | } 392 | } 393 | return replacedText; 394 | } 395 | return sectionText; 396 | } 397 | 398 | /** 399 | * Replace trailing blank spaces by   400 | */ 401 | export function trimTrailingZeros(sectionText) { 402 | if (sectionText) { 403 | let replacedText = sectionText; 404 | for (let i = replacedText.length - 1; i >= 0; i -= 1) { 405 | if (replacedText[i] === ' ') { 406 | replacedText = `${replacedText.substring(0, i)} ${replacedText.substring(i + 1)}`; 407 | } else { 408 | break; 409 | } 410 | } 411 | return replacedText; 412 | } 413 | return sectionText; 414 | } 415 | 416 | /** 417 | * The method returns markup for section to which inline styles 418 | * like BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, CODE, SUPERSCRIPT, SUBSCRIPT are applicable. 419 | */ 420 | function getStyleTagSectionMarkup(styleSection) { 421 | const { styles, text } = styleSection; 422 | let content = getSectionText(text); 423 | forEach(styles, (style, value) => { 424 | content = addInlineStyleMarkup(style, content, value); 425 | }); 426 | return content; 427 | } 428 | 429 | 430 | /** 431 | * The method returns markup for section to which inline styles 432 | like color, background-color, font-size are applicable. 433 | */ 434 | function getInlineStyleSectionMarkup(block, styleSection) { 435 | const styleTagSections = getInlineStyleSections(block, ['BOLD', 'ITALIC', 'UNDERLINE', 'STRIKETHROUGH', 'CODE', 'SUPERSCRIPT', 'SUBSCRIPT'], styleSection.start, styleSection.end); 436 | let styleSectionText = ''; 437 | styleTagSections.forEach((stylePropertySection) => { 438 | styleSectionText += getStyleTagSectionMarkup(stylePropertySection); 439 | }); 440 | styleSectionText = addStylePropertyMarkup(styleSection.styles, styleSectionText); 441 | return styleSectionText; 442 | } 443 | 444 | /* 445 | * The method returns markup for an entity section. 446 | * An entity section is a continuous section in a block 447 | * to which same entity or no entity is applicable. 448 | */ 449 | function getSectionMarkup( 450 | block, 451 | entityMap, 452 | section, 453 | customEntityTransform, 454 | ) { 455 | const entityInlineMarkup = []; 456 | const inlineStyleSections = getInlineStyleSections( 457 | block, 458 | ['COLOR', 'BGCOLOR', 'FONTSIZE', 'FONTFAMILY'], 459 | section.start, 460 | section.end, 461 | ); 462 | inlineStyleSections.forEach((styleSection) => { 463 | entityInlineMarkup.push(getInlineStyleSectionMarkup(block, styleSection)); 464 | }); 465 | let sectionText = entityInlineMarkup.join(''); 466 | if (section.type === 'ENTITY') { 467 | if (section.entityKey !== undefined && section.entityKey !== null) { 468 | sectionText = getEntityMarkup(entityMap, section.entityKey, sectionText, customEntityTransform); // eslint-disable-line max-len 469 | } 470 | } else if (section.type === 'HASHTAG') { 471 | sectionText = `${sectionText}`; 472 | } 473 | return sectionText; 474 | } 475 | 476 | /** 477 | * Function will return the markup for block preserving the inline styles and 478 | * special characters like newlines or blank spaces. 479 | */ 480 | export function getBlockInnerMarkup( 481 | block, 482 | entityMap, 483 | hashtagConfig, 484 | customEntityTransform, 485 | ) { 486 | const blockMarkup = []; 487 | const sections = getSections(block, hashtagConfig); 488 | sections.forEach((section, index) => { 489 | let sectionText = getSectionMarkup(block, entityMap, section, customEntityTransform); 490 | if (index === 0) { 491 | sectionText = trimLeadingZeros(sectionText); 492 | } 493 | if (index === sections.length - 1) { 494 | sectionText = trimTrailingZeros(sectionText); 495 | } 496 | blockMarkup.push(sectionText); 497 | }); 498 | return blockMarkup.join(''); 499 | } 500 | 501 | /** 502 | * Function will return html for the block. 503 | */ 504 | export function getBlockMarkup( 505 | block, 506 | entityMap, 507 | hashtagConfig, 508 | directional, 509 | customEntityTransform, 510 | ) { 511 | const blockHtml = []; 512 | if (isAtomicEntityBlock(block)) { 513 | blockHtml.push(getEntityMarkup( 514 | entityMap, 515 | block.entityRanges[0].key, 516 | undefined, 517 | customEntityTransform, 518 | )); 519 | } else { 520 | const blockTag = getBlockTag(block.type); 521 | if (blockTag) { 522 | blockHtml.push(`<${blockTag}`); 523 | const blockStyle = getBlockStyle(block.data); 524 | if (blockStyle) { 525 | blockHtml.push(` style="${blockStyle}"`); 526 | } 527 | if (directional) { 528 | blockHtml.push(' dir = "auto"'); 529 | } 530 | blockHtml.push('>'); 531 | blockHtml.push(getBlockInnerMarkup(block, entityMap, hashtagConfig, customEntityTransform)); 532 | blockHtml.push(``); 533 | } 534 | } 535 | blockHtml.push('\n'); 536 | return blockHtml.join(''); 537 | } 538 | -------------------------------------------------------------------------------- /js/common.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /** 4 | * Utility function to execute callback for eack key->value pair. 5 | */ 6 | export function forEach(obj, callback) { 7 | if (obj) { 8 | for (const key in obj) { // eslint-disable-line no-restricted-syntax 9 | if ({}.hasOwnProperty.call(obj, key)) { 10 | callback(key, obj[key]); 11 | } 12 | } 13 | } 14 | } 15 | 16 | /** 17 | * The function returns true if the string passed to it has no content. 18 | */ 19 | export function isEmptyString(str) { 20 | if (str === undefined || str === null || str.length === 0 || str.trim().length === 0) { 21 | return true; 22 | } 23 | return false; 24 | } 25 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { getBlockMarkup } from './block'; 4 | import { isList, getListMarkup } from './list'; 5 | 6 | /** 7 | * The function will generate html markup for given draftjs editorContent. 8 | */ 9 | export default function draftToHtml( 10 | editorContent, 11 | hashtagConfig, 12 | directional, 13 | customEntityTransform, 14 | ) { 15 | const html = []; 16 | if (editorContent) { 17 | const { blocks, entityMap } = editorContent; 18 | if (blocks && blocks.length > 0) { 19 | let listBlocks = []; 20 | blocks.forEach((block) => { 21 | if (isList(block.type)) { 22 | listBlocks.push(block); 23 | } else { 24 | if (listBlocks.length > 0) { 25 | const listHtml = getListMarkup(listBlocks, entityMap, hashtagConfig, customEntityTransform); // eslint-disable-line max-len 26 | html.push(listHtml); 27 | listBlocks = []; 28 | } 29 | const blockHtml = getBlockMarkup( 30 | block, 31 | entityMap, 32 | hashtagConfig, 33 | directional, 34 | customEntityTransform, 35 | ); 36 | html.push(blockHtml); 37 | } 38 | }); 39 | if (listBlocks.length > 0) { 40 | const listHtml = getListMarkup(listBlocks, entityMap, hashtagConfig, directional, customEntityTransform); // eslint-disable-line max-len 41 | html.push(listHtml); 42 | listBlocks = []; 43 | } 44 | } 45 | } 46 | return html.join(''); 47 | } 48 | -------------------------------------------------------------------------------- /js/list.js: -------------------------------------------------------------------------------- 1 | import { 2 | getBlockTag, 3 | getBlockStyle, 4 | getBlockInnerMarkup, 5 | } from './block'; 6 | 7 | /** 8 | * Function to check if a block is of type list. 9 | */ 10 | export function isList(blockType) { 11 | return ( 12 | blockType === 'unordered-list-item' 13 | || blockType === 'ordered-list-item' 14 | ); 15 | } 16 | 17 | /** 18 | * Function will return html markup for a list block. 19 | */ 20 | export function getListMarkup( 21 | listBlocks, 22 | entityMap, 23 | hashtagConfig, 24 | directional, 25 | customEntityTransform, 26 | ) { 27 | const listHtml = []; 28 | let nestedListBlock = []; 29 | let previousBlock; 30 | listBlocks.forEach((block) => { 31 | let nestedBlock = false; 32 | if (!previousBlock) { 33 | listHtml.push(`<${getBlockTag(block.type)}>\n`); 34 | } else if (previousBlock.type !== block.type) { 35 | listHtml.push(`\n`); 36 | listHtml.push(`<${getBlockTag(block.type)}>\n`); 37 | } else if (previousBlock.depth === block.depth) { 38 | if (nestedListBlock && nestedListBlock.length > 0) { 39 | listHtml.push(getListMarkup( 40 | nestedListBlock, 41 | entityMap, 42 | hashtagConfig, 43 | directional, 44 | customEntityTransform, 45 | )); 46 | nestedListBlock = []; 47 | } 48 | } else { 49 | nestedBlock = true; 50 | nestedListBlock.push(block); 51 | } 52 | if (!nestedBlock) { 53 | listHtml.push(''); 62 | listHtml.push(getBlockInnerMarkup( 63 | block, 64 | entityMap, 65 | hashtagConfig, 66 | customEntityTransform, 67 | )); 68 | listHtml.push('\n'); 69 | previousBlock = block; 70 | } 71 | }); 72 | if (nestedListBlock && nestedListBlock.length > 0) { 73 | listHtml.push(getListMarkup( 74 | nestedListBlock, 75 | entityMap, 76 | hashtagConfig, 77 | directional, 78 | customEntityTransform, 79 | )); 80 | } 81 | listHtml.push(`\n`); 82 | return listHtml.join(''); 83 | } 84 | -------------------------------------------------------------------------------- /lib/draftjs-to-html.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = global || self, global.draftjsToHtml = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | /** 8 | * Utility function to execute callback for eack key->value pair. 9 | */ 10 | function forEach(obj, callback) { 11 | if (obj) { 12 | for (var key in obj) { 13 | // eslint-disable-line no-restricted-syntax 14 | if ({}.hasOwnProperty.call(obj, key)) { 15 | callback(key, obj[key]); 16 | } 17 | } 18 | } 19 | } 20 | /** 21 | * The function returns true if the string passed to it has no content. 22 | */ 23 | 24 | function isEmptyString(str) { 25 | if (str === undefined || str === null || str.length === 0 || str.trim().length === 0) { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | 32 | /** 33 | * Mapping block-type to corresponding html tag. 34 | */ 35 | 36 | var blockTypesMapping = { 37 | unstyled: 'p', 38 | 'header-one': 'h1', 39 | 'header-two': 'h2', 40 | 'header-three': 'h3', 41 | 'header-four': 'h4', 42 | 'header-five': 'h5', 43 | 'header-six': 'h6', 44 | 'unordered-list-item': 'ul', 45 | 'ordered-list-item': 'ol', 46 | blockquote: 'blockquote', 47 | code: 'pre' 48 | }; 49 | /** 50 | * Function will return HTML tag for a block. 51 | */ 52 | 53 | function getBlockTag(type) { 54 | return type && blockTypesMapping[type]; 55 | } 56 | /** 57 | * Function will return style string for a block. 58 | */ 59 | 60 | function getBlockStyle(data) { 61 | var styles = ''; 62 | forEach(data, function (key, value) { 63 | if (value) { 64 | styles += "".concat(key, ":").concat(value, ";"); 65 | } 66 | }); 67 | return styles; 68 | } 69 | /** 70 | * The function returns an array of hashtag-sections in blocks. 71 | * These will be areas in block which have hashtags applicable to them. 72 | */ 73 | 74 | function getHashtagRanges(blockText, hashtagConfig) { 75 | var sections = []; 76 | 77 | if (hashtagConfig) { 78 | var counter = 0; 79 | var startIndex = 0; 80 | var text = blockText; 81 | var trigger = hashtagConfig.trigger || '#'; 82 | var separator = hashtagConfig.separator || ' '; 83 | 84 | for (; text.length > 0 && startIndex >= 0;) { 85 | if (text[0] === trigger) { 86 | startIndex = 0; 87 | counter = 0; 88 | text = text.substr(trigger.length); 89 | } else { 90 | startIndex = text.indexOf(separator + trigger); 91 | 92 | if (startIndex >= 0) { 93 | text = text.substr(startIndex + (separator + trigger).length); 94 | counter += startIndex + separator.length; 95 | } 96 | } 97 | 98 | if (startIndex >= 0) { 99 | var endIndex = text.indexOf(separator) >= 0 ? text.indexOf(separator) : text.length; 100 | var hashtag = text.substr(0, endIndex); 101 | 102 | if (hashtag && hashtag.length > 0) { 103 | sections.push({ 104 | offset: counter, 105 | length: hashtag.length + trigger.length, 106 | type: 'HASHTAG' 107 | }); 108 | } 109 | 110 | counter += trigger.length; 111 | } 112 | } 113 | } 114 | 115 | return sections; 116 | } 117 | /** 118 | * The function returns an array of entity-sections in blocks. 119 | * These will be areas in block which have same entity or no entity applicable to them. 120 | */ 121 | 122 | 123 | function getSections(block, hashtagConfig) { 124 | var sections = []; 125 | var lastOffset = 0; 126 | var sectionRanges = block.entityRanges.map(function (range) { 127 | var offset = range.offset, 128 | length = range.length, 129 | key = range.key; 130 | return { 131 | offset: offset, 132 | length: length, 133 | key: key, 134 | type: 'ENTITY' 135 | }; 136 | }); 137 | sectionRanges = sectionRanges.concat(getHashtagRanges(block.text, hashtagConfig)); 138 | sectionRanges = sectionRanges.sort(function (s1, s2) { 139 | return s1.offset - s2.offset; 140 | }); 141 | sectionRanges.forEach(function (r) { 142 | if (r.offset > lastOffset) { 143 | sections.push({ 144 | start: lastOffset, 145 | end: r.offset 146 | }); 147 | } 148 | 149 | sections.push({ 150 | start: r.offset, 151 | end: r.offset + r.length, 152 | entityKey: r.key, 153 | type: r.type 154 | }); 155 | lastOffset = r.offset + r.length; 156 | }); 157 | 158 | if (lastOffset < block.text.length) { 159 | sections.push({ 160 | start: lastOffset, 161 | end: block.text.length 162 | }); 163 | } 164 | 165 | return sections; 166 | } 167 | /** 168 | * Function to check if the block is an atomic entity block. 169 | */ 170 | 171 | 172 | function isAtomicEntityBlock(block) { 173 | if (block.entityRanges.length > 0 && (isEmptyString(block.text) || block.type === 'atomic')) { 174 | return true; 175 | } 176 | 177 | return false; 178 | } 179 | /** 180 | * The function will return array of inline styles applicable to the block. 181 | */ 182 | 183 | 184 | function getStyleArrayForBlock(block) { 185 | var text = block.text, 186 | inlineStyleRanges = block.inlineStyleRanges; 187 | var inlineStyles = { 188 | BOLD: new Array(text.length), 189 | ITALIC: new Array(text.length), 190 | UNDERLINE: new Array(text.length), 191 | STRIKETHROUGH: new Array(text.length), 192 | CODE: new Array(text.length), 193 | SUPERSCRIPT: new Array(text.length), 194 | SUBSCRIPT: new Array(text.length), 195 | COLOR: new Array(text.length), 196 | BGCOLOR: new Array(text.length), 197 | FONTSIZE: new Array(text.length), 198 | FONTFAMILY: new Array(text.length), 199 | length: text.length 200 | }; 201 | 202 | if (inlineStyleRanges && inlineStyleRanges.length > 0) { 203 | inlineStyleRanges.forEach(function (range) { 204 | var offset = range.offset; 205 | var length = offset + range.length; 206 | 207 | for (var i = offset; i < length; i += 1) { 208 | if (range.style.indexOf('color-') === 0) { 209 | inlineStyles.COLOR[i] = range.style.substring(6); 210 | } else if (range.style.indexOf('bgcolor-') === 0) { 211 | inlineStyles.BGCOLOR[i] = range.style.substring(8); 212 | } else if (range.style.indexOf('fontsize-') === 0) { 213 | inlineStyles.FONTSIZE[i] = range.style.substring(9); 214 | } else if (range.style.indexOf('fontfamily-') === 0) { 215 | inlineStyles.FONTFAMILY[i] = range.style.substring(11); 216 | } else if (inlineStyles[range.style]) { 217 | inlineStyles[range.style][i] = true; 218 | } 219 | } 220 | }); 221 | } 222 | 223 | return inlineStyles; 224 | } 225 | /** 226 | * The function will return inline style applicable at some offset within a block. 227 | */ 228 | 229 | 230 | function getStylesAtOffset(inlineStyles, offset) { 231 | var styles = {}; 232 | 233 | if (inlineStyles.COLOR[offset]) { 234 | styles.COLOR = inlineStyles.COLOR[offset]; 235 | } 236 | 237 | if (inlineStyles.BGCOLOR[offset]) { 238 | styles.BGCOLOR = inlineStyles.BGCOLOR[offset]; 239 | } 240 | 241 | if (inlineStyles.FONTSIZE[offset]) { 242 | styles.FONTSIZE = inlineStyles.FONTSIZE[offset]; 243 | } 244 | 245 | if (inlineStyles.FONTFAMILY[offset]) { 246 | styles.FONTFAMILY = inlineStyles.FONTFAMILY[offset]; 247 | } 248 | 249 | if (inlineStyles.UNDERLINE[offset]) { 250 | styles.UNDERLINE = true; 251 | } 252 | 253 | if (inlineStyles.ITALIC[offset]) { 254 | styles.ITALIC = true; 255 | } 256 | 257 | if (inlineStyles.BOLD[offset]) { 258 | styles.BOLD = true; 259 | } 260 | 261 | if (inlineStyles.STRIKETHROUGH[offset]) { 262 | styles.STRIKETHROUGH = true; 263 | } 264 | 265 | if (inlineStyles.CODE[offset]) { 266 | styles.CODE = true; 267 | } 268 | 269 | if (inlineStyles.SUBSCRIPT[offset]) { 270 | styles.SUBSCRIPT = true; 271 | } 272 | 273 | if (inlineStyles.SUPERSCRIPT[offset]) { 274 | styles.SUPERSCRIPT = true; 275 | } 276 | 277 | return styles; 278 | } 279 | /** 280 | * Function returns true for a set of styles if the value of these styles at an offset 281 | * are same as that on the previous offset. 282 | */ 283 | 284 | function sameStyleAsPrevious(inlineStyles, styles, index) { 285 | var sameStyled = true; 286 | 287 | if (index > 0 && index < inlineStyles.length) { 288 | styles.forEach(function (style) { 289 | sameStyled = sameStyled && inlineStyles[style][index] === inlineStyles[style][index - 1]; 290 | }); 291 | } else { 292 | sameStyled = false; 293 | } 294 | 295 | return sameStyled; 296 | } 297 | /** 298 | * Function returns html for text depending on inline style tags applicable to it. 299 | */ 300 | 301 | function addInlineStyleMarkup(style, content) { 302 | if (style === 'BOLD') { 303 | return "".concat(content, ""); 304 | } 305 | 306 | if (style === 'ITALIC') { 307 | return "".concat(content, ""); 308 | } 309 | 310 | if (style === 'UNDERLINE') { 311 | return "".concat(content, ""); 312 | } 313 | 314 | if (style === 'STRIKETHROUGH') { 315 | return "".concat(content, ""); 316 | } 317 | 318 | if (style === 'CODE') { 319 | return "".concat(content, ""); 320 | } 321 | 322 | if (style === 'SUPERSCRIPT') { 323 | return "".concat(content, ""); 324 | } 325 | 326 | if (style === 'SUBSCRIPT') { 327 | return "".concat(content, ""); 328 | } 329 | 330 | return content; 331 | } 332 | /** 333 | * The function returns text for given section of block after doing required character replacements. 334 | */ 335 | 336 | function getSectionText(text) { 337 | if (text && text.length > 0) { 338 | var chars = text.map(function (ch) { 339 | switch (ch) { 340 | case '\n': 341 | return '
'; 342 | 343 | case '&': 344 | return '&'; 345 | 346 | case '<': 347 | return '<'; 348 | 349 | case '>': 350 | return '>'; 351 | 352 | default: 353 | return ch; 354 | } 355 | }); 356 | return chars.join(''); 357 | } 358 | 359 | return ''; 360 | } 361 | /** 362 | * Function returns html for text depending on inline style tags applicable to it. 363 | */ 364 | 365 | 366 | function addStylePropertyMarkup(styles, text) { 367 | if (styles && (styles.COLOR || styles.BGCOLOR || styles.FONTSIZE || styles.FONTFAMILY)) { 368 | var styleString = 'style="'; 369 | 370 | if (styles.COLOR) { 371 | styleString += "color: ".concat(styles.COLOR, ";"); 372 | } 373 | 374 | if (styles.BGCOLOR) { 375 | styleString += "background-color: ".concat(styles.BGCOLOR, ";"); 376 | } 377 | 378 | if (styles.FONTSIZE) { 379 | styleString += "font-size: ".concat(styles.FONTSIZE).concat(/^\d+$/.test(styles.FONTSIZE) ? 'px' : '', ";"); 380 | } 381 | 382 | if (styles.FONTFAMILY) { 383 | styleString += "font-family: ".concat(styles.FONTFAMILY, ";"); 384 | } 385 | 386 | styleString += '"'; 387 | return "").concat(text, ""); 388 | } 389 | 390 | return text; 391 | } 392 | /** 393 | * Function will return markup for Entity. 394 | */ 395 | 396 | function getEntityMarkup(entityMap, entityKey, text, customEntityTransform) { 397 | var entity = entityMap[entityKey]; 398 | 399 | if (typeof customEntityTransform === 'function') { 400 | var html = customEntityTransform(entity, text); 401 | 402 | if (html) { 403 | return html; 404 | } 405 | } 406 | 407 | if (entity.type === 'MENTION') { 408 | return "").concat(text, ""); 409 | } 410 | 411 | if (entity.type === 'LINK') { 412 | var targetOption = entity.data.targetOption || '_self'; 413 | return "").concat(text, ""); 414 | } 415 | 416 | if (entity.type === 'IMAGE') { 417 | var alignment = entity.data.alignment; 418 | 419 | if (alignment && alignment.length) { 420 | return "
\"").concat(entity.data.alt,
"); 421 | } 422 | 423 | return "\"").concat(entity.data.alt,"); 424 | } 425 | 426 | if (entity.type === 'EMBEDDED_LINK') { 427 | return ""); 428 | } 429 | 430 | return text; 431 | } 432 | /** 433 | * For a given section in a block the function will return a further list of sections, 434 | * with similar inline styles applicable to them. 435 | */ 436 | 437 | 438 | function getInlineStyleSections(block, styles, start, end) { 439 | var styleSections = []; 440 | var text = Array.from(block.text); 441 | 442 | if (text.length > 0) { 443 | var inlineStyles = getStyleArrayForBlock(block); 444 | var section; 445 | 446 | for (var i = start; i < end; i += 1) { 447 | if (i !== start && sameStyleAsPrevious(inlineStyles, styles, i)) { 448 | section.text.push(text[i]); 449 | section.end = i + 1; 450 | } else { 451 | section = { 452 | styles: getStylesAtOffset(inlineStyles, i), 453 | text: [text[i]], 454 | start: i, 455 | end: i + 1 456 | }; 457 | styleSections.push(section); 458 | } 459 | } 460 | } 461 | 462 | return styleSections; 463 | } 464 | /** 465 | * Replace leading blank spaces by   466 | */ 467 | 468 | 469 | function trimLeadingZeros(sectionText) { 470 | if (sectionText) { 471 | var replacedText = sectionText; 472 | 473 | for (var i = 0; i < replacedText.length; i += 1) { 474 | if (sectionText[i] === ' ') { 475 | replacedText = replacedText.replace(' ', ' '); 476 | } else { 477 | break; 478 | } 479 | } 480 | 481 | return replacedText; 482 | } 483 | 484 | return sectionText; 485 | } 486 | /** 487 | * Replace trailing blank spaces by   488 | */ 489 | 490 | function trimTrailingZeros(sectionText) { 491 | if (sectionText) { 492 | var replacedText = sectionText; 493 | 494 | for (var i = replacedText.length - 1; i >= 0; i -= 1) { 495 | if (replacedText[i] === ' ') { 496 | replacedText = "".concat(replacedText.substring(0, i), " ").concat(replacedText.substring(i + 1)); 497 | } else { 498 | break; 499 | } 500 | } 501 | 502 | return replacedText; 503 | } 504 | 505 | return sectionText; 506 | } 507 | /** 508 | * The method returns markup for section to which inline styles 509 | * like BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, CODE, SUPERSCRIPT, SUBSCRIPT are applicable. 510 | */ 511 | 512 | function getStyleTagSectionMarkup(styleSection) { 513 | var styles = styleSection.styles, 514 | text = styleSection.text; 515 | var content = getSectionText(text); 516 | forEach(styles, function (style, value) { 517 | content = addInlineStyleMarkup(style, content); 518 | }); 519 | return content; 520 | } 521 | /** 522 | * The method returns markup for section to which inline styles 523 | like color, background-color, font-size are applicable. 524 | */ 525 | 526 | 527 | function getInlineStyleSectionMarkup(block, styleSection) { 528 | var styleTagSections = getInlineStyleSections(block, ['BOLD', 'ITALIC', 'UNDERLINE', 'STRIKETHROUGH', 'CODE', 'SUPERSCRIPT', 'SUBSCRIPT'], styleSection.start, styleSection.end); 529 | var styleSectionText = ''; 530 | styleTagSections.forEach(function (stylePropertySection) { 531 | styleSectionText += getStyleTagSectionMarkup(stylePropertySection); 532 | }); 533 | styleSectionText = addStylePropertyMarkup(styleSection.styles, styleSectionText); 534 | return styleSectionText; 535 | } 536 | /* 537 | * The method returns markup for an entity section. 538 | * An entity section is a continuous section in a block 539 | * to which same entity or no entity is applicable. 540 | */ 541 | 542 | 543 | function getSectionMarkup(block, entityMap, section, customEntityTransform) { 544 | var entityInlineMarkup = []; 545 | var inlineStyleSections = getInlineStyleSections(block, ['COLOR', 'BGCOLOR', 'FONTSIZE', 'FONTFAMILY'], section.start, section.end); 546 | inlineStyleSections.forEach(function (styleSection) { 547 | entityInlineMarkup.push(getInlineStyleSectionMarkup(block, styleSection)); 548 | }); 549 | var sectionText = entityInlineMarkup.join(''); 550 | 551 | if (section.type === 'ENTITY') { 552 | if (section.entityKey !== undefined && section.entityKey !== null) { 553 | sectionText = getEntityMarkup(entityMap, section.entityKey, sectionText, customEntityTransform); // eslint-disable-line max-len 554 | } 555 | } else if (section.type === 'HASHTAG') { 556 | sectionText = "").concat(sectionText, ""); 557 | } 558 | 559 | return sectionText; 560 | } 561 | /** 562 | * Function will return the markup for block preserving the inline styles and 563 | * special characters like newlines or blank spaces. 564 | */ 565 | 566 | 567 | function getBlockInnerMarkup(block, entityMap, hashtagConfig, customEntityTransform) { 568 | var blockMarkup = []; 569 | var sections = getSections(block, hashtagConfig); 570 | sections.forEach(function (section, index) { 571 | var sectionText = getSectionMarkup(block, entityMap, section, customEntityTransform); 572 | 573 | if (index === 0) { 574 | sectionText = trimLeadingZeros(sectionText); 575 | } 576 | 577 | if (index === sections.length - 1) { 578 | sectionText = trimTrailingZeros(sectionText); 579 | } 580 | 581 | blockMarkup.push(sectionText); 582 | }); 583 | return blockMarkup.join(''); 584 | } 585 | /** 586 | * Function will return html for the block. 587 | */ 588 | 589 | function getBlockMarkup(block, entityMap, hashtagConfig, directional, customEntityTransform) { 590 | var blockHtml = []; 591 | 592 | if (isAtomicEntityBlock(block)) { 593 | blockHtml.push(getEntityMarkup(entityMap, block.entityRanges[0].key, undefined, customEntityTransform)); 594 | } else { 595 | var blockTag = getBlockTag(block.type); 596 | 597 | if (blockTag) { 598 | blockHtml.push("<".concat(blockTag)); 599 | var blockStyle = getBlockStyle(block.data); 600 | 601 | if (blockStyle) { 602 | blockHtml.push(" style=\"".concat(blockStyle, "\"")); 603 | } 604 | 605 | if (directional) { 606 | blockHtml.push(' dir = "auto"'); 607 | } 608 | 609 | blockHtml.push('>'); 610 | blockHtml.push(getBlockInnerMarkup(block, entityMap, hashtagConfig, customEntityTransform)); 611 | blockHtml.push("")); 612 | } 613 | } 614 | 615 | blockHtml.push('\n'); 616 | return blockHtml.join(''); 617 | } 618 | 619 | /** 620 | * Function to check if a block is of type list. 621 | */ 622 | 623 | function isList(blockType) { 624 | return blockType === 'unordered-list-item' || blockType === 'ordered-list-item'; 625 | } 626 | /** 627 | * Function will return html markup for a list block. 628 | */ 629 | 630 | function getListMarkup(listBlocks, entityMap, hashtagConfig, directional, customEntityTransform) { 631 | var listHtml = []; 632 | var nestedListBlock = []; 633 | var previousBlock; 634 | listBlocks.forEach(function (block) { 635 | var nestedBlock = false; 636 | 637 | if (!previousBlock) { 638 | listHtml.push("<".concat(getBlockTag(block.type), ">\n")); 639 | } else if (previousBlock.type !== block.type) { 640 | listHtml.push("\n")); 641 | listHtml.push("<".concat(getBlockTag(block.type), ">\n")); 642 | } else if (previousBlock.depth === block.depth) { 643 | if (nestedListBlock && nestedListBlock.length > 0) { 644 | listHtml.push(getListMarkup(nestedListBlock, entityMap, hashtagConfig, directional, customEntityTransform)); 645 | nestedListBlock = []; 646 | } 647 | } else { 648 | nestedBlock = true; 649 | nestedListBlock.push(block); 650 | } 651 | 652 | if (!nestedBlock) { 653 | listHtml.push(''); 665 | listHtml.push(getBlockInnerMarkup(block, entityMap, hashtagConfig, customEntityTransform)); 666 | listHtml.push('\n'); 667 | previousBlock = block; 668 | } 669 | }); 670 | 671 | if (nestedListBlock && nestedListBlock.length > 0) { 672 | listHtml.push(getListMarkup(nestedListBlock, entityMap, hashtagConfig, directional, customEntityTransform)); 673 | } 674 | 675 | listHtml.push("\n")); 676 | return listHtml.join(''); 677 | } 678 | 679 | /** 680 | * The function will generate html markup for given draftjs editorContent. 681 | */ 682 | 683 | function draftToHtml(editorContent, hashtagConfig, directional, customEntityTransform) { 684 | var html = []; 685 | 686 | if (editorContent) { 687 | var blocks = editorContent.blocks, 688 | entityMap = editorContent.entityMap; 689 | 690 | if (blocks && blocks.length > 0) { 691 | var listBlocks = []; 692 | blocks.forEach(function (block) { 693 | if (isList(block.type)) { 694 | listBlocks.push(block); 695 | } else { 696 | if (listBlocks.length > 0) { 697 | var listHtml = getListMarkup(listBlocks, entityMap, hashtagConfig, customEntityTransform); // eslint-disable-line max-len 698 | 699 | html.push(listHtml); 700 | listBlocks = []; 701 | } 702 | 703 | var blockHtml = getBlockMarkup(block, entityMap, hashtagConfig, directional, customEntityTransform); 704 | html.push(blockHtml); 705 | } 706 | }); 707 | 708 | if (listBlocks.length > 0) { 709 | var listHtml = getListMarkup(listBlocks, entityMap, hashtagConfig, directional, customEntityTransform); // eslint-disable-line max-len 710 | 711 | html.push(listHtml); 712 | listBlocks = []; 713 | } 714 | } 715 | } 716 | 717 | return html.join(''); 718 | } 719 | 720 | return draftToHtml; 721 | 722 | }))); 723 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draftjs-to-html", 3 | "version": "0.9.1", 4 | "description": "A library for draftjs to html conversion.", 5 | "main": "lib/draftjs-to-html.js", 6 | "devDependencies": { 7 | "@babel/core": "^7.7.5", 8 | "@babel/preset-env": "^7.7.6", 9 | "@babel/preset-react": "^7.7.4", 10 | "@babel/register": "^7.7.4", 11 | "babel-eslint": "^10.0.3", 12 | "autoprefixer": "^9.7.3", 13 | "chai": "^4.2.0", 14 | "enzyme": "^3.10.0", 15 | "draft-js": "^0.11.3", 16 | "eslint": "^6.7.2", 17 | "eslint-config-airbnb": "^18.0.1", 18 | "eslint-plugin-import": "^2.18.2", 19 | "eslint-plugin-jsx-a11y": "^6.2.3", 20 | "eslint-plugin-mocha": "^6.2.2", 21 | "eslint-plugin-react": "^7.17.0", 22 | "jsdom": "^15.2.1", 23 | "mocha": "^6.2.2", 24 | "react": "^16.12.0", 25 | "react-dom": "^16.12.0", 26 | "rimraf": "^3.0.0", 27 | "rollup": "^1.27.9", 28 | "react-addons-test-utils": "^15.6.2", 29 | "rollup-plugin-babel": "^4.3.3", 30 | "sinon": "^7.5.0", 31 | "@size-limit/preset-small-lib": "^2.2.2" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/jpuri/draftjs-to-html.git" 36 | }, 37 | "scripts": { 38 | "size": "size-limit", 39 | "clean": "rimraf lib", 40 | "build": "npm run clean && rollup -c && npm run size", 41 | "dev": "rollup -c -w", 42 | "test": "mocha --require config/test-compiler.js config/test-setup.js js/**/*Test.js", 43 | "lint": "eslint js", 44 | "check": "npm run lint" 45 | }, 46 | "author": "Jyoti Puri", 47 | "license": "MIT", 48 | "size-limit": [ 49 | { 50 | "path": "lib/*", 51 | "webpack": false, 52 | "limit": "4.5 KB" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # DraftJS TO HTML 2 | 3 | A library for converting DraftJS Editor content to plain HTML. 4 | 5 | This is draft to HTML library I wrote for one of my projects. I am open-sourcing it so that others can also be benefitted from my work. 6 | 7 | ## Installation 8 | 9 | `npm install draftjs-to-html` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import { convertToRaw } from 'draft-js'; 15 | import draftToHtml from 'draftjs-to-html'; 16 | 17 | const rawContentState = convertToRaw(editorState.getCurrentContent()); 18 | 19 | const markup = draftToHtml( 20 | rawContentState, 21 | hashtagConfig, 22 | directional, 23 | customEntityTransform 24 | ); 25 | ``` 26 | The function parameters are: 27 | 28 | 1. **contentState**: Its instance of [RawDraftContentState](https://facebook.github.io/draft-js/docs/api-reference-data-conversion.html#content) 29 | 30 | 2. **hashConfig**: Its configuration object for hashtag, its required only if hashtags are used. If the object is not defined hashtags will be output as simple text in the markdown. 31 | ```js 32 | hashConfig = { 33 | trigger: '#', 34 | separator: ' ', 35 | } 36 | ``` 37 | Here trigger is character that marks starting of hashtag (default '#') and separator is character that separates characters (default ' '). These fields in hastag object are optional. 38 | 39 | 3. **directional**: Boolean, if directional is true text is aligned according to bidi algorithm. This is also optional. 40 | 41 | 4. **customEntityTransform**: Its function to render custom defined entities by user, its also optional. 42 | 43 | **editorState** is instance of DraftJS [EditorState](https://draftjs.org/docs/api-reference-editor-state.html#content). 44 | 45 | ## Supported conversions 46 | Following is the list of conversions it supports: 47 | 48 | 1. Convert block types to corresponding HTML tags: 49 | 50 | || Block Type | HTML Tag | 51 | | -------- | -------- | -------- | 52 | | 1 | header-one | h1 | 53 | | 2 | header-two | h2 | 54 | | 3 | header-three | h3 | 55 | | 4 | header-four | h4 | 56 | | 5 | header-five | h5 | 57 | | 6 | header-six | h6 | 58 | | 7 | unordered-list-item | ul | 59 | | 8 | ordered-list-item | ol | 60 | | 9 | blockquote | blockquote | 61 | | 10 | code | pre | 62 | | 11 | unstyled | p | 63 | 64 | It performs these additional changes to text of blocks: 65 | - replace blank space in beginning and end of block with ` ` 66 | - replace `\n` with `
` 67 | - replace `<` with `<` 68 | - replace `>` with `>` 69 | 70 | 71 | 2. Converts ordered and unordered list blocks with depths to nested structure of `