261030.8 · ff50ed91d3d1f98dd346c102059d414c95a57a2b
add colour swatches plug-in
- shows a colour box next to #rgb[a], #rrggbb[aa] and rgb()/rgba() literals
- detects literals in code and strings, skips comments
- supports HTML, XHTML, SVG, Pango, BBCode, Truecolor and xterm256 output
- chains to previously defined Decorate and DecorateLineEnd functions
reading declarations
Changes to plugins/color_swatches.lua. All files
@@ -0,0 +1,277 @@+--[[+Colour swatches: shows a small box filled with the colour next to colour+literals, like VSCode's colour decorators or Vim's hexokinase plug-in.++Recognized literals (in code and strings, but not in comments):+ #rgb, #rgba, #rrggbb, #rrggbbaa+ rgb(r, g, b), rgba(r, g, b, a), rgb(r g b / a) (numbers or percentages)++Supported output formats: HTML, XHTML, SVG, Pango, BBCode, Truecolor and+xterm256 escape sequences.++Can be combined with other plug-ins which define a Decorate function+(e.g. rainbow_brackets.lua).++Note: highlight copies the Decorate function without its upvalues, so all+state is kept in the global table ColorSwatches instead of local variables.+]]++Description="Adds colour swatches next to hex and rgb() colour literals (HTML, SVG, Pango, BBCode, Truecolor, xterm256)."++Categories = {"format", "html", "svg", "pango", "bbcode", "truecolor", "xterm256", "usability" }++function syntaxUpdate(desc)++ if HL_OUTPUT ~= HL_FORMAT_HTML and HL_OUTPUT ~= HL_FORMAT_XHTML+ and HL_OUTPUT ~= HL_FORMAT_SVG and HL_OUTPUT ~= HL_FORMAT_PANGO+ and HL_OUTPUT ~= HL_FORMAT_BBCODE+ and HL_OUTPUT ~= HL_FORMAT_TRUECOLOR and HL_OUTPUT ~= HL_FORMAT_XTERM256 then+ return+ end++ -- do not chain twice if the chunk is evaluated again in the same state+ if ColorSwatches then+ return+ end++ ColorSwatches = {+ SWATCH_CHAR = "\226\150\160", -- U+25A0 BLACK SQUARE++ isEscOutput = (HL_OUTPUT == HL_FORMAT_TRUECOLOR or HL_OUTPUT == HL_FORMAT_XTERM256),++ -- hex literal inside strings: split into "#", "00", "ff00", ... tokens+ hexBuf = nil, -- collected hex digits after a "#", nil if inactive+ hexNextCol = 0, -- column where the next adjacent hex token has to start++ -- rgb()/rgba(): split into name, parentheses, numbers and separators+ fnState = nil, -- nil, "name" (rgb/rgba seen), "args" (inside parentheses)+ fnArgs = {}, -- collected arguments as { value, isPercent }++ prevDecorate = Decorate,+ prevDecorateLineEnd = DecorateLineEnd,+ }++ function ColorSwatches.resetFunction()+ ColorSwatches.fnState = nil+ ColorSwatches.fnArgs = {}+ end++ function ColorSwatches.clamp(v, lo, hi)+ if v < lo then return lo end+ if v > hi then return hi end+ return v+ end++ -- returns r, g, b (0-255) and alpha (0-1), or nil+ function ColorSwatches.parseHex(hex)+ local n = #hex+ if string.find(hex, "^%x+$") == nil or (n ~= 3 and n ~= 4 and n ~= 6 and n ~= 8) then+ return nil+ end+ if n <= 4 then+ hex = string.gsub(hex, "(%x)", "%1%1")+ end+ local r = tonumber(string.sub(hex, 1, 2), 16)+ local g = tonumber(string.sub(hex, 3, 4), 16)+ local b = tonumber(string.sub(hex, 5, 6), 16)+ local a = 1+ if #hex == 8 then+ a = tonumber(string.sub(hex, 7, 8), 16) / 255+ end+ return r, g, b, a+ end++ function ColorSwatches.rgbFromArgs(args)+ local clamp = ColorSwatches.clamp+ if #args ~= 3 and #args ~= 4 then+ return nil+ end+ local c = {}+ for i = 1, 3 do+ local v = args[i].value+ if args[i].isPercent then v = v * 2.55 end+ c[i] = math.floor(clamp(v, 0, 255) + 0.5)+ end+ local a = 1+ if args[4] then+ a = args[4].value+ if args[4].isPercent then a = a / 100 end+ a = clamp(a, 0, 1)+ end+ return c[1], c[2], c[3], a+ end++ function ColorSwatches.swatch(r, g, b, a)+ local box = ColorSwatches.SWATCH_CHAR++ if HL_OUTPUT == HL_FORMAT_HTML or HL_OUTPUT == HL_FORMAT_XHTML then+ return string.format('<span style="display:inline-block;width:0.8em;height:0.8em;'+ ..'margin:0 0.25em;vertical-align:middle;border:1px solid #808080;'+ ..'background-color:rgba(%d,%d,%d,%.3g)"></span>', r, g, b, a)+ end++ local hex = string.format("#%02x%02x%02x", r, g, b)+ if HL_OUTPUT == HL_FORMAT_SVG then+ return ' <tspan fill="'..hex..'">'..box..'</tspan> '+ elseif HL_OUTPUT == HL_FORMAT_PANGO then+ return ' <span foreground="'..hex..'">'..box..'</span> '+ elseif HL_OUTPUT == HL_FORMAT_BBCODE then+ return ' [color='..hex..']'..box..'[/color] '+ elseif HL_OUTPUT == HL_FORMAT_TRUECOLOR then+ return string.format(" \x1B[38;2;%d;%d;%dm", r, g, b)..box+ elseif HL_OUTPUT == HL_FORMAT_XTERM256 then+ local idx = 16 + 36 * math.floor(r * 5 / 255 + 0.5)+ + 6 * math.floor(g * 5 / 255 + 0.5)+ + math.floor(b * 5 / 255 + 0.5)+ return " \x1B[38;5;"..idx.."m"..box+ end+ return ""+ end++ -- tokens returned by Decorate are not escaped by highlight+ function ColorSwatches.escapeToken(token)+ if HL_OUTPUT == HL_FORMAT_HTML or HL_OUTPUT == HL_FORMAT_XHTML+ or HL_OUTPUT == HL_FORMAT_SVG or HL_OUTPUT == HL_FORMAT_PANGO then+ token = string.gsub(token, "&", "&")+ token = string.gsub(token, "<", "<")+ token = string.gsub(token, ">", ">")+ token = string.gsub(token, '"', """)+ end+ return token+ end++ -- finishes a pending hex literal; returns its swatch or nil+ function ColorSwatches.flushHex()+ local cs = ColorSwatches+ local buf = cs.hexBuf+ cs.hexBuf = nil+ if buf then+ local r, g, b, a = cs.parseHex(buf)+ if r then+ return cs.swatch(r, g, b, a)+ end+ end+ return nil+ end++ -- handles rgb()/rgba(); returns a swatch if the closing parenthesis completes a colour+ function ColorSwatches.handleFunction(token)+ local cs = ColorSwatches++ if cs.fnState == nil then+ local name = string.lower(token)+ if name == "rgb" or name == "rgba" then+ cs.fnState = "name"+ end+ return nil+ end++ if cs.fnState == "name" then+ if token == "(" then+ cs.fnState = "args"+ else+ cs.resetFunction()+ end+ return nil+ end++ -- fnState == "args"+ local num, pct = string.match(token, "^(%d*%.?%d+)(%%?)$")+ if num then+ cs.fnArgs[#cs.fnArgs+1] = { value = tonumber(num), isPercent = (pct == "%") }+ elseif token == "%" and #cs.fnArgs > 0 then+ cs.fnArgs[#cs.fnArgs].isPercent = true+ elseif token == ")" then+ local r, g, b, a = cs.rgbFromArgs(cs.fnArgs)+ cs.resetFunction()+ if r then+ return cs.swatch(r, g, b, a)+ end+ elseif token ~= "," and token ~= "/" then+ cs.resetFunction()+ end+ return nil+ end++ -- returns prefix and suffix to be placed around the current token+ function ColorSwatches.getSwatches(token, state, column)+ local cs = ColorSwatches++ if state == HL_BLOCK_COMMENT or state == HL_LINE_COMMENT then+ cs.hexBuf = nil+ cs.resetFunction()+ return nil, nil+ end++ -- complete hex literal in a single token, e.g. CSS numbers+ local hex = string.match(token, "^#(%x+)$")+ if hex then+ local pending = cs.flushHex()+ cs.resetFunction()+ local r, g, b, a = cs.parseHex(hex)+ return pending, r and cs.swatch(r, g, b, a) or nil+ end++ -- collect adjacent hex digit tokens after a "#"+ if cs.hexBuf and column == cs.hexNextCol and string.find(token, "^%x+$") then+ cs.hexBuf = cs.hexBuf..token+ cs.hexNextCol = column + #token+ return nil, nil+ end++ -- any other token terminates a pending hex literal+ local pending = cs.flushHex()++ if token == "#" then+ cs.hexBuf = ""+ cs.hexNextCol = column + 1+ cs.resetFunction()+ return pending, nil+ end++ return pending, cs.handleFunction(token)+ end++ function Decorate(token, state, kwGroupID, lineHasStmt, lineno, column)+ local cs = ColorSwatches+ local prevRes+ if cs.prevDecorate then+ prevRes = cs.prevDecorate(token, state, kwGroupID, lineHasStmt, lineno, column)+ end++ -- whitespace is reported as empty tokens and must not break up literals+ if #token > 0 and string.find(token, "^%s*$") == nil then+ local prefix, suffix = cs.getSwatches(token, state, column or -1)++ -- terminal colours cannot be restored after a prefix, so append it instead+ if prefix and cs.isEscOutput then+ suffix = prefix..(suffix or "")+ prefix = nil+ end++ if prefix or suffix then+ return (prefix or "")..(prevRes or cs.escapeToken(token))..(suffix or "")+ end+ end++ if prevRes then+ return prevRes+ end+ end++ -- a hex literal may end the line (e.g. unterminated strings)+ function DecorateLineEnd(lineNumber)+ local cs = ColorSwatches+ local res = cs.flushHex() or ""+ if cs.prevDecorateLineEnd then+ res = res..(cs.prevDecorateLineEnd(lineNumber) or "")+ end+ if #res > 0 then+ return res+ end+ end+end++Plugins={+ { Type="lang", Chunk=syntaxUpdate },+}