--[[
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 },
}