Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Removing White Chars from ASP.NET Output using Response.Filter property

0.00/5 (No votes)
7 Feb 2005 1  
This article shows how to remove unwanted white chars from each line, and some of the new line chars too. My code correctly handles scripts embedded in HTML pages.

Introduction

This article demonstrates how to remove unwanted white characters from ASP.NET output. I present how to use HttpResponse.Filter property to achieve this effect.

Background

During my work on an ASP.NET site, I realized that pages sent to the browser by the server contained many white characters at the beginning of each line. I browsed MSDN for something to fix this, and I found HttpResponse.Filter property. HttpResponse works in an easy way - all ASP.NET output goes through it. So my task looked easy - create my own Stream implementation and trim each written line in Write() function.

First version

First version was very simple:

public class TrimStream : Stream
{
    private Stream stream;
    private StreamWriter streamWriter;
    
    public TrimStream(Stream stm)
    {
        stream = stm;
        streamWriter = new StreamWriter(stream, System.Text.Encoding.UTF8);
    }
    
    public override void Write(byte[] buffer, int offset, int count)
    {
        MemoryStream ms = new MemoryStream(buffer, offset, count, false);
        StreamReader sr = new StreamReader(ms, System.Text.Encoding.UTF8);
        bool bNewLine = false;
        string s;
        while ((s = sr.ReadLine()) != null)
        {
            s = s.Trim();
            if (s != "")
            {
                if (bNewLine)
                {
                    streamWriter.WriteLine();
                    bNewLine = false;
                }

                streamWriter.Write(s); 

                //new line chars should be emitted 

                //only when line doesn't end with '>'

                if (s[s.Length-1] != '>')
                    bNewLine = true;
            }
        }
        streamWriter.Flush();
    }
    
    /* other overrided Stream functions and properties goes here */
}

This code reads lines from the input stream, trims them, and writes to output stream only if the line is not empty. New line characters were written only when a line doesn't end with '>' char.

Unfortunately, I quickly realized that larger pages (> 30K or something like that) are written to output stream by multiple calls to Write() - on one of my pages, I found a text input box instead of a checkbox (my code replaced <input type="checkbox"> with <input type="\ncheckbox">). So I decided to completely rewrite my code, and manually handle each character in order to correctly handle this situation.

Current version

In the current version, I read all characters from the input stream, and manually analyze them line-by-line. Special handling is needed for the first and the last line:

  • on last line, I save white chars from the end of the line (they may be important if my "last line" ends somewhere in the middle of a real line);
  • on first line, I write saved white chars if they are really needed.

I also changed the logic that controls when a new line character is written - now it's emitted when neither previous line ends with '>' nor next line begins with '<'. I check this after trimming white chars and removing empty lines.

This approach is safe for scripts embedded in a page - their lines are only trimmed, and empty lines are removed. If you insert this script in an HTML page:

<script language="javascript">
<!--
function test()
{
    alert('test');
}
//-->

</script>

My code changes it as follows:

<script language="javascript"><!--
function test()
{
alert('test');
}
//--></script>

Code description

bNewLine and bLastCharGT variables are used for deciding when to write a new line character before the next line. In arBlanks array are stored white chars between calls to Write(), if any was found on end of last line. This array can be zero-sized too - in this case, last char in line was non-blank, and that doesn't end with '\n'. This is important information - in this case, a new line char shouldn't be emitted if in first line (on next call to Write are non-blank chars).

This code analyzes chars looking for '\n' char (it splits lines). When this char is found, you can find the following situations:

  • first line - if arBlanks is not null, its contents are written, and all white chars to the beginning of the current line should be written to the output stream too;
  • any line with non-blank chars - this code writes it (excluding white chars at the begin and end of it);
  • last line - if last char on this line is other than '\n', all white chars from end of this line should be saved to arBlanks array for next call to Write().
public class TrimStream : Stream
{
    private Stream stream;
    private StreamWriter streamWriter;

    private Decoder dec;

    public TrimStream(Stream stm)
    {
        stream = stm;
        streamWriter = new StreamWriter(stream, System.Text.Encoding.UTF8);

        dec = Encoding.UTF8.GetDecoder();
    }

    /// <summary>

    /// Flag - write '\n' before next line

    /// </summary>

    private bool bNewLine = false;
    /// <summary>

    /// Flag - lash non-blank char in line was '>'

    /// </summary>

    private bool bLastCharGT = false;
    /// <summary>

    /// Array holding white chars from end of last line between Write() calls

    /// </summary>

    private char[] arBlanks = null;

    public override void Write(byte[] buffer, int offset, int count)
    {
        int nCharCnt = dec.GetCharCount(buffer, offset, count);
        char[] result = new char[nCharCnt];
        int nDecoded = dec.GetChars(buffer, offset, count, result, 0);
        
        if (nDecoded <= 0)
            return;

        int nFirstNonBlank = -1; //position of first non-black line char

        int nLastNonBlank = -1; //position of last non-black line char

        int nFirstLineChar = 0; //position of first line char (any)


        bool bFirstLine = true;

        for (int nPos=0; nPos<=nDecoded; ++nPos)
        {
            bool bLastLine = nPos == nDecoded;

            char c = (nPos < nDecoded) ? result[nPos] : '\n';
            if (c == '\n') //handle new line

            {
                //first line, and we have important 

                //white chars from previous Write() call

                if (bFirstLine && (arBlanks != null))
                {
                    //current line contains non-blank chars 

                    //- write white chars from previous call

                    if (nFirstNonBlank >=0)
                    {
                        if (arBlanks.Length > 0)
                            streamWriter.Write(arBlanks, 0, arBlanks.Length);

                        arBlanks = null;
                        nFirstNonBlank = 0;
                        bNewLine = false;
                    }
                }
                bFirstLine = false;

                //current line contains any non-white chars - write them

                if (nFirstNonBlank >= 0)
                {
                    if (bNewLine && (result[nFirstNonBlank] != '<'))
                    //write new line char ?

                        streamWriter.WriteLine();

                    //write current line (trimmed)

                    streamWriter.Write(result, nFirstNonBlank, 
                          nLastNonBlank - nFirstNonBlank + 1);

                    //setting variables...

                    if (!bLastLine)
                    {
                        nFirstNonBlank = -1;
                        nLastNonBlank = -1;
                        nFirstLineChar = nPos + 1;
                    }
                    bNewLine = !bLastCharGT;
                    bLastCharGT = false;
                }

                if (bLastLine)
                //last line - optionally remember white chars from its end

                {
                    if ((arBlanks == null) && (nFirstNonBlank < 0))
                    {
                        //empty line and we don't have any 

                        //white chars from previous call - nothing to do

                    }
                    else if (nLastNonBlank < nDecoded-1)
                    //there was white chars at end of this line

                    {
                        int nNumBlanks, nFirstBlank;
                        if (nLastNonBlank < 0)
                        {
                            nNumBlanks = nDecoded - nFirstLineChar;
                            nFirstBlank = nFirstLineChar;
                        }
                        else
                        {
                            nNumBlanks = nDecoded - nLastNonBlank - 1;
                            nFirstBlank = nLastNonBlank + 1;
                        }

                        if ((arBlanks == null) || (arBlanks.Length <= 0))
                        //create new array?

                        {
                            arBlanks = new char[nNumBlanks];
                            Array.Copy(result, nFirstBlank, 
                                       arBlanks, 0, nNumBlanks);
                        }
                        else //append at end of existsing array

                        {
                            char[] ar = new char[arBlanks.Length + nNumBlanks];
                            arBlanks.CopyTo(ar, 0);
                            Array.Copy(result, nFirstBlank, ar, 
                                       arBlanks.Length, nNumBlanks);
                            arBlanks = ar;
                        }
                    }
                    else
                    //line without white-chars at end 

                    //- mark this using zero-sized array

                    {
                        arBlanks = new char[0];
                    }

                    //set variable...

                    bNewLine = false;
                }
            }
            else if (!Char.IsWhiteSpace(c)) //handle non-white chars

            {
                if (nFirstNonBlank < 0)
                    nFirstNonBlank = nPos;
                nLastNonBlank = nPos;
                bLastCharGT = (c == '>');
            }
        }

        streamWriter.Flush();
    }

    /* other overrided Stream functions and properties goes here */
}

Using the code

An instance of this class must be created on each request and assigned to the current Request.Filter property. You can do it in almost any event generated by the HttpApplication class. But if you have pages that renders something different than html code (text/html MIME type), you should take care of this. In this case, you should test the current MIME type and create (or not) this filter in HttpApplication.PostRequestHandlerExecute event handler:

private void Global_PostRequestHandlerExecute(object sender, System.EventArgs e)
{
    if (Response.ContentType == "text/html")
        Response.Filter = new TrimStream(Response.Filter);
}

Points of Interest

HttpResponse.Filter property gives us an entry point to connect our output filters to an ASP.NET output stream. This property can be used to connect any combination of filters, e.g., my filter and compression filter:

Response.Filter = new TrimStream(new CompressStream(Response.Filter));

Also, this is an interesting alternative to IHttpModule-based filters.

History

  • 2/7/2005 - First version.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here