Mercurial Hosting > sceditor
comparison src/formats/xhtml.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/xhtml.js@4c4fc447baea |
children | 0cb206904499 |
comparison
equal
deleted
inserted
replaced
3:ec68006a495e | 4:b7725dab7482 |
---|---|
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 '&': '&', | |
247 '<': '<', | |
248 '>': '>', | |
249 '"': '"', | |
250 '\xa0': ' ' | |
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)); |