-- 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