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);
if (s[s.Length-1] != '>')
bNewLine = true;
}
}
streamWriter.Flush();
}
}
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');
}
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();
}
private bool bNewLine = false;
private bool bLastCharGT = false;
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;
int nLastNonBlank = -1;
int nFirstLineChar = 0;
bool bFirstLine = true;
for (int nPos=0; nPos<=nDecoded; ++nPos)
{
bool bLastLine = nPos == nDecoded;
char c = (nPos < nDecoded) ? result[nPos] : '\n';
if (c == '\n')
{
if (bFirstLine && (arBlanks != null))
{
if (nFirstNonBlank >=0)
{
if (arBlanks.Length > 0)
streamWriter.Write(arBlanks, 0, arBlanks.Length);
arBlanks = null;
nFirstNonBlank = 0;
bNewLine = false;
}
}
bFirstLine = false;
if (nFirstNonBlank >= 0)
{
if (bNewLine && (result[nFirstNonBlank] != '<'))
streamWriter.WriteLine();
streamWriter.Write(result, nFirstNonBlank,
nLastNonBlank - nFirstNonBlank + 1);
if (!bLastLine)
{
nFirstNonBlank = -1;
nLastNonBlank = -1;
nFirstLineChar = nPos + 1;
}
bNewLine = !bLastCharGT;
bLastCharGT = false;
}
if (bLastLine)
{
if ((arBlanks == null) && (nFirstNonBlank < 0))
{
}
else if (nLastNonBlank < nDecoded-1)
{
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))
{
arBlanks = new char[nNumBlanks];
Array.Copy(result, nFirstBlank,
arBlanks, 0, nNumBlanks);
}
else
{
char[] ar = new char[arBlanks.Length + nNumBlanks];
arBlanks.CopyTo(ar, 0);
Array.Copy(result, nFirstBlank, ar,
arBlanks.Length, nNumBlanks);
arBlanks = ar;
}
}
else
{
arBlanks = new char[0];
}
bNewLine = false;
}
}
else if (!Char.IsWhiteSpace(c))
{
if (nFirstNonBlank < 0)
nFirstNonBlank = nPos;
nLastNonBlank = nPos;
bLastCharGT = (c == '>');
}
}
streamWriter.Flush();
}
}
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.