"" "^\"\"(.*)$" Where following text is a CSS class name
-- preformatted: .. "^%.%.(.*)$" Where following text is a CSS class name or @lua function name
-- block-comments: (unrendered) {{ "^{{$"
-- lists: , *# "^([*#]+)%s*(.*)$" any combination of *'s and #'s at the beginning of the line
-- definitions: : "^:(text):(def)"
-- paragraph: If none of the above apply
function new_collector()
local c = {}
c.collection = {}
c.collect = function(self, item)
self.collection[#self.collection +1] = item
end
c.pop = function(self)
local item = self.collection[#self.collection]
self.collection[#self.collection] = nil
return item
end
c.append = function(self, text)
local cur_text = self:pop()
self:collect(cur_text .. text)
end
c.concat = function(self, separator)
local concat = table.concat(self.collection, separator)
return concat
end
c.empty = function(self)
self.collection = {}
end
c.gather = function(self, separator)
local concat = table.concat(self.collection, separator)
self.collection = {}
return concat
end
return c
end
function new_stack()
local s = {}
s.stack = {}
s.push = function(self, item)
self.stack[#self.stack +1] = item
end
s.pop = function(self)
local n = #self.stack
local item = self.stack[n]
self.stack[n] = nil
return item
end
s.concat = function(self, separator)
local concat = table.concat(self.stack, separator)
return concat
end
s.empty = function(self)
self.stack = {}
end
s.gather = function(self, separator)
local concat = self:concat(separator)
self:empty()
return concat
end
s.is_empty = function(self)
if #self.stack == 0 then return true end
return false
end
s.depth = function(self)
return #self.stack
end
return s
end
--
-- get_named_field_or_value_or_default()
--
-- Here's a function which will attempt to fetch a named field from a passed parameter.
-- Or, if the passed parameter isn't a table, its value will be used if the passed param
-- is a number. Or, a default value will be returned
get_named_field_or_value_or_default = function(p, field_name, default)
local v
if type(p) == "table" and type(p[field_name]) == "number" then
v = p[field_name]
elseif type(p) == "number" then
v = p
else
v = default
end
return v
end
--
-- new_tabber()
--
-- An tabber is a table of functionality and state used for keeping track of how
-- many indent characters (spaces) need to be prepended to html for a given line.
--
-- The units this table tracks should be thought of as 'tabs'. It does not track
-- the number of spaces, but the number of tabs, even though the output is N spaces
-- per tab.
--
-- base_tabs is the number of tabs to begin with. Often this will be 0 (assumed, if
-- not supplied during initialization). But, for tables or lists within tables or
-- other nested structures, this will often be the base of the parent.
--
-- tabber.str()
-- This function will return a number of spaces to indent the beginning a line
-- as a multiple of some number of spaces. The default is parse_indent_cache[1].
--
-- tabber.indent()
-- Increments the number of tabs.
--
-- tabber.undent()
-- Decrements the number of tabs.
--
-- tabber.base == The number of tabs that should be applied even
-- when the num_tabs (see next) is 0.
-- tabber.num_tabs == The number of tabs above the base.
-- tabber.tab_width == The width of an individual tab
--
-- Note that the tabber lacks knowledge to handle arbitrary starting points for
-- the tabber.str() function. That is, it always assumes that the tab.str()
-- will begin at the left most column.
--
new_tabber = function(parent, tab_width)
local i = {}
i.base = get_named_field_or_value_or_default(parent, "cur_tab_stop", 0)
if type(parent) ~= "type" then parent = nil end
i.tab_width = get_named_field_or_value_or_default(parent, "tab_width", tab_width or 4)
i.cur_tab_stop = 0
i.indent = function(self)
self.cur_tab_stop = self.cur_tab_stop + 1
end
i.undent = function(self)
if self.cur_tab_stop == 0 then
error("undent called when cur_tab_stop is already 0")
end
self.cur_tab_stop = self.cur_tab_stop - 1
end
i.str = function(self)
return string.rep(" ", (self.cur_tab_stop + self.base) * self.tab_width)
end
return i
end
parse_inline = function (block)
block = block:gsub("%'%'%'(.-)%'%'%'", function(t) return "" .. t .. "" end)
block = block:gsub("%'%'(.-)%'%'", function(t) return "" .. t .. "" end)
return block
end
-- **************************************************
--
-- new_parser()
--
-- This routine will return a new parser table that can be
-- invoked to read lines of text and process them.
--
-- The indenting is written as though parse.body is at a
-- global leve, although it is all defined -within- the
-- function new_parser(). This was done erroneously and
-- could be modified if it would be cleaner to have everything
-- properly indented one tab stop.
--
function new_parser(read_line_func, read_line_arg)
local parse = {}
parse.warn = function(self, ...)
print("WARNING: " .. ...)
end
parse.error = function(self, ...)
error("ERROR: " .. ...)
end
parse.tabber = new_tabber(0)
parse.save_line = new_stack()
parse.read_line_t = { ["func"] = read_line_func, ["arg"] = read_line_arg }
parse.read_line = function(self)
if self.save_line:is_empty() then
return self.read_line_t.func(self.read_line_t.arg)
end
return self.save_line:pop()
end
parse.body = new_stack()
parse.wrap_class = function(self, markup, text, class)
local class_markup = "" -- Assume a blank string
if class then
class = class:gsub("%-%-.*$", "") -- A comment within a class declaration?
class = class:gsub("^%s*", "")
class = class:gsub("%s*$", "")
if class ~= "" then
class_markup = ' class="' .. class .. '"'
end
end
return "<" .. markup .. class_markup .. ">" .. text .. "" .. markup .. ">"
end
parse.wrap_block_class = function(self, wrap, block, class)
local inlined = parse_inline(block)
local wrapped = self:wrap_class(wrap, inlined, class)
return wrapped
end
parse.headings = function(self, line)
local s, e, hn, heading = line:find("^(=+)%s*(.*)$")
if not s then return false end
if #hn > 6 then
self:warn("A heading with more than 6 levels, only 1..6 are supported; reverting to 6")
hn="======"
end
return true, self:wrap_block_class("h"..#hn, heading)
end
parse.horiz_rule = function(self, line)
local s, e = line:find("^%-%-%-+$")
if not s then return false end
return true, "
"
end
--
-- blockquote, block comments and preformatted text
--
-- All are block oriented, beginning with some line of symbols
-- and terminated with a line of symbols. block_until is used
-- to grab all the lines within the block.
--
-- Blocks do not nest in any manner. In addition, preformatted
-- text has no inline parsing. Because block comments are
-- ignored, there is no additional parsing. Block quotes have
-- inline parsing, but there is no possibility of lists, tables,
-- or headings within block quotes.
--
parse.block_stack = new_stack()
parse.block_until = function(self, pattern)
local block = self.block_stack
block:push("")
local line = ""
repeat
line = self:read_line()
if not line or line:find(pattern) then
block:push("")
return block:gather("\n")
end
block:push(line)
until false -- Loop forever
end
parse.block_quote = function(self, line)
local s, e, class = line:find('^""(.*)$')
if not s then return false end
local block = self:block_until('^""$')
return true, self:wrap_block_class("blockquote", block, class)
end
parse.preformatted = function(self, line)
local s, e, class = line:find("^%.%.(.*)$")
if not s then return false end
local block = self:block_until("^%.%.$")
return true, self:wrap_block_class("pre", block, class)
end
parse.block_comments = function(self, line)
local s, e = line:find("^{{$")
if not s then return false end
local block = self:block_until("^}}$")
return true, nil
end
parse.is_blank = function(self, line)
local s = line:find("^%s*$")
if s then return true end
return false
end
--
-- list()
--
-- This function needs handle the following cases:
--
-- 1) We don't currently have any outstanding list and a single list identifier (i.e., # or *) comes in
-- A. Generate or and begin collecting a block
-- 2) We currently have an outstanding list, but this one lengthens it by some amount
-- A. We have to generate subsequent s or s to match
-- 3) We currently have an outstanding list, but this one decreases it by some amount
-- A. We need to generate subsequent
s or s to match
--
parse.list_tag = { ["open"] = { ["#"] = "", ["*"] = "" },
["close"] = { ["#"] = "
", ["*"] = "
" } }
parse.new_list = function(self)
local l = {}
l.open_item_stack = new_stack()
l.markup_stack = new_stack()
l.html_col = new_collector()
l.text_col = new_collector()
l.tabber = new_tabber(self.tabber)
l.handled_text = false
return l
end
parse.list = nil
parse.list_stack = new_stack()
parse.is_list = function(line)
if not line:find("^([*#]+)%s*(.*)$") then return false end
return true
end
parse.parse_list = function(self, line)
if not self.is_list(line) then return false end
local saved = false
if self.list then
self.list_stack:push(self.list)
saved = true
end
self.list = self:new_list()
self:parse_list2(line)
local html = self.list.html_col:gather("\n")
self.list = nil
if saved then
self.list = self.list_stack:pop()
end
return true, html
end
parse.list_open = function(self, markup_elem)
local tabber = self.list.tabber
local html_col = self.list.html_col
local markup_stack = self.list.markup_stack
local tags = self.list_tag
markup_stack:push(markup_elem)
html_col:collect(tabber:str() .. tags.open[markup_elem])
tabber:indent()
html_col:collect(tabber:str() .. "- ")
self.list.handled_text = false
end
parse.list_close = function(self)
local tabber = self.list.tabber
local html_col = self.list.html_col
local markup_stack = self.list.markup_stack
local tags = self.list_tag
if self.list.handled_text then
html_col:append("
")
else
html_col:collect(tabber:str() .. "")
end
local markup_elem = markup_stack:pop()
tabber:undent()
html_col:collect(tabber:str() .. tags.close[markup_elem])
self.list.handled_text = true
end
parse.list_item = function(self)
local tabber = self.list.tabber
local html_col = self.list.html_col
local text_col = self.list.text_col
if self.list.handled_text then
html_col:append("")
html_col:collect(tabber:str() .. "- ")
else
self.list.handled_text = true
end
local text = text_col:gather("\n")
local inlined = parse_inline(text)
html_col:append(text)
end
parse.parse_list2 = function(self, line, html)
local list = self.list
local html_col = list.html_col
local text_col = list.text_col
local markup_stack = list.markup_stack
local tags = self.list_tag
local tabber = self.list.tabber
-- The markup_stack is the base for the current setup.
-- The markup in 'line' is the html we need.
-- The difference between the markup_stack contents and 'line'
-- is the html we need to create.
local s, e, new_markup, line_text = line:find("^([*#]+)%s*(.*)$")
if not s then
self.save_line:push(line)
while not markup_stack:is_empty() do
self:list_close()
end
if html then
html_col:collect(html)
end
return
end
if html then self:error("ERROR: parse_list2() should never be called with a non-nil html value for any line that is a valid list:\n" ..
"line = '" .. line .. "'\n" ..
"html = '" .. html .. "'") end
local cur_markup = markup_stack:concat()
while cur_markup ~= new_markup do
if #new_markup > #cur_markup and new_markup:sub(1,#cur_markup) == cur_markup then
local x = new_markup:sub(#cur_markup +1, #cur_markup +1)
self:list_open(x)
elseif cur_markup ~= new_markup:sub(1, #cur_markup) then
self:list_close()
return self:parse_list2(line)
end
cur_markup = markup_stack:concat()
end
text_col:collect(line_text)
line = self:read_line()
while line and not self:is_blank(line) do
local h
h, html = self:headings(line)
if not h then h, html = self:horiz_rule(line) end
if not h then h, html = self:block_quote(line) end
if not h then h, html = self:preformatted(line) end
if not h then h, html = self:block_comments(line) end
if h then
-- line is now stale, get a new line
line = self:read_line()
else
h = self.is_list(line)
end
if h then break end
text_col:collect(line)
line = self:read_line()
end
self:list_item()
while line and self:is_blank(line) do
line = self:read_line()
end
return self:parse_list2(line, html)
end
parse.pgraph = new_stack()
parse.parse = function(self)
local process = function()
local para = self.pgraph:gather("\n")
if not para or para == "" then return end
local html = self:wrap_block_class("p", para)
self.body:push(html)
end
repeat
local line = self:read_line()
if not line then break end
local h, html = self:headings(line)
if not h then h, html = self:horiz_rule(line) end
if not h then h, html = self:block_quote(line) end
if not h then h, html = self:preformatted(line) end
if not h then h, html = self:block_comments(line) end
if not h then h, html = self:parse_list(line) end
if h then
process()
self.body:push(html)
else
self.pgraph:push(line)
end
until false -- Loop forever
process()
local html = self.body:gather("\n")
return html
end
return parse
end -- end of new_parser() which is a parser generator
test = {}
test.markup = {
"{{",
"In this file, I will test some of the markup features",
"}}",
"= Hello",
"----",
"== World!",
'""',
"This is some offset block quote",
'""',
"This is the first paragraph,",
"and it spans multiple lines",
"before starting a list",
"* A",
"{{",
"Here I am testing comments within lists",
"}}",
"* B",
"* C",
"*# 1",
"*# 2",
"*# 3",
"and some more text for C.3",
"* D",
"# One",
"#* One A",
"#** One A a",
"#* One B",
"#** One B a",
"# Two",
"* Fresh",
"..forth",
": add ( n n -- N) + ;",
"..",
"",
"And the last paragraph of the markup."
}
test.i = 0
test.read = function(self) self.i = self.i + 1; return self.markup[self.i] end
print("-- The input 'file':")
for n, l in ipairs(test.markup) do
print (l)
end
print("-- Processing ...")
p = new_parser(test.read, test)
print("-- The output:")
print (p.parse(p))