-- Syntax highlighting implementation for Hilbish local colors = require "lunacolors" local hilbish = require "hilbish" local fs = require "fs" local utils = require ".utils" local M = {} local SQ = 0x27 -- U+0027 APORSTROPHE local DQ = 0x22 -- U+0022 QUOTATION MARK local SP = 0x20 -- U+0020 SPACE local HT = 0x09 -- U+0009 CHARACTER TABULATION local LF = 0x0A -- U+000A LINE FEED (LF) local CR = 0x0D -- U+000D CARRIAGE RETURN (CR) local BS = 0x5C -- U+005C BACKSLASH -- sh builtins local sh_builtins = { "admin", "aliasar", "asa", "at", "awk", "basename", "batch", "bc", "bg", "c99", "cal", "cat", "cd", "cflow", "chgrp", "chmod", "chown", "cksum", "cmp", "comm", "command", "compress", "cp", "crontab", "csplit", "ctags", "cut", "cxref", "date", "dd", "delta", "df", "diff", "dirname", "du", "echo", "ed", "env", "ex", "expand", "expr", "false", "fc", "fg", "file", "find", "fold", "fort77", "fuser", "gencat", "get", "getconf", "getopts", "grep", "hash", "head", "iconv", "id", "ipcrm", "ipcs", "jobs", "join", "kill", "lex", "link", "ln", "locale", "localedef", "logger", "logname", "lp", "ls", "m4", "mailx", "make", "man", "mesg", "mkdir", "mkfifo", "more", "mv", "newgrp", "nice", "nl", "nm", "nohup", "od", "paste", "patch", "pathchk", "pax", "pr", "printf", "prs", "ps", "pwd", "qalter", "qdel", "qhold", "qmove", "qmsg", "qrerun", "qrls", "qselect", "qsig", "qstat", "qsub", "read", "renice", "rm", "rmdel", "rmdir", "sact", "sccs", "sed", "sh", "sleep", "sort", "split", "strings", "strip", "stty", "tabs", "tail", "talk", "tee", "test", "time", "touch", "tput", "tr", "true", "tsort", "tty", "type", "ulimit", "umask", "unalias", "uname", "uncompress", "unexpand", "unget", "uniq", "unlink", "uucp", "uudecode", "uuencode", "uustat", "uux", "val", "vi", "wait", "wc", "what", "who", "write", "xargs", "yacc", "zcat" } -- sh keywords local sh_keywords = { "if", "then", "else", "elif", "fi", "case", "esac", "for", "select", "while", "until", "do", "done", "in", "function", "time", "coproc" } -- Determine if this string is a significant keyword, and if so, what it is function M.is_cmd(str) if utils.contains(sh_builtins, str) then return "builtin" elseif utils.contains(sh_keywords, str) then return "keyword" elseif hilbish.which(str) ~= nil then return "cmd" else -- Try to use this as a filesystem glob -- This may fail with an error, so we need to make sure that -- doesn't happen first. str = str:gsub("~", os.getenv("HOME")) if pcall(fs.glob, str) then local globs = fs.glob(str) if #globs > 0 then return "file" end end end return nil end -- Crude sh lexer function M.lex_sh(s) local state = nil local escape = false local result = {} local point = 0 local index = 0 -- Go through the string for i = 1, #s do index = i local c = s:byte(i) local v = string.char(c) -- Single quoted string state if state == SQ then if c == SQ then state = nil result[#result + 1] = { string.sub(s, point, i), "string" } point = i + 1 end -- Double quoted string state elseif state == DQ then if escape then escape = false else if c == DQ then state = nil result[#result + 1] = { string.sub(s, point, i), "string" } point = i + 1 elseif c == BS then escape = true end end -- Default state else if escape then escape = false elseif c == SP or c == HT or c == LF or c == CR then if point ~= i then result[#result + 1] = { string.sub(s, point, i - 1), "other" } end result[#result + 1] = { v, "whitespace" } point = i + 1 elseif c == DQ then state = DQ if point ~= i then result[#result + 1] = { string.sub(s, point, i - 1), "other" } end point = i elseif c == SQ then state = SQ if point ~= i then result[#result + 1] = { string.sub(s, point, i - 1), "other" } end point = i elseif c == BS then escape = true end end end -- If we are still in a string then say as such if state ~= nil then result[#result + 1] = { string.sub(s, point, index), "string" } -- Otherwise, return the rest as a regular token else if point ~= index then result[#result + 1] = { string.sub(s, point, index), "other" } else local c = s:byte(index) -- If the final character isn't a space, render it anyway -- This is a side effect of the space handling code. TODO: fix. if c ~= SP and c ~= HT and c ~= LF and c ~= CR then result[#result + 1] = { string.sub(s, point, index), "other" } end end end return result end -- Sort the table's key pairs function M.pairsByKeys(t, f) local a = {} for n in pairs(t) do table.insert(a, n) end table.sort(a, f) local i = 0 local iter = function() i = i + 1 if a[i] == nil then return nil else return a[i], t[a[i]] end end return iter end -- sh highlighter function M.sh(str) -- Parse the string local table = M.lex_sh(str) -- Construct the format string local format_str = "" for _, v in M.pairsByKeys(table) do if v[2] == "string" then format_str = format_str .. "{red}" .. v[1] else local cmd = M.is_cmd(v[1]) if cmd == "cmd" then format_str = format_str .. "{bold}{brightGreen}" .. v[1] .. "{reset}" elseif cmd == "builtin" then format_str = format_str .. "{bold}{brightBlue}" .. v[1] .. "{reset}" elseif cmd == "keyword" then format_str = format_str .. "{bold}{brightMagenta}" .. v[1] .. "{reset}" elseif cmd == "file" then format_str = format_str .. "{bold}{brightYellow}" .. v[1] .. "{reset}" else format_str = format_str .. "{brightWhite}" .. v[1] end end end return colors.format(format_str) end return M