local keywords = {
    "and", "break", "do", "else", "elseif", "end", --[["false",]] "for", "function",
    "goto", "if", "in", --[["local", "nil",]] "not", "or", "repeat", "return",
    "then", --[["true",]] "until", "while"
}
local keyword_set = {}
for _, kw in ipairs(keywords) do
    keyword_set[kw] = true
end

local keyword_value_set = {
    ["true"] = true,
    ["false"] = true,
    ["nil"] = true,
    ["local"] = true,
    ["math.huge"] = true
}

local function colorize(str, color)
    return "<style color="..(theme.styles["syntax_"..color] or "#fff")..">"..str.."</style>"
end

function syntax_highlight(code)
    local out = ""
    local pos = 1
    while pos <= #code do
        local matched = false

        -- Whitespace
        local ws_start, ws_end = code:find("^%s+", pos)
        if ws_start then
            out = out..colorize(code:sub(ws_start, ws_end), "whitespace")
            pos = ws_end + 1
            matched = true
        end

        -- Single-line comment
        if not matched then
            local com_start, com_end = code:find("^%-%-[^\n]*", pos)
            if com_start then
                out = out..colorize(code:sub(com_start, com_end), "comment")
                pos = com_end + 1
                matched = true
            end
        end

        -- String (double-quoted, simple escape handling)
        if not matched then
            local str_start, str_end = code:find('^"[^"]*"', pos)
            if str_start then
                out = out..colorize(code:sub(str_start, str_end), "string")
                pos = str_end + 1
                matched = true
            end
        end

        -- String (single-quoted, similar)
        if not matched then
            local str_start, str_end = code:find("^'[^']*", pos)
            if str_start then
                out = out..colorize(code:sub(str_start, str_end), "string")
                pos = str_end + 1
                matched = true
            end
        end

        -- Documentation placeholders. These aren't in Lua, but that means they
        -- can be highlighted without worrying about confusing alternative meanings.
        if not matched then
            local str_start, str_end = code:find('^\\?<[^>]*>', pos)
            if str_start then
                out = out..colorize(code:sub(str_start, str_end), "placeholder")
                pos = str_end + 1
                matched = true
            end
        end

        -- Number (basic integer/float)
        if not matched then
            local num_start, num_end = code:find("^%d+%.?%d*[eE]?[+-]?%d*", pos)
            if num_start then
                out = out..colorize(code:sub(num_start, num_end), "number")
                pos = num_end + 1
                matched = true
            end
        end

        -- Identifier/Keyword
        if not matched then
            local id_start, id_end, id_text = code:find("^([%a_][%w_]*)", pos)
            if id_start then
                if keyword_set[id_text] then
                    out = out..colorize(id_text, "keyword")
                elseif keyword_value_set[id_text] then
                    out = out..colorize(id_text, "keyword_value")
                else
                    out = out..colorize(id_text, "identifier")
                end
                pos = id_end + 1
                matched = true
            end
        end

        -- Operators/Punctuation (catch-all for non-alphanumeric)
        if not matched then
            local op_start, op_end = code:find("^[^%s%a%d_\"'{}(),%][]", pos)
            if op_start then
                local op = code:sub(op_start, op_end)
                out = out..colorize(op, op == "-" and code:sub(op_end +1, op_end +2):find "^%d" and "number" or "operator")
                pos = op_end + 1
                matched = true
            end
        end

        -- If nothing matched, advance to avoid infinite loop (e.g., invalid char)
        if not matched then
            out = out..colorize(code:sub(pos, pos), "generic")
            pos = pos + 1
        end
    end
    return out
end
