580 lines
22 KiB
Lua
580 lines
22 KiB
Lua
-- 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
|