sync
[roojs1] / Roo / htmleditor / TidyWriter.js
index 8893386..dfb1144 100644 (file)
@@ -1,7 +1,13 @@
 /***
  * This is based loosely on tinymce 
  * @class Roo.htmleditor.TidyWriter
+ * https://github.com/thorn0/tinymce.html/blob/master/tinymce.html.js
+ *
+ * Known issues?
+ * - not tested much with 'PRE' formated elements.
  * 
+ *
+ *
  */
 
 Roo.htmleditor.TidyWriter = function(settings)
@@ -10,157 +16,385 @@ Roo.htmleditor.TidyWriter = function(settings)
     // indent, indentBefore, indentAfter, encode, htmlOutput, html = [];
     Roo.apply(this, settings);
     this.html = [];
-    
-    this.indentBefore =this.makeMap(settings.indent_before || '');
-    this.indentAfter = this.makeMap(settings.indent_after || '');
+    this.state = [];
+     
     this.encode = Roo.htmleditor.TidyEntities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
-    
+  
 }
-Roo.apply(Roo.htmleditor.TidyWriter,
-          {
-     
-
-    makeMap : function (items, delim, map) {
-               var i;
-
-               items = items || [];
-               delim = delim || ',';
-
-               if (typeof items == "string") {
-                       items = items.split(delim);
-               }
-
-               map = map || {};
-
-               i = items.length;
-               while (i--) {
-                       map[items[i]] = {};
-               }
-
-               return map;
-       },
+Roo.htmleditor.TidyWriter.prototype = {
 
-
-    indent : 0,
-    indentBefore : false,
-    indentAfter : false,
-    encod : false,
+    state : false,
     
-    encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
-        htmlOutput = 'html' == settings.element_format;
+    indent :  '  ',
+    
+    // part of state...
+    indentstr : '',
+    in_pre: false,
+    in_inline : false,
+    last_inline : false,
+    encode : false,
+     
     
-
             /**
-             * Writes the a start element such as <p id="a">.
-             *
-             * @method start
-             * @param {String} name Name of the element.
-             * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
-             * @param {Boolean} empty Optional empty state if the tag should end like <br />.
-             */
-            start: function(name, attrs, empty) {
-                var i, l, attr, value;
-                if (indent && indentBefore[name] && html.length > 0) {
-                    value = html[html.length - 1];
-                    value.length > 0 && '\n' !== value && html.push('\n');
+    * Writes the a start element such as <p id="a">.
+    *
+    * @method start
+    * @param {String} name Name of the element.
+    * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
+    * @param {Boolean} empty Optional empty state if the tag should end like <br />.
+    */
+    start: function(name, attrs, empty, node)
+    {
+        var i, l, attr, value;
+        
+        // there are some situations where adding line break && indentation will not work. will not work.
+        // <span / b / i ... formating?
+        
+        var in_inline = this.in_inline || Roo.htmleditor.TidyWriter.inline_elements.indexOf(name) > -1;
+        var in_pre    = this.in_pre    || Roo.htmleditor.TidyWriter.whitespace_elements.indexOf(name) > -1;
+        
+        var is_short   = empty ? Roo.htmleditor.TidyWriter.shortend_elements.indexOf(name) > -1 : false;
+        
+        var add_lb = name == 'BR' ? false : in_inline;
+        
+        if (!add_lb && !this.in_pre && this.lastElementEndsWS()) {
+            i_inline = false;
+        }
+
+        var indentstr =  this.indentstr;
+        
+        // e_inline = elements that can be inline, but still allow \n before and after?
+        // only 'BR' ??? any others?
+        
+        // ADD LINE BEFORE tage
+        if (!this.in_pre) {
+            if (in_inline) {
+                //code
+                if (name == 'BR') {
+                    this.addLine();
+                } else if (this.lastElementEndsWS()) {
+                    this.addLine();
+                } else{
+                    // otherwise - no new line. (and dont indent.)
+                    indentstr = '';
                 }
-                html.push('<', name);
-                if (attrs) {
-                    for (i = 0, l = attrs.length; i < l; i++) {
-                        attr = attrs[i];
-                        html.push(' ', attr.name, '="', encode(attr.value, true), '"');
-                    }
+                
+            } else {
+                this.addLine();
+            }
+        } else {
+            indentstr = '';
+        }
+        
+        this.html.push(indentstr + '<', name.toLowerCase());
+        
+        if (attrs) {
+            for (i = 0, l = attrs.length; i < l; i++) {
+                attr = attrs[i];
+                this.html.push(' ', attr.name, '="', this.encode(attr.value, true), '"');
+            }
+        }
+     
+        if (empty) {
+            if (is_short) {
+                this.html[this.html.length] = '/>';
+            } else {
+                this.html[this.html.length] = '></' + name.toLowerCase() + '>';
+            }
+            var e_inline = name == 'BR' ? false : this.in_inline;
+            
+            if (!e_inline && !this.in_pre) {
+                this.addLine();
+            }
+            return;
+        
+        }
+        // not empty..
+        this.html[this.html.length] = '>';
+        
+        // there is a special situation, where we need to turn on in_inline - if any of the imediate chidlren are one of these.
+        /*
+        if (!in_inline && !in_pre) {
+            var cn = node.firstChild;
+            while(cn) {
+                if (Roo.htmleditor.TidyWriter.inline_elements.indexOf(cn.nodeName) > -1) {
+                    in_inline = true
+                    break;
                 }
-                html[html.length] = !empty || htmlOutput ? '>' : ' />';
-                if (empty && indent && indentAfter[name] && html.length > 0) {
-                    value = html[html.length - 1];
-                    value.length > 0 && '\n' !== value && html.push('\n');
+                cn = cn.nextSibling;
+            }
+             
+        }
+        */
+        
+        
+        this.pushState({
+            indentstr : in_pre   ? '' : (this.indentstr + this.indent),
+            in_pre : in_pre,
+            in_inline :  in_inline
+        });
+        // add a line after if we are not in a
+        
+        if (!in_inline && !in_pre) {
+            this.addLine();
+        }
+        
+            
+         
+        
+    },
+    
+    lastElementEndsWS : function()
+    {
+        var value = this.html.length > 0 ? this.html[this.html.length-1] : false;
+        if (value === false) {
+            return true;
+        }
+        return value.match(/\s+$/);
+        
+    },
+    
+    /**
+     * Writes the a end element such as </p>.
+     *
+     * @method end
+     * @param {String} name Name of the element.
+     */
+    end: function(name) {
+        var value;
+        this.popState();
+        var indentstr = '';
+        var in_inline = this.in_inline || Roo.htmleditor.TidyWriter.inline_elements.indexOf(name) > -1;
+        
+        if (!this.in_pre && !in_inline) {
+            this.addLine();
+            indentstr  = this.indentstr;
+        }
+        this.html.push(indentstr + '</', name.toLowerCase(), '>');
+        this.last_inline = in_inline;
+        
+        // pop the indent state..
+    },
+    /**
+     * Writes a text node.
+     *
+     * In pre - we should not mess with the contents.
+     * 
+     *
+     * @method text
+     * @param {String} text String to write out.
+     * @param {Boolean} raw Optional raw state if true the contents wont get encoded.
+     */
+    text: function(text, node)
+    {
+        // if not in whitespace critical
+        if (text.length < 1) {
+            return;
+        }
+        if (this.in_pre) {
+            this.html[this.html.length] =  text;
+            return;   
+        }
+        
+        if (this.in_inline) {
+            text = text.replace(/\s+/g,' ') // all white space inc line breaks to a slingle' '
+            if (text != ' ') {
+                text = text.replace(/\s+/,' ')  // all white space to single white space
+                
+                    
+                // if next tag is '<BR>', then we can trim right..
+                if (node.nextSibling &&
+                    node.nextSibling.nodeType == 1 &&
+                    node.nextSibling.nodeName == 'BR' )
+                {
+                    text = text.replace(/\s+$/g,'');
                 }
-            },
-            /**
-             * Writes the a end element such as </p>.
-             *
-             * @method end
-             * @param {String} name Name of the element.
-             */
-            end: function(name) {
-                var value;
-                /*if (indent && indentBefore[name] && html.length > 0) {
-                value = html[html.length - 1];
-                if (value.length > 0 && value !== '\n')
-                    html.push('\n');
-            }*/
-                html.push('</', name, '>');
-                if (indent && indentAfter[name] && html.length > 0) {
-                    value = html[html.length - 1];
-                    value.length > 0 && '\n' !== value && html.push('\n');
+                // if previous tag was a BR, we can also trim..
+                if (node.previousSibling &&
+                    node.previousSibling.nodeType == 1 &&
+                    node.previousSibling.nodeName == 'BR' )
+                {
+                    text = this.indentstr +  text.replace(/^\s+/g,'');
                 }
-            },
-            /**
-             * Writes a text node.
-             *
-             * @method text
-             * @param {String} text String to write out.
-             * @param {Boolean} raw Optional raw state if true the contents wont get encoded.
-             */
-            text: function(text, raw) {
-                text.length > 0 && (html[html.length] = raw ? text : encode(text));
-            },
-            /**
-             * Writes a cdata node such as <![CDATA[data]]>.
-             *
-             * @method cdata
-             * @param {String} text String to write out inside the cdata.
-             */
-            cdata: function(text) {
-                html.push('<![CDATA[', text, ']]>');
-            },
-            /**
-             * Writes a comment node such as <!-- Comment -->.
-             *
-             * @method cdata
-             * @param {String} text String to write out inside the comment.
-             */
-            comment: function(text) {
-                html.push('<!--', text, '-->');
-            },
-            /**
-             * Writes a PI node such as <?xml attr="value" ?>.
-             *
-             * @method pi
-             * @param {String} name Name of the pi.
-             * @param {String} text String to write out inside the pi.
-             */
-            pi: function(name, text) {
-                text ? html.push('<?', name, ' ', encode(text), '?>') : html.push('<?', name, '?>');
-                indent && html.push('\n');
-            },
-            /**
-             * Writes a doctype node such as <!DOCTYPE data>.
-             *
-             * @method doctype
-             * @param {String} text String to write out inside the doctype.
-             */
-            doctype: function(text) {
-                html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
-            },
-            /**
-             * Resets the internal buffer if one wants to reuse the writer.
-             *
-             * @method reset
-             */
-            reset: function() {
-                html.length = 0;
-            },
-            /**
-             * Returns the contents that got serialized.
-             *
-             * @method getContent
-             * @return {String} HTML contents that got written down.
-             */
-            getContent: function() {
-                return html.join('').replace(/\n$/, '');
+                if (text.match(/\n/)) {
+                    text = text.replace(
+                        /(?![^\n]{1,64}$)([^\n]{1,64})\s/g, '$1\n' + this.indentstr
+                    );
+                    // remoeve the last whitespace / line break.
+                    text = text.replace(/\n\s+$/,'');
+                }
+                // repace long lines
+                
             }
+             
+            this.html[this.html.length] =  text;
+            return;   
+        }
+        // see if previous element was a inline element.
+        var indentstr = this.indentstr;
+   
+        text = text.replace(/\s+/g," "); // all whitespace into single white space.
+        
+        // should trim left?
+        if (node.previousSibling &&
+            node.previousSibling.nodeType == 1 &&
+            Roo.htmleditor.TidyWriter.inline_elements.indexOf(node.previousSibling.nodeName) > -1)
+        {
+            indentstr = '';
+            
+        } else {
+            this.addLine();
+            text = text.replace(/^\s+/,''); // trim left
+          
+        }
+        // should trim right?
+        if (node.nextSibling &&
+            node.nextSibling.nodeType == 1 &&
+            Roo.htmleditor.TidyWriter.inline_elements.indexOf(node.nextSibling.nodeName) > -1)
+        {
+          // noop
+            
+        }  else {
+            text = text.replace(/\s+$/,''); // trim right
+        }
+         
+              
+        
+        
+        
+        if (text.length < 1) {
+            return;
+        }
+        if (!text.match(/\n/)) {
+            this.html.push(indentstr + text);
+            return;
+        }
+        
+        text = this.indentstr + text.replace(
+            /(?![^\n]{1,64}$)([^\n]{1,64})\s/g, '$1\n' + this.indentstr
+        );
+        // remoeve the last whitespace / line break.
+        text = text.replace(/\s+$/,''); 
+        
+        this.html.push(text);
+        
+        // split and indent..
+        
+        
+    },
+    /**
+     * Writes a cdata node such as <![CDATA[data]]>.
+     *
+     * @method cdata
+     * @param {String} text String to write out inside the cdata.
+     */
+    cdata: function(text) {
+        this.html.push('<![CDATA[', text, ']]>');
+    },
+    /**
+    * Writes a comment node such as <!-- Comment -->.
+    *
+    * @method cdata
+    * @param {String} text String to write out inside the comment.
+    */
+   comment: function(text) {
+       this.html.push('<!--', text, '-->');
+   },
+    /**
+     * Writes a PI node such as <?xml attr="value" ?>.
+     *
+     * @method pi
+     * @param {String} name Name of the pi.
+     * @param {String} text String to write out inside the pi.
+     */
+    pi: function(name, text) {
+        text ? this.html.push('<?', name, ' ', this.encode(text), '?>') : this.html.push('<?', name, '?>');
+        this.indent != '' && this.html.push('\n');
+    },
+    /**
+     * Writes a doctype node such as <!DOCTYPE data>.
+     *
+     * @method doctype
+     * @param {String} text String to write out inside the doctype.
+     */
+    doctype: function(text) {
+        this.html.push('<!DOCTYPE', text, '>', this.indent != '' ? '\n' : '');
+    },
+    /**
+     * Resets the internal buffer if one wants to reuse the writer.
+     *
+     * @method reset
+     */
+    reset: function() {
+        this.html.length = 0;
+        this.state = [];
+        this.pushState({
+            indentstr : '',
+            in_pre : false, 
+            in_inline : false
+        })
+    },
+    /**
+     * Returns the contents that got serialized.
+     *
+     * @method getContent
+     * @return {String} HTML contents that got written down.
+     */
+    getContent: function() {
+        return this.html.join('').replace(/\n$/, '');
+    },
+    
+    pushState : function(cfg)
+    {
+        this.state.push(cfg);
+        Roo.apply(this, cfg);
+    },
+    
+    popState : function()
+    {
+        if (this.state.length < 1) {
+            return; // nothing to push
+        }
+        var cfg = {
+            in_pre: false,
+            indentstr : ''
         };
-    };
-});
\ No newline at end of file
+        this.state.pop();
+        if (this.state.length > 0) {
+            cfg = this.state[this.state.length-1]; 
+        }
+        Roo.apply(this, cfg);
+    },
+    
+    addLine: function()
+    {
+        if (this.html.length < 1) {
+            return;
+        }
+        
+        
+        var value = this.html[this.html.length - 1];
+        if (value.length > 0 && '\n' !== value) {
+            this.html.push('\n');
+        }
+    }
+    
+    
+//'pre script noscript style textarea video audio iframe object code'
+// shortended... 'area base basefont br col frame hr img input isindex link  meta param embed source wbr track');
+// inline 
+};
+
+Roo.htmleditor.TidyWriter.inline_elements = [
+        'SPAN','STRONG','B','EM','I','FONT','STRIKE','U','VAR',
+        'CITE','DFN','CODE','MARK','Q','SUP','SUB','SAMP'
+];
+Roo.htmleditor.TidyWriter.shortend_elements = [
+    'AREA','BASE','BASEFONT','BR','COL','FRAME','HR','IMG','INPUT',
+    'ISINDEX','LINK','','META','PARAM','EMBED','SOURCE','WBR','TRACK'
+];
+
+Roo.htmleditor.TidyWriter.whitespace_elements = [
+    'PRE','SCRIPT','NOSCRIPT','STYLE','TEXTAREA','VIDEO','AUDIO','IFRAME','OBJECT','CODE'
+];
\ No newline at end of file