diff options
author | ubq323 <ubq323@ubq323.website> | 2024-10-31 04:02:07 +0000 |
---|---|---|
committer | ubq323 <ubq323@ubq323.website> | 2024-10-31 04:02:07 +0000 |
commit | 664f471c774d6182efb0f9c4d4c9af9857b1bf52 (patch) | |
tree | 3a5194457841cce4f3e2d7acfe0a53e24a2d39cf |
initial commit
-rw-r--r-- | garkup.lua | 116 | ||||
-rw-r--r-- | html.lua | 87 | ||||
-rw-r--r-- | prose.lua | 90 |
3 files changed, 293 insertions, 0 deletions
diff --git a/garkup.lua b/garkup.lua new file mode 100644 index 0000000..2469aa8 --- /dev/null +++ b/garkup.lua @@ -0,0 +1,116 @@ +local html = require'html' +local prose = require'prose' + +local function highlight(code) + -- todo + return html.T.pre(code) +end + +local function mode_code(S) + local lang = S:line():match("^```(%w+) ?(.*)") + + local text = "" + for line in S:lines() do + if line:match("^```") then + break + else + text = text .. line + end + end + S:emit(highlight(text)) +end + +local function mode_list(S) + local items = {} + for line in S:lines() do + if line:match("^%*") then + local content = line:gsub("^%*%s*","") + table.insert(items, html.T.li(prose(S,content))) + else + S:unline(line) + break + end + end + S:emit(html.T.ul(items)) +end + +local function mode_heading(S) + local level, line = S:line():match("^#*()%s*(.*)") + level = level - 1 + S:emit(html.tag('h'..level, prose(S,line))) +end + +local mode_patterns = { + { "^```", mode_code }, + { "^*", mode_list }, + { "^#", mode_heading }, +} + +local function match_mode_pattern(S, line) + for i, row in ipairs(mode_patterns) do + local pattern, mode = row[1], row[2] + if line:match(pattern) then return mode end + end +end + +local function mode_paragraphs(S) + local para = "" + local function finish() + if para:match("%S") then + S:emit(html.T.p(prose(S,para))) + end + para = "" + end + + for line in S:lines() do + local newmode = match_mode_pattern(S, line) + if newmode then + S:unline(line) + finish() + return newmode(S) + elseif line:match("%S") then + para = para .. line + else + finish() + end + end +end + +local function split_lines(text) + local lines = {} + for x in text:gmatch("[^\n]*") do + table.insert(lines, x.."\n") + end + return lines +end + +local State = {} +function State.new(input) + return setmetatable({ + the_lines = split_lines(input) + }, {__index=State}) +end + +function State.emit(self, x) + print(html.html(x)) +end +function State.line(self) + return table.remove(self.the_lines, 1) +end +function State.unline(self, line) + table.insert(self.the_lines, 1, line) +end +function State.lines(self) + return State.line, self +end +function State.remaining(self) + return #self.the_lines > 0 +end +function State.toplevel(self) + while self:remaining() do + mode_paragraphs(self) + end +end + + +State.new(io.read"a"):toplevel() diff --git a/html.lua b/html.lua new file mode 100644 index 0000000..87ecc53 --- /dev/null +++ b/html.lua @@ -0,0 +1,87 @@ +local function fmt_attrs(attrs) + local function fmt_attr(k,v) + if v == true then + return k + else + if type(v) == "table" then v = table.concat(v," ") end + return ('%s="%s"'):format(k,v) + end + end + + local o = "" + for k,v in pairs(attrs) do + o = o .. " " .. fmt_attr(k,v) + end + return o +end + +local html +local function fmt_tag(tag) + local attrs = tag.attrs and fmt_attrs(tag.attrs) or "" + local selfclosing = (tag.body == "") + local sameline = type(tag.body) == "string" or (type(tag.body) == "table" and tag.body.tag) + local maybenl = sameline and "" or "\n" + if selfclosing then + return ("<%s%s/>"):format(tag.tname,attrs) + else + return ("<%s%s>%s%s</%s>"):format(tag.tname,attrs,maybenl,html(tag.body),tag.tname) + end +end + +local function tag(tname, a1, a2) + -- tag(tname,body) or tag(tname,attrs,body) + local body, attrs + if a2 then attrs=a1 body=a2 else attrs=nil body=a1 end + return setmetatable({tag=true,tname=tname,attrs=attrs,body=body},{__tostring=fmt_tag}) +end + +-- instead of tag('ul', {x, y, z}) +-- you can do T.ul{ x, y, z} +local T = setmetatable({}, {__index=function(_,tname) + return function (...) + return tag(tname, ...) + end +end}) + +local function safe(s) + -- marks s as safe, doesn't escape it + assert(type(s) == "string","can only mark string as safe") + return setmetatable({safe=true,s=s},{__tostring=function(x)return x.s end}) +end + +local function escape(s) + s=s:gsub("&","&") + s=s:gsub("<","<") + s=s:gsub(">",">") + s=s:gsub('"',""") + return safe(s) +end + + +html = function (x) + if type(x) == "string" then + return escape(x) + elseif type(x) == "table" then + if x.safe then + -- safestr. already escaped + return x + elseif x.tag then + -- it's a tag + return safe( tostring(x) ) + else + -- just a regular list + local o = "" + for _,item in ipairs(x) do + o = o .. tostring(html(item)) .. "\n" + end + return safe(o) + end + end +end + +return { + html = html, + tag = tag, + safe = safe, + T = T, +} diff --git a/prose.lua b/prose.lua new file mode 100644 index 0000000..ceadec0 --- /dev/null +++ b/prose.lua @@ -0,0 +1,90 @@ +local html = require'html' + +local extensions = {} + +local function prose(S, text) + -- this may be a hack + local replacements = {} + local SO, SI = "\x0e", "\x0f" + -- if debugview then SO,SI = "\x1b[7m", "\x1b[0m" end + local function emplace(s) + table.insert(replacements, s) + return SO .. #replacements .. SI + end + + -- no way to match 'after ws or at start of string' + -- so, just add ws to start and end of string + -- this may also be a hack + text = ' ' .. text .. ' ' + + local function simple_sub(delim, tagname) + return { + "(%s)"..delim..'(.-)'..delim..'(%W)', -- wo wimple + function(a,x,b) return a..emplace(html.T[tagname](x))..b end + } + end + + local function extension(str) + local verb, rest = str:match("^{(%w+);%s*(.*)}$") + if verb then + return emplace(extensions[verb](S, rest)) + end + local verbonly = str:match("^{(%w+)}$") + if verbonly then + return emplace(extensions[verbonly](S)) + end + error("invalid extension syntax: "..str) + end + + local subs = { + { "%b{}", extension }, + simple_sub('%*', 'strong'), + simple_sub('_', 'em'), + simple_sub('`', 'code'), + } + + for i, row in ipairs(subs) do + local pattern,replacement = row[1], row[2] + text = text:gsub(pattern,replacement) + end + + text = text:gsub("^%s*",""):gsub("%s*$","") + -- print(text) + local insects = {""} + local n = 1 + while n <= #text do + local _, nn, id = text:find('^'..SO:gsub("%[","%%[").."(%d+)"..SI:gsub("%[","%%["), n) + if nn then + table.insert(insects, replacements[tonumber(id)]) + table.insert(insects, "") + n = nn + 1 + else + local c = text:sub(n,n) + -- print("'"..c.."'") + insects[#insects] = insects[#insects] .. c + n = n + 1 + end + end + + -- for i,n in ipairs(insects) do print(i,type(n),n) end + + return insects +end + +extensions.fn = function(S,text) + S.footnotes = S.footnotes or {} + local n = #S.footnotes + 1 + local link_id = "fnref_"..n + local note_id = "fn_"..n + S.footnotes[n] = html.T.li({id=note_id}, { + prose(S,text), html.safe" ", + html.T.a({href="#"..link_id, role="doc-backlink"}, html.safe"↩︎")}) + return html.T.sup({id=link_id}, html.T.a({href="#"..note_id, role='doc-noteref'}, '('..n..')')) +end + +extensions.fns = function(S) + if not S.footnotes then return "" end + return {html.T.hr{}, html.T.ol(S.footnotes)} +end + +return prose |