├── .babelrc
├── .gitignore
├── README.md
├── demo
├── index.ejs
├── index.js
└── style.css
├── package.json
├── src
├── converters.js
├── fromDelta.js
└── toDelta.js
├── tests
├── fromDelta.spec.js
├── multi.spec.js
└── simple.spec.js
├── tools
├── demoServer.js
└── testSetup.js
└── webpack.config.demo.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "latest",
4 | "stage-1"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Why
2 | I was looking to use QuillJS as a markdown editor, but couldn't find any direct way to input markdown. Wanting to know if there were any blocking issues, i started coding. This is the result, it is not finished, but it is usable to edit markdown using it.
3 | ## Issues
4 | - The conversion from Markdown to Quill delta format needs refactoring, or at least the image conversion should be fixed.
5 | - The code for the conversion to Markdown needs cleanup, also the resulting Markdown needs wrapping of long lines and could use more line spacing
6 | - More documentation, although the tests explain a lot
7 | - Document how to extend with your own formats
8 | - Build and publish on npm
9 | - Could use quill-delta or parchment to help with the conversions
10 |
11 |
12 | BTW this text is written using the Quill editor and converted into Markdown with the code
13 |
--------------------------------------------------------------------------------
/demo/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 | Demo
11 |
12 |
13 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import toDelta from '../src/toDelta.js';
2 | import fromDelta from '../src/fromDelta.js';
3 | require('./style.css');
4 | import Quill from 'quill';
5 | import '../node_modules/quill/dist/quill.snow.css';
6 | import _ from 'lodash';
7 |
8 | var input = document.getElementById('input');
9 | var output = document.getElementById('output');
10 | input.addEventListener('keydown', _.debounce(onInputChange), 500);
11 |
12 | var quill = new Quill('#editor-container', {
13 | modules: {
14 | toolbar: [
15 | [{ header: [1, 2, false] }],
16 | ['bold', 'italic', 'link'],
17 | [{ 'list': 'ordered'}, { 'list': 'bullet' }],
18 | ['image', 'code-block','blockquote']
19 | ]
20 | },
21 | placeholder: 'Compose an epic...',
22 | theme: 'snow' // or 'bubble'
23 | });
24 | quill.on('text-change', function() {
25 | var contents = quill.getContents();
26 | output.innerText = JSON.stringify(contents.ops, null, 2);
27 | output.nextElementSibling.innerText = fromDelta(contents.ops);
28 | });
29 | onInputChange();
30 |
31 | function onInputChange() {
32 | var contents = toDelta(input.value);
33 | quill.setContents(contents);
34 | input.nextElementSibling.innerText = JSON.stringify(contents, null, 2);
35 | }
36 |
--------------------------------------------------------------------------------
/demo/style.css:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 1024px;
3 | margin: 0 auto;
4 | }
5 | #editor-container, textarea, pre {
6 | height: 150px;
7 | }
8 | textarea, pre {
9 | width: 100%;
10 | overflow: auto;
11 | }
12 | pre {
13 | border: groove 2px;
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quill-delta-markdown",
3 | "version": "0.0.0",
4 | "description": "To and from Markdown and Quill delta",
5 | "main": "index.js",
6 | "scripts": {
7 | "demo": "babel-node tools/demoServer.js",
8 | "test": "mocha tools/testSetup.js \"tests/**/*.spec.js\" --reporter progress",
9 | "test:watch": "mocha tools/testSetup.js \"tests/**/*.spec.js\" --reporter progress --watch"
10 | },
11 | "keywords": [
12 | "quill",
13 | "delta",
14 | "markdown"
15 | ],
16 | "author": {
17 | "name": "Bart Visscher",
18 | "email": "bartv@thisnet.nl"
19 | },
20 | "license": "ISC",
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/bartv2/quill-delta-markdown.git"
24 | },
25 | "devDependencies": {
26 | "autoprefixer": "^6.5.1",
27 | "babel-cli": "6.14.0",
28 | "babel-core": "6.14.0",
29 | "babel-loader": "^6.2.5",
30 | "babel-preset-latest": "6.14.0",
31 | "babel-preset-stage-1": "6.13.0",
32 | "babel-register": "6.14.0",
33 | "browser-sync": "^2.17.5",
34 | "chai": "^3.5.0",
35 | "css-loader": "^0.25.0",
36 | "html-webpack-plugin": "^2.24.0",
37 | "json-loader": "^0.5.4",
38 | "mocha": "3.0.2",
39 | "postcss-loader": "^1.0.0",
40 | "style-loader": "^0.13.1",
41 | "webpack": "^1.13.2",
42 | "webpack-dev-middleware": "^1.8.4",
43 | "webpack-hot-middleware": "^2.13.0"
44 | },
45 | "dependencies": {
46 | "commonmark": "^0.26.0",
47 | "lodash": "^4.16.4",
48 | "quill-delta": "^3.4.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/converters.js:
--------------------------------------------------------------------------------
1 | import isEmpty from 'lodash/isEmpty';
2 | import { changeAttribute } from './toDelta';
3 |
4 | function addOnEnter(name) {
5 | return (event, attributes) => {
6 | if (!event.entering) {
7 | return null;
8 | }
9 | return { insert: event.node.literal, attributes: {...attributes, [name]: true}};
10 | };
11 | }
12 |
13 | const converters = [
14 | // inline
15 | { filter: 'code', makeDelta: addOnEnter('code')},
16 | { filter: 'html_inline', makeDelta: addOnEnter('html_inline')},
17 | // TODO: underline
18 | // TODO: strike
19 | { filter: 'emph', attribute: 'italic' },
20 | { filter: 'strong', attribute: 'bold' },
21 | // TODO: script
22 | { filter: 'link', attribute: (node, event, attributes) => {
23 | changeAttribute(attributes, event, 'link', node.destination);
24 | }},
25 | { filter: 'text', makeDelta: (event, attributes) => {
26 | if (isEmpty(attributes)) {
27 | return {insert: event.node.literal};
28 | } else {
29 | return {insert: event.node.literal, attributes: {...attributes}};
30 | }
31 | }},
32 | { filter: 'softbreak', makeDelta: (event, attributes) => {
33 | if (isEmpty(attributes)) {
34 | return {insert: ' '};
35 | } else {
36 | return {insert: ' ', attributes: {...attributes}};
37 | }
38 | }},
39 |
40 | // block
41 | { filter: 'block_quote', lineAttribute: true, attribute: 'blockquote' },
42 | { filter: 'code_block', lineAttribute: true, makeDelta: addOnEnter('code-block') },
43 | { filter: 'heading', lineAttribute: true, makeDelta: (event, attributes) => {
44 | if (event.entering) {
45 | return null;
46 | }
47 | return { insert: "\n", attributes: {...attributes, header: event.node.level}};
48 | }},
49 | { filter: 'list', lineAttribute: true, attribute: (node, event, attributes) => {
50 | changeAttribute(attributes, event, 'list', node.listType);
51 | }},
52 | { filter: 'paragraph', lineAttribute: true, makeDelta: (event, attributes) => {
53 | if (event.entering) {
54 | return null;
55 | }
56 |
57 | if (isEmpty(attributes)) {
58 | return { insert: "\n"};
59 | } else {
60 | return { insert: "\n", attributes: {...attributes}};
61 | }
62 | }},
63 |
64 | // embeds
65 | { filter: 'image', attribute: (node, event, attributes) => {
66 | changeAttribute(attributes, event, 'image', node.destination);
67 | if (node.title) {
68 | changeAttribute(attributes, event, 'title', node.title);
69 | }
70 | }},
71 |
72 | ];
73 |
74 | export default converters;
75 |
--------------------------------------------------------------------------------
/src/fromDelta.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 |
3 | exports = module.exports = function(ops) {
4 | return _.trimEnd(convert(ops).render()) + "\n";
5 | };
6 | var id = 0;
7 | function node(data)
8 | {
9 | this.id = ++id;
10 | if (_.isArray(data)) {
11 | this.open = data[0];
12 | this.close = data[1];
13 | } else if (_.isString(data)) {
14 | this.text = data;
15 | } else {
16 | // this.close = "\n";
17 | }
18 | this.children = [];
19 | }
20 | node.prototype.append = function(e)
21 | {
22 | if (!(e instanceof node)) {
23 | e = new node(e);
24 | }
25 | if (e._parent) {
26 | _.pull(e._parent.children, e);
27 | }
28 | e._parent = this;
29 | this.children = this.children.concat(e);
30 | }
31 | node.prototype.render = function()
32 | {
33 | var text = '';
34 |
35 | if (this.open) {
36 | text += this.open;
37 | }
38 |
39 | if (this.text) {
40 | text += this.text;
41 | }
42 |
43 | for (var i = 0; i < this.children.length; i++) {
44 | text += this.children[i].render();
45 | }
46 |
47 | if (this.close) {
48 | text += this.close;
49 | }
50 |
51 | return text;
52 | }
53 | node.prototype.parent = function()
54 | {
55 | return this._parent;
56 | }
57 | var format = exports.format = {
58 |
59 | embed: {
60 | image: function(src, attributes) {
61 | this.append('');
62 | }
63 | },
64 |
65 | inline: {
66 | italic: function() {
67 | return ['*', '*'];
68 | },
69 | bold: function() {
70 | return ['**', '**'];
71 | },
72 | code: function() {
73 | return ['`', '`'];
74 | },
75 | link: function(href) {
76 | return ['[', ']('+href+')'];
77 | }
78 | },
79 |
80 | block: {
81 | header: function(header) {
82 | this.open = '#'.repeat(header)+' '+this.open;
83 | },
84 | blockquote: function(header) {
85 | this.open = '> '+this.open;
86 | },
87 | 'code-block': function(header) {
88 | this.open = "```\n"+this.open;
89 | this.close = this.close+"```\n";
90 | },
91 | list: {
92 | group: function() {
93 | return new node(['', "\n"])
94 | },
95 | line: function(type, group) {
96 | if (type == 'ordered') {
97 | group.count = group.count || 0;
98 | var count = ++group.count;
99 | this.open = count+'. '+this.open;
100 | } else {
101 | this.open = '- '+this.open;
102 | }
103 | }
104 | }
105 | }
106 |
107 | };
108 |
109 | function convert(ops) {
110 | var group, line, el, activeInline, beginningOfLine;
111 | var root = new node();
112 |
113 | function newLine() {
114 | el = line = new node(["", "\n"]);
115 | root.append(line);
116 | activeInline = {};
117 | }
118 | newLine();
119 |
120 | for (var i = 0; i < ops.length; i++) {
121 | var op = ops[i];
122 |
123 | if (_.isObject(op.insert)) {
124 | for (var k in op.insert) {
125 | if (format.embed[k]) {
126 | applyStyles(op.attributes);
127 | format.embed[k].call(el, op.insert[k], op.attributes);
128 | }
129 | }
130 | } else {
131 | var lines = op.insert.split('\n');
132 |
133 | if (isLinifyable(op.attributes)) {
134 | // Some line-level styling (ie headings) is applied by inserting a \n
135 | // with the style; the style applies back to the previous \n.
136 | // There *should* only be one style in an insert operation.
137 |
138 | for (var j = 1; j < lines.length; j++) {
139 | for (var k in op.attributes) {
140 | if (format.block[k]) {
141 |
142 | var fn = format.block[k];
143 | if (typeof fn == 'object') {
144 | if (group && group.type != k) {
145 | group = null;
146 | }
147 | if (!group && fn.group) {
148 | group = {
149 | el: fn.group(),
150 | type: k,
151 | value: op.attributes[k],
152 | distance: 0
153 | };
154 | root.append(group.el);
155 | }
156 |
157 | if (group) {
158 | group.el.append(line);
159 | group.distance = 0;
160 | }
161 | fn = fn.line;
162 | }
163 |
164 | fn.call(line, op.attributes[k], group);
165 | newLine();
166 | break;
167 | }
168 | }
169 | }
170 | beginningOfLine = true;
171 |
172 | } else {
173 | for (var j = 0; j < lines.length; j++) {
174 | if ((j > 0 || beginningOfLine) && group && ++group.distance >= 2) {
175 | group = null;
176 | }
177 | applyStyles(op.attributes, ops[i+1] && ops[i+1].attributes);
178 | el.append(lines[j]);
179 | if (j < lines.length-1) {
180 | newLine();
181 | }
182 | }
183 | beginningOfLine = false;
184 |
185 | }
186 | }
187 | }
188 |
189 | return root;
190 |
191 | function applyStyles(attrs, next) {
192 |
193 | var first = [], then = [];
194 | attrs = attrs || {};
195 |
196 | var tag = el, seen = {};
197 | while (tag._format) {
198 | seen[tag._format] = true;
199 | if (!attrs[tag._format]) {
200 | for (var k in seen) {
201 | delete activeInline[k];
202 | }
203 | el = tag.parent();
204 | }
205 |
206 | tag = tag.parent();
207 | }
208 |
209 | for (var k in attrs) {
210 | if (format.inline[k]) {
211 |
212 | if (activeInline[k]) {
213 | if (activeInline[k] != attrs[k]) {
214 | // ie when two links abut
215 |
216 | } else {
217 | continue; // do nothing -- we should already be inside this style's tag
218 | }
219 | }
220 |
221 | if (next && attrs[k] == next[k]) {
222 | first.push(k); // if the next operation has the same style, this should be the outermost tag
223 | } else {
224 | then.push(k);
225 | }
226 | activeInline[k] = attrs[k];
227 |
228 | }
229 | }
230 |
231 | first.forEach(apply);
232 | then.forEach(apply);
233 |
234 | function apply(fmt) {
235 | var newEl = format.inline[fmt].call(null, attrs[fmt]);
236 | if (_.isArray(newEl)) {
237 | newEl = new node(newEl);
238 | }
239 | newEl._format = fmt;
240 | el.append(newEl);
241 | el = newEl;
242 | }
243 | }
244 | }
245 |
246 | function isLinifyable(attrs) {
247 | for (var k in attrs) {
248 | if (format.block[k]) {
249 | return true;
250 | }
251 | }
252 | return false;
253 | }
254 |
--------------------------------------------------------------------------------
/src/toDelta.js:
--------------------------------------------------------------------------------
1 | import {isEmpty, unset} from 'lodash';
2 | import commonmark from 'commonmark';
3 | import converters from './converters';
4 |
5 | export function changeAttribute(attributes, event, attribute, value)
6 | {
7 | if (event.entering) {
8 | attributes[attribute] = value;
9 | } else {
10 | attributes = unset(attributes, attribute);
11 | }
12 | return attributes;
13 | }
14 |
15 | function applyAttribute(node, event, attributes, attribute)
16 | {
17 | if (typeof attribute == 'string') {
18 | changeAttribute(attributes, event, attribute, true);
19 | } else if (typeof attribute == 'function') {
20 | attribute(node, event, attributes);
21 | }
22 | }
23 |
24 | function toDelta(markdown) {
25 | var parsed = toDelta.commonmark.parse(markdown);
26 | var walker = parsed.walker();
27 | var event, node;
28 | var deltas = [];
29 | var attributes = {};
30 | var lineAttributes = {};
31 |
32 | while ((event = walker.next())) {
33 | node = event.node;
34 | for (var i = 0; i < toDelta.converters.length; i++) {
35 | const converter = toDelta.converters[i];
36 | if (node.type == converter.filter) {
37 | if (converter.lineAttribute) {
38 | applyAttribute(node, event, lineAttributes, converter.attribute);
39 | } else {
40 | applyAttribute(node, event, attributes, converter.attribute);
41 | }
42 | if (converter.makeDelta) {
43 | let delta = converter.makeDelta(event, converter.lineAttribute ? lineAttributes : attributes);
44 | if (delta) {
45 | deltas.push(delta);
46 | }
47 | }
48 | break;
49 | }
50 | }
51 | }
52 | if (isEmpty(deltas) || deltas[deltas.length-1].insert.indexOf("\n") == -1) {
53 | deltas.push({insert: "\n"});
54 | }
55 |
56 | return deltas;
57 | }
58 |
59 | toDelta.commonmark = new commonmark.Parser();
60 | toDelta.converters = converters;
61 |
62 | export default toDelta;
63 |
--------------------------------------------------------------------------------
/tests/fromDelta.spec.js:
--------------------------------------------------------------------------------
1 | var render = require('../src/fromDelta'),
2 | expect = require('chai').expect;
3 |
4 | describe('fromDelta', function() {
5 |
6 | it('renders inline format', function() {
7 |
8 | expect(render([
9 | {
10 | "insert": "Hi "
11 | },
12 | {
13 | "attributes": {
14 | "bold": true
15 | },
16 | "insert": "mom"
17 | }
18 | ]))
19 | .to.equal('Hi **mom**\n');
20 |
21 | });
22 |
23 | it('renders embed format', function() {
24 |
25 | expect(render([
26 | {
27 | "insert": "LOOK AT THE KITTEN!\n"
28 | },
29 | {
30 | "insert": {
31 | "image": "https://placekitten.com/g/200/300"
32 | },
33 | }
34 | ]))
35 | .to.equal('LOOK AT THE KITTEN!\n\n');
36 |
37 | });
38 |
39 | it('renders block format', function() {
40 |
41 | expect(render([
42 | {
43 | "insert": "Headline"
44 | },
45 | {
46 | "attributes": {
47 | "header": 1
48 | },
49 | "insert": "\n"
50 | }
51 | ]))
52 | .to.equal('# Headline\n');
53 | });
54 |
55 | it('renders lists with inline formats correctly', function() {
56 |
57 | expect(render([
58 | {
59 | "attributes": {
60 | "italic": true
61 | },
62 | "insert": "Glenn v. Brumby"
63 | },
64 | {
65 | "insert": ", 663 F.3d 1312 (11th Cir. 2011)"
66 | },
67 | {
68 | "attributes": {
69 | "list": 'ordered'
70 | },
71 | "insert": "\n"
72 | },
73 | {
74 | "attributes": {
75 | "italic": true
76 | },
77 | "insert": "Barnes v. City of Cincinnati"
78 | },
79 | {
80 | "insert": ", 401 F.3d 729 (6th Cir. 2005)"
81 | },
82 | {
83 | "attributes": {
84 | "list": 'ordered'
85 | },
86 | "insert": "\n"
87 | }
88 | ]))
89 | .to.equal('1. *Glenn v. Brumby*, 663 F.3d 1312 (11th Cir. 2011)\n2. *Barnes v. City of Cincinnati*, 401 F.3d 729 (6th Cir. 2005)\n');
90 |
91 | });
92 |
93 | it('renders adjacent lists correctly', function() {
94 |
95 | expect(render([
96 | {
97 | "insert": "Item 1"
98 | },
99 | {
100 | "insert": "\n",
101 | "attributes": {
102 | "list": 'ordered'
103 | }
104 | },
105 | {
106 | "insert": "Item 2"
107 | },
108 | {
109 | "insert": "\n",
110 | "attributes": {
111 | "list": 'ordered'
112 | }
113 | },
114 | {
115 | "insert": "Item 3"
116 | },
117 | {
118 | "insert": "\n",
119 | "attributes": {
120 | "list": 'ordered'
121 | }
122 | },
123 | {
124 | "insert": "Intervening paragraph\nItem 4"
125 | },
126 | {
127 | "insert": "\n",
128 | "attributes": {
129 | "list": 'ordered'
130 | }
131 | },
132 | {
133 | "insert": "Item 5"
134 | },
135 | {
136 | "insert": "\n",
137 | "attributes": {
138 | "list": 'ordered'
139 | }
140 | },
141 | {
142 | "insert": "Item 6"
143 | },
144 | {
145 | "insert": "\n",
146 | "attributes": {
147 | "list": 'ordered'
148 | }
149 | }
150 | ]))
151 | .to.equal('1. Item 1\n2. Item 2\n3. Item 3\n\nIntervening paragraph\n1. Item 4\n2. Item 5\n3. Item 6\n');
152 |
153 | });
154 |
155 | it('renders adjacent inline formats correctly', function() {
156 | expect(render([
157 | {
158 | "attributes" : {
159 | "italic" : true
160 | },
161 | "insert" : "Italics! "
162 | },
163 | {
164 | "attributes": {
165 | "italic": true,
166 | "link": "http://example.com"
167 | },
168 | "insert": "Italic link"
169 | },
170 | {
171 | "attributes": {
172 | "link": "http://example.com"
173 | },
174 | "insert": " regular link"
175 | }
176 |
177 | ]))
178 | .to.equal('*Italics! [Italic link](http://example.com)*[ regular link](http://example.com)'+"\n");
179 | });
180 |
181 | it('handles embed inserts with inline styles', function() {
182 | expect(render([
183 | {
184 | "insert": {
185 | "image": "https://placekitten.com/g/200/300",
186 | },
187 | "attributes": {
188 | "link": "http://example.com"
189 | },
190 | }
191 | ]))
192 | .to.equal('[](http://example.com)'+"\n");
193 | });
194 | /*
195 | it('is XSS safe in regular text', function() {
196 | expect(render([
197 | {
198 | "insert": '
'
199 | }
200 | ]))
201 | .to.equal('<img src=x onerror="doBadThings()">
');
202 | });
203 |
204 | it('is XSS safe in images', function() {
205 | expect(render([
206 | {
207 | "insert": {
208 | "image": '">
'
209 | },
210 | }
211 | ]))
212 | .to.equal('">)
');
213 | });*/
214 | });
215 |
--------------------------------------------------------------------------------
/tests/multi.spec.js:
--------------------------------------------------------------------------------
1 | import chai, {expect} from 'chai';
2 | import toDelta from '../src/toDelta';
3 |
4 | describe('toDelta', () => {
5 | it('converts text with emphasis and strong', () => {
6 | const input = 'Hello *w**or**ld*';
7 | const expected = [
8 | { insert: 'Hello '},
9 | { insert: 'w', attributes: { "italic": true } },
10 | { insert: 'or', attributes: { "bold": true, "italic": true } },
11 | { insert: 'ld', attributes: { "italic": true } },
12 | { insert: "\n" }
13 | ];
14 |
15 | var result = toDelta(input);
16 |
17 | expect(result).to.deep.equal(expected);
18 | });
19 |
20 | it('converts text with strong and emphasis', () => {
21 | const input = 'Hello **w*or*ld**';
22 | const expected = [
23 | { insert: 'Hello '},
24 | { insert: 'w', attributes: { "bold": true } },
25 | { insert: 'or', attributes: { "bold": true, "italic": true } },
26 | { insert: 'ld', attributes: { "bold": true } },
27 | { insert: "\n" }
28 | ];
29 |
30 | var result = toDelta(input);
31 |
32 | expect(result).to.deep.equal(expected);
33 | });
34 |
35 |
36 | it('converts text with strong link', () => {
37 | const input = 'Hello **[world](url)**';
38 | const expected = [{ insert: 'Hello '}, { insert: 'world', attributes: { "link": 'url', "bold": true } }, { insert: "\n" }];
39 |
40 | var result = toDelta(input);
41 |
42 | expect(result).to.deep.equal(expected);
43 | });
44 |
45 | it('converts text block quote', () => {
46 | const input = "> line *1*\n>\n> line 2\n";
47 | const expected = [
48 | { insert: 'line '},
49 | { insert: '1', attributes: { "italic": true } },
50 | { insert: "\n", attributes: { "blockquote": true } },
51 | { insert: 'line 2' },
52 | { insert: "\n", attributes: { "blockquote": true } }
53 | ];
54 |
55 | var result = toDelta(input);
56 |
57 | expect(result).to.deep.equal(expected);
58 | });
59 |
60 | it('converts text code block', () => {
61 | const input = "```\nline 1\nline 2\n```\n\n";
62 | const expected = [
63 | { insert: "line 1\nline 2\n", attributes: { "code-block": true } }
64 | ];
65 |
66 | var result = toDelta(input);
67 |
68 | expect(result).to.deep.equal(expected);
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/tests/simple.spec.js:
--------------------------------------------------------------------------------
1 | import chai, {expect} from 'chai';
2 | import toDelta from '../src/toDelta';
3 |
4 | describe('toDelta', () => {
5 | it('converts text with emphasis', () => {
6 | const input = 'Hello *world*';
7 | const expected = [{ insert: 'Hello '}, { insert: 'world', attributes: { "italic": true } }, { insert: "\n" }];
8 |
9 | var result = toDelta(input);
10 |
11 | expect(result).to.deep.equal(expected);
12 | });
13 |
14 | it('converts text with strong', () => {
15 | const input = 'Hello **world**';
16 | const expected = [{ insert: 'Hello '}, { insert: 'world', attributes: { "bold": true } }, { insert: "\n" }];
17 |
18 | var result = toDelta(input);
19 |
20 | expect(result).to.deep.equal(expected);
21 | });
22 |
23 | it('converts text with link', () => {
24 | const input = 'Hello [world](url)';
25 | const expected = [{ insert: 'Hello '}, { insert: 'world', attributes: { "link": 'url' } }, { insert: "\n" }];
26 |
27 | var result = toDelta(input);
28 |
29 | expect(result).to.deep.equal(expected);
30 | });
31 |
32 | it('converts text with image', () => {
33 | const input = 'Hello ';
34 | const expected = [{ insert: 'Hello '}, { insert: { "image": 'url' }, attributes: { alt: 'world' } }, { insert: "\n" }];
35 |
36 | var result = toDelta(input);
37 |
38 | expect(result).to.deep.equal(expected);
39 | });
40 |
41 |
42 | it('converts text with image with title', () => {
43 | const input = 'Hello ';
44 | const expected = [{ insert: 'Hello '}, { insert: { "image": 'url' }, attributes: { alt: 'world', title: 'title' } }, { insert: "\n" }];
45 |
46 | var result = toDelta(input);
47 |
48 | expect(result).to.deep.equal(expected);
49 | });
50 |
51 | it('converts multi paragraphs', () => {
52 | const input = "line 1\n\nline 2\n";
53 | const expected = [{ insert: 'line 1'}, { insert: "\n" }, { insert: 'line 2' }, { insert: "\n" }];
54 |
55 | var result = toDelta(input);
56 |
57 | expect(result).to.deep.equal(expected);
58 | });
59 |
60 | it('converts headings level 1', () => {
61 | const input = "# heading\n";
62 | const expected = [{ insert: 'heading'}, { insert: "\n", attributes: { header: 1 }}];
63 |
64 | var result = toDelta(input);
65 |
66 | expect(result).to.deep.equal(expected);
67 | });
68 |
69 | it('converts bullet list', () => {
70 | const input = "- line 1\n- line 2\n";
71 | const expected = [
72 | { insert: 'line 1'}, { insert: "\n", attributes: { list: 'bullet' } },
73 | { insert: 'line 2' }, { insert: "\n", attributes: { list: 'bullet' } }
74 | ];
75 |
76 | var result = toDelta(input);
77 |
78 | expect(result).to.deep.equal(expected);
79 | });
80 |
81 | it('converts bullet list with softbreak', () => {
82 | const input = "- line 1\nmore\n- line 2\n";
83 | const expected = [
84 | { insert: 'line 1'}, { insert: ' '}, { insert: 'more'}, { insert: "\n", attributes: { list: 'bullet' } },
85 | { insert: 'line 2' }, { insert: "\n", attributes: { list: 'bullet' } }
86 | ];
87 |
88 | var result = toDelta(input);
89 |
90 | expect(result).to.deep.equal(expected);
91 | });
92 |
93 | it('converts ordered list', () => {
94 | const input = "1. line 1\n2. line 2\n";
95 | const expected = [
96 | { insert: 'line 1'}, { insert: "\n", attributes: { list: 'ordered' } },
97 | { insert: 'line 2' }, { insert: "\n", attributes: { list: 'ordered' } }
98 | ];
99 |
100 | var result = toDelta(input);
101 |
102 | expect(result).to.deep.equal(expected);
103 | });
104 |
105 | it('converts text with inline code block', () => {
106 | const input = "start `code` more\n";
107 | const expected = [
108 | { "insert": "start " },
109 | {
110 | "attributes": { "code": true },
111 | "insert": "code"
112 | },
113 | { "insert": " more" },
114 | { "insert": "\n" }
115 | ];
116 |
117 | var result = toDelta(input);
118 |
119 | expect(result).to.deep.equal(expected);
120 | });
121 |
122 | it('converts text with html', () => {
123 | const input = "start more\n";
124 | const expected = [
125 | { "insert": "start " },
126 | {
127 | "attributes": { "html_inline": true },
128 | "insert": ""
129 | },
130 | { "insert": " more" },
131 | { "insert": "\n" }
132 | ];
133 |
134 | var result = toDelta(input);
135 |
136 | expect(result).to.deep.equal(expected);
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/tools/demoServer.js:
--------------------------------------------------------------------------------
1 | // This file configures the development web server
2 | // which supports hot reloading and synchronized testing.
3 |
4 | // Require Browsersync along with webpack and middleware for it
5 | import browserSync from 'browser-sync';
6 | import webpack from 'webpack';
7 | import webpackDevMiddleware from 'webpack-dev-middleware';
8 | import webpackHotMiddleware from 'webpack-hot-middleware';
9 | import config from '../webpack.config.demo';
10 |
11 | const bundler = webpack(config);
12 |
13 | // Run Browsersync and use middleware for Hot Module Replacement
14 | browserSync({
15 | port: 3000,
16 | ui: {
17 | port: 3001
18 | },
19 | server: {
20 | baseDir: 'src',
21 |
22 | middleware: [
23 | webpackDevMiddleware(bundler, {
24 | // Dev middleware can't access config, so we provide publicPath
25 | publicPath: config.output.publicPath,
26 |
27 | // These settings suppress noisy webpack output so only errors are displayed to the console.
28 | noInfo: false,
29 | quiet: false,
30 | stats: {
31 | assets: false,
32 | colors: true,
33 | version: false,
34 | hash: false,
35 | timings: false,
36 | chunks: false,
37 | chunkModules: false
38 | },
39 |
40 | // for other settings see
41 | // http://webpack.github.io/docs/webpack-dev-middleware.html
42 | }),
43 |
44 | // bundler should be the same as above
45 | webpackHotMiddleware(bundler)
46 | ]
47 | },
48 |
49 | // no need to watch '*.js' here, webpack will take care of it for us,
50 | // including full page reloads if HMR won't work
51 | files: [
52 | 'demo/*.html',
53 | 'src/*.html'
54 | ]
55 | });
56 |
--------------------------------------------------------------------------------
/tools/testSetup.js:
--------------------------------------------------------------------------------
1 | // Register babel so that it will transpile ES6 to ES5
2 | // before our tests run.
3 | require('babel-register')();
4 |
5 |
--------------------------------------------------------------------------------
/webpack.config.demo.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import HtmlWebpackPlugin from 'html-webpack-plugin';
3 | import autoprefixer from 'autoprefixer';
4 |
5 | export default {
6 | resolve: {
7 | extensions: ['', '.js', '.jsx']
8 | },
9 | debug: true,
10 | devtool: 'cheap-module-source-map', // more info:https://webpack.github.io/docs/build-performance.html#sourcemaps and https://webpack.github.io/docs/configuration.html#devtool
11 | noInfo: true, // set to false to see a list of every file being bundled.
12 | entry: [
13 | 'webpack-hot-middleware/client?reload=true',
14 | './demo'
15 | ],
16 | target: 'web', // necessary per https://webpack.github.io/docs/testing.html#compile-and-test
17 | output: {
18 | path: `${__dirname}/src`, // Note: Physical files are only output by the production build task `npm run build`.
19 | publicPath: '/',
20 | filename: 'bundle.js'
21 | },
22 | plugins: [
23 | new webpack.DefinePlugin({
24 | 'process.env.NODE_ENV': JSON.stringify('development'),
25 | __DEV__: true
26 | }),
27 | new webpack.HotModuleReplacementPlugin(),
28 | new webpack.NoErrorsPlugin(),
29 | new HtmlWebpackPlugin({ // Create HTML file that includes references to bundled CSS and JS.
30 | template: 'demo/index.ejs',
31 | minify: {
32 | removeComments: true,
33 | collapseWhitespace: true
34 | },
35 | inject: true
36 | })
37 | ],
38 | module: {
39 | loaders: [
40 | {test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel']},
41 | {test: /\.eot(\?v=\d+.\d+.\d+)?$/, loader: 'file'},
42 | {test: /\.json$/, loader: 'json'},
43 | {test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url?limit=10000&mimetype=application/font-woff"},
44 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream'},
45 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml'},
46 | {test: /\.(jpe?g|png|gif)$/i, loader: 'file?name=[name].[ext]'},
47 | {test: /\.ico$/, loader: 'file?name=[name].[ext]'},
48 | {test: /(\.css|\.scss)$/, loaders: ['style', 'css?sourceMap', 'postcss']}
49 | ]
50 | },
51 | postcss: ()=> [autoprefixer]
52 | };
53 |
--------------------------------------------------------------------------------