this repo has no description
at cactus 1094 lines 26 kB view raw
1"use strict" 212||+typeof await/2//2; export default 3/** 4 Legacy parsers factory 5 @implements Parser_Collection 6*/ 7class Markup_Legacy { constructor() { 8 /** 9 @type {Object<string,Parser>} 10 @property {Parser} 12y - 12y parser 11 @property {Parser} bbcode - bbcode parser 12 @property {Parser} plaintext - plaintext parser/autolinker 13 */ 14 this.langs = {} 15 16 const BLOCKS = Object.freeze({__proto__:null, divider: 1, code: 1, audio: 1, video: 1, youtube: 1, heading: 1, quote: 1, list: 1, list_item: 1, table: 1, table_row: 1, image: 1, error: 1, align: 1, spoiler: 1}) 17 18 function convert_cell_args(props, h) { 19 let args = { 20 header: props.h || h, 21 colspan: props.cs, // TODO: validate 22 rowspan: props.rs, 23 align: props.align, 24 color: props.c, 25 } 26 if (props.c && props.c[0]=='#') 27 args.truecolor = props.c 28 return args 29 } 30 31 /*********** 32 ** STATE ** 33 ***********/ 34 let c, i, code 35 let skipNextLineBreak 36 let textBuffer 37 let curr, tree 38 let openBlocks 39 let stack 40 41 let startOfLine 42 function lineStart() { 43 startOfLine = true 44 } 45 function scan() { 46 if ("\n"===c || !c) 47 lineStart() 48 else if (" "!==c) 49 startOfLine = false 50 i++ 51 c = code.charAt(i) 52 } 53 function stack_top() { 54 return stack[stack.length-1] 55 } 56 57 function init(text) { 58 code = text 59 openBlocks = 0 60 startOfLine = true 61 skipNextLineBreak = false 62 textBuffer = "" 63 tree = curr = {type: 'ROOT', content: []} 64 stack = [{node: curr, type: 'ROOT'}] 65 restore(0) 66 } 67 // move to pos 68 function restore(pos) { 69 i = pos-1 70 scan() 71 } 72 73 //try to read a char 74 function eatChar(chr) { 75 if (c===chr) { 76 scan() 77 return true 78 } 79 } 80 81 // read a url 82 // if `allow` is true, url is only ended by end of file or ]] or ][ (TODO) 83 function readUrl(allow) { 84 let start = i 85 let depth = 0 86 if (allow) 87 while (c) { 88 if (eatChar("[")) { 89 depth++ 90 } else if ("]"===c) { 91 depth-- 92 if (depth<0) 93 break 94 scan() 95 } else 96 scan() 97 } 98 else { 99 while (c) { 100 if (/[-\w$.+!*',;/?:@=&#%~]/.test(c)) { 101 scan() 102 } else if (eatChar("(")) { 103 depth++ 104 } else if (")"===c) { 105 depth-- 106 if (depth < 0) 107 break 108 scan() 109 } else 110 break 111 } 112 if (/[,.?!:]/.test(code.charAt(i-1))) { 113 i -= 2 114 scan() 115 } 116 } 117 return code.substring(start, i) 118 } 119 120 /*********** 121 ** stack ** 122 ***********/ 123 function stackContains(type) { 124 return stack.some(x=>x.type==type) 125 } 126 function top_is(type) { 127 let top = stack_top() 128 return top && top.type===type 129 } 130 131 /**************** 132 ** outputting ** 133 ****************/ 134 function endBlock() { 135 flushText() 136 let item = stack.pop() 137 if (item.isBlock) 138 skipNextLineBreak = true 139 140 if (stack.length) { 141 let i = stack.length-1 142 // this skips {} fake nodes 143 // it will always find at least the root element I hope 144 while (!stack[i].node) 145 i-- 146 curr = stack[i].node 147 openBlocks-- 148 } else { 149 curr = null 150 } 151 } 152 153 // output contents of text buffer 154 function flushText() { 155 if (textBuffer) { 156 curr.content.push(textBuffer) 157 textBuffer = "" 158 } 159 } 160 161 // add linebreak to output 162 // todo: skipping linebreaks should skip / *\n? */ (spaces before/after!) 163 // so like [h1]test[/h1] [h2]test[/h2] 164 // no extra linebreak there 165 function addLineBreak() { 166 if (skipNextLineBreak) 167 skipNextLineBreak = false 168 else 169 addText("\n") 170 } 171 172 // add text to output (buffered) 173 function addText(text) { 174 if (text) { 175 textBuffer += text 176 skipNextLineBreak = false 177 } 178 } 179 180 // call at end of parsing to flush output 181 function endAll() { 182 flushText() 183 while (stack.length) 184 endBlock() 185 openBlocks = code = stack = curr = null // memory leak ... 186 } 187 188 function add_block(type, args) { 189 flushText() 190 curr.content.push({type, args}) 191 skipNextLineBreak = BLOCKS[type] 192 } 193 194 function start_block(type, args, data) { 195 //let type = data.type 196 let node = {type, args, content: []} 197 data.type = type 198 openBlocks++ 199 if (openBlocks > 10) 200 throw new Error("too deep nestted blocks") 201 data.node = node 202 if (BLOCKS[type]) { 203 data.isBlock = true 204 skipNextLineBreak = true 205 } 206 flushText() 207 curr.content.push(node) 208 curr = node 209 210 stack.push(data) 211 return data 212 } 213 214 const URL_RX = /\b(https?:[/][/]|sbs:)/y 215 216 // check for /\b(http://|https://|sbs:)/ basically 217 function isUrlStart() { 218 URL_RX.lastIndex = i 219 return URL_RX.test(code) 220 } 221 222 const FR = /(?:(?!https?:\/\/|sbs:)[^\n\\{}*/_~>\]|`![-])+/y 223 224 this.langs['12y'] = function(codeInput) { 225 init(codeInput) 226 curr.lang = '12y' 227 if (!codeInput) 228 return tree 229 230 while (c) { 231 FR.lastIndex = i 232 let m = FR.exec(code) 233 if (m) { 234 addText(m[0]) 235 restore(FR.lastIndex) 236 } else if (eatChar("\n")) { 237 endLine() 238 //========== 239 // \ escape 240 } else if (eatChar("\\")) { 241 addText(c) 242 scan() 243 //=============== 244 // { group start (why did I call these "groups"?) 245 } else if ("{"===c) { 246 readEnv() 247 //============= 248 // } group end 249 } else if (eatChar("}")) { 250 if (stackContains(undefined)) 251 closeAll(false) 252 else 253 addText("}") 254 //================ 255 // * heading/bold 256 } else if ("*"===c) { 257 if (startOfLine && ("*"===code[i+1] || " "===code[i+1])) { 258 let headingLevel = 0 259 while (eatChar("*")) 260 headingLevel++ 261 if (headingLevel > 3) 262 headingLevel = 3 263 264 if (eatChar(" ")) 265 start_block('heading', {level: headingLevel}, {}) 266 else 267 addText("*".repeat(headingLevel)) 268 } else { 269 doMarkup('bold') 270 } 271 } else if ("/"===c) { 272 doMarkup('italic') 273 } else if ("_"===c) { 274 doMarkup('underline') 275 } else if ("~"===c) { 276 doMarkup('strikethrough') 277 //============ 278 // >... quote 279 } else if (startOfLine && eatChar(">")) { 280 start_block('quote', {cite: null}, {}) 281 //============== 282 // -... list/hr 283 } else if (startOfLine && eatChar("-")) { 284 //textBuffer = "" //hack: /// what the heck why did i do this *travelling to 2019 and sneaking up behind myself and pushing myself down the stairs* 285 // it used to work since textbuffer got flushed at EOL... 286 textBuffer = textBuffer.replace(/ +$/, "") 287 //---------- 288 // --... hr 289 if (eatChar("-")) { 290 let count = 2 291 while (eatChar("-")) 292 count++ 293 //------------- 294 // ---<EOL> hr 295 if ("\n"===c || !c) { //this is kind of bad 296 add_block('divider', null) 297 //---------- 298 // ---... normal text 299 } else { 300 addText("-".repeat(count)) 301 } 302 //------------ 303 // - ... list 304 } else if (eatChar(" ")) { 305 let spaces = 0 306 for (let x=i-3; code[x]===" "; x--) 307 spaces++ 308 start_block('list', {}, {level: spaces}) 309 start_block('list_item', null, {level: spaces}) 310 //--------------- 311 // - normal char 312 } else 313 addText("-") 314 //========================== 315 // ] end link if inside one 316 } else if ("]"===c && stack_top().inBrackets){ //this might break if it assumes .top() exists. needs more testing 317 scan() 318 if (stack_top().big) { 319 if (eatChar("]")) 320 endBlock() 321 else 322 addText("]") 323 } else 324 endBlock() 325 //============ 326 // |... table 327 } else if ("|"===c) { 328 let top = stack_top() 329 // continuation 330 if ('table_cell'===top.type) { 331 scan() 332 let row = top.row 333 let table = top.row.table 334 let eaten = eatChar("\n") 335 //-------------- 336 // | | next row 337 if (eaten && eatChar("|")) { 338 // number of cells in first row 339 // determines number of columns in table 340 if (table.columns == null) 341 table.columns = row.cells 342 // end blocks 343 endBlock() //cell 344 if (top_is('table_row')) //always 345 endBlock() 346 // start row 347 // calculate number of cells in row which will be 348 // already filled due to previous row-spanning cells 349 let cells = 0 350 table.rowspans = table.rowspans.map((span)=>{ 351 cells++ 352 return span-1 353 }).filter(span => span>0) 354 row = start_block('table_row', null, {table, cells}) 355 row.header = eatChar("*") 356 // start cell 357 startCell(row) 358 //-------------------------- 359 // | next cell or table end 360 } else { 361 row.cells++ 362 textBuffer = textBuffer.replace(/ +$/, "") //strip trailing spaces (TODO: allow \<space>) 363 // end of table 364 // table ends when number of cells in current row = number of cells in first row 365 // single-row tables are not easily possible .. 366 // TODO: fix single row tables 367 if (table.columns!=null && row.cells>table.columns) { 368 endBlock() //end cell 369 if (top_is('table_row')) //always 370 endBlock() //row 371 if (top_is('table')) //always 372 endBlock() //table 373 if (eaten) 374 addLineBreak() 375 } else { // next cell 376 endBlock() //cell 377 startCell(row) 378 } 379 } 380 // start of new table (must be at beginning of line) 381 } else if (startOfLine) { 382 scan() 383 let table = start_block('table', null, {columns: null, rowspans: []}) 384 let row = start_block('table_row', null, {table, cells: 0}) 385 row.header = eatChar("*") 386 startCell(row) 387 } else { 388 scan() 389 addText("|") 390 } 391 //=========== 392 // `... code 393 } else if (eatChar("`")) { 394 //--------------- 395 // ``... 396 if (eatChar("`")) { 397 //---------------- 398 // ``` code block 399 if (eatChar("`")) { 400 // read lang name 401 let start = i 402 while (c && "\n"!==c && "`"!==c) 403 scan() 404 //treat first line as language name, if it matches the pattern. otherwise it's code 405 let language = code.substring(start, i) 406 let eaten = false 407 if (/^\s*?\w*?\s*?$/.test(language)) { 408 language = language.trim().toLowerCase() 409 eaten = eatChar("\n") 410 start = i 411 } 412 413 i = code.indexOf("```", i) 414 let text = code.substring(start, -1!==i ? i : code.length) 415 add_block('code', {lang: language||'sb', text}) 416 skipNextLineBreak = eaten 417 restore(-1===i ? code.length : i+3) 418 //------------ 419 // `` invalid 420 } else { 421 addText("``") 422 } 423 // -------------- 424 // ` inline code 425 } else { 426 let start = i 427 let codeText = "" 428 while (c) { 429 if ("`"===c) { 430 if ("`"!==code.charAt(i+1)) 431 break 432 if (i===start+1 && codeText.startsWith(" ")) 433 codeText = codeText.slice(1) 434 scan() 435 } 436 codeText += c 437 scan() 438 } 439 add_block('icode', {text: codeText}) 440 scan() 441 } 442 // 443 //================ 444 // link 445 } else if (readLink()) { 446 // 447 //============= 448 // normal char 449 } else { 450 addText(c) 451 scan() 452 } 453 } 454 // END 455 endAll() 456 return tree 457 458 function endAll() { 459 flushText() 460 while (stack.length) 461 endBlock() 462 openBlocks = code = stack = curr = null // memory leak ... 463 } 464 465 // ################################### 466 467 function readBracketedLink(embed) { 468 if (eatChar("[")) { 469 if (eatChar("[")) { 470 // read url: 471 //let start = i // todo bug: are we supposed to use this? 472 let after = false 473 let url = readUrl(true) 474 if (eatChar("]")) { 475 if (eatChar("]")) { 476 } else if (eatChar("[")) 477 after = true 478 } 479 if (embed) { 480 let [type, args] = urlType(url) 481 if (after) { 482 let altText = "" 483 while (c) { 484 if ("]"===c && "]"===code[i+1]) { //messy 485 scan() 486 scan() 487 break 488 } 489 eatChar("\\") 490 altText += c 491 scan() 492 } 493 args.alt = altText 494 } 495 add_block(type, args) 496 } else { 497 if (after) 498 start_block('link', {url}, {big: true, inBrackets: true}) 499 else 500 add_block('simple_link', {url}) 501 } 502 return true 503 } else { 504 addText("[") 505 } 506 } 507 return false 508 } 509 510 function readEnv() { 511 if (!eatChar("{")) 512 return false 513 stack.push({type:null}) 514 lineStart() 515 516 let start = i 517 if (eatChar("#")){ 518 let name = readTagName() 519 let props = readProps() 520 // todo: make this better lol 521 let arg = props[""] 522 if ('spoiler'===name && !stackContains("spoiler")) { 523 let label = arg==true ? "spoiler" : arg 524 start_block('spoiler', {label}, {}) 525 } else if ('ruby'===name) { 526 start_block('ruby', {text: String(arg)}, {}) 527 } else if ('align'===name) { 528 if (!(arg=='center'||arg=='right'||arg=='left')) 529 arg = null 530 start_block('align', {align: arg}, {}) 531 } else if ('anchor'===name) { 532 start_block('anchor', {name: String(arg)}, {}) 533 } else if ('bg'===name) { 534 // TODO: validate 535 start_block('background_color', {color: String(arg)}, {}) 536 } else if ('sub'===name) { 537 start_block('subscript', null, {}) 538 } else if ('sup'===name) { 539 start_block('superscript', null, {}) 540 } else { 541 add_block('invalid', {text: code.substring(start, i), reason: "invalid tag"}) 542 } 543 /*if (displayBlock({type:name})) 544 skipNextLineBreak = true //what does this even do?*/ 545 } 546 lineStart() 547 return true 548 } 549 550 // read table cell properties and start cell block, and eat whitespace 551 // assumed to be called when pointing to char after | 552 function startCell(row) { 553 let props = {} 554 if (eatChar("#")) 555 props = readProps() 556 557 if (props.rs) 558 row.table.rowspans.push(props.rs-1) 559 if (props.cs) 560 row.cells += props.cs-1 561 562 let args = convert_cell_args(props, row.header) 563 564 start_block('table_cell', args, {row: row}) 565 while (eatChar(" ")) { 566 } 567 } 568 569 // split string on first occurance 570 function split1(string, sep) { 571 let n = string.indexOf(sep) 572 if (-1===n) 573 return [string, null] 574 else 575 return [string.slice(0, n), string.slice(n+sep.length)] 576 } 577 578 function readTagName() { 579 let start = i 580 while (c>="a" && c<="z") 581 scan() 582 if (i > start) 583 return code.substring(start, i) 584 } 585 586 // read properties key=value,key=value... ended by a space or \n or } or { 587 // =value is optional and defaults to `true` 588 function readProps() { 589 let start = i 590 let end = code.indexOf(" ", i) 591 if (end < 0) 592 end = code.length 593 let end2 = code.indexOf("\n", i) 594 if (end2 >= 0 && end2 < end) 595 end = end2 596 end2 = code.indexOf("}", i) 597 if (end2 >= 0 && end2 < end) 598 end = end2 599 end2 = code.indexOf("{", i) 600 if (end2 >= 0 && end2 < end) 601 end = end2 602 603 restore(end) 604 eatChar(" ") 605 606 let propst = code.substring(start, end) 607 let props = {} 608 for (let x of propst.split(",")) { 609 let pair = split1(x, "=") 610 if (pair[1] == null) 611 pair[1] = true 612 props[pair[0]] = pair[1] 613 } 614 return props 615 } 616 617 function readLink() { 618 let embed = eatChar("!") 619 if (readBracketedLink(embed) || readPlainLink(embed)) 620 return true 621 if (embed) { 622 addText("!") 623 return true 624 //lesson: if anything is eaten, you must return true if it's in the top level if switch block 625 } 626 } 627 628 function readPlainLink(embed) { 629 if (!isUrlStart()) 630 return 631 632 let url = readUrl() 633 let after = eatChar("[") 634 635 if (embed) { 636 let [type, args] = urlType(url) 637 if (after) { 638 let altText = "" 639 while (c && "]"!==c && "\n"!==c) { 640 eatChar("\\") 641 altText += c 642 scan() 643 } 644 scan() 645 args.alt = altText 646 } 647 add_block(type, args) 648 } else { 649 if (after) 650 start_block('link', {url}, {inBrackets: true}) 651 else 652 add_block('simple_link', {url}) 653 } 654 return true 655 } 656 657 // closeAll(true) - called at end of document 658 // closeAll(false) - called at end of {} block 659 function closeAll(force) { 660 while (stack.length) { 661 let top = stack_top() 662 if ('ROOT'===top.type) 663 break 664 if (!force && top.type == null) { 665 endBlock() 666 break 667 } 668 endBlock() 669 } 670 } 671 672 // called at the end of a line (unescaped newline) 673 function endLine() { 674 while (1) { 675 let top = stack_top() 676 if ('heading'===top.type || 'quote'===top.type) { 677 endBlock() 678 } else if ('list_item'===top.type) { 679 endBlock() 680 let indent = 0 681 while (eatChar(" ")) 682 indent++ 683 // OPTION 1: 684 // no next item; end list 685 if ("-"!==c) { 686 while (top_is('list')) //should ALWAYS happen at least once 687 endBlock() 688 addText(" ".repeat(indent)) 689 } else { 690 scan() 691 while (eatChar(" ")) { 692 } 693 // OPTION 2: 694 // next item has same indent level; add item to list 695 if (indent == top.level) { 696 start_block('list_item', null, {level: indent}) 697 // OPTION 3: 698 // next item has larger indent; start nested list 699 } else if (indent > top.level) { 700 start_block('list', {}, {level: indent}) 701 // then made the first item of the new list 702 start_block('list_item', null, {level: indent}) 703 // OPTION 4: 704 // next item has less indent; try to exist 1 or more layers of nested lists 705 // if this fails, fall back to just creating a new item in the current list 706 } else { 707 // TODO: currently this will just fail completely 708 while (1) { 709 top = stack_top() 710 if (top && 'list'===top.type) { 711 if (top.level <= indent) 712 break 713 endBlock() 714 } else { 715 // no suitable list was found :( 716 // so just create a new one 717 start_block('list', {}, {level: indent}) 718 break 719 } 720 } 721 start_block('list_item', null, {level: indent}) 722 } 723 break //really? 724 } 725 } else { 726 addLineBreak() 727 break 728 } 729 } 730 } 731 732 // audio, video, image, youtube 733 function urlType(url) { 734 if (/(\.(mp3|ogg|wav|m4a|flac|aac|oga|opus|wma)(?!\w)|#audio$)/i.test(url)) 735 return ["audio", {url}] 736 if (/(\.(mp4|mkv|mov|webm|avi|flv|m4v|mpeg|ogv|ogm|ogx|wmv|xvid)(?!\w)|#video$)/i.test(url)) 737 return ["video", {url}] 738 if (/^https?:[/][/](?:www[.]|music[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/]|youtube.com[/]shorts[/])[\w-]{11}/.test(url)) 739 return ["youtube", {url}] 740 let size = /^([^#]*)#(\d+)x(\d+)$/.exec(url) 741 if (size) 742 return ["image", {url: size[1], width: +size[2], height: +size[3]}] 743 return ["image", {url}] 744 } 745 746 // common code for all text styling tags (bold etc.) 747 function doMarkup(type) { 748 let symbol = c 749 scan() 750 if (canStartMarkup(type)) 751 start_block(type, null, {}) 752 else if (canEndMarkup(type)) 753 endBlock() 754 else 755 addText(symbol) 756 } 757 758 function canStartMarkup(type) { 759 return ( 760 " \t\n({'\"".includes(code.charAt(i-2)) &&// prev 761 !" \t\n,'\"".includes(c) &&// next 762 !stackContains(type) 763 ) 764 } 765 function canEndMarkup(type) { 766 return ( 767 top_is(type) &&// 768 !" \t\n,'\"".includes(code.charAt(i-2)) &&//prev 769 " \t\n-.,:!?')}\"".includes(c)//next 770 ) 771 } 772 } 773 774 // start_block 775 const block_translate = Object.freeze({ 776 __proto__: null, 777 // things without arguments 778 b: 'bold', 779 i: 'italic', 780 u: 'underline', 781 s: 'strikethrough', 782 sup: 'superscript', 783 sub: 'subscript', 784 table: 'table', 785 tr: 'table_row', 786 item: 'list_item', 787 // with args 788 td(args) { 789 return ['table_cell', convert_cell_args(args)] 790 }, 791 th(args) { 792 return ['table_cell', convert_cell_args(args, true)] 793 }, 794 align(args) { 795 let align = args[''] 796 if (align!='left' && align!='right' && align!='center') 797 align = null 798 return ['align', {align}] 799 }, 800 list(args) { 801 return ['list', {style: args['']}] 802 }, 803 spoiler(args) { 804 return ['spoiler', {label: args['']}] 805 }, 806 ruby(args) { 807 return ['ruby', {text: args['']}] 808 }, 809 quote(args) { 810 return ['quote', {cite: args['']}] 811 }, 812 anchor(args) { 813 return ['anchor', {name: args['']}] 814 }, 815 h1(args) { 816 return ['heading', {level: 1}] 817 }, 818 h2(args) { 819 return ['heading', {level: 2}] 820 }, 821 h3(args) { 822 return ['heading', {level: 3}] 823 }, 824 // [url=http://example.com]...[/url] form 825 url(args) { 826 return ['link', {url: args['']}] 827 }, 828 code: 2, 829 youtube: 2, 830 audio: 2, 831 video: 2, 832 img: 2, 833 }) 834 // add_block 835 const block_translate_2 = Object.freeze({ 836 __proto__:null, 837 code(args, contents) { 838 let inline = 'inline'===args[""] 839 if (inline) 840 return ['icode', {text: contents}] 841 else { 842 if (contents.startsWith("\n")) 843 contents = contents.slice(1) 844 return ['code', {text: contents, lang: args.lang||'sb'}] 845 } 846 }, 847 // [url]http://example.com[/url] form 848 url(args, contents) { 849 return ['simple_link', {url: contents}] 850 }, 851 youtube(args, contents) { 852 return ['youtube', {url: contents}] // TODO: set id here 853 }, 854 audio(args, contents) { 855 return ['audio', {url: contents}] 856 }, 857 video(args, contents) { 858 return ['video', {url: contents}] 859 }, 860 img(args, contents) { 861 return ['image', {url: contents, alt: args['']}] 862 }, 863 }) 864 865 this.langs['bbcode'] = function(codeInput) { 866 init(codeInput) 867 curr.lang = 'bbcode' 868 if (!codeInput) 869 return tree 870 871 let point = 0 872 873 while (c) { 874 //=========== 875 // [... tag? 876 if (eatChar("[")) { 877 point = i-1 878 // [/... end tag? 879 if (eatChar("/")) { 880 let name = readTagName() 881 // invalid end tag 882 if (!eatChar("]") || !name) { 883 cancel() 884 // valid end tag 885 } else { 886 // end last item in lists (mostly unnecessary now with greedy closing) 887 if (name == "list" && stack_top().type == "list_item") 888 endBlock(point) 889 if (greedyCloseTag(name)) { 890 // eat whitespace between table cells 891 if (name == 'td' || name == 'th' || name == 'tr') 892 while (eatChar(' ')||eatChar('\n')) { 893 } 894 } else { 895 // ignore invalid block 896 //addBlock('invalid', code.substring(point, i), "unexpected closing tag") 897 } 898 } 899 // [... start tag? 900 } else { 901 let name = readTagName() 902 if (!name || !block_translate[name]) { 903 // special case [*] list item 904 if (eatChar("*") && eatChar("]")) { 905 if (stack_top().type == "list_item") 906 endBlock(point) 907 let top = stack_top() 908 if (top.type == "list") 909 start_block('list_item', null, {bbcode: 'item'}) 910 else 911 cancel() 912 } else 913 cancel() 914 } else { 915 // [tag=... 916 let arg = true, args = {} 917 if (eatChar("=")) { 918 let start=i 919 if (eatChar('"')) { 920 start++ 921 while (c && '"'!==c) 922 scan() 923 if ('"'===c) { 924 scan() 925 arg = code.substring(start, i-1) 926 } 927 } else { 928 while (c && "]"!==c && " "!==c) 929 scan() 930 if ("]"===c || " "===c) 931 arg = code.substring(start, i) 932 } 933 } 934 if (eatChar(" ")) { 935 args = readArgList() || {} 936 } 937 if (arg !== true) 938 args[""] = arg 939 if (eatChar("]")) { 940 // simple tag 941 if (block_translate_2[name] && !('url'===name && arg!==true)) { 942 let endTag = "[/"+name+"]" 943 let end = code.indexOf(endTag, i) 944 if (end < 0) 945 cancel() 946 else { 947 let contents = code.substring(i, end) 948 restore(end + endTag.length) 949 950 let [t, a] = block_translate_2[name](args, contents) 951 add_block(t, a) 952 } 953 } else if ('item'!==name && block_translate[name] && !('spoiler'===name && stackContains(name))) { 954 if ('tr'===name || 'table'===name) 955 while (eatChar(" ") || eatChar("\n")) { 956 } 957 958 let tx = block_translate[name] 959 if ('string'===typeof tx) 960 start_block(tx, null, {bbcode: name}) 961 else { 962 let [t, a] = tx(args) 963 start_block(t, a, {bbcode: name}) 964 } 965 } else 966 add_block('invalid', {text: code.substring(point, i), message: "invalid tag"}) 967 } else 968 cancel() 969 } 970 } 971 } else if (readPlainLink()) { 972 } else if (eatChar("\n")) { 973 addLineBreak() 974 } else { 975 addText(c) 976 scan() 977 } 978 } 979 endAll() 980 return tree 981 982 function cancel() { 983 restore(point) 984 addText(c) 985 scan() 986 } 987 988 function greedyCloseTag(name) { 989 for (let j=0; j<stack.length; j++) 990 if (stack[j].bbcode === name) { 991 while (stack_top().bbcode !== name)//scary 992 endBlock() 993 endBlock() 994 return true 995 } 996 } 997 998 function readPlainLink() { 999 if (isUrlStart()) { 1000 let url = readUrl() 1001 add_block('simple_link', {url}) 1002 return true 1003 } 1004 } 1005 1006 function readArgList() { 1007 let args = {} 1008 while (1) { 1009 // read key 1010 let start = i 1011 while (isTagChar(c)) 1012 scan() 1013 let key = code.substring(start, i) 1014 // key=... 1015 if (eatChar("=")) { 1016 // key="... 1017 if (eatChar('"')) { 1018 start = i 1019 while (!'"\n'.includes(c)) 1020 scan() 1021 if (eatChar('"')) 1022 args[key] = code.substring(start, i-2) 1023 else 1024 return null 1025 // key=... 1026 } else { 1027 start = i 1028 while (!" ]\n".includes(c)) 1029 scan() 1030 if ("]"===c) { 1031 args[key] = code.substring(start, i) 1032 return args 1033 } else if (eatChar(" ")) { 1034 args[key] = code.substring(start, i-1) 1035 } else 1036 return null 1037 } 1038 // key ... 1039 } else if (eatChar(" ")) { 1040 args[key] = true 1041 // key]... 1042 } else if ("]"===c) { 1043 args[key] = true 1044 return args 1045 // key<other char> (error) 1046 } else 1047 return null 1048 } 1049 } 1050 1051 function readTagName() { 1052 let start = i 1053 while (isTagChar(c)) 1054 scan() 1055 return code.substring(start, i) 1056 } 1057 1058 function isTagChar(c) { 1059 return /[a-z0-9]/i.test(c) 1060 } 1061 } 1062 1063 this.langs['plaintext'] = function(text) { 1064 let root = {type: 'ROOT', content: []} 1065 1066 let linkRegex = /\b(?:https?:\/\/|sbs:)[-\w$.+!*'(),;/?:@=&#%]*/g 1067 let result 1068 let last = 0 1069 while (result = linkRegex.exec(text)) { 1070 // text before link 1071 let before = text.substring(last, result.index) 1072 if (before) 1073 root.content.push(before) 1074 // generate link 1075 let url = result[0] 1076 root.content.push({type: 'simple_link', args: {url}}) 1077 last = result.index + result[0].length 1078 } 1079 // text after last link (or entire message if no links were found) 1080 let after = text.slice(last) 1081 if (after) 1082 root.content.push(after) 1083 1084 return root 1085 } 1086 1087 /** 1088 default markup language (plaintext) 1089 @type {Parser} 1090 */ 1091 this.default_lang = this.langs['plaintext'] 1092}} 1093 1094export default Markup_Legacy