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