├── .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 |
14 |
41 |
42 |
43 |
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('![]('+src+')'); 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![](https://placekitten.com/g/200/300)\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('[![](https://placekitten.com/g/200/300)](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 ![world](url)'; 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 ![world](url "title")'; 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 | --------------------------------------------------------------------------------