if not modules then modules = { } end modules ['luaotfload-colors'] = {
    version   = 2.200,
    comment   = "companion to luaotfload.lua (font color)",
    author    = "Khaled Hosny, Elie Roux, Philipp Gesang",
    copyright = "Luaotfload Development Team",
    license   = "GNU GPL v2"
}

--[[doc--
buggy coloring with the pre_output_filter when expansion is enabled
    · tfmdata for different expansion values is split over different objects
    · in ``initializeexpansion()``, chr.expansion_factor is set, and only
      those characters that have it are affected
    · in constructors.scale: chr.expansion_factor = ve*1000 if commented out
      makes the bug vanish
--doc]]--


local color_callback = config.luaotfload.color_callback
if not color_callback then
    --- maybe this would be better as a method: "early" | "late"
    color_callback = "pre_linebreak_filter"
--  color_callback = "pre_output_filter" --- old behavior, breaks expansion
end


local newnode               = node.new
local nodetype              = node.id
local traverse_nodes        = node.traverse
local insert_node_before    = node.insert_before
local insert_node_after     = node.insert_after

local stringformat          = string.format
local stringgsub            = string.gsub
local stringfind            = string.find
local stringsub             = string.sub

local otffeatures           = fonts.constructors.newfeatures("otf")
local identifiers           = fonts.hashes.identifiers
local registerotffeature    = otffeatures.register

local add_color_callback --[[ this used to be a global‽ ]]

--[[doc--
This converts a single octet into a decimal with three digits of
precision. The optional second argument limits precision to a single
digit.
--doc]]--

--- string -> bool? -> string
local hex_to_dec = function (hex,one) --- one isn’t actually used anywhere ...
    if one then
        return stringformat("%.1g", tonumber(hex, 16)/255)
    else
        return stringformat("%.3g", tonumber(hex, 16)/255)
    end
end

--[[doc--
Color string validator / parser.
--doc]]--

local lpeg           = require"lpeg"
local lpegmatch      = lpeg.match
local C, Cg, Ct, P, R, S = lpeg.C, lpeg.Cg, lpeg.Ct, lpeg.P, lpeg.R, lpeg.S

local digit16        = R("09", "af", "AF")
local octet          = C(digit16 * digit16)

local p_rgb          = octet * octet * octet
local p_rgba         = p_rgb * octet
local valid_digits   = C(p_rgba + p_rgb) -- matches eight or six hex digits

local p_Crgb         = Cg(octet/hex_to_dec, "red") --- for captures
                     * Cg(octet/hex_to_dec, "green")
                     * Cg(octet/hex_to_dec, "blue")
local p_Crgba        = p_Crgb * Cg(octet/hex_to_dec, "alpha")
local extract_color  = Ct(p_Crgba + p_Crgb)

--- string -> (string | nil)
local sanitize_color_expression = function (digits)
    digits = tostring(digits)
    local sanitized = lpegmatch(valid_digits, digits)
    if not sanitized then
        luaotfload.warning(
            "“%s” is not a valid rgb[a] color expression", digits)
        return nil
    end
    return sanitized
end

--[[doc--
``setcolor`` modifies tfmdata.properties.color in place
--doc]]--

--- fontobj -> string -> unit
---
---         (where “string” is a rgb value as three octet
---         hexadecimal, with an optional fourth transparency
---         value)
---
local setcolor = function (tfmdata, value)
    local sanitized  = sanitize_color_expression(value)
    local properties = tfmdata.properties

    if sanitized then
        properties.color = sanitized
        add_color_callback()
    end
end

registerotffeature {
    name        = "color",
    description = "color",
    initializers = {
        base = setcolor,
        node = setcolor,
    }
}


--- something is carried around in ``res``
--- for later use by color_handler() --- but what?

local res --- <- state of what?

--- float -> unit
local function pageresources(alpha)
    local res2
    if not res then
       res = "/TransGs1<</ca 1/CA 1>>"
    end
    res2 = stringformat("/TransGs%s<</ca %s/CA %s>>",
                        alpha, alpha, alpha)
    res  = stringformat("%s%s",
                        res,
                        stringfind(res, res2) and "" or res2)
end

--- we store results of below color handler as tuples of
--- push/pop strings
local color_cache = { } --- (string, (string * string)) hash_t

--- string -> (string * string)
local hex_to_rgba = function (digits)
    if not digits then
        return
    end

    --- this is called like a thousand times, so some
    --- memoizing is in order.
    local cached = color_cache[digits]
    if not cached then
        local push, pop
        local rgb = lpegmatch(extract_color, digits)
        if rgb.alpha then
            pageresources(rgb.alpha)
            push = stringformat(
                        "/TransGs%g gs %s %s %s rg",
                        rgb.alpha,
                        rgb.red,
                        rgb.green,
                        rgb.blue)
            pop  = "0 g /TransGs1 gs"
        else
            push = stringformat(
                        "%s %s %s rg",
                        rgb.red,
                        rgb.green,
                        rgb.blue)
            pop  = "0 g"
        end
        color_cache[digits] = { push, pop }
        return push, pop
    end

    return cached[1], cached[2]
end

--- Luatex internal types

local glyph_t           = nodetype("glyph")
local hlist_t           = nodetype("hlist")
local vlist_t           = nodetype("vlist")
local whatsit_t         = nodetype("whatsit")
local page_insert_t     = nodetype("page_insert")
local sub_box_t         = nodetype("sub_box")

--- node -> nil | -1 | color‽
local lookup_next_color
lookup_next_color = function (head) --- paragraph material
    for n in traverse_nodes(head) do
        local n_id = n.id

        if n_id == glyph_t then
            local n_font
            if  identifiers[n_font]
            and identifiers[n_font].properties
            and identifiers[n_font].properties.color
            then
                return identifiers[n.font].properties.color
            else
                return -1
            end

        elseif n_id == vlist_t or n_id == hlist_t or n_id == sub_box_t then
            local r = lookup_next_color(n.list)
            if r then
                return r
            end

        elseif n_id == whatsit_t or n_id == page_insert_t then
            return -1
        end
    end
    return nil
end

--[[doc--
While the second argument and second returned value are apparently
always nil when the function is called, they temporarily take string
values during the node list traversal.
--doc]]--

local cnt = 0
--- node -> string -> int -> (node * string)
local node_colorize
node_colorize = function (head, current_color, next_color)
    for n in traverse_nodes(head) do
        local n_id      = n.id
        local nextnode  = n.next

        if n_id == hlist_t or n_id == vlist_t or n_id == sub_box_t then
            local next_color_in = lookup_next_color(nextnode) or next_color
            n.list, current_color = node_colorize(n.list, current_color, next_color_in)

        elseif n_id == glyph_t then
            cnt = cnt + 1
            local tfmdata = identifiers[n.font]

            --- colorization is restricted to those fonts
            --- that received the “color” property upon
            --- loading (see ``setcolor()`` above)
            if tfmdata and tfmdata.properties  and tfmdata.properties.color then
                local font_color = tfmdata.properties.color
--                luaotfload.info(
--                    "n: %d; %s; %d %s, %s",
--                    cnt, utf.char(n.char), n.font, "<TRUE>", font_color)
                if font_color ~= current_color then
                    local pushcolor = hex_to_rgba(font_color)
                    local push      = newnode(whatsit_t, 8)
                    push.mode       = 1
                    push.data       = pushcolor
                    head            = insert_node_before(head, n, push)
                    current_color   = font_color
                end
                local next_color_in = lookup_next_color (nextnode) or next_color
                if next_color_in ~= font_color then
                    local _, popcolor = hex_to_rgba(font_color)
                    local pop         = newnode(whatsit_t, 8)
                    pop.mode          = 1
                    pop.data          = popcolor
                    head              = insert_node_after(head, n, pop)
                    current_color     = nil
                end

--            else
--                luaotfload.info(
--                    "n: %d; %s; %d %s",
--                    cnt, utf.char(n.char), n.font, "<FALSE>")
            end
        end
    end
    return head, current_color
end

--- node -> node
local color_handler = function (head)
    -- check if our page resources existed in the previous run
    -- and remove it to avoid duplicating it later
    if res then
        local r = "/ExtGState<<" .. res .. ">>"
        tex.pdfpageresources = stringgsub(tex.pdfpageresources, r, "")
    end
    local new_head = node_colorize(head, nil, nil)
    -- now append our page resources
    if res and stringfind(res, "%S") then -- test for non-empty string
        local r = "/ExtGState<<" .. res .. ">>"
        tex.pdfpageresources = tex.pdfpageresources..r
    end
    return new_head
end

local color_callback_activated = 0

--- unit -> unit
add_color_callback = function ( )
    if color_callback_activated == 0 then
        luatexbase.add_to_callback(color_callback,
                                   color_handler,
                                   "luaotfload.color_handler")
        color_callback_activated = 1
    end
end

-- vim:tw=71:sw=4:ts=4:expandtab