Mercurial Hosting > sceditor
comparison src/formats/bbcode.js @ 4:b7725dab7482
move /development/* to /
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 04 Aug 2022 17:59:02 -0600 |
parents | src/development/formats/bbcode.js@4c4fc447baea |
children | 0cb206904499 |
comparison
equal
deleted
inserted
replaced
3:ec68006a495e | 4:b7725dab7482 |
---|---|
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)); |