comparison src/development/formats/bbcode.js @ 0:4c4fc447baea

start with sceditor-3.1.1
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 04 Aug 2022 15:21:29 -0600
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4c4fc447baea
1 /**
2 * SCEditor BBCode Plugin
3 * http://www.sceditor.com/
4 *
5 * Copyright (C) 2011-2017, Sam Clarke (samclarke.com)
6 *
7 * SCEditor is licensed under the MIT license:
8 * http://www.opensource.org/licenses/mit-license.php
9 *
10 * @fileoverview SCEditor BBCode Format
11 * @author Sam Clarke
12 */
13 (function (sceditor) {
14 /*eslint max-depth: off*/
15 'use strict';
16
17 var escapeEntities = sceditor.escapeEntities;
18 var escapeUriScheme = sceditor.escapeUriScheme;
19 var dom = sceditor.dom;
20 var utils = sceditor.utils;
21
22 var css = dom.css;
23 var attr = dom.attr;
24 var is = dom.is;
25 var extend = utils.extend;
26 var each = utils.each;
27
28 var EMOTICON_DATA_ATTR = 'data-sceditor-emoticon';
29
30 var getEditorCommand = sceditor.command.get;
31
32 var QuoteType = {
33 /** @lends BBCodeParser.QuoteType */
34 /**
35 * Always quote the attribute value
36 * @type {Number}
37 */
38 always: 1,
39
40 /**
41 * Never quote the attributes value
42 * @type {Number}
43 */
44 never: 2,
45
46 /**
47 * Only quote the attributes value when it contains spaces to equals
48 * @type {Number}
49 */
50 auto: 3
51 };
52
53 var defaultCommandsOverrides = {
54 bold: {
55 txtExec: ['[b]', '[/b]']
56 },
57 italic: {
58 txtExec: ['[i]', '[/i]']
59 },
60 underline: {
61 txtExec: ['[u]', '[/u]']
62 },
63 strike: {
64 txtExec: ['[s]', '[/s]']
65 },
66 subscript: {
67 txtExec: ['[sub]', '[/sub]']
68 },
69 superscript: {
70 txtExec: ['[sup]', '[/sup]']
71 },
72 left: {
73 txtExec: ['[left]', '[/left]']
74 },
75 center: {
76 txtExec: ['[center]', '[/center]']
77 },
78 right: {
79 txtExec: ['[right]', '[/right]']
80 },
81 justify: {
82 txtExec: ['[justify]', '[/justify]']
83 },
84 font: {
85 txtExec: function (caller) {
86 var editor = this;
87
88 getEditorCommand('font')._dropDown(
89 editor,
90 caller,
91 function (fontName) {
92 editor.insertText(
93 '[font=' + fontName + ']',
94 '[/font]'
95 );
96 }
97 );
98 }
99 },
100 size: {
101 txtExec: function (caller) {
102 var editor = this;
103
104 getEditorCommand('size')._dropDown(
105 editor,
106 caller,
107 function (fontSize) {
108 editor.insertText(
109 '[size=' + fontSize + ']',
110 '[/size]'
111 );
112 }
113 );
114 }
115 },
116 color: {
117 txtExec: function (caller) {
118 var editor = this;
119
120 getEditorCommand('color')._dropDown(
121 editor,
122 caller,
123 function (color) {
124 editor.insertText(
125 '[color=' + color + ']',
126 '[/color]'
127 );
128 }
129 );
130 }
131 },
132 bulletlist: {
133 txtExec: function (caller, selected) {
134 this.insertText(
135 '[ul]\n[li]' +
136 selected.split(/\r?\n/).join('[/li]\n[li]') +
137 '[/li]\n[/ul]'
138 );
139 }
140 },
141 orderedlist: {
142 txtExec: function (caller, selected) {
143 this.insertText(
144 '[ol]\n[li]' +
145 selected.split(/\r?\n/).join('[/li]\n[li]') +
146 '[/li]\n[/ol]'
147 );
148 }
149 },
150 table: {
151 txtExec: ['[table][tr][td]', '[/td][/tr][/table]']
152 },
153 horizontalrule: {
154 txtExec: ['[hr]']
155 },
156 code: {
157 txtExec: ['[code]', '[/code]']
158 },
159 image: {
160 txtExec: function (caller, selected) {
161 var editor = this;
162
163 getEditorCommand('image')._dropDown(
164 editor,
165 caller,
166 selected,
167 function (url, width, height) {
168 var attrs = '';
169
170 if (width) {
171 attrs += ' width=' + width;
172 }
173
174 if (height) {
175 attrs += ' height=' + height;
176 }
177
178 editor.insertText(
179 '[img' + attrs + ']' + url + '[/img]'
180 );
181 }
182 );
183 }
184 },
185 email: {
186 txtExec: function (caller, selected) {
187 var editor = this;
188
189 getEditorCommand('email')._dropDown(
190 editor,
191 caller,
192 function (url, text) {
193 editor.insertText(
194 '[email=' + url + ']' +
195 (text || selected || url) +
196 '[/email]'
197 );
198 }
199 );
200 }
201 },
202 link: {
203 txtExec: function (caller, selected) {
204 var editor = this;
205
206 getEditorCommand('link')._dropDown(
207 editor,
208 caller,
209 function (url, text) {
210 editor.insertText(
211 '[url=' + url + ']' +
212 (text || selected || url) +
213 '[/url]'
214 );
215 }
216 );
217 }
218 },
219 quote: {
220 txtExec: ['[quote]', '[/quote]']
221 },
222 youtube: {
223 txtExec: function (caller) {
224 var editor = this;
225
226 getEditorCommand('youtube')._dropDown(
227 editor,
228 caller,
229 function (id) {
230 editor.insertText('[youtube]' + id + '[/youtube]');
231 }
232 );
233 }
234 },
235 rtl: {
236 txtExec: ['[rtl]', '[/rtl]']
237 },
238 ltr: {
239 txtExec: ['[ltr]', '[/ltr]']
240 }
241 };
242
243 var bbcodeHandlers = {
244 // START_COMMAND: Bold
245 b: {
246 tags: {
247 b: null,
248 strong: null
249 },
250 styles: {
251 // 401 is for FF 3.5
252 'font-weight': ['bold', 'bolder', '401', '700', '800', '900']
253 },
254 format: '[b]{0}[/b]',
255 html: '<strong>{0}</strong>'
256 },
257 // END_COMMAND
258
259 // START_COMMAND: Italic
260 i: {
261 tags: {
262 i: null,
263 em: null
264 },
265 styles: {
266 'font-style': ['italic', 'oblique']
267 },
268 format: '[i]{0}[/i]',
269 html: '<em>{0}</em>'
270 },
271 // END_COMMAND
272
273 // START_COMMAND: Underline
274 u: {
275 tags: {
276 u: null
277 },
278 styles: {
279 'text-decoration': ['underline']
280 },
281 format: '[u]{0}[/u]',
282 html: '<u>{0}</u>'
283 },
284 // END_COMMAND
285
286 // START_COMMAND: Strikethrough
287 s: {
288 tags: {
289 s: null,
290 strike: null
291 },
292 styles: {
293 'text-decoration': ['line-through']
294 },
295 format: '[s]{0}[/s]',
296 html: '<s>{0}</s>'
297 },
298 // END_COMMAND
299
300 // START_COMMAND: Subscript
301 sub: {
302 tags: {
303 sub: null
304 },
305 format: '[sub]{0}[/sub]',
306 html: '<sub>{0}</sub>'
307 },
308 // END_COMMAND
309
310 // START_COMMAND: Superscript
311 sup: {
312 tags: {
313 sup: null
314 },
315 format: '[sup]{0}[/sup]',
316 html: '<sup>{0}</sup>'
317 },
318 // END_COMMAND
319
320 // START_COMMAND: Font
321 font: {
322 tags: {
323 font: {
324 face: null
325 }
326 },
327 styles: {
328 'font-family': null
329 },
330 quoteType: QuoteType.never,
331 format: function (element, content) {
332 var font;
333
334 if (!is(element, 'font') || !(font = attr(element, 'face'))) {
335 font = css(element, 'font-family');
336 }
337
338 return '[font=' + _stripQuotes(font) + ']' +
339 content + '[/font]';
340 },
341 html: '<font face="{defaultattr}">{0}</font>'
342 },
343 // END_COMMAND
344
345 // START_COMMAND: Size
346 size: {
347 tags: {
348 font: {
349 size: null
350 }
351 },
352 styles: {
353 'font-size': null
354 },
355 format: function (element, content) {
356 var fontSize = attr(element, 'size'),
357 size = 2;
358
359 if (!fontSize) {
360 fontSize = css(element, 'fontSize');
361 }
362
363 // Most browsers return px value but IE returns 1-7
364 if (fontSize.indexOf('px') > -1) {
365 // convert size to an int
366 fontSize = fontSize.replace('px', '') - 0;
367
368 if (fontSize < 12) {
369 size = 1;
370 }
371 if (fontSize > 15) {
372 size = 3;
373 }
374 if (fontSize > 17) {
375 size = 4;
376 }
377 if (fontSize > 23) {
378 size = 5;
379 }
380 if (fontSize > 31) {
381 size = 6;
382 }
383 if (fontSize > 47) {
384 size = 7;
385 }
386 } else {
387 size = fontSize;
388 }
389
390 return '[size=' + size + ']' + content + '[/size]';
391 },
392 html: '<font size="{defaultattr}">{!0}</font>'
393 },
394 // END_COMMAND
395
396 // START_COMMAND: Color
397 color: {
398 tags: {
399 font: {
400 color: null
401 }
402 },
403 styles: {
404 color: null
405 },
406 quoteType: QuoteType.never,
407 format: function (elm, content) {
408 var color;
409
410 if (!is(elm, 'font') || !(color = attr(elm, 'color'))) {
411 color = elm.style.color || css(elm, 'color');
412 }
413
414 return '[color=' + _normaliseColour(color) + ']' +
415 content + '[/color]';
416 },
417 html: function (token, attrs, content) {
418 return '<font color="' +
419 escapeEntities(_normaliseColour(attrs.defaultattr), true) +
420 '">' + content + '</font>';
421 }
422 },
423 // END_COMMAND
424
425 // START_COMMAND: Lists
426 ul: {
427 tags: {
428 ul: null
429 },
430 breakStart: true,
431 isInline: false,
432 skipLastLineBreak: true,
433 format: '[ul]{0}[/ul]',
434 html: '<ul>{0}</ul>'
435 },
436 list: {
437 breakStart: true,
438 isInline: false,
439 skipLastLineBreak: true,
440 html: '<ul>{0}</ul>'
441 },
442 ol: {
443 tags: {
444 ol: null
445 },
446 breakStart: true,
447 isInline: false,
448 skipLastLineBreak: true,
449 format: '[ol]{0}[/ol]',
450 html: '<ol>{0}</ol>'
451 },
452 li: {
453 tags: {
454 li: null
455 },
456 isInline: false,
457 closedBy: ['/ul', '/ol', '/list', '*', 'li'],
458 format: '[li]{0}[/li]',
459 html: '<li>{0}</li>'
460 },
461 '*': {
462 isInline: false,
463 closedBy: ['/ul', '/ol', '/list', '*', 'li'],
464 html: '<li>{0}</li>'
465 },
466 // END_COMMAND
467
468 // START_COMMAND: Table
469 table: {
470 tags: {
471 table: null
472 },
473 isInline: false,
474 isHtmlInline: true,
475 skipLastLineBreak: true,
476 format: '[table]{0}[/table]',
477 html: '<table>{0}</table>'
478 },
479 tr: {
480 tags: {
481 tr: null
482 },
483 isInline: false,
484 skipLastLineBreak: true,
485 format: '[tr]{0}[/tr]',
486 html: '<tr>{0}</tr>'
487 },
488 th: {
489 tags: {
490 th: null
491 },
492 allowsEmpty: true,
493 isInline: false,
494 format: '[th]{0}[/th]',
495 html: '<th>{0}</th>'
496 },
497 td: {
498 tags: {
499 td: null
500 },
501 allowsEmpty: true,
502 isInline: false,
503 format: '[td]{0}[/td]',
504 html: '<td>{0}</td>'
505 },
506 // END_COMMAND
507
508 // START_COMMAND: Emoticons
509 emoticon: {
510 allowsEmpty: true,
511 tags: {
512 img: {
513 src: null,
514 'data-sceditor-emoticon': null
515 }
516 },
517 format: function (element, content) {
518 return attr(element, EMOTICON_DATA_ATTR) + content;
519 },
520 html: '{0}'
521 },
522 // END_COMMAND
523
524 // START_COMMAND: Horizontal Rule
525 hr: {
526 tags: {
527 hr: null
528 },
529 allowsEmpty: true,
530 isSelfClosing: true,
531 isInline: false,
532 format: '[hr]{0}',
533 html: '<hr />'
534 },
535 // END_COMMAND
536
537 // START_COMMAND: Image
538 img: {
539 allowsEmpty: true,
540 tags: {
541 img: {
542 src: null
543 }
544 },
545 allowedChildren: ['#'],
546 quoteType: QuoteType.never,
547 format: function (element, content) {
548 var width, height,
549 attribs = '',
550 style = function (name) {
551 return element.style ? element.style[name] : null;
552 };
553
554 // check if this is an emoticon image
555 if (attr(element, EMOTICON_DATA_ATTR)) {
556 return content;
557 }
558
559 width = attr(element, 'width') || style('width');
560 height = attr(element, 'height') || style('height');
561
562 // only add width and height if one is specified
563 if ((element.complete && (width || height)) ||
564 (width && height)) {
565
566 attribs = '=' + dom.width(element) + 'x' +
567 dom.height(element);
568 }
569
570 return '[img' + attribs + ']' + attr(element, 'src') + '[/img]';
571 },
572 html: function (token, attrs, content) {
573 var undef, width, height, match,
574 attribs = '';
575
576 // handle [img width=340 height=240]url[/img]
577 width = attrs.width;
578 height = attrs.height;
579
580 // handle [img=340x240]url[/img]
581 if (attrs.defaultattr) {
582 match = attrs.defaultattr.split(/x/i);
583
584 width = match[0];
585 height = (match.length === 2 ? match[1] : match[0]);
586 }
587
588 if (width !== undef) {
589 attribs += ' width="' + escapeEntities(width, true) + '"';
590 }
591
592 if (height !== undef) {
593 attribs += ' height="' + escapeEntities(height, true) + '"';
594 }
595
596 return '<img' + attribs +
597 ' src="' + escapeUriScheme(content) + '" />';
598 }
599 },
600 // END_COMMAND
601
602 // START_COMMAND: URL
603 url: {
604 allowsEmpty: true,
605 tags: {
606 a: {
607 href: null
608 }
609 },
610 quoteType: QuoteType.never,
611 format: function (element, content) {
612 var url = attr(element, 'href');
613
614 // make sure this link is not an e-mail,
615 // if it is return e-mail BBCode
616 if (url.substr(0, 7) === 'mailto:') {
617 return '[email="' + url.substr(7) + '"]' +
618 content + '[/email]';
619 }
620
621 return '[url=' + url + ']' + content + '[/url]';
622 },
623 html: function (token, attrs, content) {
624 attrs.defaultattr =
625 escapeEntities(attrs.defaultattr, true) || content;
626
627 return '<a href="' + escapeUriScheme(attrs.defaultattr) + '">' +
628 content + '</a>';
629 }
630 },
631 // END_COMMAND
632
633 // START_COMMAND: E-mail
634 email: {
635 quoteType: QuoteType.never,
636 html: function (token, attrs, content) {
637 return '<a href="mailto:' +
638 (escapeEntities(attrs.defaultattr, true) || content) +
639 '">' + content + '</a>';
640 }
641 },
642 // END_COMMAND
643
644 // START_COMMAND: Quote
645 quote: {
646 tags: {
647 blockquote: null
648 },
649 isInline: false,
650 quoteType: QuoteType.never,
651 format: function (element, content) {
652 var authorAttr = 'data-author';
653 var author = '';
654 var cite;
655 var children = element.children;
656
657 for (var i = 0; !cite && i < children.length; i++) {
658 if (is(children[i], 'cite')) {
659 cite = children[i];
660 }
661 }
662
663 if (cite || attr(element, authorAttr)) {
664 author = cite && cite.textContent ||
665 attr(element, authorAttr);
666
667 attr(element, authorAttr, author);
668
669 if (cite) {
670 element.removeChild(cite);
671 }
672
673 content = this.elementToBbcode(element);
674 author = '=' + author.replace(/(^\s+|\s+$)/g, '');
675
676 if (cite) {
677 element.insertBefore(cite, element.firstChild);
678 }
679 }
680
681 return '[quote' + author + ']' + content + '[/quote]';
682 },
683 html: function (token, attrs, content) {
684 if (attrs.defaultattr) {
685 content = '<cite>' + escapeEntities(attrs.defaultattr) +
686 '</cite>' + content;
687 }
688
689 return '<blockquote>' + content + '</blockquote>';
690 }
691 },
692 // END_COMMAND
693
694 // START_COMMAND: Code
695 code: {
696 tags: {
697 code: null
698 },
699 isInline: false,
700 allowedChildren: ['#', '#newline'],
701 format: '[code]{0}[/code]',
702 html: '<code>{0}</code>'
703 },
704 // END_COMMAND
705
706
707 // START_COMMAND: Left
708 left: {
709 styles: {
710 'text-align': [
711 'left',
712 '-webkit-left',
713 '-moz-left',
714 '-khtml-left'
715 ]
716 },
717 isInline: false,
718 allowsEmpty: true,
719 format: '[left]{0}[/left]',
720 html: '<div align="left">{0}</div>'
721 },
722 // END_COMMAND
723
724 // START_COMMAND: Centre
725 center: {
726 styles: {
727 'text-align': [
728 'center',
729 '-webkit-center',
730 '-moz-center',
731 '-khtml-center'
732 ]
733 },
734 isInline: false,
735 allowsEmpty: true,
736 format: '[center]{0}[/center]',
737 html: '<div align="center">{0}</div>'
738 },
739 // END_COMMAND
740
741 // START_COMMAND: Right
742 right: {
743 styles: {
744 'text-align': [
745 'right',
746 '-webkit-right',
747 '-moz-right',
748 '-khtml-right'
749 ]
750 },
751 isInline: false,
752 allowsEmpty: true,
753 format: '[right]{0}[/right]',
754 html: '<div align="right">{0}</div>'
755 },
756 // END_COMMAND
757
758 // START_COMMAND: Justify
759 justify: {
760 styles: {
761 'text-align': [
762 'justify',
763 '-webkit-justify',
764 '-moz-justify',
765 '-khtml-justify'
766 ]
767 },
768 isInline: false,
769 allowsEmpty: true,
770 format: '[justify]{0}[/justify]',
771 html: '<div align="justify">{0}</div>'
772 },
773 // END_COMMAND
774
775 // START_COMMAND: YouTube
776 youtube: {
777 allowsEmpty: true,
778 tags: {
779 iframe: {
780 'data-youtube-id': null
781 }
782 },
783 format: function (element, content) {
784 element = attr(element, 'data-youtube-id');
785
786 return element ? '[youtube]' + element + '[/youtube]' : content;
787 },
788 html: '<iframe width="560" height="315" frameborder="0" ' +
789 'src="https://www.youtube-nocookie.com/embed/{0}?wmode=opaque" ' +
790 'data-youtube-id="{0}" allowfullscreen></iframe>'
791 },
792 // END_COMMAND
793
794
795 // START_COMMAND: Rtl
796 rtl: {
797 styles: {
798 direction: ['rtl']
799 },
800 isInline: false,
801 format: '[rtl]{0}[/rtl]',
802 html: '<div style="direction: rtl">{0}</div>'
803 },
804 // END_COMMAND
805
806 // START_COMMAND: Ltr
807 ltr: {
808 styles: {
809 direction: ['ltr']
810 },
811 isInline: false,
812 format: '[ltr]{0}[/ltr]',
813 html: '<div style="direction: ltr">{0}</div>'
814 },
815 // END_COMMAND
816
817 // this is here so that commands above can be removed
818 // without having to remove the , after the last one.
819 // Needed for IE.
820 ignore: {}
821 };
822
823 /**
824 * Formats a string replacing {name} with the values of
825 * obj.name properties.
826 *
827 * If there is no property for the specified {name} then
828 * it will be left intact.
829 *
830 * @param {string} str
831 * @param {Object} obj
832 * @return {string}
833 * @since 2.0.0
834 */
835 function formatBBCodeString(str, obj) {
836 return str.replace(/\{([^}]+)\}/g, function (match, group) {
837 var undef,
838 escape = true;
839
840 if (group.charAt(0) === '!') {
841 escape = false;
842 group = group.substring(1);
843 }
844
845 if (group === '0') {
846 escape = false;
847 }
848
849 if (obj[group] === undef) {
850 return match;
851 }
852
853 return escape ? escapeEntities(obj[group], true) : obj[group];
854 });
855 }
856
857 /**
858 * Removes the first and last divs from the HTML.
859 *
860 * This is needed for pasting
861 * @param {string} html
862 * @return {string}
863 * @private
864 */
865 function removeFirstLastDiv(html) {
866 var node, next, removeDiv,
867 output = document.createElement('div');
868
869 removeDiv = function (node, isFirst) {
870 // Don't remove divs that have styling
871 if (dom.hasStyling(node)) {
872 return;
873 }
874
875 if ((node.childNodes.length !== 1 ||
876 !is(node.firstChild, 'br'))) {
877 while ((next = node.firstChild)) {
878 output.insertBefore(next, node);
879 }
880 }
881
882 if (isFirst) {
883 var lastChild = output.lastChild;
884
885 if (node !== lastChild && is(lastChild, 'div') &&
886 node.nextSibling === lastChild) {
887 output.insertBefore(document.createElement('br'), node);
888 }
889 }
890
891 output.removeChild(node);
892 };
893
894 css(output, 'display', 'none');
895 output.innerHTML = html.replace(/<\/div>\n/g, '</div>');
896
897 if ((node = output.firstChild) && is(node, 'div')) {
898 removeDiv(node, true);
899 }
900
901 if ((node = output.lastChild) && is(node, 'div')) {
902 removeDiv(node);
903 }
904
905 return output.innerHTML;
906 }
907
908 function isFunction(fn) {
909 return typeof fn === 'function';
910 }
911
912 /**
913 * Removes any leading or trailing quotes ('")
914 *
915 * @return string
916 * @since v1.4.0
917 */
918 function _stripQuotes(str) {
919 return str ?
920 str.replace(/\\(.)/g, '$1').replace(/^(["'])(.*?)\1$/, '$2') : str;
921 }
922
923 /**
924 * Formats a string replacing {0}, {1}, {2}, ect. with
925 * the params provided
926 *
927 * @param {string} str The string to format
928 * @param {...string} arg The strings to replace
929 * @return {string}
930 * @since v1.4.0
931 */
932 function _formatString(str) {
933 var undef;
934 var args = arguments;
935
936 return str.replace(/\{(\d+)\}/g, function (_, matchNum) {
937 return args[matchNum - 0 + 1] !== undef ?
938 args[matchNum - 0 + 1] :
939 '{' + matchNum + '}';
940 });
941 }
942
943 var TOKEN_OPEN = 'open';
944 var TOKEN_CONTENT = 'content';
945 var TOKEN_NEWLINE = 'newline';
946 var TOKEN_CLOSE = 'close';
947
948
949 /*
950 * @typedef {Object} TokenizeToken
951 * @property {string} type
952 * @property {string} name
953 * @property {string} val
954 * @property {Object.<string, string>} attrs
955 * @property {array} children
956 * @property {TokenizeToken} closing
957 */
958
959 /**
960 * Tokenize token object
961 *
962 * @param {string} type The type of token this is,
963 * should be one of tokenType
964 * @param {string} name The name of this token
965 * @param {string} val The originally matched string
966 * @param {array} attrs Any attributes. Only set on
967 * TOKEN_TYPE_OPEN tokens
968 * @param {array} children Any children of this token
969 * @param {TokenizeToken} closing This tokens closing tag.
970 * Only set on TOKEN_TYPE_OPEN tokens
971 * @class {TokenizeToken}
972 * @name {TokenizeToken}
973 * @memberOf BBCodeParser.prototype
974 */
975 // eslint-disable-next-line max-params
976 function TokenizeToken(type, name, val, attrs, children, closing) {
977 var base = this;
978
979 base.type = type;
980 base.name = name;
981 base.val = val;
982 base.attrs = attrs || {};
983 base.children = children || [];
984 base.closing = closing || null;
985 };
986
987 TokenizeToken.prototype = {
988 /** @lends BBCodeParser.prototype.TokenizeToken */
989 /**
990 * Clones this token
991 *
992 * @return {TokenizeToken}
993 */
994 clone: function () {
995 var base = this;
996
997 return new TokenizeToken(
998 base.type,
999 base.name,
1000 base.val,
1001 extend({}, base.attrs),
1002 [],
1003 base.closing ? base.closing.clone() : null
1004 );
1005 },
1006 /**
1007 * Splits this token at the specified child
1008 *
1009 * @param {TokenizeToken} splitAt The child to split at
1010 * @return {TokenizeToken} The right half of the split token or
1011 * empty clone if invalid splitAt lcoation
1012 */
1013 splitAt: function (splitAt) {
1014 var offsetLength;
1015 var base = this;
1016 var clone = base.clone();
1017 var offset = base.children.indexOf(splitAt);
1018
1019 if (offset > -1) {
1020 // Work out how many items are on the right side of the split
1021 // to pass to splice()
1022 offsetLength = base.children.length - offset;
1023 clone.children = base.children.splice(offset, offsetLength);
1024 }
1025
1026 return clone;
1027 }
1028 };
1029
1030
1031 /**
1032 * SCEditor BBCode parser class
1033 *
1034 * @param {Object} options
1035 * @class BBCodeParser
1036 * @name BBCodeParser
1037 * @since v1.4.0
1038 */
1039 function BBCodeParser(options) {
1040 var base = this;
1041
1042 base.opts = extend({}, BBCodeParser.defaults, options);
1043
1044 /**
1045 * Takes a BBCode string and splits it into open,
1046 * content and close tags.
1047 *
1048 * It does no checking to verify a tag has a matching open
1049 * or closing tag or if the tag is valid child of any tag
1050 * before it. For that the tokens should be passed to the
1051 * parse function.
1052 *
1053 * @param {string} str
1054 * @return {array}
1055 * @memberOf BBCodeParser.prototype
1056 */
1057 base.tokenize = function (str) {
1058 var matches, type, i;
1059 var tokens = [];
1060 // The token types in reverse order of precedence
1061 // (they're looped in reverse)
1062 var tokenTypes = [
1063 {
1064 type: TOKEN_CONTENT,
1065 regex: /^([^\[\r\n]+|\[)/
1066 },
1067 {
1068 type: TOKEN_NEWLINE,
1069 regex: /^(\r\n|\r|\n)/
1070 },
1071 {
1072 type: TOKEN_OPEN,
1073 regex: /^\[[^\[\]]+\]/
1074 },
1075 // Close must come before open as they are
1076 // the same except close has a / at the start.
1077 {
1078 type: TOKEN_CLOSE,
1079 regex: /^\[\/[^\[\]]+\]/
1080 }
1081 ];
1082
1083 strloop:
1084 while (str.length) {
1085 i = tokenTypes.length;
1086 while (i--) {
1087 type = tokenTypes[i].type;
1088
1089 // Check if the string matches any of the tokens
1090 if (!(matches = str.match(tokenTypes[i].regex)) ||
1091 !matches[0]) {
1092 continue;
1093 }
1094
1095 // Add the match to the tokens list
1096 tokens.push(tokenizeTag(type, matches[0]));
1097
1098 // Remove the match from the string
1099 str = str.substr(matches[0].length);
1100
1101 // The token has been added so start again
1102 continue strloop;
1103 }
1104
1105 // If there is anything left in the string which doesn't match
1106 // any of the tokens then just assume it's content and add it.
1107 if (str.length) {
1108 tokens.push(tokenizeTag(TOKEN_CONTENT, str));
1109 }
1110
1111 str = '';
1112 }
1113
1114 return tokens;
1115 };
1116
1117 /**
1118 * Extracts the name an params from a tag
1119 *
1120 * @param {string} type
1121 * @param {string} val
1122 * @return {Object}
1123 * @private
1124 */
1125 function tokenizeTag(type, val) {
1126 var matches, attrs, name,
1127 openRegex = /\[([^\]\s=]+)(?:([^\]]+))?\]/,
1128 closeRegex = /\[\/([^\[\]]+)\]/;
1129
1130 // Extract the name and attributes from opening tags and
1131 // just the name from closing tags.
1132 if (type === TOKEN_OPEN && (matches = val.match(openRegex))) {
1133 name = lower(matches[1]);
1134
1135 if (matches[2] && (matches[2] = matches[2].trim())) {
1136 attrs = tokenizeAttrs(matches[2]);
1137 }
1138 }
1139
1140 if (type === TOKEN_CLOSE &&
1141 (matches = val.match(closeRegex))) {
1142 name = lower(matches[1]);
1143 }
1144
1145 if (type === TOKEN_NEWLINE) {
1146 name = '#newline';
1147 }
1148
1149 // Treat all tokens without a name and
1150 // all unknown BBCodes as content
1151 if (!name || ((type === TOKEN_OPEN || type === TOKEN_CLOSE) &&
1152 !bbcodeHandlers[name])) {
1153
1154 type = TOKEN_CONTENT;
1155 name = '#';
1156 }
1157
1158 return new TokenizeToken(type, name, val, attrs);
1159 }
1160
1161 /**
1162 * Extracts the individual attributes from a string containing
1163 * all the attributes.
1164 *
1165 * @param {string} attrs
1166 * @return {Object} Assoc array of attributes
1167 * @private
1168 */
1169 function tokenizeAttrs(attrs) {
1170 var matches,
1171 /*
1172 ([^\s=]+) Anything that's not a space or equals
1173 = Equals sign =
1174 (?:
1175 (?:
1176 (["']) The opening quote
1177 (
1178 (?:\\\2|[^\2])*? Anything that isn't the
1179 unescaped opening quote
1180 )
1181 \2 The opening quote again which
1182 will close the string
1183 )
1184 | If not a quoted string then match
1185 (
1186 (?:.(?!\s\S+=))*.? Anything that isn't part of
1187 [space][non-space][=] which
1188 would be a new attribute
1189 )
1190 )
1191 */
1192 attrRegex = /([^\s=]+)=(?:(?:(["'])((?:\\\2|[^\2])*?)\2)|((?:.(?!\s\S+=))*.))/g,
1193 ret = {};
1194
1195 // if only one attribute then remove the = from the start and
1196 // strip any quotes
1197 if (attrs.charAt(0) === '=' && attrs.indexOf('=', 1) < 0) {
1198 ret.defaultattr = _stripQuotes(attrs.substr(1));
1199 } else {
1200 if (attrs.charAt(0) === '=') {
1201 attrs = 'defaultattr' + attrs;
1202 }
1203
1204 // No need to strip quotes here, the regex will do that.
1205 while ((matches = attrRegex.exec(attrs))) {
1206 ret[lower(matches[1])] =
1207 _stripQuotes(matches[3]) || matches[4];
1208 }
1209 }
1210
1211 return ret;
1212 }
1213
1214 /**
1215 * Parses a string into an array of BBCodes
1216 *
1217 * @param {string} str
1218 * @param {boolean} preserveNewLines If to preserve all new lines, not
1219 * strip any based on the passed
1220 * formatting options
1221 * @return {array} Array of BBCode objects
1222 * @memberOf BBCodeParser.prototype
1223 */
1224 base.parse = function (str, preserveNewLines) {
1225 var ret = parseTokens(base.tokenize(str));
1226 var opts = base.opts;
1227
1228 if (opts.fixInvalidNesting) {
1229 fixNesting(ret);
1230 }
1231
1232 normaliseNewLines(ret, null, preserveNewLines);
1233
1234 if (opts.removeEmptyTags) {
1235 removeEmpty(ret);
1236 }
1237
1238 return ret;
1239 };
1240
1241 /**
1242 * Checks if an array of TokenizeToken's contains the
1243 * specified token.
1244 *
1245 * Checks the tokens name and type match another tokens
1246 * name and type in the array.
1247 *
1248 * @param {string} name
1249 * @param {string} type
1250 * @param {array} arr
1251 * @return {Boolean}
1252 * @private
1253 */
1254 function hasTag(name, type, arr) {
1255 var i = arr.length;
1256
1257 while (i--) {
1258 if (arr[i].type === type && arr[i].name === name) {
1259 return true;
1260 }
1261 }
1262
1263 return false;
1264 }
1265
1266 /**
1267 * Checks if the child tag is allowed as one
1268 * of the parent tags children.
1269 *
1270 * @param {TokenizeToken} parent
1271 * @param {TokenizeToken} child
1272 * @return {Boolean}
1273 * @private
1274 */
1275 function isChildAllowed(parent, child) {
1276 var parentBBCode = parent ? bbcodeHandlers[parent.name] : {},
1277 allowedChildren = parentBBCode.allowedChildren;
1278
1279 if (base.opts.fixInvalidChildren && allowedChildren) {
1280 return allowedChildren.indexOf(child.name || '#') > -1;
1281 }
1282
1283 return true;
1284 }
1285
1286 // TODO: Tidy this parseTokens() function up a bit.
1287 /**
1288 * Parses an array of tokens created by tokenize()
1289 *
1290 * @param {array} toks
1291 * @return {array} Parsed tokens
1292 * @see tokenize()
1293 * @private
1294 */
1295 function parseTokens(toks) {
1296 var token, bbcode, curTok, clone, i, next,
1297 cloned = [],
1298 output = [],
1299 openTags = [],
1300 /**
1301 * Returns the currently open tag or undefined
1302 * @return {TokenizeToken}
1303 */
1304 currentTag = function () {
1305 return last(openTags);
1306 },
1307 /**
1308 * Adds a tag to either the current tags children
1309 * or to the output array.
1310 * @param {TokenizeToken} token
1311 * @private
1312 */
1313 addTag = function (token) {
1314 if (currentTag()) {
1315 currentTag().children.push(token);
1316 } else {
1317 output.push(token);
1318 }
1319 },
1320 /**
1321 * Checks if this tag closes the current tag
1322 * @param {string} name
1323 * @return {Void}
1324 */
1325 closesCurrentTag = function (name) {
1326 return currentTag() &&
1327 (bbcode = bbcodeHandlers[currentTag().name]) &&
1328 bbcode.closedBy &&
1329 bbcode.closedBy.indexOf(name) > -1;
1330 };
1331
1332 while ((token = toks.shift())) {
1333 next = toks[0];
1334
1335 /*
1336 * Fixes any invalid children.
1337 *
1338 * If it is an element which isn't allowed as a child of it's
1339 * parent then it will be converted to content of the parent
1340 * element. i.e.
1341 * [code]Code [b]only[/b] allows text.[/code]
1342 * Will become:
1343 * <code>Code [b]only[/b] allows text.</code>
1344 * Instead of:
1345 * <code>Code <b>only</b> allows text.</code>
1346 */
1347 // Ignore tags that can't be children
1348 if (!isChildAllowed(currentTag(), token)) {
1349
1350 // exclude closing tags of current tag
1351 if (token.type !== TOKEN_CLOSE || !currentTag() ||
1352 token.name !== currentTag().name) {
1353 token.name = '#';
1354 token.type = TOKEN_CONTENT;
1355 }
1356 }
1357
1358 switch (token.type) {
1359 case TOKEN_OPEN:
1360 // Check it this closes a parent,
1361 // e.g. for lists [*]one [*]two
1362 if (closesCurrentTag(token.name)) {
1363 openTags.pop();
1364 }
1365
1366 addTag(token);
1367 bbcode = bbcodeHandlers[token.name];
1368
1369 // If this tag is not self closing and it has a closing
1370 // tag then it is open and has children so add it to the
1371 // list of open tags. If has the closedBy property then
1372 // it is closed by other tags so include everything as
1373 // it's children until one of those tags is reached.
1374 if (bbcode && !bbcode.isSelfClosing &&
1375 (bbcode.closedBy ||
1376 hasTag(token.name, TOKEN_CLOSE, toks))) {
1377 openTags.push(token);
1378 } else if (!bbcode || !bbcode.isSelfClosing) {
1379 token.type = TOKEN_CONTENT;
1380 }
1381 break;
1382
1383 case TOKEN_CLOSE:
1384 // check if this closes the current tag,
1385 // e.g. [/list] would close an open [*]
1386 if (currentTag() && token.name !== currentTag().name &&
1387 closesCurrentTag('/' + token.name)) {
1388
1389 openTags.pop();
1390 }
1391
1392 // If this is closing the currently open tag just pop
1393 // the close tag off the open tags array
1394 if (currentTag() && token.name === currentTag().name) {
1395 currentTag().closing = token;
1396 openTags.pop();
1397
1398 // If this is closing an open tag that is the parent of
1399 // the current tag then clone all the tags including the
1400 // current one until reaching the parent that is being
1401 // closed. Close the parent and then add the clones back
1402 // in.
1403 } else if (hasTag(token.name, TOKEN_OPEN, openTags)) {
1404
1405 // Remove the tag from the open tags
1406 while ((curTok = openTags.pop())) {
1407
1408 // If it's the tag that is being closed then
1409 // discard it and break the loop.
1410 if (curTok.name === token.name) {
1411 curTok.closing = token;
1412 break;
1413 }
1414
1415 // Otherwise clone this tag and then add any
1416 // previously cloned tags as it's children
1417 clone = curTok.clone();
1418
1419 if (cloned.length) {
1420 clone.children.push(last(cloned));
1421 }
1422
1423 cloned.push(clone);
1424 }
1425
1426 // Place block linebreak before cloned tags
1427 if (next && next.type === TOKEN_NEWLINE) {
1428 bbcode = bbcodeHandlers[token.name];
1429 if (bbcode && bbcode.isInline === false) {
1430 addTag(next);
1431 toks.shift();
1432 }
1433 }
1434
1435 // Add the last cloned child to the now current tag
1436 // (the parent of the tag which was being closed)
1437 addTag(last(cloned));
1438
1439 // Add all the cloned tags to the open tags list
1440 i = cloned.length;
1441 while (i--) {
1442 openTags.push(cloned[i]);
1443 }
1444
1445 cloned.length = 0;
1446
1447 // This tag is closing nothing so treat it as content
1448 } else {
1449 token.type = TOKEN_CONTENT;
1450 addTag(token);
1451 }
1452 break;
1453
1454 case TOKEN_NEWLINE:
1455 // handle things like
1456 // [*]list\nitem\n[*]list1
1457 // where it should come out as
1458 // [*]list\nitem[/*]\n[*]list1[/*]
1459 // instead of
1460 // [*]list\nitem\n[/*][*]list1[/*]
1461 if (currentTag() && next && closesCurrentTag(
1462 (next.type === TOKEN_CLOSE ? '/' : '') +
1463 next.name
1464 )) {
1465 // skip if the next tag is the closing tag for
1466 // the option tag, i.e. [/*]
1467 if (!(next.type === TOKEN_CLOSE &&
1468 next.name === currentTag().name)) {
1469 bbcode = bbcodeHandlers[currentTag().name];
1470
1471 if (bbcode && bbcode.breakAfter) {
1472 openTags.pop();
1473 } else if (bbcode &&
1474 bbcode.isInline === false &&
1475 base.opts.breakAfterBlock &&
1476 bbcode.breakAfter !== false) {
1477 openTags.pop();
1478 }
1479 }
1480 }
1481
1482 addTag(token);
1483 break;
1484
1485 default: // content
1486 addTag(token);
1487 break;
1488 }
1489 }
1490
1491 return output;
1492 }
1493
1494 /**
1495 * Normalise all new lines
1496 *
1497 * Removes any formatting new lines from the BBCode
1498 * leaving only content ones. I.e. for a list:
1499 *
1500 * [list]
1501 * [*] list item one
1502 * with a line break
1503 * [*] list item two
1504 * [/list]
1505 *
1506 * would become
1507 *
1508 * [list] [*] list item one
1509 * with a line break [*] list item two [/list]
1510 *
1511 * Which makes it easier to convert to HTML or add
1512 * the formatting new lines back in when converting
1513 * back to BBCode
1514 *
1515 * @param {array} children
1516 * @param {TokenizeToken} parent
1517 * @param {boolean} onlyRemoveBreakAfter
1518 * @return {void}
1519 */
1520 function normaliseNewLines(children, parent, onlyRemoveBreakAfter) {
1521 var token, left, right, parentBBCode, bbcode,
1522 removedBreakEnd, removedBreakBefore, remove;
1523 var childrenLength = children.length;
1524 // TODO: this function really needs tidying up
1525 if (parent) {
1526 parentBBCode = bbcodeHandlers[parent.name];
1527 }
1528
1529 var i = childrenLength;
1530 while (i--) {
1531 if (!(token = children[i])) {
1532 continue;
1533 }
1534
1535 if (token.type === TOKEN_NEWLINE) {
1536 left = i > 0 ? children[i - 1] : null;
1537 right = i < childrenLength - 1 ? children[i + 1] : null;
1538 remove = false;
1539
1540 // Handle the start and end new lines
1541 // e.g. [tag]\n and \n[/tag]
1542 if (!onlyRemoveBreakAfter && parentBBCode &&
1543 parentBBCode.isSelfClosing !== true) {
1544 // First child of parent so must be opening line break
1545 // (breakStartBlock, breakStart) e.g. [tag]\n
1546 if (!left) {
1547 if (parentBBCode.isInline === false &&
1548 base.opts.breakStartBlock &&
1549 parentBBCode.breakStart !== false) {
1550 remove = true;
1551 }
1552
1553 if (parentBBCode.breakStart) {
1554 remove = true;
1555 }
1556 // Last child of parent so must be end line break
1557 // (breakEndBlock, breakEnd)
1558 // e.g. \n[/tag]
1559 // remove last line break (breakEndBlock, breakEnd)
1560 } else if (!removedBreakEnd && !right) {
1561 if (parentBBCode.isInline === false &&
1562 base.opts.breakEndBlock &&
1563 parentBBCode.breakEnd !== false) {
1564 remove = true;
1565 }
1566
1567 if (parentBBCode.breakEnd) {
1568 remove = true;
1569 }
1570
1571 removedBreakEnd = remove;
1572 }
1573 }
1574
1575 if (left && left.type === TOKEN_OPEN) {
1576 if ((bbcode = bbcodeHandlers[left.name])) {
1577 if (!onlyRemoveBreakAfter) {
1578 if (bbcode.isInline === false &&
1579 base.opts.breakAfterBlock &&
1580 bbcode.breakAfter !== false) {
1581 remove = true;
1582 }
1583
1584 if (bbcode.breakAfter) {
1585 remove = true;
1586 }
1587 } else if (bbcode.isInline === false) {
1588 remove = true;
1589 }
1590 }
1591 }
1592
1593 if (!onlyRemoveBreakAfter && !removedBreakBefore &&
1594 right && right.type === TOKEN_OPEN) {
1595
1596 if ((bbcode = bbcodeHandlers[right.name])) {
1597 if (bbcode.isInline === false &&
1598 base.opts.breakBeforeBlock &&
1599 bbcode.breakBefore !== false) {
1600 remove = true;
1601 }
1602
1603 if (bbcode.breakBefore) {
1604 remove = true;
1605 }
1606
1607 removedBreakBefore = remove;
1608
1609 if (remove) {
1610 children.splice(i, 1);
1611 continue;
1612 }
1613 }
1614 }
1615
1616 if (remove) {
1617 children.splice(i, 1);
1618 }
1619
1620 // reset double removedBreakBefore removal protection.
1621 // This is needed for cases like \n\n[\tag] where
1622 // only 1 \n should be removed but without this they both
1623 // would be.
1624 removedBreakBefore = false;
1625 } else if (token.type === TOKEN_OPEN) {
1626 normaliseNewLines(token.children, token,
1627 onlyRemoveBreakAfter);
1628 }
1629 }
1630 }
1631
1632 /**
1633 * Fixes any invalid nesting.
1634 *
1635 * If it is a block level element inside 1 or more inline elements
1636 * then those inline elements will be split at the point where the
1637 * block level is and the block level element placed between the split
1638 * parts. i.e.
1639 * [inline]A[blocklevel]B[/blocklevel]C[/inline]
1640 * Will become:
1641 * [inline]A[/inline][blocklevel]B[/blocklevel][inline]C[/inline]
1642 *
1643 * @param {array} children
1644 * @param {array} [parents] Null if there is no parents
1645 * @param {boolea} [insideInline] If inside an inline element
1646 * @param {array} [rootArr] Root array if there is one
1647 * @return {array}
1648 * @private
1649 */
1650 function fixNesting(children, parents, insideInline, rootArr) {
1651 var token, i, parent, parentIndex, parentParentChildren, right;
1652
1653 var isInline = function (token) {
1654 var bbcode = bbcodeHandlers[token.name];
1655
1656 return !bbcode || bbcode.isInline !== false;
1657 };
1658
1659 parents = parents || [];
1660 rootArr = rootArr || children;
1661
1662 // This must check the length each time as it can change when
1663 // tokens are moved to fix the nesting.
1664 for (i = 0; i < children.length; i++) {
1665 if (!(token = children[i]) || token.type !== TOKEN_OPEN) {
1666 continue;
1667 }
1668
1669 if (insideInline && !isInline(token)) {
1670 // if this is a blocklevel element inside an inline one then
1671 // split the parent at the block level element
1672 parent = last(parents);
1673 right = parent.splitAt(token);
1674
1675 parentParentChildren = parents.length > 1 ?
1676 parents[parents.length - 2].children : rootArr;
1677
1678 // If parent inline is allowed inside this tag, clone it and
1679 // wrap this tags children in it.
1680 if (isChildAllowed(token, parent)) {
1681 var clone = parent.clone();
1682 clone.children = token.children;
1683 token.children = [clone];
1684 }
1685
1686 parentIndex = parentParentChildren.indexOf(parent);
1687 if (parentIndex > -1) {
1688 // remove the block level token from the right side of
1689 // the split inline element
1690 right.children.splice(0, 1);
1691
1692 // insert the block level token and the right side after
1693 // the left side of the inline token
1694 parentParentChildren.splice(
1695 parentIndex + 1, 0, token, right
1696 );
1697
1698 // If token is a block and is followed by a newline,
1699 // then move the newline along with it to the new parent
1700 var next = right.children[0];
1701 if (next && next.type === TOKEN_NEWLINE) {
1702 if (!isInline(token)) {
1703 right.children.splice(0, 1);
1704 parentParentChildren.splice(
1705 parentIndex + 2, 0, next
1706 );
1707 }
1708 }
1709
1710 // return to parents loop as the
1711 // children have now increased
1712 return;
1713 }
1714
1715 }
1716
1717 parents.push(token);
1718
1719 fixNesting(
1720 token.children,
1721 parents,
1722 insideInline || isInline(token),
1723 rootArr
1724 );
1725
1726 parents.pop();
1727 }
1728 }
1729
1730 /**
1731 * Removes any empty BBCodes which are not allowed to be empty.
1732 *
1733 * @param {array} tokens
1734 * @private
1735 */
1736 function removeEmpty(tokens) {
1737 var token, bbcode;
1738
1739 /**
1740 * Checks if all children are whitespace or not
1741 * @private
1742 */
1743 var isTokenWhiteSpace = function (children) {
1744 var j = children.length;
1745
1746 while (j--) {
1747 var type = children[j].type;
1748
1749 if (type === TOKEN_OPEN || type === TOKEN_CLOSE) {
1750 return false;
1751 }
1752
1753 if (type === TOKEN_CONTENT &&
1754 /\S|\u00A0/.test(children[j].val)) {
1755 return false;
1756 }
1757 }
1758
1759 return true;
1760 };
1761
1762 var i = tokens.length;
1763 while (i--) {
1764 // So skip anything that isn't a tag since only tags can be
1765 // empty, content can't
1766 if (!(token = tokens[i]) || token.type !== TOKEN_OPEN) {
1767 continue;
1768 }
1769
1770 bbcode = bbcodeHandlers[token.name];
1771
1772 // Remove any empty children of this tag first so that if they
1773 // are all removed this one doesn't think it's not empty.
1774 removeEmpty(token.children);
1775
1776 if (isTokenWhiteSpace(token.children) && bbcode &&
1777 !bbcode.isSelfClosing && !bbcode.allowsEmpty) {
1778 tokens.splice.apply(tokens, [i, 1].concat(token.children));
1779 }
1780 }
1781 }
1782
1783 /**
1784 * Converts a BBCode string to HTML
1785 *
1786 * @param {string} str
1787 * @param {boolean} preserveNewLines If to preserve all new lines, not
1788 * strip any based on the passed
1789 * formatting options
1790 * @return {string}
1791 * @memberOf BBCodeParser.prototype
1792 */
1793 base.toHTML = function (str, preserveNewLines) {
1794 return convertToHTML(base.parse(str, preserveNewLines), true);
1795 };
1796
1797 /**
1798 * @private
1799 */
1800 function convertToHTML(tokens, isRoot) {
1801 var undef, token, bbcode, content, html, needsBlockWrap,
1802 blockWrapOpen, isInline, lastChild,
1803 ret = '';
1804
1805 isInline = function (bbcode) {
1806 return (!bbcode || (bbcode.isHtmlInline !== undef ?
1807 bbcode.isHtmlInline : bbcode.isInline)) !== false;
1808 };
1809
1810 while (tokens.length > 0) {
1811 if (!(token = tokens.shift())) {
1812 continue;
1813 }
1814
1815 if (token.type === TOKEN_OPEN) {
1816 lastChild = token.children[token.children.length - 1] || {};
1817 bbcode = bbcodeHandlers[token.name];
1818 needsBlockWrap = isRoot && isInline(bbcode);
1819 content = convertToHTML(token.children, false);
1820
1821 if (bbcode && bbcode.html) {
1822 // Only add a line break to the end if this is
1823 // blocklevel and the last child wasn't block-level
1824 if (!isInline(bbcode) &&
1825 isInline(bbcodeHandlers[lastChild.name]) &&
1826 !bbcode.isPreFormatted &&
1827 !bbcode.skipLastLineBreak) {
1828 // Add placeholder br to end of block level
1829 // elements
1830 content += '<br />';
1831 }
1832
1833 if (!isFunction(bbcode.html)) {
1834 token.attrs['0'] = content;
1835 html = formatBBCodeString(
1836 bbcode.html,
1837 token.attrs
1838 );
1839 } else {
1840 html = bbcode.html.call(
1841 base,
1842 token,
1843 token.attrs,
1844 content
1845 );
1846 }
1847 } else {
1848 html = token.val + content +
1849 (token.closing ? token.closing.val : '');
1850 }
1851 } else if (token.type === TOKEN_NEWLINE) {
1852 if (!isRoot) {
1853 ret += '<br />';
1854 continue;
1855 }
1856
1857 // If not already in a block wrap then start a new block
1858 if (!blockWrapOpen) {
1859 ret += '<div>';
1860 }
1861
1862 ret += '<br />';
1863
1864 // Normally the div acts as a line-break with by moving
1865 // whatever comes after onto a new line.
1866 // If this is the last token, add an extra line-break so it
1867 // shows as there will be nothing after it.
1868 if (!tokens.length) {
1869 ret += '<br />';
1870 }
1871
1872 ret += '</div>\n';
1873 blockWrapOpen = false;
1874 continue;
1875 // content
1876 } else {
1877 needsBlockWrap = isRoot;
1878 html = escapeEntities(token.val, true);
1879 }
1880
1881 if (needsBlockWrap && !blockWrapOpen) {
1882 ret += '<div>';
1883 blockWrapOpen = true;
1884 } else if (!needsBlockWrap && blockWrapOpen) {
1885 ret += '</div>\n';
1886 blockWrapOpen = false;
1887 }
1888
1889 ret += html;
1890 }
1891
1892 if (blockWrapOpen) {
1893 ret += '</div>\n';
1894 }
1895
1896 return ret;
1897 }
1898
1899 /**
1900 * Takes a BBCode string, parses it then converts it back to BBCode.
1901 *
1902 * This will auto fix the BBCode and format it with the specified
1903 * options.
1904 *
1905 * @param {string} str
1906 * @param {boolean} preserveNewLines If to preserve all new lines, not
1907 * strip any based on the passed
1908 * formatting options
1909 * @return {string}
1910 * @memberOf BBCodeParser.prototype
1911 */
1912 base.toBBCode = function (str, preserveNewLines) {
1913 return convertToBBCode(base.parse(str, preserveNewLines));
1914 };
1915
1916 /**
1917 * Converts parsed tokens back into BBCode with the
1918 * formatting specified in the options and with any
1919 * fixes specified.
1920 *
1921 * @param {array} toks Array of parsed tokens from base.parse()
1922 * @return {string}
1923 * @private
1924 */
1925 function convertToBBCode(toks) {
1926 var token, attr, bbcode, isBlock, isSelfClosing, quoteType,
1927 breakBefore, breakStart, breakEnd, breakAfter,
1928 ret = '';
1929
1930 while (toks.length > 0) {
1931 if (!(token = toks.shift())) {
1932 continue;
1933 }
1934 // TODO: tidy this
1935 bbcode = bbcodeHandlers[token.name];
1936 isBlock = !(!bbcode || bbcode.isInline !== false);
1937 isSelfClosing = bbcode && bbcode.isSelfClosing;
1938
1939 breakBefore = (isBlock && base.opts.breakBeforeBlock &&
1940 bbcode.breakBefore !== false) ||
1941 (bbcode && bbcode.breakBefore);
1942
1943 breakStart = (isBlock && !isSelfClosing &&
1944 base.opts.breakStartBlock &&
1945 bbcode.breakStart !== false) ||
1946 (bbcode && bbcode.breakStart);
1947
1948 breakEnd = (isBlock && base.opts.breakEndBlock &&
1949 bbcode.breakEnd !== false) ||
1950 (bbcode && bbcode.breakEnd);
1951
1952 breakAfter = (isBlock && base.opts.breakAfterBlock &&
1953 bbcode.breakAfter !== false) ||
1954 (bbcode && bbcode.breakAfter);
1955
1956 quoteType = (bbcode ? bbcode.quoteType : null) ||
1957 base.opts.quoteType || QuoteType.auto;
1958
1959 if (!bbcode && token.type === TOKEN_OPEN) {
1960 ret += token.val;
1961
1962 if (token.children) {
1963 ret += convertToBBCode(token.children);
1964 }
1965
1966 if (token.closing) {
1967 ret += token.closing.val;
1968 }
1969 } else if (token.type === TOKEN_OPEN) {
1970 if (breakBefore) {
1971 ret += '\n';
1972 }
1973
1974 // Convert the tag and it's attributes to BBCode
1975 ret += '[' + token.name;
1976 if (token.attrs) {
1977 if (token.attrs.defaultattr) {
1978 ret += '=' + quote(
1979 token.attrs.defaultattr,
1980 quoteType,
1981 'defaultattr'
1982 );
1983
1984 delete token.attrs.defaultattr;
1985 }
1986
1987 for (attr in token.attrs) {
1988 if (token.attrs.hasOwnProperty(attr)) {
1989 ret += ' ' + attr + '=' +
1990 quote(token.attrs[attr], quoteType, attr);
1991 }
1992 }
1993 }
1994 ret += ']';
1995
1996 if (breakStart) {
1997 ret += '\n';
1998 }
1999
2000 // Convert the tags children to BBCode
2001 if (token.children) {
2002 ret += convertToBBCode(token.children);
2003 }
2004
2005 // add closing tag if not self closing
2006 if (!isSelfClosing && !bbcode.excludeClosing) {
2007 if (breakEnd) {
2008 ret += '\n';
2009 }
2010
2011 ret += '[/' + token.name + ']';
2012 }
2013
2014 if (breakAfter) {
2015 ret += '\n';
2016 }
2017
2018 // preserve whatever was recognized as the
2019 // closing tag if it is a self closing tag
2020 if (token.closing && isSelfClosing) {
2021 ret += token.closing.val;
2022 }
2023 } else {
2024 ret += token.val;
2025 }
2026 }
2027
2028 return ret;
2029 }
2030
2031 /**
2032 * Quotes an attribute
2033 *
2034 * @param {string} str
2035 * @param {BBCodeParser.QuoteType} quoteType
2036 * @param {string} name
2037 * @return {string}
2038 * @private
2039 */
2040 function quote(str, quoteType, name) {
2041 var needsQuotes = /\s|=/.test(str);
2042
2043 if (isFunction(quoteType)) {
2044 return quoteType(str, name);
2045 }
2046
2047 if (quoteType === QuoteType.never ||
2048 (quoteType === QuoteType.auto && !needsQuotes)) {
2049 return str;
2050 }
2051
2052 return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
2053 }
2054
2055 /**
2056 * Returns the last element of an array or null
2057 *
2058 * @param {array} arr
2059 * @return {Object} Last element
2060 * @private
2061 */
2062 function last(arr) {
2063 if (arr.length) {
2064 return arr[arr.length - 1];
2065 }
2066
2067 return null;
2068 }
2069
2070 /**
2071 * Converts a string to lowercase.
2072 *
2073 * @param {string} str
2074 * @return {string} Lowercase version of str
2075 * @private
2076 */
2077 function lower(str) {
2078 return str.toLowerCase();
2079 }
2080 };
2081
2082 /**
2083 * Quote type
2084 * @type {Object}
2085 * @class QuoteType
2086 * @name BBCodeParser.QuoteType
2087 * @since 1.4.0
2088 */
2089 BBCodeParser.QuoteType = QuoteType;
2090
2091 /**
2092 * Default BBCode parser options
2093 * @type {Object}
2094 */
2095 BBCodeParser.defaults = {
2096 /**
2097 * If to add a new line before block level elements
2098 *
2099 * @type {Boolean}
2100 */
2101 breakBeforeBlock: false,
2102
2103 /**
2104 * If to add a new line after the start of block level elements
2105 *
2106 * @type {Boolean}
2107 */
2108 breakStartBlock: false,
2109
2110 /**
2111 * If to add a new line before the end of block level elements
2112 *
2113 * @type {Boolean}
2114 */
2115 breakEndBlock: false,
2116
2117 /**
2118 * If to add a new line after block level elements
2119 *
2120 * @type {Boolean}
2121 */
2122 breakAfterBlock: true,
2123
2124 /**
2125 * If to remove empty tags
2126 *
2127 * @type {Boolean}
2128 */
2129 removeEmptyTags: true,
2130
2131 /**
2132 * If to fix invalid nesting,
2133 * i.e. block level elements inside inline elements.
2134 *
2135 * @type {Boolean}
2136 */
2137 fixInvalidNesting: true,
2138
2139 /**
2140 * If to fix invalid children.
2141 * i.e. A tag which is inside a parent that doesn't
2142 * allow that type of tag.
2143 *
2144 * @type {Boolean}
2145 */
2146 fixInvalidChildren: true,
2147
2148 /**
2149 * Attribute quote type
2150 *
2151 * @type {BBCodeParser.QuoteType}
2152 * @since 1.4.1
2153 */
2154 quoteType: QuoteType.auto,
2155
2156 /**
2157 * Whether to use strict matching on attributes and styles.
2158 *
2159 * When true this will perform AND matching requiring all tag
2160 * attributes and styles to match.
2161 *
2162 * When false will perform OR matching and will match if any of
2163 * a tags attributes or styles match.
2164 *
2165 * @type {Boolean}
2166 * @since 3.1.0
2167 */
2168 strictMatch: false
2169 };
2170
2171 /**
2172 * Converts a number 0-255 to hex.
2173 *
2174 * Will return 00 if number is not a valid number.
2175 *
2176 * @param {any} number
2177 * @return {string}
2178 * @private
2179 */
2180 function toHex(number) {
2181 number = parseInt(number, 10);
2182
2183 if (isNaN(number)) {
2184 return '00';
2185 }
2186
2187 number = Math.max(0, Math.min(number, 255)).toString(16);
2188
2189 return number.length < 2 ? '0' + number : number;
2190 }
2191 /**
2192 * Normalises a CSS colour to hex #xxxxxx format
2193 *
2194 * @param {string} colorStr
2195 * @return {string}
2196 * @private
2197 */
2198 function _normaliseColour(colorStr) {
2199 var match;
2200
2201 colorStr = colorStr || '#000';
2202
2203 // rgb(n,n,n);
2204 if ((match =
2205 colorStr.match(/rgb\((\d{1,3}),\s*?(\d{1,3}),\s*?(\d{1,3})\)/i))) {
2206 return '#' +
2207 toHex(match[1]) +
2208 toHex(match[2]) +
2209 toHex(match[3]);
2210 }
2211
2212 // expand shorthand
2213 if ((match = colorStr.match(/#([0-f])([0-f])([0-f])\s*?$/i))) {
2214 return '#' +
2215 match[1] + match[1] +
2216 match[2] + match[2] +
2217 match[3] + match[3];
2218 }
2219
2220 return colorStr;
2221 }
2222
2223 /**
2224 * SCEditor BBCode format
2225 * @since 2.0.0
2226 */
2227 function bbcodeFormat() {
2228 var base = this;
2229
2230 base.stripQuotes = _stripQuotes;
2231
2232 /**
2233 * cache of all the tags pointing to their bbcodes to enable
2234 * faster lookup of which bbcode a tag should have
2235 * @private
2236 */
2237 var tagsToBBCodes = {};
2238
2239 /**
2240 * Allowed children of specific HTML tags. Empty array if no
2241 * children other than text nodes are allowed
2242 * @private
2243 */
2244 var validChildren = {
2245 ul: ['li', 'ol', 'ul'],
2246 ol: ['li', 'ol', 'ul'],
2247 table: ['tr'],
2248 tr: ['td', 'th'],
2249 code: ['br', 'p', 'div']
2250 };
2251
2252 /**
2253 * Populates tagsToBBCodes and stylesToBBCodes for easier lookups
2254 *
2255 * @private
2256 */
2257 function buildBbcodeCache() {
2258 each(bbcodeHandlers, function (bbcode, handler) {
2259 var
2260 isBlock = handler.isInline === false,
2261 tags = bbcodeHandlers[bbcode].tags,
2262 styles = bbcodeHandlers[bbcode].styles;
2263
2264 if (styles) {
2265 tagsToBBCodes['*'] = tagsToBBCodes['*'] || {};
2266 tagsToBBCodes['*'][isBlock] =
2267 tagsToBBCodes['*'][isBlock] || {};
2268 tagsToBBCodes['*'][isBlock][bbcode] = [
2269 ['style', Object.entries(styles)]
2270 ];
2271 }
2272
2273 if (tags) {
2274 each(tags, function (tag, values) {
2275 if (values && values.style) {
2276 values.style = Object.entries(values.style);
2277 }
2278
2279 tagsToBBCodes[tag] = tagsToBBCodes[tag] || {};
2280 tagsToBBCodes[tag][isBlock] =
2281 tagsToBBCodes[tag][isBlock] || {};
2282 tagsToBBCodes[tag][isBlock][bbcode] =
2283 values && Object.entries(values);
2284 });
2285 }
2286 });
2287 };
2288
2289 /**
2290 * Handles adding newlines after block level elements
2291 *
2292 * @param {HTMLElement} element The element to convert
2293 * @param {string} content The tags text content
2294 * @return {string}
2295 * @private
2296 */
2297 function handleBlockNewlines(element, content) {
2298 var tag = element.nodeName.toLowerCase();
2299 var isInline = dom.isInline;
2300 if (!isInline(element, true) || tag === 'br') {
2301 var isLastBlockChild, parent, parentLastChild,
2302 previousSibling = element.previousSibling;
2303
2304 // Skips selection makers and ignored elements
2305 // Skip empty inline elements
2306 while (previousSibling &&
2307 previousSibling.nodeType === 1 &&
2308 !is(previousSibling, 'br') &&
2309 isInline(previousSibling, true) &&
2310 !previousSibling.firstChild) {
2311 previousSibling = previousSibling.previousSibling;
2312 }
2313
2314 // If it's the last block of an inline that is the last
2315 // child of a block then it shouldn't cause a line break
2316 // <block><inline><br></inline></block>
2317 do {
2318 parent = element.parentNode;
2319 parentLastChild = parent && parent.lastChild;
2320
2321 isLastBlockChild = parentLastChild === element;
2322 element = parent;
2323 } while (parent && isLastBlockChild && isInline(parent, true));
2324
2325 // If this block is:
2326 // * Not the last child of a block level element
2327 // * Is a <li> tag (lists are blocks)
2328 if (!isLastBlockChild || tag === 'li') {
2329 content += '\n';
2330 }
2331
2332 // Check for:
2333 // <block>text<block>text</block></block>
2334 //
2335 // The second opening <block> opening tag should cause a
2336 // line break because the previous sibing is inline.
2337 if (tag !== 'br' && previousSibling &&
2338 !is(previousSibling, 'br') &&
2339 isInline(previousSibling, true)) {
2340 content = '\n' + content;
2341 }
2342 }
2343
2344 return content;
2345 }
2346
2347 /**
2348 * Handles a HTML tag and finds any matching BBCodes
2349 *
2350 * @param {HTMLElement} element The element to convert
2351 * @param {string} content The Tags text content
2352 * @param {boolean} blockLevel
2353 * @return {string} Content with any matching BBCode tags
2354 * wrapped around it.
2355 * @private
2356 */
2357 function handleTags(element, content, blockLevel) {
2358 function isStyleMatch(style) {
2359 var property = style[0];
2360 var values = style[1];
2361 var val = dom.getStyle(element, property);
2362 var parent = element.parentNode;
2363
2364 // if the parent has the same style use that instead of this one
2365 // so you don't end up with [i]parent[i]child[/i][/i]
2366 if (!val || parent && dom.hasStyle(parent, property, val)) {
2367 return false;
2368 }
2369
2370 return !values || values.includes(val);
2371 }
2372
2373 function createAttributeMatch(isStrict) {
2374 return function (attribute) {
2375 var name = attribute[0];
2376 var value = attribute[1];
2377
2378 // code tags should skip most styles
2379 if (name === 'style' && element.nodeName === 'CODE') {
2380 return false;
2381 }
2382
2383 if (name === 'style' && value) {
2384 return value[isStrict ? 'every' : 'some'](isStyleMatch);
2385 } else {
2386 var val = attr(element, name);
2387
2388 return val && (!value || value.includes(val));
2389 }
2390 };
2391 }
2392
2393 function handleTag(tag) {
2394 if (!tagsToBBCodes[tag] || !tagsToBBCodes[tag][blockLevel]) {
2395 return;
2396 }
2397
2398 // loop all bbcodes for this tag
2399 each(tagsToBBCodes[tag][blockLevel], function (bbcode, attrs) {
2400 var fn, format,
2401 isStrict = bbcodeHandlers[bbcode].strictMatch;
2402
2403 if (typeof isStrict === 'undefined') {
2404 isStrict = base.opts.strictMatch;
2405 }
2406
2407 // Skip if the element doesn't have the attribute or the
2408 // attribute doesn't match one of the required values
2409 fn = isStrict ? 'every' : 'some';
2410 if (attrs && !attrs[fn](createAttributeMatch(isStrict))) {
2411 return;
2412 }
2413
2414 format = bbcodeHandlers[bbcode].format;
2415 if (isFunction(format)) {
2416 content = format.call(base, element, content);
2417 } else {
2418 content = _formatString(format, content);
2419 }
2420 return false;
2421 });
2422 }
2423
2424 handleTag('*');
2425 handleTag(element.nodeName.toLowerCase());
2426 return content;
2427 }
2428
2429 /**
2430 * Converts a HTML dom element to BBCode starting from
2431 * the innermost element and working backwards
2432 *
2433 * @private
2434 * @param {HTMLElement} element
2435 * @return {string} BBCode
2436 * @memberOf SCEditor.plugins.bbcode.prototype
2437 */
2438 function elementToBbcode(element) {
2439 var toBBCode = function (node, vChildren) {
2440 var ret = '';
2441
2442 dom.traverse(node, function (node) {
2443 var content = '',
2444 nodeType = node.nodeType,
2445 tag = node.nodeName.toLowerCase(),
2446 vChild = validChildren[tag],
2447 firstChild = node.firstChild,
2448 isValidChild = true;
2449
2450 if (typeof vChildren === 'object') {
2451 isValidChild = vChildren.indexOf(tag) > -1;
2452
2453 // Emoticons should always be converted
2454 if (is(node, 'img') && attr(node, EMOTICON_DATA_ATTR)) {
2455 isValidChild = true;
2456 }
2457
2458 // if this tag is one of the parents allowed children
2459 // then set this tags allowed children to whatever it
2460 // allows, otherwise set to what the parent allows
2461 if (!isValidChild) {
2462 vChild = vChildren;
2463 }
2464 }
2465
2466 // 3 = text and 1 = element
2467 if (nodeType !== 3 && nodeType !== 1) {
2468 return;
2469 }
2470
2471 if (nodeType === 1) {
2472 // skip empty nlf elements (new lines automatically
2473 // added after block level elements like quotes)
2474 if (is(node, '.sceditor-nlf') && !firstChild) {
2475 return;
2476 }
2477
2478 // don't convert iframe contents
2479 if (tag !== 'iframe') {
2480 content = toBBCode(node, vChild);
2481 }
2482
2483 // TODO: isValidChild is no longer needed. Should use
2484 // valid children bbcodes instead by creating BBCode
2485 // tokens like the parser.
2486 if (isValidChild) {
2487 // code tags should skip most styles
2488 if (tag !== 'code') {
2489 // First parse inline codes
2490 content = handleTags(node, content, false);
2491 }
2492
2493 content = handleTags(node, content, true);
2494 ret += handleBlockNewlines(node, content);
2495 } else {
2496 ret += content;
2497 }
2498 } else {
2499 ret += node.nodeValue;
2500 }
2501 }, false, true);
2502
2503 return ret;
2504 };
2505
2506 return toBBCode(element);
2507 };
2508
2509 /**
2510 * Initializer
2511 * @private
2512 */
2513 base.init = function () {
2514 base.opts = this.opts;
2515 base.elementToBbcode = elementToBbcode;
2516
2517 // build the BBCode cache
2518 buildBbcodeCache();
2519
2520 this.commands = extend(
2521 true, {}, defaultCommandsOverrides, this.commands
2522 );
2523
2524 // Add BBCode helper methods
2525 this.toBBCode = base.toSource;
2526 this.fromBBCode = base.toHtml;
2527 };
2528
2529 /**
2530 * Converts BBCode into HTML
2531 *
2532 * @param {boolean} asFragment
2533 * @param {string} source
2534 * @param {boolean} [legacyAsFragment] Used by fromBBCode() method
2535 */
2536 function toHtml(asFragment, source, legacyAsFragment) {
2537 var parser = new BBCodeParser(base.opts.parserOptions);
2538 var html = parser.toHTML(
2539 base.opts.bbcodeTrim ? source.trim() : source
2540 );
2541
2542 return (asFragment || legacyAsFragment) ?
2543 removeFirstLastDiv(html) : html;
2544 }
2545
2546 /**
2547 * Converts HTML into BBCode
2548 *
2549 * @param {boolean} asFragment
2550 * @param {string} html
2551 * @param {!Document} [context]
2552 * @param {!HTMLElement} [parent]
2553 * @return {string}
2554 * @private
2555 */
2556 function toSource(asFragment, html, context, parent) {
2557 context = context || document;
2558
2559 var bbcode, elements;
2560 var containerParent = context.createElement('div');
2561 var container = context.createElement('div');
2562 var parser = new BBCodeParser(base.opts.parserOptions);
2563
2564 container.innerHTML = html;
2565 css(containerParent, 'visibility', 'hidden');
2566 containerParent.appendChild(container);
2567 context.body.appendChild(containerParent);
2568
2569 if (asFragment) {
2570 // Add text before and after so removeWhiteSpace doesn't remove
2571 // leading and trailing whitespace
2572 containerParent.insertBefore(
2573 context.createTextNode('#'),
2574 containerParent.firstChild
2575 );
2576 containerParent.appendChild(context.createTextNode('#'));
2577 }
2578
2579 // Match parents white-space handling
2580 if (parent) {
2581 css(container, 'whiteSpace', css(parent, 'whiteSpace'));
2582 }
2583
2584 // Remove all nodes with sceditor-ignore class
2585 elements = container.getElementsByClassName('sceditor-ignore');
2586 while (elements.length) {
2587 elements[0].parentNode.removeChild(elements[0]);
2588 }
2589
2590 dom.removeWhiteSpace(containerParent);
2591
2592 bbcode = elementToBbcode(container);
2593
2594 context.body.removeChild(containerParent);
2595
2596 bbcode = parser.toBBCode(bbcode, true);
2597
2598 if (base.opts.bbcodeTrim) {
2599 bbcode = bbcode.trim();
2600 }
2601
2602 return bbcode;
2603 };
2604
2605 base.toHtml = toHtml.bind(null, false);
2606 base.fragmentToHtml = toHtml.bind(null, true);
2607 base.toSource = toSource.bind(null, false);
2608 base.fragmentToSource = toSource.bind(null, true);
2609 };
2610
2611 /**
2612 * Gets a BBCode
2613 *
2614 * @param {string} name
2615 * @return {Object|null}
2616 * @since 2.0.0
2617 */
2618 bbcodeFormat.get = function (name) {
2619 return bbcodeHandlers[name] || null;
2620 };
2621
2622 /**
2623 * Adds a BBCode to the parser or updates an existing
2624 * BBCode if a BBCode with the specified name already exists.
2625 *
2626 * @param {string} name
2627 * @param {Object} bbcode
2628 * @return {this}
2629 * @since 2.0.0
2630 */
2631 bbcodeFormat.set = function (name, bbcode) {
2632 if (name && bbcode) {
2633 // merge any existing command properties
2634 bbcode = extend(bbcodeHandlers[name] || {}, bbcode);
2635
2636 bbcode.remove = function () {
2637 delete bbcodeHandlers[name];
2638 };
2639
2640 bbcodeHandlers[name] = bbcode;
2641 }
2642
2643 return this;
2644 };
2645
2646 /**
2647 * Renames a BBCode
2648 *
2649 * This does not change the format or HTML handling, those must be
2650 * changed manually.
2651 *
2652 * @param {string} name [description]
2653 * @param {string} newName [description]
2654 * @return {this|false}
2655 * @since 2.0.0
2656 */
2657 bbcodeFormat.rename = function (name, newName) {
2658 if (name in bbcodeHandlers) {
2659 bbcodeHandlers[newName] = bbcodeHandlers[name];
2660
2661 delete bbcodeHandlers[name];
2662 }
2663
2664 return this;
2665 };
2666
2667 /**
2668 * Removes a BBCode
2669 *
2670 * @param {string} name
2671 * @return {this}
2672 * @since 2.0.0
2673 */
2674 bbcodeFormat.remove = function (name) {
2675 if (name in bbcodeHandlers) {
2676 delete bbcodeHandlers[name];
2677 }
2678
2679 return this;
2680 };
2681
2682 bbcodeFormat.formatBBCodeString = formatBBCodeString;
2683
2684 sceditor.formats.bbcode = bbcodeFormat;
2685 sceditor.BBCodeParser = BBCodeParser;
2686 }(sceditor));