@ 261103.2
current
--[[
Rainbow brackets: colours (), [] and {} by their nesting depth, like
VSCode's bracket pair colorization or Vim's rainbow plug-ins.

Closing brackets without a matching opening bracket are marked in red.

Optional plug-in parameter: comma separated list of hex colours used for the
nesting levels, e.g. --plug-in-param="#ffd700,#da70d6,#179fff"
Without parameter, a palette is chosen based on the theme's canvas colour.

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. color_swatches.lua).

Note: highlight copies the Decorate function without its upvalues, so all
state is kept in the global table RainbowBrackets instead of local variables.
]]

Description="Colours brackets by their nesting depth (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 RainbowBrackets then
    return
  end

  RainbowBrackets = {
    -- VSCode default bracket pair colours
    darkPalette  = { "#ffd700", "#da70d6", "#179fff" },
    lightPalette = { "#0431fa", "#319331", "#7b3814" },
    unmatchedColour = "#ff1212",

    openers = { ["("]=")", ["["]="]", ["{"]="}" },
    closers = { [")"]=true, ["]"]=true, ["}"]=true },
    stack   = {},   -- currently open brackets
    palette = nil,  -- resolved lazily, after the theme chunk stored its value

    prevDecorate = Decorate,
  }

  function RainbowBrackets.parsePalette(param)
    local p = {}
    for c in string.gmatch(param, "#?%x%x%x%x%x%x") do
      if string.sub(c, 1, 1) ~= "#" then c = "#"..c end
      p[#p+1] = string.lower(c)
    end
    return p
  end

  function RainbowBrackets.getPalette()
    local rb = RainbowBrackets
    if rb.palette then return rb.palette end
    if HL_PLUGIN_PARAM and #HL_PLUGIN_PARAM > 0 then
      rb.palette = rb.parsePalette(HL_PLUGIN_PARAM)
    end
    if not rb.palette or #rb.palette == 0 then
      rb.palette = (StoreValue("rbLightCanvas") == "1") and rb.lightPalette or rb.darkPalette
    end
    return rb.palette
  end

  function RainbowBrackets.colourize(token, colour)
    local r = tonumber(string.sub(colour, 2, 3), 16)
    local g = tonumber(string.sub(colour, 4, 5), 16)
    local b = tonumber(string.sub(colour, 6, 7), 16)

    if HL_OUTPUT == HL_FORMAT_HTML or HL_OUTPUT == HL_FORMAT_XHTML then
      return '<span style="color:'..colour..'">'..token..'</span>'
    elseif HL_OUTPUT == HL_FORMAT_SVG then
      return '<tspan fill="'..colour..'">'..token..'</tspan>'
    elseif HL_OUTPUT == HL_FORMAT_PANGO then
      return '<span foreground="'..colour..'">'..token..'</span>'
    elseif HL_OUTPUT == HL_FORMAT_BBCODE then
      return '[color='..colour..']'..token..'[/color]'
    elseif HL_OUTPUT == HL_FORMAT_TRUECOLOR then
      return string.format("\x1B[38;2;%d;%d;%dm", r, g, b)..token
    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"..token
    end
    return token
  end

  function RainbowBrackets.colourForDepth(depth)
    local p = RainbowBrackets.getPalette()
    return p[(depth - 1) % #p + 1]
  end

  -- returns the coloured brackets or nil if the token is not a bracket operator
  function RainbowBrackets.colourBrackets(token, state)
    local rb = RainbowBrackets

    if state ~= HL_OPERATOR then
      return nil
    end

    -- only handle tokens which consist of brackets exclusively, so that no
    -- other operator characters need to be escaped or recoloured
    if string.find(token, "^[%(%)%[%]{}]+$") == nil then
      return nil
    end

    local out = {}
    for ch in string.gmatch(token, ".") do
      if rb.openers[ch] then
        rb.stack[#rb.stack+1] = ch
        out[#out+1] = rb.colourize(ch, rb.colourForDepth(#rb.stack))
      else
        local top = rb.stack[#rb.stack]
        if top and rb.openers[top] == ch then
          out[#out+1] = rb.colourize(ch, rb.colourForDepth(#rb.stack))
          rb.stack[#rb.stack] = nil
        else
          out[#out+1] = rb.colourize(ch, rb.unmatchedColour)
        end
      end
    end
    return table.concat(out)
  end

  function Decorate(token, state, kwGroupID, lineHasStmt, lineno, column)
    local rb = RainbowBrackets
    local prevRes
    if rb.prevDecorate then
      prevRes = rb.prevDecorate(token, state, kwGroupID, lineHasStmt, lineno, column)
    end

    local coloured = rb.colourBrackets(token, state)
    if coloured then
      if prevRes == nil then
        return coloured
      end
      -- keep additions of other plug-ins which were appended to the token
      if string.sub(prevRes, 1, #token) == token then
        return coloured..string.sub(prevRes, #token + 1)
      end
    end

    if prevRes then
      return prevRes
    end
  end
end

function themeUpdate(desc)
  local canvas = Canvas and Canvas.Colour or ""
  local light = "0"
  if string.match(canvas, "^#%x%x%x%x%x%x") then
    local r = tonumber(string.sub(canvas, 2, 3), 16)
    local g = tonumber(string.sub(canvas, 4, 5), 16)
    local b = tonumber(string.sub(canvas, 6, 7), 16)
    -- perceived luminance (ITU-R BT.601)
    if (0.299 * r + 0.587 * g + 0.114 * b) > 128 then
      light = "1"
    end
  end
  StoreValue("rbLightCanvas", light)
end

Plugins={
  { Type="lang", Chunk=syntaxUpdate },
  { Type="theme", Chunk=themeUpdate },
}