"Nobility is a graceful ornament to the civil order. It is the Corinthian capital of polished society." - Edmund Burke


Screwturn Wiki is the fantastic asp.net based wiki software available at http://www.screwturn.eu (it's also the software that powers this web site). Screwturn supports plugins written in any .NET language to create additional functionality; I've written a few of those plugins and they are available below.

Edit

Download links


Edit

MyPlugins.zip

Summary: MyPlugins.zip is a vb.net project that contains a set of sample plugins for the ScrewTurn wiki. These are currently running on a version 2.0 copy of ScrewTurn wiki.

Edit

General

Each plugin is represented by it's own class in the source code. The basic structure is that each plugin's name is the code that it finds and replaces in the text. For example, if a class is named "CategoryList", then the code replaces any occurrence of {CategoryList} in the text. For some plugins, the text takes parameters. For example, the "NewestPages" class takes a single parameter representing the number of days you wish to see. The resulting tag in the text looks like this: {NewestPages:7}. If the parameter is omitted, the class may have a default value.

Edit

Individual plugins

{CategoryList} - displays a list of all of the categories in your wiki with links to the page that displays those category's pages. The list it produces is wiki text which is then interpreted by the main formatting engine. Example:

* [AllPages.aspx?Cat=Community|Community]
* [AllPages.aspx?Cat=Development|Development]
* [AllPages.aspx?Cat=-|Uncategorized]

{CategoryIndex:CategoryName} - This class takes a parameter that is a category name. If left off, the category name will be assumed to be "*" (i.e. all categories). The category name Index will be automatically skipped in cases.

The list produced by this class is a list of all pages organized by category. If a page has more than one category, then it is listed more than once.

An optional additional parameter can follow the category name (separated by a pipe "|" character) that will be interpreted as a filter on the page titles. For example: {CategoryIndex:*|A*} will display all of the categories, but only those pages that begin with the letter "A". {CategoryIndex:Help*|A*} will display all pages in categories that start with "Help" and where the title starts with the letter "A". The wildcards are required unless you wish to match the value exactly.

{CurrentDateTime} - displays the current date/time in the format configured into the wiki on the admin page.

{CurrentDay} - displays the current day of the week (Monday, Tuesday, etc.)

{CurrentYear} - displays the current year (4 digits)

{NewestPagesList:7} - displays the most recently created pages. The parameter specifies the number of days (default=7). This means it will only display the pages created in the last 7 days.

{RecentUpdatesList:7} - displays the most recently changed pages. The parameter specifies the number of days (default=7). This means it will only display the pages changed in the last 7 days.

{PageArchiveIndex:CategoryName} - displays an index of all pages (filtered by category; default = "*"). The category "Index" is always skipped. This index is organized by Creation Date.

{PageVersion} - displays a version number based on the last modification date (number of days since 1/1/2000).

{PageModifyDate} - displays the date this page was last modified.

{RandomLine:PageName} - selects a random paragraph of text from the page indicated in the parameter and display it. Useful for a random quotes page or putting random quotes in the header. Will ignore blank lines in the target page.

{Reference:Text of the reference here} - creates a reference link. Basically a linked number is display in the main text and then the reference text is displayed at the bottom of the page.

If you want the reference to create a link to another page (such as a link to an Amazon page for the referenced book), code it like this:

{Reference:link|reference text}

{ReferenceIndex} - creates an index of all of the references created using the {Reference} tag above.

{TrackBack} - displays a link to all of the other pages in your wiki that directly reference this one.

Edit

Release notes (updates from original version)

2008/02/29 10:19:49 - fixed CategoryIndex so it can work in the sidebar, footer, header, etc. It was dependent on Context.Page and that object was Nothing in those situations. It now detects that situation and works around it.

Edit

AlphaIndex.zip

Custom filter written for a specific forum request: http://www.screwturn.eu/forum/viewtopic.php?t=4124

This started as a request to allow the CategoryIndex tag in MyPlugins.dll to show only those whose title began with a certain letter. That led to the requestor creating a page with 26 calls to that plugin. I then took the code he manually used to create that page and create this plugin to generate it all in one call.

In addition to a section for each first letter, there's a table of contents along the top that links to each section. Also, the first half appears in a div that fills the left half of the viewable area and the remainder is in the right half. The split between left and right is done at a letter boundary, so it won't be perfectly balanced.

This plugin is dependent on an image existing in the upload directory called "dwiki%20stuff%2ftop.png". If that's not there, it'll just show the word "Image" next to each letter. This image is the link to return to the top of the page, so a picture of a small upward pointing arrow seems most appropriate. Since the source code also available in this filter, feel free to modify it in whatever way works best for you.

Edit

ExternalPageProvider.zip

Purpose: I have a couple of systems that can or do produce files that I would like to be able to see within the context of my wiki but I don't want them to be editable and I do want the wiki to know when the external programs have changed them. To support this, I created this provider.

It works by scanning a designated directory (currently a Constant subdirectory of the Public directory, but it could be changed to use a configuration string) for any files that match it's list of known file types (extension). When it registers that list with the wiki, it prefixes the page name and page title with "External." to help minimize overlap problems with existing pages.

Within the code are a series of functions that each of the same signature and are designed to convert a given file type into something the wiki can display. For example, files with the extension ".txt" are wrapped in the wiki tags needed to make the text appear in fixed font. It's also been wrapped to 80 character lines. Files with the extension ".csv" are read as comma-separated data and a wiki table is generated.

To add support for additional file types, create a function with this signature:

Delegate Function File2WikiText(ByVal fn As String) As String

Code the function to read the file and return the appropriate wiki text. The marked up text is cached within the dll to avoid having to call the file handler function more than needed. Then, add a line to the InitHandlers function to register that extension.

This provider is implemented as a "Read-only" provider, so any of the methods in the interface that I felt weren't going to be available when the page is read-only aren't really coded (just a simple exception is thrown to flag the situation in case it does get called). Others that I felt didn't apply were coded in such a way that they are ignored.

The main code is in the following methods:

AllPages: reads the list of files from the directory (ignoring those that have extensions that aren't registered in InitHandlers) and returns the list.

AllCategories: Generates one category for each extension supported and assigns the appropriate pages to it.

GetContent: Reads the external file, passes it through the appropriate handler and returns the wiki text.

From the Admin page, the only function that's actually supported is the delete page function. That will cause the provider to rename the page to the extension ".bak" instead of actually deleting it. The other functions on this page (renaming the page or setting it's status) are ignored.

To stay fresh, this plugin uses a FileSystemWatcher to flag any changes in the directory it's watching. If a file changes, it triggers a request to the wiki engine to refresh it's page list.

More details at this forum thread.

2008/07/23 09:05:22 -- Updated to show unknown extensions as the name of the file, it's date/time/size info and a link to it. This same header is added to many of the known file types as well (except .html and .wiki).

Edit

MyCodeFormatter.dll

To format code samples, I've created a small Screwturn plugin that just calls CSharpFormat.dll. I'm not comfortable loading the entire project since it contains the work of someone other than me. Instead I'll describe the way I created it and list my portion of the source:

  1. Created a class library project name MyCodeFormatter.dll. Referenced Screwturn.Wiki.PluginFramework.dll and CSharpFormat.dll.
  2. Created the following class:

Option Explicit On
Option Strict On

Imports System Imports System.Collections.Generic Imports ScrewTurn.Wiki.PluginFramework Imports Manoli.Utils

Public Class CodeFormatterClass Implements IFormatterProvider

Private Shared NewLineCharArray() As Char = Microsoft.VisualBasic.ControlChars.CrLf.ToCharArray Private Shared WhiteSpaceCharArray() As Char = (" " & Microsoft.VisualBasic.ControlChars.CrLf & Microsoft.VisualBasic.ControlChars.Tab).ToCharArray

Private _host As IHost = Nothing Private MyStyleSheet As String = "" Private fmtdict As New Dictionary(Of String, CSharpFormat.SourceFormat)

Private Class ParmDictClass Inherits Dictionary(Of String, String)

Public Function ItemAsBool _ (ByVal parmname As String) As Boolean

If Me.ContainsKey(parmname) = False Then Return False

Dim s As String = Item(parmname) If s Is Nothing Then Return False 'anything that starts with "Y" or "T" is "true": 'intended values: true, false, yes, no Return ("tTyY".IndexOf(s.Chars(0)) >= 0) End Function

Public Function ItemAsStr _ (ByVal parmName As String) As String

If Not Me.ContainsKey(parmName) Then Return "" Return Item(parmName) End Function

Public Shared Function ParseCodeTag(ByVal tag As String) _ As ParmDictClass

Dim tmp As New ParmDictClass If tag Is Nothing Then Return tmp If tag.Length <= 6 Then Return tmp

tag = tag.Substring(5, tag.Length - 6).Trim.ToLower If tag.Length = 0 Then Return tmp

Dim aParms() As String = tag.Split(WhiteSpaceCharArray, StringSplitOptions.RemoveEmptyEntries) For Each Parm As String In aParms Dim a() As String = Parm.Split("="c) Dim parmKey As String = "" Dim parmVal As String = "" If a.Length = 1 Then parmKey = "lang" 'parm without key names is the "lang" parm parmVal = StripQuotes(a(0)) Else parmKey = a(0) parmVal = StripQuotes(a(1)) End If

If tmp.ContainsKey(parmKey) Then tmp(parmKey) = parmVal Else tmp.Add(parmKey, parmVal) End If Next Return tmp End Function End Class

Private Shared Function CountLeadingSpaces(ByVal s As String) As Integer If s Is Nothing Then Return -1 If s.Length = 0 Then Return -1

Dim n As Integer = 0 For i = 0 To s.Length - 1 If s.Chars(i) <> " "c Then Return i Next Return -1 'all spaces? treat as if it was empty End Function

Private Shared Function StripLeadingSpaces(ByVal s As String) As String If s Is Nothing Then Return ""

Dim buf As New Text.StringBuilder Dim lines As New List(Of String) lines.AddRange(s.Split(NewLineCharArray, StringSplitOptions.RemoveEmptyEntries))

Do Until lines.Count = 0 OrElse lines(0).Trim.Length > 0 lines.RemoveAt(0) Loop Do Until lines.Count = 0 OrElse lines(lines.Count - 1).Trim.Length > 0 lines.RemoveAt(lines.Count - 1) Loop

Dim result As Integer = -1 For i = 0 To lines.Count - 1 Dim n As Integer = CountLeadingSpaces(lines(i)) If n = 0 Then result = -1 Exit For End If If n <> -1 AndAlso (n < result OrElse result = -1) Then result = n Next If result > 0 Then For i = 0 To lines.Count - 1 lines(i) = lines(i).Substring(result) Next End If

For i = 0 To lines.Count - 1 buf.AppendLine(lines(i)) Next Return buf.ToString End Function

Private Shared Function StripQuotes(ByVal s As String) As String If s Is Nothing Then Return "" If s.Length < 2 Then Return s

If s.Chars(0) = """"c _ AndAlso s.Chars(s.Length - 1) = """"c Then Return s.Substring(1, s.Length - 2) End If

Return s End Function

'Private Shared Function TextOnlyFormatter(ByVal code As String) As String ' code = code.Replace("&", "&amp;") ' code = code.Replace("&", "&amp;") ' code = code.Replace("<", "&lt;") ' code = code.Replace(">", "&gt;") ' Return "<pre class=""csharpcode"">" & code & "</pre>" 'End Function

Private Function TextOnlyFormatter(ByVal code As String) As String If code Is Nothing Then Return ""

Dim buf As New Text.StringBuilder(CInt(code.Length * 1.25)) buf.Append("<pre class=""csharpcode"">") Dim prevPos As Integer = 0 Dim currPos As Integer = code.IndexOfAny("&<>".ToCharArray, 0) Do Until currPos < 0 If currPos > prevPos Then buf.Append(code.Substring(prevPos, currPos - prevPos)) End If Select Case code.Chars(currPos) Case "&"c buf.Append("&amp;") Case "<"c buf.Append("&lt;") Case ">"c buf.Append("&gt;") End Select prevPos = currPos + 1 currPos = code.IndexOfAny("&<>".ToCharArray, prevPos) Loop

currPos = code.Length If currPos > prevPos Then buf.Append(code.Substring(prevPos, currPos - prevPos)) End If buf.Append("</pre>")

Return buf.ToString End Function

Private Function FindOpenTag(ByVal raw As String, ByVal startPos As Integer) As Integer 'since the start tag can be either be <code> or <code langParm>, 'we need to search for "<code" and then check that the 5th character 'is either a space or a greater-than symbol. If we just find <code 'without one of those 2 things, we should keep searching until 'we find another one or we find nothing and return a negative value. Const OpenTag As String = "<code" Const gt As Char = ">"c Dim p1 As Integer = raw.IndexOf(OpenTag, startPos, StringComparison.CurrentCultureIgnoreCase) Do While p1 > 0 AndAlso raw.Chars(p1 + OpenTag.Length) <> gt _ AndAlso raw.Chars(p1 + OpenTag.Length) <> " "c p1 = raw.IndexOf(OpenTag, p1 + 1, StringComparison.CurrentCultureIgnoreCase) Loop Return p1 End Function

Public Function Format _ (ByVal raw As String, _ ByVal context As ContextInformation, _ ByVal phase As FormattingPhase) As String _ Implements IFormatterProvider.Format

If raw Is Nothing Then Return ""

Const CloseTag As String = "</code>" Const gt As Char = ">"c

Try Dim firstTime As Boolean = True Dim pOpenTagStart As Integer = FindOpenTag(raw, 0)

Do Until pOpenTagStart < 0 If firstTime Then raw = MyStyleSheet & Microsoft.VisualBasic.ControlChars.CrLf & raw pOpenTagStart += MyStyleSheet.Length + 2 firstTime = False End If

Dim pCloseTag As Integer = raw.IndexOf(CloseTag, pOpenTagStart, StringComparison.CurrentCultureIgnoreCase) Dim pOpenTagClose As Integer = raw.IndexOf(gt, pOpenTagStart) If pCloseTag >= 0 And pCloseTag > pOpenTagClose Then Dim code As String = StripLeadingSpaces(raw.Substring(pOpenTagClose + 1, pCloseTag - pOpenTagClose - 1)) code = code.Replace("&null", "") Dim tag As String = raw.Substring(pOpenTagStart, pOpenTagClose - pOpenTagStart + 1) Dim parms As ParmDictClass = ParmDictClass.ParseCodeTag(tag) Dim lang As String = parms.ItemAsStr("lang")

If fmtdict.ContainsKey(lang) Then fmtdict(lang).EmbedStyleSheet = False fmtdict(lang).Alternate = parms.ItemAsBool("alternate") fmtdict(lang).LineNumbers = parms.ItemAsBool("linenumbers") fmtdict(lang).TabSpaces = 4 code = fmtdict(lang).FormatCode(code) Else code = TextOnlyFormatter(code) End If

raw = raw.Substring(0, pOpenTagStart) & "<nowiki>" & code & "</nowiki>" & raw.Substring(pCloseTag + 7) End If

If pCloseTag < 0 Then pCloseTag = pOpenTagStart pOpenTagStart = FindOpenTag(raw, pCloseTag) Loop Catch ex As Exception Return "<pre>" & ex.Message & ex.StackTrace & "</pre>" End Try Return raw End Function

Public ReadOnly Property PerformPhase1() As Boolean Implements IFormatterProvider.PerformPhase1 Get Return True End Get End Property

Public ReadOnly Property PerformPhase2() As Boolean _ Implements IFormatterProvider.PerformPhase2 Get Return False End Get End Property

Public ReadOnly Property PerformPhase3() As Boolean _ Implements IFormatterProvider.PerformPhase3 Get Return False End Get End Property

Public ReadOnly Property Information() As ComponentInformation Implements IProvider.Information Get Return New ComponentInformation("MyCodeFormatter", "Mike Mestemaker", "") End Get End Property

Public Sub Init(ByVal host As IHost, ByVal config As String) Implements IProvider.Init MyStyleSheet = "<style type=""text/css"">" & _ CompressCSS.Exec(CSharpFormat.SourceFormat.GetCssString) & _ "</style>"

_host = host

Dim tmp As CSharpFormat.SourceFormat

tmp = New CSharpFormat.VisualBasicFormat fmtdict.Add("vb.net", tmp) fmtdict.Add("vb", tmp) fmtdict.Add("visual basic", tmp) fmtdict.Add("vbscript", tmp) tmp = New CSharpFormat.CSharpFormat fmtdict.Add("cs", tmp) fmtdict.Add("c#", tmp) fmtdict.Add("c", tmp) fmtdict.Add("c++", tmp) tmp = New CSharpFormat.HtmlFormat fmtdict.Add("xml", tmp) fmtdict.Add("html", tmp) tmp = New CSharpFormat.JavaScriptFormat fmtdict.Add("js", tmp) fmtdict.Add("javascript", tmp) tmp = New CSharpFormat.TsqlFormat fmtdict.Add("sql", tmp)

End Sub

Public Sub Shutdown() Implements IProvider.Shutdown fmtdict.Clear() _host = Nothing End Sub End Class

The CompressCSS class is based on the code from this article on Zack Owens blog:

Imports System
Imports System.Text.RegularExpressions

Public Class CompressCSS Public Shared Function Exec(ByVal body As String) As String body = Regex.Replace(body, "/\*.+?\*/", "", RegexOptions.Singleline) body = body.Replace(" ", String.Empty) body = body.Replace(Environment.NewLine + Environment.NewLine + Environment.NewLine, String.Empty) body = body.Replace(Environment.NewLine + Environment.NewLine, Environment.NewLine) body = body.Replace(Environment.NewLine, String.Empty) body = body.Replace("\t", String.Empty) body = body.Replace(" {", "{") body = body.Replace(" :", ":") body = body.Replace(": ", ":") body = body.Replace(", ", ",") body = body.Replace("; ", ";") body = body.Replace(";}", "}") body = Regex.Replace(body, "/\*[^\*]*\*+([^/\*]*\*+)*/", "$1") body = Regex.Replace(body, "(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,}(?=&nbsp;)|(?<=&ndsp;)\s{2,}(?=[<])", String.Empty)

Return body End Function End Class

Lastly, after I built the dll's, I ran ILMerge to merge the dll's into a single dll for upload to the website:

ILMerge /out:CodeFormatter.dll MyCodeFormatter.dll CSharpFormat.dll

In addition to doing basic code formatting, this dll allows access to the Alternate and LineNumbers properties of CSharpFormat. Just create your code tag like this:

<code lang="vb" alternate="true" linenumbers="true">
</code>

Lastly, if the lang parameter is an unknown language, the formatter will simply convert any wiki or html tags so they appear as-is and then it will wrap the text in a <pre> tag so it appears as fixed text.

The dll only can be downloaded here.

ScrewTurn Wiki version 2.0.33. Current Page Count: 23. Some of the icons created by FamFamFam.