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