-- -- Line elements -- -- headings:

,

,

, ... "^(=+)%s*(.*)$" one equal sign per hn -- horizontal rule:
"^----*" three or more hyphens -- -- Block elements -- -- Parse precedence: -- -- tables: || -- block-quote:
"" "^\"\"(.*)$" 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 .. "" 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))