--- -- WikiFormatter class -- Responsible for processing wiki pages. It supports TWiki like syntax. -- It also defines 'templates' for wiki page headers. -- This class is used by [[WikiEngine]]. It calls methods of [[WikiEngine]] -- class to check if particular page exists or is special page. -- [[WikiEngine]] instance is defined in [[new]] method. require("htk") require("StringBuffer") require("Object") WikiFormatter = class("WikiFormatter", Object) --- -- Constants used in regular expressions and for page 'templates'. wiki = { transToken = "~$~", wikiWordPattern = "(%u+%l+%u+[%a%d]*)", menuSeparator = " | ", menuStart = " ( ", menuEnd = " ) ", menuColor = "#AAAAFF" } --- function WikiFormatter.new(class, store) --print("store", store) local newObject = WikiFormatter.super.new(class, { insideTable = false, insidePre = false, insideNoAutoLink = false, isList = false, listTypes = {}, listElements = {}, store = store , engine = nil, -- reference to WikiEngine result = {}, }) return newObject end function WikiFormatter.methods:setWikiEngine(engine) self.engine = engine end --- -- page templates function WikiFormatter.methods:pageHeaderTemplate(title, additions) local header = HTK.TABLE { width = "100%", cellspacing = 0, cellpadding = 0, border = 0, HTK.TR { HTK.TD { rowspan = 2, align = "center", valign = "bottom", width = "0%", self:existentPageLink("StartPage", HTK.IMG { border = 0, src = serverConfig.logoFile } ) }, HTK.TD { rowspan = 2, width = "0%", " " }, HTK.TD { width = "100%", valign = "center", HTK.H1 { " " ..title } } }, HTK.TR { -- bgcolor = wiki.menuColor, HTK.TD { additions } } } .. HTK.HR {} return header end function WikiFormatter.methods:pageHeaderTable(title, actions, isSpecial) if isSpecial then return self:pageHeaderTemplate(title, actions) else return self:pageHeaderTemplate(self:reversePageLink(title), actions) end end --- -- template for regular wiki page function WikiFormatter.methods:wikiPageTemplate(title, content) return HTK.BODY { self:standardPageHeader(title), self:formatWikiPage(content) } end --- -- template for regular not found wiki page function WikiFormatter.methods:notFoundPageTemplate(title) return HTK.BODY { self:specialPageHeader(title), HTK.P { "The specifed page was not found.", HTK.BR {}, "Try to find it using: " , self:existentPageLink("SearchPage", "Search") , " or ", self:existentPageLink("IndexPage", "Index page"), ".", HTK.BR {}, "You can also create new page: " , self:existentPageLink("EditPage?page=" .. title, title), "." } } end --- -- template for special page: IndexPage function WikiFormatter.methods:specialIndexPageTemplate(pages) local list = "" for i, pageTitle in ipairs(pages) do list = list .. HTK.LI { self:internalLink(pageTitle, nil, pageTitle) } .. "\n" end return HTK.BODY { self:specialPageHeader("Index page") , HTK.P {"The following is a complete list of pages on this server:"}, HTK.UL {list} } end --- -- template for special page: RecentChanges function WikiFormatter.methods:specialRecentChangesTemplate(pages) local list = "" for i, page in ipairs(pages) do local filedate = page[1] local pageTitle = page[2] list = list .. HTK.LI { HTK.B {filedate}, " - ", self:internalLink(pageTitle, nil, pageTitle) } .. "\n" end return HTK.BODY { self:specialPageHeader("Recent changes") , HTK.P {"The following is a list of recently changed pages on this server:"}, HTK.UL {list} } end --- -- template for special page: ReverseLink function WikiFormatter.methods:specialReverseLinkTemplate(pageName, pages) if pages ~= nil then message = "The following is a complete list of pages with links to " .. self:internalLink(pageName, nil, pageName) .. " page:" list = "" for i, pageTitle in ipairs(pages) do list = list .. HTK.LI { self:internalLink(pageTitle, nil, pageTitle) } .. "\n" end else message = "There are no pages with links to " .. self:internalLink(pageName, nil, pageName) .. " page." list = nil end return HTK.BODY { self:specialPageHeader("Reverse links for " .. pageName) , HTK.P {message}, HTK.UL {list} } end --- -- functions for creating page headers function WikiFormatter.methods:specialPageHeader(title) local actions = HTK.P { wiki.menuStart, self:existentPageLink("SearchPage", "Search"), wiki.menuSeparator, self:existentPageLink("IndexPage", "Index page"), wiki.menuSeparator, self:existentPageLink("RecentChanges", "Recent changes"), wiki.menuEnd } return self:pageHeaderTable(title, actions, true) end function WikiFormatter.methods:standardPageHeader(title) local actions = HTK.P { wiki.menuStart, self:existentPageLink("EditPage?page=" .. title, "Edit"), wiki.menuSeparator, self:existentPageLink(title .. "?view=printable" , "Printable"), wiki.menuSeparator, self:existentPageLink("SearchPage", "Search"), wiki.menuSeparator, self:existentPageLink("IndexPage", "Index page"), wiki.menuSeparator, self:existentPageLink("RecentChanges", "Recent changes"), wiki.menuEnd } return self:pageHeaderTable(title, actions, false) end --- -- function for creating back link function WikiFormatter.methods:reversePageLink(theTopic) return HTK.A { href = "ReverseLink?page=" .. theTopic, theTopic } end --- -- function for creating regular link to other wiki page function WikiFormatter.methods:existentPageLink(theTopic, theText) return HTK.A {class="twikiLink", href = theTopic, theText} end --- -- function for creating link to non existend wiki page function WikiFormatter.methods:nonExistentPageLink(theTopic, theText) return HTK.SPAN { class="twikiNewLink", style = "background : #FFFFCE;", HTK.FONT { color = "#0000FF", theText }, HTK.A {href = "EditPage?page=" .. theTopic, HTK.SUP {"?"} } } end function WikiFormatter.methods:processTableLine(line) -- check if this is table definition local beginPos, endPos, capture = string.find(line, "^%s*(|.*|)%s*$") if capture == nil then -- check if table needs to be closed if self.insideTable then self.insideTable = false self.result:addString("\n") end return line -- return line for further processing else line = capture end -- process table local result = "" if not self.insideTable then result = '\n' self.insideTable = true end local row = "" for cell, span in string.gfind(line, "([^|]+)(|+)") do local colSpan = string.len(span) local beginPos, endPos, capture = string.find(cell, "^%s*%*(.*)%*%s*$") local cellContent = "" if beginPos ~= nil then if colSpan == 1 then cellContent = HTK.TH {capture} else cellContent = HTK.TH {colspan = colSpan, capture} end else if colSpan == 1 then cellContent = HTK.TD {cell} else cellContent = HTK.TD {colspan = colSpan, cell} end end row = row .. cellContent end result = result .. HTK.TR {row} return result end --- Render bulleted and numbered lists, including nesting. -- Called from several places. Accumulates listTypes and istElements -- to track nested lists. function WikiFormatter.methods:emitList(theType, theElement, theDepth, theOlType) local result = StringBuffer:new() self.isList = true -- ordered list type if theOlType == nil then theOlType = "" else theOlType = string.gsub (theOlType, "^(.).*" ,"$1") if theOlType == "1" then theOlType = "" end end -- print("LIST", table.getn(self.listElements), self.listElements[table.getn(self.listElements)], -- table.getn(self.listTypes), self.listTypes[table.getn(self.listTypes)]) if table.getn(self.listTypes) < theDepth then local firstTime = true while table.getn(self.listTypes) < theDepth do table.insert(self.listTypes, theType) table.insert(self.listElements, theElement) if not firstTime then result:addString("<" .. theElement .. ">\n") end if theOlType ~= "" then result:addString("<" .. theType .. " type=\"" .. theOlType .. "\">\n") else result:addString("<" .. theType .. ">\n") end firstTime = false; end elseif table.getn(self.listTypes) > theDepth then while table.getn(self.listTypes) > theDepth do local element = table.remove(self.listElements) result:addString("\n") local type = table.remove(self.listTypes) result:addString("\n") end if table.getn(self.listElements) > 0 then result:addString("\n") end elseif table.getn(self.listElements) > 0 then result:addString("\n") end if (table.getn(self.listTypes) > 0) and (self.listTypes[table.getn(self.listTypes)] ~= theType) then local lastType = table.remove(self.listTypes) result:addString("\n<" .. theType .. ">\n") table.insert(self.listTypes, theType) table.remove(self.listElements) table.insert(self.listElements, theElement) end return result:toString() end function WikiFormatter.methods:calculateListLevel(header) local level = string.len(header) if math.mod(level, 3) == 0 then level = level / 3 end return level end function WikiFormatter.methods:specificLink(theLink, theText) -- Strip leading/trailing spaces theLink = string.gsub(theLink, "^%s*", "") theLink = string.gsub(theLink, "%s*$", "") if string.find(theLink, "^%w+:") ~= nil then -- External link: add before WikiWord -- inside link text, to prevent double links theText = string.gsub(theText, "([*s%(])(%u+%l+)", "%1%2") return HTK.A { class="twikiLink", href = theLink, theText } else -- Internal link -- Extract '#anchor' local anchor = nil theLink = string.gsub(theLink, "#" .. wiki.wikiWordPattern, function(text) anchor = text end) return self:internalLink(theLink, anchor, theText) end end function WikiFormatter.methods:internalLink(theTopic, theAnchor, theText) -- check if given page is special one if self.engine:isSpecialPage(theTopic) then return HTK.A { class="twikiLink", href = theTopic, theTopic } end local originalTopic = theTopic local exists = self.store:topicExists(theTopic) -- is plural ? if (not exists) and (string.sub(theTopic, -1) == "s") then -- Topic name is plural in form and doesn't exist as written local tmp = theTopic tmp = string.gsub(tmp, "ies$" ,"y") -- plurals like policy / policies tmp = string.gsub(tmp, "sses$" , "ss") -- plurals like address / addresses tmp = string.gsub(tmp, "([Xx])es$", "%1") -- plurals like box / boxes tmp = string.gsub(tmp, "([A-Za-rt-z])s$", "%1") -- others, excluding ending ss like address(es) if self.store:topicExists(tmp) then exists = true end theTopic = tmp end if exists then local link = theTopic if theAnchor ~= nil then link = theTopic .. "#" .. theAnchor end local text = theText if theText == nil then if theAnchor ~= nil then text = originalTopic .. "#" .. theAnchor else text = originalTopic end end return self:existentPageLink(link, text) else local text = theText if theText == nil then text = originalTopic end return self:nonExistentPageLink(originalTopic, text) end end function WikiFormatter.methods:processUrl(urlType, urlAddress) local url = urlType .. ":" .. urlAddress if (urlType == "http") or (urlType == "ftp") or (urlType == "gopher") or (urlType == "mailto") or (urlType == "file") or (urlType == "news") or (urlType == "telnet") or (urlType == "https") then return HTK.A { href = url, url } else return url end end function WikiFormatter.methods:isValidTitle(title) local beginPos, endPos, captured = string.find(title, wiki.wikiWordPattern) if captured ~= title then return false else return true end end function WikiFormatter.methods:formatWikiLine(line) -- blockquote email (indented with '> ') line = string.gsub(line, "^>(.*)$", HTK.CITE {"%1"} .. HTK.BR {}) -- embedded HTML line = string.gsub(line, "", "--" .. wiki.transToken) line = string.gsub(line, "(<<+)", string.rep("<", string.len("%1"))) line = string.gsub(line, "(>>+)", string.rep(">", string.len("%1"))) line = string.gsub(line, "", "nopTOKEN") line = string.gsub(line, "<(%S.-)>", wiki.transToken .. "%1" .. wiki.transToken) line = string.gsub(line, "<", "<") line = string.gsub(line, ">", ">") line = string.gsub(line, wiki.transToken .. "(%S.-)" .. wiki.transToken, "<%1>") line = string.gsub(line, "nopTOKEN", "") line = string.gsub(line, wiki.transToken .. "!%-%-", "") -- handle embedded URLs line = string.gsub(line, '([^%[])(%w+):([^%s<>"]+[^%s\.,!?;:)<=_*])([^%]])', function(charBefore, urlType, urlAddress, charAfter) return charBefore .. self:processUrl(urlType, urlAddress) .. charAfter end) -- entities line = string.gsub(line, "&(%w%w-);", wiki.transToken .. "%1;"); -- "&abc;" line = string.gsub(line, "&#(%d+);", wiki.transToken .. "#%1;"); -- "{" line = string.gsub(line, "&", "&"); -- escape standalone "&" line = string.gsub(line, wiki.transToken, "&") -- headings ---+ line = string.gsub(line, "^(%-%-%-%++ )(.*)$", function(header, restOfLine) local level = string.len(header) - 4; -- decrease for 3 dashes and 1 space if level <= 6 then return "" .. restOfLine .. "" else return header .. restOfLine; -- no processing, there was syntax error end end) -- horizontal rule ---- line = string.gsub(line, "^%-%-%-%-+", HTK.HR {}) -- table | cell | cell | cell | line = self:processTableLine(line) -- paragraphs local matches = 0 line, matches = string.gsub(line, "^%s*$", HTK.P {}) if (matches > 0) or (string.find(line, "^%S+" ) ~= nil) then self.isList = false end -- lists --[[# Definition list s/^(\t+)\$\s(([^:]+|:[^\s]+)+?):\s/
$2 <\/dt>
/o && ( $result .= &emitList( "dl", "dd", length $1 ) ); s/^(\t+)(\S+?):\s/
$2<\/dt>
/o && ( $result .= &emitList( "dl", "dd", length $1 ) ); --]] -- unnumbered list line = string.gsub(line, "^([ ]+)%* ", function(header) self.result:addString(self:emitList("UL", "LI", self:calculateListLevel(header))) return "
  • " end) -- numbered list line = string.gsub(line, "^([ ]+)(%d+%.?) ?", function(header, listType) self.result:addString(self:emitList("OL", "LI", self:calculateListLevel(header), listType)) return "
  • " end) -- special numbering list line = string.gsub(line, "^([ ]+)([1AaIi]%.) ?", function(header, listType) self.result:addString(self:emitList("OL", "LI", self:calculateListLevel(header), listType)) return "
  • " end) -- finish the list if not self.isList then self.result:addString(self:emitList("", "", 0)) self.isList = false end -- '#WikiName' anchors line = string.gsub(line, "^(#)" .. wiki.wikiWordPattern .. "", HTK.A {name = "%2"}) -- add spaces for better processing for the following patterns line = " " .. line .. " " -- emphasizing -- bold monospaced ==ble ble== line = string.gsub(line, "([%s(])==([^%s].-[^%s])==([%s,.;:!?])", "%1" .. HTK.TT { HTK.STRONG {"%2"} } .. "%3") line = string.gsub(line, "([%s(])==([^%s]-)==([%s,.;:!?])", "%1" .. HTK.TT { HTK.STRONG {"%2"} } .. "%3") -- bold italic __ble ble__ line = string.gsub(line, "([%s(])__([^%s].-[^%s])__([%s,.;:!?])", "%1" .. HTK.STRONG { HTK.EM {"%2"} } .. "%3") line = string.gsub(line, "([%s(])__([^%s]-)__([%s,.;:!?])", "%1" .. HTK.STRONG { HTK.EM {"%2"} } .. "%3") -- bold *ble ble* line = string.gsub(line, "([%s(])%*([^%s].-[^%s])%*([%s,.;:!?])", "%1" .. HTK.STRONG {"%2"} .. "%3") line = string.gsub(line, "([%s(])%*([^%s]-)%*([%s,.;:!?])", "%1" .. HTK.STRONG {"%2"} .. "%3") -- italic _ble ble_ line = string.gsub(line, "([%s(])_([^%s].-[^%s])_([%s,.;:!?])", "%1" .. HTK.EM {"%2"} .. "%3") line = string.gsub(line, "([%s(])_([^%s]-)_([%s,.;:!?])", "%1" .. HTK.EM {"%2"} .. "%3") -- monospaced =ble ble= line = string.gsub(line, "([%s(])=([^%s].-[^%s])=([%s,.;:!?])", "%1" .. HTK.TT {"%2"} .. "%3") line = string.gsub(line, "([%s(])=([^%s]-)=([%s,.;:!?])", "%1" .. HTK.TT {"%2"} .. "%3") -- Make internal links -- hyper links [[http://ble.ble][Ble ble ble]] -- or [[WikiName][Ble ble]] line = string.gsub(line, "%[%[([^%]]+)%]%[([^%]]+)%]%]", function(link, text) return self:specificLink(link, text) end) -- WikiWords if (not self.insideNoAutoLink ) then -- TopicName#anchor link: line = string.gsub(line, "([%s(])" .. wiki.wikiWordPattern .. "#" .. wiki.wikiWordPattern , function(prefix, title, anchor) return prefix .. self:internalLink(title, anchor) end) -- s/([\s\(])($regex{wikiWordRegex})($regex{anchorRegex})/&internalLink($1,$theWeb,$2,"$TranslationToken$2$3$TranslationToken",$3,1)/geo; -- TopicName link: line = string.gsub(line, "([%s(])" .. wiki.wikiWordPattern, function(prefix, title) return prefix .. self:internalLink(title) end) end -- process strings like %BR% line = string.gsub(line, "%%(%S+)%%", function(text) return self:processDefinedVariables(text) end) return line end -- TODO write more rules for %xx% strings -- define them in configuration table? function WikiFormatter.methods:processDefinedVariables(text) if text == "BR" then return HTK.BR {} else return line end end function WikiFormatter.methods:takeOutVerbatim(text) local verbatimList = {} local verbatimIndex = 1 local processedText = string.gsub(text, "(.-)", function(verbatimText) verbatimList[verbatimIndex] = verbatimText local substitution = "VERBATIM(" .. verbatimIndex .. ")VERBATIM" verbatimIndex = verbatimIndex + 1 return substitution end) return processedText, verbatimList end function WikiFormatter.methods:putBackVerbatim(text, verbatimList) local processedText = string.gsub(text, "VERBATIM%((%d+)%)VERBATIM", function(verbatimIndex) local verbatimText = verbatimList[tonumber(verbatimIndex)] local substitution = HTK.PRE { verbatimText } return substitution end) return processedText; end function WikiFormatter.methods:formatWikiPage(content) -- initial cleanup content = string.gsub(content, "\r", "") -- clutch to enforce correct rendering at end of doc content = string.gsub(content, "(\n?)$", "\n\n") local verbatimList = {} -- remove .. sections content, verbatimList = self:takeOutVerbatim(content) -- join lines ending in "\" content = string.gsub(content, "\\\n", ""); -- list test self.isList = false -- spliting into lines ignores last line if it doesn't end with \n -- so add \n just for a case -- content = content .. "\n"; self.result = StringBuffer:new() for line in string.gfind(content, "([^\n]*)\n") do if string.find(string.lower(line), "
    ", 1, true) ~= nil then
                self.insidePre = true
            elseif string.find(string.lower(line), "
    ", 1, true) ~= nil then self.insidePre = false end if string.find(string.lower(line), "", 1, true) ~= nil then self.insideNoAutoLink = true elseif string.find(string.lower(line), "", 1, true) ~= nil then self.insideNoAutoLink = false end -- in TWiki at this place tabs are converted to 3 spaces -- I decided not to do that - it enables editing files -- in text editor and no preprocessing is needed -- Also saving edited page is simpler - no need to change -- 3 spaces to tabs if self.insidePre then -- process line inside
     tags
                -- close list tags if any
                if table.getn(self.listTypes) > 0 then
                    self.result:addString(self:emitList( "", "", 0 ))
                    self.isList = false
                end
                self.result:addString(line .. "\n")
            else
                -- normal state, do wiki rendering
                self.result:addString(self:formatWikiLine(line))
            end
        end
        -- close started tags
        if self.insideTable then
            self.result:addString("
  • \n") end -- close list tags self.result:addString(self:emitList( "", "", 0 )) if self.insidePre then self.result:addString("\n") end local processedLines = self.result:toString() -- insert removed .. sections processedLines = self:putBackVerbatim(processedLines, verbatimList) -- clean up clutch processedLines = string.gsub(processedLines, "\n?\n$", "") return processedLines end