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 "" 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+%-%-%-", "
    ") --
    %2
    text = text:gsub("\1IMG:(%d+)\1", function (n) local img = images[tonumber(n)] return ("
    \"%s\"
    "):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