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