local babycode = {}
local string_trim = require("lapis.util").trim
local emoji = require("lib.babycode-emoji")
local function s_split(s, delimiter, max_matches, trim, allow_empty)
local result = {}
if s == "" then
return result
end
trim = trim == nil and true or trim
local tr = function(subj)
if trim then return string_trim(subj) else return subj end
end
max_matches = max_matches or -1
allow_empty = allow_empty == nil and true or allow_empty
if delimiter == "" then
for i=1, #s do
local c = s:sub(i, 1)
if allow_empty or c ~= "" then
table.insert(result, c)
if max_matches > 0 and #result == max_matches then
break
end
end
end
return result
end
local current_pos = 1
local delim_len = #delimiter
while true do
if max_matches > 0 and #result >= max_matches then
break
end
---@diagnostic disable-next-line: param-type-mismatch
local start_pos, end_pos = s:find(delimiter, current_pos, true)
if not start_pos then
break
end
local substr = s:sub(current_pos, start_pos - 1)
if allow_empty or substr ~= "" then
table.insert(result, tr(substr))
end
current_pos = end_pos + 1
end
local substr = s:sub(current_pos)
if allow_empty or substr ~= "" then
table.insert(result, tr(substr))
end
return result
end
local function get_list_items(list_body, escape_html)
list_body = list_body:gsub(" +%s*\r?\n", " ")
list_body = list_body:gsub("(%S)(\r?\n\r?\n)\r?\n*", "%1\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 .. "
" .. rendered .. "
"
end
return lis
end
---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"
end
end)
text = text:gsub("%[ul%](.-)%[/ul%]", function(list_body)
return "
" .. get_list_items(list_body, escape_html) .. "
"
end)
text = text:gsub("%[ol%](.-)%[/ol%]", function(list_body)
return "" .. get_list_items(list_body, escape_html) .. ""
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", " ")
text = text:gsub("(%S)(\r?\n\r?\n)\r?\n*", "%1
")
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%]", "%1")
text = text:gsub("%[i%](.-)%[/i%]", "%1")
text = text:gsub("%[s%](.-)%[/s%]", "%1")
-- 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
end
end)
-- replace loose links
text = text:gsub("(https?://[%w-_%.%?%.:/%+=&~%@#%%]+[%w-/])", function(url)
if not text:find(']*>'..url..'') then
return ''..url..''
end
return url
end)
text = text:gsub("\1URL:(%d+)\1", function(n)
local url = url_tags[tonumber(n)]
return ("%s"):format(url.url, url.label)
end)
-- rule
text = text:gsub("\n+%-%-%-", "")
--
text = text:gsub("\1IMG:(%d+)\1", function (n)
local img = images[tonumber(n)]
return ("
"):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 = (""):format(code)
return "
" .. button .. ""..code.."
"
end)
text = text:gsub("\1ICODE:(%d+)\1", function (n)
local code = inline_codes[tonumber(n)]
return "" .. code .. ""
end)
return text
end
return babycode