Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

a Tiny Parser Generator v1.2

, 21 Sep 2010 CPOL
@TinyPG is a utility that makes it easier to write and try out your own parser/compiler.
TinyPG_v1.0_binaries.zip
Release
Examples
BNFGrammar v1.0.tpg
simple expression1.tpg
simple expression2.tpg
Templates
TinyPG.exe
TinyPG_v1.0_sources.zip
TinyPG v1.0
bin
Debug
Release
TinyPG.vshost.exe
TinyPG.vshost.exe.manifest
Compiler
backup
vssver2.scc
vssver2.scc
Controls
DockExtender
vssver2.scc
vssver2.scc
Debug
vssver2.scc
Examples
BNFGrammar v0.2.tpg
BNFGrammar v1.0.tpg
simple expression1.tpg
simple expression2.tpg
test.tpg
vssver2.scc
mssccprj.scc
obj
Debug
TempPE
Release
TempPE
Properties.Resources.Designer.cs.dll
Properties
vssver2.scc
Templates
vssver2.scc
TinyPG.csproj.user
TinyPG.suo
vssver2.scc
TinyPG_v1.1_sources.zip
TinyPG v1.1
bin
Release
Examples
BNFGrammar v1.0.tpg
BNFGrammar v1.1.tpg
simple expression1.tpg
simple expression2.tpg
Templates
TinyPG.exe
Compiler
vssver2.scc
Controls
DockExtender
vssver2.scc
vssver2.scc
Debug
vssver2.scc
Examples
BNFGrammar v0.2.tpg
BNFGrammar v1.0.tpg
BNFGrammar v1.1.tpg
simple expression1.tpg
simple expression2.tpg
vssver2.scc
Highlighter
vssver2.scc
mssccprj.scc
Properties
vssver2.scc
Templates
vssver2.scc
TinyPG.csproj.user
TinyPG.suo
vssver2.scc
TinyPG_v1.2_binaries.zip
BNFGrammar v1.0.tpg
BNFGrammar v1.1.tpg
GrammarHighlighter.tpg
simple expression1.tpg
simple expression2.tpg
C#
VB
TinyPG.exe
TinyPG_v1.2_sources.zip
TinyPG v1.2
bin
Release
Examples
BNFGrammar v1.0.tpg
BNFGrammar v1.1.tpg
GrammarHighlighter.tpg
simple expression1.tpg
simple expression2.tpg
Templates
C#
VB
TinyPG.exe
CodeGenerators
CSharp
vssver2.scc
VBNet
vssver2.scc
vssver2.scc
Compiler
BNFGrammar.tpg
vssver2.scc
Controls
DockExtender
vssver2.scc
vssver2.scc
Debug
vssver2.scc
Examples
BNFGrammar v0.2.tpg
BNFGrammar v1.0.tpg
BNFGrammar v1.1.tpg
BNFGrammar_vb v1.1.tpg
GrammarHighlighter.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression1_vb.tpg
simple expression2.tpg
simple expression2_vb.tpg
vssver2.scc
Highlighter
GrammarHighlighter v1.2.tpg
vssver2.scc
mssccprj.scc
Properties
vssver2.scc
Templates
C#
vssver2.scc
VB
vssver2.scc
vssver2.scc
TinyPG.csproj.user
TinyPG.suo
vssver2.scc
TinyPG_v1_3_binaries.zip
BNFGrammar 1.3.tpg
BNFGrammar_vb 1.3.tpg
GrammarHighlighter v1.3.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression2.tpg
TinyPG.exe
TinyPG.vshost.exe
TinyPG.vshost.exe.manifest
TinyPG_v1_3_sources.zip
TinyPG v1.3
TinyPG
bin
Debug
Examples
BNFGrammar 1.3.tpg
BNFGrammar v1.0.tpg
BNFGrammar v1.1.tpg
BNFGrammar_vb 1.3.tpg
GrammarHighlighter v1.3.tpg
GrammarHighlighter.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression2.tpg
Templates
C#
VB
TinyPG.exe
TinyPG.pdb
TinyPG.vshost.exe
Release
Examples
BNFGrammar 1.3.tpg
BNFGrammar_vb 1.3.tpg
GrammarHighlighter v1.3.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression2.tpg
Templates
C#
VB
TinyPG.exe
TinyPG.pdb
TinyPG.vshost.exe
TinyPG.vshost.exe.manifest
CodeGenerators
CSharp
vssver2.scc
VBNet
vssver2.scc
vssver2.scc
Compiler
BNFGrammar 1.3.tpg
vssver2.scc
Controls
DockExtender
vssver2.scc
vssver2.scc
Debug
vssver2.scc
Examples
BNFGrammar 1.3.tpg
BNFGrammar_vb 1.3.tpg
csharpstruct.tpg
GrammarHighlighter v1.3.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression1_vb.tpg
simple expression2.tpg
simple expression2_vb.tpg
vssver2.scc
Highlighter
GrammarHighlighter v1.3.tpg
vssver2.scc
mssccprj.scc
obj
Debug
TempPE
Properties.Resources.Designer.cs.dll
TinyPG.Controls.AutoComplete.resources
TinyPG.Controls.RegExControl.resources
TinyPG.csproj.GenerateResource.Cache
TinyPG.exe
TinyPG.MainForm.resources
TinyPG.pdb
TinyPG.Properties.Resources.resources
Release
TempPE
Properties.Resources.Designer.cs.dll
TinyPG.Controls.AutoComplete.resources
TinyPG.Controls.RegExControl.resources
TinyPG.csproj.GenerateResource.Cache
TinyPG.exe
TinyPG.MainForm.resources
TinyPG.pdb
TinyPG.Properties.Resources.resources
Properties
vssver2.scc
Templates
C#
vssver2.scc
VB
vssver2.scc
vssver2.scc
TinyPG.csproj.user
TinyPG.suo
vssver2.scc
TinyPG.UnitTests
bin
Release
Examples
BNFGrammar 1.3.tpg
BNFGrammar v1.0.tpg
BNFGrammar v1.1.tpg
BNFGrammar_vb 1.3.tpg
GrammarHighlighter v1.3.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression2.tpg
Templates
C#
VB
TinyPG.exe
TinyPG.UnitTests.dll
localtestrun.testrunconfig
mssccprj.scc
Properties
Testfiles
BNFGrammar v1.1.tpg
BNFGrammar_vb v1.1.tpg
GrammarHighlighter.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression1_vb.tpg
simple expression2.tpg
simple expression2_vb.tpg
vssver2.scc
Testfiles
BNFGrammar v1.1.tpg
BNFGrammar_vb v1.1.tpg
GrammarHighlighter.tpg
GrammarHighlighter_vb.tpg
simple expression1.tpg
simple expression1_vb.tpg
simple expression2.tpg
simple expression2_vb.tpg
vssver2.scc
TestResults
TinyPG.testrunconfig
TinyPG.UnitTests.csproj.user
vssver2.scc
' Generated by TinyPG v1.3 available at www.codeproject.com

Imports Microsoft.VisualBasic
Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
Imports System.Drawing
Imports System.Threading

Namespace <%Namespace%>
    ' Summary:
    '     System.EventArgs is the base class for classes containing event data.
    <Serializable()> _
    <ComVisible(True)> _
    Public Class ContextSwitchEventArgs
        Inherits EventArgs
        Public ReadOnly PreviousContext As ParseNode
        Public ReadOnly NewContext As ParseNode

        ' Summary:
        '     Initializes a new instance of the System.EventArgs class.
        Public Sub New(ByVal prevContext As ParseNode, ByVal nextContext As ParseNode)
            PreviousContext = prevContext
            NewContext = nextContext
        End Sub
    End Class

    ' delegate for firing context switch events
    Public Delegate Sub ContextSwitchEventHandler(ByVal sender As Object, ByVal e As ContextSwitchEventArgs)

    ''' <summary>
    ''' Takes control over the RichTextBox and will color the text accoording to the rules of the parser and the scanner
    ''' this control extender will also support Undo/Redo functionality.
    ''' </summary>
    Public Class TextHighlighter
        Implements IDisposable
        Private Class UndoItem
            ''' <summary>
            ''' contains the information for an undo/redo action
            ''' </summary>
            ''' <param name="text">the full text to be undone/redone</param>
            ''' <param name="position">position of the caret after the un/redo action</param>
            ''' <param name="scroll">position of the scrollbars after un/redo action</param>
            Public Sub New(ByVal text As String, ByVal position As Integer, ByVal scroll As Point)
                Me.Text = text
                Me.Position = position
                Me.ScrollPosition = scroll
            End Sub

            Public Text As String
            Public Position As Integer
            Public ScrollPosition As Point
        End Class


        ' some winapís required
        <DllImport("user32", CharSet:=CharSet.Auto)> _
        Private Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, ByVal lParam As IntPtr) As IntPtr
        End Function

        <DllImport("user32.dll")> _
        Private Shared Function PostMessageA(ByVal hWnd As IntPtr, ByVal nBar As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Boolean
        End Function

        <DllImport("user32.dll", CharSet:=CharSet.Auto)> _
        Private Shared Function GetScrollPos(ByVal hWnd As Integer, ByVal nBar As Integer) As Integer
        End Function

        <DllImport("user32.dll")> _
        Private Shared Function SetScrollPos(ByVal hWnd As IntPtr, ByVal nBar As Integer, ByVal nPos As Integer, ByVal bRedraw As Boolean) As Integer
        End Function

        Private Const WM_SETREDRAW As Integer = 11
        Private Const WM_USER As Integer = 1024
        Private Const EM_GETEVENTMASK As Integer = (WM_USER + 59)
        Private Const EM_SETEVENTMASK As Integer = (WM_USER + 69)
        Private Const SB_HORZ As Integer = 0
        Private Const SB_VERT As Integer = 1
        Private Const WM_HSCROLL As Integer = 276
        Private Const WM_VSCROLL As Integer = 277
        Private Const SB_THUMBPOSITION As Integer = 4
        Private Const UNDO_BUFFER As Integer = 100

        Private Property HScrollPos() As Integer
            Get
                Return GetScrollPos(Textbox.Handle.ToInt32(), SB_HORZ)
            End Get
            Set(ByVal value As Integer)
                SetScrollPos(DirectCast(Textbox.Handle, IntPtr), SB_HORZ, value, True)
                PostMessageA(DirectCast(Textbox.Handle, IntPtr), WM_HSCROLL, SB_THUMBPOSITION + 65536 * value, 0)
            End Set
        End Property

        Private Property VScrollPos() As Integer
            Get
                Return GetScrollPos(Textbox.Handle.ToInt32(), SB_VERT)
            End Get
            Set(ByVal value As Integer)
                SetScrollPos(DirectCast(Textbox.Handle, IntPtr), SB_VERT, value, True)
                PostMessageA(DirectCast(Textbox.Handle, IntPtr), WM_VSCROLL, SB_THUMBPOSITION + 65536 * value, 0)
            End Set
        End Property

        ' public shared members
        Public Tree As ParseTree
        Public ReadOnly Textbox As RichTextBox

        ' private members
        Private Parser As Parser
        Private Scanner As Scanner
        Private stateLocked As IntPtr = IntPtr.Zero

        Private UndoIndex As Integer = -1
        Private UndoList As List(Of UndoItem)

        Private currentContext As ParseNode
        Public Event SwitchContext As ContextSwitchEventHandler

        Private threadAutoHighlight As Thread


        Private Sub DoAction(ByVal text As String, ByVal position As Integer)

            If stateLocked <> IntPtr.Zero Then
                Return
            End If

            Dim ua As New UndoItem(text, position, New Point(HScrollPos, VScrollPos))
            UndoList.RemoveRange(UndoIndex, UndoList.Count - UndoIndex)
            UndoList.Add(ua)
            If UndoList.Count > UNDO_BUFFER Then
                UndoList.RemoveAt(0)
            End If

            ' make undo/redo a little smarter, remove single strokes
            ' reducing nr of undo states
            If UndoList.Count > 7 Then
                Dim canRemove As Boolean = True
                Dim nextItem As UndoItem = ua
                Dim i As Integer = 0
                While i < 6
    Dim prevItem As UndoItem = UndoList(UndoList.Count - 2 - i)
                    canRemove = canRemove And (Math.Abs(prevItem.Text.Length - nextItem.Text.Length) <= 1 AndAlso Math.Abs(prevItem.Position - nextItem.Position) <= 1)
                    nextItem = prevItem
                    System.Math.Max(System.Threading.Interlocked.Increment(i), i - 1)
                End While
                If canRemove Then
                    UndoList.RemoveRange(UndoList.Count - 6, 5)
                End If
            End If
            UndoIndex = UndoList.Count
        End Sub

    Public Sub ClearUndo()
        UndoList = New List(Of UndoItem)()
        UndoIndex = 0
    End Sub

    Public Sub Undo()
        If Not CanUndo Then
            Return
        End If

        System.Math.Max(System.Threading.Interlocked.Decrement(UndoIndex), UndoIndex + 1)
        If UndoIndex < 1 Then
            UndoIndex = 1
        End If

        ' implement undo action here
        Dim ua As UndoItem = UndoList(UndoIndex - 1)
        RestoreState(ua)
    End Sub

    Public Sub Redo()
        If Not CanRedo Then
            Return
        End If

        System.Math.Max(System.Threading.Interlocked.Increment(UndoIndex), UndoIndex - 1)
        If UndoIndex > UndoList.Count Then
            UndoIndex = UndoList.Count
        End If

        Dim ua As UndoItem = UndoList(UndoIndex - 1)
        RestoreState(ua)

    End Sub

    Private Sub RestoreState(ByVal item As UndoItem)
        Lock()
        ' restore state
        Textbox.Rtf = item.Text
        Textbox.[Select](item.Position, 0)
        HScrollPos = item.ScrollPosition.X
        VScrollPos = item.ScrollPosition.Y

        Unlock()
    End Sub

    Public ReadOnly Property CanUndo() As Boolean
        Get
            Return UndoIndex > 0
        End Get
    End Property

    Public ReadOnly Property CanRedo() As Boolean
        Get
            Return UndoIndex < UndoList.Count
        End Get
    End Property

    Public Sub New(ByVal textbox As RichTextBox, ByVal scanner As Scanner, ByVal parser As Parser)
        Me.Textbox = textbox
        Me.Scanner = scanner
        Me.Parser = parser

        ClearUndo()

        AddHandler Textbox.TextChanged, AddressOf Textbox_TextChanged
        AddHandler textbox.KeyDown, AddressOf textbox_KeyDown
        AddHandler Textbox.SelectionChanged, AddressOf Textbox_SelectionChanged
        AddHandler Textbox.Disposed, AddressOf Textbox_Disposed

        Tree = New ParseTree()
        currentContext = Tree

        threadAutoHighlight = New Thread(AddressOf AutoHighlightStart)
        threadAutoHighlight.Start()
    End Sub


    Public Sub Lock()
        ' Stop redrawing:  
        SendMessage(Textbox.Handle, WM_SETREDRAW, 0, IntPtr.Zero)
        ' Stop sending of events:  
        stateLocked = SendMessage(Textbox.Handle, EM_GETEVENTMASK, 0, IntPtr.Zero)
        ' change colors and stuff in the RichTextBox  
    End Sub

    Public Sub Unlock()
        ' turn on events  
        SendMessage(Textbox.Handle, EM_SETEVENTMASK, 0, stateLocked)
        ' turn on redrawing  
        SendMessage(Textbox.Handle, WM_SETREDRAW, 1, IntPtr.Zero)

        stateLocked = IntPtr.Zero
        Textbox.Invalidate()
    End Sub

    Sub textbox_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
        ' undo/redo
        If e.KeyValue = 89 AndAlso e.Control Then
            Redo()
            ' CTRL-Y
        End If
        If e.KeyValue = 90 AndAlso e.Control Then
            Undo()
            ' CTRL-Z
        End If
    End Sub

    Sub Textbox_TextChanged(ByVal sender As Object, ByVal e As EventArgs)
        If stateLocked <> IntPtr.Zero Then
            Return
        End If

        DoAction(Textbox.Rtf, Textbox.SelectionStart)

        HighlightText()
    End Sub

    Sub Textbox_SelectionChanged(ByVal sender As Object, ByVal e As EventArgs)
        If stateLocked <> IntPtr.Zero Then
            Return
        End If

        Dim newContext As ParseNode = GetCurrentContext()

        If currentContext Is Nothing Then
            currentContext = newContext
        End If
        If newContext Is Nothing Then
            Return
        End If

        If newContext.Token.Type <> currentContext.Token.Type Then
            RaiseEvent SwitchContext(Me, New ContextSwitchEventArgs(currentContext, newContext))
            'SwitchContext.Invoke(Me, New ContextSwitchEventArgs(currentContext, newContext))
            currentContext = newContext
        End If

    End Sub

    ''' <summary>
    ''' this handy function returns the section in which the user is editing currently
    ''' </summary>
    ''' <returns></returns>
    Public Function GetCurrentContext() As ParseNode
        Dim node As ParseNode = FindNode(Tree, Textbox.SelectionStart)
        Return node
    End Function

    Private Function FindNode(ByVal node As ParseNode, ByVal posstart As Integer) As ParseNode

        If node Is Nothing Then
            Return Nothing
        End If

        If node.Token.StartPos <= posstart AndAlso (node.Token.StartPos + node.Token.Length) >= posstart Then
            For Each n As ParseNode In node.Nodes
                If n.Token.StartPos <= posstart AndAlso (n.Token.StartPos + n.Token.Length) >= posstart Then
                    Return FindNode(n, posstart)
                End If
            Next
            Return node
        Else
            Return Nothing
        End If
    End Function

    ''' <summary>
    ''' use HighlighText to start the text highlight process from the caller's thread.
    ''' this method is not used internally. 
    ''' </summary>
    Public Sub HighlightText()
        SyncLock treelock
            textChanged = True
            currentText = Textbox.Text
        End SyncLock
    End Sub

    Private Sub HighlightTextInternal()
        ' highlight the text (used internally only)
        Lock()

        Dim hscroll As Integer = HScrollPos
        Dim vscroll As Integer = VScrollPos

        Dim selstart As Integer = Textbox.SelectionStart

        HighlighTextCore()

        Textbox.[Select](selstart, 0)

        HScrollPos = hscroll
        VScrollPos = vscroll

        Unlock()
    End Sub

    ''' <summary>
    ''' this method should be used only by HighlightText or RestoreState methods
    ''' </summary>
    Private Sub HighlighTextCore()
        'Tree = Parser.Parse(Textbox.Text);
        Dim sb As New StringBuilder()
        If Tree Is Nothing Then
            Return
        End If

        Dim start As ParseNode = Tree.Nodes(0)
        HightlightNode(start, sb)

        ' append any trailing skipped tokens that were scanned
        For Each skiptoken As Token In Scanner.Skipped
            HighlightToken(skiptoken, sb)
            sb.Append(skiptoken.Text.Replace("\", "\\").Replace("{", "\{").Replace("}", "\}").Replace(vbLf, "\par" & vbLf))
        Next

        sb = Unicode(sb)     ' <--- without this, unicode characters will be garbled after highlighting

        AddRtfHeader(sb)
        AddRtfEnd(sb)

        Textbox.Rtf = sb.ToString()

    End Sub


    ''' <summary>
    ''' added function to convert unicode characters in the stringbuilder to rtf unicode escapes
    ''' </summary>
    Function Unicode(ByVal sb As StringBuilder) As StringBuilder

        Dim i As Integer
        Dim uc As StringBuilder = New StringBuilder
        For i = 0 To sb.Length - 1
            Dim c As Char = sb(i)
            If AscW(c) < 127 Then
                uc.Append(c.ToString())
            Else
                uc.Append("\u" & CStr(AscW(c)) + "?")
            End If
        Next

        Return uc

    End Function

    ' thread start for the automatic highlighting
    Private Shared treelock As New Object()
    Private isDisposing As Boolean
    Private textChanged As Boolean
    Private currentText As String

    Private Sub AutoHighlightStart()
        Dim _tree As ParseTree
        Dim _currenttext As String = ""
        While Not isDisposing
            Dim _textchanged As Boolean
            SyncLock treelock
                _textchanged = textChanged
                If textChanged Then
                    textChanged = False
                    _currenttext = currentText
                End If
            End SyncLock
            If Not _textchanged Then
                Thread.Sleep(200)
                Continue While
            End If

            _tree = DirectCast(Parser.Parse(_currenttext), ParseTree)

            SyncLock treelock
                If textChanged Then
                    Continue While
                Else
                    ' assign new tree
                    Tree = _tree
                End If
            End SyncLock


            Textbox.Invoke(New MethodInvoker(AddressOf HighlightTextInternal))
        End While
    End Sub


    ''' <summary>
    ''' inserts the RTF codes to highlight text blocks
    ''' </summary>
    ''' <param name="node">the node to highlight, will be appended to sb</param>
    ''' <param name="sb">the final output string</param>
    Private Sub HightlightNode(ByVal node As ParseNode, ByVal sb As StringBuilder)
        If node.Nodes.Count = 0 Then
            If (node.Token.Skipped IsNot Nothing) Then
                For Each skiptoken As Token In node.Token.Skipped
                    HighlightToken(skiptoken, sb)
                    sb.Append(skiptoken.Text.Replace("\", "\\").Replace("{", "\{").Replace("}", "\}").Replace(vbLf, "\par" & vbLf))
                Next
            End If

            HighlightToken(node.Token, sb)
            sb.Append(node.Token.Text.Replace("\", "\\").Replace("{", "\{").Replace("}", "\}").Replace("" & Chr(10) & "", "\par" & Chr(10) & ""))
            sb.Append("}")
        End If

        For Each n As ParseNode In node.Nodes
            HightlightNode(n, sb)
        Next
    End Sub

    ''' <summary>
    ''' inserts the RTF codes to highlight text blocks
    ''' </summary>
    ''' <param name="token">the token to highlight, will be appended to sb</param>
    ''' <param name="sb">the final output string</param>
    Private Sub HighlightToken(ByVal token As Token, ByVal sb As StringBuilder)
        Select Case token.Type
<%HightlightTokens%>
            Case Else
                sb.Append("{{\cf0 ")
                Exit Select
        End Select
    End Sub

    ' define the color palette to be used here
    Private Sub AddRtfHeader(ByVal sb As StringBuilder)
        sb.Insert(0, "{\rtf1\ansi\deff0{\fonttbl{\f0\fnil\fcharset0 Consolas;}}{\colortbl;<%RtfColorPalette%>}\viewkind4\uc1\pard\lang1033\f0\fs20")
    End Sub

    Private Sub AddRtfEnd(ByVal sb As StringBuilder)
        sb.Append("}")
    End Sub

    Sub Textbox_Disposed(ByVal sender As Object, ByVal e As EventArgs)
        Dispose()
    End Sub

#Region "IDisposable Members"

    Public Sub Dispose() Implements IDisposable.Dispose
        isDisposing = True
        threadAutoHighlight.Join(1000)
        If threadAutoHighlight.IsAlive Then
            threadAutoHighlight.Abort()
        End If
    End Sub

#End Region

    End Class
End Namespace

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Herre Kuijpers
Architect Capgemini
Netherlands Netherlands
Currently Herre Kuijpers is employed at Capgemini Netherlands for over 10 years, where he developed skills with all kinds of technologies, methodologies and programming languages such as c#, ASP.Net, Silverlight, VC++, Javascript, SQL, UML, RUP, WCF. Currently he fulfills the role of software architect in various projects.

Herre Kuijpers is a very experienced software architect with deep knowledge of software design and development on the Microsoft .Net platform. He has a broad knowledge of Microsoft products and knows how these, in combination with custom software, can be optimally implemented in the often complex environment of the customer.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150414.1 | Last Updated 21 Sep 2010
Article Copyright 2008 by Herre Kuijpers
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid