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