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