add babycode parser, courtesy of kaesa
This commit is contained in:
180
lib/babycode.lua
180
lib/babycode.lua
@ -3,6 +3,8 @@ local babycode = {}
|
||||
local string_trim = require("lapis.util").trim
|
||||
local emoji = require("lib.babycode-emoji")
|
||||
|
||||
local Parser = require("lib.babycode-parser")
|
||||
|
||||
local function s_split(s, delimiter, max_matches, trim, allow_empty)
|
||||
local result = {}
|
||||
if s == "" then
|
||||
@ -55,116 +57,94 @@ local function s_split(s, delimiter, max_matches, trim, allow_empty)
|
||||
return result
|
||||
end
|
||||
|
||||
local function get_list_items(list_body, escape_html)
|
||||
list_body = list_body:gsub(" +%s*\r?\n", "<br>")
|
||||
list_body = list_body:gsub("(%S)(\r?\n\r?\n)\r?\n*", "%1\1")
|
||||
local function list(tag, children)
|
||||
local list_body = children:gsub(" +\n", "<br>"):gsub("\n\n+", "\1")
|
||||
local list_items = s_split(list_body, "\1")
|
||||
local lis = ""
|
||||
for _, li in ipairs(list_items) do
|
||||
local rendered = babycode.to_html(li, escape_html)
|
||||
lis = lis .. "<li>" .. rendered .. "</li>"
|
||||
lis = lis .. "<li>" .. li .. "</li>"
|
||||
end
|
||||
return lis
|
||||
return "<" .. tag .. ">" .. lis .. "</" .. tag .. ">"
|
||||
end
|
||||
|
||||
local tags = {
|
||||
b = "<strong>$S</strong>",
|
||||
i = "<em>$S</em>",
|
||||
s = "<del>$S</del>",
|
||||
img = "<div class=\"post-img-container\"><img class=\"block-img\" src=$A alt=%S></div>",
|
||||
url = "<a href=\"$A\">$S</a>",
|
||||
quote = "<blockquote>$S</blockquote>",
|
||||
code = function(children)
|
||||
local is_inline = children:match("\n") == nil
|
||||
if is_inline then
|
||||
return "<code class=\"inline-code\">" .. children .. "</code>"
|
||||
else
|
||||
local t = string_trim(children)
|
||||
local button = ("<button type=button class=\"copy-code\" value=\"%s\">Copy</button>"):format(t)
|
||||
return "<pre><span class=\"copy-code-container\">"..button.."</span><code>"..t.."</code></pre>"
|
||||
end
|
||||
end,
|
||||
ul = function(children)
|
||||
return list("ul", children)
|
||||
end,
|
||||
ol = function(children)
|
||||
return list("ol", children)
|
||||
end,
|
||||
}
|
||||
|
||||
local text_only = {
|
||||
code = true,
|
||||
}
|
||||
|
||||
---renders babycode to html
|
||||
---@param s string input babycode
|
||||
---@param escape_html fun(s: string): string function that escapes html
|
||||
function babycode.to_html(s, escape_html)
|
||||
if not s or s == "" then return "" end
|
||||
local text = escape_html(s)
|
||||
-- extract code blocks and store them as placeholders
|
||||
-- don't want to process bbcode embedded into a code block
|
||||
local code_blocks = {}
|
||||
local inline_codes = {}
|
||||
text = text:gsub("%[code%](.-)%[/code%]", function(code)
|
||||
local is_inline = code:match("\n") == nil
|
||||
if is_inline then
|
||||
table.insert(inline_codes, code)
|
||||
return "\1ICODE:"..#inline_codes.."\1"
|
||||
else
|
||||
-- strip leading and trailing newlines, preserve others
|
||||
local m, _ = code:gsub("^%s*(.-)%s*$", "%1")
|
||||
table.insert(code_blocks, m)
|
||||
return "\1CODE:"..#code_blocks.."\1"
|
||||
---@param html_escape fun(s: string): string function to escape html
|
||||
function babycode.to_html(s, html_escape)
|
||||
-- normalize line ending chars
|
||||
local subj = string_trim(html_escape(s)):gsub("\r\n", "\n"):gsub("\r", "\n")
|
||||
local parser = Parser.new(subj)
|
||||
parser.valid_bbcode_tags = tags
|
||||
parser.valid_emotes = emoji
|
||||
parser.bbcode_tags_only_text_children = text_only
|
||||
|
||||
local elements = parser:parse()
|
||||
local out = ""
|
||||
local function fold(element, nobr)
|
||||
if type(element) == "string" then
|
||||
if nobr then
|
||||
return element
|
||||
end
|
||||
return element:gsub(" +\n", "<br>"):gsub("\n\n+", "<br><br>")
|
||||
end
|
||||
end)
|
||||
|
||||
text = text:gsub("%[ul%](.-)%[/ul%]", function(list_body)
|
||||
return "<ul>" .. get_list_items(list_body, escape_html) .. "</ul>"
|
||||
end)
|
||||
text = text:gsub("%[ol%](.-)%[/ol%]", function(list_body)
|
||||
return "<ol>" .. get_list_items(list_body, escape_html) .. "</ol>"
|
||||
end)
|
||||
|
||||
-- images
|
||||
local images = {}
|
||||
text = text:gsub("%[img=(.-)%](.-)%[/img%]", function (img, alt)
|
||||
table.insert(images, {img = img, alt = alt})
|
||||
return "\1IMG:"..#images.."\1"
|
||||
end)
|
||||
|
||||
-- normalize newlines, attempt #4
|
||||
text = text:gsub(" +%s*\r?\n", "<br>")
|
||||
text = text:gsub("(%S)(\r?\n\r?\n)\r?\n*", "%1<br><br>")
|
||||
|
||||
local url_tags = {}
|
||||
-- replace `[url=https://example.com]Example[/url] tags
|
||||
text = text:gsub("%[url=([^%]]+)%](.-)%[/url%]", function(url, label)
|
||||
table.insert(url_tags, {url = url, label = label})
|
||||
return "\1URL:"..#url_tags.."\1"
|
||||
end)
|
||||
|
||||
-- bold, italics, strikethrough
|
||||
text = text:gsub("%[b%](.-)%[/b%]", "<strong>%1</strong>")
|
||||
text = text:gsub("%[i%](.-)%[/i%]", "<em>%1</em>")
|
||||
text = text:gsub("%[s%](.-)%[/s%]", "<del>%1</del>")
|
||||
|
||||
-- these can be nested, so replace open and closed separately
|
||||
text = text:gsub("%[(/?)quote%]", "<%1blockquote>")
|
||||
|
||||
text = text:gsub(":(.-):", function(code)
|
||||
if emoji[code] then
|
||||
return emoji[code]
|
||||
else
|
||||
return code
|
||||
if element.type == "bbcode" then
|
||||
local c = ""
|
||||
for _, child in ipairs(element.children) do
|
||||
local _nobr = element.name == "code" or element.name == "ul" or element.name == "ol"
|
||||
c = c .. fold(child, _nobr)
|
||||
end
|
||||
local res = ""
|
||||
if type(tags[element.name]) == "string" then
|
||||
res = (tags[element.name]):gsub("%$S", c)
|
||||
if element.attribute then
|
||||
res = res:gsub("%$A", element.attribute)
|
||||
end
|
||||
return res
|
||||
elseif type(tags[element.name]) == "function" then
|
||||
res = tags[element.name](c, element.attribute)
|
||||
end
|
||||
return res
|
||||
elseif element.type == "link" then
|
||||
return "<a href=\""..element.url.."\">"..element.url.."</a>"
|
||||
elseif element.type == "emote" then
|
||||
return emoji[element.name]
|
||||
elseif element.type == "ruler" then
|
||||
return "<hr>"
|
||||
end
|
||||
end)
|
||||
|
||||
-- replace loose links
|
||||
text = text:gsub("(https?://[%w-_%.%?%.:/%+=&~%@#%%]+[%w-/])", function(url)
|
||||
if not text:find('<a[^>]*>'..url..'</a>') then
|
||||
return '<a href="'..url..'">'..url..'</a>'
|
||||
end
|
||||
return url
|
||||
end)
|
||||
|
||||
text = text:gsub("\1URL:(%d+)\1", function(n)
|
||||
local url = url_tags[tonumber(n)]
|
||||
return ("<a href=%s>%s</a>"):format(url.url, url.label)
|
||||
end)
|
||||
|
||||
-- rule
|
||||
text = text:gsub("\n+%-%-%-", "<hr>")
|
||||
|
||||
-- <div class=\"post-img-container\"><img src=%1 alt=%2></div>
|
||||
text = text:gsub("\1IMG:(%d+)\1", function (n)
|
||||
local img = images[tonumber(n)]
|
||||
return ("<div class=\"block-img-container\"><img class=\"block-img\" src=\"%s\" alt=\"%s\"></div>"):format(img.img, img.alt)
|
||||
end)
|
||||
-- replace code block placeholders back with their original contents
|
||||
text = text:gsub("\1CODE:(%d+)\1", function(n)
|
||||
local code = code_blocks[tonumber(n)]
|
||||
local button = ("<button type=button class=\"copy-code\" value=\"%s\">Copy</button>"):format(code)
|
||||
return "<pre><span class=\"copy-code-container\">" .. button .. "</span><code>"..code.."</code></pre>"
|
||||
end)
|
||||
|
||||
text = text:gsub("\1ICODE:(%d+)\1", function (n)
|
||||
local code = inline_codes[tonumber(n)]
|
||||
return "<code class=\"inline-code\">" .. code .. "</code>"
|
||||
end)
|
||||
|
||||
return text
|
||||
end
|
||||
for _, e in ipairs(elements) do
|
||||
out = out .. fold(e, false)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
return babycode
|
||||
|
Reference in New Issue
Block a user