demontools.lua
local DemonTools = {}
local cheatConsole = Geyser.MiniConsole:new({name = "DemonnicCheatConsole", width = 4000, wrapWidth = 10000, color = "black"})
cheatConsole:hide()
local function exists(path)
  path = path:gsub([[\$]], "")
  path = path:gsub([[/$]], "")
  return io.exists(path)
end
local function isWindows()
  return package.config:sub(1, 1) == [[\]]
end
local function isDir(path)
  if not exists(path) then return false end
    path = path:gsub([[\]], "/")
  if path:ends("/") then
    path = path:sub(1,-2)
  end
  local ok, err, code = lfs.attributes(path, "mode")
  if ok then
    if ok == "directory" then
      return true
    else
      return false
    end
  end
  return ok, err, code
end
local function mkdir_p(path)
  path = path:gsub("\\", "/")
  local pathTbl = path:split("/")
  local cwd = "/"
  if isWindows() then
    cwd = ""
  end
  for index, dirName in ipairs(pathTbl) do
    if index == 1 then
      cwd = cwd .. dirName
    else
      cwd = cwd .. "/" .. dirName
      cwd = cwd:gsub("//", "/")
    end
    if not table.contains({"/", "C:"}, cwd) and not exists(cwd) then
      local ok, err = lfs.mkdir(cwd)
      if not ok then
        return ok, err
      end
    end
  end
  return true
end
local htmlHeader = [=[  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
    <link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
    <style type="text/css">
      body {
        background-color: black;
        font-family: 'Droid Sans Mono';
        white-space: pre;
        font-size: 12px;
      }
    </style>
  </head>
<body><span>
]=]
local htmlHeaderPattern = [=[  <!DOCTYPE HTML PUBLIC "%-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta http%-equiv="Content%-Type" content="text/html;charset=utf%-8" >
    <link href='http://fonts.googleapis.com/css%?family=Droid%+Sans%+Mono' rel='stylesheet' type='text/css'>
    <style type="text/css">
      body {
        background%-color: black;
        font%-family: 'Droid Sans Mono';
        white%-space: pre;
        font%-size: 12px;
      }
    </style>
  </head>
<body><span>
]=]
-- Internal function, used to turn a string variable name into a value
local function getValueAt(accessString)
  local ok, err = pcall(loadstring("return " .. tostring(accessString)))
  if ok then return err end
  return nil, err
end
-- internal sorting function, sorts first by hue, then luminosity, then value
local function sortColorsByHue(lhs, rhs)
  local lh, ll, lv = unpack(lhs.sort)
  local rh, rl, rv = unpack(rhs.sort)
  if lh < rh then
    return true
  elseif lh > rh then
    return false
  elseif ll < rl then
    return true
  elseif ll > rl then
    return false
  else
    return lv < rv
  end
end
-- internal sorting function, removes _ from snake_case and compares to camelCase
local function sortColorsByName(a, b)
  local aname = string.gsub(string.lower(a.name), "_", "")
  local bname = string.gsub(string.lower(b.name), "_", "")
  return aname < bname
end
-- internal function used to turn sorted colors table into columns
local function chunkify(tbl, num_chunks)
  local pop = function(t)
    return table.remove(t, 1)
  end
  tbl = table.deepcopy(tbl)
  local tblsize = #tbl
  local base_chunk_size = tblsize / num_chunks
  local chunky_chunks = tblsize % num_chunks
  local chunks = {}
  for i = 1, num_chunks do
    local chunk_size = base_chunk_size
    if i <= chunky_chunks then
      chunk_size = chunk_size + 1
    end
    local chunk = {}
    for j = 1, chunk_size do
      chunk[j] = pop(tbl)
    end
    chunks[i] = chunk
  end
  return chunks
end
-- internal function, converts rgb to hsv
-- found at https://github.com/EmmanuelOga/columns/blob/master/utils/color.lua#L89
local function rgbToHsv(r, g, b)
  r, g, b = r / 255, g / 255, b / 255
  local max, min = math.max(r, g, b), math.min(r, g, b)
  local h, s, v
  v = max
  local d = max - min
  if max == 0 then
    s = 0
  else
    s = d / max
  end
  if max == min then
    h = 0
    -- achromatic
  else
    if max == r then
      h = (g - b) / d
      if g < b then
        h = h + 6
      end
    elseif max == g then
      h = (b - r) / d + 2
    elseif max == b then
      h = (r - g) / d + 4
    end
    h = h / 6
  end
  return h, s, v
end
-- internal stepping function, removes some of the noise for a more pleasing sort
-- cribbed from the python on https://www.alanzucconi.com/2015/09/30/colour-sorting/
local function step(r, g, b)
  local lum = math.sqrt(.241 * r + .691 * g + .068 * b)
  local reps = 8
  local h, s, v = rgbToHsv(r, g, b)
  local h2 = math.floor(h * reps)
  local lum2 = math.floor(lum * reps)
  local v2 = math.floor(v * reps)
  if h2 % 2 == 1 then
    v2 = reps - v2
    lum2 = reps - lum2
  end
  return h2, lum2, v2
end
local function calc_luminosity(r, g, b)
  r = r < 11 and r / (255 * 12.92) or ((0.055 + r / 255) / 1.055) ^ 2.4
  g = g < 11 and g / (255 * 12.92) or ((0.055 + g / 255) / 1.055) ^ 2.4
  b = b < 11 and b / (255 * 12.92) or ((0.055 + b / 255) / 1.055) ^ 2.4
  return (0.2126 * r) + (0.7152 * g) + (0.0722 * b)
end
local function include(color, options)
  if options.removeDupes and (string.find(color, "_") and not color:starts("ansi")) or string.find(color:lower(), 'gray') then
    return false
  end
  if options.removeAnsi255 and string.find(color, "ansi_%d%d%d") then
    return false
  end
end
local function echoColor(color, options)
  local rgb = color.rgb
  local fgc = "white"
  if calc_luminosity(unpack(rgb)) > 0.5 then
    fgc = "black"
  end
  local colorString
  if options.justText then
    colorString = string.format('<%s:%s> %-23s<reset> ', color.name, 'black', color.name)
  else
    colorString = string.format('<%s:%s> %-23s<reset> ', fgc, color.name, color.name)
  end
  if options.window == "main" then
    if options.echoOnly then
      cecho(colorString)
    else
      cechoLink(colorString, [[appendCmdLine("]] .. color.name .. [[")]], table.concat(rgb, ", "), true)
    end
  else
    if options.echoOnly then
      cecho(options.window, colorString)
    else
      cechoLink(options.window, colorString, [[appendCmdLine("]] .. color.name .. [[")]], table.concat(rgb, ", "), true)
    end
  end
end
local cnames = {}
local function _color_name(rgb)
  if cnames[rgb] then
    return cnames[rgb]
  end
  local least_distance = math.huge
  local cname = ""
  for name, color in pairs(color_table) do
    local color_distance = math.sqrt((color[1] - rgb[1]) ^ 2 + (color[2] - rgb[2]) ^ 2 + (color[3] - rgb[3]) ^ 2)
    if color_distance < least_distance then
      least_distance = color_distance
      cname = name
    end
  end
  cnames[rgb] = cname
  return cname
end
local function rgbToAnsi(rgb)
  local result = ""
  local cols = rgb:split(":")
  local fore = cols[1]
  local back = cols[2]
  if fore ~= "" then
    local components = fore:split(",")
    result = string.format("%s\27[38:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0")
  end
  if back then
    local components = back:split(",")
    result = string.format("%s\27[48:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0")
  end
  return result
end
local function hexToAnsi(hexcode)
  local result = ""
  local cols = hexcode:split(",")
  local fore = cols[1]
  local back = cols[2]
  if fore ~= "" then
    local components = {tonumber(fore:sub(1, 2), 16), tonumber(fore:sub(3, 4), 16), tonumber(fore:sub(5, 6), 16)}
    result = string.format("%s\27[38:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0")
  end
  if back then
    local components = {tonumber(back:sub(1, 2), 16), tonumber(back:sub(3, 4), 16), tonumber(back:sub(5, 6), 16)}
    result = string.format("%s\27[48:2::%s:%s:%sm", result, components[1] or "0", components[2] or "0", components[3] or "0")
  end
  return result
end
local function hexToRgb(hexcode)
  local result = "<"
  local cols = hexcode:split(",")
  local fore = cols[1]
  local back = cols[2]
  if fore ~= "" then
    local r, g, b = Geyser.Color.parse("#" .. fore)
    result = string.format("%s%s,%s,%s", result, r, g, b)
  end
  if back then
    local r, g, b = Geyser.Color.parse("#" .. back)
    result = string.format("%s:%s,%s,%s", result, r, g, b)
  end
  return string.format("%s>", result)
end
local function rgbToHex(rgb)
  local result = "#"
  local cols = rgb:split(":")
  local fore = cols[1]
  local back = cols[2]
  if fore ~= "" then
    local r, g, b = unpack(string.split(fore, ","))
    result = string.format("%s%02x%02x%02x", result, r, g, b)
  end
  if back then
    local r, g, b = unpack(string.split(back, ","))
    result = string.format("%s,%02x%02x%02x", result, r, g, b)
  end
  return result
end
local function rgbToCname(rgb)
  local result = "<"
  local cols = rgb:split(":")
  local fore = cols[1]
  local back = cols[2]
  if fore ~= "" then
    result = string.format("%s%s", result, _color_name(fore:split(",")))
  end
  if back then
    result = string.format("%s:%s", result, _color_name(back:split(",")))
  end
  return string.format("%s>", result)
end
local function cnameToRgb(cname)
  local result = "<"
  local cols = cname:split(":")
  local fore = cols[1]
  local back = cols[2]
  if fore ~= "" then
    local rgb = color_table[fore] or {0, 0, 0}
    result = string.format("%s%s", result, table.concat(rgb, ","))
  end
  if back then
    local rgb = color_table[back] or {0, 0, 0}
    result = string.format("%s:%s", result, table.concat(rgb, ","))
  end
  return string.format("%s>", result)
end
local function toFromDecho(from, to, text)
  local patterns = {d = _Echos.Patterns.Decimal[1], c = _Echos.Patterns.Color[1], h = _Echos.Patterns.Hex[1]}
  local funcs = {d = {c = rgbToCname, h = rgbToHex, a = rgbToAnsi}, c = {d = cnameToRgb}, h = {d = hexToRgb}}
  local resetCodes = {d = "<r>", h = "#r", c = "<reset>", a = "\27[39;49m"}
  local colorPattern = patterns[from]
  local func = funcs[from][to]
  local reset = resetCodes[to]
  local result = ""
  for str, color, res in rex.split(text, colorPattern) do
    result = result .. str
    if color then
      if color:sub(1, 1) == "|" then
        color = color:gsub("|c", "#")
      end
      if from == "h" then
        result = result .. func(color:sub(2, -1))
      else
        result = result .. func(color:match("<(.+)>"))
      end
    end
    if res then
      result = result .. reset
    end
  end
  return result
end
local function decho2cecho(text)
  return toFromDecho("d", "c", text)
end
local function cecho2decho(text)
  return toFromDecho("c", "d", text)
end
local function decho2hecho(text)
  return toFromDecho("d", "h", text)
end
local function hecho2decho(text)
  return toFromDecho("h", "d", text)
end
local function cecho2ansi(text)
  local dtext = cecho2decho(text)
  return decho2ansi(dtext)
end
local function cecho2hecho(text)
  local dtext = cecho2decho(text)
  return decho2hecho(dtext)
end
local function hecho2cecho(text)
  local dtext = hecho2decho(text)
  return decho2cecho(dtext)
end
local function ansi2decho(tstring)
  local cpattern = [=[\e\[([0-9;:]+)m]=]
  local result = ""
  local resets = {"39;49", "00", "0"}
  local colours = {
    [0] = color_table.ansiBlack,
    [1] = color_table.ansiRed,
    [2] = color_table.ansiGreen,
    [3] = color_table.ansiYellow,
    [4] = color_table.ansiBlue,
    [5] = color_table.ansiMagenta,
    [6] = color_table.ansiCyan,
    [7] = color_table.ansiWhite,
  }
  local lightColours = {
    [0] = color_table.ansiLightBlack,
    [1] = color_table.ansiLightRed,
    [2] = color_table.ansiLightGreen,
    [3] = color_table.ansiLightYellow,
    [4] = color_table.ansiLightBlue,
    [5] = color_table.ansiLightMagenta,
    [6] = color_table.ansiLightCyan,
    [7] = color_table.ansiLightWhite,
  }
  local function colorCodeToRGB(color, parts)
    local rgb
    if color ~= 8 then
      rgb = colours[color]
    else
      if parts[2] == "5" then
        local color_number = tonumber(parts[3])
        if color_number < 8 then
          rgb = colours[color_number]
        elseif color_number > 7 and color_number < 16 then
          rgb = lightColours[color_number - 8]
        else
          rgb = color_table["ansi_" .. color_number]
        end
      elseif parts[2] == "2" then
        local r = parts[4] or 0
        local g = parts[5] or 0
        local b = parts[6] or 0
        if r == "" then
          r = 0
        end
        if g == "" then
          g = 0
        end
        if b == "" then
          b = 0
        end
        rgb = {r, g, b}
      end
    end
    return rgb
  end
  for str, color in rex.split(tstring, cpattern) do
    result = result .. str
    if color then
      if table.contains(resets, color) then
        result = result .. "<r>"
      else
        local parts
        if color:find(";") then
          parts = color:split(";")
        else
          parts = color:split(":")
        end
        local code = parts[1]
        if code:starts("3") then
          color = tonumber(code:sub(2, 2))
          local rgb = colorCodeToRGB(color, parts)
          result = string.format("%s<%s,%s,%s>", result, rgb[1], rgb[2], rgb[3])
        elseif code:starts("4") then
          color = tonumber(code:sub(2, 2))
          local rgb = colorCodeToRGB(color, parts)
          result = string.format("%s<:%s,%s,%s>", result, rgb[1], rgb[2], rgb[3])
        elseif tonumber(code) >= 90 and tonumber(code) <= 97 then
          local rgb = colours[tonumber(code) - 90]
          result = string.format("%s<%s,%s,%s>", result, rgb[1], rgb[2], rgb[3])
        elseif tonumber(code) >= 100 and tonumber(code) <= 107 then
          local rgb = colours[tonumber(code) - 100]
          result = string.format("%s<:%s,%s,%s>", result, rgb[1], rgb[2], rgb[3])
        end
      end
    end
  end
  return result
end
local function decho2ansi(text)
  local colorPattern = _Echos.Patterns.Decimal[1]
  local result = ""
  for str, color, res in rex.split(text, colorPattern) do
    result = result .. str
    if color then
      result = result .. rgbToAnsi(color:match("<(.+)>"))
    end
    if res then
      result = result .. "\27[39;49m"
    end
  end
  return result
end
local function hecho2ansi(text)
  local colorPattern = _Echos.Patterns.Hex[1]
  local result = ""
  for str, color, res in rex.split(text, colorPattern) do
    result = result .. str
    if color then
      if color:sub(1, 1) == "|" then
        color = color:gsub("|c", "#")
      end
      result = result .. hexToAnsi(color:sub(2, -1))
    end
    if res then
      result = result .. "\27[39;49m"
    end
  end
  return result
end
local function ansi2hecho(text)
  local dtext = ansi2decho(text)
  return decho2hecho(dtext)
end
local function displayColors(options)
  options = options or {}
  local optionsType = type(options)
  assert(optionsType == "table", "displayColors(options) argument error: options as table expects, got " .. optionsType)
  options.cols = options.cols or 4
  options.search = options.search or ""
  options.sort = options.sort or false
  if options.removeDupes == nil then
    options.removeDupes = true
  end
  if options.removeAnsi255 == nil then
    options.removeAnsi255 = true
  end
  if options.columnSort == nil then
    options.columnSort = true
  end
  if type(options.window) == "table" then
    options.window = options.window.name
  end
  options.window = options.window or "main"
  local color_table = options.color_table or color_table
  local cols, search, sort = options.cols, options.search, options.sort
  local colors = {}
  for k, v in pairs(color_table) do
    local color = {}
    color.rgb = v
    color.name = k
    color.sort = {step(unpack(v))}
    if include(k, options) and k:lower():find(search) then
      table.insert(colors, color)
    end
  end
  if sort then
    table.sort(colors, sortColorsByName)
  else
    table.sort(colors, sortColorsByHue)
  end
  if options.columnSort then
    local columns_table = chunkify(colors, cols)
    local lines = #columns_table[1]
    for i = 1, lines do
      for j = 1, cols do
        local color = columns_table[j][i]
        if color then
          echoColor(color, options)
        end
      end
      echo(options.window, "\n")
    end
  else
    local i = 1
    for _, k in ipairs(colors) do
      echoColor(k, options)
      if i == cols then
        echo(options.window, "\n")
        i = 1
      else
        i = i + 1
      end
    end
    if i ~= 1 then
      echo(options.window, "\n")
    end
  end
end
local function cecho2string(text)
  local pattern = _Echos.Patterns.Color[2]
  local result = rex.gsub(text, pattern, "")
  return result
end
local function decho2string(text)
  local pattern = _Echos.Patterns.Decimal[2]
  local result = rex.gsub(text, pattern, "")
  return result
end
local function hecho2string(text)
  local pattern = _Echos.Patterns.Hex[2]
  local result = rex.gsub(text, pattern, "")
  return result
end
local function append2decho()
  cheatConsole:clear()
  cheatConsole:appendBuffer()
  local str = copy2decho(cheatConsole.name)
  cheatConsole:clear()
  return str
end
local function html2decho(text)
  text = text:gsub(htmlHeaderPattern, "")
  text = text:gsub("<span style='color: rgb%((%d+,%d+,%d+)%);background: rgb%((%d+,%d+,%d+)%);'>", "<%1:%2>")
  text = text:gsub("<br>", "\n")
  text = text:gsub("</span>", "")
  return text
end
local function html2cecho(text)
  local dtext = html2decho(text)
  return decho2cecho(dtext)
end
local function html2hecho(text)
  local dtext = html2decho(text)
  return decho2hecho(dtext)
end
local function html2ansi(text)
  local dtext = html2decho(text)
  return decho2ansi(dtext)
end
local function html2string(text)
  local dtext = html2decho(text)
  return decho2string(text)
end
local function consoleToString(options)
  options = options or {}
  options.win = options.win or "main"
  options.format = options.format or "d"
  options.start_line = options.start_line or 0
  if options.includeHtmlWrapper == nil then
    options.includeHtmlWrapper = true
  end
  local console_line_count = options.win == "main" and getLineCount() or getLineCount(options.win)
  if not options.end_line then
    options.end_line = console_line_count
  end
  if options.end_line > console_line_count then
    options.end_line = console_line_count
  end
  local start, finish, format = options.start_line, options.end_line, options.format
  local current_x, current_y
  if options.win == "main" then
    current_x = getColumnNumber()
    current_y = getLineNumber()
  else
    current_x = getColumnNumber(options.win)
    current_y = getLineNumber(options.win)
  end
  local function move(x, y)
    if options.win == "main" then
      return moveCursor(x, y)
    else
      return moveCursor(options.win, x, y)
    end
  end
  local function gcl()
    local win, raw
    if options.win ~= "main" then
      win = options.win
      raw = getCurrentLine(win)
    else
      win = nil
      raw = getCurrentLine()
    end
    if raw == "" then
      return ""
    end
    if format == "h" then
      return copy2html(win)
    elseif format == "d" then
      return copy2decho(win)
    elseif format == "a" then
      return decho2ansi(copy2decho(win))
    elseif format == "c" then
      return decho2cecho(copy2decho(win))
    elseif format == "x" then
      return decho2hecho(copy2decho(win))
    elseif format == "r" then
      return raw
    end
  end
  local lines = {}
  if format == "h" and options.includeHtmlWrapper then
    lines[#lines + 1] = htmlHeader
  end
  for line_number = start, finish do
    move(0, line_number)
    lines[#lines + 1] = gcl()
  end
  if format == "h" and options.includeHtmlWrapper then
    lines[#lines + 1] = "</span></body></html>"
  end
  moveCursor(current_x, current_y)
  return table.concat(lines, "\n")
end
local function decho2html(text)
  cheatConsole:clear()
  text = text:gsub("\n", "<br>")
  cheatConsole:decho(text)
  local html = copy2html(cheatConsole.name)
  cheatConsole:clear()
  return html
end
local function cecho2html(text)
  local dtext = cecho2decho(text)
  return decho2html(dtext)
end
local function hecho2html(text)
  local dtext = hecho2decho(text)
  return decho2html(dtext)
end
local function ansi2html(text)
  local dtext = ansi2decho(text)
  return decho2html(dtext)
end
local function scientific_round(number, sigDigits)
  local decimalPlace = string.find(number, "%.")
  if not decimalPlace or (sigDigits < decimalPlace) then
    local numberTable = {}
    local count = 1
    for digit in string.gmatch(number, "%d") do
      table.insert(numberTable, digit)
    end
    local endNumber = ""
    for i, digit in ipairs(numberTable) do
      if i < sigDigits then
        endNumber = endNumber .. digit
      end
      if i == sigDigits then
        if tonumber(numberTable[i + 1]) >= 5 then
          endNumber = endNumber .. digit + 1
        else
          endNumber = endNumber .. digit
        end
      end
      if i > sigDigits and (not decimalPlace or (i < decimalPlace)) then
        endNumber = endNumber .. "0"
      end
    end
    return tonumber(endNumber)
  else
    local decimalDigits = sigDigits - decimalPlace + 1
    return tonumber(string.format("%" .. decimalPlace - 1 .. "." .. decimalDigits .. "f", number))
  end
end
local function roundInt(number)
  return math.floor(number + 0.5)
end
function string.tobyte(self)
  return (self:gsub('.', function(c)
    return string.byte(c)
  end))
end
function string.tocolor(self)
              local strTable = {}
  local part1 = {}
  local part2 = {}
  _ = self:gsub(".", function(c)
    table.insert(strTable, c)
  end)
  for index, value in ipairs(strTable) do
    if (index % 2 == 0) then
      table.insert(part1, value)
    else
      table.insert(part2, value)
    end
  end
  local newStr = string.reverse(table.concat(part1)) .. table.concat(part2)
    math.randomseed(string.cut(newStr:tobyte(), 18))
  local r = math.random(0, 255)
  local g = math.random(0, 255)
  local b = math.random(0, 255)
  math.randomseed(os.time())
  return {r, g, b}
end
local function colorMunge(strForColor, strToEcho, format)
  format = format or 'd'
  local rgb = strForColor:tocolor()
  local color
  if format == "d" then
    color = string.format("<%s>", table.concat(rgb, ","))
  elseif format == "c" then
    color = string.format("<%s>", _color_name(rgb))
  elseif format == "h" then
    color = string.format("#%02x%02x%02x", rgb[1], rgb[2], rgb[3])
  end
  return color .. strToEcho
end
local function colorMungeEcho(strForColor, strToEcho, format, win)
  format = format or "d"
  win = win or "main"
  local str = colorMunge(strForColor, strToEcho, format)
  local func
  if format == "d" then
    func = decho
  end
  if format == "c" then
    func = cecho
  end
  if format == "h" then
    func = hecho
  end
  if win == "main" then
    func(str)
  else
    func(win, str)
  end
end
local function milliToHuman(milliseconds)
  local totalseconds = math.floor(milliseconds / 1000)
  milliseconds = milliseconds % 1000
  local seconds = totalseconds % 60
  local minutes = math.floor(totalseconds / 60)
  local hours = math.floor(minutes / 60)
  minutes = minutes % 60
  return string.format("%02d:%02d:%02d:%03d", hours, minutes, seconds, milliseconds)
end
function DemonTools.chunkify(tbl, num_chunks)
  return chunkify(tbl, num_chunks)
end
function DemonTools.ansi2cecho(text)
  local dtext = ansi2decho(text)
  return decho2cecho(dtext)
end
function DemonTools.ansi2decho(text)
  return ansi2decho(text)
end
function DemonTools.ansi2hecho(text)
  return ansi2hecho(text)
end
function DemonTools.cecho2decho(text)
  return cecho2decho(text)
end
function DemonTools.cecho2ansi(text)
  return cecho2ansi(text)
end
function DemonTools.cecho2hecho(text)
  return cecho2hecho(text)
end
function DemonTools.decho2cecho(text)
  return decho2cecho(text)
end
function DemonTools.decho2ansi(text)
  return decho2ansi(text)
end
function DemonTools.decho2hecho(text)
  return decho2hecho(text)
end
function DemonTools.decho2html(text)
  return decho2html(text)
end
function DemonTools.cecho2html(text)
  return cecho2html(text)
end
function DemonTools.hecho2html(text)
  return hecho2html(text)
end
function DemonTools.ansi2html(text)
  return ansi2html(text)
end
function DemonTools.html2cecho(text)
  return html2cecho(text)
end
function DemonTools.html2decho(text)
  return html2decho(text)
end
function DemonTools.html2ansi(text)
  return html2ansi(text)
end
function DemonTools.html2hecho(text)
  return html2hecho(text)
end
function DemonTools.cecho2string(text)
  return cecho2string(text)
end
function DemonTools.decho2string(text)
  return decho2string(text)
end
function DemonTools.hecho2string(text)
  return hecho2string(text)
end
function DemonTools.html2string(text)
  return html2string(text)
end
function DemonTools.hecho2ansi(text)
  return hecho2ansi(text)
end
function DemonTools.hecho2cecho(text)
  return hecho2cecho(text)
end
function DemonTools.hecho2decho(text)
  return hecho2decho(text)
end
function DemonTools.append2decho()
  return append2decho()
end
function DemonTools.consoleToString(options)
  return consoleToString(options)
end
function DemonTools.displayColors(options)
  return displayColors(options)
end
function DemonTools.roundInt(number)
  local num = tonumber(number)
  local numType = type(num)
  assert(numType == "number", string.format("DemonTools.roundInt(number): number as number expected, got %s", type(number)))
  return roundInt(num)
end
function DemonTools.scientific_round(number, sig_digits)
  return scientific_round(number, sig_digits)
end
function DemonTools.string2color(str)
  return string.tocolor(str)
end
function DemonTools.colorMunge(strForColor, strToColor, format)
  return colorMunge(strForColor, strToColor, format)
end
function DemonTools.colorMungeEcho(strForColor, strToEcho, format, win)
  colorMungeEcho(strForColor, strToEcho, format, win)
end
function DemonTools.milliToHuman(milliseconds, tbl)
  local human = milliToHuman(milliseconds)
  local output
  if tbl then
    local timetbl = human:split(":")
    output = {
      hours = tonumber(timetbl[1]),
      minutes = tonumber(timetbl[2]),
      seconds = tonumber(timetbl[3]),
      milliseconds = tonumber(timetbl[4]),
      original = milliseconds,
    }
  else
    output = human
  end
  return output
end
function DemonTools.getValueAt(variableString)
  return getValueAt(variableString)
end
function DemonTools.exists(path)
  return exists(path)
end
function DemonTools.isDir(path)
  return isDir(path)
end
function DemonTools.isWindows()
  return isWindows()
end
function DemonTools.mkdir_p(path)
  return mkdir_p(path)
end
DemonTools.htmlHeader = htmlHeader
DemonTools.htmlHeaderPattern = htmlHeaderPattern
local echoOutputs = {
  Color = {
    ["\27reset"] = "<reset>",
    ["\27bold"] = "<b>",
    ["\27boldoff"] = "</b>",
    ["\27italics"] = "<i>",
    ["\27italicsoff"] = "</i>",
    ["\27underline"] = "<u>",
    ["\27underlineoff"] = "</u>",
    ["\27strikethrough"] = "<s>",
    ["\27strikethroughoff"] = "</s>",
    ["\27overline"] = "<o>",
    ["\27overlineoff"] = "</o>",
  },
  Decimal = {
    ["\27reset"] = "<r>",
    ["\27bold"] = "<b>",
    ["\27boldoff"] = "</b>",
    ["\27italics"] = "<i>",
    ["\27italicsoff"] = "</i>",
    ["\27underline"] = "<u>",
    ["\27underlineoff"] = "</u>",
    ["\27strikethrough"] = "<s>",
    ["\27strikethroughoff"] = "</s>",
    ["\27overline"] = "<o>",
    ["\27overlineoff"] = "</o>",
  },
  Hex = {
    ["\27reset"] = "#r",
    ["\27bold"] = "#b",
    ["\27boldoff"] = "#/b",
    ["\27italics"] = "#i",
    ["\27italicsoff"] = "#/i",
    ["\27underline"] = "#u",
    ["\27underlineoff"] = "#/u",
    ["\27strikethrough"] = "#s",
    ["\27strikethroughoff"] = "#/s",
    ["\27overline"] = "#o",
    ["\27overlineoff"] = "#/o",
  }
}
local echoPatterns = _Echos.Patterns
local echoProcess = _Echos.Process
function DemonTools.toHTML(t, reset)
  reset = reset or {
    background = { 0, 0, 0 },
    bold = false,
    foreground = { 255, 255, 255 },
    italic = false,
    overline = false,
    reverse = false,
    strikeout = false,
    underline = false
  }
  local format = table.deepcopy(reset)
  local result = getHTMLformat(format)
  for _,v in ipairs(t) do
    local formatChanged = false
    if type(v) == "table" then
      if v.fg then
        format.foreground = {v.fg[1], v.fg[2], v.fg[3]}
        formatChanged = true
      end
      if v.bg then
        format.background = {v.bg[1], v.bg[2], v.bg[3]}
        formatChanged = true
      end
    elseif v == "\27bold" then
      format.bold = true
      formatChanged = true
    elseif v == "\27boldoff" then
      format.bold = false
      formatChanged = true
    elseif v == "\27italics" then
      format.italic = true
      formatChanged = true
    elseif v == "\27italicsoff" then
      format.italic = false
      formatChanged = true
    elseif v == "\27underline" then
      format.underline = true
      formatChanged = true
    elseif v == "\27underlineoff" then
      format.underline = false
      formatChanged = true
    elseif v == "\27strikethrough" then
      format.strikeout = true
      formatChanged = true
    elseif v == "\27strikethroughoff" then
      format.strikeout = false
      formatChanged = true
    elseif v == "\27overline" then
      format.overline = true
      formatChanged = true
    elseif v == "\27overlineoff" then
      format.overline = false
      formatChanged = true
    elseif v == "\27reset" then
      format = table.deepcopy(reset)
      formatChanged = true
    end
    v = formatChanged and getHTMLformat(format) or v
    result = result .. v
  end
  return result
end
local function toEcho(colorType, colors)
  colorType = colorType:lower()
  local result
  if colorType == "hex" then
    local fg,bg = "", ""
    if colors.fg then
      fg = string.format("%02x%02x%02x", unpack(colors.fg))
    end
    if colors.bg then
      bg = string.format(",%02x%02x%02x", unpack(colors.bg))
    end
    result = string.format("#%s%s", fg, bg)
  elseif colorType == "color" then
    local fg,bg = "",""
    if colors.fg then
      fg = closestColor(colors.fg)
    end
    if colors.bg then
      bg = ":" .. closestColor(colors.bg[1], colors.bg[2], colors.bg[3])
    end
    result = string.format("<%s%s>", fg, bg)
  elseif colorType == "decimal" then
    local fg,bg = "", ""
    if colors.fg then
      fg = string.format("%d,%d,%d", unpack(colors.fg))
    end
    if colors.bg then
      bg = string.format(":%d,%d,%d", unpack(colors.bg))
    end
    result = string.format("<%s%s>", fg, bg)
  end
  return result
end
function DemonTools.echoConverter(str, from, to, resetFormat)
  local strType, fromType, toType, resetType = type(str), type(from), type(to), type(resetFormat)
  local errTemplate = "bad argument #{argNum} type ({argName} as string expected, got {argType})"
  local argNum, argName, argType
  local err = false
  if strType ~= "string" then
    argNum = 1
    argName = "str"
    argType = strType
    err = true
  elseif fromType ~= "string" then
    argNum = 2
    argName = "from"
    argType = fromType
    err = true
  elseif toType ~= "string" then
    argNum = 3
    argName = "to"
    argType = toType
    err = true
  elseif resetFormat and resetType ~= "table" then
    argType = resetType
    errTemplate = "bad argument #4 type (optional resetFormat as table of formatting options expected, got {argType})"
    err = true
  end
  if err then
    printError(f(errTemplate), true, true)
  end
  from = from:title()
  local t = echoProcess(str, from)
  if not echoPatterns[from] then
    local msg = "argument #4 (from) must be a valid echo type. Valid types are: " .. table.concat(table.keys(echoPatterns), ",")
  end
  local processed = echoProcess(str, from)
  if to:lower() == "html" then
    return DemonTools.toHTML(processed, resetFormat)
  end
  local outputs = echoOutputs[to]
  if not outputs then
    local msg = "argument #3 (to) must be a valid echo type. Valid types are: " .. table.concat(table.keys(echoOutputs), ",")
    printError(msg, true, true)
  end
  local result = ""
  for _, token in ipairs(processed) do
    local formatter = outputs[token]
    if formatter and token:find("\27") then
      result = result .. formatter
    elseif type(token) == "table" then
      result = result .. toEcho(to, token)
    else
      result = result .. token
    end
  end
  return result
end
return DemonTools