├── .gitignore ├── LICENSE.txt ├── README.md ├── lib └── index.js ├── package.json ├── rollup.config.js ├── src ├── MarkdownRenderer.js └── markdownparser.js └── test └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 MakerLoop Inc 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slate editor markdown serializer 2 | 3 | Experimental Markdown serializer for slate (https://github.com/ianstormtaylor/slate/tree/master). 4 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var slate = require('slate'); 4 | var immutable = require('immutable'); 5 | 6 | /** 7 | * Ported from: 8 | * https://github.com/chjj/marked/blob/49b7eaca/lib/marked.js 9 | * TODO: 10 | * Use ES6 classes 11 | * Add flow annotations 12 | */ 13 | /* eslint-disable no-spaced-func */ 14 | 15 | var hasOwnProperty = Object.prototype.hasOwnProperty; 16 | 17 | var assign = Object.assign || function (obj) { 18 | var i = 1; 19 | for (; i < arguments.length; i++) { 20 | var target = arguments[i]; 21 | for (var key in target) { 22 | if (hasOwnProperty.call(target, key)) { 23 | obj[key] = target[key]; 24 | } 25 | } 26 | } 27 | return obj; 28 | }; 29 | 30 | var flatten = function flatten(ary) { 31 | return [].concat.apply([], ary); 32 | }; 33 | 34 | var noop = function noop() {}; 35 | noop.exec = noop; 36 | 37 | var defaults = { 38 | gfm: true, 39 | breaks: false, 40 | pedantic: false, 41 | smartLists: false, 42 | silent: false, 43 | langPrefix: 'lang-', 44 | renderer: new Renderer() 45 | }; 46 | 47 | /** 48 | * Block-Level Grammar 49 | */ 50 | 51 | var block = { 52 | newline: /^\n+/, 53 | code: /^( {4}[^\n]+\n*)+/, 54 | fences: noop, 55 | hr: /^( *[-*_]){3,} *(?:\n+|$)/, 56 | heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, 57 | nptable: noop, 58 | lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, 59 | blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, 60 | list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, 61 | def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, 62 | paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|def))+)\n*/, 63 | text: /^[^\n]+/ 64 | }; 65 | 66 | block.bullet = /(?:[*+-]|\d+\.)/; 67 | block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; 68 | block.item = replace(block.item, 'gm')(/bull/g, block.bullet)(); 69 | 70 | block.list = replace(block.list)(/bull/g, block.bullet)('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))')('def', '\\n+(?=' + block.def.source + ')')(); 71 | 72 | block.blockquote = replace(block.blockquote)('def', block.def)(); 73 | 74 | block.paragraph = replace(block.paragraph)('hr', block.hr)('heading', block.heading)('lheading', block.lheading)('blockquote', block.blockquote)('def', block.def)(); 75 | 76 | /** 77 | * Normal Block Grammar 78 | */ 79 | 80 | block.normal = assign({}, block); 81 | 82 | /** 83 | * GFM Block Grammar 84 | */ 85 | 86 | block.gfm = assign({}, block.normal, { 87 | fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, 88 | paragraph: /^/, 89 | heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ 90 | }); 91 | 92 | block.gfm.paragraph = replace(block.paragraph)('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|' + block.list.source.replace('\\1', '\\3') + '|')(); 93 | 94 | /** 95 | * Block Lexer 96 | */ 97 | 98 | function Lexer(options) { 99 | this.tokens = []; 100 | this.tokens.links = {}; 101 | this.options = assign({}, options || defaults); 102 | this.rules = block.normal; 103 | 104 | if (this.options.gfm) { 105 | this.rules = block.gfm; 106 | } 107 | } 108 | 109 | /** 110 | * Expose Block Rules 111 | */ 112 | 113 | Lexer.rules = block; 114 | 115 | /** 116 | * Static Lex Method 117 | */ 118 | 119 | Lexer.parse = function (src, options) { 120 | var lexer = new Lexer(options); 121 | return lexer.parse(src); 122 | }; 123 | 124 | /** 125 | * Preprocessing 126 | */ 127 | 128 | Lexer.prototype.parse = function (src) { 129 | src = src.replace(/\r\n|\r/g, '\n').replace(/\t/g, ' ').replace(/\u00a0/g, ' ').replace(/\u2424/g, '\n'); 130 | 131 | return this.token(src, true); 132 | }; 133 | 134 | /** 135 | * Lexing 136 | */ 137 | 138 | Lexer.prototype.token = function (src, top, bq) { 139 | var next; 140 | var loose; 141 | var cap; 142 | var bull; 143 | var b; 144 | var item; 145 | var space; 146 | var i; 147 | var l; 148 | 149 | src = src.replace(/^ +$/gm, ''); 150 | 151 | while (src) { 152 | // newline 153 | if (cap = this.rules.newline.exec(src)) { 154 | src = src.substring(cap[0].length); 155 | if (cap[0].length > 1) { 156 | this.tokens.push({ 157 | type: 'space' 158 | }); 159 | } 160 | } 161 | 162 | // code 163 | if (cap = this.rules.code.exec(src)) { 164 | src = src.substring(cap[0].length); 165 | cap = cap[0].replace(/^ {4}/gm, ''); 166 | this.tokens.push({ 167 | type: 'code', 168 | text: !this.options.pedantic ? cap.replace(/\n+$/, '') : cap 169 | }); 170 | continue; 171 | } 172 | 173 | // fences (gfm) 174 | if (cap = this.rules.fences.exec(src)) { 175 | src = src.substring(cap[0].length); 176 | this.tokens.push({ 177 | type: 'code', 178 | lang: cap[2], 179 | text: cap[3] 180 | }); 181 | continue; 182 | } 183 | 184 | // heading 185 | if (cap = this.rules.heading.exec(src)) { 186 | src = src.substring(cap[0].length); 187 | this.tokens.push({ 188 | type: 'heading', 189 | depth: cap[1].length, 190 | text: cap[2] 191 | }); 192 | continue; 193 | } 194 | 195 | // lheading 196 | if (cap = this.rules.lheading.exec(src)) { 197 | src = src.substring(cap[0].length); 198 | this.tokens.push({ 199 | type: 'heading', 200 | depth: cap[2] === '=' ? 1 : 2, 201 | text: cap[1] 202 | }); 203 | continue; 204 | } 205 | 206 | // hr 207 | if (cap = this.rules.hr.exec(src)) { 208 | src = src.substring(cap[0].length); 209 | this.tokens.push({ 210 | type: 'hr' 211 | }); 212 | continue; 213 | } 214 | 215 | // blockquote 216 | if (cap = this.rules.blockquote.exec(src)) { 217 | src = src.substring(cap[0].length); 218 | 219 | this.tokens.push({ 220 | type: 'blockquote_start' 221 | }); 222 | 223 | cap = cap[0].replace(/^ *> ?/gm, ''); 224 | 225 | // Pass `top` to keep the current 226 | // "toplevel" state. This is exactly 227 | // how markdown.pl works. 228 | this.token(cap, top, true); 229 | 230 | this.tokens.push({ 231 | type: 'blockquote_end' 232 | }); 233 | 234 | continue; 235 | } 236 | 237 | // list 238 | if (cap = this.rules.list.exec(src)) { 239 | src = src.substring(cap[0].length); 240 | bull = cap[2]; 241 | 242 | this.tokens.push({ 243 | type: 'list_start', 244 | ordered: bull.length > 1 245 | }); 246 | 247 | // Get each top-level item. 248 | cap = cap[0].match(this.rules.item); 249 | 250 | next = false; 251 | l = cap.length; 252 | i = 0; 253 | 254 | for (; i < l; i++) { 255 | item = cap[i]; 256 | 257 | // Remove the list item's bullet 258 | // so it is seen as the next token. 259 | space = item.length; 260 | item = item.replace(/^ *([*+-]|\d+\.) +/, ''); 261 | 262 | // Outdent whatever the 263 | // list item contains. Hacky. 264 | if (~item.indexOf('\n ')) { 265 | space -= item.length; 266 | item = !this.options.pedantic ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') : item.replace(/^ {1,4}/gm, ''); 267 | } 268 | 269 | // Determine whether the next list item belongs here. 270 | // Backpedal if it does not belong in this list. 271 | if (this.options.smartLists && i !== l - 1) { 272 | b = block.bullet.exec(cap[i + 1])[0]; 273 | if (bull !== b && !(bull.length > 1 && b.length > 1)) { 274 | src = cap.slice(i + 1).join('\n') + src; 275 | i = l - 1; 276 | } 277 | } 278 | 279 | // Determine whether item is loose or not. 280 | // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ 281 | // for discount behavior. 282 | loose = next || /\n\n(?!\s*$)/.test(item); 283 | if (i !== l - 1) { 284 | next = item.charAt(item.length - 1) === '\n'; 285 | if (!loose) { 286 | loose = next; 287 | } 288 | } 289 | 290 | this.tokens.push({ 291 | type: loose ? 'loose_item_start' : 'list_item_start' 292 | }); 293 | 294 | // Recurse. 295 | this.token(item, false, bq); 296 | 297 | this.tokens.push({ 298 | type: 'list_item_end' 299 | }); 300 | } 301 | 302 | this.tokens.push({ 303 | type: 'list_end' 304 | }); 305 | 306 | continue; 307 | } 308 | 309 | // def 310 | if (!bq && top && (cap = this.rules.def.exec(src))) { 311 | src = src.substring(cap[0].length); 312 | this.tokens.links[cap[1].toLowerCase()] = { 313 | href: cap[2], 314 | title: cap[3] 315 | }; 316 | continue; 317 | } 318 | 319 | // top-level paragraph 320 | if (top && (cap = this.rules.paragraph.exec(src))) { 321 | src = src.substring(cap[0].length); 322 | this.tokens.push({ 323 | type: 'paragraph', 324 | text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1] 325 | }); 326 | continue; 327 | } 328 | 329 | // text 330 | if (cap = this.rules.text.exec(src)) { 331 | // Top-level should never reach here. 332 | src = src.substring(cap[0].length); 333 | this.tokens.push({ 334 | type: 'text', 335 | text: cap[0] 336 | }); 337 | continue; 338 | } 339 | 340 | if (src) { 341 | throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); 342 | } 343 | } 344 | 345 | return this.tokens; 346 | }; 347 | 348 | /** 349 | * Inline-Level Grammar 350 | */ 351 | 352 | var inline = { 353 | escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, 354 | link: /^!?\[(inside)\]\(href\)/, 355 | reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, 356 | nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, 357 | strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, 358 | em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, 359 | code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, 360 | br: /^ {2,}\n(?!\s*$)/, 361 | del: noop, 362 | ins: noop, 363 | text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; 368 | 369 | inline.link = replace(inline.link)('inside', inline._inside)('href', inline._href)(); 370 | 371 | inline.reflink = replace(inline.reflink)('inside', inline._inside)(); 372 | 373 | /** 374 | * Normal Inline Grammar 375 | */ 376 | 377 | inline.normal = assign({}, inline); 378 | 379 | /** 380 | * Pedantic Inline Grammar 381 | */ 382 | 383 | inline.pedantic = assign({}, inline.normal, { 384 | strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, 385 | em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ 386 | }); 387 | 388 | /** 389 | * GFM Inline Grammar 390 | */ 391 | 392 | inline.gfm = assign({}, inline.normal, { 393 | escape: replace(inline.escape)('])', '~|])')(), 394 | del: /^~~(?=\S)([\s\S]*?\S)~~/, 395 | ins: /^\+\+(?=\S)([\s\S]*?\S)\+\+/, 396 | text: replace(inline.text)(']|', '~+]|')() 397 | }); 398 | 399 | /** 400 | * GFM + Line Breaks Inline Grammar 401 | */ 402 | 403 | inline.breaks = assign({}, inline.gfm, { 404 | br: replace(inline.br)('{2,}', '*')(), 405 | text: replace(inline.gfm.text)('{2,}', '*')() 406 | }); 407 | 408 | /** 409 | * Inline Lexer & Compiler 410 | */ 411 | 412 | function InlineLexer(links, options) { 413 | this.options = assign({}, options || defaults); 414 | this.links = links; 415 | this.rules = inline.normal; 416 | this.renderer = this.options.renderer || new Renderer(); 417 | this.renderer.options = this.options; 418 | 419 | if (!this.links) { 420 | throw new Error('Tokens array requires a `links` property.'); 421 | } 422 | 423 | if (this.options.gfm) { 424 | if (this.options.breaks) { 425 | this.rules = inline.breaks; 426 | } else { 427 | this.rules = inline.gfm; 428 | } 429 | } else if (this.options.pedantic) { 430 | this.rules = inline.pedantic; 431 | } 432 | } 433 | 434 | /** 435 | * Expose Inline Rules 436 | */ 437 | 438 | InlineLexer.rules = inline; 439 | 440 | /** 441 | * Static Lexing/Compiling Method 442 | */ 443 | 444 | InlineLexer.parse = function (src, links, options) { 445 | var inline = new InlineLexer(links, options); 446 | return inline.parse(src); 447 | }; 448 | 449 | /** 450 | * Lexing/Compiling 451 | */ 452 | 453 | InlineLexer.prototype.parse = function (src) { 454 | var out = []; 455 | var link; 456 | var cap; 457 | 458 | while (src) { 459 | // escape 460 | if (cap = this.rules.escape.exec(src)) { 461 | src = src.substring(cap[0].length); 462 | out.push({ 463 | kind: "text", 464 | ranges: [{ 465 | text: cap[1] 466 | }] 467 | }); 468 | continue; 469 | } 470 | 471 | // link 472 | if (cap = this.rules.link.exec(src)) { 473 | src = src.substring(cap[0].length); 474 | this.inLink = true; 475 | out.push(this.outputLink(cap, { href: cap[2], title: cap[3] })); 476 | this.inLink = false; 477 | continue; 478 | } 479 | 480 | // reflink, nolink 481 | // TODO 482 | if ((cap = this.rules.reflink.exec(src)) || (cap = this.rules.nolink.exec(src))) { 483 | src = src.substring(cap[0].length); 484 | link = (cap[2] || cap[1]).replace(/\s+/g, ' '); 485 | link = this.links[link.toLowerCase()]; 486 | if (!link || !link.href) { 487 | out.push({ 488 | kind: "text", 489 | ranges: [{ 490 | text: cap[0].charAt(0) 491 | }] 492 | }); 493 | src = cap[0].substring(1) + src; 494 | continue; 495 | } 496 | this.inLink = true; 497 | out.push(this.outputLink(cap, link)); 498 | this.inLink = false; 499 | continue; 500 | } 501 | 502 | // strong 503 | if (cap = this.rules.strong.exec(src)) { 504 | src = src.substring(cap[0].length); 505 | out.push(this.renderer.strong(this.parse(cap[2] || cap[1]))); 506 | continue; 507 | } 508 | 509 | // em 510 | if (cap = this.rules.em.exec(src)) { 511 | src = src.substring(cap[0].length); 512 | out.push(this.renderer.em(this.parse(cap[2] || cap[1]))); 513 | continue; 514 | } 515 | 516 | // code 517 | if (cap = this.rules.code.exec(src)) { 518 | src = src.substring(cap[0].length); 519 | out.push(this.renderer.codespan(cap[2])); 520 | continue; 521 | } 522 | 523 | // br 524 | if (cap = this.rules.br.exec(src)) { 525 | src = src.substring(cap[0].length); 526 | out.push(this.renderer.br()); 527 | continue; 528 | } 529 | 530 | // del (gfm) 531 | if (cap = this.rules.del.exec(src)) { 532 | src = src.substring(cap[0].length); 533 | out.push(this.renderer.del(this.parse(cap[1]))); 534 | continue; 535 | } 536 | 537 | // ins (gfm extended) 538 | if (cap = this.rules.ins.exec(src)) { 539 | src = src.substring(cap[0].length); 540 | out.push(this.renderer.ins(this.parse(cap[1]))); 541 | continue; 542 | } 543 | 544 | // text 545 | if (cap = this.rules.text.exec(src)) { 546 | src = src.substring(cap[0].length); 547 | out.push(this.renderer.text(cap[0])); 548 | continue; 549 | } 550 | 551 | if (src) { 552 | throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); 553 | } 554 | } 555 | 556 | return out; 557 | }; 558 | 559 | /** 560 | * Compile Link 561 | */ 562 | 563 | InlineLexer.prototype.outputLink = function (cap, link) { 564 | var href = link.href; 565 | var title = link.title; 566 | 567 | return cap[0].charAt(0) !== '!' ? this.renderer.link(href, title, this.parse(cap[1])) : this.renderer.image(href, title, cap[1]); 568 | }; 569 | 570 | /** 571 | * Renderer 572 | */ 573 | 574 | function Renderer(options) { 575 | this.options = options || {}; 576 | } 577 | 578 | Renderer.prototype.groupTextInRanges = function (childNode) { 579 | var node = flatten(childNode); 580 | return node.reduce(function (acc, current) { 581 | 582 | if (current instanceof TextNode) { 583 | var accLast = acc.length - 1; 584 | if (accLast >= 0 && acc[accLast] && acc[accLast]['kind'] === 'text') { 585 | // If the previous item was a text kind, push the current text to it's range 586 | acc[accLast].ranges.push(current); 587 | return acc; 588 | } else { 589 | // Else, create a new text kind 590 | acc.push({ 591 | kind: "text", 592 | ranges: [current] 593 | }); 594 | return acc; 595 | } 596 | } else { 597 | acc.push(current); 598 | return acc; 599 | } 600 | }, []); 601 | }; 602 | 603 | Renderer.prototype.code = function (childNode, lang) { 604 | var data = {}; 605 | if (lang) { 606 | data.language = this.options.langPrefix + lang; 607 | } 608 | return { 609 | kind: "text", 610 | ranges: [{ 611 | text: childNode, 612 | marks: [{ 613 | type: "code", 614 | data: data 615 | }] 616 | }] 617 | }; 618 | }; 619 | 620 | Renderer.prototype.blockquote = function (childNode) { 621 | return { 622 | kind: "block", 623 | type: "block-quote", 624 | nodes: this.groupTextInRanges(childNode) 625 | }; 626 | }; 627 | 628 | Renderer.prototype.heading = function (childNode, level) { 629 | return { 630 | kind: "block", 631 | type: "heading" + level, 632 | nodes: this.groupTextInRanges(childNode) 633 | }; 634 | }; 635 | 636 | Renderer.prototype.hr = function () { 637 | return { 638 | kind: "block", 639 | type: "horizontal-rule", 640 | nodes: [{ 641 | kind: "text", 642 | ranges: [{ 643 | text: '' 644 | }] 645 | }], 646 | isVoid: true 647 | }; 648 | }; 649 | 650 | Renderer.prototype.list = function (childNode, isOrdered) { 651 | var type = isOrdered ? 'numbered-list' : 'bulleted-list'; 652 | return { 653 | kind: "block", 654 | type: type, 655 | nodes: this.groupTextInRanges(childNode) 656 | }; 657 | }; 658 | 659 | Renderer.prototype.listitem = function (childNode) { 660 | return { 661 | kind: "block", 662 | type: "list-item", 663 | nodes: this.groupTextInRanges(childNode) 664 | }; 665 | }; 666 | 667 | Renderer.prototype.paragraph = function (childNode) { 668 | return { 669 | kind: "block", 670 | type: "paragraph", 671 | nodes: this.groupTextInRanges(childNode) 672 | }; 673 | }; 674 | 675 | // span level renderer 676 | Renderer.prototype.strong = function (childNode) { 677 | return childNode.map(function (node) { 678 | if (node.marks) { 679 | node.marks.push({ type: "bold" }); 680 | } else { 681 | node.marks = [{ type: "bold" }]; 682 | } 683 | return node; 684 | }); 685 | }; 686 | 687 | Renderer.prototype.em = function (childNode) { 688 | return childNode.map(function (node) { 689 | if (node.marks) { 690 | node.marks.push({ type: "italic" }); 691 | } else { 692 | node.marks = [{ type: "italic" }]; 693 | } 694 | return node; 695 | }); 696 | }; 697 | 698 | Renderer.prototype.codespan = function (text) { 699 | return new TextNode(text, { "type": "code" }); 700 | }; 701 | 702 | Renderer.prototype.br = function () { 703 | return { 704 | text: '--' 705 | }; 706 | }; 707 | 708 | Renderer.prototype.del = function (childNode) { 709 | return childNode.map(function (node) { 710 | if (node.marks) { 711 | node.marks.push({ type: "deleted" }); 712 | } else { 713 | node.marks = [{ type: "deleted" }]; 714 | } 715 | return node; 716 | }); 717 | }; 718 | 719 | Renderer.prototype.ins = function (childNode) { 720 | return childNode.map(function (node) { 721 | if (node.marks) { 722 | node.marks.push({ type: "inserted" }); 723 | } else { 724 | node.marks = [{ type: "inserted" }]; 725 | } 726 | return node; 727 | }); 728 | }; 729 | 730 | Renderer.prototype.link = function (href, title, childNode) { 731 | var data = { 732 | href: href 733 | }; 734 | if (title) { 735 | data.title = title; 736 | } 737 | return { 738 | kind: "inline", 739 | type: "link", 740 | nodes: this.groupTextInRanges(childNode), 741 | data: data 742 | }; 743 | }; 744 | 745 | Renderer.prototype.image = function (href, title, alt) { 746 | var data = { 747 | src: href 748 | }; 749 | 750 | if (title) { 751 | data.title = title; 752 | } 753 | if (alt) { 754 | data.alt = alt; 755 | } 756 | 757 | return { 758 | kind: "block", 759 | type: "image", 760 | nodes: [{ 761 | kind: "text", 762 | ranges: [{ 763 | text: "" 764 | }] 765 | }], 766 | isVoid: true, 767 | data: data 768 | }; 769 | }; 770 | 771 | Renderer.prototype.text = function (childNode) { 772 | return new TextNode(childNode); 773 | }; 774 | 775 | // Auxiliary object constructors: 776 | function TextNode(text, marks) { 777 | this.text = text; 778 | if (marks) { 779 | this.marks = [marks]; 780 | } 781 | } 782 | 783 | /** 784 | * Parsing & Compiling 785 | */ 786 | 787 | function Parser(options) { 788 | this.tokens = []; 789 | this.token = null; 790 | this.options = assign({}, options || defaults); 791 | this.options.renderer = this.options.renderer || new Renderer(); 792 | this.renderer = this.options.renderer; 793 | this.renderer.options = this.options; 794 | } 795 | 796 | /** 797 | * Static Parse Method 798 | */ 799 | 800 | Parser.parse = function (src, options, renderer) { 801 | var parser = new Parser(options, renderer); 802 | return parser.parse(src); 803 | }; 804 | 805 | /** 806 | * Parse Loop 807 | */ 808 | 809 | Parser.prototype.parse = function (src) { 810 | this.inline = new InlineLexer(src.links, this.options, this.renderer); 811 | this.tokens = src.slice().reverse(); 812 | 813 | var out = []; 814 | while (this.next()) { 815 | out.push(this.tok()); 816 | } 817 | 818 | return out; 819 | }; 820 | 821 | /** 822 | * Next Token 823 | */ 824 | 825 | Parser.prototype.next = function () { 826 | return this.token = this.tokens.pop(); 827 | }; 828 | 829 | /** 830 | * Preview Next Token 831 | */ 832 | 833 | Parser.prototype.peek = function () { 834 | return this.tokens[this.tokens.length - 1] || 0; 835 | }; 836 | 837 | /** 838 | * Parse Text Tokens 839 | */ 840 | 841 | Parser.prototype.parseText = function () { 842 | var body = this.token.text; 843 | 844 | while (this.peek().type === 'text') { 845 | body += '\n' + this.next().text; 846 | } 847 | 848 | return this.inline.parse(body); 849 | }; 850 | 851 | /** 852 | * Parse Current Token 853 | */ 854 | 855 | Parser.prototype.tok = function () { 856 | switch (this.token.type) { 857 | case 'space': 858 | { 859 | return { 860 | "kind": "text", 861 | "ranges": [{ 862 | "text": "" 863 | }] 864 | }; 865 | } 866 | case 'hr': 867 | { 868 | return this.renderer.hr(); 869 | } 870 | case 'heading': 871 | { 872 | return this.renderer.heading(this.inline.parse(this.token.text), this.token.depth); 873 | } 874 | case 'code': 875 | { 876 | return this.renderer.code(this.token.text, this.token.lang); 877 | } 878 | case 'blockquote_start': 879 | { 880 | var body = []; 881 | 882 | while (this.next().type !== 'blockquote_end') { 883 | body.push(this.inline.parse(this.token.text)); 884 | } 885 | return this.renderer.blockquote(body); 886 | } 887 | case 'list_start': 888 | { 889 | var _body = []; 890 | var ordered = this.token.ordered; 891 | 892 | while (this.next().type !== 'list_end') { 893 | _body.push(this.tok()); 894 | } 895 | 896 | return this.renderer.list(_body, ordered); 897 | } 898 | case 'list_item_start': 899 | { 900 | var _body2 = []; 901 | 902 | while (this.next().type !== 'list_item_end') { 903 | _body2.push(this.token.type === 'text' ? this.parseText() : this.tok()); 904 | } 905 | 906 | return this.renderer.listitem(_body2); 907 | } 908 | case 'loose_item_start': 909 | { 910 | var _body3 = []; 911 | 912 | while (this.next().type !== 'list_item_end') { 913 | _body3.push(this.tok()); 914 | } 915 | 916 | return this.renderer.listitem(_body3); 917 | } 918 | case 'paragraph': 919 | { 920 | return this.renderer.paragraph(this.inline.parse(this.token.text)); 921 | } 922 | case 'text': 923 | { 924 | return this.renderer.text(this.parseText()); 925 | } 926 | } 927 | }; 928 | 929 | /** 930 | * Helpers 931 | */ 932 | 933 | function replace(regex, options) { 934 | regex = regex.source; 935 | options = options || ''; 936 | return function self(name, val) { 937 | if (!name) { 938 | return new RegExp(regex, options); 939 | } 940 | val = val.source || val; 941 | val = val.replace(/(^|[^\[])\^/g, '$1'); 942 | regex = regex.replace(name, val); 943 | return self; 944 | }; 945 | } 946 | 947 | var MarkdownParser = { 948 | parse: function parse(src, options) { 949 | options = assign({}, defaults, options); 950 | try { 951 | var fragment = Parser.parse(Lexer.parse(src, options), options); 952 | } catch (e) { 953 | if (options.silent) { 954 | fragment = [{ 955 | kind: "block", 956 | type: "paragraph", 957 | nodes: [{ 958 | kind: "text", 959 | ranges: [{ 960 | text: "An error occured:" 961 | }, { 962 | text: e.message 963 | }] 964 | }] 965 | }]; 966 | } else { 967 | throw e; 968 | } 969 | } 970 | var mainNode = { nodes: fragment }; 971 | return mainNode; 972 | } 973 | }; 974 | 975 | var classCallCheck = function (instance, Constructor) { 976 | if (!(instance instanceof Constructor)) { 977 | throw new TypeError("Cannot call a class as a function"); 978 | } 979 | }; 980 | 981 | var createClass = function () { 982 | function defineProperties(target, props) { 983 | for (var i = 0; i < props.length; i++) { 984 | var descriptor = props[i]; 985 | descriptor.enumerable = descriptor.enumerable || false; 986 | descriptor.configurable = true; 987 | if ("value" in descriptor) descriptor.writable = true; 988 | Object.defineProperty(target, descriptor.key, descriptor); 989 | } 990 | } 991 | 992 | return function (Constructor, protoProps, staticProps) { 993 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 994 | if (staticProps) defineProperties(Constructor, staticProps); 995 | return Constructor; 996 | }; 997 | }(); 998 | 999 | var toConsumableArray = function (arr) { 1000 | if (Array.isArray(arr)) { 1001 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 1002 | 1003 | return arr2; 1004 | } else { 1005 | return Array.from(arr); 1006 | } 1007 | }; 1008 | 1009 | /** 1010 | * String. 1011 | */ 1012 | 1013 | var String = new immutable.Record({ 1014 | kind: 'string', 1015 | text: '' 1016 | }); 1017 | 1018 | /** 1019 | * Rules to (de)serialize nodes. 1020 | * 1021 | * @type {Object} 1022 | */ 1023 | 1024 | var RULES = [{ 1025 | serialize: function serialize(obj, children) { 1026 | if (obj.kind == 'string') { 1027 | return children; 1028 | } 1029 | } 1030 | }, { 1031 | serialize: function serialize(obj, children) { 1032 | if (obj.kind != 'block') return; 1033 | 1034 | switch (obj.type) { 1035 | case 'paragraph': 1036 | return '\n' + children + '\n'; 1037 | case 'block-quote': 1038 | return '> ' + children + '\n'; 1039 | case 'bulleted-list': 1040 | return children; 1041 | case 'list-item': 1042 | return '* ' + children + '\n'; 1043 | case 'heading1': 1044 | return '# ' + children; 1045 | case 'heading2': 1046 | return '## ' + children; 1047 | case 'heading3': 1048 | return '### ' + children; 1049 | case 'heading4': 1050 | return '#### ' + children; 1051 | case 'heading5': 1052 | return '##### ' + children; 1053 | case 'heading6': 1054 | return '###### ' + children; 1055 | case 'heading6': 1056 | return '###### ' + children; 1057 | case 'horizontal-rule': 1058 | return '---'; 1059 | case 'image': 1060 | var title = obj.getIn(['data', 'title']); 1061 | var src = obj.getIn(['data', 'src']); 1062 | var alt = obj.getIn(['data', 'alt']); 1063 | return '![' + title + '](' + src + ' "' + alt + '")'; 1064 | } 1065 | } 1066 | }, { 1067 | serialize: function serialize(obj, children) { 1068 | if (obj.kind != 'inline') return; 1069 | switch (obj.type) { 1070 | case 'link': 1071 | return '[' + children + '](' + obj.getIn(['data', 'href']) + ')'; 1072 | } 1073 | } 1074 | }, 1075 | // Add a new rule that handles marks... 1076 | { 1077 | serialize: function serialize(obj, children) { 1078 | if (obj.kind != 'mark') return; 1079 | switch (obj.type) { 1080 | case 'bold': 1081 | return '**' + children + '**'; 1082 | case 'italic': 1083 | return '*' + children + '*'; 1084 | case 'code': 1085 | return '`' + children + '`'; 1086 | case 'inserted': 1087 | return '__' + children + '__'; 1088 | case 'deleted': 1089 | return '~~' + children + '~~'; 1090 | 1091 | } 1092 | } 1093 | }]; 1094 | 1095 | /** 1096 | * Markdown serializer. 1097 | * 1098 | * @type {Markdown} 1099 | */ 1100 | 1101 | var Markdown = function () { 1102 | 1103 | /** 1104 | * Create a new serializer with `rules`. 1105 | * 1106 | * @param {Object} options 1107 | * @property {Array} rules 1108 | * @return {Markdown} serializer 1109 | */ 1110 | 1111 | function Markdown() { 1112 | var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 1113 | classCallCheck(this, Markdown); 1114 | 1115 | this.rules = [].concat(toConsumableArray(options.rules || []), RULES); 1116 | 1117 | this.serializeNode = this.serializeNode.bind(this); 1118 | this.serializeRange = this.serializeRange.bind(this); 1119 | this.serializeString = this.serializeString.bind(this); 1120 | } 1121 | 1122 | /** 1123 | * Serialize a `state` object into an HTML string. 1124 | * 1125 | * @param {State} state 1126 | * @return {String} markdown 1127 | */ 1128 | 1129 | createClass(Markdown, [{ 1130 | key: 'serialize', 1131 | value: function serialize(state) { 1132 | var document = state.document; 1133 | 1134 | var elements = document.nodes.map(this.serializeNode); 1135 | 1136 | return elements.join('\n').trim(); 1137 | } 1138 | 1139 | /** 1140 | * Serialize a `node`. 1141 | * 1142 | * @param {Node} node 1143 | * @return {String} 1144 | */ 1145 | 1146 | }, { 1147 | key: 'serializeNode', 1148 | value: function serializeNode(node) { 1149 | if (node.kind == 'text') { 1150 | var ranges = node.getRanges(); 1151 | return ranges.map(this.serializeRange); 1152 | } 1153 | 1154 | var children = node.nodes.map(this.serializeNode); 1155 | children = children.flatten().length === 0 ? '' : children.flatten().join(''); 1156 | 1157 | var _iteratorNormalCompletion = true; 1158 | var _didIteratorError = false; 1159 | var _iteratorError = undefined; 1160 | 1161 | try { 1162 | for (var _iterator = this.rules[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 1163 | var rule = _step.value; 1164 | 1165 | if (!rule.serialize) continue; 1166 | var ret = rule.serialize(node, children); 1167 | if (ret) return ret; 1168 | } 1169 | } catch (err) { 1170 | _didIteratorError = true; 1171 | _iteratorError = err; 1172 | } finally { 1173 | try { 1174 | if (!_iteratorNormalCompletion && _iterator.return) { 1175 | _iterator.return(); 1176 | } 1177 | } finally { 1178 | if (_didIteratorError) { 1179 | throw _iteratorError; 1180 | } 1181 | } 1182 | } 1183 | } 1184 | 1185 | /** 1186 | * Serialize a `range`. 1187 | * 1188 | * @param {Range} range 1189 | * @return {String} 1190 | */ 1191 | 1192 | }, { 1193 | key: 'serializeRange', 1194 | value: function serializeRange(range) { 1195 | var _this = this; 1196 | 1197 | var string = new String({ text: range.text }); 1198 | var text = this.serializeString(string); 1199 | 1200 | return range.marks.reduce(function (children, mark) { 1201 | var _iteratorNormalCompletion2 = true; 1202 | var _didIteratorError2 = false; 1203 | var _iteratorError2 = undefined; 1204 | 1205 | try { 1206 | for (var _iterator2 = _this.rules[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 1207 | var rule = _step2.value; 1208 | 1209 | if (!rule.serialize) continue; 1210 | var ret = rule.serialize(mark, children); 1211 | if (ret) return ret; 1212 | } 1213 | } catch (err) { 1214 | _didIteratorError2 = true; 1215 | _iteratorError2 = err; 1216 | } finally { 1217 | try { 1218 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 1219 | _iterator2.return(); 1220 | } 1221 | } finally { 1222 | if (_didIteratorError2) { 1223 | throw _iteratorError2; 1224 | } 1225 | } 1226 | } 1227 | }, text); 1228 | } 1229 | 1230 | /** 1231 | * Serialize a `string`. 1232 | * 1233 | * @param {String} string 1234 | * @return {String} 1235 | */ 1236 | 1237 | }, { 1238 | key: 'serializeString', 1239 | value: function serializeString(string) { 1240 | var _iteratorNormalCompletion3 = true; 1241 | var _didIteratorError3 = false; 1242 | var _iteratorError3 = undefined; 1243 | 1244 | try { 1245 | for (var _iterator3 = this.rules[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { 1246 | var rule = _step3.value; 1247 | 1248 | if (!rule.serialize) continue; 1249 | var ret = rule.serialize(string, string.text); 1250 | if (ret) return ret; 1251 | } 1252 | } catch (err) { 1253 | _didIteratorError3 = true; 1254 | _iteratorError3 = err; 1255 | } finally { 1256 | try { 1257 | if (!_iteratorNormalCompletion3 && _iterator3.return) { 1258 | _iterator3.return(); 1259 | } 1260 | } finally { 1261 | if (_didIteratorError3) { 1262 | throw _iteratorError3; 1263 | } 1264 | } 1265 | } 1266 | } 1267 | 1268 | /** 1269 | * Deserialize a markdown `string`. 1270 | * 1271 | * @param {String} markdown 1272 | * @return {State} state 1273 | */ 1274 | 1275 | }, { 1276 | key: 'deserialize', 1277 | value: function deserialize(markdown) { 1278 | var nodes = MarkdownParser.parse(markdown); 1279 | var state = slate.Raw.deserialize(nodes, { terse: true }); 1280 | return state; 1281 | } 1282 | }]); 1283 | return Markdown; 1284 | }(); 1285 | 1286 | module.exports = Markdown; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slate-markdown-serializer", 3 | "version": "0.1.5", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "rollup -c" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/netlify/slate-markdown-serializer.git" 14 | }, 15 | "peerDependencies": { 16 | "immutable": "^3.8.1", 17 | "slate": "^0.11.0" 18 | }, 19 | "devDependencies": { 20 | "rollup": "^0.34.2", 21 | "rollup-plugin-babel": "^2.6.1", 22 | "babel-preset-es2015-rollup": "^1.1.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | export default { 4 | // tell rollup our main entry point 5 | entry: 'src/MarkdownRenderer.js', 6 | dest: 'lib/index.js', 7 | format: 'cjs', 8 | moduleName: 'MarkdownRenderer', 9 | plugins: [ 10 | babel({ 11 | exclude: 'node_modules/**', 12 | presets: 'es2015-rollup' 13 | }) 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/MarkdownRenderer.js: -------------------------------------------------------------------------------- 1 | import markdownparser from './markdownparser' 2 | import { Raw } from 'slate' 3 | import { Record } from 'immutable' 4 | 5 | 6 | /** 7 | * String. 8 | */ 9 | 10 | const String = new Record({ 11 | kind: 'string', 12 | text: '' 13 | }) 14 | 15 | /** 16 | * Rules to (de)serialize nodes. 17 | * 18 | * @type {Object} 19 | */ 20 | 21 | const RULES = [ 22 | { 23 | serialize(obj, children) { 24 | if (obj.kind == 'string') { 25 | return children 26 | } 27 | } 28 | }, 29 | { 30 | serialize(obj, children) { 31 | if (obj.kind != 'block') return 32 | 33 | switch (obj.type) { 34 | case 'paragraph': return `\n${children}\n` 35 | case 'block-quote': return `> ${children}\n` 36 | case 'bulleted-list': return children 37 | case 'list-item': return `* ${children}\n` 38 | case 'heading1': return `# ${children}` 39 | case 'heading2': return `## ${children}` 40 | case 'heading3': return `### ${children}` 41 | case 'heading4': return `#### ${children}` 42 | case 'heading5': return `##### ${children}` 43 | case 'heading6': return `###### ${children}` 44 | case 'heading6': return `###### ${children}` 45 | case 'horizontal-rule': return `---` 46 | case 'image': 47 | let title = obj.getIn(['data','title']); 48 | let src = obj.getIn(['data','src']); 49 | let alt = obj.getIn(['data','alt']); 50 | return `![${title}](${src} "${alt}")` 51 | } 52 | } 53 | }, 54 | { 55 | serialize(obj, children) { 56 | if (obj.kind != 'inline') return 57 | switch (obj.type) { 58 | case 'link': return `[${children}](${obj.getIn(['data','href'])})` 59 | } 60 | } 61 | }, 62 | // Add a new rule that handles marks... 63 | { 64 | serialize(obj, children) { 65 | if (obj.kind != 'mark') return 66 | switch (obj.type) { 67 | case 'bold': return `**${children}**` 68 | case 'italic': return `*${children}*` 69 | case 'code': return `\`${children}\`` 70 | case 'inserted': return `__${children}__` 71 | case 'deleted': return `~~${children}~~` 72 | 73 | } 74 | } 75 | } 76 | ] 77 | 78 | 79 | 80 | /** 81 | * Markdown serializer. 82 | * 83 | * @type {Markdown} 84 | */ 85 | 86 | class Markdown { 87 | 88 | /** 89 | * Create a new serializer with `rules`. 90 | * 91 | * @param {Object} options 92 | * @property {Array} rules 93 | * @return {Markdown} serializer 94 | */ 95 | 96 | constructor(options = {}) { 97 | this.rules = [ 98 | ...(options.rules || []), 99 | ...RULES 100 | ]; 101 | 102 | this.serializeNode = this.serializeNode.bind(this); 103 | this.serializeRange = this.serializeRange.bind(this); 104 | this.serializeString = this.serializeString.bind(this); 105 | } 106 | 107 | 108 | /** 109 | * Serialize a `state` object into an HTML string. 110 | * 111 | * @param {State} state 112 | * @return {String} markdown 113 | */ 114 | 115 | serialize(state) { 116 | const { document } = state 117 | const elements = document.nodes.map(this.serializeNode) 118 | 119 | return elements.join('\n').trim(); 120 | } 121 | 122 | /** 123 | * Serialize a `node`. 124 | * 125 | * @param {Node} node 126 | * @return {String} 127 | */ 128 | 129 | serializeNode(node) { 130 | if (node.kind == 'text') { 131 | const ranges = node.getRanges() 132 | return ranges.map(this.serializeRange) 133 | } 134 | 135 | let children = node.nodes.map(this.serializeNode); 136 | children = children.flatten().length === 0 ? '' : children.flatten().join('') 137 | 138 | for (const rule of this.rules) { 139 | if (!rule.serialize) continue 140 | const ret = rule.serialize(node, children) 141 | if (ret) return ret 142 | } 143 | } 144 | 145 | /** 146 | * Serialize a `range`. 147 | * 148 | * @param {Range} range 149 | * @return {String} 150 | */ 151 | 152 | serializeRange(range) { 153 | const string = new String({ text: range.text }) 154 | const text = this.serializeString(string) 155 | 156 | return range.marks.reduce((children, mark) => { 157 | for (const rule of this.rules) { 158 | if (!rule.serialize) continue 159 | const ret = rule.serialize(mark, children) 160 | if (ret) return ret 161 | } 162 | }, text); 163 | } 164 | 165 | /** 166 | * Serialize a `string`. 167 | * 168 | * @param {String} string 169 | * @return {String} 170 | */ 171 | 172 | serializeString(string) { 173 | for (const rule of this.rules) { 174 | if (!rule.serialize) continue 175 | const ret = rule.serialize(string, string.text) 176 | if (ret) return ret 177 | } 178 | } 179 | 180 | /** 181 | * Deserialize a markdown `string`. 182 | * 183 | * @param {String} markdown 184 | * @return {State} state 185 | */ 186 | deserialize(markdown) { 187 | const nodes = markdownparser.parse(markdown) 188 | const state = Raw.deserialize(nodes, { terse: true }) 189 | return state 190 | } 191 | 192 | 193 | } 194 | 195 | /** 196 | * Export. 197 | */ 198 | 199 | export default Markdown 200 | -------------------------------------------------------------------------------- /src/markdownparser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ported from: 3 | * https://github.com/chjj/marked/blob/49b7eaca/lib/marked.js 4 | * TODO: 5 | * Use ES6 classes 6 | * Add flow annotations 7 | */ 8 | /* eslint-disable no-spaced-func */ 9 | 10 | 11 | const hasOwnProperty = Object.prototype.hasOwnProperty; 12 | 13 | const assign = Object.assign || function(obj) { 14 | var i = 1; 15 | for (; i < arguments.length; i++) { 16 | var target = arguments[i]; 17 | for (var key in target) { 18 | if (hasOwnProperty.call(target, key)) { 19 | obj[key] = target[key]; 20 | } 21 | } 22 | } 23 | return obj; 24 | }; 25 | 26 | const flatten = function(ary){ 27 | return [].concat.apply([], ary) 28 | }; 29 | 30 | const noop = function() {}; 31 | noop.exec = noop; 32 | 33 | var defaults = { 34 | gfm: true, 35 | breaks: false, 36 | pedantic: false, 37 | smartLists: false, 38 | silent: false, 39 | langPrefix: 'lang-', 40 | renderer: new Renderer(), 41 | }; 42 | 43 | 44 | /** 45 | * Block-Level Grammar 46 | */ 47 | 48 | var block = { 49 | newline: /^\n+/, 50 | code: /^( {4}[^\n]+\n*)+/, 51 | fences: noop, 52 | hr: /^( *[-*_]){3,} *(?:\n+|$)/, 53 | heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, 54 | nptable: noop, 55 | lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, 56 | blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, 57 | list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, 58 | def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, 59 | paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|def))+)\n*/, 60 | text: /^[^\n]+/, 61 | }; 62 | 63 | block.bullet = /(?:[*+-]|\d+\.)/; 64 | block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; 65 | block.item = replace(block.item, 'gm') 66 | (/bull/g, block.bullet) 67 | (); 68 | 69 | block.list = replace(block.list) 70 | (/bull/g, block.bullet) 71 | ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') 72 | ('def', '\\n+(?=' + block.def.source + ')') 73 | (); 74 | 75 | block.blockquote = replace(block.blockquote) 76 | ('def', block.def) 77 | (); 78 | 79 | block.paragraph = replace(block.paragraph) 80 | ('hr', block.hr) 81 | ('heading', block.heading) 82 | ('lheading', block.lheading) 83 | ('blockquote', block.blockquote) 84 | ('def', block.def) 85 | (); 86 | 87 | /** 88 | * Normal Block Grammar 89 | */ 90 | 91 | block.normal = assign({}, block); 92 | 93 | /** 94 | * GFM Block Grammar 95 | */ 96 | 97 | block.gfm = assign({}, block.normal, { 98 | fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, 99 | paragraph: /^/, 100 | heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/, 101 | }); 102 | 103 | block.gfm.paragraph = replace(block.paragraph) 104 | ('(?!', '(?!' 105 | + block.gfm.fences.source.replace('\\1', '\\2') + '|' 106 | + block.list.source.replace('\\1', '\\3') + '|') 107 | (); 108 | 109 | /** 110 | * Block Lexer 111 | */ 112 | 113 | function Lexer(options) { 114 | this.tokens = []; 115 | this.tokens.links = {}; 116 | this.options = assign({}, options || defaults); 117 | this.rules = block.normal; 118 | 119 | if (this.options.gfm) { 120 | this.rules = block.gfm; 121 | } 122 | } 123 | 124 | /** 125 | * Expose Block Rules 126 | */ 127 | 128 | Lexer.rules = block; 129 | 130 | /** 131 | * Static Lex Method 132 | */ 133 | 134 | Lexer.parse = function(src, options) { 135 | var lexer = new Lexer(options); 136 | return lexer.parse(src); 137 | }; 138 | 139 | /** 140 | * Preprocessing 141 | */ 142 | 143 | Lexer.prototype.parse = function(src) { 144 | src = src 145 | .replace(/\r\n|\r/g, '\n') 146 | .replace(/\t/g, ' ') 147 | .replace(/\u00a0/g, ' ') 148 | .replace(/\u2424/g, '\n'); 149 | 150 | return this.token(src, true); 151 | }; 152 | 153 | /** 154 | * Lexing 155 | */ 156 | 157 | Lexer.prototype.token = function(src, top, bq) { 158 | var next; 159 | var loose; 160 | var cap; 161 | var bull; 162 | var b; 163 | var item; 164 | var space; 165 | var i; 166 | var l; 167 | 168 | src = src.replace(/^ +$/gm, ''); 169 | 170 | while (src) { 171 | // newline 172 | if ((cap = this.rules.newline.exec(src))) { 173 | src = src.substring(cap[0].length); 174 | if (cap[0].length > 1) { 175 | this.tokens.push({ 176 | type: 'space', 177 | }); 178 | } 179 | } 180 | 181 | // code 182 | if ((cap = this.rules.code.exec(src))) { 183 | src = src.substring(cap[0].length); 184 | cap = cap[0].replace(/^ {4}/gm, ''); 185 | this.tokens.push({ 186 | type: 'code', 187 | text: !this.options.pedantic ? cap.replace(/\n+$/, '') : cap, 188 | }); 189 | continue; 190 | } 191 | 192 | // fences (gfm) 193 | if ((cap = this.rules.fences.exec(src))) { 194 | src = src.substring(cap[0].length); 195 | this.tokens.push({ 196 | type: 'code', 197 | lang: cap[2], 198 | text: cap[3], 199 | }); 200 | continue; 201 | } 202 | 203 | // heading 204 | if ((cap = this.rules.heading.exec(src))) { 205 | src = src.substring(cap[0].length); 206 | this.tokens.push({ 207 | type: 'heading', 208 | depth: cap[1].length, 209 | text: cap[2], 210 | }); 211 | continue; 212 | } 213 | 214 | // lheading 215 | if ((cap = this.rules.lheading.exec(src))) { 216 | src = src.substring(cap[0].length); 217 | this.tokens.push({ 218 | type: 'heading', 219 | depth: cap[2] === '=' ? 1 : 2, 220 | text: cap[1], 221 | }); 222 | continue; 223 | } 224 | 225 | // hr 226 | if ((cap = this.rules.hr.exec(src))) { 227 | src = src.substring(cap[0].length); 228 | this.tokens.push({ 229 | type: 'hr', 230 | }); 231 | continue; 232 | } 233 | 234 | // blockquote 235 | if ((cap = this.rules.blockquote.exec(src))) { 236 | src = src.substring(cap[0].length); 237 | 238 | this.tokens.push({ 239 | type: 'blockquote_start', 240 | }); 241 | 242 | cap = cap[0].replace(/^ *> ?/gm, ''); 243 | 244 | // Pass `top` to keep the current 245 | // "toplevel" state. This is exactly 246 | // how markdown.pl works. 247 | this.token(cap, top, true); 248 | 249 | this.tokens.push({ 250 | type: 'blockquote_end', 251 | }); 252 | 253 | continue; 254 | } 255 | 256 | // list 257 | if ((cap = this.rules.list.exec(src))) { 258 | src = src.substring(cap[0].length); 259 | bull = cap[2]; 260 | 261 | this.tokens.push({ 262 | type: 'list_start', 263 | ordered: bull.length > 1, 264 | }); 265 | 266 | // Get each top-level item. 267 | cap = cap[0].match(this.rules.item); 268 | 269 | next = false; 270 | l = cap.length; 271 | i = 0; 272 | 273 | for (; i < l; i++) { 274 | item = cap[i]; 275 | 276 | // Remove the list item's bullet 277 | // so it is seen as the next token. 278 | space = item.length; 279 | item = item.replace(/^ *([*+-]|\d+\.) +/, ''); 280 | 281 | // Outdent whatever the 282 | // list item contains. Hacky. 283 | if (~item.indexOf('\n ')) { 284 | space -= item.length; 285 | item = !this.options.pedantic 286 | ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') 287 | : item.replace(/^ {1,4}/gm, ''); 288 | } 289 | 290 | // Determine whether the next list item belongs here. 291 | // Backpedal if it does not belong in this list. 292 | if (this.options.smartLists && i !== l - 1) { 293 | b = block.bullet.exec(cap[i + 1])[0]; 294 | if (bull !== b && !(bull.length > 1 && b.length > 1)) { 295 | src = cap.slice(i + 1).join('\n') + src; 296 | i = l - 1; 297 | } 298 | } 299 | 300 | // Determine whether item is loose or not. 301 | // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ 302 | // for discount behavior. 303 | loose = next || /\n\n(?!\s*$)/.test(item); 304 | if (i !== l - 1) { 305 | next = item.charAt(item.length - 1) === '\n'; 306 | if (!loose) { 307 | loose = next; 308 | } 309 | } 310 | 311 | this.tokens.push({ 312 | type: loose ? 'loose_item_start' : 'list_item_start', 313 | }); 314 | 315 | // Recurse. 316 | this.token(item, false, bq); 317 | 318 | this.tokens.push({ 319 | type: 'list_item_end', 320 | }); 321 | } 322 | 323 | this.tokens.push({ 324 | type: 'list_end', 325 | }); 326 | 327 | continue; 328 | } 329 | 330 | // def 331 | if ((!bq && top) && (cap = this.rules.def.exec(src))) { 332 | src = src.substring(cap[0].length); 333 | this.tokens.links[cap[1].toLowerCase()] = { 334 | href: cap[2], 335 | title: cap[3], 336 | }; 337 | continue; 338 | } 339 | 340 | // top-level paragraph 341 | if (top && (cap = this.rules.paragraph.exec(src))) { 342 | src = src.substring(cap[0].length); 343 | this.tokens.push({ 344 | type: 'paragraph', 345 | text: cap[1].charAt(cap[1].length - 1) === '\n' 346 | ? cap[1].slice(0, -1) 347 | : cap[1], 348 | }); 349 | continue; 350 | } 351 | 352 | // text 353 | if ((cap = this.rules.text.exec(src))) { 354 | // Top-level should never reach here. 355 | src = src.substring(cap[0].length); 356 | this.tokens.push({ 357 | type: 'text', 358 | text: cap[0], 359 | }); 360 | continue; 361 | } 362 | 363 | if (src) { 364 | throw new 365 | Error('Infinite loop on byte: ' + src.charCodeAt(0)); 366 | } 367 | } 368 | 369 | return this.tokens; 370 | }; 371 | 372 | /** 373 | * Inline-Level Grammar 374 | */ 375 | 376 | var inline = { 377 | escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, 378 | link: /^!?\[(inside)\]\(href\)/, 379 | reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, 380 | nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, 381 | strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, 382 | em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, 383 | code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, 384 | br: /^ {2,}\n(?!\s*$)/, 385 | del: noop, 386 | ins: noop, 387 | text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; 392 | 393 | inline.link = replace(inline.link) 394 | ('inside', inline._inside) 395 | ('href', inline._href) 396 | (); 397 | 398 | inline.reflink = replace(inline.reflink) 399 | ('inside', inline._inside) 400 | (); 401 | 402 | /** 403 | * Normal Inline Grammar 404 | */ 405 | 406 | inline.normal = assign({}, inline); 407 | 408 | /** 409 | * Pedantic Inline Grammar 410 | */ 411 | 412 | inline.pedantic = assign({}, inline.normal, { 413 | strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, 414 | em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, 415 | }); 416 | 417 | /** 418 | * GFM Inline Grammar 419 | */ 420 | 421 | inline.gfm = assign({}, inline.normal, { 422 | escape: replace(inline.escape)('])', '~|])')(), 423 | del: /^~~(?=\S)([\s\S]*?\S)~~/, 424 | ins: /^\+\+(?=\S)([\s\S]*?\S)\+\+/, 425 | text: replace(inline.text)(']|', '~+]|')(), 426 | }); 427 | 428 | /** 429 | * GFM + Line Breaks Inline Grammar 430 | */ 431 | 432 | inline.breaks = assign({}, inline.gfm, { 433 | br: replace(inline.br)('{2,}', '*')(), 434 | text: replace(inline.gfm.text)('{2,}', '*')(), 435 | }); 436 | 437 | /** 438 | * Inline Lexer & Compiler 439 | */ 440 | 441 | function InlineLexer(links, options) { 442 | this.options = assign({}, options || defaults); 443 | this.links = links; 444 | this.rules = inline.normal; 445 | this.renderer = this.options.renderer || new Renderer(); 446 | this.renderer.options = this.options; 447 | 448 | if (!this.links) { 449 | throw new 450 | Error('Tokens array requires a `links` property.'); 451 | } 452 | 453 | if (this.options.gfm) { 454 | if (this.options.breaks) { 455 | this.rules = inline.breaks; 456 | } else { 457 | this.rules = inline.gfm; 458 | } 459 | } else if (this.options.pedantic) { 460 | this.rules = inline.pedantic; 461 | } 462 | } 463 | 464 | /** 465 | * Expose Inline Rules 466 | */ 467 | 468 | InlineLexer.rules = inline; 469 | 470 | /** 471 | * Static Lexing/Compiling Method 472 | */ 473 | 474 | InlineLexer.parse = function(src, links, options) { 475 | var inline = new InlineLexer(links, options); 476 | return inline.parse(src); 477 | }; 478 | 479 | /** 480 | * Lexing/Compiling 481 | */ 482 | 483 | InlineLexer.prototype.parse = function(src) { 484 | var out = [] 485 | var link; 486 | var cap; 487 | 488 | while (src) { 489 | // escape 490 | if ((cap = this.rules.escape.exec(src))) { 491 | src = src.substring(cap[0].length); 492 | out.push({ 493 | kind: "text", 494 | ranges: [ 495 | { 496 | text: cap[1] 497 | } 498 | ] 499 | }); 500 | continue; 501 | } 502 | 503 | // link 504 | if ((cap = this.rules.link.exec(src))) { 505 | src = src.substring(cap[0].length); 506 | this.inLink = true; 507 | out.push(this.outputLink(cap, {href: cap[2], title: cap[3]})); 508 | this.inLink = false; 509 | continue; 510 | } 511 | 512 | // reflink, nolink 513 | // TODO 514 | if ((cap = this.rules.reflink.exec(src)) 515 | || (cap = this.rules.nolink.exec(src))) { 516 | src = src.substring(cap[0].length); 517 | link = (cap[2] || cap[1]).replace(/\s+/g, ' '); 518 | link = this.links[link.toLowerCase()]; 519 | if (!link || !link.href) { 520 | out.push( 521 | { 522 | kind: "text", 523 | ranges: [ 524 | { 525 | text: cap[0].charAt(0) 526 | } 527 | ] 528 | } 529 | ) 530 | src = cap[0].substring(1) + src; 531 | continue; 532 | } 533 | this.inLink = true; 534 | out.push(this.outputLink(cap, link)); 535 | this.inLink = false; 536 | continue; 537 | } 538 | 539 | // strong 540 | if ((cap = this.rules.strong.exec(src))) { 541 | src = src.substring(cap[0].length); 542 | out.push(this.renderer.strong(this.parse(cap[2] || cap[1]))); 543 | continue; 544 | } 545 | 546 | // em 547 | if ((cap = this.rules.em.exec(src))) { 548 | src = src.substring(cap[0].length); 549 | out.push(this.renderer.em(this.parse(cap[2] || cap[1]))); 550 | continue; 551 | } 552 | 553 | // code 554 | if ((cap = this.rules.code.exec(src))) { 555 | src = src.substring(cap[0].length); 556 | out.push(this.renderer.codespan(cap[2])); 557 | continue; 558 | } 559 | 560 | // br 561 | if ((cap = this.rules.br.exec(src))) { 562 | src = src.substring(cap[0].length); 563 | out.push(this.renderer.br()); 564 | continue; 565 | } 566 | 567 | // del (gfm) 568 | if ((cap = this.rules.del.exec(src))) { 569 | src = src.substring(cap[0].length); 570 | out.push(this.renderer.del(this.parse(cap[1]))); 571 | continue; 572 | } 573 | 574 | // ins (gfm extended) 575 | if ((cap = this.rules.ins.exec(src))) { 576 | src = src.substring(cap[0].length); 577 | out.push(this.renderer.ins(this.parse(cap[1]))); 578 | continue; 579 | } 580 | 581 | // text 582 | if ((cap = this.rules.text.exec(src))) { 583 | src = src.substring(cap[0].length); 584 | out.push(this.renderer.text(cap[0])) 585 | continue; 586 | } 587 | 588 | if (src) { 589 | throw new 590 | Error('Infinite loop on byte: ' + src.charCodeAt(0)); 591 | } 592 | } 593 | 594 | return out; 595 | }; 596 | 597 | /** 598 | * Compile Link 599 | */ 600 | 601 | InlineLexer.prototype.outputLink = function(cap, link) { 602 | var href = link.href; 603 | var title = link.title; 604 | 605 | return cap[0].charAt(0) !== '!' 606 | ? this.renderer.link(href, title, this.parse(cap[1])) 607 | : this.renderer.image(href, title, cap[1]); 608 | }; 609 | 610 | /** 611 | * Renderer 612 | */ 613 | 614 | function Renderer(options) { 615 | this.options = options || {}; 616 | } 617 | 618 | Renderer.prototype.groupTextInRanges = function(childNode) { 619 | let node = flatten(childNode) 620 | return node.reduce((acc, current) => { 621 | 622 | if(current instanceof TextNode){ 623 | let accLast = acc.length - 1; 624 | if(accLast >= 0 && acc[accLast] && acc[accLast]['kind'] === 'text'){ 625 | // If the previous item was a text kind, push the current text to it's range 626 | acc[accLast].ranges.push(current) 627 | return acc; 628 | } else { 629 | // Else, create a new text kind 630 | acc.push({ 631 | kind: "text", 632 | ranges: [current] 633 | }) 634 | return acc; 635 | } 636 | } else { 637 | acc.push(current) 638 | return acc; 639 | } 640 | }, []) 641 | }; 642 | 643 | Renderer.prototype.code = function(childNode, lang) { 644 | var data = {}; 645 | if (lang) { 646 | data.language = this.options.langPrefix + lang; 647 | } 648 | return { 649 | kind: "text", 650 | ranges: [ 651 | { 652 | text: childNode, 653 | marks: [ 654 | { 655 | type: "code", 656 | data: data 657 | } 658 | ] 659 | } 660 | ] 661 | } 662 | }; 663 | 664 | Renderer.prototype.blockquote = function(childNode) { 665 | return { 666 | kind: "block", 667 | type: "block-quote", 668 | nodes: this.groupTextInRanges(childNode) 669 | } 670 | }; 671 | 672 | Renderer.prototype.heading = function(childNode, level) { 673 | return { 674 | kind: "block", 675 | type: "heading" + level, 676 | nodes: this.groupTextInRanges(childNode) 677 | } 678 | }; 679 | 680 | Renderer.prototype.hr = function() { 681 | return { 682 | kind: "block", 683 | type: "horizontal-rule", 684 | nodes: [ 685 | { 686 | kind: "text", 687 | ranges: [ 688 | { 689 | text: '' 690 | } 691 | ] 692 | } 693 | ], 694 | isVoid: true, 695 | } 696 | }; 697 | 698 | Renderer.prototype.list = function(childNode, isOrdered) { 699 | var type = isOrdered ? 'numbered-list' : 'bulleted-list' 700 | return { 701 | kind: "block", 702 | type: type, 703 | nodes: this.groupTextInRanges(childNode) 704 | } 705 | }; 706 | 707 | Renderer.prototype.listitem = function(childNode) { 708 | return { 709 | kind: "block", 710 | type: "list-item", 711 | nodes: this.groupTextInRanges(childNode) 712 | } 713 | }; 714 | 715 | Renderer.prototype.paragraph = function(childNode) { 716 | return { 717 | kind: "block", 718 | type: "paragraph", 719 | nodes: this.groupTextInRanges(childNode) 720 | } 721 | }; 722 | 723 | // span level renderer 724 | Renderer.prototype.strong = function(childNode) { 725 | return childNode.map(node => { 726 | if(node.marks) { 727 | node.marks.push({type: "bold"}) 728 | } else { 729 | node.marks = [{type: "bold"}]; 730 | } 731 | return node; 732 | }) 733 | }; 734 | 735 | Renderer.prototype.em = function(childNode) { 736 | return childNode.map(node => { 737 | if(node.marks) { 738 | node.marks.push({type: "italic"}) 739 | } else { 740 | node.marks = [{type: "italic"}]; 741 | } 742 | return node; 743 | }) 744 | }; 745 | 746 | Renderer.prototype.codespan = function(text) { 747 | return new TextNode(text, {"type": "code"}) 748 | }; 749 | 750 | Renderer.prototype.br = function() { 751 | return { 752 | text: '--', 753 | } 754 | }; 755 | 756 | Renderer.prototype.del = function(childNode) { 757 | return childNode.map(node => { 758 | if(node.marks) { 759 | node.marks.push({type: "deleted"}) 760 | } else { 761 | node.marks = [{type: "deleted"}]; 762 | } 763 | return node; 764 | }) 765 | }; 766 | 767 | Renderer.prototype.ins = function(childNode) { 768 | return childNode.map(node => { 769 | if(node.marks) { 770 | node.marks.push({type: "inserted"}) 771 | } else { 772 | node.marks = [{type: "inserted"}]; 773 | } 774 | return node; 775 | }) 776 | }; 777 | 778 | Renderer.prototype.link = function(href, title, childNode) { 779 | var data = { 780 | href: href, 781 | }; 782 | if (title) { 783 | data.title = title; 784 | } 785 | return { 786 | kind: "inline", 787 | type: "link", 788 | nodes: this.groupTextInRanges(childNode), 789 | data: data 790 | } 791 | }; 792 | 793 | Renderer.prototype.image = function(href, title, alt) { 794 | var data = { 795 | src: href 796 | } 797 | 798 | if (title) { 799 | data.title = title; 800 | } 801 | if (alt) { 802 | data.alt = alt; 803 | } 804 | 805 | return { 806 | kind: "block", 807 | type: "image", 808 | nodes: [ 809 | { 810 | kind: "text", 811 | ranges: [ 812 | { 813 | text: "" 814 | } 815 | ] 816 | } 817 | ], 818 | isVoid: true, 819 | data: data 820 | } 821 | }; 822 | 823 | Renderer.prototype.text = function(childNode) { 824 | return new TextNode(childNode); 825 | }; 826 | 827 | // Auxiliary object constructors: 828 | function TextNode(text, marks) { 829 | this.text = text; 830 | if(marks){ 831 | this.marks = [marks] 832 | } 833 | } 834 | 835 | /** 836 | * Parsing & Compiling 837 | */ 838 | 839 | function Parser(options) { 840 | this.tokens = []; 841 | this.token = null; 842 | this.options = assign({}, options || defaults); 843 | this.options.renderer = this.options.renderer || new Renderer(); 844 | this.renderer = this.options.renderer; 845 | this.renderer.options = this.options; 846 | } 847 | 848 | /** 849 | * Static Parse Method 850 | */ 851 | 852 | Parser.parse = function(src, options, renderer) { 853 | var parser = new Parser(options, renderer); 854 | return parser.parse(src); 855 | }; 856 | 857 | /** 858 | * Parse Loop 859 | */ 860 | 861 | Parser.prototype.parse = function(src) { 862 | this.inline = new InlineLexer(src.links, this.options, this.renderer); 863 | this.tokens = src.slice().reverse(); 864 | 865 | var out = []; 866 | while (this.next()) { 867 | out.push(this.tok()); 868 | } 869 | 870 | return out; 871 | }; 872 | 873 | /** 874 | * Next Token 875 | */ 876 | 877 | Parser.prototype.next = function() { 878 | return (this.token = this.tokens.pop()); 879 | }; 880 | 881 | /** 882 | * Preview Next Token 883 | */ 884 | 885 | Parser.prototype.peek = function() { 886 | return this.tokens[this.tokens.length - 1] || 0; 887 | }; 888 | 889 | /** 890 | * Parse Text Tokens 891 | */ 892 | 893 | Parser.prototype.parseText = function() { 894 | var body = this.token.text; 895 | 896 | while (this.peek().type === 'text') { 897 | body += '\n' + this.next().text; 898 | } 899 | 900 | return this.inline.parse(body); 901 | }; 902 | 903 | /** 904 | * Parse Current Token 905 | */ 906 | 907 | Parser.prototype.tok = function() { 908 | switch (this.token.type) { 909 | case 'space': { 910 | return { 911 | "kind": "text", 912 | "ranges": [ 913 | { 914 | "text": "" 915 | } 916 | ] 917 | } 918 | } 919 | case 'hr': { 920 | return this.renderer.hr(); 921 | } 922 | case 'heading': { 923 | return this.renderer.heading( 924 | this.inline.parse(this.token.text), 925 | this.token.depth 926 | ); 927 | } 928 | case 'code': { 929 | return this.renderer.code( 930 | this.token.text, 931 | this.token.lang 932 | ); 933 | } 934 | case 'blockquote_start': { 935 | let body = []; 936 | 937 | while (this.next().type !== 'blockquote_end') { 938 | body.push(this.inline.parse(this.token.text)); 939 | } 940 | return this.renderer.blockquote(body); 941 | } 942 | case 'list_start': { 943 | let body = []; 944 | var ordered = this.token.ordered; 945 | 946 | while (this.next().type !== 'list_end') { 947 | body.push(this.tok()); 948 | } 949 | 950 | return this.renderer.list(body, ordered); 951 | } 952 | case 'list_item_start': { 953 | let body = []; 954 | 955 | while (this.next().type !== 'list_item_end') { 956 | body.push(this.token.type === 'text' 957 | ? this.parseText() 958 | : this.tok()); 959 | } 960 | 961 | return this.renderer.listitem(body); 962 | } 963 | case 'loose_item_start': { 964 | let body = []; 965 | 966 | while (this.next().type !== 'list_item_end') { 967 | body.push(this.tok()); 968 | } 969 | 970 | return this.renderer.listitem(body); 971 | } 972 | case 'paragraph': { 973 | return this.renderer.paragraph(this.inline.parse(this.token.text)); 974 | } 975 | case 'text': { 976 | return this.renderer.text(this.parseText()); 977 | } 978 | } 979 | }; 980 | 981 | /** 982 | * Helpers 983 | */ 984 | 985 | function replace(regex, options) { 986 | regex = regex.source; 987 | options = options || ''; 988 | return function self(name, val) { 989 | if (!name) { 990 | return new RegExp(regex, options); 991 | } 992 | val = val.source || val; 993 | val = val.replace(/(^|[^\[])\^/g, '$1'); 994 | regex = regex.replace(name, val); 995 | return self; 996 | }; 997 | } 998 | 999 | const MarkdownParser = { 1000 | parse(src, options) { 1001 | options = assign({}, defaults, options); 1002 | try { 1003 | var fragment = Parser.parse(Lexer.parse(src, options), options); 1004 | } catch (e) { 1005 | if (options.silent) { 1006 | fragment = [{ 1007 | kind: "block", 1008 | type: "paragraph", 1009 | nodes: [ 1010 | { 1011 | kind: "text", 1012 | ranges: [ 1013 | { 1014 | text: "An error occured:" 1015 | }, 1016 | { 1017 | text: e.message 1018 | } 1019 | ] 1020 | } 1021 | ] 1022 | }]; 1023 | } else { 1024 | throw e; 1025 | } 1026 | } 1027 | let mainNode = {nodes: fragment}; 1028 | return mainNode; 1029 | }, 1030 | }; 1031 | 1032 | export default MarkdownParser; 1033 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/slate-markdown-serializer/85a4e67bf9f6dca0d3d79416aa3d56332134ecf0/test/.gitkeep --------------------------------------------------------------------------------