Roo/htmleditor/TidyWriter.js
[roojs1] / Roo / htmleditor / TidyWriter.js
1 /***
2  * This is based loosely on tinymce 
3  * @class Roo.htmleditor.TidyWriter
4  * https://github.com/thorn0/tinymce.html/blob/master/tinymce.html.js
5  *
6  * Known issues?
7  * - not tested much with 'PRE' formated elements.
8  * - long text inside of inline can be wrapped and clened?
9  *
10  *
11  */
12
13 Roo.htmleditor.TidyWriter = function(settings)
14 {
15     
16     // indent, indentBefore, indentAfter, encode, htmlOutput, html = [];
17     Roo.apply(this, settings);
18     this.html = [];
19     this.state = [];
20      
21     this.encode = Roo.htmleditor.TidyEntities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
22   
23 }
24 Roo.htmleditor.TidyWriter.prototype = {
25
26
27
28     makeMap : function (items, delim, map) {
29                 var i;
30                 items = items || [];
31                 delim = delim || ',';
32                 if (typeof items == "string") {
33                         items = items.split(delim);
34                 }
35                 map = map || {};
36                 i = items.length;
37                 while (i--) {
38                         map[items[i]] = {};
39                 }
40                 return map;
41         },
42
43     state : false,
44     
45     indent :  '  ',
46     
47     // part of state...
48     indentstr : '',
49     in_pre: false,
50     in_inline : false,
51     last_inline : false,
52     encode : false,
53      
54     
55             /**
56     * Writes the a start element such as <p id="a">.
57     *
58     * @method start
59     * @param {String} name Name of the element.
60     * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
61     * @param {Boolean} empty Optional empty state if the tag should end like <br />.
62     */
63     start: function(name, attrs, empty, node)
64     {
65         var i, l, attr, value;
66         
67         // there are some situations where adding line break && indentation will not work. will not work.
68         // <span / b / i ... formating?
69         
70         var in_inline = this.in_inline || Roo.htmleditor.TidyWriter.inline_elements.indexOf(name) > -1;
71         var in_pre    = this.in_pre    || Roo.htmleditor.TidyWriter.whitespace_elements.indexOf(name) > -1;
72         
73         var is_short   = empty ? Roo.htmleditor.TidyWriter.shortend_elements.indexOf(name) > -1 : false;
74         
75         var add_lb = name == 'BR' ? false : in_inline;
76         
77         if (!add_lb && !this.in_pre && this.lastElementEndsWS()) {
78             i_inline = false;
79         }
80
81         var indentstr =  this.indentstr;
82         
83         // e_inline = elements that can be inline, but still allow \n before and after?
84         // only 'BR' ??? any others?
85         
86         // ADD LINE BEFORE tage
87         if (!this.in_pre) {
88             if (in_inline) {
89                 //code
90                 if (name == 'BR') {
91                     this.addLine();
92                 } else if (this.lastElementEndsWS()) {
93                     this.addLine();
94                 } else{
95                     // otherwise - no new line. (and dont indent.)
96                     
97                     indentstr = '';
98                 }
99                 
100             } else {
101                 this.addLine();
102             }
103         } else {
104             indentstr = '';
105         }
106         
107         this.html.push(indentstr + '<', name.toLowerCase());
108         
109         if (attrs) {
110             for (i = 0, l = attrs.length; i < l; i++) {
111                 attr = attrs[i];
112                 this.html.push(' ', attr.name, '="', this.encode(attr.value, true), '"');
113             }
114         }
115      
116         if (empty) {
117             if (is_short) {
118                 this.html[this.html.length] = '/>';
119             } else {
120                 this.html[this.html.length] = '></' + name.toLowerCase() + '>';
121             }
122             var e_inline = name == 'BR' ? false : this.in_inline;
123             
124             if (!e_inline && !this.in_pre) {
125                 this.addLine();
126             }
127             return;
128         
129         }
130         // not empty..
131         this.html[this.html.length] = '>';
132         
133         // there is a special situation, where we need to turn on in_inline - if any of the imediate chidlren are one of these.
134         /*
135         if (!in_inline && !in_pre) {
136             var cn = node.firstChild;
137             while(cn) {
138                 if (Roo.htmleditor.TidyWriter.inline_elements.indexOf(cn.nodeName) > -1) {
139                     in_inline = true
140                     break;
141                 }
142                 cn = cn.nextSibling;
143             }
144              
145         }
146         */
147         
148         
149         this.pushState({
150             indentstr : in_pre || in_inline ? '' : (this.indentstr + this.indent),
151             in_pre : in_pre,
152             in_inline :  in_inline
153         });
154         // add a line after if we are not in a
155         
156         if (!in_inline && !in_pre) {
157             this.addLine();
158         }
159         
160             
161          
162         
163     },
164     
165     lastElementEndsWS : function()
166     {
167         var value = this.html.length > 0 ? this.html[this.html.length-1] : false;
168         if (value === false) {
169             return true;
170         }
171         return value.match(/\s+$/);
172         
173     },
174     
175     /**
176      * Writes the a end element such as </p>.
177      *
178      * @method end
179      * @param {String} name Name of the element.
180      */
181     end: function(name) {
182         var value;
183         this.popState();
184         var indentstr = '';
185         var in_inline = this.in_inline || Roo.htmleditor.TidyWriter.inline_elements.indexOf(name) > -1;
186         
187         if (!this.in_pre && !in_inline) {
188             this.addLine();
189             indentstr  = this.indentstr;
190         }
191         this.html.push(indentstr + '</', name.toLowerCase(), '>');
192         this.last_inline = in_inline;
193         
194         // pop the indent state..
195     },
196     /**
197      * Writes a text node.
198      *
199      * In pre - we should not mess with the contents.
200      * 
201      *
202      * @method text
203      * @param {String} text String to write out.
204      * @param {Boolean} raw Optional raw state if true the contents wont get encoded.
205      */
206     text: function(text, node)
207     {
208         // if not in whitespace critical
209         if (text.length < 1) {
210             return;
211         }
212         if (this.in_pre || this.in_inline) {
213             this.html[this.html.length] =  text;
214             return;   
215         }
216         // see if last element was a inline element.
217         var indentstr = this.indentstr;
218         if (node.previousSibling &&
219             node.previousSibling.nodeType == 1 &&
220             Roo.htmleditor.TidyWriter.inline_elements.indexOf(node.previousSibling.nodeName) > -1)
221         {
222             indentstr = '';
223         } else {
224             this.addLine();
225         }
226             
227         
228         
229         text = text.replace(/\s/g," ") // all line breaks to ' '
230                 .replace(/^\s+/,'')  // leding white space
231                 .replace(/\s+$/,''); // clean trailing white space
232         
233         if (text.length < 1) {
234             return;
235         }
236         if (!text.match(/\n/)) {
237             this.html.push(indentstr + text);
238             return;
239         }
240         
241         text = this.indentstr + text.replace(
242             /(?![^\n]{1,64}$)([^\n]{1,64})\s/g, '$1\n' + this.indentstr
243         );
244         // remoeve the last whitespace / line break.
245         text = text.replace(/\s+$/,''); 
246         
247         this.html.push(text);
248         
249         // split and indent..
250         
251         
252     },
253     /**
254      * Writes a cdata node such as <![CDATA[data]]>.
255      *
256      * @method cdata
257      * @param {String} text String to write out inside the cdata.
258      */
259     cdata: function(text) {
260         this.html.push('<![CDATA[', text, ']]>');
261     },
262     /**
263     * Writes a comment node such as <!-- Comment -->.
264     *
265     * @method cdata
266     * @param {String} text String to write out inside the comment.
267     */
268    comment: function(text) {
269        this.html.push('<!--', text, '-->');
270    },
271     /**
272      * Writes a PI node such as <?xml attr="value" ?>.
273      *
274      * @method pi
275      * @param {String} name Name of the pi.
276      * @param {String} text String to write out inside the pi.
277      */
278     pi: function(name, text) {
279         text ? this.html.push('<?', name, ' ', this.encode(text), '?>') : this.html.push('<?', name, '?>');
280         this.indent != '' && this.html.push('\n');
281     },
282     /**
283      * Writes a doctype node such as <!DOCTYPE data>.
284      *
285      * @method doctype
286      * @param {String} text String to write out inside the doctype.
287      */
288     doctype: function(text) {
289         this.html.push('<!DOCTYPE', text, '>', this.indent != '' ? '\n' : '');
290     },
291     /**
292      * Resets the internal buffer if one wants to reuse the writer.
293      *
294      * @method reset
295      */
296     reset: function() {
297         this.html.length = 0;
298         this.state = [];
299         this.pushState({
300             indentstr : '',
301             in_pre : false, 
302             in_inline : false
303         })
304     },
305     /**
306      * Returns the contents that got serialized.
307      *
308      * @method getContent
309      * @return {String} HTML contents that got written down.
310      */
311     getContent: function() {
312         return this.html.join('').replace(/\n$/, '');
313     },
314     
315     pushState : function(cfg)
316     {
317         this.state.push(cfg);
318         Roo.apply(this, cfg);
319     },
320     
321     popState : function()
322     {
323         if (this.state.length < 1) {
324             return; // nothing to push
325         }
326         var cfg = {
327             in_pre: false,
328             indentstr : ''
329         };
330         this.state.pop();
331         if (this.state.length > 0) {
332             cfg = this.state[this.state.length-1]; 
333         }
334         Roo.apply(this, cfg);
335     },
336     
337     addLine: function()
338     {
339         if (this.html.length < 1) {
340             return;
341         }
342         
343         
344         var value = this.html[this.html.length - 1];
345         if (value.length > 0 && '\n' !== value) {
346             this.html.push('\n');
347         }
348     }
349     
350     
351 //'pre script noscript style textarea video audio iframe object code'
352 // shortended... 'area base basefont br col frame hr img input isindex link  meta param embed source wbr track');
353 // inline 
354 };
355
356 Roo.htmleditor.TidyWriter.inline_elements = [
357         'SPAN','STRONG','B','EM','I','FONT','STRIKE','U','VAR',
358         'CITE','DFN','CODE','MARK','Q','SUP','SUB','SAMP'
359 ];
360 Roo.htmleditor.TidyWriter.shortend_elements = [
361     'AREA','BASE','BASEFONT','BR','COL','FRAME','HR','IMG','INPUT',
362     'ISINDEX','LINK','','META','PARAM','EMBED','SOURCE','WBR','TRACK'
363 ];
364
365 Roo.htmleditor.TidyWriter.whitespace_elements = [
366     'PRE','SCRIPT','NOSCRIPT','STYLE','TEXTAREA','VIDEO','AUDIO','IFRAME','OBJECT','CODE'
367 ];