1 /** |
|
2 * Schema.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /** |
|
12 * Schema validator class. |
|
13 * |
|
14 * @class tinymce.html.Schema |
|
15 * @example |
|
16 * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) |
|
17 * alert('span is valid child of p.'); |
|
18 * |
|
19 * if (tinymce.activeEditor.schema.getElementRule('p')) |
|
20 * alert('P is a valid element.'); |
|
21 * |
|
22 * @class tinymce.html.Schema |
|
23 * @version 3.4 |
|
24 */ |
|
25 define("tinymce/html/Schema", [ |
|
26 "tinymce/util/Tools" |
|
27 ], function(Tools) { |
|
28 var mapCache = {}, dummyObj = {}; |
|
29 var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray; |
|
30 |
|
31 function split(items, delim) { |
|
32 return items ? items.split(delim || ' ') : []; |
|
33 } |
|
34 |
|
35 /** |
|
36 * Builds a schema lookup table |
|
37 * |
|
38 * @private |
|
39 * @param {String} type html4, html5 or html5-strict schema type. |
|
40 * @return {Object} Schema lookup table. |
|
41 */ |
|
42 function compileSchema(type) { |
|
43 var schema = {}, globalAttributes, blockContent; |
|
44 var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent; |
|
45 |
|
46 function add(name, attributes, children) { |
|
47 var ni, i, attributesOrder, args = arguments; |
|
48 |
|
49 function arrayToMap(array, obj) { |
|
50 var map = {}, i, l; |
|
51 |
|
52 for (i = 0, l = array.length; i < l; i++) { |
|
53 map[array[i]] = obj || {}; |
|
54 } |
|
55 |
|
56 return map; |
|
57 } |
|
58 |
|
59 children = children || []; |
|
60 attributes = attributes || ""; |
|
61 |
|
62 if (typeof children === "string") { |
|
63 children = split(children); |
|
64 } |
|
65 |
|
66 // Split string children |
|
67 for (i = 3; i < args.length; i++) { |
|
68 if (typeof args[i] === "string") { |
|
69 args[i] = split(args[i]); |
|
70 } |
|
71 |
|
72 children.push.apply(children, args[i]); |
|
73 } |
|
74 |
|
75 name = split(name); |
|
76 ni = name.length; |
|
77 while (ni--) { |
|
78 attributesOrder = [].concat(globalAttributes, split(attributes)); |
|
79 schema[name[ni]] = { |
|
80 attributes: arrayToMap(attributesOrder), |
|
81 attributesOrder: attributesOrder, |
|
82 children: arrayToMap(children, dummyObj) |
|
83 }; |
|
84 } |
|
85 } |
|
86 |
|
87 function addAttrs(name, attributes) { |
|
88 var ni, schemaItem, i, l; |
|
89 |
|
90 name = split(name); |
|
91 ni = name.length; |
|
92 attributes = split(attributes); |
|
93 while (ni--) { |
|
94 schemaItem = schema[name[ni]]; |
|
95 for (i = 0, l = attributes.length; i < l; i++) { |
|
96 schemaItem.attributes[attributes[i]] = {}; |
|
97 schemaItem.attributesOrder.push(attributes[i]); |
|
98 } |
|
99 } |
|
100 } |
|
101 |
|
102 // Use cached schema |
|
103 if (mapCache[type]) { |
|
104 return mapCache[type]; |
|
105 } |
|
106 |
|
107 // Attributes present on all elements |
|
108 globalAttributes = split("id accesskey class dir lang style tabindex title"); |
|
109 |
|
110 // Event attributes can be opt-in/opt-out |
|
111 /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " + |
|
112 "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " + |
|
113 "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " + |
|
114 "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " + |
|
115 "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " + |
|
116 "onwaiting" |
|
117 );*/ |
|
118 |
|
119 // Block content elements |
|
120 blockContent = split( |
|
121 "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul" |
|
122 ); |
|
123 |
|
124 // Phrasing content elements from the HTML5 spec (inline) |
|
125 phrasingContent = split( |
|
126 "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " + |
|
127 "label map noscript object q s samp script select small span strong sub sup " + |
|
128 "textarea u var #text #comment" |
|
129 ); |
|
130 |
|
131 // Add HTML5 items to globalAttributes, blockContent, phrasingContent |
|
132 if (type != "html4") { |
|
133 globalAttributes.push.apply(globalAttributes, split("contenteditable contextmenu draggable dropzone " + |
|
134 "hidden spellcheck translate")); |
|
135 blockContent.push.apply(blockContent, split("article aside details dialog figure header footer hgroup section nav")); |
|
136 phrasingContent.push.apply(phrasingContent, split("audio canvas command datalist mark meter output progress time wbr " + |
|
137 "video ruby bdi keygen")); |
|
138 } |
|
139 |
|
140 // Add HTML4 elements unless it's html5-strict |
|
141 if (type != "html5-strict") { |
|
142 globalAttributes.push("xml:lang"); |
|
143 |
|
144 html4PhrasingContent = split("acronym applet basefont big font strike tt"); |
|
145 phrasingContent.push.apply(phrasingContent, html4PhrasingContent); |
|
146 |
|
147 each(html4PhrasingContent, function(name) { |
|
148 add(name, "", phrasingContent); |
|
149 }); |
|
150 |
|
151 html4BlockContent = split("center dir isindex noframes"); |
|
152 blockContent.push.apply(blockContent, html4BlockContent); |
|
153 |
|
154 // Flow content elements from the HTML5 spec (block+inline) |
|
155 flowContent = [].concat(blockContent, phrasingContent); |
|
156 |
|
157 each(html4BlockContent, function(name) { |
|
158 add(name, "", flowContent); |
|
159 }); |
|
160 } |
|
161 |
|
162 // Flow content elements from the HTML5 spec (block+inline) |
|
163 flowContent = flowContent || [].concat(blockContent, phrasingContent); |
|
164 |
|
165 // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement |
|
166 // Schema items <element name>, <specific attributes>, <children ..> |
|
167 add("html", "manifest", "head body"); |
|
168 add("head", "", "base command link meta noscript script style title"); |
|
169 add("title hr noscript br"); |
|
170 add("base", "href target"); |
|
171 add("link", "href rel media hreflang type sizes hreflang"); |
|
172 add("meta", "name http-equiv content charset"); |
|
173 add("style", "media type scoped"); |
|
174 add("script", "src async defer type charset"); |
|
175 add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " + |
|
176 "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " + |
|
177 "onpopstate onresize onscroll onstorage onunload", flowContent); |
|
178 add("address dt dd div caption", "", flowContent); |
|
179 add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent); |
|
180 add("blockquote", "cite", flowContent); |
|
181 add("ol", "reversed start type", "li"); |
|
182 add("ul", "", "li"); |
|
183 add("li", "value", flowContent); |
|
184 add("dl", "", "dt dd"); |
|
185 add("a", "href target rel media hreflang type", phrasingContent); |
|
186 add("q", "cite", phrasingContent); |
|
187 add("ins del", "cite datetime", flowContent); |
|
188 add("img", "src sizes srcset alt usemap ismap width height"); |
|
189 add("iframe", "src name width height", flowContent); |
|
190 add("embed", "src type width height"); |
|
191 add("object", "data type typemustmatch name usemap form width height", flowContent, "param"); |
|
192 add("param", "name value"); |
|
193 add("map", "name", flowContent, "area"); |
|
194 add("area", "alt coords shape href target rel media hreflang type"); |
|
195 add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : "")); |
|
196 add("colgroup", "span", "col"); |
|
197 add("col", "span"); |
|
198 add("tbody thead tfoot", "", "tr"); |
|
199 add("tr", "", "td th"); |
|
200 add("td", "colspan rowspan headers", flowContent); |
|
201 add("th", "colspan rowspan headers scope abbr", flowContent); |
|
202 add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent); |
|
203 add("fieldset", "disabled form name", flowContent, "legend"); |
|
204 add("label", "form for", phrasingContent); |
|
205 add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " + |
|
206 "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width" |
|
207 ); |
|
208 add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value", |
|
209 type == "html4" ? flowContent : phrasingContent); |
|
210 add("select", "disabled form multiple name required size", "option optgroup"); |
|
211 add("optgroup", "disabled label", "option"); |
|
212 add("option", "disabled label selected value"); |
|
213 add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap"); |
|
214 add("menu", "type label", flowContent, "li"); |
|
215 add("noscript", "", flowContent); |
|
216 |
|
217 // Extend with HTML5 elements |
|
218 if (type != "html4") { |
|
219 add("wbr"); |
|
220 add("ruby", "", phrasingContent, "rt rp"); |
|
221 add("figcaption", "", flowContent); |
|
222 add("mark rt rp summary bdi", "", phrasingContent); |
|
223 add("canvas", "width height", flowContent); |
|
224 add("video", "src crossorigin poster preload autoplay mediagroup loop " + |
|
225 "muted controls width height buffered", flowContent, "track source"); |
|
226 add("audio", "src crossorigin preload autoplay mediagroup loop muted controls buffered volume", flowContent, "track source"); |
|
227 add("picture", "", "img source"); |
|
228 add("source", "src srcset type media sizes"); |
|
229 add("track", "kind src srclang label default"); |
|
230 add("datalist", "", phrasingContent, "option"); |
|
231 add("article section nav aside header footer", "", flowContent); |
|
232 add("hgroup", "", "h1 h2 h3 h4 h5 h6"); |
|
233 add("figure", "", flowContent, "figcaption"); |
|
234 add("time", "datetime", phrasingContent); |
|
235 add("dialog", "open", flowContent); |
|
236 add("command", "type label icon disabled checked radiogroup command"); |
|
237 add("output", "for form name", phrasingContent); |
|
238 add("progress", "value max", phrasingContent); |
|
239 add("meter", "value min max low high optimum", phrasingContent); |
|
240 add("details", "open", flowContent, "summary"); |
|
241 add("keygen", "autofocus challenge disabled form keytype name"); |
|
242 } |
|
243 |
|
244 // Extend with HTML4 attributes unless it's html5-strict |
|
245 if (type != "html5-strict") { |
|
246 addAttrs("script", "language xml:space"); |
|
247 addAttrs("style", "xml:space"); |
|
248 addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace"); |
|
249 addAttrs("embed", "align name hspace vspace"); |
|
250 addAttrs("param", "valuetype type"); |
|
251 addAttrs("a", "charset name rev shape coords"); |
|
252 addAttrs("br", "clear"); |
|
253 addAttrs("applet", "codebase archive code object alt name width height align hspace vspace"); |
|
254 addAttrs("img", "name longdesc align border hspace vspace"); |
|
255 addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align"); |
|
256 addAttrs("font basefont", "size color face"); |
|
257 addAttrs("input", "usemap align"); |
|
258 addAttrs("select", "onchange"); |
|
259 addAttrs("textarea"); |
|
260 addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align"); |
|
261 addAttrs("ul", "type compact"); |
|
262 addAttrs("li", "type"); |
|
263 addAttrs("ol dl menu dir", "compact"); |
|
264 addAttrs("pre", "width xml:space"); |
|
265 addAttrs("hr", "align noshade size width"); |
|
266 addAttrs("isindex", "prompt"); |
|
267 addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor"); |
|
268 addAttrs("col", "width align char charoff valign"); |
|
269 addAttrs("colgroup", "width align char charoff valign"); |
|
270 addAttrs("thead", "align char charoff valign"); |
|
271 addAttrs("tr", "align char charoff valign bgcolor"); |
|
272 addAttrs("th", "axis align char charoff valign nowrap bgcolor width height"); |
|
273 addAttrs("form", "accept"); |
|
274 addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height"); |
|
275 addAttrs("tfoot", "align char charoff valign"); |
|
276 addAttrs("tbody", "align char charoff valign"); |
|
277 addAttrs("area", "nohref"); |
|
278 addAttrs("body", "background bgcolor text link vlink alink"); |
|
279 } |
|
280 |
|
281 // Extend with HTML5 attributes unless it's html4 |
|
282 if (type != "html4") { |
|
283 addAttrs("input button select textarea", "autofocus"); |
|
284 addAttrs("input textarea", "placeholder"); |
|
285 addAttrs("a", "download"); |
|
286 addAttrs("link script img", "crossorigin"); |
|
287 addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc |
|
288 } |
|
289 |
|
290 // Special: iframe, ruby, video, audio, label |
|
291 |
|
292 // Delete children of the same name from it's parent |
|
293 // For example: form can't have a child of the name form |
|
294 each(split('a form meter progress dfn'), function(name) { |
|
295 if (schema[name]) { |
|
296 delete schema[name].children[name]; |
|
297 } |
|
298 }); |
|
299 |
|
300 // Delete header, footer, sectioning and heading content descendants |
|
301 /*each('dt th address', function(name) { |
|
302 delete schema[name].children[name]; |
|
303 });*/ |
|
304 |
|
305 // Caption can't have tables |
|
306 delete schema.caption.children.table; |
|
307 |
|
308 // TODO: LI:s can only have value if parent is OL |
|
309 |
|
310 // TODO: Handle transparent elements |
|
311 // a ins del canvas map |
|
312 |
|
313 mapCache[type] = schema; |
|
314 |
|
315 return schema; |
|
316 } |
|
317 |
|
318 function compileElementMap(value, mode) { |
|
319 var styles; |
|
320 |
|
321 if (value) { |
|
322 styles = {}; |
|
323 |
|
324 if (typeof value == 'string') { |
|
325 value = { |
|
326 '*': value |
|
327 }; |
|
328 } |
|
329 |
|
330 // Convert styles into a rule list |
|
331 each(value, function(value, key) { |
|
332 styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/); |
|
333 }); |
|
334 } |
|
335 |
|
336 return styles; |
|
337 } |
|
338 |
|
339 /** |
|
340 * Constructs a new Schema instance. |
|
341 * |
|
342 * @constructor |
|
343 * @method Schema |
|
344 * @param {Object} settings Name/value settings object. |
|
345 */ |
|
346 return function(settings) { |
|
347 var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems; |
|
348 var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses; |
|
349 var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap; |
|
350 var customElementsMap = {}, specialElements = {}; |
|
351 |
|
352 // Creates an lookup table map object for the specified option or the default value |
|
353 function createLookupTable(option, default_value, extendWith) { |
|
354 var value = settings[option]; |
|
355 |
|
356 if (!value) { |
|
357 // Get cached default map or make it if needed |
|
358 value = mapCache[option]; |
|
359 |
|
360 if (!value) { |
|
361 value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); |
|
362 value = extend(value, extendWith); |
|
363 |
|
364 mapCache[option] = value; |
|
365 } |
|
366 } else { |
|
367 // Create custom map |
|
368 value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/)); |
|
369 } |
|
370 |
|
371 return value; |
|
372 } |
|
373 |
|
374 settings = settings || {}; |
|
375 schemaItems = compileSchema(settings.schema); |
|
376 |
|
377 // Allow all elements and attributes if verify_html is set to false |
|
378 if (settings.verify_html === false) { |
|
379 settings.valid_elements = '*[*]'; |
|
380 } |
|
381 |
|
382 validStyles = compileElementMap(settings.valid_styles); |
|
383 invalidStyles = compileElementMap(settings.invalid_styles, 'map'); |
|
384 validClasses = compileElementMap(settings.valid_classes, 'map'); |
|
385 |
|
386 // Setup map objects |
|
387 whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object'); |
|
388 selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); |
|
389 shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' + |
|
390 'meta param embed source wbr track'); |
|
391 boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' + |
|
392 'noshade nowrap readonly selected autoplay loop controls'); |
|
393 nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object script', shortEndedElementsMap); |
|
394 moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap); |
|
395 textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + |
|
396 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); |
|
397 blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + |
|
398 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + |
|
399 'datalist select optgroup', textBlockElementsMap); |
|
400 textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' + |
|
401 'dfn code mark q sup sub samp'); |
|
402 |
|
403 each((settings.special || 'script noscript style textarea').split(' '), function(name) { |
|
404 specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi'); |
|
405 }); |
|
406 |
|
407 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. |
|
408 function patternToRegExp(str) { |
|
409 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); |
|
410 } |
|
411 |
|
412 // Parses the specified valid_elements string and adds to the current rules |
|
413 // This function is a bit hard to read since it's heavily optimized for speed |
|
414 function addValidElements(validElements) { |
|
415 var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, |
|
416 prefix, outputName, globalAttributes, globalAttributesOrder, key, value, |
|
417 elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/, |
|
418 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, |
|
419 hasPatternsRegExp = /[*?+]/; |
|
420 |
|
421 if (validElements) { |
|
422 // Split valid elements into an array with rules |
|
423 validElements = split(validElements, ','); |
|
424 |
|
425 if (elements['@']) { |
|
426 globalAttributes = elements['@'].attributes; |
|
427 globalAttributesOrder = elements['@'].attributesOrder; |
|
428 } |
|
429 |
|
430 // Loop all rules |
|
431 for (ei = 0, el = validElements.length; ei < el; ei++) { |
|
432 // Parse element rule |
|
433 matches = elementRuleRegExp.exec(validElements[ei]); |
|
434 if (matches) { |
|
435 // Setup local names for matches |
|
436 prefix = matches[1]; |
|
437 elementName = matches[2]; |
|
438 outputName = matches[3]; |
|
439 attrData = matches[5]; |
|
440 |
|
441 // Create new attributes and attributesOrder |
|
442 attributes = {}; |
|
443 attributesOrder = []; |
|
444 |
|
445 // Create the new element |
|
446 element = { |
|
447 attributes: attributes, |
|
448 attributesOrder: attributesOrder |
|
449 }; |
|
450 |
|
451 // Padd empty elements prefix |
|
452 if (prefix === '#') { |
|
453 element.paddEmpty = true; |
|
454 } |
|
455 |
|
456 // Remove empty elements prefix |
|
457 if (prefix === '-') { |
|
458 element.removeEmpty = true; |
|
459 } |
|
460 |
|
461 if (matches[4] === '!') { |
|
462 element.removeEmptyAttrs = true; |
|
463 } |
|
464 |
|
465 // Copy attributes from global rule into current rule |
|
466 if (globalAttributes) { |
|
467 for (key in globalAttributes) { |
|
468 attributes[key] = globalAttributes[key]; |
|
469 } |
|
470 |
|
471 attributesOrder.push.apply(attributesOrder, globalAttributesOrder); |
|
472 } |
|
473 |
|
474 // Attributes defined |
|
475 if (attrData) { |
|
476 attrData = split(attrData, '|'); |
|
477 for (ai = 0, al = attrData.length; ai < al; ai++) { |
|
478 matches = attrRuleRegExp.exec(attrData[ai]); |
|
479 if (matches) { |
|
480 attr = {}; |
|
481 attrType = matches[1]; |
|
482 attrName = matches[2].replace(/::/g, ':'); |
|
483 prefix = matches[3]; |
|
484 value = matches[4]; |
|
485 |
|
486 // Required |
|
487 if (attrType === '!') { |
|
488 element.attributesRequired = element.attributesRequired || []; |
|
489 element.attributesRequired.push(attrName); |
|
490 attr.required = true; |
|
491 } |
|
492 |
|
493 // Denied from global |
|
494 if (attrType === '-') { |
|
495 delete attributes[attrName]; |
|
496 attributesOrder.splice(inArray(attributesOrder, attrName), 1); |
|
497 continue; |
|
498 } |
|
499 |
|
500 // Default value |
|
501 if (prefix) { |
|
502 // Default value |
|
503 if (prefix === '=') { |
|
504 element.attributesDefault = element.attributesDefault || []; |
|
505 element.attributesDefault.push({name: attrName, value: value}); |
|
506 attr.defaultValue = value; |
|
507 } |
|
508 |
|
509 // Forced value |
|
510 if (prefix === ':') { |
|
511 element.attributesForced = element.attributesForced || []; |
|
512 element.attributesForced.push({name: attrName, value: value}); |
|
513 attr.forcedValue = value; |
|
514 } |
|
515 |
|
516 // Required values |
|
517 if (prefix === '<') { |
|
518 attr.validValues = makeMap(value, '?'); |
|
519 } |
|
520 } |
|
521 |
|
522 // Check for attribute patterns |
|
523 if (hasPatternsRegExp.test(attrName)) { |
|
524 element.attributePatterns = element.attributePatterns || []; |
|
525 attr.pattern = patternToRegExp(attrName); |
|
526 element.attributePatterns.push(attr); |
|
527 } else { |
|
528 // Add attribute to order list if it doesn't already exist |
|
529 if (!attributes[attrName]) { |
|
530 attributesOrder.push(attrName); |
|
531 } |
|
532 |
|
533 attributes[attrName] = attr; |
|
534 } |
|
535 } |
|
536 } |
|
537 } |
|
538 |
|
539 // Global rule, store away these for later usage |
|
540 if (!globalAttributes && elementName == '@') { |
|
541 globalAttributes = attributes; |
|
542 globalAttributesOrder = attributesOrder; |
|
543 } |
|
544 |
|
545 // Handle substitute elements such as b/strong |
|
546 if (outputName) { |
|
547 element.outputName = elementName; |
|
548 elements[outputName] = element; |
|
549 } |
|
550 |
|
551 // Add pattern or exact element |
|
552 if (hasPatternsRegExp.test(elementName)) { |
|
553 element.pattern = patternToRegExp(elementName); |
|
554 patternElements.push(element); |
|
555 } else { |
|
556 elements[elementName] = element; |
|
557 } |
|
558 } |
|
559 } |
|
560 } |
|
561 } |
|
562 |
|
563 function setValidElements(validElements) { |
|
564 elements = {}; |
|
565 patternElements = []; |
|
566 |
|
567 addValidElements(validElements); |
|
568 |
|
569 each(schemaItems, function(element, name) { |
|
570 children[name] = element.children; |
|
571 }); |
|
572 } |
|
573 |
|
574 // Adds custom non HTML elements to the schema |
|
575 function addCustomElements(customElements) { |
|
576 var customElementRegExp = /^(~)?(.+)$/; |
|
577 |
|
578 if (customElements) { |
|
579 // Flush cached items since we are altering the default maps |
|
580 mapCache.text_block_elements = mapCache.block_elements = null; |
|
581 |
|
582 each(split(customElements, ','), function(rule) { |
|
583 var matches = customElementRegExp.exec(rule), |
|
584 inline = matches[1] === '~', |
|
585 cloneName = inline ? 'span' : 'div', |
|
586 name = matches[2]; |
|
587 |
|
588 children[name] = children[cloneName]; |
|
589 customElementsMap[name] = cloneName; |
|
590 |
|
591 // If it's not marked as inline then add it to valid block elements |
|
592 if (!inline) { |
|
593 blockElementsMap[name.toUpperCase()] = {}; |
|
594 blockElementsMap[name] = {}; |
|
595 } |
|
596 |
|
597 // Add elements clone if needed |
|
598 if (!elements[name]) { |
|
599 var customRule = elements[cloneName]; |
|
600 |
|
601 customRule = extend({}, customRule); |
|
602 delete customRule.removeEmptyAttrs; |
|
603 delete customRule.removeEmpty; |
|
604 |
|
605 elements[name] = customRule; |
|
606 } |
|
607 |
|
608 // Add custom elements at span/div positions |
|
609 each(children, function(element, elmName) { |
|
610 if (element[cloneName]) { |
|
611 children[elmName] = element = extend({}, children[elmName]); |
|
612 element[name] = element[cloneName]; |
|
613 } |
|
614 }); |
|
615 }); |
|
616 } |
|
617 } |
|
618 |
|
619 // Adds valid children to the schema object |
|
620 function addValidChildren(validChildren) { |
|
621 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; |
|
622 |
|
623 if (validChildren) { |
|
624 each(split(validChildren, ','), function(rule) { |
|
625 var matches = childRuleRegExp.exec(rule), parent, prefix; |
|
626 |
|
627 if (matches) { |
|
628 prefix = matches[1]; |
|
629 |
|
630 // Add/remove items from default |
|
631 if (prefix) { |
|
632 parent = children[matches[2]]; |
|
633 } else { |
|
634 parent = children[matches[2]] = {'#comment': {}}; |
|
635 } |
|
636 |
|
637 parent = children[matches[2]]; |
|
638 |
|
639 each(split(matches[3], '|'), function(child) { |
|
640 if (prefix === '-') { |
|
641 // Clone the element before we delete |
|
642 // things in it to not mess up default schemas |
|
643 children[matches[2]] = parent = extend({}, children[matches[2]]); |
|
644 |
|
645 delete parent[child]; |
|
646 } else { |
|
647 parent[child] = {}; |
|
648 } |
|
649 }); |
|
650 } |
|
651 }); |
|
652 } |
|
653 } |
|
654 |
|
655 function getElementRule(name) { |
|
656 var element = elements[name], i; |
|
657 |
|
658 // Exact match found |
|
659 if (element) { |
|
660 return element; |
|
661 } |
|
662 |
|
663 // No exact match then try the patterns |
|
664 i = patternElements.length; |
|
665 while (i--) { |
|
666 element = patternElements[i]; |
|
667 |
|
668 if (element.pattern.test(name)) { |
|
669 return element; |
|
670 } |
|
671 } |
|
672 } |
|
673 |
|
674 if (!settings.valid_elements) { |
|
675 // No valid elements defined then clone the elements from the schema spec |
|
676 each(schemaItems, function(element, name) { |
|
677 elements[name] = { |
|
678 attributes: element.attributes, |
|
679 attributesOrder: element.attributesOrder |
|
680 }; |
|
681 |
|
682 children[name] = element.children; |
|
683 }); |
|
684 |
|
685 // Switch these on HTML4 |
|
686 if (settings.schema != "html5") { |
|
687 each(split('strong/b em/i'), function(item) { |
|
688 item = split(item, '/'); |
|
689 elements[item[1]].outputName = item[0]; |
|
690 }); |
|
691 } |
|
692 |
|
693 // Add default alt attribute for images |
|
694 elements.img.attributesDefault = [{name: 'alt', value: ''}]; |
|
695 |
|
696 // Remove these if they are empty by default |
|
697 each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function(name) { |
|
698 if (elements[name]) { |
|
699 elements[name].removeEmpty = true; |
|
700 } |
|
701 }); |
|
702 |
|
703 // Padd these by default |
|
704 each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function(name) { |
|
705 elements[name].paddEmpty = true; |
|
706 }); |
|
707 |
|
708 // Remove these if they have no attributes |
|
709 each(split('span'), function(name) { |
|
710 elements[name].removeEmptyAttrs = true; |
|
711 }); |
|
712 |
|
713 // Remove these by default |
|
714 // TODO: Reenable in 4.1 |
|
715 /*each(split('script style'), function(name) { |
|
716 delete elements[name]; |
|
717 });*/ |
|
718 } else { |
|
719 setValidElements(settings.valid_elements); |
|
720 } |
|
721 |
|
722 addCustomElements(settings.custom_elements); |
|
723 addValidChildren(settings.valid_children); |
|
724 addValidElements(settings.extended_valid_elements); |
|
725 |
|
726 // Todo: Remove this when we fix list handling to be valid |
|
727 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); |
|
728 |
|
729 // Delete invalid elements |
|
730 if (settings.invalid_elements) { |
|
731 each(explode(settings.invalid_elements), function(item) { |
|
732 if (elements[item]) { |
|
733 delete elements[item]; |
|
734 } |
|
735 }); |
|
736 } |
|
737 |
|
738 // If the user didn't allow span only allow internal spans |
|
739 if (!getElementRule('span')) { |
|
740 addValidElements('span[!data-mce-type|*]'); |
|
741 } |
|
742 |
|
743 /** |
|
744 * Name/value map object with valid parents and children to those parents. |
|
745 * |
|
746 * @example |
|
747 * children = { |
|
748 * div:{p:{}, h1:{}} |
|
749 * }; |
|
750 * @field children |
|
751 * @type Object |
|
752 */ |
|
753 self.children = children; |
|
754 |
|
755 /** |
|
756 * Name/value map object with valid styles for each element. |
|
757 * |
|
758 * @method getValidStyles |
|
759 * @type Object |
|
760 */ |
|
761 self.getValidStyles = function() { |
|
762 return validStyles; |
|
763 }; |
|
764 |
|
765 /** |
|
766 * Name/value map object with valid styles for each element. |
|
767 * |
|
768 * @method getInvalidStyles |
|
769 * @type Object |
|
770 */ |
|
771 self.getInvalidStyles = function() { |
|
772 return invalidStyles; |
|
773 }; |
|
774 |
|
775 /** |
|
776 * Name/value map object with valid classes for each element. |
|
777 * |
|
778 * @method getValidClasses |
|
779 * @type Object |
|
780 */ |
|
781 self.getValidClasses = function() { |
|
782 return validClasses; |
|
783 }; |
|
784 |
|
785 /** |
|
786 * Returns a map with boolean attributes. |
|
787 * |
|
788 * @method getBoolAttrs |
|
789 * @return {Object} Name/value lookup map for boolean attributes. |
|
790 */ |
|
791 self.getBoolAttrs = function() { |
|
792 return boolAttrMap; |
|
793 }; |
|
794 |
|
795 /** |
|
796 * Returns a map with block elements. |
|
797 * |
|
798 * @method getBlockElements |
|
799 * @return {Object} Name/value lookup map for block elements. |
|
800 */ |
|
801 self.getBlockElements = function() { |
|
802 return blockElementsMap; |
|
803 }; |
|
804 |
|
805 /** |
|
806 * Returns a map with text block elements. Such as: p,h1-h6,div,address |
|
807 * |
|
808 * @method getTextBlockElements |
|
809 * @return {Object} Name/value lookup map for block elements. |
|
810 */ |
|
811 self.getTextBlockElements = function() { |
|
812 return textBlockElementsMap; |
|
813 }; |
|
814 |
|
815 /** |
|
816 * Returns a map of inline text format nodes for example strong/span or ins. |
|
817 * |
|
818 * @method getTextInlineElements |
|
819 * @return {Object} Name/value lookup map for text format elements. |
|
820 */ |
|
821 self.getTextInlineElements = function() { |
|
822 return textInlineElementsMap; |
|
823 }; |
|
824 |
|
825 /** |
|
826 * Returns a map with short ended elements such as BR or IMG. |
|
827 * |
|
828 * @method getShortEndedElements |
|
829 * @return {Object} Name/value lookup map for short ended elements. |
|
830 */ |
|
831 self.getShortEndedElements = function() { |
|
832 return shortEndedElementsMap; |
|
833 }; |
|
834 |
|
835 /** |
|
836 * Returns a map with self closing tags such as <li>. |
|
837 * |
|
838 * @method getSelfClosingElements |
|
839 * @return {Object} Name/value lookup map for self closing tags elements. |
|
840 */ |
|
841 self.getSelfClosingElements = function() { |
|
842 return selfClosingElementsMap; |
|
843 }; |
|
844 |
|
845 /** |
|
846 * Returns a map with elements that should be treated as contents regardless if it has text |
|
847 * content in them or not such as TD, VIDEO or IMG. |
|
848 * |
|
849 * @method getNonEmptyElements |
|
850 * @return {Object} Name/value lookup map for non empty elements. |
|
851 */ |
|
852 self.getNonEmptyElements = function() { |
|
853 return nonEmptyElementsMap; |
|
854 }; |
|
855 |
|
856 /** |
|
857 * Returns a map with elements that the caret should be moved in front of after enter is |
|
858 * pressed |
|
859 * |
|
860 * @method getMoveCaretBeforeOnEnterElements |
|
861 * @return {Object} Name/value lookup map for elements to place the caret in front of. |
|
862 */ |
|
863 self.getMoveCaretBeforeOnEnterElements = function() { |
|
864 return moveCaretBeforeOnEnterElementsMap; |
|
865 }; |
|
866 |
|
867 /** |
|
868 * Returns a map with elements where white space is to be preserved like PRE or SCRIPT. |
|
869 * |
|
870 * @method getWhiteSpaceElements |
|
871 * @return {Object} Name/value lookup map for white space elements. |
|
872 */ |
|
873 self.getWhiteSpaceElements = function() { |
|
874 return whiteSpaceElementsMap; |
|
875 }; |
|
876 |
|
877 /** |
|
878 * Returns a map with special elements. These are elements that needs to be parsed |
|
879 * in a special way such as script, style, textarea etc. The map object values |
|
880 * are regexps used to find the end of the element. |
|
881 * |
|
882 * @method getSpecialElements |
|
883 * @return {Object} Name/value lookup map for special elements. |
|
884 */ |
|
885 self.getSpecialElements = function() { |
|
886 return specialElements; |
|
887 }; |
|
888 |
|
889 /** |
|
890 * Returns true/false if the specified element and it's child is valid or not |
|
891 * according to the schema. |
|
892 * |
|
893 * @method isValidChild |
|
894 * @param {String} name Element name to check for. |
|
895 * @param {String} child Element child to verify. |
|
896 * @return {Boolean} True/false if the element is a valid child of the specified parent. |
|
897 */ |
|
898 self.isValidChild = function(name, child) { |
|
899 var parent = children[name]; |
|
900 |
|
901 return !!(parent && parent[child]); |
|
902 }; |
|
903 |
|
904 /** |
|
905 * Returns true/false if the specified element name and optional attribute is |
|
906 * valid according to the schema. |
|
907 * |
|
908 * @method isValid |
|
909 * @param {String} name Name of element to check. |
|
910 * @param {String} attr Optional attribute name to check for. |
|
911 * @return {Boolean} True/false if the element and attribute is valid. |
|
912 */ |
|
913 self.isValid = function(name, attr) { |
|
914 var attrPatterns, i, rule = getElementRule(name); |
|
915 |
|
916 // Check if it's a valid element |
|
917 if (rule) { |
|
918 if (attr) { |
|
919 // Check if attribute name exists |
|
920 if (rule.attributes[attr]) { |
|
921 return true; |
|
922 } |
|
923 |
|
924 // Check if attribute matches a regexp pattern |
|
925 attrPatterns = rule.attributePatterns; |
|
926 if (attrPatterns) { |
|
927 i = attrPatterns.length; |
|
928 while (i--) { |
|
929 if (attrPatterns[i].pattern.test(name)) { |
|
930 return true; |
|
931 } |
|
932 } |
|
933 } |
|
934 } else { |
|
935 return true; |
|
936 } |
|
937 } |
|
938 |
|
939 // No match |
|
940 return false; |
|
941 }; |
|
942 |
|
943 /** |
|
944 * Returns true/false if the specified element is valid or not |
|
945 * according to the schema. |
|
946 * |
|
947 * @method getElementRule |
|
948 * @param {String} name Element name to check for. |
|
949 * @return {Object} Element object or undefined if the element isn't valid. |
|
950 */ |
|
951 self.getElementRule = getElementRule; |
|
952 |
|
953 /** |
|
954 * Returns an map object of all custom elements. |
|
955 * |
|
956 * @method getCustomElements |
|
957 * @return {Object} Name/value map object of all custom elements. |
|
958 */ |
|
959 self.getCustomElements = function() { |
|
960 return customElementsMap; |
|
961 }; |
|
962 |
|
963 /** |
|
964 * Parses a valid elements string and adds it to the schema. The valid elements |
|
965 * format is for example "element[attr=default|otherattr]". |
|
966 * Existing rules will be replaced with the ones specified, so this extends the schema. |
|
967 * |
|
968 * @method addValidElements |
|
969 * @param {String} valid_elements String in the valid elements format to be parsed. |
|
970 */ |
|
971 self.addValidElements = addValidElements; |
|
972 |
|
973 /** |
|
974 * Parses a valid elements string and sets it to the schema. The valid elements |
|
975 * format is for example "element[attr=default|otherattr]". |
|
976 * Existing rules will be replaced with the ones specified, so this extends the schema. |
|
977 * |
|
978 * @method setValidElements |
|
979 * @param {String} valid_elements String in the valid elements format to be parsed. |
|
980 */ |
|
981 self.setValidElements = setValidElements; |
|
982 |
|
983 /** |
|
984 * Adds custom non HTML elements to the schema. |
|
985 * |
|
986 * @method addCustomElements |
|
987 * @param {String} custom_elements Comma separated list of custom elements to add. |
|
988 */ |
|
989 self.addCustomElements = addCustomElements; |
|
990 |
|
991 /** |
|
992 * Parses a valid children string and adds them to the schema structure. The valid children |
|
993 * format is for example: "element[child1|child2]". |
|
994 * |
|
995 * @method addValidChildren |
|
996 * @param {String} valid_children Valid children elements string to parse |
|
997 */ |
|
998 self.addValidChildren = addValidChildren; |
|
999 |
|
1000 self.elements = elements; |
|
1001 }; |
|
1002 }); |
|