-- Support for : Colors | Styles -- Minetest : Yes | No -- IRC : Yes | Yes -- Discord : No | Yes -- Assumptions : -- 1. IRC users only use IRC formatting chars and don't rely on Markdown -- 2. Minetest users only use Minetest color codes + Markdown -- 3. Discord users only rely on Markdown -- Note : Markdown means Discord Markdown -- Resulting conversions : -- Minetest colors -> IRC colors -- IRC colors -> Minetest colors -- Discord styles -> IRC styles -- IRC styles -> Discord styles -- Resources : -- https://modern.ircdocs.horse/formatting.html -- https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline- -- https://github.com/minetest/minetest/blob/master/doc/lua_api.txt -- Notes: -- While code ("`code`") to IRC is straightforward (monospace), monospace to code is undefined behavior (concat code blocks? no code blocks at all?) local function is_digit(char) return char >= "0" and char <= "9" end local md_escape={ ["*"]=true, ["_"]=true, ["~"]=true, ["`"]=true, ["\\"]=true, ["|"]=true } function escape_markdown(text) local res={} for i=1, text:len() do local char=text:sub(i,i) if md_escape[char] then table.insert(res, "\\") end table.insert(res, char) end return table.concat(res) end -- minetest characters: color starter local minetest_color_starter=string.char(0x1b) -- irc characters local irc_escape_code=string.char(0x02)..string.char(0x02) local irc_disable=string.char(0x0F) local irc_color_reverse=string.char(0x16) local irc_color_starter=string.char(0x03) local irc_hex_color_starter=string.char(0x04) local irc_bold=string.char(0x02) local irc_italics=string.char(0x1D) local irc_underlined=string.char(0x1F) local irc_strikethrough=string.char(0x1F) local irc_monospace=string.char(0x11) -- Converts Discord-style Markdown to IRC format local irc_style_to_md={ [irc_bold]="**", -- Bold [irc_italics]="*", -- Italics [irc_underlined]="__", -- Underlined [irc_strikethrough]="~~", -- Strikethrough } local md_style_to_irc=modlib.table.flip(irc_style_to_md) local irc_escape={ [irc_bold]=true, [irc_italics]=true, [irc_underlined]=true, [irc_strikethrough]="~~", [irc_disable]=true, [irc_color_reverse]=true, [irc_monospace]=true } local function skip_color_code(text, i) for j=1,2 do if is_digit(text:sub(i+1,i+1)) then i=i+1 end end if text:sub(i+1,i+1) == "," and is_digit(text:sub(i+1,i+1)) then i=i+1 if is_digit(text:sub(i+1,i+1)) then i=i+1 end end return i end function escape_irc(text) local res={} local i=1 while i <= text:len() do local char=text:sub(i,i) if char == irc_color_starter then i=skip_color_code(text, i) elseif char == irc_hex_color_starter then i=i+6 elseif not irc_escape[char] then table.insert(res, char) end i=i+1 end return table.concat(res) end md_style_to_irc["_"]=md_style_to_irc["*"] local markdown_trie = trie.new() for tag, toggle in pairs(md_style_to_irc) do trie.insert(markdown_trie, tag, {name=tag, reversed=tag, opening=toggle, closing=toggle, space_sensitive=true}) end trie.insert(markdown_trie, "||", {name="||", reversed="||", opening=irc_color_starter.."01,01", closing=irc_color_starter, escape_func=is_digit}) trie.insert(markdown_trie, "***", {name="***", reversed="***", opening=irc_italics..irc_bold, closing=irc_italics..irc_bold, space_sensitive=true, conversion={ ["*"]={name="**", reversed=irc_italics.."**", opening=irc_italics..irc_bold, closing=irc_bold}, ["**"]={name="*", reversed=irc_bold.."*", opening=irc_italics..irc_bold, closing=irc_italics} } }) trie.insert(markdown_trie, "___", {reversed="___", opening=irc_underlined..irc_italics, closing=irc_underlined..irc_italics, space_sensitive=true, conversion={ ["_"]={name="__", reversed=irc_italics.."__", opening=irc_italics..irc_underlined, closing=irc_underlined}, ["__"]={name="_", reversed=irc_underlined.."_", opening=irc_underlined..irc_italics, closing=irc_italics} } }) local markdown_code_tag="`" -- Strips Markdown for Minetest. Won't strip invalid Markdown (like 1*1=2) function strip_markdown(markdown) local i=1 local res={} local tags={} while i <= markdown:len() do local char = markdown:sub(i,i) local function process() if char == markdown_code_tag then local closing = markdown:find("[^\\]`", i+1) if closing then table.insert(res, markdown:sub(i+1, closing)) i=closing+1 return end elseif char == "\\" and md_escape[markdown:sub(i+1,i+1)] then table.insert(res, markdown:sub(i+1,i+1)) i=i+1 return elseif char == " " then if res[#res] and res[#res].space_sensitive then res[#res] = res[#res].reversed table.remove(tags) end else local tag, offset = trie.find_longest(markdown_trie, markdown, i) if tag then for index, tag_index in modlib.table.rpairs(tags) do local conversion = res[tag_index].conversion and res[tag_index].conversion[tag.name] if res[tag_index].name == tag.name or conversion then if tag.space_sensitive and markdown:sub(i-1,i-1) == " " then table.insert(res, tag.reversed) else local index_2 = #tags while index_2 > index do local tag_index_2 = tags[index_2] res[tag_index_2] = res[tag_index_2].reversed table.remove(tags) index_2 = index_2 - 1 end if conversion then res[tag_index] = conversion else table.remove(tags) res[tag_index] = "" end end i=offset return end end table.insert(res, tag) table.insert(tags, #res) i=offset return end end table.insert(res, char) end process() i=i+1 end for _, tag in pairs(tags) do if res[tag].reversed then res[tag]=res[tag].reversed end end return table.concat(res) end function markdown_to_irc(markdown) local i=1 local res={} local tags={} while i <= markdown:len() do local char = markdown:sub(i,i) local function process() if char == markdown_code_tag then local closing = markdown:find("[^\\]`", i+1) if closing then table.insert(res, irc_monospace) table.insert(res, markdown:sub(i+1, closing)) table.insert(res, irc_monospace) i=closing+1 return end elseif char == "\\" and md_escape[markdown:sub(i+1,i+1)] then table.insert(res, markdown:sub(i+1,i+1)) i=i+1 return elseif char == " " then if res[#res] and res[#res].space_sensitive then res[#res] = res[#res].reversed table.remove(tags) end else local tag, offset = trie.find_longest(markdown_trie, markdown, i) if tag then for index, tag_index in modlib.table.rpairs(tags) do local conversion = res[tag_index].conversion and res[tag_index].conversion[tag.name] if res[tag_index].name == tag.name or conversion then if tag.space_sensitive and markdown:sub(i-1,i-1) == " " then table.insert(res, tag.reversed) else local index_2 = #tags while index_2 > index do local tag_index_2 = tags[index_2] res[tag_index_2] = res[tag_index_2].reversed table.remove(tags) index_2 = index_2 - 1 end if conversion then res[tag_index] = conversion else table.remove(tags) res[tag_index] = res[tag_index].opening end table.insert(res, tag.closing) if tag.escape_func and tag.escape_func(markdown:sub(offset+1,offset+1)) then table.insert(res, irc_escape_code) end end i=offset return end end table.insert(res, tag) table.insert(tags, #res) i=offset return end end table.insert(res, char) end process() i=i+1 end for _, tag in pairs(tags) do if res[tag].reversed then res[tag]=res[tag].reversed end end return table.concat(res) end -- Converts Markdown to IRC function markdown_to_irc(markdown) local i=1 local res={} local tags={} while i <= markdown:len() do local char = markdown:sub(i,i) local function process() if char == markdown_code_tag then local closing = markdown:find("[^\\]`", i+1) if closing then table.insert(res, irc_monospace) table.insert(res, markdown:sub(i+1, closing)) table.insert(res, irc_monospace) i=closing+1 return end elseif char == "\\" and md_escape[markdown:sub(i+1,i+1)] then table.insert(res, markdown:sub(i+1,i+1)) i=i+1 return elseif char == " " then if res[#res] and res[#res].space_sensitive then res[#res] = res[#res].reversed table.remove(tags) end else local tag, offset = trie.find_longest(markdown_trie, markdown, i) if tag then for index, tag_index in modlib.table.rpairs(tags) do local conversion = res[tag_index].conversion and res[tag_index].conversion[tag.name] if res[tag_index].name == tag.name or conversion then if tag.space_sensitive and markdown:sub(i-1,i-1) == " " then table.insert(res, tag.reversed) else local index_2 = #tags while index_2 > index do local tag_index_2 = tags[index_2] res[tag_index_2] = res[tag_index_2].reversed table.remove(tags) index_2 = index_2 - 1 end if conversion then res[tag_index] = conversion else table.remove(tags) res[tag_index] = res[tag_index].opening end table.insert(res, tag.closing) if tag.escape_func and tag.escape_func(markdown:sub(offset+1,offset+1)) then table.insert(res, irc_escape_code) end end i=offset return end end table.insert(res, tag) table.insert(tags, #res) i=offset return end end table.insert(res, char) end process() i=i+1 end for _, tag in pairs(tags) do if res[tag].reversed then res[tag]=res[tag].reversed end end return table.concat(res) end -- Converts IRC text modifiers to Discord markdown, escaping included function irc_to_markdown(irc) local res={} local active={} local i=1 while i <= irc:len() do local char=irc:sub(i,i) local md=irc_style_to_md[char] if md then while irc:sub(i+1)==" " do table.insert(res, " ") i=i+1 end local insert = true for index, open_md in modlib.table.rpairs(active) do if open_md == md then table.remove(active, index) local i=#res while res[i] == " " do i=i-1 end table.insert(res, i+1, md) insert = false break end end if insert then table.insert(active, md) table.insert(res, md) end elseif char == irc_disable then for _, md in modlib.table.rpairs(active) do table.insert(res, md) end active={} elseif md_escape[char] then table.insert(res, "\\") elseif char == irc_color_starter then --color if irc:sub(i+2, i+2) == "," then if is_digit(irc:sub(i+1, i+1)) and irc:sub(i+1, i+1) == irc:sub(i+3, i+3) and not is_digit(irc:sub(i+4, i+4)) then table.insert(active, "||") table.insert(res, md) end elseif irc:sub(i+3, i+3) == "," then local fg, bg = irc:sub(i+1, i+2), irc:sub(i+4, i+5) if is_digit(irc:sub(i+1, i+1)) and is_digit(irc:sub(i+2, i+2)) and fg == bg and fg ~= "99" then table.insert(active, "||") table.insert(res, md) end else for index, open_md in modlib.table.rpairs(active) do if open_md == "||" then table.remove(active, index) local j=#res while res[j] == " " do j=j-1 end table.insert(res, j+1, md) end end end i=skip_color_code(irc, i) else table.insert(res, char) end i=i+1 end for _, thing in modlib.table.rpairs(active) do table.insert(res, thing) end return table.concat(res) end -- Converts IRC colors to Minetest colors, background colors included local user_defined_colors={'000000','0000FF','00FF00','FF0000', '654321','FF00FF','FFA500','FFFF00', '90EE90','00FFFF','E0FFFF','ADD8E6','FF69B4','808080','D3D3D3'} user_defined_colors[0]='FFFFFF' local conversion_table={[16]='470000',[17]='472100',[18]='474700',[19]='324700',[20]='004700',[21]='00472c',[22]='004747',[23]='002747',[24]='000047',[25]='2e0047',[26]='470047',[27]='47002a',[28]='740000',[29]='743a00',[30]='747400',[31]='517400',[32]='007400',[33]='007449',[34]='007474',[35]='004074',[36]='000074',[37]='4b0074',[38]='740074',[39]='740045',[40]='b50000',[41]='b56300',[42]='b5b500',[43]='7db500',[44]='00b500',[45]='00b571',[46]='00b5b5',[47]='0063b5',[48]='0000b5',[49]='7500b5',[50]='b500b5',[51]='b5006b',[52]='ff0000',[53]='ff8c00',[54]='ffff00',[55]='b2ff00',[56]='00ff00',[57]='00ffa0',[58]='00ffff',[59]='008cff',[60]='0000ff',[61]='a500ff',[62]='ff00ff',[63]='ff0098',[64]='ff5959',[65]='ffb459',[66]='ffff71',[67]='cfff60',[68]='6fff6f',[69]='65ffc9',[70]='6dffff',[71]='59b4ff',[72]='5959ff',[73]='c459ff',[74]='ff66ff',[75]='ff59bc',[76]='ff9c9c',[77]='ffd39c',[78]='ffff9c',[79]='e2ff9c',[80]='9cff9c',[81]='9cffdb',[82]='9cffff',[83]='9cd3ff',[84]='9c9cff',[85]='dc9cff',[86]='ff9cff',[87]='ff94d3',[88]='000000',[89]='131313',[90]='282828',[91]='363636',[92]='4d4d4d',[93]='656565',[94]='818181',[95]='9f9f9f',[96]='bcbcbc',[97]='e2e2e2',[98]='ffffff'} function hex_to_table(color) return {tonumber(color:sub(1, 2), 16), tonumber(color:sub(3, 4), 16), tonumber(color:sub(5, 6), 16)} end function table_to_hex(color) return string.format("%02X", color[1])..string.format("%02X", color[2])..string.format("%02X", color[3]) end modlib.table.add_all(conversion_table, user_defined_colors) local reversed = modlib.table.flip(conversion_table) for k, v in pairs(reversed) do reversed[string.upper(k)]=v reversed[string.lower(k)]=v end reversed.FFFFFF=0 reversed.ffffff=0 function irc_to_minetest(irc) local fg, background local reversed = false local rope={} local i=1 while i <= irc:len() do local char = irc:sub(i,i) if char == irc_color_starter then local j=i+1 if is_digit(irc:sub(j,j)) then fg=irc:byte(j,j)-string.byte("0") i=j j=j+1 if is_digit(irc:sub(j,j)) then fg=fg*10+(irc:byte(j,j)-string.byte("0")) i=j j=j+1 end local bg if irc:sub(j,j) == "," and is_digit(irc:sub(j+1,j+1)) then local bg=irc:byte(j,j)-string.byte("0") i=j j=j+1 if is_digit(irc:sub(j,j)) then bg=bg*10+(irc:byte(j,j)-string.byte("0")) i=j end end if bg then -- no proper implementation for background escape sequences yet - see the "Escape sequences" part of the Lua API -- table.insert(rope, minetest.get_background_escape_sequence("#"..conversion_table[bg])) background = bg end if reversed then table.insert(rope, minetest.get_color_escape_sequence("#"..conversion_table[bg])) else table.insert(rope, minetest.get_color_escape_sequence("#"..conversion_table[fg])) end else table.insert(rope, minetest.get_color_escape_sequence("#FFFFFF")) end elseif char == irc_disable then table.insert(rope, minetest.get_color_escape_sequence("#FFFFFF")) background = nil elseif char == irc_color_reverse then reversed = not reversed if background then table.insert(rope, minetest.get_color_escape_sequence("#"..conversion_table[background])) -- no proper implementation for background escape sequences yet - see the "Escape sequences" part of the Lua API -- table.insert(rope, minetest.get_background_escape_sequence("#"..conversion_table[fg])) background, fg = fg, background end elseif not irc_style_to_md[char] and char ~= irc_monospace then table.insert(rope, char) end i=i+1 end return table.concat(rope) end local color_conv = (bridges.irc and bridges.irc.convert_minetest_colors) or "hex" if color_conv == "hex" then -- always use hex, no matter what function convert_color_to_irc(color) return nil, irc_hex_color_starter..color end elseif color_conv == "hex_safer" then function convert_color_to_irc(color) -- prefer simple colors local rev = reversed[color] if rev then return ",", irc_color_starter..((rev < 10 and "0") or "")..tostring(rev) end return nil, irc_hex_color_starter..color end elseif color_conv == "disabled" then function convert_color_to_irc(color) return error("Color conversion to IRC is disabled. Check your config.") end else local color_chooser if color_conv == "safest" then local closest_color_basic = closest_color_finder(modlib.table.process(user_defined_colors, function(k, color) return hex_to_table(color) end)) color_chooser = closest_color_basic else local closest_color_extended = closest_color_finder(modlib.table.process(conversion_table, function(k, color) return hex_to_table(color) end)) color_chooser = closest_color_extended end function convert_color_to_irc(color) local rev = reversed[color] if rev and rev <= 15 then return function(c) return c=="," end, irc_color_starter..((rev < 10 and "0") or "")..tostring(reversed[color]) end local closest = reversed[table_to_hex(color_chooser(hex_to_table(color)))] if not closest then error(table_to_hex(color_chooser(hex_to_table(color)))) end return function(c) return c=="," end, irc_color_starter..((closest < 10 and "0") or "")..tostring(closest) end end local old_convert_color_to_irc = convert_color_to_irc function convert_color_to_irc(color) if color:lower() == "ffffff" then -- treat white as color reset, by default. TODO think of doing the same for close colors (can we?) return function(c) return c >= "0" and c <= "9" end, irc_color_starter end return old_convert_color_to_irc(color) end function minetest_to_irc(message) local i=1 local res={} while i <= message:len() do local color if message:sub(i,i) == minetest_color_starter and message:sub(i+1, i+4) == "(c@#" and message:sub(i+11, i+11) == ")" then is_color = true color = message:sub(i+5, i+10) for j=1, 6 do local c = color:sub(j, j):lower() if not (c >= "0" and c <= "9") and not (c >= "a" and c <= "f") then color = nil break end end end if color then local needs_escape, color = convert_color_to_irc(color) table.insert(res, color) if needs_escape and needs_escape(message:sub(i+12, i+12)) then table.insert(res, irc_escape_code) end i=i+11 else table.insert(res, message:sub(i,i)) end i=i+1 end return table.concat(res) end