HTML Tidy als Filter in ASP.NET

Auf der MSDN-Website wurde in 2003-05 ein Add-In vorgestellt, das das bekannte HTML Tidy in Visual Studio .NET integriert. Der Nutzen dieses Ansatzes hält sich meiner Meinung nach in Grenzen, zumal das Add-In nur mit ASCII-Zeichen sicher umgehen kann und Tidy von Web-Controls nicht den „fertigen“ HTML-Code zu sehen bekommt.

Wesentlich sinnvoller erscheint es mir, Tidy den HTML-Code bearbeiten zu lassen, den ein Browser sehen würde. Das kann ein Filter leisten. Insbesondere kann Tidy HTML in XHTML konvertieren.

Filter sind im .NET-Framework von der Klasse System.IO.Stream abgeleitet. Meine Klasse TidyFilter implementiert einen Filter, der einen Byte-Strom an Tidy weiterreicht:

Option Strict On
Option Explicit On 

Imports System.IO

Public Class TidyFilter : Inherits Stream

    Const TIDY_EXE As String = "tidy.exe"
    Const TIDY_CFG As String = "tidy.cfg"

    Private _sink As Stream
    Private _position As Long
    Private mstr As New MemoryStream

#Region "trivial"

    Public Sub New(ByVal sink As Stream)
        _sink = sink
    End Sub

    Public Overrides ReadOnly Property CanRead() As Boolean
        Get
            Return True
        End Get
    End Property

    Public Overrides ReadOnly Property CanSeek() As Boolean
        Get
            Return True
        End Get
    End Property

    Public Overrides ReadOnly Property CanWrite() As Boolean
        Get
            Return True
        End Get
    End Property

    Public Overrides ReadOnly Property Length() As Long
        Get
            Return 0
        End Get
    End Property

    Public Overrides Property Position() As Long
        Get
            Return _position
        End Get
        Set(ByVal Value As Long)
            _position = Value
        End Set
    End Property

    Public Overrides Function Seek(ByVal offset As Long, ByVal direction As System.IO.SeekOrigin) As Long
        Return _sink.Seek(offset, direction)
    End Function

    Public Overrides Sub SetLength(ByVal length As Long)
        _sink.SetLength(length)
    End Sub

    Public Overrides Sub Flush()
        _sink.Flush()
    End Sub

    Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
        _sink.Read(buffer, offset, count)
    End Function

#End Region

    Public Overrides Sub Close()

        Dim path As String = System.Web.HttpContext.Current.Request.PhysicalApplicationPath

        Try

            'Prepare process.
            Dim psi As New System.Diagnostics.ProcessStartInfo
            psi.FileName = path & TIDY_EXE
            psi.Arguments = "-config " & path & TIDY_CFG
            psi.WorkingDirectory = path
            psi.CreateNoWindow = True
            psi.UseShellExecute = False
            psi.RedirectStandardInput = True
            psi.RedirectStandardOutput = True
            psi.RedirectStandardError = False

            'Create process.
            Dim p As New System.Diagnostics.Process
            p.StartInfo = psi

            If p.Start() Then

                'Feed Tidy.
                mstr.WriteTo(p.StandardInput.BaseStream)
                p.StandardInput.Close()

                'Write Tidy output into the original response stream.
                Const BUFFER_SIZE As Integer = 4096
                Dim buffer(BUFFER_SIZE) As Byte
                Dim count As Integer

                Do
                    count = p.StandardOutput.BaseStream.Read(buffer, 0, BUFFER_SIZE)
                    _sink.Write(buffer, 0, count)
                Loop Until count = 0
                p.StandardOutput.Close()

            Else
                Throw New Exception
            End If

        Catch ex As Exception

            'Something went wrong, output original source.
            mstr.WriteTo(_sink)

        Finally
            _sink.Close()
        End Try

    End Sub

    Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
        mstr.Write(buffer, offset, count)
    End Sub

End Class

Tidy installieren

Sie benötigen eine ausführbare .exe-Datei sowie eine Konfigurationsdatei tidy.cfg. Die Konfigurationsoptionen sind ausführlich dokumentiert, meine eigene Konfigurationsdatei sieht derzeit so aus:

input-xml: no
output-xhtml: yes
doctype: strict
add-xml-decl: no
output-bom: no
char-encoding: utf8
markup: yes
wrap: 0
numeric-entities: yes
quote-nbsp: no
newline: CRLF
force-output: yes
tidy-mark: no

tidy.exe muß in einem Verzeichnis abgelegt werden, das Ausführberechtigungen für ausführbare Dateien besitzt. Möglicherweise müssen Sie die Datei deshalb ins cgi-bin-Verzeichnis verschieben. Den Pfad zu den beiden Dateien müssen Sie relativ zum Stammverzeichnis Ihrer ASP.NET-Applikation angeben:

Const TIDY_EXE As String = "cgi-bin\tidy.exe"
Const TIDY_CFG As String = "tidy.cfg"

Ich verwende hier die „klassische“ .exe-Datei. Es gibt zwar einen einen COM-Wrapper für TidyLib, das COM-Objekt müßte jedoch auf dem Server registriert werden, was die Portabilität einschränken würde. Die Performance ist dennoch zufriedenstellend.

Kompilieren des Quellcodes

Kompilieren in Visual Studio .NET

Erstellen Sie einfach eine neue Klasse und fügen Sie den obigen Quellcode ein.

Kompilieren mit vbc.exe

Der Quellcode wird mit folgendem Befehl kompiliert:

vbc.exe /target:library /r:System.dll,System.Web.dll /imports:System TidyFilter.vb

Dieser Aufruf erzeugt eine Datei TidyFilter.dll. Diese müssen Sie in das \bin-Verzeichnis Ihrer ASP.NET-Applikation kopieren. Um den Filter zu verwenden, müssen Sie die Assembly in der .aspx-Datei referenzieren:

<%@ Assembly Name="TidyFilter" %>

Filter aktivieren

Der Filter kann bspw. in der OnInit-Methode einer Seite aufgerufen werden:

Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
    Response.Filter = new TidyFilter(Response.Filter)
End Sub

Der Filter arbeitet mit Byte-Streams, nicht mit Strings. Sie müssen also dafür sorgen, daß ASP.NET die Zeichencodierung verwendet, die Tidy erwartet. Schreiben Sie bspw. <globalization responseEncoding="utf-8" /> in der web.config und char-encoding: utf8 in der tidy.cfg. Tidy unterstützt neben UTF-8 einige andere Codierungen, aber es gibt eigentlich keinen Grund, nicht UTF-8 zu verwenden.