4 * Copyright 2010, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
12 var transitional = {}, boolAttrMap, blockElementsMap, shortEndedElementsMap, nonEmptyElementsMap, customElementsMap = {},
13 whiteSpaceElementsMap, selfClosingElementsMap, makeMap = tinymce.makeMap, each = tinymce.each;
15 function split(str, delim) {
16 return str.split(delim || ',');
20 * Unpacks the specified lookup and string data it will also parse it into an object
21 * map with sub object for it's children. This will later also include the attributes.
23 function unpack(lookup, data) {
24 var key, elements = {};
26 function replace(value) {
27 return value.replace(/[A-Z]+/g, function(key) {
28 return replace(lookup[key]);
34 if (lookup.hasOwnProperty(key))
35 lookup[key] = replace(lookup[key]);
38 // Unpack and parse data into object map
39 replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) {
40 attributes = split(attributes, '|');
43 attributes : makeMap(attributes),
44 attributesOrder : attributes,
45 children : makeMap(children, '|', {'#comment' : {}})
52 // Build a lookup table for block elements both lowercase and uppercase
53 blockElementsMap = 'h1,h2,h3,h4,h5,h6,hr,p,div,address,pre,form,table,tbody,thead,tfoot,' +
54 'th,tr,td,li,ol,ul,caption,blockquote,center,dl,dt,dd,dir,fieldset,' +
55 'noscript,menu,isindex,samp,header,footer,article,section,hgroup';
56 blockElementsMap = makeMap(blockElementsMap, ',', makeMap(blockElementsMap.toUpperCase()));
58 // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size
59 transitional = unpack({
62 ZG : 'E|span|width|align|char|charoff|valign',
63 X : 'p|T|div|U|W|isindex|fieldset|table',
64 ZF : 'E|align|char|charoff|valign',
65 W : 'pre|hr|blockquote|address|center|noframes',
66 ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height',
68 U : 'ul|ol|dl|menu|dir',
69 ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q',
70 T : 'h1|h2|h3|h4|h5|h6',
77 O : 'input|select|textarea|label|button',
79 M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym',
82 J : 'tt|i|b|u|s|strike',
83 I : 'big|small|font|basefont',
86 F : 'object|applet|img|map|iframe',
88 D : 'accesskey|tabindex|onfocus|onblur',
89 C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup',
90 B : 'lang|xml:lang|dir',
91 A : 'id|class|style|title'
92 }, 'script[id|charset|type|language|src|defer|xml:space][]' +
93 'style[B|id|type|media|title|xml:space][]' +
94 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' +
95 'param[id|name|value|valuetype|type][]' +
97 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' +
101 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' +
103 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' +
104 'map[B|C|A|name][X|form|Q|area]' +
106 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' +
116 'font[A|B|size|color|face][#|S]' +
117 'basefont[id|size|color|face][]' +
131 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' +
132 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' +
133 'optgroup[E|disabled|label][option]' +
134 'option[E|selected|disabled|label|value][]' +
135 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' +
136 'label[E|for|accesskey|onfocus|onblur][#|S]' +
137 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' +
139 'ins[E|cite|datetime][#|Y]' +
141 'del[E|cite|datetime][#|Y]' +
143 'div[E|align][#|Y]' +
144 'ul[E|type|compact][li]' +
145 'li[E|type|value][#|Y]' +
146 'ol[E|type|compact|start][li]' +
147 'dl[E|compact][dt|dd]' +
150 'menu[E|compact][li]' +
151 'dir[E|compact][li]' +
152 'pre[E|width|xml:space][#|ZA]' +
153 'hr[E|align|noshade|size|width][]' +
154 'blockquote[E|cite][#|Y]' +
155 'address[E][#|S|p]' +
158 'isindex[A|B|prompt][]' +
159 'fieldset[E][#|legend|Y]' +
160 'legend[E|accesskey|align][#|S]' +
161 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' +
162 'caption[E|align][#|S]' +
164 'colgroup[ZG][col]' +
166 'tr[ZF|bgcolor][th|td]' +
168 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' +
173 'area[E|D|shape|coords|href|nohref|alt|target][]' +
174 'base[id|href|target][]' +
175 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]'
178 boolAttrMap = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected,autoplay,loop,controls');
179 shortEndedElementsMap = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,source');
180 nonEmptyElementsMap = tinymce.extend(makeMap('td,th,iframe,video,audio,object'), shortEndedElementsMap);
181 whiteSpaceElementsMap = makeMap('pre,script,style,textarea');
182 selfClosingElementsMap = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
185 * Schema validator class.
187 * @class tinymce.html.Schema
189 * if (tinymce.activeEditor.schema.isValidChild('p', 'span'))
190 * alert('span is valid child of p.');
192 * if (tinymce.activeEditor.schema.getElementRule('p'))
193 * alert('P is a valid element.');
195 * @class tinymce.html.Schema
200 * Constructs a new Schema instance.
204 * @param {Object} settings Name/value settings object.
206 tinymce.html.Schema = function(settings) {
207 var self = this, elements = {}, children = {}, patternElements = [], validStyles;
209 settings = settings || {};
211 // Allow all elements and attributes if verify_html is set to false
212 if (settings.verify_html === false)
213 settings.valid_elements = '*[*]';
216 if (settings.valid_styles) {
219 // Convert styles into a rule list
220 each(settings.valid_styles, function(value, key) {
221 validStyles[key] = tinymce.explode(value);
225 // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
226 function patternToRegExp(str) {
227 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
230 // Parses the specified valid_elements string and adds to the current rules
231 // This function is a bit hard to read since it's heavily optimized for speed
232 function addValidElements(valid_elements) {
233 var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
234 prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value,
235 elementRuleRegExp = /^([#+-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/,
236 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
237 hasPatternsRegExp = /[*?+]/;
239 if (valid_elements) {
240 // Split valid elements into an array with rules
241 valid_elements = split(valid_elements);
244 globalAttributes = elements['@'].attributes;
245 globalAttributesOrder = elements['@'].attributesOrder;
249 for (ei = 0, el = valid_elements.length; ei < el; ei++) {
250 // Parse element rule
251 matches = elementRuleRegExp.exec(valid_elements[ei]);
253 // Setup local names for matches
255 elementName = matches[2];
256 outputName = matches[3];
257 attrData = matches[4];
259 // Create new attributes and attributesOrder
261 attributesOrder = [];
263 // Create the new element
265 attributes : attributes,
266 attributesOrder : attributesOrder
269 // Padd empty elements prefix
271 element.paddEmpty = true;
273 // Remove empty elements prefix
275 element.removeEmpty = true;
277 // Copy attributes from global rule into current rule
278 if (globalAttributes) {
279 for (key in globalAttributes)
280 attributes[key] = globalAttributes[key];
282 attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
285 // Attributes defined
287 attrData = split(attrData, '|');
288 for (ai = 0, al = attrData.length; ai < al; ai++) {
289 matches = attrRuleRegExp.exec(attrData[ai]);
292 attrType = matches[1];
293 attrName = matches[2].replace(/::/g, ':');
298 if (attrType === '!') {
299 element.attributesRequired = element.attributesRequired || [];
300 element.attributesRequired.push(attrName);
301 attr.required = true;
304 // Denied from global
305 if (attrType === '-') {
306 delete attributes[attrName];
307 attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1);
314 if (prefix === '=') {
315 element.attributesDefault = element.attributesDefault || [];
316 element.attributesDefault.push({name: attrName, value: value});
317 attr.defaultValue = value;
321 if (prefix === ':') {
322 element.attributesForced = element.attributesForced || [];
323 element.attributesForced.push({name: attrName, value: value});
324 attr.forcedValue = value;
329 attr.validValues = makeMap(value, '?');
332 // Check for attribute patterns
333 if (hasPatternsRegExp.test(attrName)) {
334 element.attributePatterns = element.attributePatterns || [];
335 attr.pattern = patternToRegExp(attrName);
336 element.attributePatterns.push(attr);
338 // Add attribute to order list if it doesn't already exist
339 if (!attributes[attrName])
340 attributesOrder.push(attrName);
342 attributes[attrName] = attr;
348 // Global rule, store away these for later usage
349 if (!globalAttributes && elementName == '@') {
350 globalAttributes = attributes;
351 globalAttributesOrder = attributesOrder;
354 // Handle substitute elements such as b/strong
356 element.outputName = elementName;
357 elements[outputName] = element;
360 // Add pattern or exact element
361 if (hasPatternsRegExp.test(elementName)) {
362 element.pattern = patternToRegExp(elementName);
363 patternElements.push(element);
365 elements[elementName] = element;
371 function setValidElements(valid_elements) {
373 patternElements = [];
375 addValidElements(valid_elements);
377 each(transitional, function(element, name) {
378 children[name] = element.children;
382 // Adds custom non HTML elements to the schema
383 function addCustomElements(custom_elements) {
384 var customElementRegExp = /^(~)?(.+)$/;
386 if (custom_elements) {
387 each(split(custom_elements), function(rule) {
388 var matches = customElementRegExp.exec(rule),
389 inline = matches[1] === '~',
390 cloneName = inline ? 'span' : 'div',
393 children[name] = children[cloneName];
394 customElementsMap[name] = cloneName;
396 // If it's not marked as inline then add it to valid block elements
398 blockElementsMap[name] = {};
400 // Add custom elements at span/div positions
401 each(children, function(element, child) {
402 if (element[cloneName])
403 element[name] = element[cloneName];
409 // Adds valid children to the schema object
410 function addValidChildren(valid_children) {
411 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
413 if (valid_children) {
414 each(split(valid_children), function(rule) {
415 var matches = childRuleRegExp.exec(rule), parent, prefix;
420 // Add/remove items from default
422 parent = children[matches[2]];
424 parent = children[matches[2]] = {'#comment' : {}};
426 parent = children[matches[2]];
428 each(split(matches[3], '|'), function(child) {
430 delete parent[child];
439 function getElementRule(name) {
440 var element = elements[name], i;
446 // No exact match then try the patterns
447 i = patternElements.length;
449 element = patternElements[i];
451 if (element.pattern.test(name))
456 if (!settings.valid_elements) {
457 // No valid elements defined then clone the elements from the transitional spec
458 each(transitional, function(element, name) {
460 attributes : element.attributes,
461 attributesOrder : element.attributesOrder
464 children[name] = element.children;
468 each(split('strong/b,em/i'), function(item) {
469 item = split(item, '/');
470 elements[item[1]].outputName = item[0];
473 // Add default alt attribute for images
474 elements.img.attributesDefault = [{name: 'alt', value: ''}];
476 // Remove these if they are empty by default
477 each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr'), function(name) {
478 elements[name].removeEmpty = true;
481 // Padd these by default
482 each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) {
483 elements[name].paddEmpty = true;
486 setValidElements(settings.valid_elements);
488 addCustomElements(settings.custom_elements);
489 addValidChildren(settings.valid_children);
490 addValidElements(settings.extended_valid_elements);
492 // Todo: Remove this when we fix list handling to be valid
493 addValidChildren('+ol[ul|ol],+ul[ul|ol]');
495 // If the user didn't allow span only allow internal spans
496 if (!getElementRule('span'))
497 addValidElements('span[!data-mce-type|*]');
499 // Delete invalid elements
500 if (settings.invalid_elements) {
501 tinymce.each(tinymce.explode(settings.invalid_elements), function(item) {
503 delete elements[item];
508 * Name/value map object with valid parents and children to those parents.
517 self.children = children;
520 * Name/value map object with valid styles for each element.
525 self.styles = validStyles;
528 * Returns a map with boolean attributes.
530 * @method getBoolAttrs
531 * @return {Object} Name/value lookup map for boolean attributes.
533 self.getBoolAttrs = function() {
538 * Returns a map with block elements.
540 * @method getBoolAttrs
541 * @return {Object} Name/value lookup map for block elements.
543 self.getBlockElements = function() {
544 return blockElementsMap;
548 * Returns a map with short ended elements such as BR or IMG.
550 * @method getShortEndedElements
551 * @return {Object} Name/value lookup map for short ended elements.
553 self.getShortEndedElements = function() {
554 return shortEndedElementsMap;
558 * Returns a map with self closing tags such as <li>.
560 * @method getSelfClosingElements
561 * @return {Object} Name/value lookup map for self closing tags elements.
563 self.getSelfClosingElements = function() {
564 return selfClosingElementsMap;
568 * Returns a map with elements that should be treated as contents regardless if it has text
569 * content in them or not such as TD, VIDEO or IMG.
571 * @method getNonEmptyElements
572 * @return {Object} Name/value lookup map for non empty elements.
574 self.getNonEmptyElements = function() {
575 return nonEmptyElementsMap;
579 * Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
581 * @method getWhiteSpaceElements
582 * @return {Object} Name/value lookup map for white space elements.
584 self.getWhiteSpaceElements = function() {
585 return whiteSpaceElementsMap;
589 * Returns true/false if the specified element and it's child is valid or not
590 * according to the schema.
592 * @method isValidChild
593 * @param {String} name Element name to check for.
594 * @param {String} child Element child to verify.
595 * @return {Boolean} True/false if the element is a valid child of the specified parent.
597 self.isValidChild = function(name, child) {
598 var parent = children[name];
600 return !!(parent && parent[child]);
604 * Returns true/false if the specified element is valid or not
605 * according to the schema.
607 * @method getElementRule
608 * @param {String} name Element name to check for.
609 * @return {Object} Element object or undefined if the element isn't valid.
611 self.getElementRule = getElementRule;
614 * Returns an map object of all custom elements.
616 * @method getCustomElements
617 * @return {Object} Name/value map object of all custom elements.
619 self.getCustomElements = function() {
620 return customElementsMap;
624 * Parses a valid elements string and adds it to the schema. The valid elements format is for example "element[attr=default|otherattr]".
625 * Existing rules will be replaced with the ones specified, so this extends the schema.
627 * @method addValidElements
628 * @param {String} valid_elements String in the valid elements format to be parsed.
630 self.addValidElements = addValidElements;
633 * Parses a valid elements string and sets it to the schema. The valid elements format is for example "element[attr=default|otherattr]".
634 * Existing rules will be replaced with the ones specified, so this extends the schema.
636 * @method setValidElements
637 * @param {String} valid_elements String in the valid elements format to be parsed.
639 self.setValidElements = setValidElements;
642 * Adds custom non HTML elements to the schema.
644 * @method addCustomElements
645 * @param {String} custom_elements Comma separated list of custom elements to add.
647 self.addCustomElements = addCustomElements;
650 * Parses a valid children string and adds them to the schema structure. The valid children format is for example: "element[child1|child2]".
652 * @method addValidChildren
653 * @param {String} valid_children Valid children elements string to parse
655 self.addValidChildren = addValidChildren;
658 // Expose boolMap and blockElementMap as static properties for usage in DOMUtils
659 tinymce.html.Schema.boolAttrMap = boolAttrMap;
660 tinymce.html.Schema.blockElementsMap = blockElementsMap;