├── .gitignore ├── LICENSE.txt ├── README.md ├── bower.json ├── example.html ├── gulpfile.js ├── mdedit.css ├── mdedit.js ├── mdedit.min.js ├── package.json ├── prism-all.js ├── prism.css └── src ├── Editor.js ├── SelectionManager.js ├── UndoManager.js ├── actions.js ├── md.js ├── util.js └── yaml.js /.gitignore: -------------------------------------------------------------------------------- 1 | compiler.jar 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 James Taylor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mdEdit 2 | 3 | Syntax-highlighted / semi-formatted markdown editor view with minimal dependencies. 4 | 5 | As seen in my [online markdown editor](//jbt.github.io/markdown-editor) (the left-hand side is this editor, or more accurately it will be once I've merged [this pull request](https://github.com/jbt/markdown-editor/pull/25)). 6 | 7 | Requires [Prism](//prismjs.com) - Prism core is required plus any languages you want to be syntax-highlighted inside fenced code blocks. The bundled `prism-all.js` includes _all_ available languages. 8 | 9 | 10 | ## Usage 11 | 12 | * Include `prism.css` and `mdedit.css` 13 | * Include `prism-all.js` and `mdedit.js` 14 | * Include a `
` element where you want an editor 15 | * Then `var editor = mdEdit(thatPreElement, {options})`; 16 | 17 | 18 | ## API 19 | 20 | * The `options` parameter to the constructor may include the following configuration options: 21 | * `className` - any css classes to apply to the editor view 22 | * `change` - callback function that is called whenever the editor value changes (value is passed as an argument) 23 | 24 | * `editor.getValue()` - returns the current value of the editor view 25 | * `editor.setValue(val)` - sets the current value to `val` and updates the view 26 | 27 | 28 | ## Browser support 29 | 30 | Anything that supports ES5 well enough. That means (hopefully) IE9+, and all recent versions of all the other browsers. 31 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdEdit", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/jbt/mdEdit", 5 | "authors": [ 6 | "James Taylor" 7 | ], 8 | "description": "nice syntax-highlighted markdown editor view component", 9 | "main": ["mdedit.js", "mdedit.css"], 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "markdown", 17 | "editor", 18 | "highlight" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | # Big heading 30 | 31 | ## Smaller heading 32 | 33 | ### Smaller 34 | 35 | #### And 36 | 37 | ##### Smaller 38 | 39 | Something **strong**. Something _emphatic_. Some `code`. Something ~~deleted~~. 40 | 41 | * Here 42 | * is a 43 | * list 44 | 45 | 1. Numbered 46 | 2. Lists 47 | 3. Too 48 | 49 | > Block quotes 50 | > as well 51 | 52 | [Links](foo) too [with titles](<bar> "and all that jazz"). Also . 53 | 54 | <html is="also valid markdown of course"> 55 | 56 | ```javascript 57 | $('code').with(/languages/, function(){ 58 | // Also gets syntax highlighting 59 | }); 60 | ``` 61 | 62 | \<escaped html doesn't count though> 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | # H1 102 | 103 | ## H2 104 | 105 | ### H3 106 | 107 | #### H4 108 | 109 | ##### H5 110 | 111 | ##### H6 112 | 113 | ## H2 with trailing hashes ## 114 | 115 | 116 | 117 | ### H3 with some leading space 118 | 119 | 120 | # H1 indented too far 121 | 122 | 123 | seText-style H1 124 | ====== 125 | 126 | 127 | 128 | seText-style H2 129 | ------ 130 | 131 | 132 | unaligned H1 133 | = 134 | 135 | 136 | unaligned h2 137 | -- 138 | 139 | 140 | h1 indented too far 141 | = 142 | 143 | 144 | 145 | 146 | not an hr: 147 | 148 | === 149 | 150 | 151 | this is an hr: 152 | 153 | *** 154 | 155 | and this: 156 | 157 | --- 158 | 159 | and this: 160 | 161 | ___ 162 | 163 | 164 | and this: 165 | 166 | * * * * ** ** 167 | 168 | not this: 169 | 170 | * _ * 171 | 172 | 173 | 174 | 175 | **strong** stuff 176 | 177 | __strong 178 | stuff__ 179 | 180 | 181 | **not strong 182 | 183 | stuff** 184 | 185 | 186 | 187 | 188 | Here is some _emphatic_ stuff 189 | 190 | 191 | *emphatic 192 | stuff I say!* 193 | 194 | 195 | _not 196 | 197 | very emphatic_ 198 | 199 | 200 | 201 | 202 | 203 | 204 | Here is a **strong, *emphatic* thing**. 205 | 206 | And **another *one*** 207 | 208 | And ***another one!*** 209 | 210 | And **_this_ one too** 211 | 212 | And __this *one*__ 213 | 214 | This __is **silly though**__ 215 | 216 | 217 | Here is an _emphatic, **strong** thing_ 218 | 219 | And _another __one___ 220 | 221 | And *another **one*** 222 | 223 | 224 | ~~deleted stuff~~ 225 | 226 | 227 | * list 228 | * listy 229 | * listy 230 | 231 | 1. num 232 | * bered 233 | * lists 234 | moar 235 | * blah 236 | 237 | 238 | * **formatting** _in_ ~~lists~~ please with `code` and stuff 239 | * nesting? 240 | * maybe 241 | * too? 242 | 243 | 244 | 245 | `code` 246 | 247 | Lots of text with `code` in it 248 | 249 | Code `has no *stuff inside it*`. 250 | 251 | # Even `headings` can have code _and other_ **things** 252 | 253 | 254 | 255 | 256 | 257 | [link](aaaa) 258 | 259 | [link](<aaa>) 260 | 261 | [link](aaaa "title") 262 | 263 | [link](aaaa(foo) "tit\"le") 264 | 265 | [link](<blahblah> 'som"e"thing') 266 | 267 | 268 |  269 | 270 |  271 | 272 |  273 | 274 |  "tit\"le") 275 | 276 |  277 | 278 | 279 | 280 | [link **with** _somme_ `formatting`](foo) 281 | 282 |  283 | 284 | 285 | This probably won't work: 286 | 287 | [link with ](bar) 288 | 289 | 290 | 291 | > blockquote 292 | 293 | 294 | 295 | > blockquote 296 | > long 297 | > long 298 | > blockquote 299 | 300 | > blockquote 301 | with 302 | lazy 303 | coninutation 304 | > and picking 305 | > back up 306 | agan 307 | 308 | > * list 309 | * inside 310 | * blockquote 311 | * omg 312 | 313 | 314 | 315 | [refdef]: /blah "title" 316 | 317 | [def2]: 318 | /blah 319 | "title" 320 | 321 | 322 | 323 | [link][withRef] 324 | 325 | [link] [with other ref] 326 | 327 | ![img][withref] 328 | 329 | ![img] [withotherref] 330 | 331 | 332 | [shortcut] 333 | 334 | 335 | [shortcut **can have stuff in it**] 336 | 337 | [shortcut **can have stuff in it**]: /ref "can't though" 338 | 339 | 340 | ![img shortcut] 341 | 342 | 343 | ``` 344 | CODE BLOK 345 | ``` 346 | 347 | 348 | ~~~ 349 | CODE BLOK 350 | ~~~ 351 | 352 | indented 353 | code is da 354 | best 355 | 356 | 357 | ```html 358 | <hi iam="some html"> 359 | ``` 360 | 361 | ~~~html 362 | <hi iam="some html"> 363 | ~~~ 364 | 365 | 366 | 367 | Righty 368 | 369 | <table> 370 | hi I am some html 371 | </table> 372 | 373 | 374 | ## <i class="foo bar">ok then</i> stuffs 375 | 376 | 377 | 378 | --- 379 | Still to do: 380 | 381 | * footnotes 382 | * emoji 383 | * tables 384 | * emails + urls 385 | 386 | Bigger stuff: 387 | 388 | * Tweak Prism for a better concept of ordering and precedence 389 | * Lots of actions 390 | * ctrl-b bold/unbold 391 | * ctrl-i em/un-em 392 | * Sections? 393 | * urldef linkText / linkLabel? 394 | * think through linktext not allowing other links etc 395 | 396 |397 | 398 | 399 | 400 | 407 | 408 | 409 | 410 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var concat = require('gulp-concat'); 3 | var rename = require('gulp-rename'); 4 | var uglify = require('gulp-uglify'); 5 | var wrap = require('gulp-wrap-umd'); 6 | var del = require('del'); 7 | 8 | gulp.task('build', function(){ 9 | return gulp.src([ 10 | 'src/yaml.js', 11 | 'src/md.js', 12 | 'src/util.js', 13 | 'src/actions.js', 14 | 'src/SelectionManager.js', 15 | 'src/UndoManager.js', 16 | 'src/Editor.js' 17 | ]) 18 | .pipe(concat('mdedit.js')) 19 | .pipe(wrap({ 20 | namespace: 'mdEdit', 21 | deps: [ 22 | { name: 'prismjs', paramName: 'Prism', globalName: 'Prism', amdName: 'prismjs' } 23 | ], 24 | exports: 'Editor' 25 | })) 26 | .pipe(gulp.dest('./')) 27 | .pipe(rename({ suffix: '.min' })) 28 | .pipe(uglify()) 29 | .pipe(gulp.dest('./')); 30 | }); 31 | 32 | gulp.task('clean', function(cb) { 33 | del(['mdedit.js', 'mdedit.min.js'], cb); 34 | }); 35 | -------------------------------------------------------------------------------- /mdedit.css: -------------------------------------------------------------------------------- 1 | 2 | .mdedit { 3 | font-size: 12px; 4 | line-height: 1.3; 5 | color: black; 6 | /*text-shadow: 0 1px white;*/ 7 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 8 | direction: ltr; 9 | text-align: left; 10 | white-space: pre; 11 | /*************/ white-space: pre-wrap; 12 | word-spacing: normal; 13 | word-break: normal; 14 | line-height: 1.5; 15 | 16 | -moz-tab-size: 4; 17 | -o-tab-size: 4; 18 | tab-size: 4; 19 | 20 | -webkit-hyphens: none; 21 | -moz-hyphens: none; 22 | -ms-hyphens: none; 23 | hyphens: none; 24 | padding: 1em; 25 | overflow: auto; 26 | background: #f5f2f0; 27 | } 28 | 29 | .mdedit:focus { 30 | outline: none; 31 | } 32 | 33 | .mdedit::-moz-selection, .mdedit ::-moz-selection { 34 | background: #b3d4fc; 35 | } 36 | 37 | .mdedit::selection, .mdedit ::selection { 38 | background: #b3d4fc; 39 | } 40 | 41 | .mdedit .heading { font-weight: bold; } 42 | .mdedit .heading .heading-hash { font-weight: normal; } 43 | .mdedit .heading .heading-setext-line { font-weight: normal; } 44 | .mdedit .heading.heading-1 { font-size: 2em; } 45 | .mdedit .heading.heading-2 { font-size: 1.75em; } 46 | .mdedit .heading.heading-3 { font-size: 1.5em; } 47 | .mdedit .heading.heading-4 { font-size: 1.3em; } 48 | .mdedit .heading.heading-5 { font-size: 1.2em; } 49 | .mdedit .heading.heading-6 { font-size: 1.15em; } 50 | .mdedit .marker { color: rgba(120,120,120,0.5); } 51 | .mdedit .strong { font-weight: bold; } 52 | .mdedit .em { font-style: italic; } 53 | .mdedit .strike-inner { text-decoration: line-through } 54 | .mdedit .code { background: rgba(120,120,120,0.1); padding: 0.15em 0; } 55 | .mdedit .href, .mdedit .braced-href .braced-href-inner { color: rgba(120,120,120,0.8); text-decoration: underline; } 56 | .mdedit .blockquote-content, .mdedit .code-block.untagged .code-inner, .mdedit .code-block .code-language, .mdedit .code-block.indented { color: #555; } 57 | .mdedit .link-text-inner { text-decoration: underline; } 58 | -------------------------------------------------------------------------------- /mdedit.js: -------------------------------------------------------------------------------- 1 | 2 | (function(root, factory) { 3 | if (typeof define === 'function' && define.amd) { 4 | define(["prismjs"], factory); 5 | } else if (typeof exports === 'object') { 6 | module.exports = factory(require('prismjs')); 7 | } else { 8 | root.mdEdit = factory(root.Prism); 9 | } 10 | }(this, function(Prism) { 11 | 12 | var yaml = { 13 | 'scalar': { 14 | 'pattern': /([\-:]\s*(![^\s]+)?[ \t]*[|>])[ \t]*(?:(\n[ \t]+)[^\r\n]+(?:\3[^\r\n]+)*)/, 15 | 'lookbehind': true, 16 | 'alias': 'string' 17 | }, 18 | 'comment': /#[^\n]+/, 19 | 'key': { 20 | 'pattern': /(\s*[:\-,[{\n?][ \t]*(![^\s]+)?[ \t]*)[^\n{[\]},#]+?(?=\s*:\s)/, 21 | 'lookbehind': true, 22 | 'alias': 'atrule' 23 | }, 24 | 'directive': { 25 | 'pattern': /((^|\n)[ \t]*)%[^\n]+/, 26 | 'lookbehind': true, 27 | 'alias': 'important' 28 | }, 29 | 'datetime': { 30 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)(\d{4}-\d\d?-\d\d?([tT]|[ \t]+)\d\d?:\d{2}:\d{2}(\.\d*)?[ \t]*(Z|[-+]\d\d?(:\d{2})?)?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(:\d{2}(\.\d*)?)?)(?=[ \t]*(\n|$|,|]|}))/, 31 | 'lookbehind': true, 32 | 'alias': 'number' 33 | }, 34 | 'boolean': { 35 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)(true|false)[ \t]*(?=\n|$|,|]|})/i, 36 | 'lookbehind': true, 37 | 'alias': 'important' 38 | }, 39 | 'null': { 40 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)(null|~)[ \t]*(?=\n|$|,|]|})/i, 41 | 'lookbehind': true, 42 | 'alias': 'important' 43 | }, 44 | 'string': { 45 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')(?=[ \t]*(\n|$|,|]|}))/, 46 | 'lookbehind': true 47 | }, 48 | 'number': { 49 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)[+\-]?(0x[\dA-Fa-f]+|0o[0-7]+|(\d+\.?\d*|\.?\d+)(e[\+\-]?\d+)?|\.inf|\.nan)[ \t]*(?=\n|$|,|]|})/i, 50 | 'lookbehind': true 51 | }, 52 | 'tag': /![^\s]+/, 53 | 'important': /[&*][\w]+/, 54 | 'punctuation': /([:[\]{}\-,|>?]|---|\.\.\.)/ 55 | }; 56 | 57 | var md = (function(){ 58 | var md = { 59 | 'comment': Prism['languages']['markup']['comment'] 60 | }; 61 | 62 | 63 | md['front-matter'] = { 64 | 'pattern': /^---\n[\s\S]*?\n---(?=\n|$)/, 65 | 'inside': { 66 | 'marker front-matter-marker start': /^---/, 67 | 'marker front-matter-marker end': /---$/, 68 | 'rest': yaml 69 | } 70 | }; 71 | 72 | 73 | var inlines = {}; 74 | var blocks = {}; 75 | 76 | function inline(name, def){ 77 | blocks[name] = inlines[name] = md[name] = def; 78 | } 79 | function block(name, def){ 80 | blocks[name] = md[name] = def; 81 | } 82 | 83 | 84 | var langAliases = { 85 | 'markup': [ 'markup', 'html', 'xml' ], 86 | 'javascript': [ 'javascript', 'js' ] 87 | }; 88 | 89 | for(var i in Prism['languages']){ 90 | if(!Prism['languages'].hasOwnProperty(i)) continue; 91 | var l = Prism['languages'][i]; 92 | if(typeof l === 'function') continue; 93 | 94 | var aliases = langAliases[i]; 95 | var matches = aliases ? aliases.join('|') : i; 96 | 97 | block('code-block fenced ' + i, { 98 | 'pattern': new RegExp('(^ {0,3}|\\n {0,3})(([`~])\\3\\3) *(' + matches + ')( [^`\n]*)? *\\n(?:[\\s\\S]*?)\\n {0,3}(\\2\\3*(?= *\\n)|$)', 'gi'), 99 | 'lookbehind': true, 100 | 'inside': { 101 | 'code-language': { 102 | 'pattern': /(^([`~])\2+)((?!\2)[^\2\n])+/, 103 | 'lookbehind': true 104 | }, 105 | 'marker code-fence start': /^([`~])\1+/, 106 | 'marker code-fence end': /([`~])\1+$/, 107 | 'code-inner': { 108 | 'pattern': /(^\n)[\s\S]*(?=\n$)/, 109 | 'lookbehind': true, 110 | 'alias': 'language-' + i, 111 | 'inside': l 112 | } 113 | } 114 | }); 115 | } 116 | 117 | 118 | block('code-block fenced untagged', { 119 | 'pattern': /(^ {0,3}|\n {0,3})(([`~])\3\3)[^`\n]*\n(?:[\s\S]*?)\n {0,3}(\2\3*(?= *\n)|$)/g, 120 | 'lookbehind': true, 121 | 'inside': { 122 | 'code-language': { 123 | 'pattern': /(^([`~])\2+)((?!\2)[^\2\n])+/, 124 | 'lookbehind': true 125 | }, 126 | 'marker code-fence start': /^([`~])\1+/, 127 | 'marker code-fence end': /([`~])\1+$/, 128 | 'code-inner': { 129 | 'pattern': /(^\n)[\s\S]*(?=\n$)/, 130 | 'lookbehind': true 131 | } 132 | } 133 | }); 134 | 135 | 136 | block('heading setext-heading heading-1', { 137 | 'pattern': /^ {0,3}[^\s].*\n {0,3}=+[ \t]*$/gm, 138 | 'inside': { 139 | 'marker heading-setext-line': { 140 | 'pattern': /^( {0,3}[^\s].*\n) {0,3}=+[ \t]*$/gm, 141 | 'lookbehind': true 142 | }, 143 | 'rest': inlines 144 | } 145 | }); 146 | 147 | block('heading setext-heading heading-2', { 148 | 'pattern': /^ {0,3}[^\s].*\n {0,3}-+[ \t]*$/gm, 149 | 'inside': { 150 | 'marker heading-setext-line': { 151 | 'pattern': /^( {0,3}[^\s].*\n) {0,3}-+[ \t]*$/gm, 152 | 'lookbehind': true 153 | }, 154 | 'rest': inlines 155 | } 156 | }); 157 | 158 | var headingInside = { 159 | 'marker heading-hash start': /^ *#+ */, 160 | 'marker heading-hash end': / +#+ *$/, 161 | 'rest': inlines 162 | }; 163 | for(var i = 1; i <= 6; i += 1){ 164 | block('heading heading-'+i, { 165 | 'pattern': new RegExp('^ {0,3}#{'+i+'}(?![#\\S]).*$', 'gm'), 166 | 'inside': headingInside 167 | }); 168 | } 169 | 170 | 171 | 172 | var linkText = { 173 | 'pattern': /^\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 174 | 'inside': { 175 | 'marker bracket start': /^\[/, 176 | 'marker bracket end': /\]$/, 177 | 'link-text-inner': { 178 | 'pattern': /[\w\W]+/, 179 | 'inside': inlines 180 | } 181 | } 182 | }; 183 | 184 | var linkLabel = { 185 | 'pattern': /\[(?:\\.|[^\]])*\]/, 186 | 'inside': { 187 | 'marker bracket start': /^\[/, 188 | 'marker bracket end': /\]$/, 189 | 'link-label-inner': /[\w\W]+/ 190 | } 191 | }; 192 | 193 | var imageText = { 194 | 'pattern': /^!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 195 | 'inside': { 196 | 'marker image-bang': /^!/, 197 | 'marker bracket start': /^\[/, 198 | 'marker bracket end': /\]$/, 199 | 'image-text-inner': { 200 | 'pattern': /[\w\W]+/, 201 | 'inside': inlines 202 | } 203 | } 204 | }; 205 | 206 | var linkURL = { 207 | 'pattern': /^(\s*)(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))+/, 208 | 'lookbehind': true 209 | }; 210 | 211 | var linkBracedURL = { 212 | 'pattern': /^(\s*)<(?:\\.|[^<>\n])*>/, 213 | 'lookbehind': true, 214 | 'inside': { 215 | 'marker brace start': /^, 216 | 'marker brace end': />$/, 217 | 'braced-href-inner': /[\w\W]+/ 218 | } 219 | }; 220 | 221 | var linkTitle = { 222 | 'pattern': /('(?:\\'|[^'])+'|"(?:\\"|[^"])+")\s*$/, 223 | // 'lookbehind': true, 224 | 'inside': { 225 | 'marker quote start': /^['"]/, 226 | 'marker quote end': /['"]$/, 227 | 'title-inner': /[\w\W]+/ 228 | } 229 | }; 230 | 231 | var linkParams = { 232 | 'pattern': /\( *(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)( +('(?:[^']|\\')+'|"(?:[^"]|\\")+"))? *\)/, 233 | 'inside': { 234 | 'marker bracket start': /^\(/, 235 | 'marker bracket end': /\)$/, 236 | 'link-params-inner': { 237 | 'pattern': /[\w\W]+/, 238 | 'inside': { 239 | 'link-title': linkTitle, 240 | 'href': linkURL, 241 | 'braced-href': linkBracedURL 242 | } 243 | } 244 | } 245 | }; 246 | 247 | 248 | 249 | 250 | block('hr', { 251 | 'pattern': /^[\t ]*([*\-_])([\t ]*\1){2,}([\t ]*$)/gm, 252 | 'inside': { 253 | 'marker hr-marker': /[*\-_]/g 254 | } 255 | }); 256 | 257 | block('urldef', { 258 | 'pattern': /^( {0,3})\[(?:\\.|[^\]])+]: *\n? *(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)( *\n? *('(?:\\'|[^'])+'|"(?:\\"|[^"])+"))?$/gm, 259 | 'lookbehind': true, 260 | 'inside': { 261 | 'link-label': linkLabel, 262 | 'marker urldef-colon': /^:/, 263 | 'link-title': linkTitle, 264 | 'href': linkURL, 265 | 'braced-href': linkBracedURL 266 | } 267 | }); 268 | 269 | block('blockquote', { 270 | 'pattern': /^[\t ]*>[\t ]?.+(?:\n(?!\n)|.)*/gm, 271 | 'inside': { 272 | 'marker quote-marker': /^[\t ]*>[\t ]?/gm, 273 | 'blockquote-content': { 274 | 'pattern': /[^>]+/, 275 | 'inside': { 276 | 'rest': blocks 277 | } 278 | } 279 | } 280 | }); 281 | 282 | block('list', { 283 | 'pattern': /^[\t ]*([*+\-]|\d+\.)[\t ].+(?:\n(?!\n)|.)*/gm, 284 | 'inside': { 285 | 'li': { 286 | 'pattern': /^[\t ]*([*+\-]|\d+\.)[\t ].+(?:\n|[ \t]+[^*+\- \t].*\n)*/gm, 287 | 'inside': { 288 | 'marker list-item': /^[ \t]*([*+\-]|\d+\.)[ \t]/m, 289 | 'rest': blocks 290 | } 291 | } 292 | } 293 | }); 294 | 295 | block('code-block indented', { 296 | 'pattern': /(^|(?:^|(?:^|\n)(?![ \t]*([*+\-]|\d+\.)[ \t]).*\n)\s*?\n)((?: {4}|\t).*(?:\n|$))+/g, 297 | 'lookbehind': true 298 | }); 299 | 300 | block('p', { 301 | 'pattern': /[^\n](?:\n(?!\n)|.)*[^\n]/g, 302 | 'inside': inlines 303 | }); 304 | 305 | inline('image', { 306 | 'pattern': /(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]\(\s*(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)(\s+('(?:[^']|\\')+'|"(?:[^"]|\\")+"))?\s*\)/, 307 | 'lookbehind': true, 308 | 'inside': { 309 | 'link-text': imageText, 310 | 'link-params': linkParams 311 | } 312 | }); 313 | 314 | inline('link', { 315 | 'pattern': /(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]\(\s*(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)(\s+('(?:[^']|\\')+'|"(?:[^"]|\\")+"))?\s*\)/, 316 | 'lookbehind': true, 317 | 'inside': { 318 | 'link-text': linkText, 319 | 'link-params': linkParams 320 | } 321 | }); 322 | 323 | inline('image image-ref', { 324 | 'pattern': /(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\] ?\[(?:\\.|[^\]])*\]/, 325 | 'lookbehind': true, 326 | 'inside': { 327 | 'link-text': imageText, 328 | 'link-label': linkLabel 329 | } 330 | }); 331 | inline('link link-ref', { 332 | 'pattern': /(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\] ?\[(?:\\.|[^\]])*\]/, 333 | 'lookbehind': true, 334 | 'inside': { 335 | 'link-text': linkText, 336 | 'link-label': linkLabel 337 | } 338 | }); 339 | 340 | inline('image image-ref shortcut-ref', { 341 | 'pattern': /(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 342 | 'lookbehind': true, 343 | 'inside': { 344 | 'marker image-bang': /^!/, 345 | 'link-text': linkText 346 | } 347 | }); 348 | inline('link link-ref shortcut-ref', { 349 | 'pattern': /(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 350 | 'lookbehind': true, 351 | 'inside': { 352 | 'link-text': linkText 353 | } 354 | }); 355 | 356 | 357 | inline('code', { 358 | 'pattern': /(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/g, 359 | 'lookbehind': true, 360 | 'inside': { 361 | 'marker code-marker start': /^`/, 362 | 'marker code-marker end': /`$/, 363 | 'code-inner': /[\w\W]+/ 364 | } 365 | }); 366 | 367 | inline('strong', { 368 | 'pattern': /(^|[^\\*_]|\\[*_])([_\*])\2(?:\n(?!\n)|.)+?\2{2}(?!\2)/g, 369 | // 'pattern': /(^|[^\\])(\*\*|__)(?:\n(?!\n)|.)+?\2/, 370 | 'lookbehind': true, 371 | 'inside': { 372 | 'marker strong-marker start': /^(\*\*|__)/, 373 | 'marker strong-marker end': /(\*\*|__)$/, 374 | 'strong-inner': { 375 | 'pattern': /[\w\W]+/, 376 | 'inside': inlines 377 | } 378 | } 379 | }); 380 | 381 | inline('em', { 382 | // 'pattern': /(^|[^\\])(\*|_)(\S[^\2]*?)??[^\s\\]+?\2/g, 383 | 'pattern': /(^|[^\\*_]|\\[*_])(\*|_)(?:\n(?!\n)|.)+?\2(?!\2)/g, 384 | 'lookbehind': true, 385 | 'inside': { 386 | 'marker em-marker start': /^(\*|_)/, 387 | 'marker em-marker end': /(\*|_)$/, 388 | 'em-inner': { 389 | 'pattern': /[\w\W]+/, 390 | 'inside': inlines 391 | } 392 | } 393 | }); 394 | 395 | inline('strike', { 396 | 'pattern': /(^|\n|(?!\\)\W)(~~)(?=\S)([^\r]*?\S)\2/gm, 397 | 'lookbehind': true, 398 | 'inside': { 399 | 'marker strike-marker start': /^~~/, 400 | 'marker strike-marker end': /~~$/, 401 | 'strike-inner': { 402 | 'pattern': /[\w\W]+/, 403 | 'inside': inlines 404 | } 405 | } 406 | }); 407 | 408 | inline('comment', Prism['languages']['markup']['comment']); 409 | 410 | var tag = Prism['languages']['markup']['tag']; 411 | var tagMatch = tag['pattern']; 412 | 413 | inline('tag', { 414 | 'pattern': new RegExp("(^|[^\\\\])" + tagMatch.source, 'i'), 415 | 'lookbehind': true, 416 | 'inside': tag['inside'] 417 | }); 418 | inline('entity', Prism['languages']['markup']['entity']); 419 | 420 | return md; 421 | })(); 422 | 423 | var evt = { 424 | bind: function(el, evt, fn){ 425 | el.addEventListener(evt, fn, false); 426 | } 427 | }; 428 | 429 | 430 | function spliceString(str, i, remove, add){ 431 | remove = +remove || 0; 432 | add = add || ''; 433 | 434 | return str.slice(0,i) + add + str.slice(i+remove); 435 | } 436 | 437 | var actions = { 438 | 'newline': function(state, options){ 439 | var s = state.start; 440 | var lf = state.before.lastIndexOf('\n') + 1; 441 | var afterLf = state.before.slice(lf); 442 | var indent = afterLf.match(/^\s*/)[0]; 443 | var add = indent; 444 | var clearPrevLine = false; 445 | 446 | if(/^ {0,3}$/.test(indent)){ // maybe list 447 | var l = afterLf.slice(indent.length); 448 | if(/^[*+\-]\s+/.test(l)){ 449 | add += l.match(/^[*+\-]\s+/)[0]; 450 | clearPrevLine = /^[*+\-]\s+$/.test(l); 451 | }else if(/^\d+\.\s+/.test(l)){ 452 | add += l.match(/^\d+\.\s+/)[0] 453 | .replace(/^\d+/, function(n){ return +n+1; }); 454 | clearPrevLine = /^\d+\.\s+$/.test(l); 455 | }else if(/^>/.test(l)){ 456 | add += l.match(/^>\s*/)[0]; 457 | clearPrevLine = /^>\s*$/.test(l); 458 | } 459 | } 460 | 461 | add = '\n' + add; 462 | 463 | var del = state.sel; 464 | state.sel = ''; 465 | 466 | if(clearPrevLine){ // if prev line was actually an empty liste item, clear it 467 | del = afterLf + del; 468 | state.before = state.before.slice(0, lf); 469 | state.start -= afterLf.length; 470 | s -= afterLf.length; 471 | add = '\n'; 472 | } 473 | 474 | state.before += add; 475 | state.start += add.length; 476 | state.end = state.start; 477 | 478 | return { add: add, del: del, start: s }; 479 | }, 480 | 481 | 'indent': function(state, options){ 482 | var lf = state.before.lastIndexOf('\n') + 1; 483 | 484 | // TODO deal with soft tabs 485 | 486 | if(options.inverse){ 487 | if(/\s/.test(state.before.charAt(lf))){ 488 | state.before = spliceString(state.before, lf, 1); 489 | state.start -= 1; 490 | } 491 | state.sel = state.sel.replace(/\r?\n(?!\r?\n)\s/, '\n'); 492 | }else if(state.sel || options.ctrl){ 493 | state.before = spliceString(state.before, lf, 0, '\t'); 494 | state.sel = state.sel.replace(/\r?\n/, '\n\t'); 495 | state.start += 1; 496 | }else{ 497 | state.before += '\t'; 498 | state.start += 1; 499 | state.end += 1; 500 | 501 | return { add: '\t', del: '', start: state.start - 1 }; 502 | } 503 | 504 | state.end = state.start + state.sel.length; 505 | 506 | return { 507 | action: 'indent', 508 | start: state.start, 509 | end: state.end, 510 | inverse: options.inverse 511 | }; 512 | }, 513 | 514 | 'wrap': function(state, options){ 515 | var match = { 516 | '(': ')', 517 | '[': ']', 518 | '{': '}', 519 | '<': '>' 520 | }[options.bracket] || options.bracket; 521 | 522 | state.before += options.bracket; 523 | state.after = match + state.after; 524 | state.start += 1; 525 | state.end += 1; 526 | 527 | return { 528 | add: options.bracket + state.sel + match, 529 | del: state.sel, 530 | start: state.start - 1, 531 | end: state.end - 1 532 | }; 533 | } 534 | }; 535 | 536 | function SelectionManager(elt){ 537 | this.elt = elt; 538 | } 539 | 540 | SelectionManager.prototype.getStart = function(){ 541 | var selection = getSelection(); 542 | 543 | if(!selection.rangeCount) return 0; 544 | 545 | var range = selection.getRangeAt(0); 546 | var el = range.startContainer; 547 | var container = el; 548 | var offset = range.startOffset; 549 | 550 | if(!(this.elt.compareDocumentPosition(el) & 0x10)){ 551 | // selection is outside this element. 552 | return 0; 553 | } 554 | 555 | do{ 556 | while((el = el.previousSibling)){ 557 | if(el.textContent){ 558 | offset += el.textContent.length; 559 | } 560 | } 561 | 562 | el = container = container.parentNode; 563 | }while(el && el !== this.elt); 564 | 565 | return offset; 566 | }; 567 | 568 | SelectionManager.prototype.getEnd = function(){ 569 | var selection = getSelection(); 570 | 571 | if(!selection.rangeCount) return 0; 572 | 573 | return this.getStart() + String(selection.getRangeAt(0)).length; 574 | }; 575 | 576 | SelectionManager.prototype.setRange = function(start, end, noscroll){ 577 | var range = document.createRange(); 578 | var startOffset = findOffset(this.elt, start); 579 | var endOffset = startOffset; 580 | if(end && end !== start){ 581 | endOffset = findOffset(this.elt, end); 582 | }else{ 583 | if(noscroll !== false) scrollToCaret.call(this, endOffset.element, endOffset.offset); 584 | } 585 | 586 | range.setStart(startOffset.element, startOffset.offset); 587 | range.setEnd(endOffset.element, endOffset.offset); 588 | 589 | var selection = getSelection(); 590 | selection.removeAllRanges(); 591 | selection.addRange(range); 592 | }; 593 | 594 | 595 | 596 | var caret = document.createElement('span'); 597 | caret.style.position = 'absolute'; 598 | caret.innerHTML = '|'; 599 | 600 | function scrollToCaret(el, offset){ 601 | var t = el.textContent; 602 | var p = el.parentNode; 603 | var before = t.slice(0, offset); 604 | var after = t.slice(offset); 605 | 606 | el.textContent = after; 607 | var b4 = document.createTextNode(before); 608 | p.insertBefore(caret, el); 609 | p.insertBefore(b4, caret); 610 | 611 | // caret.scrollIntoViewIfNeeded(); 612 | var tp = caret.offsetTop; 613 | var h = caret.offsetHeight; 614 | var ch = this.elt.offsetHeight; 615 | var st = this.elt.scrollTop; 616 | 617 | el.textContent = t; 618 | p.removeChild(caret); 619 | p.removeChild(b4); 620 | 621 | if(tp - st < 0){ 622 | this.elt.scrollTop = tp; 623 | }else if(tp - st + h > ch){ 624 | this.elt.scrollTop = tp + h - ch; 625 | } 626 | } 627 | 628 | 629 | 630 | 631 | function findOffset(root, ss) { 632 | if(!root) { 633 | return null; 634 | } 635 | 636 | var offset = 0, 637 | element = root; 638 | 639 | do { 640 | var container = element; 641 | element = element.firstChild; 642 | 643 | if(element) { 644 | do { 645 | var len = element.textContent.length; 646 | 647 | if(offset <= ss && offset + len > ss) { 648 | break; 649 | } 650 | 651 | offset += len; 652 | } while(element = element.nextSibling); 653 | } 654 | 655 | if(!element) { 656 | // It's the container's lastChild 657 | break; 658 | } 659 | } while(element && element.hasChildNodes() && element.nodeType != 3); 660 | 661 | if(element) { 662 | return { 663 | element: element, 664 | offset: ss - offset 665 | }; 666 | } 667 | else if(container) { 668 | element = container; 669 | 670 | while(element && element.lastChild) { 671 | element = element.lastChild; 672 | } 673 | 674 | if(element.nodeType === 3) { 675 | return { 676 | element: element, 677 | offset: element.textContent.length 678 | }; 679 | } 680 | else { 681 | return { 682 | element: element, 683 | offset: 0 684 | }; 685 | } 686 | } 687 | 688 | return { 689 | element: root, 690 | offset: 0, 691 | error: true 692 | }; 693 | } 694 | 695 | function UndoManager(editor){ 696 | this.editor = editor; 697 | 698 | this.undoStack = []; 699 | this.redoStack = []; 700 | } 701 | 702 | UndoManager.prototype.action = function(a){ 703 | /// sanity? 704 | 705 | if(this.undoStack.length && this.canCombine(this.undoStack[this.undoStack.length-1], a)){ 706 | this.undoStack.push(this.combine(this.undoStack.pop(), a)); 707 | }else{ 708 | this.undoStack.push(a); 709 | } 710 | this.redoStack = []; 711 | }; 712 | 713 | UndoManager.prototype.canCombine = function(a, b){ 714 | return ( 715 | !a.action && !b.action && 716 | !Array.isArray(a) && !Array.isArray(b) && 717 | !(a.del && b.add) && !(a.add && b.del) && 718 | !(a.add && !b.add) && !(!a.add && b.add) && 719 | !(a.add && a.del) && 720 | !(b.add && b.del) && 721 | a.start + a.add.length === b.start + b.del.length 722 | ); 723 | }; 724 | 725 | UndoManager.prototype.combine = function(a, b){ 726 | return { 727 | add: a.add + b.add, 728 | del: b.del + a.del, 729 | start: Math.min(a.start, b.start) 730 | }; 731 | }; 732 | 733 | UndoManager.prototype.undo = function(){ 734 | if(!this.undoStack.length) return; 735 | 736 | var a = this.undoStack.pop(); 737 | this.redoStack.push(a); 738 | 739 | this.applyInverse(a); 740 | }; 741 | 742 | UndoManager.prototype.redo = function(){ 743 | if(!this.redoStack.length) return; 744 | 745 | var a = this.redoStack.pop(); 746 | this.undoStack.push(a); 747 | 748 | this.apply(a); 749 | }; 750 | 751 | UndoManager.prototype.apply = function apply(a){ 752 | if(Array.isArray(a)){ 753 | a.forEach(apply.bind(this)); 754 | return; 755 | } 756 | 757 | if(a.action){ 758 | this.editor.action(a.action, { 759 | inverse: a.inverse, 760 | start: a.start, 761 | end: a.end, 762 | noHistory: true 763 | }); 764 | }else{ 765 | this.editor.apply(a); 766 | } 767 | }; 768 | 769 | UndoManager.prototype.applyInverse = function inv(a){ 770 | if(Array.isArray(a)){ 771 | a.forEach(inv.bind(this)); 772 | return; 773 | } 774 | 775 | if(a.action){ 776 | this.editor.action(a.action, { 777 | inverse: !a.inverse, 778 | start: a.start, 779 | end: a.end, 780 | noHistory: true 781 | }); 782 | }else{ 783 | this.editor.apply({ 784 | start: a.start, 785 | end: a.end, 786 | del: a.add, 787 | add: a.del 788 | }); 789 | } 790 | }; 791 | 792 | function Editor(el, opts){ 793 | 794 | if(!(this instanceof Editor)){ 795 | return new Editor(el, opts); 796 | } 797 | 798 | opts = opts || {}; 799 | 800 | if(el.tagName === 'PRE'){ 801 | this.el = el; 802 | }else{ 803 | this.el = document.createElement('pre'); 804 | el.appendChild(this.el); 805 | } 806 | 807 | var cname = opts['className'] || ''; 808 | 809 | this.el.className = this.el.className ? this.el.className + ' ' : ''; 810 | this.el.className += 'mdedit' + (cname ? ' ' + cname : ''); 811 | this.el.setAttribute('contenteditable', true); 812 | 813 | var inner = this.inner = document.createElement('div'); 814 | inner.innerHTML = this.el.innerHTML; 815 | this.el.innerHTML = ''; 816 | this.el.appendChild(inner); 817 | 818 | this.selMgr = new SelectionManager(el); 819 | this.undoMgr = new UndoManager(this); 820 | 821 | evt.bind(el, 'cut', this.cut.bind(this)); 822 | evt.bind(el, 'paste', this.paste.bind(this)); 823 | evt.bind(el, 'keyup', this.keyup.bind(this)); 824 | evt.bind(el, 'input', this.changed.bind(this)); 825 | evt.bind(el, 'keydown', this.keydown.bind(this)); 826 | evt.bind(el, 'keypress', this.keypress.bind(this)); 827 | 828 | 829 | var changeCb = opts['change']; 830 | this.changeCb = changeCb || function(){}; 831 | 832 | this.changed(); 833 | } 834 | 835 | Editor.prototype.fireChange = function(){ 836 | var prev = this._prevValue; 837 | var now = this.getValue(); 838 | if(prev !== now){ 839 | this.changeCb(now); 840 | this._prevValue = now; 841 | } 842 | }; 843 | 844 | Editor.prototype['setValue'] = function(val){ 845 | this.setText(val); 846 | this.changed(); 847 | }; 848 | 849 | Editor.prototype['getValue'] = function(){ 850 | return this.getText(); 851 | }; 852 | 853 | Editor.prototype.getText = function(){ 854 | return this.inner.textContent; 855 | }; 856 | 857 | Editor.prototype.setText = function(val){ 858 | this.inner.textContent = val; 859 | }; 860 | 861 | Editor.prototype.keyup = function(evt){ 862 | var keyCode = evt && evt.keyCode || 0, 863 | code = this.getText(); 864 | 865 | if([ 866 | 9, 91, 93, 16, 17, 18, // modifiers 867 | 20, // caps lock 868 | 13, // Enter (handled by keydown) 869 | 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, // F[0-12] 870 | 27 // Esc 871 | ].indexOf(keyCode) > -1) { 872 | return; 873 | } 874 | 875 | if([ 876 | 33, 34, // PgUp, PgDn 877 | 35, 36, // End, Home 878 | 37, 39, 38, 40 // Left, Right, Up, Down 879 | ].indexOf(keyCode) === -1) { 880 | this.changed(); 881 | } 882 | }; 883 | 884 | Editor.prototype.changed = function(evt){ 885 | var code = this.getText(); 886 | 887 | var ss = this.selMgr.getStart(), 888 | se = this.selMgr.getEnd(); 889 | 890 | this.saveScrollPos(); 891 | 892 | var setHTML; 893 | 894 | if(code === this._prevCode){ 895 | if(this.inner.innerHTML !== this._prevHTML) setHTML = this._prevHTML; 896 | }else{ 897 | this._prevHTML = setHTML = Prism['highlight'](code, md); 898 | } 899 | this._prevCode = code; 900 | 901 | if(setHTML !== undefined){ 902 | if(!/\n$/.test(code)) { 903 | setHTML += '\n'; 904 | } 905 | 906 | var dummy = this.inner.cloneNode(false); 907 | dummy.innerHTML = setHTML; 908 | this.el.replaceChild(dummy, this.inner); 909 | this.inner = dummy; 910 | } 911 | 912 | this.restoreScrollPos(); 913 | 914 | if(ss !== null || se !== null) { 915 | this.selMgr.setRange(ss, se); 916 | } 917 | 918 | this.fireChange(); 919 | }; 920 | 921 | Editor.prototype.saveScrollPos = function(){ 922 | if(this.st === undefined) this.st = this.el.scrollTop; 923 | setTimeout(function(){ 924 | this.st = undefined; 925 | }.bind(this), 500); 926 | }; 927 | 928 | Editor.prototype.restoreScrollPos = function(){ 929 | this.el.scrollTop = this.st; 930 | this.st = undefined; 931 | }; 932 | 933 | 934 | Editor.prototype.keypress = function(evt){ 935 | var ctrl = evt.metaKey || evt.ctrlKey; 936 | 937 | if(ctrl) return; 938 | 939 | var code = evt.charCode; 940 | 941 | if(!code) return; 942 | 943 | var start = this.selMgr.getStart(); 944 | var end = this.selMgr.getEnd(); 945 | 946 | var chr = String.fromCharCode(code); 947 | 948 | if(/[\[\{\(<"'~\*_]/.test(chr) && start !== end){ 949 | this.action('wrap', { 950 | bracket: chr 951 | }); 952 | evt.preventDefault(); 953 | return; 954 | } 955 | 956 | 957 | this.undoMgr.action({ 958 | add: chr, 959 | del: start === end ? '' : this.getText().slice(start, end), 960 | start: start 961 | }); 962 | }; 963 | 964 | Editor.prototype.keydown = function(evt){ 965 | var cmdOrCtrl = evt.metaKey || evt.ctrlKey; 966 | 967 | switch(evt.keyCode) { 968 | case 8: // Backspace 969 | case 46: // Delete 970 | var start = this.selMgr.getStart(); 971 | var end = this.selMgr.getEnd(); 972 | var length = start === end ? 1 : Math.abs(end - start); 973 | start = evt.keyCode === 8 ? end - length : start; 974 | this.undoMgr.action({ 975 | add: '', 976 | del: this.getText().slice(start, start + length), 977 | start: start 978 | }); 979 | break; 980 | case 9: // Tab 981 | if(!cmdOrCtrl) { 982 | this.action('indent', { 983 | inverse: evt.shiftKey 984 | }); 985 | evt.preventDefault(); 986 | } 987 | break; 988 | case 219: // [ 989 | case 221: // ] 990 | if(cmdOrCtrl && !evt.shiftKey) { 991 | this.action('indent', { 992 | inverse: evt.keyCode === 219, 993 | ctrl: true 994 | }); 995 | evt.preventDefault(); 996 | } 997 | break; 998 | case 13: 999 | this.action('newline'); 1000 | evt.preventDefault(); 1001 | break; 1002 | case 89: 1003 | if(cmdOrCtrl){ 1004 | this.undoMgr.redo(); 1005 | evt.preventDefault(); 1006 | } 1007 | break; 1008 | case 90: 1009 | if(cmdOrCtrl) { 1010 | evt.shiftKey ? this.undoMgr.redo() : this.undoMgr.undo(); 1011 | evt.preventDefault(); 1012 | } 1013 | 1014 | break; 1015 | } 1016 | }; 1017 | 1018 | Editor.prototype.apply = function(action){ 1019 | this.setText(spliceString(this.getText(), action.start, action.del.length, action.add)); 1020 | this.selMgr.setRange(action.start, action.start + action.add.length); 1021 | this.changed(); 1022 | }; 1023 | 1024 | Editor.prototype.action = function(act, opts){ 1025 | opts = opts || {}; 1026 | var text = this.getText(); 1027 | var start = opts.start || this.selMgr.getStart(); 1028 | var end = opts.end || this.selMgr.getEnd(); 1029 | 1030 | var state = { 1031 | start: start, 1032 | end: end, 1033 | before: text.slice(0, start), 1034 | after: text.slice(end), 1035 | sel: text.slice(start, end) 1036 | }; 1037 | 1038 | var a = actions[act](state, opts); 1039 | 1040 | this.saveScrollPos(); 1041 | 1042 | this.setText(state.before + state.sel + state.after); 1043 | 1044 | if(a && !opts.noHistory){ 1045 | this.undoMgr.action(a); 1046 | } 1047 | this.selMgr.setRange(state.start, state.end, false); 1048 | 1049 | this.changed(); 1050 | 1051 | }; 1052 | 1053 | Editor.prototype.cut = function(){ 1054 | var start = this.selMgr.getStart(); 1055 | var end = this.selMgr.getEnd(); 1056 | if(start === end) return; 1057 | 1058 | this.undoMgr.action({ 1059 | add: '', 1060 | del: this.getText().slice(start, end), 1061 | start: start 1062 | }); 1063 | }; 1064 | 1065 | Editor.prototype.paste = function(evt){ 1066 | var start = this.selMgr.getStart(); 1067 | var end = this.selMgr.getEnd(); 1068 | var selection = start === end ? '' : this.getText().slice(start, end); 1069 | 1070 | var self = this; 1071 | 1072 | function applyPasted(pasted){ 1073 | self.undoMgr.action({ 1074 | add: pasted, 1075 | del: selection, 1076 | start: start 1077 | }); 1078 | 1079 | start += pasted.length; 1080 | self.selMgr.setRange(start, start); 1081 | self.changed(); 1082 | } 1083 | 1084 | if(evt.clipboardData){ 1085 | evt.preventDefault(); 1086 | 1087 | var pasted = evt.clipboardData.getData('text/plain'); 1088 | 1089 | this.apply({ 1090 | add: pasted, 1091 | del: selection, 1092 | start: start 1093 | }); 1094 | 1095 | applyPasted(pasted); 1096 | }else{ 1097 | // handle IE9 with no clipboardData. Flickers a bit if styles have changed :( 1098 | setTimeout(function(){ 1099 | var newEnd = self.selMgr.getEnd(); 1100 | 1101 | applyPasted(self.getText().slice(start, newEnd)); 1102 | }, 0); 1103 | } 1104 | }; 1105 | 1106 | return Editor; 1107 | 1108 | })); 1109 | -------------------------------------------------------------------------------- /mdedit.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"function"==typeof define&&define.amd?define(["prismjs"],t):"object"==typeof exports?module.exports=t(require("prismjs")):e.mdEdit=t(e.Prism)}(this,function(e){function t(e,t,n,r){return n=+n||0,r=r||"",e.slice(0,t)+r+e.slice(t+n)}function n(e){this.elt=e}function r(e,t){var n=e.textContent,r=e.parentNode,i=n.slice(0,t),a=n.slice(t);e.textContent=a;var s=document.createTextNode(i);r.insertBefore(c,e),r.insertBefore(s,c);var o=c.offsetTop,d=c.offsetHeight,l=this.elt.offsetHeight,h=this.elt.scrollTop;e.textContent=n,r.removeChild(c),r.removeChild(s),0>o-h?this.elt.scrollTop=o:o-h+d>l&&(this.elt.scrollTop=o+d-l)}function i(e,t){if(!e)return null;var n=0,r=e;do{var i=r;if(r=r.firstChild)do{var a=r.textContent.length;if(t>=n&&n+a>t)break;n+=a}while(r=r.nextSibling);if(!r)break}while(r&&r.hasChildNodes()&&3!=r.nodeType);if(r)return{element:r,offset:t-n};if(i){for(r=i;r&&r.lastChild;)r=r.lastChild;return 3===r.nodeType?{element:r,offset:r.textContent.length}:{element:r,offset:0}}return{element:e,offset:0,error:!0}}function a(e){this.editor=e,this.undoStack=[],this.redoStack=[]}function s(e,t){if(!(this instanceof s))return new s(e,t);t=t||{},"PRE"===e.tagName?this.el=e:(this.el=document.createElement("pre"),e.appendChild(this.el));var r=t.className||"";this.el.className=this.el.className?this.el.className+" ":"",this.el.className+="mdedit"+(r?" "+r:""),this.el.setAttribute("contenteditable",!0);var i=this.inner=document.createElement("div");i.innerHTML=this.el.innerHTML,this.el.innerHTML="",this.el.appendChild(i),this.selMgr=new n(e),this.undoMgr=new a(this),l.bind(e,"cut",this.cut.bind(this)),l.bind(e,"paste",this.paste.bind(this)),l.bind(e,"keyup",this.keyup.bind(this)),l.bind(e,"input",this.changed.bind(this)),l.bind(e,"keydown",this.keydown.bind(this)),l.bind(e,"keypress",this.keypress.bind(this));var o=t.change;this.changeCb=o||function(){},this.changed()}var o={scalar:{pattern:/([\-:]\s*(![^\s]+)?[ \t]*[|>])[ \t]*(?:(\n[ \t]+)[^\r\n]+(?:\3[^\r\n]+)*)/,lookbehind:!0,alias:"string"},comment:/#[^\n]+/,key:{pattern:/(\s*[:\-,[{\n?][ \t]*(![^\s]+)?[ \t]*)[^\n{[\]},#]+?(?=\s*:\s)/,lookbehind:!0,alias:"atrule"},directive:{pattern:/((^|\n)[ \t]*)%[^\n]+/,lookbehind:!0,alias:"important"},datetime:{pattern:/([:\-,[{]\s*(![^\s]+)?[ \t]*)(\d{4}-\d\d?-\d\d?([tT]|[ \t]+)\d\d?:\d{2}:\d{2}(\.\d*)?[ \t]*(Z|[-+]\d\d?(:\d{2})?)?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(:\d{2}(\.\d*)?)?)(?=[ \t]*(\n|$|,|]|}))/,lookbehind:!0,alias:"number"},"boolean":{pattern:/([:\-,[{]\s*(![^\s]+)?[ \t]*)(true|false)[ \t]*(?=\n|$|,|]|})/i,lookbehind:!0,alias:"important"},"null":{pattern:/([:\-,[{]\s*(![^\s]+)?[ \t]*)(null|~)[ \t]*(?=\n|$|,|]|})/i,lookbehind:!0,alias:"important"},string:{pattern:/([:\-,[{]\s*(![^\s]+)?[ \t]*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')(?=[ \t]*(\n|$|,|]|}))/,lookbehind:!0},number:{pattern:/([:\-,[{]\s*(![^\s]+)?[ \t]*)[+\-]?(0x[\dA-Fa-f]+|0o[0-7]+|(\d+\.?\d*|\.?\d+)(e[\+\-]?\d+)?|\.inf|\.nan)[ \t]*(?=\n|$|,|]|})/i,lookbehind:!0},tag:/![^\s]+/,important:/[&*][\w]+/,punctuation:/([:[\]{}\-,|>?]|---|\.\.\.)/},d=function(){function t(e,t){a[e]=i[e]=r[e]=t}function n(e,t){a[e]=r[e]=t}var r={comment:e.languages.markup.comment};r["front-matter"]={pattern:/^---\n[\s\S]*?\n---(?=\n|$)/,inside:{"marker front-matter-marker start":/^---/,"marker front-matter-marker end":/---$/,rest:o}};var i={},a={},s={markup:["markup","html","xml"],javascript:["javascript","js"]};for(var d in e.languages)if(e.languages.hasOwnProperty(d)){var l=e.languages[d];if("function"!=typeof l){var h=s[d],c=h?h.join("|"):d;n("code-block fenced "+d,{pattern:new RegExp("(^ {0,3}|\\n {0,3})(([`~])\\3\\3) *("+c+")( [^`\n]*)? *\\n(?:[\\s\\S]*?)\\n {0,3}(\\2\\3*(?= *\\n)|$)","gi"),lookbehind:!0,inside:{"code-language":{pattern:/(^([`~])\2+)((?!\2)[^\2\n])+/,lookbehind:!0},"marker code-fence start":/^([`~])\1+/,"marker code-fence end":/([`~])\1+$/,"code-inner":{pattern:/(^\n)[\s\S]*(?=\n$)/,lookbehind:!0,alias:"language-"+d,inside:l}}})}}n("code-block fenced untagged",{pattern:/(^ {0,3}|\n {0,3})(([`~])\3\3)[^`\n]*\n(?:[\s\S]*?)\n {0,3}(\2\3*(?= *\n)|$)/g,lookbehind:!0,inside:{"code-language":{pattern:/(^([`~])\2+)((?!\2)[^\2\n])+/,lookbehind:!0},"marker code-fence start":/^([`~])\1+/,"marker code-fence end":/([`~])\1+$/,"code-inner":{pattern:/(^\n)[\s\S]*(?=\n$)/,lookbehind:!0}}}),n("heading setext-heading heading-1",{pattern:/^ {0,3}[^\s].*\n {0,3}=+[ \t]*$/gm,inside:{"marker heading-setext-line":{pattern:/^( {0,3}[^\s].*\n) {0,3}=+[ \t]*$/gm,lookbehind:!0},rest:i}}),n("heading setext-heading heading-2",{pattern:/^ {0,3}[^\s].*\n {0,3}-+[ \t]*$/gm,inside:{"marker heading-setext-line":{pattern:/^( {0,3}[^\s].*\n) {0,3}-+[ \t]*$/gm,lookbehind:!0},rest:i}});for(var p={"marker heading-hash start":/^ *#+ */,"marker heading-hash end":/ +#+ *$/,rest:i},d=1;6>=d;d+=1)n("heading heading-"+d,{pattern:new RegExp("^ {0,3}#{"+d+"}(?![#\\S]).*$","gm"),inside:p});var g={pattern:/^\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/,inside:{"marker bracket start":/^\[/,"marker bracket end":/\]$/,"link-text-inner":{pattern:/[\w\W]+/,inside:i}}},u={pattern:/\[(?:\\.|[^\]])*\]/,inside:{"marker bracket start":/^\[/,"marker bracket end":/\]$/,"link-label-inner":/[\w\W]+/}},f={pattern:/^!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/,inside:{"marker image-bang":/^!/,"marker bracket start":/^\[/,"marker bracket end":/\]$/,"image-text-inner":{pattern:/[\w\W]+/,inside:i}}},k={pattern:/^(\s*)(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))+/,lookbehind:!0},m={pattern:/^(\s*)<(?:\\.|[^<>\n])*>/,lookbehind:!0,inside:{"marker brace start":/^,"marker brace end":/>$/,"braced-href-inner":/[\w\W]+/}},b={pattern:/('(?:\\'|[^'])+'|"(?:\\"|[^"])+")\s*$/,inside:{"marker quote start":/^['"]/,"marker quote end":/['"]$/,"title-inner":/[\w\W]+/}},v={pattern:/\( *(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)( +('(?:[^']|\\')+'|"(?:[^"]|\\")+"))? *\)/,inside:{"marker bracket start":/^\(/,"marker bracket end":/\)$/,"link-params-inner":{pattern:/[\w\W]+/,inside:{"link-title":b,href:k,"braced-href":m}}}};n("hr",{pattern:/^[\t ]*([*\-_])([\t ]*\1){2,}([\t ]*$)/gm,inside:{"marker hr-marker":/[*\-_]/g}}),n("urldef",{pattern:/^( {0,3})\[(?:\\.|[^\]])+]: *\n? *(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)( *\n? *('(?:\\'|[^'])+'|"(?:\\"|[^"])+"))?$/gm,lookbehind:!0,inside:{"link-label":u,"marker urldef-colon":/^:/,"link-title":b,href:k,"braced-href":m}}),n("blockquote",{pattern:/^[\t ]*>[\t ]?.+(?:\n(?!\n)|.)*/gm,inside:{"marker quote-marker":/^[\t ]*>[\t ]?/gm,"blockquote-content":{pattern:/[^>]+/,inside:{rest:a}}}}),n("list",{pattern:/^[\t ]*([*+\-]|\d+\.)[\t ].+(?:\n(?!\n)|.)*/gm,inside:{li:{pattern:/^[\t ]*([*+\-]|\d+\.)[\t ].+(?:\n|[ \t]+[^*+\- \t].*\n)*/gm,inside:{"marker list-item":/^[ \t]*([*+\-]|\d+\.)[ \t]/m,rest:a}}}}),n("code-block indented",{pattern:/(^|(?:^|(?:^|\n)(?![ \t]*([*+\-]|\d+\.)[ \t]).*\n)\s*?\n)((?: {4}|\t).*(?:\n|$))+/g,lookbehind:!0}),n("p",{pattern:/[^\n](?:\n(?!\n)|.)*[^\n]/g,inside:i}),t("image",{pattern:/(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]\(\s*(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)(\s+('(?:[^']|\\')+'|"(?:[^"]|\\")+"))?\s*\)/,lookbehind:!0,inside:{"link-text":f,"link-params":v}}),t("link",{pattern:/(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]\(\s*(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)(\s+('(?:[^']|\\')+'|"(?:[^"]|\\")+"))?\s*\)/,lookbehind:!0,inside:{"link-text":g,"link-params":v}}),t("image image-ref",{pattern:/(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\] ?\[(?:\\.|[^\]])*\]/,lookbehind:!0,inside:{"link-text":f,"link-label":u}}),t("link link-ref",{pattern:/(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\] ?\[(?:\\.|[^\]])*\]/,lookbehind:!0,inside:{"link-text":g,"link-label":u}}),t("image image-ref shortcut-ref",{pattern:/(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/,lookbehind:!0,inside:{"marker image-bang":/^!/,"link-text":g}}),t("link link-ref shortcut-ref",{pattern:/(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/,lookbehind:!0,inside:{"link-text":g}}),t("code",{pattern:/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/g,lookbehind:!0,inside:{"marker code-marker start":/^`/,"marker code-marker end":/`$/,"code-inner":/[\w\W]+/}}),t("strong",{pattern:/(^|[^\\*_]|\\[*_])([_\*])\2(?:\n(?!\n)|.)+?\2{2}(?!\2)/g,lookbehind:!0,inside:{"marker strong-marker start":/^(\*\*|__)/,"marker strong-marker end":/(\*\*|__)$/,"strong-inner":{pattern:/[\w\W]+/,inside:i}}}),t("em",{pattern:/(^|[^\\*_]|\\[*_])(\*|_)(?:\n(?!\n)|.)+?\2(?!\2)/g,lookbehind:!0,inside:{"marker em-marker start":/^(\*|_)/,"marker em-marker end":/(\*|_)$/,"em-inner":{pattern:/[\w\W]+/,inside:i}}}),t("strike",{pattern:/(^|\n|(?!\\)\W)(~~)(?=\S)([^\r]*?\S)\2/gm,lookbehind:!0,inside:{"marker strike-marker start":/^~~/,"marker strike-marker end":/~~$/,"strike-inner":{pattern:/[\w\W]+/,inside:i}}}),t("comment",e.languages.markup.comment);var y=e.languages.markup.tag,x=y.pattern;return t("tag",{pattern:new RegExp("(^|[^\\\\])"+x.source,"i"),lookbehind:!0,inside:y.inside}),t("entity",e.languages.markup.entity),r}(),l={bind:function(e,t,n){e.addEventListener(t,n,!1)}},h={newline:function(e,t){var n=e.start,r=e.before.lastIndexOf("\n")+1,i=e.before.slice(r),a=i.match(/^\s*/)[0],s=a,o=!1;if(/^ {0,3}$/.test(a)){var d=i.slice(a.length);/^[*+\-]\s+/.test(d)?(s+=d.match(/^[*+\-]\s+/)[0],o=/^[*+\-]\s+$/.test(d)):/^\d+\.\s+/.test(d)?(s+=d.match(/^\d+\.\s+/)[0].replace(/^\d+/,function(e){return+e+1}),o=/^\d+\.\s+$/.test(d)):/^>/.test(d)&&(s+=d.match(/^>\s*/)[0],o=/^>\s*$/.test(d))}s="\n"+s;var l=e.sel;return e.sel="",o&&(l=i+l,e.before=e.before.slice(0,r),e.start-=i.length,n-=i.length,s="\n"),e.before+=s,e.start+=s.length,e.end=e.start,{add:s,del:l,start:n}},indent:function(e,n){var r=e.before.lastIndexOf("\n")+1;if(n.inverse)/\s/.test(e.before.charAt(r))&&(e.before=t(e.before,r,1),e.start-=1),e.sel=e.sel.replace(/\r?\n(?!\r?\n)\s/,"\n");else{if(!e.sel&&!n.ctrl)return e.before+=" ",e.start+=1,e.end+=1,{add:" ",del:"",start:e.start-1};e.before=t(e.before,r,0," "),e.sel=e.sel.replace(/\r?\n/,"\n "),e.start+=1}return e.end=e.start+e.sel.length,{action:"indent",start:e.start,end:e.end,inverse:n.inverse}},wrap:function(e,t){var n={"(":")","[":"]","{":"}","<":">"}[t.bracket]||t.bracket;return e.before+=t.bracket,e.after=n+e.after,e.start+=1,e.end+=1,{add:t.bracket+e.sel+n,del:e.sel,start:e.start-1,end:e.end-1}}};n.prototype.getStart=function(){var e=getSelection();if(!e.rangeCount)return 0;var t=e.getRangeAt(0),n=t.startContainer,r=n,i=t.startOffset;if(!(16&this.elt.compareDocumentPosition(n)))return 0;do{for(;n=n.previousSibling;)n.textContent&&(i+=n.textContent.length);n=r=r.parentNode}while(n&&n!==this.elt);return i},n.prototype.getEnd=function(){var e=getSelection();return e.rangeCount?this.getStart()+String(e.getRangeAt(0)).length:0},n.prototype.setRange=function(e,t,n){var a=document.createRange(),s=i(this.elt,e),o=s;t&&t!==e?o=i(this.elt,t):n!==!1&&r.call(this,o.element,o.offset),a.setStart(s.element,s.offset),a.setEnd(o.element,o.offset);var d=getSelection();d.removeAllRanges(),d.addRange(a)};var c=document.createElement("span");return c.style.position="absolute",c.innerHTML="|",a.prototype.action=function(e){this.undoStack.length&&this.canCombine(this.undoStack[this.undoStack.length-1],e)?this.undoStack.push(this.combine(this.undoStack.pop(),e)):this.undoStack.push(e),this.redoStack=[]},a.prototype.canCombine=function(e,t){return!(e.action||t.action||Array.isArray(e)||Array.isArray(t)||e.del&&t.add||e.add&&t.del||e.add&&!t.add||!e.add&&t.add||e.add&&e.del||t.add&&t.del||e.start+e.add.length!==t.start+t.del.length)},a.prototype.combine=function(e,t){return{add:e.add+t.add,del:t.del+e.del,start:Math.min(e.start,t.start)}},a.prototype.undo=function(){if(this.undoStack.length){var e=this.undoStack.pop();this.redoStack.push(e),this.applyInverse(e)}},a.prototype.redo=function(){if(this.redoStack.length){var e=this.redoStack.pop();this.undoStack.push(e),this.apply(e)}},a.prototype.apply=function p(e){return Array.isArray(e)?void e.forEach(p.bind(this)):void(e.action?this.editor.action(e.action,{inverse:e.inverse,start:e.start,end:e.end,noHistory:!0}):this.editor.apply(e))},a.prototype.applyInverse=function g(e){return Array.isArray(e)?void e.forEach(g.bind(this)):void(e.action?this.editor.action(e.action,{inverse:!e.inverse,start:e.start,end:e.end,noHistory:!0}):this.editor.apply({start:e.start,end:e.end,del:e.add,add:e.del}))},s.prototype.fireChange=function(){var e=this._prevValue,t=this.getValue();e!==t&&(this.changeCb(t),this._prevValue=t)},s.prototype.setValue=function(e){this.setText(e),this.changed()},s.prototype.getValue=function(){return this.getText()},s.prototype.getText=function(){return this.inner.textContent},s.prototype.setText=function(e){this.inner.textContent=e},s.prototype.keyup=function(e){var t=e&&e.keyCode||0;this.getText();[9,91,93,16,17,18,20,13,112,113,114,115,116,117,118,119,120,121,122,123,27].indexOf(t)>-1||-1===[33,34,35,36,37,39,38,40].indexOf(t)&&this.changed()},s.prototype.changed=function(t){var n=this.getText(),r=this.selMgr.getStart(),i=this.selMgr.getEnd();this.saveScrollPos();var a;if(n===this._prevCode?this.inner.innerHTML!==this._prevHTML&&(a=this._prevHTML):this._prevHTML=a=e.highlight(n,d),this._prevCode=n,void 0!==a){/\n$/.test(n)||(a+="\n");var s=this.inner.cloneNode(!1);s.innerHTML=a,this.el.replaceChild(s,this.inner),this.inner=s}this.restoreScrollPos(),(null!==r||null!==i)&&this.selMgr.setRange(r,i),this.fireChange()},s.prototype.saveScrollPos=function(){void 0===this.st&&(this.st=this.el.scrollTop),setTimeout(function(){this.st=void 0}.bind(this),500)},s.prototype.restoreScrollPos=function(){this.el.scrollTop=this.st,this.st=void 0},s.prototype.keypress=function(e){var t=e.metaKey||e.ctrlKey;if(!t){var n=e.charCode;if(n){var r=this.selMgr.getStart(),i=this.selMgr.getEnd(),a=String.fromCharCode(n);return/[\[\{\(<"'~\*_]/.test(a)&&r!==i?(this.action("wrap",{bracket:a}),void e.preventDefault()):void this.undoMgr.action({add:a,del:r===i?"":this.getText().slice(r,i),start:r})}}},s.prototype.keydown=function(e){var t=e.metaKey||e.ctrlKey;switch(e.keyCode){case 8:case 46:var n=this.selMgr.getStart(),r=this.selMgr.getEnd(),i=n===r?1:Math.abs(r-n);n=8===e.keyCode?r-i:n,this.undoMgr.action({add:"",del:this.getText().slice(n,n+i),start:n});break;case 9:t||(this.action("indent",{inverse:e.shiftKey}),e.preventDefault());break;case 219:case 221:t&&!e.shiftKey&&(this.action("indent",{inverse:219===e.keyCode,ctrl:!0}),e.preventDefault());break;case 13:this.action("newline"),e.preventDefault();break;case 89:t&&(this.undoMgr.redo(),e.preventDefault());break;case 90:t&&(e.shiftKey?this.undoMgr.redo():this.undoMgr.undo(),e.preventDefault())}},s.prototype.apply=function(e){this.setText(t(this.getText(),e.start,e.del.length,e.add)),this.selMgr.setRange(e.start,e.start+e.add.length),this.changed()},s.prototype.action=function(e,t){t=t||{};var n=this.getText(),r=t.start||this.selMgr.getStart(),i=t.end||this.selMgr.getEnd(),a={start:r,end:i,before:n.slice(0,r),after:n.slice(i),sel:n.slice(r,i)},s=h[e](a,t);this.saveScrollPos(),this.setText(a.before+a.sel+a.after),s&&!t.noHistory&&this.undoMgr.action(s),this.selMgr.setRange(a.start,a.end,!1),this.changed()},s.prototype.cut=function(){var e=this.selMgr.getStart(),t=this.selMgr.getEnd();e!==t&&this.undoMgr.action({add:"",del:this.getText().slice(e,t),start:e})},s.prototype.paste=function(e){function t(e){a.undoMgr.action({add:e,del:i,start:n}),n+=e.length,a.selMgr.setRange(n,n),a.changed()}var n=this.selMgr.getStart(),r=this.selMgr.getEnd(),i=n===r?"":this.getText().slice(n,r),a=this;if(e.clipboardData){e.preventDefault();var s=e.clipboardData.getData("text/plain");this.apply({add:s,del:i,start:n}),t(s)}else setTimeout(function(){var e=a.selMgr.getEnd();t(a.getText().slice(n,e))},0)},s}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdEdit", 3 | "version": "0.1.0", 4 | "description": "nice syntax-highlighted markdown editor view component", 5 | "main": "mdedit.js", 6 | "devDependencies": { 7 | "del": "^2.0.2", 8 | "gulp": "^3.9.0", 9 | "gulp-concat": "^2.6.0", 10 | "gulp-rename": "^1.2.2", 11 | "gulp-uglify": "^1.4.1", 12 | "gulp-wrap-umd": "^0.2.1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/jbt/mdEdit" 17 | }, 18 | "keywords": [ 19 | "markdown", 20 | "editor", 21 | "highlight" 22 | ], 23 | "author": "James Taylor", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/jbt/mdEdit/issues" 27 | }, 28 | "scripts": { 29 | "build": "gulp build", 30 | "clean": "gulp clean" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+markdown */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | text-shadow: 0 1px white; 12 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 13 | direction: ltr; 14 | text-align: left; 15 | white-space: pre; 16 | white-space: pre-wrap; 17 | word-spacing: normal; 18 | word-break: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 33 | text-shadow: none; 34 | background: #b3d4fc; 35 | } 36 | 37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 38 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 39 | text-shadow: none; 40 | background: #b3d4fc; 41 | } 42 | 43 | @media print { 44 | code[class*="language-"], 45 | pre[class*="language-"] { 46 | text-shadow: none; 47 | } 48 | } 49 | 50 | /* Code blocks */ 51 | pre[class*="language-"] { 52 | padding: 1em; 53 | margin: .5em 0; 54 | overflow: auto; 55 | } 56 | 57 | :not(pre) > code[class*="language-"], 58 | pre[class*="language-"] { 59 | background: #f5f2f0; 60 | } 61 | 62 | /* Inline code */ 63 | :not(pre) > code[class*="language-"] { 64 | padding: .1em; 65 | border-radius: .3em; 66 | } 67 | 68 | .token.comment, 69 | .token.prolog, 70 | .token.doctype, 71 | .token.cdata { 72 | color: slategray; 73 | } 74 | 75 | .token.punctuation { 76 | color: #999; 77 | } 78 | 79 | .namespace { 80 | opacity: .7; 81 | } 82 | 83 | .token.property, 84 | .token.tag, 85 | .token.boolean, 86 | .token.number, 87 | .token.constant, 88 | .token.symbol, 89 | .token.deleted { 90 | color: #905; 91 | } 92 | 93 | .token.selector, 94 | .token.attr-name, 95 | .token.string, 96 | .token.char, 97 | .token.builtin, 98 | .token.inserted { 99 | color: #690; 100 | } 101 | 102 | .token.operator, 103 | .token.entity, 104 | .token.url, 105 | .language-css .token.string, 106 | .style .token.string { 107 | color: #a67f59; 108 | background: hsla(0, 0%, 100%, .5); 109 | } 110 | 111 | .token.atrule, 112 | .token.attr-value, 113 | .token.keyword { 114 | color: #07a; 115 | } 116 | 117 | .token.function { 118 | color: #DD4A68; 119 | } 120 | 121 | .token.regex, 122 | .token.important, 123 | .token.variable { 124 | color: #e90; 125 | } 126 | 127 | .token.important, 128 | .token.bold { 129 | font-weight: bold; 130 | } 131 | .token.italic { 132 | font-style: italic; 133 | } 134 | 135 | .token.entity { 136 | cursor: help; 137 | } 138 | -------------------------------------------------------------------------------- /src/Editor.js: -------------------------------------------------------------------------------- 1 | function Editor(el, opts){ 2 | 3 | if(!(this instanceof Editor)){ 4 | return new Editor(el, opts); 5 | } 6 | 7 | opts = opts || {}; 8 | 9 | if(el.tagName === 'PRE'){ 10 | this.el = el; 11 | }else{ 12 | this.el = document.createElement('pre'); 13 | el.appendChild(this.el); 14 | } 15 | 16 | var cname = opts['className'] || ''; 17 | 18 | this.el.className = this.el.className ? this.el.className + ' ' : ''; 19 | this.el.className += 'mdedit' + (cname ? ' ' + cname : ''); 20 | this.el.setAttribute('contenteditable', true); 21 | 22 | var inner = this.inner = document.createElement('div'); 23 | inner.innerHTML = this.el.innerHTML; 24 | this.el.innerHTML = ''; 25 | this.el.appendChild(inner); 26 | 27 | this.selMgr = new SelectionManager(el); 28 | this.undoMgr = new UndoManager(this); 29 | 30 | evt.bind(el, 'cut', this.cut.bind(this)); 31 | evt.bind(el, 'paste', this.paste.bind(this)); 32 | evt.bind(el, 'keyup', this.keyup.bind(this)); 33 | evt.bind(el, 'input', this.changed.bind(this)); 34 | evt.bind(el, 'keydown', this.keydown.bind(this)); 35 | evt.bind(el, 'keypress', this.keypress.bind(this)); 36 | 37 | 38 | var changeCb = opts['change']; 39 | this.changeCb = changeCb || function(){}; 40 | 41 | this.changed(); 42 | } 43 | 44 | Editor.prototype.fireChange = function(){ 45 | var prev = this._prevValue; 46 | var now = this.getValue(); 47 | if(prev !== now){ 48 | this.changeCb(now); 49 | this._prevValue = now; 50 | } 51 | }; 52 | 53 | Editor.prototype['setValue'] = function(val){ 54 | this.setText(val); 55 | this.changed(); 56 | }; 57 | 58 | Editor.prototype['getValue'] = function(){ 59 | return this.getText(); 60 | }; 61 | 62 | Editor.prototype.getText = function(){ 63 | return this.inner.textContent; 64 | }; 65 | 66 | Editor.prototype.setText = function(val){ 67 | this.inner.textContent = val; 68 | }; 69 | 70 | Editor.prototype.keyup = function(evt){ 71 | var keyCode = evt && evt.keyCode || 0, 72 | code = this.getText(); 73 | 74 | if([ 75 | 9, 91, 93, 16, 17, 18, // modifiers 76 | 20, // caps lock 77 | 13, // Enter (handled by keydown) 78 | 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, // F[0-12] 79 | 27 // Esc 80 | ].indexOf(keyCode) > -1) { 81 | return; 82 | } 83 | 84 | if([ 85 | 33, 34, // PgUp, PgDn 86 | 35, 36, // End, Home 87 | 37, 39, 38, 40 // Left, Right, Up, Down 88 | ].indexOf(keyCode) === -1) { 89 | this.changed(); 90 | } 91 | }; 92 | 93 | Editor.prototype.changed = function(evt){ 94 | var code = this.getText(); 95 | 96 | var ss = this.selMgr.getStart(), 97 | se = this.selMgr.getEnd(); 98 | 99 | this.saveScrollPos(); 100 | 101 | var setHTML; 102 | 103 | if(code === this._prevCode){ 104 | if(this.inner.innerHTML !== this._prevHTML) setHTML = this._prevHTML; 105 | }else{ 106 | this._prevHTML = setHTML = Prism['highlight'](code, md); 107 | } 108 | this._prevCode = code; 109 | 110 | if(setHTML !== undefined){ 111 | if(!/\n$/.test(code)) { 112 | setHTML += '\n'; 113 | } 114 | 115 | var dummy = this.inner.cloneNode(false); 116 | dummy.innerHTML = setHTML; 117 | this.el.replaceChild(dummy, this.inner); 118 | this.inner = dummy; 119 | } 120 | 121 | this.restoreScrollPos(); 122 | 123 | if(ss !== null || se !== null) { 124 | this.selMgr.setRange(ss, se); 125 | } 126 | 127 | this.fireChange(); 128 | }; 129 | 130 | Editor.prototype.saveScrollPos = function(){ 131 | if(this.st === undefined) this.st = this.el.scrollTop; 132 | setTimeout(function(){ 133 | this.st = undefined; 134 | }.bind(this), 500); 135 | }; 136 | 137 | Editor.prototype.restoreScrollPos = function(){ 138 | this.el.scrollTop = this.st; 139 | this.st = undefined; 140 | }; 141 | 142 | 143 | Editor.prototype.keypress = function(evt){ 144 | var ctrl = evt.metaKey || evt.ctrlKey; 145 | 146 | if(ctrl) return; 147 | 148 | var code = evt.charCode; 149 | 150 | if(!code) return; 151 | 152 | var start = this.selMgr.getStart(); 153 | var end = this.selMgr.getEnd(); 154 | 155 | var chr = String.fromCharCode(code); 156 | 157 | if(/[\[\{\(<"'~\*_]/.test(chr) && start !== end){ 158 | this.action('wrap', { 159 | bracket: chr 160 | }); 161 | evt.preventDefault(); 162 | return; 163 | } 164 | 165 | 166 | this.undoMgr.action({ 167 | add: chr, 168 | del: start === end ? '' : this.getText().slice(start, end), 169 | start: start 170 | }); 171 | }; 172 | 173 | Editor.prototype.keydown = function(evt){ 174 | var cmdOrCtrl = evt.metaKey || evt.ctrlKey; 175 | 176 | switch(evt.keyCode) { 177 | case 8: // Backspace 178 | case 46: // Delete 179 | var start = this.selMgr.getStart(); 180 | var end = this.selMgr.getEnd(); 181 | var length = start === end ? 1 : Math.abs(end - start); 182 | start = evt.keyCode === 8 ? end - length : start; 183 | this.undoMgr.action({ 184 | add: '', 185 | del: this.getText().slice(start, start + length), 186 | start: start 187 | }); 188 | break; 189 | case 9: // Tab 190 | if(!cmdOrCtrl) { 191 | this.action('indent', { 192 | inverse: evt.shiftKey 193 | }); 194 | evt.preventDefault(); 195 | } 196 | break; 197 | case 219: // [ 198 | case 221: // ] 199 | if(cmdOrCtrl && !evt.shiftKey) { 200 | this.action('indent', { 201 | inverse: evt.keyCode === 219, 202 | ctrl: true 203 | }); 204 | evt.preventDefault(); 205 | } 206 | break; 207 | case 13: 208 | this.action('newline'); 209 | evt.preventDefault(); 210 | break; 211 | case 89: 212 | if(cmdOrCtrl){ 213 | this.undoMgr.redo(); 214 | evt.preventDefault(); 215 | } 216 | break; 217 | case 90: 218 | if(cmdOrCtrl) { 219 | evt.shiftKey ? this.undoMgr.redo() : this.undoMgr.undo(); 220 | evt.preventDefault(); 221 | } 222 | 223 | break; 224 | } 225 | }; 226 | 227 | Editor.prototype.apply = function(action){ 228 | this.setText(spliceString(this.getText(), action.start, action.del.length, action.add)); 229 | this.selMgr.setRange(action.start, action.start + action.add.length); 230 | this.changed(); 231 | }; 232 | 233 | Editor.prototype.action = function(act, opts){ 234 | opts = opts || {}; 235 | var text = this.getText(); 236 | var start = opts.start || this.selMgr.getStart(); 237 | var end = opts.end || this.selMgr.getEnd(); 238 | 239 | var state = { 240 | start: start, 241 | end: end, 242 | before: text.slice(0, start), 243 | after: text.slice(end), 244 | sel: text.slice(start, end) 245 | }; 246 | 247 | var a = actions[act](state, opts); 248 | 249 | this.saveScrollPos(); 250 | 251 | this.setText(state.before + state.sel + state.after); 252 | 253 | if(a && !opts.noHistory){ 254 | this.undoMgr.action(a); 255 | } 256 | this.selMgr.setRange(state.start, state.end, false); 257 | 258 | this.changed(); 259 | 260 | }; 261 | 262 | Editor.prototype.cut = function(){ 263 | var start = this.selMgr.getStart(); 264 | var end = this.selMgr.getEnd(); 265 | if(start === end) return; 266 | 267 | this.undoMgr.action({ 268 | add: '', 269 | del: this.getText().slice(start, end), 270 | start: start 271 | }); 272 | }; 273 | 274 | Editor.prototype.paste = function(evt){ 275 | var start = this.selMgr.getStart(); 276 | var end = this.selMgr.getEnd(); 277 | var selection = start === end ? '' : this.getText().slice(start, end); 278 | 279 | var self = this; 280 | 281 | function applyPasted(pasted){ 282 | self.undoMgr.action({ 283 | add: pasted, 284 | del: selection, 285 | start: start 286 | }); 287 | 288 | start += pasted.length; 289 | self.selMgr.setRange(start, start); 290 | self.changed(); 291 | } 292 | 293 | if(evt.clipboardData){ 294 | evt.preventDefault(); 295 | 296 | var pasted = evt.clipboardData.getData('text/plain'); 297 | 298 | this.apply({ 299 | add: pasted, 300 | del: selection, 301 | start: start 302 | }); 303 | 304 | applyPasted(pasted); 305 | }else{ 306 | // handle IE9 with no clipboardData. Flickers a bit if styles have changed :( 307 | setTimeout(function(){ 308 | var newEnd = self.selMgr.getEnd(); 309 | 310 | applyPasted(self.getText().slice(start, newEnd)); 311 | }, 0); 312 | } 313 | }; 314 | -------------------------------------------------------------------------------- /src/SelectionManager.js: -------------------------------------------------------------------------------- 1 | function SelectionManager(elt){ 2 | this.elt = elt; 3 | } 4 | 5 | SelectionManager.prototype.getStart = function(){ 6 | var selection = getSelection(); 7 | 8 | if(!selection.rangeCount) return 0; 9 | 10 | var range = selection.getRangeAt(0); 11 | var el = range.startContainer; 12 | var container = el; 13 | var offset = range.startOffset; 14 | 15 | if(!(this.elt.compareDocumentPosition(el) & 0x10)){ 16 | // selection is outside this element. 17 | return 0; 18 | } 19 | 20 | do{ 21 | while((el = el.previousSibling)){ 22 | if(el.textContent){ 23 | offset += el.textContent.length; 24 | } 25 | } 26 | 27 | el = container = container.parentNode; 28 | }while(el && el !== this.elt); 29 | 30 | return offset; 31 | }; 32 | 33 | SelectionManager.prototype.getEnd = function(){ 34 | var selection = getSelection(); 35 | 36 | if(!selection.rangeCount) return 0; 37 | 38 | return this.getStart() + String(selection.getRangeAt(0)).length; 39 | }; 40 | 41 | SelectionManager.prototype.setRange = function(start, end, noscroll){ 42 | var range = document.createRange(); 43 | var startOffset = findOffset(this.elt, start); 44 | var endOffset = startOffset; 45 | if(end && end !== start){ 46 | endOffset = findOffset(this.elt, end); 47 | }else{ 48 | if(noscroll !== false) scrollToCaret.call(this, endOffset.element, endOffset.offset); 49 | } 50 | 51 | range.setStart(startOffset.element, startOffset.offset); 52 | range.setEnd(endOffset.element, endOffset.offset); 53 | 54 | var selection = getSelection(); 55 | selection.removeAllRanges(); 56 | selection.addRange(range); 57 | }; 58 | 59 | 60 | 61 | var caret = document.createElement('span'); 62 | caret.style.position = 'absolute'; 63 | caret.innerHTML = '|'; 64 | 65 | function scrollToCaret(el, offset){ 66 | var t = el.textContent; 67 | var p = el.parentNode; 68 | var before = t.slice(0, offset); 69 | var after = t.slice(offset); 70 | 71 | el.textContent = after; 72 | var b4 = document.createTextNode(before); 73 | p.insertBefore(caret, el); 74 | p.insertBefore(b4, caret); 75 | 76 | // caret.scrollIntoViewIfNeeded(); 77 | var tp = caret.offsetTop; 78 | var h = caret.offsetHeight; 79 | var ch = this.elt.offsetHeight; 80 | var st = this.elt.scrollTop; 81 | 82 | el.textContent = t; 83 | p.removeChild(caret); 84 | p.removeChild(b4); 85 | 86 | if(tp - st < 0){ 87 | this.elt.scrollTop = tp; 88 | }else if(tp - st + h > ch){ 89 | this.elt.scrollTop = tp + h - ch; 90 | } 91 | } 92 | 93 | 94 | 95 | 96 | function findOffset(root, ss) { 97 | if(!root) { 98 | return null; 99 | } 100 | 101 | var offset = 0, 102 | element = root; 103 | 104 | do { 105 | var container = element; 106 | element = element.firstChild; 107 | 108 | if(element) { 109 | do { 110 | var len = element.textContent.length; 111 | 112 | if(offset <= ss && offset + len > ss) { 113 | break; 114 | } 115 | 116 | offset += len; 117 | } while(element = element.nextSibling); 118 | } 119 | 120 | if(!element) { 121 | // It's the container's lastChild 122 | break; 123 | } 124 | } while(element && element.hasChildNodes() && element.nodeType != 3); 125 | 126 | if(element) { 127 | return { 128 | element: element, 129 | offset: ss - offset 130 | }; 131 | } 132 | else if(container) { 133 | element = container; 134 | 135 | while(element && element.lastChild) { 136 | element = element.lastChild; 137 | } 138 | 139 | if(element.nodeType === 3) { 140 | return { 141 | element: element, 142 | offset: element.textContent.length 143 | }; 144 | } 145 | else { 146 | return { 147 | element: element, 148 | offset: 0 149 | }; 150 | } 151 | } 152 | 153 | return { 154 | element: root, 155 | offset: 0, 156 | error: true 157 | }; 158 | } 159 | -------------------------------------------------------------------------------- /src/UndoManager.js: -------------------------------------------------------------------------------- 1 | function UndoManager(editor){ 2 | this.editor = editor; 3 | 4 | this.undoStack = []; 5 | this.redoStack = []; 6 | } 7 | 8 | UndoManager.prototype.action = function(a){ 9 | /// sanity? 10 | 11 | if(this.undoStack.length && this.canCombine(this.undoStack[this.undoStack.length-1], a)){ 12 | this.undoStack.push(this.combine(this.undoStack.pop(), a)); 13 | }else{ 14 | this.undoStack.push(a); 15 | } 16 | this.redoStack = []; 17 | }; 18 | 19 | UndoManager.prototype.canCombine = function(a, b){ 20 | return ( 21 | !a.action && !b.action && 22 | !Array.isArray(a) && !Array.isArray(b) && 23 | !(a.del && b.add) && !(a.add && b.del) && 24 | !(a.add && !b.add) && !(!a.add && b.add) && 25 | !(a.add && a.del) && 26 | !(b.add && b.del) && 27 | a.start + a.add.length === b.start + b.del.length 28 | ); 29 | }; 30 | 31 | UndoManager.prototype.combine = function(a, b){ 32 | return { 33 | add: a.add + b.add, 34 | del: b.del + a.del, 35 | start: Math.min(a.start, b.start) 36 | }; 37 | }; 38 | 39 | UndoManager.prototype.undo = function(){ 40 | if(!this.undoStack.length) return; 41 | 42 | var a = this.undoStack.pop(); 43 | this.redoStack.push(a); 44 | 45 | this.applyInverse(a); 46 | }; 47 | 48 | UndoManager.prototype.redo = function(){ 49 | if(!this.redoStack.length) return; 50 | 51 | var a = this.redoStack.pop(); 52 | this.undoStack.push(a); 53 | 54 | this.apply(a); 55 | }; 56 | 57 | UndoManager.prototype.apply = function apply(a){ 58 | if(Array.isArray(a)){ 59 | a.forEach(apply.bind(this)); 60 | return; 61 | } 62 | 63 | if(a.action){ 64 | this.editor.action(a.action, { 65 | inverse: a.inverse, 66 | start: a.start, 67 | end: a.end, 68 | noHistory: true 69 | }); 70 | }else{ 71 | this.editor.apply(a); 72 | } 73 | }; 74 | 75 | UndoManager.prototype.applyInverse = function inv(a){ 76 | if(Array.isArray(a)){ 77 | a.forEach(inv.bind(this)); 78 | return; 79 | } 80 | 81 | if(a.action){ 82 | this.editor.action(a.action, { 83 | inverse: !a.inverse, 84 | start: a.start, 85 | end: a.end, 86 | noHistory: true 87 | }); 88 | }else{ 89 | this.editor.apply({ 90 | start: a.start, 91 | end: a.end, 92 | del: a.add, 93 | add: a.del 94 | }); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | var actions = { 2 | 'newline': function(state, options){ 3 | var s = state.start; 4 | var lf = state.before.lastIndexOf('\n') + 1; 5 | var afterLf = state.before.slice(lf); 6 | var indent = afterLf.match(/^\s*/)[0]; 7 | var add = indent; 8 | var clearPrevLine = false; 9 | 10 | if(/^ {0,3}$/.test(indent)){ // maybe list 11 | var l = afterLf.slice(indent.length); 12 | if(/^[*+\-]\s+/.test(l)){ 13 | add += l.match(/^[*+\-]\s+/)[0]; 14 | clearPrevLine = /^[*+\-]\s+$/.test(l); 15 | }else if(/^\d+\.\s+/.test(l)){ 16 | add += l.match(/^\d+\.\s+/)[0] 17 | .replace(/^\d+/, function(n){ return +n+1; }); 18 | clearPrevLine = /^\d+\.\s+$/.test(l); 19 | }else if(/^>/.test(l)){ 20 | add += l.match(/^>\s*/)[0]; 21 | clearPrevLine = /^>\s*$/.test(l); 22 | } 23 | } 24 | 25 | add = '\n' + add; 26 | 27 | var del = state.sel; 28 | state.sel = ''; 29 | 30 | if(clearPrevLine){ // if prev line was actually an empty liste item, clear it 31 | del = afterLf + del; 32 | state.before = state.before.slice(0, lf); 33 | state.start -= afterLf.length; 34 | s -= afterLf.length; 35 | add = '\n'; 36 | } 37 | 38 | state.before += add; 39 | state.start += add.length; 40 | state.end = state.start; 41 | 42 | return { add: add, del: del, start: s }; 43 | }, 44 | 45 | 'indent': function(state, options){ 46 | var lf = state.before.lastIndexOf('\n') + 1; 47 | 48 | // TODO deal with soft tabs 49 | 50 | if(options.inverse){ 51 | if(/\s/.test(state.before.charAt(lf))){ 52 | state.before = spliceString(state.before, lf, 1); 53 | state.start -= 1; 54 | } 55 | state.sel = state.sel.replace(/\r?\n(?!\r?\n)\s/, '\n'); 56 | }else if(state.sel || options.ctrl){ 57 | state.before = spliceString(state.before, lf, 0, '\t'); 58 | state.sel = state.sel.replace(/\r?\n/, '\n\t'); 59 | state.start += 1; 60 | }else{ 61 | state.before += '\t'; 62 | state.start += 1; 63 | state.end += 1; 64 | 65 | return { add: '\t', del: '', start: state.start - 1 }; 66 | } 67 | 68 | state.end = state.start + state.sel.length; 69 | 70 | return { 71 | action: 'indent', 72 | start: state.start, 73 | end: state.end, 74 | inverse: options.inverse 75 | }; 76 | }, 77 | 78 | 'wrap': function(state, options){ 79 | var match = { 80 | '(': ')', 81 | '[': ']', 82 | '{': '}', 83 | '<': '>' 84 | }[options.bracket] || options.bracket; 85 | 86 | state.before += options.bracket; 87 | state.after = match + state.after; 88 | state.start += 1; 89 | state.end += 1; 90 | 91 | return { 92 | add: options.bracket + state.sel + match, 93 | del: state.sel, 94 | start: state.start - 1, 95 | end: state.end - 1 96 | }; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /src/md.js: -------------------------------------------------------------------------------- 1 | var md = (function(){ 2 | var md = { 3 | 'comment': Prism['languages']['markup']['comment'] 4 | }; 5 | 6 | 7 | md['front-matter'] = { 8 | 'pattern': /^---\n[\s\S]*?\n---(?=\n|$)/, 9 | 'inside': { 10 | 'marker front-matter-marker start': /^---/, 11 | 'marker front-matter-marker end': /---$/, 12 | 'rest': yaml 13 | } 14 | }; 15 | 16 | 17 | var inlines = {}; 18 | var blocks = {}; 19 | 20 | function inline(name, def){ 21 | blocks[name] = inlines[name] = md[name] = def; 22 | } 23 | function block(name, def){ 24 | blocks[name] = md[name] = def; 25 | } 26 | 27 | 28 | var langAliases = { 29 | 'markup': [ 'markup', 'html', 'xml' ], 30 | 'javascript': [ 'javascript', 'js' ] 31 | }; 32 | 33 | for(var i in Prism['languages']){ 34 | if(!Prism['languages'].hasOwnProperty(i)) continue; 35 | var l = Prism['languages'][i]; 36 | if(typeof l === 'function') continue; 37 | 38 | var aliases = langAliases[i]; 39 | var matches = aliases ? aliases.join('|') : i; 40 | 41 | block('code-block fenced ' + i, { 42 | 'pattern': new RegExp('(^ {0,3}|\\n {0,3})(([`~])\\3\\3) *(' + matches + ')( [^`\n]*)? *\\n(?:[\\s\\S]*?)\\n {0,3}(\\2\\3*(?= *\\n)|$)', 'gi'), 43 | 'lookbehind': true, 44 | 'inside': { 45 | 'code-language': { 46 | 'pattern': /(^([`~])\2+)((?!\2)[^\2\n])+/, 47 | 'lookbehind': true 48 | }, 49 | 'marker code-fence start': /^([`~])\1+/, 50 | 'marker code-fence end': /([`~])\1+$/, 51 | 'code-inner': { 52 | 'pattern': /(^\n)[\s\S]*(?=\n$)/, 53 | 'lookbehind': true, 54 | 'alias': 'language-' + i, 55 | 'inside': l 56 | } 57 | } 58 | }); 59 | } 60 | 61 | 62 | block('code-block fenced untagged', { 63 | 'pattern': /(^ {0,3}|\n {0,3})(([`~])\3\3)[^`\n]*\n(?:[\s\S]*?)\n {0,3}(\2\3*(?= *\n)|$)/g, 64 | 'lookbehind': true, 65 | 'inside': { 66 | 'code-language': { 67 | 'pattern': /(^([`~])\2+)((?!\2)[^\2\n])+/, 68 | 'lookbehind': true 69 | }, 70 | 'marker code-fence start': /^([`~])\1+/, 71 | 'marker code-fence end': /([`~])\1+$/, 72 | 'code-inner': { 73 | 'pattern': /(^\n)[\s\S]*(?=\n$)/, 74 | 'lookbehind': true 75 | } 76 | } 77 | }); 78 | 79 | 80 | block('heading setext-heading heading-1', { 81 | 'pattern': /^ {0,3}[^\s].*\n {0,3}=+[ \t]*$/gm, 82 | 'inside': { 83 | 'marker heading-setext-line': { 84 | 'pattern': /^( {0,3}[^\s].*\n) {0,3}=+[ \t]*$/gm, 85 | 'lookbehind': true 86 | }, 87 | 'rest': inlines 88 | } 89 | }); 90 | 91 | block('heading setext-heading heading-2', { 92 | 'pattern': /^ {0,3}[^\s].*\n {0,3}-+[ \t]*$/gm, 93 | 'inside': { 94 | 'marker heading-setext-line': { 95 | 'pattern': /^( {0,3}[^\s].*\n) {0,3}-+[ \t]*$/gm, 96 | 'lookbehind': true 97 | }, 98 | 'rest': inlines 99 | } 100 | }); 101 | 102 | var headingInside = { 103 | 'marker heading-hash start': /^ *#+ */, 104 | 'marker heading-hash end': / +#+ *$/, 105 | 'rest': inlines 106 | }; 107 | for(var i = 1; i <= 6; i += 1){ 108 | block('heading heading-'+i, { 109 | 'pattern': new RegExp('^ {0,3}#{'+i+'}(?![#\\S]).*$', 'gm'), 110 | 'inside': headingInside 111 | }); 112 | } 113 | 114 | 115 | 116 | var linkText = { 117 | 'pattern': /^\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 118 | 'inside': { 119 | 'marker bracket start': /^\[/, 120 | 'marker bracket end': /\]$/, 121 | 'link-text-inner': { 122 | 'pattern': /[\w\W]+/, 123 | 'inside': inlines 124 | } 125 | } 126 | }; 127 | 128 | var linkLabel = { 129 | 'pattern': /\[(?:\\.|[^\]])*\]/, 130 | 'inside': { 131 | 'marker bracket start': /^\[/, 132 | 'marker bracket end': /\]$/, 133 | 'link-label-inner': /[\w\W]+/ 134 | } 135 | }; 136 | 137 | var imageText = { 138 | 'pattern': /^!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 139 | 'inside': { 140 | 'marker image-bang': /^!/, 141 | 'marker bracket start': /^\[/, 142 | 'marker bracket end': /\]$/, 143 | 'image-text-inner': { 144 | 'pattern': /[\w\W]+/, 145 | 'inside': inlines 146 | } 147 | } 148 | }; 149 | 150 | var linkURL = { 151 | 'pattern': /^(\s*)(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))+/, 152 | 'lookbehind': true 153 | }; 154 | 155 | var linkBracedURL = { 156 | 'pattern': /^(\s*)<(?:\\.|[^<>\n])*>/, 157 | 'lookbehind': true, 158 | 'inside': { 159 | 'marker brace start': /^, 160 | 'marker brace end': />$/, 161 | 'braced-href-inner': /[\w\W]+/ 162 | } 163 | }; 164 | 165 | var linkTitle = { 166 | 'pattern': /('(?:\\'|[^'])+'|"(?:\\"|[^"])+")\s*$/, 167 | // 'lookbehind': true, 168 | 'inside': { 169 | 'marker quote start': /^['"]/, 170 | 'marker quote end': /['"]$/, 171 | 'title-inner': /[\w\W]+/ 172 | } 173 | }; 174 | 175 | var linkParams = { 176 | 'pattern': /\( *(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)( +('(?:[^']|\\')+'|"(?:[^"]|\\")+"))? *\)/, 177 | 'inside': { 178 | 'marker bracket start': /^\(/, 179 | 'marker bracket end': /\)$/, 180 | 'link-params-inner': { 181 | 'pattern': /[\w\W]+/, 182 | 'inside': { 183 | 'link-title': linkTitle, 184 | 'href': linkURL, 185 | 'braced-href': linkBracedURL 186 | } 187 | } 188 | } 189 | }; 190 | 191 | 192 | 193 | 194 | block('hr', { 195 | 'pattern': /^[\t ]*([*\-_])([\t ]*\1){2,}([\t ]*$)/gm, 196 | 'inside': { 197 | 'marker hr-marker': /[*\-_]/g 198 | } 199 | }); 200 | 201 | block('urldef', { 202 | 'pattern': /^( {0,3})\[(?:\\.|[^\]])+]: *\n? *(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)( *\n? *('(?:\\'|[^'])+'|"(?:\\"|[^"])+"))?$/gm, 203 | 'lookbehind': true, 204 | 'inside': { 205 | 'link-label': linkLabel, 206 | 'marker urldef-colon': /^:/, 207 | 'link-title': linkTitle, 208 | 'href': linkURL, 209 | 'braced-href': linkBracedURL 210 | } 211 | }); 212 | 213 | block('blockquote', { 214 | 'pattern': /^[\t ]*>[\t ]?.+(?:\n(?!\n)|.)*/gm, 215 | 'inside': { 216 | 'marker quote-marker': /^[\t ]*>[\t ]?/gm, 217 | 'blockquote-content': { 218 | 'pattern': /[^>]+/, 219 | 'inside': { 220 | 'rest': blocks 221 | } 222 | } 223 | } 224 | }); 225 | 226 | block('list', { 227 | 'pattern': /^[\t ]*([*+\-]|\d+\.)[\t ].+(?:\n(?!\n)|.)*/gm, 228 | 'inside': { 229 | 'li': { 230 | 'pattern': /^[\t ]*([*+\-]|\d+\.)[\t ].+(?:\n|[ \t]+[^*+\- \t].*\n)*/gm, 231 | 'inside': { 232 | 'marker list-item': /^[ \t]*([*+\-]|\d+\.)[ \t]/m, 233 | 'rest': blocks 234 | } 235 | } 236 | } 237 | }); 238 | 239 | block('code-block indented', { 240 | 'pattern': /(^|(?:^|(?:^|\n)(?![ \t]*([*+\-]|\d+\.)[ \t]).*\n)\s*?\n)((?: {4}|\t).*(?:\n|$))+/g, 241 | 'lookbehind': true 242 | }); 243 | 244 | block('p', { 245 | 'pattern': /[^\n](?:\n(?!\n)|.)*[^\n]/g, 246 | 'inside': inlines 247 | }); 248 | 249 | inline('image', { 250 | 'pattern': /(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]\(\s*(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)(\s+('(?:[^']|\\')+'|"(?:[^"]|\\")+"))?\s*\)/, 251 | 'lookbehind': true, 252 | 'inside': { 253 | 'link-text': imageText, 254 | 'link-params': linkParams 255 | } 256 | }); 257 | 258 | inline('link', { 259 | 'pattern': /(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]\(\s*(?:(?!<)(?:\\.|[^\(\)\s]|\([^\(\)\s]*\))*|<(?:[^<>\n]|\\.)*>)(\s+('(?:[^']|\\')+'|"(?:[^"]|\\")+"))?\s*\)/, 260 | 'lookbehind': true, 261 | 'inside': { 262 | 'link-text': linkText, 263 | 'link-params': linkParams 264 | } 265 | }); 266 | 267 | inline('image image-ref', { 268 | 'pattern': /(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\] ?\[(?:\\.|[^\]])*\]/, 269 | 'lookbehind': true, 270 | 'inside': { 271 | 'link-text': imageText, 272 | 'link-label': linkLabel 273 | } 274 | }); 275 | inline('link link-ref', { 276 | 'pattern': /(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\] ?\[(?:\\.|[^\]])*\]/, 277 | 'lookbehind': true, 278 | 'inside': { 279 | 'link-text': linkText, 280 | 'link-label': linkLabel 281 | } 282 | }); 283 | 284 | inline('image image-ref shortcut-ref', { 285 | 'pattern': /(^|[^\\])!\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 286 | 'lookbehind': true, 287 | 'inside': { 288 | 'marker image-bang': /^!/, 289 | 'link-text': linkText 290 | } 291 | }); 292 | inline('link link-ref shortcut-ref', { 293 | 'pattern': /(^|[^\\])\[(?:\\.|[^\[\]]|\[[^\[\]]*\])*\]/, 294 | 'lookbehind': true, 295 | 'inside': { 296 | 'link-text': linkText 297 | } 298 | }); 299 | 300 | 301 | inline('code', { 302 | 'pattern': /(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/g, 303 | 'lookbehind': true, 304 | 'inside': { 305 | 'marker code-marker start': /^`/, 306 | 'marker code-marker end': /`$/, 307 | 'code-inner': /[\w\W]+/ 308 | } 309 | }); 310 | 311 | inline('strong', { 312 | 'pattern': /(^|[^\\*_]|\\[*_])([_\*])\2(?:\n(?!\n)|.)+?\2{2}(?!\2)/g, 313 | // 'pattern': /(^|[^\\])(\*\*|__)(?:\n(?!\n)|.)+?\2/, 314 | 'lookbehind': true, 315 | 'inside': { 316 | 'marker strong-marker start': /^(\*\*|__)/, 317 | 'marker strong-marker end': /(\*\*|__)$/, 318 | 'strong-inner': { 319 | 'pattern': /[\w\W]+/, 320 | 'inside': inlines 321 | } 322 | } 323 | }); 324 | 325 | inline('em', { 326 | // 'pattern': /(^|[^\\])(\*|_)(\S[^\2]*?)??[^\s\\]+?\2/g, 327 | 'pattern': /(^|[^\\*_]|\\[*_])(\*|_)(?:\n(?!\n)|.)+?\2(?!\2)/g, 328 | 'lookbehind': true, 329 | 'inside': { 330 | 'marker em-marker start': /^(\*|_)/, 331 | 'marker em-marker end': /(\*|_)$/, 332 | 'em-inner': { 333 | 'pattern': /[\w\W]+/, 334 | 'inside': inlines 335 | } 336 | } 337 | }); 338 | 339 | inline('strike', { 340 | 'pattern': /(^|\n|(?!\\)\W)(~~)(?=\S)([^\r]*?\S)\2/gm, 341 | 'lookbehind': true, 342 | 'inside': { 343 | 'marker strike-marker start': /^~~/, 344 | 'marker strike-marker end': /~~$/, 345 | 'strike-inner': { 346 | 'pattern': /[\w\W]+/, 347 | 'inside': inlines 348 | } 349 | } 350 | }); 351 | 352 | inline('comment', Prism['languages']['markup']['comment']); 353 | 354 | var tag = Prism['languages']['markup']['tag']; 355 | var tagMatch = tag['pattern']; 356 | 357 | inline('tag', { 358 | 'pattern': new RegExp("(^|[^\\\\])" + tagMatch.source, 'i'), 359 | 'lookbehind': true, 360 | 'inside': tag['inside'] 361 | }); 362 | inline('entity', Prism['languages']['markup']['entity']); 363 | 364 | return md; 365 | })(); 366 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | var evt = { 2 | bind: function(el, evt, fn){ 3 | el.addEventListener(evt, fn, false); 4 | } 5 | }; 6 | 7 | 8 | function spliceString(str, i, remove, add){ 9 | remove = +remove || 0; 10 | add = add || ''; 11 | 12 | return str.slice(0,i) + add + str.slice(i+remove); 13 | } 14 | -------------------------------------------------------------------------------- /src/yaml.js: -------------------------------------------------------------------------------- 1 | var yaml = { 2 | 'scalar': { 3 | 'pattern': /([\-:]\s*(![^\s]+)?[ \t]*[|>])[ \t]*(?:(\n[ \t]+)[^\r\n]+(?:\3[^\r\n]+)*)/, 4 | 'lookbehind': true, 5 | 'alias': 'string' 6 | }, 7 | 'comment': /#[^\n]+/, 8 | 'key': { 9 | 'pattern': /(\s*[:\-,[{\n?][ \t]*(![^\s]+)?[ \t]*)[^\n{[\]},#]+?(?=\s*:\s)/, 10 | 'lookbehind': true, 11 | 'alias': 'atrule' 12 | }, 13 | 'directive': { 14 | 'pattern': /((^|\n)[ \t]*)%[^\n]+/, 15 | 'lookbehind': true, 16 | 'alias': 'important' 17 | }, 18 | 'datetime': { 19 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)(\d{4}-\d\d?-\d\d?([tT]|[ \t]+)\d\d?:\d{2}:\d{2}(\.\d*)?[ \t]*(Z|[-+]\d\d?(:\d{2})?)?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(:\d{2}(\.\d*)?)?)(?=[ \t]*(\n|$|,|]|}))/, 20 | 'lookbehind': true, 21 | 'alias': 'number' 22 | }, 23 | 'boolean': { 24 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)(true|false)[ \t]*(?=\n|$|,|]|})/i, 25 | 'lookbehind': true, 26 | 'alias': 'important' 27 | }, 28 | 'null': { 29 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)(null|~)[ \t]*(?=\n|$|,|]|})/i, 30 | 'lookbehind': true, 31 | 'alias': 'important' 32 | }, 33 | 'string': { 34 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')(?=[ \t]*(\n|$|,|]|}))/, 35 | 'lookbehind': true 36 | }, 37 | 'number': { 38 | 'pattern': /([:\-,[{]\s*(![^\s]+)?[ \t]*)[+\-]?(0x[\dA-Fa-f]+|0o[0-7]+|(\d+\.?\d*|\.?\d+)(e[\+\-]?\d+)?|\.inf|\.nan)[ \t]*(?=\n|$|,|]|})/i, 39 | 'lookbehind': true 40 | }, 41 | 'tag': /![^\s]+/, 42 | 'important': /[&*][\w]+/, 43 | 'punctuation': /([:[\]{}\-,|>?]|---|\.\.\.)/ 44 | }; 45 | --------------------------------------------------------------------------------