commit 35c8a92b5a2a04ffded54eff19f1f793dfde3a6e Author: perro Date: Thu May 11 18:32:59 2023 -0700 Init diff --git a/dog-1.0-0.rockspec b/dog-1.0-0.rockspec new file mode 100644 index 0000000..0c91861 --- /dev/null +++ b/dog-1.0-0.rockspec @@ -0,0 +1,23 @@ +package = "Dog" +version = "1.0-0" +source = { + url = "ssh://git@git.cuates.net:2274/perro/lua-dog.git", + tag = "v1.0.0", +} +description = { + summary = "Lua extensions for lazy dogs", + detailed = [[ + Adds functions to Lua Standard Libraries. + ]], + homepage = "https://git.cuates.net/perro/lua-dog", + license = "GPLv3", +} +dependencies = { + "lua >= 5.4, < 5.5" +} +build = { + type = "builtin", + modules = { + dog = "src/dog.lua" + } +} diff --git a/src/dog.lua b/src/dog.lua new file mode 100644 index 0000000..53e53b0 --- /dev/null +++ b/src/dog.lua @@ -0,0 +1,297 @@ +-- Tries popen +-- @param ... string: Chunks for popen +-- @return boolean, string: Status and output of popen +function io.try(...) + local cmd = table.concat({...}, " ") .. " 2>&1" + local handle = io.popen(cmd) + local output = handle:read("*a") + local status = (handle:close() ~= nil and true or false) + return status, output +end + +-- Gets OS short name +-- It is just intented to know if it is Linux, macOS or Windows. +-- @return string: "linux" or "bsd" or "macos" or "windows" +function os.uname() + local status, output = io.try("uname") + if status then + output = output:gsub(" .*", ""):lower() + if output ~= "linux" and output:match("bsd") ~= nil then + return "bsd" + elseif out ~= "linux" then + return "macos" + end + return "linux" + end + return "windows" +end + +-- Checks if OS is windows +-- @return boolean: Windows or not +function os.iswin() + return os.uname() == "windows" +end + +-- Checks if OS is Unix +-- @return boolean: Unix or not +function os.isunix() + return os.uname() ~= "windows" +end + +-- Gets OS language +-- @return string, string, string: Language, locale and encoding +function os.lang() + local lang = os.getenv("LANG") + if lang ~= nil then + return lang:match("(%w%w)_?(%w?%w?)%.?(.*)") + end + return "en", "US", "UTF-8" +end + +-- Gets an equivalency table between ASCII and Unicode +-- Note: filled on demand. +-- @return table: ASCII-Unicode table equivalency +function utf8.table() + return { + ["a"] = {"á", "à", "ä"}, + ["e"] = {"é", "è", "ë"}, + ["i"] = {"í", "ì", "ï"}, + ["o"] = {"ó", "ò", "ö"}, + ["u"] = {"ú", "ù", "ü"}, + ["n"] = {"ñ"}, + } +end + +-- Checks if string is empty +-- @return boolean: Empty or not +function string:isempty() + return self == '' +end + +-- Changes newlines so everything is in one line +-- @return string: String with formatted newlines as literal "\n" +function string:linearize() + return self:gsub("\n", "\\n") +end + +-- Normalizes string +-- @return string: Normalized string +function string:normalize() + self = self:lower() + for newchar, chars in pairs(utf8.table()) do + for _, oldchar in ipairs(chars) do + self = self:gsub(oldchar, newchar) + end + end + return self +end + +-- Adds indent +-- @param num number: Indent size, 2 by default +-- @param char string: Indent character, space by default +-- @return strin: Indented string +function string:indent(num, char) + num = num or 2 + char = char or " " + char = string.rep(char, num) + return char .. self:gsub("\n", "\n" .. char) +end + +-- Splits the string +-- Note: only support splitting by one character. +-- Could be solved by slicing with find. +-- @param sep string: String separator, space by default +-- @return table: String matches +function string:split(sep) + sep = sep or "%s+" + local parts = {} + for part in self:gmatch("([^" .. sep .. "]+)") do + table.insert(parts, part) + end + return parts +end + +-- Removes spaces at the beginning of the string +-- @return string: Left stripped string +function string:lstrip() + return self:gsub("^%s+", "") +end + +-- Removes spaces at the end of the string +-- @return string: Right stripped string +function string:rstrip() + return self:gsub("%s+$", "") +end + +-- Removes spaces at the beginning and at the end of the string +-- @return string: Stripped string +function string:strip() + self = self:lstrip():rstrip() + return self +end + +-- Alias of strip +function string:trim() + return self:strip() +end + +-- The following are heavily influenced by Python pathlib +-- Check: https://docs.python.org/3/library/pathlib.html + +-- Checks if string is a file or a directory +-- @return boolean: Exists or not +function string:exists() + return os.rename(self, self) ~= nil +end + +-- Checks if string is a file +-- @return boolean: File or not +function string:isfile() + if self:exists() then + return io.open(self, "a+") ~= nil + end + return false +end + +-- Checks if string is a directory +-- @return boolean: Directory or not +function string:isdir() + if self:exists() then + return io.open(self, "a+") == nil + elseif self == "." or self == ".." then + return true + end + return false +end + +-- Reads file content as string +-- @return string or nil: File as string or nil +function string:readtext() + if self:exists() then + return io.open(self):read("*a") + end +end + +-- Read file content as lines +-- @return table: Table of file lines or nil +function string:readlines() + local lines = {} + if self:exists() then + for line in io.open(self):lines() do + table.insert(lines, line) + end + end + return lines +end + +-- Gets file without suffix +-- @return string: File wihtout suffix +function string:stem() + return self:gsub("%.%a+$", "") +end + +-- Gets file extensions +-- @return table: List of file extensions +function string:suffixes() + local suffixes = {} + for suffix in self:gmatch("%.%a+") do + table.insert(suffixes, suffix) + end + return suffixes +end + +-- Gets file final extension +-- @return string: Final file extension +function string:suffix() + local suffixes = self:suffixes() + if suffixes[#suffixes] then + return suffixes[#suffixes] + end + return "" +end + +-- Requires Pandoc +-- Check: https://pandoc.org/lua-filters.html#module-pandoc + +if pandoc ~= nil then + + -- Gets file extension namespace + -- Check: https://pandoc.org/MANUAL.html#general-options + -- If suffix is 'md' or empty, the default is markdown. + -- For the rest, the format is the suffix. + -- @param file string: File name + -- @return string: File extension according to Pandoc format namespaces + function pandoc.getext(file) + local ext = file:suffix():gsub("^.", "") + return ((ext == "md" or ext:isempty()) and "markdown" or ext) + end + + -- Pandoc converter + -- Converts input file name into output format name. + -- If ofile is not nil, writes output file name. + -- If iformat is nil, defaults to ifile extension name. + -- @param ifile string: Input file name + -- @param oformat string: Output format name + -- @param ofile string or nil: Output file name + -- @param iformat string or nil: Input format name + -- @return string: Output file content + function pandoc.convert(ifile, oformat, ofile, iformat) + iformat = (iformat == nil and pandoc.getext(ifile) or iformat) + local doc = pandoc.write(pandoc.read(ifile:readtext(), iformat), oformat) + if ofile ~= nil then + local eol = (os:isunix() and "\n" or "\r\n") + io.open(ofile, "w"):write(doc, eol):close() + end + return doc + end + + -- Stringifies Pandoc content + -- Avoids undesired behavios of pandoc.utils.stringify, such as quotes and + -- backslashes conversions. + -- @param content pandoc.MetaValue: Pandoc content value + -- @return string: Pandoc stringified value + function pandoc.utils.rawstringify(content) + return pandoc.utils.stringify(content:walk { + Plain = function(plain) + table.insert(plain.content, pandoc.Space()) + return plain + end, + Quoted = function(quoted) + local q = (quoted.quotetype == SingleQuoted and '"' or "'") + return pandoc.Str(q .. pandoc.utils.stringify(quoted.content) .. q) + end, + RawInline = function(rawinline) + return pandoc.Str(rawinline.text:gsub("\\n", "\n"):gsub("\\t", "\t")) + end, + SoftBreak = function(softbreak) + return pandoc.Str("\n") + end, + Inline = function(inline) return pandoc.utils.stringify(inline) end, + }) + end + + -- Converts pandoc.Meta to table + -- Note that stringified numbers, such "1", are converted to numbers because + -- Pandoc doesn't make distinctions between strings and numbers. + -- Check: https://pandoc.org/lua-filters.html#type-metavalue + -- @param meta pandoc.Meta: Pandoc MetaValues collection + -- @return table: Pandoc metadata as table + function pandoc.metatotable(meta) + local newmeta = {} + for k, v in pairs(meta) do + if pandoc.utils.type(v) == "table" then + newmeta[k] = pandoc.metatotable(v) + else + if type(v) == "boolean" then + newmeta[k] = v + elseif tonumber(v) then + newmeta[k] = tonumber(v) + else + newmeta[k] = pandoc.utils.rawstringify(v) + end + end + end + return newmeta + end + +end