c5464017d141ed2f0e5ed38f0e4e4fec36040fac
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / QuickTag / quick-tag.js
1 /**
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13 /*
14 * Quick Tag Editor Plugin for TYPO3 htmlArea RTE
15 */
16 HTMLArea.QuickTag = Ext.extend(HTMLArea.Plugin, {
17 /*
18 * This function gets called by the class constructor
19 */
20 configurePlugin: function (editor) {
21 this.pageTSConfiguration = this.editorConfiguration.buttons.inserttag;
22 this.allowedTags = (this.pageTSConfiguration && this.pageTSConfiguration.tags) ? this.pageTSConfiguration.tags : null;
23 this.denyTags = (this.pageTSConfiguration && this.pageTSConfiguration.denyTags) ? this.pageTSConfiguration.denyTags : null;
24 this.allowedAttribs = (this.pageTSConfiguration && this.pageTSConfiguration.allowedAttribs) ? this.pageTSConfiguration.allowedAttribs : null;
25 this.quotes = new RegExp('^\w+\s*([a-zA-Z_0-9:;]+=\"[^\"]*\"\s*|[a-zA-Z_0-9:;]+=\'[^\']*\'\s*)*$');
26 /*
27 * Registering plugin "About" information
28 */
29 var pluginInformation = {
30 version : '2.3',
31 developer : 'Cau Guanabara & Stanislas Rolland',
32 developerUrl : 'http://www.sjbr.ca',
33 copyrightOwner : 'Cau Guanabara & Stanislas Rolland',
34 sponsor : 'Independent production & SJBR',
35 sponsorUrl : 'http://www.sjbr.ca',
36 license : 'GPL'
37 };
38 this.registerPluginInformation(pluginInformation);
39 /*
40 * Registering the button
41 */
42 var buttonId = 'InsertTag';
43 var buttonConfiguration = {
44 id : buttonId,
45 tooltip : this.localize('Quick Tag Editor'),
46 iconCls : 'htmlarea-action-tag-insert',
47 action : 'onButtonPress',
48 selection : true,
49 dialog : true
50 };
51 this.registerButton(buttonConfiguration);
52 return true;
53 },
54 /*
55 * Sets of default configuration values for dialogue form fields
56 */
57 configDefaults: {
58 combo: {
59 editable: true,
60 typeAhead: true,
61 triggerAction: 'all',
62 forceSelection: true,
63 mode: 'local',
64 valueField: 'value',
65 displayField: 'text',
66 helpIcon: true
67 }
68 },
69 /*
70 * This function gets called when the button was pressed.
71 *
72 * @param object editor: the editor instance
73 * @param string id: the button id or the key
74 * @param object target: the target element of the contextmenu event, when invoked from the context menu
75 *
76 * @return boolean false if action is completed
77 */
78 onButtonPress: function (editor, id, target) {
79 // Could be a button or its hotkey
80 var buttonId = this.translateHotKey(id);
81 buttonId = buttonId ? buttonId : id;
82 this.openDialogue(
83 'Quick Tag Editor',
84 {
85 buttonId: buttonId
86 },
87 this.getWindowDimensions({ width: 570}, buttonId),
88 this.buildItemsConfig(),
89 this.setTag
90 );
91 this.insertedTag = this.dialog.find('itemId', 'insertedTag')[0];
92 this.tagCombo = this.dialog.find('itemId', 'tags')[0];
93 this.attributeCombo = this.dialog.find('itemId', 'attributes')[0];
94 this.valueCombo = this.dialog.find('itemId', 'values')[0];
95 this.colorCombo = this.dialog.find('itemId', 'colors')[0];
96 },
97 /*
98 * Build the window items config
99 */
100 buildItemsConfig: function (element, buttonId) {
101 var tagStore = new Ext.data.ArrayStore({
102 autoDestroy: true,
103 fields: [ { name: 'text'}, { name: 'value'}],
104 data: this.tags
105 });
106 if (this.denyTags) {
107 var denyTags = new RegExp('^(' + this.denyTags.split(',').join('|').replace(/ /g, '') + ')$', 'i');
108 tagStore.filterBy(function (record) {
109 return !denyTags.test(record.get('value'));
110 });
111 // Make sure the combo list is filtered
112 tagStore.snapshot = tagStore.data;
113 }
114 var attributeStore = new Ext.data.ArrayStore({
115 autoDestroy: true,
116 fields: [ {name: 'tag'}, { name: 'text'}, { name: 'value'}],
117 data: this.attributes
118 });
119 this.valueRecord = Ext.data.Record.create([{name: 'attribute'}, { name: 'text'}, { name: 'value'}]);
120 var valueStore = new Ext.data.ArrayStore({
121 autoDestroy: true,
122 fields: [ {name: 'attribute'}, { name: 'text'}, { name: 'value'}],
123 data: this.values,
124 listeners: {
125 load: {
126 fn: this.captureClasses,
127 scope: this
128 }
129 }
130 });
131 var itemsConfig = [{
132 xtype: 'textarea',
133 itemId: 'tagopen',
134 width: 400,
135 itemId: 'insertedTag',
136 fieldLabel: '<',
137 labelSeparator: '',
138 grow: true,
139 listeners: {
140 change: {
141 fn: this.filterAttributes,
142 scope: this
143 },
144 focus: {
145 fn: this.filterAttributes,
146 scope: this
147 }
148 }
149 },{
150 xtype: 'displayfield',
151 text: '>'
152 }, Ext.apply({
153 xtype: 'combo',
154 itemId: 'tags',
155 fieldLabel: this.localize('TAGs'),
156 store: tagStore,
157 listeners: {
158 select: {
159 fn: this.onTagSelect,
160 scope: this
161 }
162 }
163 }, this.configDefaults['combo'])
164 , Ext.apply({
165 xtype: 'combo',
166 itemId: 'attributes',
167 fieldLabel: this.localize('ATTRIBUTES'),
168 store: attributeStore,
169 hidden: true,
170 listeners: {
171 select: {
172 fn: this.onAttributeSelect,
173 scope: this
174 }
175 }
176 }, this.configDefaults['combo'])
177 , Ext.apply({
178 xtype: 'combo',
179 itemId: 'values',
180 fieldLabel: this.localize('OPTIONS'),
181 store: valueStore,
182 hidden: true,
183 listeners: {
184 select: {
185 fn: this.onValueSelect,
186 scope: this
187 }
188 }
189 }, this.configDefaults['combo'])
190 ,{
191 xtype: 'colorpalettefield',
192 fieldLabel: this.localize('Colors'),
193 itemId: 'colors',
194 colors: this.editorConfiguration.disableColorPicker ? [] : null,
195 colorsConfiguration: this.editorConfiguration.colors,
196 hidden: true,
197 listeners: {
198 select: {
199 fn: this.onColorSelect,
200 scope: this
201 }
202 }
203 }
204 ];
205 return {
206 xtype: 'fieldset',
207 title: this.localize('Quick Tag Editor'),
208 defaultType: 'textfield',
209 labelWidth: 100,
210 defaults: {
211 helpIcon: true
212 },
213 items: itemsConfig
214 };
215 },
216 /*
217 * Add a record for each class selector found in the stylesheets
218 */
219 captureClasses: function (valueStore) {
220 this.parseCssRule(this.editor.document.styleSheets, valueStore);
221 },
222 parseCssRule: function (rules, valueStore) {
223 Ext.each(rules, function (rule) {
224 if (rule.selectorText) {
225 if (/^(\w*)\.(\w+)$/.test(rule.selectorText)) {
226 valueStore.add(new this.valueRecord({
227 attribute: 'class',
228 text: rule.selectorText,
229 value: RegExp.$2 + '"'
230 }));
231 }
232 } else {
233 // ImportRule (Mozilla)
234 if (rule.styleSheet) {
235 this.parseCssRule(rule.styleSheet.cssRules, valueStore);
236 }
237 // MediaRule (Mozilla)
238 if (rule.cssRules) {
239 this.parseCssRule(rule.cssRules, valueStore);
240 }
241 // IE imports
242 if (rule.imports) {
243 this.parseCssRule(rule.imports, valueStore);
244 }
245 if (rule.rules) {
246 this.parseCssRule(rule.rules, valueStore);
247 }
248 }
249 }, this);
250 },
251 /*
252 * Handler invoked when a tag is selected
253 * Update the attributes combo and the inserted tag field
254 */
255 onTagSelect: function (tagCombo, tagRecord) {
256 var tag = tagRecord.get('value');
257 this.filterAttributes();
258 this.attributeCombo.clearValue();
259 this.attributeCombo.show();
260 this.valueCombo.hide();
261 this.insertedTag.setValue(tag);
262 this.insertedTag.focus(false, 50);
263 },
264 /*
265 * Filter out attributes not applicable to the tag, already present in the tag or not allowed
266 */
267 filterAttributes: function () {
268 var tag = this.tagCombo.getValue();
269 var insertedTag = this.insertedTag.getValue();
270 var attributeStore = this.attributeCombo.getStore();
271 if (attributeStore.realSnapshot) {
272 attributeStore.snapshot = attributeStore.realSnapshot;
273 delete attributeStore.realSnapshot;
274 attributeStore.clearFilter(true);
275 }
276 var allowedAttribs = '';
277 if (this.allowedAttribs) {
278 allowedAttribs = this.allowedAttribs.split(',').join('|').replace(/ /g, '');
279 }
280 if (this.tags && this.tags[tag] && this.tags[tag].allowedAttribs) {
281 allowedAttribs += allowedAttribs ? '|' : '';
282 allowedAttribs += this.tags[tag].allowedAttribs.split(',').join('|').replace(/ /g, '');
283 }
284 if (allowedAttribs) {
285 var allowedAttribs = new RegExp('^(' + allowedAttribs + ')$');
286 }
287 attributeStore.filterBy(function (attributeRecord) {
288 // Filter out attributes already used in the tag, not applucable to tag or not allowed
289 var testAttrib = new RegExp('(' + attributeRecord.get('value') + ')', 'ig');
290 var tagValue = attributeRecord.get('tag');
291 return (tagValue == 'all' || tagValue == tag) && !testAttrib.test(insertedTag) && (!allowedAttribs || allowedAttribs.test(attributeRecord.get('text')));
292 });
293 // Make sure the combo list is filtered
294 attributeStore.realSnapshot = attributeStore.snapshot;
295 attributeStore.snapshot = attributeStore.data;
296 },
297 /*
298 * Filter out not applicable to the attribute or style values already present in the tag
299 * Filter out classes not applicable to the current tag
300 */
301 filterValues: function (attribute) {
302 var tag = this.tagCombo.getValue();
303 var insertedTag = this.insertedTag.getValue();
304 var valueStore = this.valueCombo.getStore();
305 if (valueStore.realSnapshot) {
306 valueStore.snapshot = valueStore.realSnapshot;
307 delete valueStore.realSnapshot;
308 valueStore.clearFilter(true);
309 }
310 var expr = new RegExp('(^' + tag + '[\.])|(^[\.])', 'i');
311 valueStore.filterBy(function (valueRecord) {
312 var value = valueRecord.get('value');
313 if (attribute === 'style') {
314 expr = new RegExp('(' + ((value.charAt(0) == '+' || value.charAt(0) == '-') ? '\\' : '') + value + ')', 'ig');
315 }
316 return valueRecord.get('attribute') == attribute && (attribute !== 'style' || !expr.test(insertedTag)) && (attribute !== 'class' || expr.test(valueRecord.get('text')));
317 });
318 // Make sure the combo list is filtered
319 valueStore.realSnapshot = valueStore.snapshot;
320 valueStore.snapshot = valueStore.data;
321 this.valueCombo.setVisible(valueStore.getCount() ? true : false);
322 },
323 /*
324 * Handler invoked when an attribute is selected
325 * Update the values combo and the inserted tag field
326 */
327 onAttributeSelect: function (attributeCombo, attributeRecord) {
328 var insertedTag = this.insertedTag.getValue();
329 var attribute = attributeRecord.get('text');
330 this.valueCombo.clearValue();
331 if (/color/.test(attribute)) {
332 this.valueCombo.hide();
333 this.colorCombo.show();
334 } else {
335 this.filterValues(attribute);
336 }
337 this.insertedTag.setValue(insertedTag + ((/\"/.test(insertedTag) && (!/\"$/.test(insertedTag) || /=\"$/.test(insertedTag))) ? '" ' : ' ') + attributeRecord.get('value'));
338 this.insertedTag.focus(false, 50);
339 },
340 /*
341 * Handler invoked when a value is selected
342 * Update the inserted tag field
343 */
344 onValueSelect: function (combo, record) {
345 var style = this.attributeCombo.getValue() === 'style="';
346 this.insertedTag.setValue(this.insertedTag.getValue() + (style && !/="$/.test(this.insertedTag.getValue()) ? '; ' : '') + combo.getValue());
347 this.insertedTag.focus(false, 50);
348 combo.clearValue();
349 if (style) {
350 if (/color/.test(record.get('text'))) {
351 this.colorCombo.show();
352 }
353 } else {
354 combo.hide();
355 this.attributeCombo.clearValue();
356 }
357 },
358 /*
359 * Handler invoked when a color is selected
360 * Update the inserted tag field
361 */
362 onColorSelect: function (combo, record) {
363 var style = this.attributeCombo.getValue() === 'style="';
364 this.insertedTag.setValue(this.insertedTag.getValue() + '#' + combo.getValue() + (style ? '' : '"'));
365 this.insertedTag.focus(false, 50);
366 combo.setValue('');
367 combo.hide();
368 if (!style) {
369 this.attributeCombo.clearValue();
370 }
371 },
372 /*
373 * Handler invoked when a OK button is pressed
374 */
375 setTag: function (button, event) {
376 this.restoreSelection();
377 var insertedTag = this.insertedTag.getValue();
378 var currentTag = this.tagCombo.getValue();
379 if (!insertedTag) {
380 TYPO3.Dialog.InformationDialog({
381 title: this.getButton('InsertTag').tooltip.title,
382 msg: this.localize('Enter the TAG you want to insert'),
383 fn: function () { this.insertedTag.focus(); },
384 scope: this
385 });
386 event.stopEvent();
387 return false;
388 }
389 if (this.quotes.test(insertedTag)) {
390 if (this.quotes.test(insertedTag + '"')) {
391 TYPO3.Dialog.InformationDialog({
392 title: this.getButton('InsertTag').tooltip.title,
393 msg: this.localize('There are some unclosed quote'),
394 fn: function () { this.insertedTag.focus(); this.insertedTag.select(); },
395 scope: this
396 });
397 event.stopEvent();
398 return false;
399 } else {
400 this.insertedTag.setValue(insertedTag + '"');
401 }
402 }
403 insertedTag = insertedTag.replace(/(<|>)/g, '');
404 var tagOpen = '<' + insertedTag + '>';
405 var tagClose = tagOpen.replace(/^<(\w+) ?.*>/, '</$1>');
406 var subTags = this.subTags[currentTag];
407 if (subTags) {
408 tagOpen = tagOpen + this.subTags.open;
409 tagClose = this.subTags.close + tagClose;
410 }
411 this.editor.getSelection().surroundHtml(tagOpen, tagClose);
412 this.close();
413 event.stopEvent();
414 },
415 /*
416 * Open the dialogue window
417 *
418 * @param string title: the window title
419 * @param object arguments: some arguments for the handler
420 * @param integer dimensions: the opening dimensions of the window
421 * @param object items: the configuration of the window items
422 * @param function handler: handler when the OK button if clicked
423 *
424 * @return void
425 */
426 openDialogue: function (title, arguments, dimensions, items, handler) {
427 if (this.dialog) {
428 this.dialog.close();
429 }
430 this.dialog = new Ext.Window({
431 title: this.localize(title),
432 arguments: arguments,
433 cls: 'htmlarea-window',
434 border: false,
435 width: dimensions.width,
436 height: 'auto',
437 iconCls: this.getButton(arguments.buttonId).iconCls,
438 listeners: {
439 close: {
440 fn: this.onClose,
441 scope: this
442 }
443 },
444 items: {
445 xtype: 'container',
446 layout: 'form',
447 defaults: {
448 labelWidth: 150
449 },
450 items: items
451 },
452 buttons: [
453 this.buildButtonConfig('OK', handler),
454 this.buildButtonConfig('Cancel', this.onCancel)
455 ]
456 });
457 this.show();
458 },
459 tags: [
460 ['a', 'a'],
461 ['abbr', 'abbr'],
462 ['acronym', 'acronym'],
463 ['address', 'address'],
464 ['b', 'b'],
465 ['big', 'big'],
466 ['blockquote', 'blockquote'],
467 ['cite', 'cite'],
468 ['code', 'code'],
469 ['div', 'div'],
470 ['em', 'em'],
471 ['fieldset', 'fieldset'],
472 ['font', 'font'],
473 ['h1', 'h1'],
474 ['h2', 'h2'],
475 ['h3', 'h3'],
476 ['h4', 'h4'],
477 ['h5', 'h5'],
478 ['h6', 'h6'],
479 ['i', 'i'],
480 ['legend', 'legend'],
481 ['li', 'li'],
482 ['ol', 'ol'],
483 ['ul', 'ul'],
484 ['p', 'p'],
485 ['pre', 'pre'],
486 ['q', 'q'],
487 ['small', 'small'],
488 ['span', 'span'],
489 ['strike', 'strike'],
490 ['strong', 'strong'],
491 ['sub', 'sub'],
492 ['sup', 'sup'],
493 ['table', 'table'],
494 ['tt', 'tt'],
495 ['u', 'u']
496 ],
497 attributes: [
498 ['all', 'class', 'class="'],
499 ['all', 'dir', 'dir="'],
500 ['all', 'id', 'id="'],
501 ['all', 'lang', 'lang="'],
502 ['all', 'onFocus', 'onFocus="'],
503 ['all', 'onBlur', 'onBlur="'],
504 ['all', 'onClick', 'onClick="'],
505 ['all', 'onDblClick', 'onDblClick="'],
506 ['all', 'onMouseDown', 'onMouseDown="'],
507 ['all', 'onMouseUp', 'onMouseUp="'],
508 ['all', 'onMouseOver', 'onMouseOver="'],
509 ['all', 'onMouseMove', 'onMouseMove="'],
510 ['all', 'onMouseOut', 'onMouseOut="'],
511 ['all', 'onKeyPress', 'onKeyPress="'],
512 ['all', 'onKeyDown', 'onKeyDown="'],
513 ['all', 'onKeyUp', 'onKeyUp="'],
514 ['all', 'style', 'style="'],
515 ['all', 'title', 'title="'],
516 ['all', 'xml:lang', 'xml:lang="'],
517 ['a', 'href', 'href="'],
518 ['a', 'name', 'name="'],
519 ['a', 'target', 'target="'],
520 ['font', 'face', 'face="'],
521 ['font', 'size', 'size="'],
522 ['font', 'color', 'color="'],
523 ['div', 'align', 'align="'],
524 ['h1', 'align', 'align="'],
525 ['h2', 'align', 'align="'],
526 ['h3', 'align', 'align="'],
527 ['h4', 'align', 'align="'],
528 ['h5', 'align', 'align="'],
529 ['h6', 'align', 'align="'],
530 ['p', 'align', 'align="'],
531 ['table', 'align', 'align="'],
532 ['table', 'width', 'width="'],
533 ['table', 'height', 'height="'],
534 ['table', 'cellpadding', 'cellpadding="'],
535 ['table', 'cellspacing', 'cellspacing="'],
536 ['table', 'background', 'background="'],
537 ['table', 'bgcolor', 'bgcolor="'],
538 ['table', 'border', 'border="'],
539 ['table', 'bordercolor', 'bordercolor="']
540 ],
541 values: [
542 ['href', 'http://', 'http://'],
543 ['href', 'https://', 'https://'],
544 ['href', 'ftp://', 'ftp://'],
545 ['href', 'mailto:', 'mailto:'],
546 ['href', '#', '#"'],
547 ['target', '_top', '_top"'],
548 ['target', '_self', '_self"'],
549 ['target', '_parent', '_parent"'],
550 ['target', '_blank', '_blank"'],
551 ['face', 'Verdana', 'Verdana"'],
552 ['face', 'Arial', 'Arial"'],
553 ['face', 'Tahoma', 'Tahoma"'],
554 ['face', 'Courier New', 'Courier New"'],
555 ['face', 'Times New Roman', 'Times New Roman"'],
556 ['size', '1', '1"'],
557 ['size', '2', '2"'],
558 ['size', '3', '3"'],
559 ['size', '4', '4"'],
560 ['size', '5', '5"'],
561 ['size', '6', '6"'],
562 ['size', '+1', '+1"'],
563 ['size', '+2', '+2"'],
564 ['size', '+3', '+3"'],
565 ['size', '+4', '+4"'],
566 ['size', '+5', '+5"'],
567 ['size', '+6', '+6"'],
568 ['size', '-1', '-1"'],
569 ['size', '-2', '-2"'],
570 ['size', '-3', '-3"'],
571 ['size', '-4', '-4"'],
572 ['size', '-5', '-5"'],
573 ['size', '-6', '-6"'],
574 ['align', 'center', 'center"'],
575 ['align', 'left', 'left"'],
576 ['align', 'right', 'right"'],
577 ['align', 'justify', 'justify"'],
578 ['dir', 'rtl', 'rtl"'],
579 ['dir', 'ltr', 'ltr"'],
580 ['lang', 'Afrikaans ', 'af"'],
581 ['lang', 'Albanian ', 'sq"'],
582 ['lang', 'Arabic ', 'ar"'],
583 ['lang', 'Basque ', 'eu"'],
584 ['lang', 'Breton ', 'br"'],
585 ['lang', 'Bulgarian ', 'bg"'],
586 ['lang', 'Belarusian ', 'be"'],
587 ['lang', 'Catalan ', 'ca"'],
588 ['lang', 'Chinese ', 'zh"'],
589 ['lang', 'Croatian ', 'hr"'],
590 ['lang', 'Czech ', 'cs"'],
591 ['lang', 'Danish ', 'da"'],
592 ['lang', 'Dutch ', 'nl"'],
593 ['lang', 'English ', 'en"'],
594 ['lang', 'Estonian ', 'et"'],
595 ['lang', 'Faeroese ', 'fo"'],
596 ['lang', 'Farsi ', 'fa"'],
597 ['lang', 'Finnish ', 'fi"'],
598 ['lang', 'French ', 'fr"'],
599 ['lang', 'Gaelic ', 'gd"'],
600 ['lang', 'German ', 'de"'],
601 ['lang', 'Greek ', 'el"'],
602 ['lang', 'Hebrew ', 'he"'],
603 ['lang', 'Hindi ', 'hi"'],
604 ['lang', 'Hungarian ', 'hu"'],
605 ['lang', 'Icelandic ', 'is"'],
606 ['lang', 'Indonesian ', 'id"'],
607 ['lang', 'Italian ', 'it"'],
608 ['lang', 'Japanese ', 'ja"'],
609 ['lang', 'Korean ', 'ko"'],
610 ['lang', 'Latvian ', 'lv"'],
611 ['lang', 'Lithuanian ', 'lt"'],
612 ['lang', 'Macedonian ', 'mk"'],
613 ['lang', 'Malaysian ', 'ms"'],
614 ['lang', 'Maltese ', 'mt"'],
615 ['lang', 'Norwegian ', 'no"'],
616 ['lang', 'Polish ', 'pl"'],
617 ['lang', 'Portuguese ', 'pt"'],
618 ['lang', 'Rhaeto-Romanic ', 'rm"'],
619 ['lang', 'Romanian ', 'ro"'],
620 ['lang', 'Russian ', 'ru"'],
621 ['lang', 'Sami ', 'sz"'],
622 ['lang', 'Serbian ', 'sr"'],
623 ['lang', 'Setswana ', 'tn"'],
624 ['lang', 'Slovak ', 'sk"'],
625 ['lang', 'Slovenian ', 'sl"'],
626 ['lang', 'Spanish ', 'es"'],
627 ['lang', 'Sutu ', 'sx"'],
628 ['lang', 'Swedish ', 'sv"'],
629 ['lang', 'Thai ', 'th"'],
630 ['lang', 'Tsonga ', 'ts"'],
631 ['lang', 'Turkish ', 'tr"'],
632 ['lang', 'Ukrainian ', 'uk"'],
633 ['lang', 'Urdu ', 'ur"'],
634 ['lang', 'Vietnamese ', 'vi"'],
635 ['lang', 'Xhosa ', 'xh"'],
636 ['lang', 'Yiddish ', 'yi"'],
637 ['lang', 'Zulu', 'zu"'],
638 ['style', 'azimuth', 'azimuth: '],
639 ['style', 'background', 'background: '],
640 ['style', 'background-attachment', 'background-attachment: '],
641 ['style', 'background-color', 'background-color: '],
642 ['style', 'background-image', 'background-image: '],
643 ['style', 'background-position', 'background-position: '],
644 ['style', 'background-repeat', 'background-repeat: '],
645 ['style', 'border', 'border: '],
646 ['style', 'border-bottom', 'border-bottom: '],
647 ['style', 'border-left', 'border-left: '],
648 ['style', 'border-right', 'border-right: '],
649 ['style', 'border-top', 'border-top: '],
650 ['style', 'border-bottom-color', 'border-bottom-color: '],
651 ['style', 'border-left-color', 'border-left-color: '],
652 ['style', 'border-right-color', 'border-right-color: '],
653 ['style', 'border-top-color', 'border-top-color: '],
654 ['style', 'border-bottom-style', 'border-bottom-style: '],
655 ['style', 'border-left-style', 'border-left-style: '],
656 ['style', 'border-right-style', 'border-right-style: '],
657 ['style', 'border-top-style', 'border-top-style: '],
658 ['style', 'border-bottom-width', 'border-bottom-width: '],
659 ['style', 'border-left-width', 'border-left-width: '],
660 ['style', 'border-right-width', 'border-right-width: '],
661 ['style', 'border-top-width', 'border-top-width: '],
662 ['style', 'border-collapse', 'border-collapse: '],
663 ['style', 'border-color', 'border-color: '],
664 ['style', 'border-style', 'border-style: '],
665 ['style', 'border-width', 'border-width: '],
666 ['style', 'bottom', 'bottom: '],
667 ['style', 'caption-side', 'caption-side: '],
668 ['style', 'cell-spacing', 'cell-spacing: '],
669 ['style', 'clear', 'clear: '],
670 ['style', 'clip', 'clip: '],
671 ['style', 'color', 'color: '],
672 ['style', 'column-span', 'column-span: '],
673 ['style', 'content', 'content: '],
674 ['style', 'cue', 'cue: '],
675 ['style', 'cue-after', 'cue-after: '],
676 ['style', 'cue-before', 'cue-before: '],
677 ['style', 'cursor', 'cursor: '],
678 ['style', 'direction', 'direction: '],
679 ['style', 'display', 'display: '],
680 ['style', 'elevation', 'elevation: '],
681 ['style', 'filter', 'filter: '],
682 ['style', 'float', 'float: '],
683 ['style', 'font-family', 'font-family: '],
684 ['style', 'font-size', 'font-size: '],
685 ['style', 'font-size-adjust', 'font-size-adjust: '],
686 ['style', 'font-style', 'font-style: '],
687 ['style', 'font-variant', 'font-variant: '],
688 ['style', 'font-weight', 'font-weight: '],
689 ['style', 'height', 'height: '],
690 ['style', '!important', '!important: '],
691 ['style', 'left', 'left: '],
692 ['style', 'letter-spacing', 'letter-spacing: '],
693 ['style', 'line-height', 'line-height: '],
694 ['style', 'list-style', 'list-style: '],
695 ['style', 'list-style-image', 'list-style-image: '],
696 ['style', 'list-style-position', 'list-style-position: '],
697 ['style', 'list-style-type', 'list-style-type: '],
698 ['style', 'margin', 'margin: '],
699 ['style', 'margin-bottom', 'margin-bottom: '],
700 ['style', 'margin-left', 'margin-left: '],
701 ['style', 'margin-right', 'margin-right: '],
702 ['style', 'margin-top', 'margin-top: '],
703 ['style', 'marks', 'marks: '],
704 ['style', 'max-height', 'max-height: '],
705 ['style', 'min-height', 'min-height: '],
706 ['style', 'max-width', 'max-width: '],
707 ['style', 'min-width', 'min-width: '],
708 ['style', 'orphans', 'orphans: '],
709 ['style', 'overflow', 'overflow: '],
710 ['style', 'padding', 'padding: '],
711 ['style', 'padding-bottom', 'padding-bottom: '],
712 ['style', 'padding-left', 'padding-left: '],
713 ['style', 'padding-right', 'padding-right: '],
714 ['style', 'padding-top', 'padding-top: '],
715 ['style', 'page-break-after', 'page-break-after: '],
716 ['style', 'page-break-before', 'page-break-before: '],
717 ['style', 'pause', 'pause: '],
718 ['style', 'pause-after', 'pause-after: '],
719 ['style', 'pause-before', 'pause-before: '],
720 ['style', 'pitch', 'pitch: '],
721 ['style', 'pitch-range', 'pitch-range: '],
722 ['style', 'play-during', 'play-during: '],
723 ['style', 'position', 'position: '],
724 ['style', 'richness', 'richness: '],
725 ['style', 'right', 'right: '],
726 ['style', 'row-span', 'row-span: '],
727 ['style', 'size', 'size: '],
728 ['style', 'speak', 'speak: '],
729 ['style', 'speak-date', 'speak-date: '],
730 ['style', 'speak-header', 'speak-header: '],
731 ['style', 'speak-numeral', 'speak-numeral: '],
732 ['style', 'speak-punctuation', 'speak-punctuation: '],
733 ['style', 'speak-time', 'speak-time: '],
734 ['style', 'speech-rate', 'speech-rate: '],
735 ['style', 'stress', 'stress: '],
736 ['style', 'table-layout', 'table-layout: '],
737 ['style', 'text-align', 'text-align: '],
738 ['style', 'text-decoration', 'text-decoration: '],
739 ['style', 'text-indent', 'text-indent: '],
740 ['style', 'text-shadow', 'text-shadow: '],
741 ['style', 'text-transform', 'text-transform: '],
742 ['style', 'top', 'top: '],
743 ['style', 'vertical-align', 'vertical-align: '],
744 ['style', 'visibility', 'visibility: '],
745 ['style', 'voice-family', 'voice-family: '],
746 ['style', 'volume', 'volume: '],
747 ['style', 'white-space', 'white-space: '],
748 ['style', 'widows', 'widows: '],
749 ['style', 'width', 'width: '],
750 ['style', 'word-spacing', 'word-spacing: '],
751 ['style', 'z-index', 'z-index: ']
752 ],
753 subTags: {
754 'table': {
755 'open': '<tbody><tr><td>',
756 'close': '</td></tr></tbody>'
757 }
758 }
759 });