├── .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
\n- 2
\n- 3
\n
\n';
28 | output = '\n- 1
\n- 2
\n- 3
\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
\n- 2
\n
\n- 3
\n
\n';
35 | output = '\n- 1
\n\n- 2
\n
\n- 3
\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
\n- 2
\n- 3
\n
\n- 4
\n
\n';
42 | output = '\n- 1
\n\n- 2
\n- '
43 | + '3
\n
\n- 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
\n- 2
\n- 3
\n
'
50 | + '\n
\n- 3
\n
\n';
51 | output = '\n- 1
\n\n- 2
\n\n- 3'
52 | + '
\n
\n
\n- 3
\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
\n- 2
\n- 3
\n
\n';
68 | output = '\n- 1
\n- 2
\n- 3
\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
\n- 2
\n
\n- 3
\n
\n';
75 | output = '\n- 1
\n\n- 2
\n
\n- 3
\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
\n- 2
\n- 3
\n
\n- 4
\n
\n';
82 | output = '\n- 1
\n\n- 2
\n- 3'
83 | + '
\n
\n- 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
\n- 2
\n- 3
\n
'
90 | + '\n
\n- 3
\n
\n';
91 | output = '\n- 1
\n\n- 2
\n\n- 3'
92 | + '
\n
\n
\n- 3
\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 ``;
338 | }
339 | return `
`;
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(`${blockTag}>`);
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(`${getBlockTag(previousBlock.type)}>\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(`${getBlockTag(previousBlock.type)}>\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 "");
421 | }
422 |
423 | return "
");
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("".concat(blockTag, ">"));
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("".concat(getBlockTag(previousBlock.type), ">\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("".concat(getBlockTag(previousBlock.type), ">\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 `, ` and `- `.
72 |
73 | 3. Converts inline styles BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, CODE, SUPERSCRIPT, SUBSCRIPT to corresponding HTML tags: `, , ,
, , `.
74 |
75 | 4. Converts inline styles color, background-color, font-size, font-family to a span tag with inline style details:
76 | ``. (The inline styles in JSON object should start with strings `color` or `font-size` like `color-red`, `color-green` or `fontsize-12`, `fontsize-20`).
77 |
78 | 5. Converts entity range of type link to anchor tag using entity data url for href, targetOption for target: `text`. Default target is `_self`.
79 |
80 | 6. Converts entity range of type mention to anchor tag using entity data url for href and value for data-value, it also adds class to it: `text`.
81 |
82 | 7. Converts atomic entity image to image tag using entity data src for image source, and if present alt, alignment, height, width also: `
`.
83 |
84 | 8. Converts embedded links to iFrames, using width, height and src from entity data. ``
85 |
86 | 9. Converts hashtags to anchor tag: `#tag`.
87 |
88 | 9. `customEntityTransform` can be used for transformation of a custom entity block to html. If present its call to generate html for entity. It can take 2 parameter:
89 | 1. `entity` ( object with { type, mutalibity, data})
90 | 2. `text` text present in the block.
91 |
92 | 10. Adding style property to block tag for block level styles like text-align: `text
`.
93 |
94 | 11. RTL, if directional function parameter is true, generated blocks have property `dir = "auto"` thus they get aligned according to bidi algorithm.
95 |
96 | ## License
97 | MIT.
98 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import pkg from './package.json';
3 |
4 | export default [
5 | {
6 | input: 'js/index.js',
7 | external: ['react', 'react-dom', 'draft-js'],
8 | output: [{ file: pkg.main, format: 'umd', name: 'draftjsToHtml' }],
9 | plugins: [babel()],
10 | },
11 | ];
12 |
--------------------------------------------------------------------------------