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.
EditDownload links
EditSummary: 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.
EditGeneral
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.
EditIndividual 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.
EditRelease 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.
EditCustom filter written for a specific forum request:
http://www.screwturn.eu/forum/viewtopic.php?t=4124This 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.
EditPurpose: 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).
EditMyCodeFormatter.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:
- Created a class library project name MyCodeFormatter.dll. Referenced Screwturn.Wiki.PluginFramework.dll and CSharpFormat.dll.
- 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("&", "&")
' code = code.Replace("&", "&")
' code = code.Replace("<", "<")
' code = code.Replace(">", ">")
' 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("&")
Case "<"c
buf.Append("<")
Case ">"c
buf.Append(">")
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,}(?= )|(?<=&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.