comparison src/development/formats/xhtml.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 XHTML Plugin
3 * http://www.sceditor.com/
4 *
5 * Copyright (C) 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 * @author Sam Clarke
11 */
12 (function (sceditor) {
13 'use strict';
14
15 var dom = sceditor.dom;
16 var utils = sceditor.utils;
17
18 var css = dom.css;
19 var attr = dom.attr;
20 var is = dom.is;
21 var removeAttr = dom.removeAttr;
22 var convertElement = dom.convertElement;
23 var extend = utils.extend;
24 var each = utils.each;
25 var isEmptyObject = utils.isEmptyObject;
26
27 var getEditorCommand = sceditor.command.get;
28
29 var defaultCommandsOverrides = {
30 bold: {
31 txtExec: ['<strong>', '</strong>']
32 },
33 italic: {
34 txtExec: ['<em>', '</em>']
35 },
36 underline: {
37 txtExec: ['<span style="text-decoration:underline;">', '</span>']
38 },
39 strike: {
40 txtExec: ['<span style="text-decoration:line-through;">', '</span>']
41 },
42 subscript: {
43 txtExec: ['<sub>', '</sub>']
44 },
45 superscript: {
46 txtExec: ['<sup>', '</sup>']
47 },
48 left: {
49 txtExec: ['<div style="text-align:left;">', '</div>']
50 },
51 center: {
52 txtExec: ['<div style="text-align:center;">', '</div>']
53 },
54 right: {
55 txtExec: ['<div style="text-align:right;">', '</div>']
56 },
57 justify: {
58 txtExec: ['<div style="text-align:justify;">', '</div>']
59 },
60 font: {
61 txtExec: function (caller) {
62 var editor = this;
63
64 getEditorCommand('font')._dropDown(
65 editor,
66 caller,
67 function (font) {
68 editor.insertText('<span style="font-family:' +
69 font + ';">', '</span>');
70 }
71 );
72 }
73 },
74 size: {
75 txtExec: function (caller) {
76 var editor = this;
77
78 getEditorCommand('size')._dropDown(
79 editor,
80 caller,
81 function (size) {
82 editor.insertText('<span style="font-size:' +
83 size + ';">', '</span>');
84 }
85 );
86 }
87 },
88 color: {
89 txtExec: function (caller) {
90 var editor = this;
91
92 getEditorCommand('color')._dropDown(
93 editor,
94 caller,
95 function (color) {
96 editor.insertText('<span style="color:' +
97 color + ';">', '</span>');
98 }
99 );
100 }
101 },
102 bulletlist: {
103 txtExec: ['<ul><li>', '</li></ul>']
104 },
105 orderedlist: {
106 txtExec: ['<ol><li>', '</li></ol>']
107 },
108 table: {
109 txtExec: ['<table><tr><td>', '</td></tr></table>']
110 },
111 horizontalrule: {
112 txtExec: ['<hr />']
113 },
114 code: {
115 txtExec: ['<code>', '</code>']
116 },
117 image: {
118 txtExec: function (caller, selected) {
119 var editor = this;
120
121 getEditorCommand('image')._dropDown(
122 editor,
123 caller,
124 selected,
125 function (url, width, height) {
126 var attrs = '';
127
128 if (width) {
129 attrs += ' width="' + width + '"';
130 }
131
132 if (height) {
133 attrs += ' height="' + height + '"';
134 }
135
136 editor.insertText(
137 '<img' + attrs + ' src="' + url + '" />'
138 );
139 }
140 );
141 }
142 },
143 email: {
144 txtExec: function (caller, selected) {
145 var editor = this;
146
147 getEditorCommand('email')._dropDown(
148 editor,
149 caller,
150 function (url, text) {
151 editor.insertText(
152 '<a href="mailto:' + url + '">' +
153 (text || selected || url) +
154 '</a>'
155 );
156 }
157 );
158 }
159 },
160 link: {
161 txtExec: function (caller, selected) {
162 var editor = this;
163
164 getEditorCommand('link')._dropDown(
165 editor,
166 caller,
167 function (url, text) {
168 editor.insertText(
169 '<a href="' + url + '">' +
170 (text || selected || url) +
171 '</a>'
172 );
173 }
174 );
175 }
176 },
177 quote: {
178 txtExec: ['<blockquote>', '</blockquote>']
179 },
180 youtube: {
181 txtExec: function (caller) {
182 var editor = this;
183
184 getEditorCommand('youtube')._dropDown(
185 editor,
186 caller,
187 function (id, time) {
188 editor.insertText(
189 '<iframe width="560" height="315" ' +
190 'src="https://www.youtube.com/embed/{id}?' +
191 'wmode=opaque&start=' + time + '" ' +
192 'data-youtube-id="' + id + '" ' +
193 'frameborder="0" allowfullscreen></iframe>'
194 );
195 }
196 );
197 }
198 },
199 rtl: {
200 txtExec: ['<div stlye="direction:rtl;">', '</div>']
201 },
202 ltr: {
203 txtExec: ['<div stlye="direction:ltr;">', '</div>']
204 }
205 };
206
207 /**
208 * XHTMLSerializer part of the XHTML plugin.
209 *
210 * @class XHTMLSerializer
211 * @name jQuery.sceditor.XHTMLSerializer
212 * @since v1.4.1
213 */
214 sceditor.XHTMLSerializer = function () {
215 var base = this;
216
217 var opts = {
218 indentStr: '\t'
219 };
220
221 /**
222 * Array containing the output, used as it's faster
223 * than string concatenation in slow browsers.
224 * @type {Array}
225 * @private
226 */
227 var outputStringBuilder = [];
228
229 /**
230 * Current indention level
231 * @type {number}
232 * @private
233 */
234 var currentIndent = 0;
235
236 // TODO: use escape.entities
237 /**
238 * Escapes XHTML entities
239 *
240 * @param {string} str
241 * @return {string}
242 * @private
243 */
244 function escapeEntities(str) {
245 var entities = {
246 '&': '&amp;',
247 '<': '&lt;',
248 '>': '&gt;',
249 '"': '&quot;',
250 '\xa0': '&nbsp;'
251 };
252
253 return !str ? '' : str.replace(/[&<>"\xa0]/g, function (entity) {
254 return entities[entity] || entity;
255 });
256 };
257
258 /**
259 * Replace spaces including newlines with a single
260 * space except for non-breaking spaces
261 *
262 * @param {string} str
263 * @return {string}
264 * @private
265 */
266 function trim(str) {
267 return str.replace(/[^\S\u00A0]+/g, ' ');
268 };
269
270 /**
271 * Serializes a node to XHTML
272 *
273 * @param {Node} node Node to serialize
274 * @param {boolean} onlyChildren If to only serialize the nodes
275 * children and not the node
276 * itself
277 * @return {string} The serialized node
278 * @name serialize
279 * @memberOf jQuery.sceditor.XHTMLSerializer.prototype
280 * @since v1.4.1
281 */
282 base.serialize = function (node, onlyChildren) {
283 outputStringBuilder = [];
284
285 if (onlyChildren) {
286 node = node.firstChild;
287
288 while (node) {
289 serializeNode(node);
290 node = node.nextSibling;
291 }
292 } else {
293 serializeNode(node);
294 }
295
296 return outputStringBuilder.join('');
297 };
298
299 /**
300 * Serializes a node to the outputStringBuilder
301 *
302 * @param {Node} node
303 * @return {void}
304 * @private
305 */
306 function serializeNode(node, parentIsPre) {
307 switch (node.nodeType) {
308 case 1: // element
309 handleElement(node, parentIsPre);
310 break;
311
312 case 3: // text
313 handleText(node, parentIsPre);
314 break;
315
316 case 4: // cdata section
317 handleCdata(node);
318 break;
319
320 case 8: // comment
321 handleComment(node);
322 break;
323
324 case 9: // document
325 case 11: // document fragment
326 handleDoc(node);
327 break;
328
329 // Ignored types
330 case 2: // attribute
331 case 5: // entity ref
332 case 6: // entity
333 case 7: // processing instruction
334 case 10: // document type
335 case 12: // notation
336 break;
337 }
338 };
339
340 /**
341 * Handles doc node
342 * @param {Node} node
343 * @return {void}
344 * @private
345 */
346 function handleDoc(node) {
347 var child = node.firstChild;
348
349 while (child) {
350 serializeNode(child);
351 child = child.nextSibling;
352 }
353 };
354
355 /**
356 * Handles element nodes
357 * @param {Node} node
358 * @return {void}
359 * @private
360 */
361 function handleElement(node, parentIsPre) {
362 var child, attr, attrValue,
363 tagName = node.nodeName.toLowerCase(),
364 isIframe = tagName === 'iframe',
365 attrIdx = node.attributes.length,
366 firstChild = node.firstChild,
367 // pre || pre-wrap with any vendor prefix
368 isPre = parentIsPre ||
369 /pre(?:\-wrap)?$/i.test(css(node, 'whiteSpace')),
370 selfClosing = !node.firstChild && !dom.canHaveChildren(node) &&
371 !isIframe;
372
373 if (is(node, '.sceditor-ignore')) {
374 return;
375 }
376
377 output('<' + tagName, !parentIsPre && canIndent(node));
378 while (attrIdx--) {
379 attr = node.attributes[attrIdx];
380
381 attrValue = attr.value;
382
383 output(' ' + attr.name.toLowerCase() + '="' +
384 escapeEntities(attrValue) + '"', false);
385 }
386 output(selfClosing ? ' />' : '>', false);
387
388 if (!isIframe) {
389 child = firstChild;
390 }
391
392 while (child) {
393 currentIndent++;
394
395 serializeNode(child, isPre);
396 child = child.nextSibling;
397
398 currentIndent--;
399 }
400
401 if (!selfClosing) {
402 output(
403 '</' + tagName + '>',
404 !isPre && !isIframe && canIndent(node) &&
405 firstChild && canIndent(firstChild)
406 );
407 }
408 };
409
410 /**
411 * Handles CDATA nodes
412 * @param {Node} node
413 * @return {void}
414 * @private
415 */
416 function handleCdata(node) {
417 output('<![CDATA[' + escapeEntities(node.nodeValue) + ']]>');
418 };
419
420 /**
421 * Handles comment nodes
422 * @param {Node} node
423 * @return {void}
424 * @private
425 */
426 function handleComment(node) {
427 output('<!-- ' + escapeEntities(node.nodeValue) + ' -->');
428 };
429
430 /**
431 * Handles text nodes
432 * @param {Node} node
433 * @return {void}
434 * @private
435 */
436 function handleText(node, parentIsPre) {
437 var text = node.nodeValue;
438
439 if (!parentIsPre) {
440 text = trim(text);
441 }
442
443 if (text) {
444 output(escapeEntities(text), !parentIsPre && canIndent(node));
445 }
446 };
447
448 /**
449 * Adds a string to the outputStringBuilder.
450 *
451 * The string will be indented unless indent is set to boolean false.
452 * @param {string} str
453 * @param {boolean} indent
454 * @return {void}
455 * @private
456 */
457 function output(str, indent) {
458 var i = currentIndent;
459
460 if (indent !== false) {
461 // Don't add a new line if it's the first element
462 if (outputStringBuilder.length) {
463 outputStringBuilder.push('\n');
464 }
465
466 while (i--) {
467 outputStringBuilder.push(opts.indentStr);
468 }
469 }
470
471 outputStringBuilder.push(str);
472 };
473
474 /**
475 * Checks if should indent the node or not
476 * @param {Node} node
477 * @return {boolean}
478 * @private
479 */
480 function canIndent(node) {
481 var prev = node.previousSibling;
482
483 if (node.nodeType !== 1 && prev) {
484 return !dom.isInline(prev);
485 }
486
487 // first child of a block element
488 if (!prev && !dom.isInline(node.parentNode)) {
489 return true;
490 }
491
492 return !dom.isInline(node);
493 };
494 };
495
496 /**
497 * SCEditor XHTML plugin
498 * @class xhtml
499 * @name jQuery.sceditor.plugins.xhtml
500 * @since v1.4.1
501 */
502 function xhtmlFormat() {
503 var base = this;
504
505 /**
506 * Tag converters cache
507 * @type {Object}
508 * @private
509 */
510 var tagConvertersCache = {};
511
512 /**
513 * Attributes filter cache
514 * @type {Object}
515 * @private
516 */
517 var attrsCache = {};
518
519 /**
520 * Init
521 * @return {void}
522 */
523 base.init = function () {
524 if (!isEmptyObject(xhtmlFormat.converters || {})) {
525 each(
526 xhtmlFormat.converters,
527 function (idx, converter) {
528 each(converter.tags, function (tagname) {
529 if (!tagConvertersCache[tagname]) {
530 tagConvertersCache[tagname] = [];
531 }
532
533 tagConvertersCache[tagname].push(converter);
534 });
535 }
536 );
537 }
538
539 this.commands = extend(true,
540 {}, defaultCommandsOverrides, this.commands);
541 };
542
543 /**
544 * Converts the WYSIWYG content to XHTML
545 *
546 * @param {boolean} isFragment
547 * @param {string} html
548 * @param {Document} context
549 * @param {HTMLElement} [parent]
550 * @return {string}
551 * @memberOf jQuery.sceditor.plugins.xhtml.prototype
552 */
553 function toSource(isFragment, html, context) {
554 var xhtml,
555 container = context.createElement('div');
556 container.innerHTML = html;
557
558 css(container, 'visibility', 'hidden');
559 context.body.appendChild(container);
560
561 convertTags(container);
562 removeTags(container);
563 removeAttribs(container);
564
565 if (!isFragment) {
566 wrapInlines(container);
567 }
568
569 xhtml = (new sceditor.XHTMLSerializer()).serialize(container, true);
570
571 context.body.removeChild(container);
572
573 return xhtml;
574 };
575
576 base.toSource = toSource.bind(null, false);
577
578 base.fragmentToSource = toSource.bind(null, true);;
579
580 /**
581 * Runs all converters for the specified tagName
582 * against the DOM node.
583 * @param {string} tagName
584 * @return {Node} node
585 * @private
586 */
587 function convertNode(tagName, node) {
588 if (!tagConvertersCache[tagName]) {
589 return;
590 }
591
592 tagConvertersCache[tagName].forEach(function (converter) {
593 if (converter.tags[tagName]) {
594 each(converter.tags[tagName], function (attr, values) {
595 if (!node.getAttributeNode) {
596 return;
597 }
598
599 attr = node.getAttributeNode(attr);
600
601 if (!attr || values && values.indexOf(attr.value) < 0) {
602 return;
603 }
604
605 converter.conv.call(base, node);
606 });
607 } else if (converter.conv) {
608 converter.conv.call(base, node);
609 }
610 });
611 };
612
613 /**
614 * Converts any tags/attributes to their XHTML equivalents
615 * @param {Node} node
616 * @return {void}
617 * @private
618 */
619 function convertTags(node) {
620 dom.traverse(node, function (node) {
621 var tagName = node.nodeName.toLowerCase();
622
623 convertNode('*', node);
624 convertNode(tagName, node);
625 }, true);
626 };
627
628 /**
629 * Tests if a node is empty and can be removed.
630 *
631 * @param {Node} node
632 * @return {boolean}
633 * @private
634 */
635 function isEmpty(node, excludeBr) {
636 var rect,
637 childNodes = node.childNodes,
638 tagName = node.nodeName.toLowerCase(),
639 nodeValue = node.nodeValue,
640 childrenLength = childNodes.length,
641 allowedEmpty = xhtmlFormat.allowedEmptyTags || [];
642
643 if (excludeBr && tagName === 'br') {
644 return true;
645 }
646
647 if (is(node, '.sceditor-ignore')) {
648 return true;
649 }
650
651 if (allowedEmpty.indexOf(tagName) > -1 || tagName === 'td' ||
652 !dom.canHaveChildren(node)) {
653
654 return false;
655 }
656
657 // \S|\u00A0 = any non space char
658 if (nodeValue && /\S|\u00A0/.test(nodeValue)) {
659 return false;
660 }
661
662 while (childrenLength--) {
663 if (!isEmpty(childNodes[childrenLength],
664 excludeBr && !node.previousSibling && !node.nextSibling)) {
665 return false;
666 }
667 }
668
669 // Treat tags with a width and height from CSS as not empty
670 if (node.getBoundingClientRect &&
671 (node.className || node.hasAttributes('style'))) {
672 rect = node.getBoundingClientRect();
673 return !rect.width || !rect.height;
674 }
675
676 return true;
677 };
678
679 /**
680 * Removes any tags that are not white listed or if no
681 * tags are white listed it will remove any tags that
682 * are black listed.
683 *
684 * @param {Node} rootNode
685 * @return {void}
686 * @private
687 */
688 function removeTags(rootNode) {
689 dom.traverse(rootNode, function (node) {
690 var remove,
691 tagName = node.nodeName.toLowerCase(),
692 parentNode = node.parentNode,
693 nodeType = node.nodeType,
694 isBlock = !dom.isInline(node),
695 previousSibling = node.previousSibling,
696 nextSibling = node.nextSibling,
697 isTopLevel = parentNode === rootNode,
698 noSiblings = !previousSibling && !nextSibling,
699 empty = tagName !== 'iframe' && isEmpty(node,
700 isTopLevel && noSiblings && tagName !== 'br'),
701 document = node.ownerDocument,
702 allowedTags = xhtmlFormat.allowedTags,
703 firstChild = node.firstChild,
704 disallowedTags = xhtmlFormat.disallowedTags;
705
706 // 3 = text node
707 if (nodeType === 3) {
708 return;
709 }
710
711 if (nodeType === 4) {
712 tagName = '!cdata';
713 } else if (tagName === '!' || nodeType === 8) {
714 tagName = '!comment';
715 }
716
717 if (nodeType === 1) {
718 // skip empty nlf elements (new lines automatically
719 // added after block level elements like quotes)
720 if (is(node, '.sceditor-nlf')) {
721 if (!firstChild || (node.childNodes.length === 1 &&
722 /br/i.test(firstChild.nodeName))) {
723 // Mark as empty,it will be removed by the next code
724 empty = true;
725 } else {
726 node.classList.remove('sceditor-nlf');
727
728 if (!node.className) {
729 removeAttr(node, 'class');
730 }
731 }
732 }
733 }
734
735 if (empty) {
736 remove = true;
737 // 3 is text node which do not get filtered
738 } else if (allowedTags && allowedTags.length) {
739 remove = (allowedTags.indexOf(tagName) < 0);
740 } else if (disallowedTags && disallowedTags.length) {
741 remove = (disallowedTags.indexOf(tagName) > -1);
742 }
743
744 if (remove) {
745 if (!empty) {
746 if (isBlock && previousSibling &&
747 dom.isInline(previousSibling)) {
748 parentNode.insertBefore(
749 document.createTextNode(' '), node);
750 }
751
752 // Insert all the childen after node
753 while (node.firstChild) {
754 parentNode.insertBefore(node.firstChild,
755 nextSibling);
756 }
757
758 if (isBlock && nextSibling &&
759 dom.isInline(nextSibling)) {
760 parentNode.insertBefore(
761 document.createTextNode(' '), nextSibling);
762 }
763 }
764
765 parentNode.removeChild(node);
766 }
767 }, true);
768 };
769
770 /**
771 * Merges two sets of attribute filters into one
772 *
773 * @param {Object} filtersA
774 * @param {Object} filtersB
775 * @return {Object}
776 * @private
777 */
778 function mergeAttribsFilters(filtersA, filtersB) {
779 var ret = {};
780
781 if (filtersA) {
782 ret = extend({}, ret, filtersA);
783 }
784
785 if (!filtersB) {
786 return ret;
787 }
788
789 each(filtersB, function (attrName, values) {
790 if (Array.isArray(values)) {
791 ret[attrName] = (ret[attrName] || []).concat(values);
792 } else if (!ret[attrName]) {
793 ret[attrName] = null;
794 }
795 });
796
797 return ret;
798 };
799
800 /**
801 * Wraps adjacent inline child nodes of root
802 * in paragraphs.
803 *
804 * @param {Node} root
805 * @private
806 */
807 function wrapInlines(root) {
808 // Strip empty text nodes so they don't get wrapped.
809 dom.removeWhiteSpace(root);
810
811 var wrapper;
812 var node = root.firstChild;
813 var next;
814 while (node) {
815 next = node.nextSibling;
816
817 if (dom.isInline(node) && !is(node, '.sceditor-ignore')) {
818 if (!wrapper) {
819 wrapper = root.ownerDocument.createElement('p');
820 node.parentNode.insertBefore(wrapper, node);
821 }
822
823 wrapper.appendChild(node);
824 } else {
825 wrapper = null;
826 }
827
828 node = next;
829 }
830 };
831
832 /**
833 * Removes any attributes that are not white listed or
834 * if no attributes are white listed it will remove
835 * any attributes that are black listed.
836 * @param {Node} node
837 * @return {void}
838 * @private
839 */
840 function removeAttribs(node) {
841 var tagName, attr, attrName, attrsLength, validValues, remove,
842 allowedAttribs = xhtmlFormat.allowedAttribs,
843 isAllowed = allowedAttribs &&
844 !isEmptyObject(allowedAttribs),
845 disallowedAttribs = xhtmlFormat.disallowedAttribs,
846 isDisallowed = disallowedAttribs &&
847 !isEmptyObject(disallowedAttribs);
848
849 attrsCache = {};
850
851 dom.traverse(node, function (node) {
852 if (!node.attributes) {
853 return;
854 }
855
856 tagName = node.nodeName.toLowerCase();
857 attrsLength = node.attributes.length;
858
859 if (attrsLength) {
860 if (!attrsCache[tagName]) {
861 if (isAllowed) {
862 attrsCache[tagName] = mergeAttribsFilters(
863 allowedAttribs['*'],
864 allowedAttribs[tagName]
865 );
866 } else {
867 attrsCache[tagName] = mergeAttribsFilters(
868 disallowedAttribs['*'],
869 disallowedAttribs[tagName]
870 );
871 }
872 }
873
874 while (attrsLength--) {
875 attr = node.attributes[attrsLength];
876 attrName = attr.name;
877 validValues = attrsCache[tagName][attrName];
878 remove = false;
879
880 if (isAllowed) {
881 remove = validValues !== null &&
882 (!Array.isArray(validValues) ||
883 validValues.indexOf(attr.value) < 0);
884 } else if (isDisallowed) {
885 remove = validValues === null ||
886 (Array.isArray(validValues) &&
887 validValues.indexOf(attr.value) > -1);
888 }
889
890 if (remove) {
891 node.removeAttribute(attrName);
892 }
893 }
894 }
895 });
896 };
897 };
898
899 /**
900 * Tag conveters, a converter is applied to all
901 * tags that match the criteria.
902 * @type {Array}
903 * @name jQuery.sceditor.plugins.xhtml.converters
904 * @since v1.4.1
905 */
906 xhtmlFormat.converters = [
907 {
908 tags: {
909 '*': {
910 width: null
911 }
912 },
913 conv: function (node) {
914 css(node, 'width', attr(node, 'width'));
915 removeAttr(node, 'width');
916 }
917 },
918 {
919 tags: {
920 '*': {
921 height: null
922 }
923 },
924 conv: function (node) {
925 css(node, 'height', attr(node, 'height'));
926 removeAttr(node, 'height');
927 }
928 },
929 {
930 tags: {
931 'li': {
932 value: null
933 }
934 },
935 conv: function (node) {
936 removeAttr(node, 'value');
937 }
938 },
939 {
940 tags: {
941 '*': {
942 text: null
943 }
944 },
945 conv: function (node) {
946 css(node, 'color', attr(node, 'text'));
947 removeAttr(node, 'text');
948 }
949 },
950 {
951 tags: {
952 '*': {
953 color: null
954 }
955 },
956 conv: function (node) {
957 css(node, 'color', attr(node, 'color'));
958 removeAttr(node, 'color');
959 }
960 },
961 {
962 tags: {
963 '*': {
964 face: null
965 }
966 },
967 conv: function (node) {
968 css(node, 'fontFamily', attr(node, 'face'));
969 removeAttr(node, 'face');
970 }
971 },
972 {
973 tags: {
974 '*': {
975 align: null
976 }
977 },
978 conv: function (node) {
979 css(node, 'textAlign', attr(node, 'align'));
980 removeAttr(node, 'align');
981 }
982 },
983 {
984 tags: {
985 '*': {
986 border: null
987 }
988 },
989 conv: function (node) {
990 css(node, 'borderWidth', attr(node, 'border'));
991 removeAttr(node, 'border');
992 }
993 },
994 {
995 tags: {
996 applet: {
997 name: null
998 },
999 img: {
1000 name: null
1001 },
1002 layer: {
1003 name: null
1004 },
1005 map: {
1006 name: null
1007 },
1008 object: {
1009 name: null
1010 },
1011 param: {
1012 name: null
1013 }
1014 },
1015 conv: function (node) {
1016 if (!attr(node, 'id')) {
1017 attr(node, 'id', attr(node, 'name'));
1018 }
1019
1020 removeAttr(node, 'name');
1021 }
1022 },
1023 {
1024 tags: {
1025 '*': {
1026 vspace: null
1027 }
1028 },
1029 conv: function (node) {
1030 css(node, 'marginTop', attr(node, 'vspace') - 0);
1031 css(node, 'marginBottom', attr(node, 'vspace') - 0);
1032 removeAttr(node, 'vspace');
1033 }
1034 },
1035 {
1036 tags: {
1037 '*': {
1038 hspace: null
1039 }
1040 },
1041 conv: function (node) {
1042 css(node, 'marginLeft', attr(node, 'hspace') - 0);
1043 css(node, 'marginRight', attr(node, 'hspace') - 0);
1044 removeAttr(node, 'hspace');
1045 }
1046 },
1047 {
1048 tags: {
1049 'hr': {
1050 noshade: null
1051 }
1052 },
1053 conv: function (node) {
1054 css(node, 'borderStyle', 'solid');
1055 removeAttr(node, 'noshade');
1056 }
1057 },
1058 {
1059 tags: {
1060 '*': {
1061 nowrap: null
1062 }
1063 },
1064 conv: function (node) {
1065 css(node, 'whiteSpace', 'nowrap');
1066 removeAttr(node, 'nowrap');
1067 }
1068 },
1069 {
1070 tags: {
1071 big: null
1072 },
1073 conv: function (node) {
1074 css(convertElement(node, 'span'), 'fontSize', 'larger');
1075 }
1076 },
1077 {
1078 tags: {
1079 small: null
1080 },
1081 conv: function (node) {
1082 css(convertElement(node, 'span'), 'fontSize', 'smaller');
1083 }
1084 },
1085 {
1086 tags: {
1087 b: null
1088 },
1089 conv: function (node) {
1090 convertElement(node, 'strong');
1091 }
1092 },
1093 {
1094 tags: {
1095 u: null
1096 },
1097 conv: function (node) {
1098 css(convertElement(node, 'span'), 'textDecoration',
1099 'underline');
1100 }
1101 },
1102 {
1103 tags: {
1104 s: null,
1105 strike: null
1106 },
1107 conv: function (node) {
1108 css(convertElement(node, 'span'), 'textDecoration',
1109 'line-through');
1110 }
1111 },
1112 {
1113 tags: {
1114 dir: null
1115 },
1116 conv: function (node) {
1117 convertElement(node, 'ul');
1118 }
1119 },
1120 {
1121 tags: {
1122 center: null
1123 },
1124 conv: function (node) {
1125 css(convertElement(node, 'div'), 'textAlign', 'center');
1126 }
1127 },
1128 {
1129 tags: {
1130 font: {
1131 size: null
1132 }
1133 },
1134 conv: function (node) {
1135 css(node, 'fontSize', css(node, 'fontSize'));
1136 removeAttr(node, 'size');
1137 }
1138 },
1139 {
1140 tags: {
1141 font: null
1142 },
1143 conv: function (node) {
1144 // All it's attributes will be converted
1145 // by the attribute converters
1146 convertElement(node, 'span');
1147 }
1148 },
1149 {
1150 tags: {
1151 '*': {
1152 type: ['_moz']
1153 }
1154 },
1155 conv: function (node) {
1156 removeAttr(node, 'type');
1157 }
1158 },
1159 {
1160 tags: {
1161 '*': {
1162 '_moz_dirty': null
1163 }
1164 },
1165 conv: function (node) {
1166 removeAttr(node, '_moz_dirty');
1167 }
1168 },
1169 {
1170 tags: {
1171 '*': {
1172 '_moz_editor_bogus_node': null
1173 }
1174 },
1175 conv: function (node) {
1176 node.parentNode.removeChild(node);
1177 }
1178 },
1179 {
1180 tags: {
1181 '*': {
1182 'data-sce-target': null
1183 }
1184 },
1185 conv: function (node) {
1186 var rel = attr(node, 'rel') || '';
1187 var target = attr(node, 'data-sce-target');
1188
1189 // Only allow the value _blank and only on links
1190 if (target === '_blank' && is(node, 'a')) {
1191 if (!/(^|\s)noopener(\s|$)/.test(rel)) {
1192 attr(node, 'rel', 'noopener' + (rel ? ' ' + rel : ''));
1193 }
1194
1195 attr(node, 'target', target);
1196 }
1197
1198
1199 removeAttr(node, 'data-sce-target');
1200 }
1201 },
1202 {
1203 tags: {
1204 code: null
1205 },
1206 conv: function (node) {
1207 var node, nodes = node.getElementsByTagName('div');
1208 while ((node = nodes[0])) {
1209 node.style.display = 'block';
1210 convertElement(node, 'span');
1211 }
1212 }
1213 }
1214 ];
1215
1216 /**
1217 * Allowed attributes map.
1218 *
1219 * To allow an attribute for all tags use * as the tag name.
1220 *
1221 * Leave empty or null to allow all attributes. (the disallow
1222 * list will be used to filter them instead)
1223 * @type {Object}
1224 * @name jQuery.sceditor.plugins.xhtml.allowedAttribs
1225 * @since v1.4.1
1226 */
1227 xhtmlFormat.allowedAttribs = {};
1228
1229 /**
1230 * Attributes that are not allowed.
1231 *
1232 * Only used if allowed attributes is null or empty.
1233 * @type {Object}
1234 * @name jQuery.sceditor.plugins.xhtml.disallowedAttribs
1235 * @since v1.4.1
1236 */
1237 xhtmlFormat.disallowedAttribs = {};
1238
1239 /**
1240 * Array containing all the allowed tags.
1241 *
1242 * If null or empty all tags will be allowed.
1243 * @type {Array}
1244 * @name jQuery.sceditor.plugins.xhtml.allowedTags
1245 * @since v1.4.1
1246 */
1247 xhtmlFormat.allowedTags = [];
1248
1249 /**
1250 * Array containing all the disallowed tags.
1251 *
1252 * Only used if allowed tags is null or empty.
1253 * @type {Array}
1254 * @name jQuery.sceditor.plugins.xhtml.disallowedTags
1255 * @since v1.4.1
1256 */
1257 xhtmlFormat.disallowedTags = [];
1258
1259 /**
1260 * Array containing tags which should not be removed when empty.
1261 *
1262 * @type {Array}
1263 * @name jQuery.sceditor.plugins.xhtml.allowedEmptyTags
1264 * @since v2.0.0
1265 */
1266 xhtmlFormat.allowedEmptyTags = [];
1267
1268 sceditor.formats.xhtml = xhtmlFormat;
1269 }(sceditor));