this repo has no description
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