@ 261105.1 · fetching

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/. All files

plugins/color_swatches.lua

new file mode 100644@@ -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, "&", "&amp;")+      token = string.gsub(token, "<", "&lt;")+      token = string.gsub(token, ">", "&gt;")+      token = string.gsub(token, '"', "&quot;")+    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 },+}