跳转到内容

模組:PythonScript/Mw/Html

维基百科,自由的百科全书
-- Module:PythonScript/Mw/Html
-- MediaWiki HTML 與 SVG 建構器特化子模組:
-- 實作 MarkupBuilderBase 抽象基底類別、HtmlObject (mw.html)、SvgObject (mw.svg)、
-- 黑名單與 extensionTag 擴充標籤探針防護、以及 subst: 展開防護
local MwHtml = {}

local PyClass = require("Module:PythonScript/Class")
local Runtime = require("Module:PythonScript/Runtime")
local RUtils = require("Module:PythonScript/Runtime/Utils")

local T_FUNC = type(function() end)
local T_TABLE = type({})
local B = Runtime.builtins

-- 由主模組 Module:PythonScript/Mw 透過 MwHtml.bind() 注入的共用參照 (零循環依賴)
local mw_private = nil
local py2lua = nil
local is_none = nil
local check_no_kwargs = nil

function MwHtml.bind(deps)
    mw_private = deps.mw_private
    py2lua = deps.py2lua
    is_none = deps.is_none
    check_no_kwargs = deps.check_no_kwargs
end

-- ==========================================
-- 1. 原生 Lua 實例特徵偵測
-- ==========================================
function MwHtml.is_lua_html(obj)
    if type(obj) ~= T_TABLE or obj.__type ~= nil then return false end
    local ok, res = pcall(function()
        return type(obj.wikitext) == T_FUNC
            and type(obj.addClass) == T_FUNC
            and type(obj.cssText) == T_FUNC
            and type(obj.allDone) == T_FUNC
            and type(obj.getAttr) == T_FUNC
    end)
    return ok and res == true
end

function MwHtml.is_lua_svg(obj)
    if type(obj) ~= T_TABLE or obj.__type ~= nil then return false end
    local ok, res = pcall(function()
        return type(obj.setContent) == T_FUNC
            and type(obj.setAttribute) == T_FUNC
            and type(obj.setImgAttribute) == T_FUNC
            and type(obj.toImage) == T_FUNC
    end)
    return ok and res == true
end

-- ==========================================
-- 2. HTML 標籤黑名單與擴充標籤探針 (Extension Tag Probe)
-- ==========================================
local HTML_TAG_BLACKLIST = {
    a = true, img = true, audio = true, video = true, source = true,
    html = true, body = true, head = true, meta = true, title = true,
    link = true, style = true, script = true, noscript = true, canvas = true,
    iframe = true, embed = true, object = true, param = true, applet = true,
    form = true, input = true, button = true, textarea = true, select = true,
    option = true, label = true, base = true, frame = true, frameset = true
}

local SAFE_HTML_AND_SVG_TAGS = {
    div = true, p = true, span = true, s = true, ul = true, ol = true, li = true,
    sub = true, sup = true, cite = true, dfn = true, abbr = true, code = true,
    th = true, tr = true, td = true, table = true, caption = true, thead = true,
    tbody = true, tfoot = true, col = true, colgroup = true, dl = true, dt = true,
    dd = true, h1 = true, h2 = true, h3 = true, h4 = true, h5 = true, h6 = true,
    hr = true, br = true, wbr = true, b = true, i = true, u = true, small = true,
    strong = true, em = true, mark = true, del = true, ins = true, blockquote = true,
    q = true, kbd = true, samp = true, var = true, time = true, data = true,
    ruby = true, rt = true, rp = true, bdi = true, bdo = true, center = true,
    -- SVG 合法內部標籤 (允許使用 mw.html 建構 SVG 內容)
    svg = true, g = true, path = true, rect = true, circle = true, ellipse = true,
    line = true, polyline = true, polygon = true, text = true, tspan = true,
    defs = true, clippath = true, lineargradient = true, radialgradient = true,
    stop = true, use = true, symbol = true, marker = true, pattern = true,
    mask = true, filter = true, fegaussianblur = true, feoffset = true,
    feblend = true, fecolormatrix = true, animate = true, animatetransform = true,
    mpath = true, desc = true, metadata = true, switch = true, foreignobject = true
}

local COMMON_HTML_ATTRS = {
    id = true, title = true, lang = true, dir = true, style = true, ["class"] = true,
    tabindex = true, role = true, hidden = true, align = true, valign = true,
    width = true, height = true, bgcolor = true, border = true, cellpadding = true,
    cellspacing = true, colspan = true, rowspan = true, scope = true, headers = true,
    abbr = true, cite = true, datetime = true, start = true, reversed = true,
    type = true, value = true,
    x = true, y = true, x1 = true, y1 = true, x2 = true, y2 = true,
    cx = true, cy = true, r = true, rx = true, ry = true, d = true,
    points = true, fill = true, stroke = true, transform = true,
    viewBox = true, viewport = true, xmlns = true
}

local ext_tag_probe_cache = {}

local function validate_html_tag(tag_name)
    if is_none(tag_name) or tag_name == "" then return end
    if type(tag_name) ~= "string" then
        Runtime.raise("TypeError", "tag name must be a string, not '" .. RUtils.get_type_str(tag_name) .. "'")
    end
    local tag_low = tag_name:lower()
    if HTML_TAG_BLACKLIST[tag_low] then
        Runtime.raise("NotImplementedError", "HTML tag '<" .. tag_name .. ">' is not supported in MediaWiki environment")
    end
    if SAFE_HTML_AND_SVG_TAGS[tag_low] then return end

    if ext_tag_probe_cache[tag_low] == nil then
        local is_ext = false
        local ok_f, frame = pcall(mw.getCurrentFrame)
        if ok_f and frame and type(frame.extensionTag) == T_FUNC then
            local ok_e, res = pcall(frame.extensionTag, frame, tag_low, "__MW_HTML_PROBE__")
            if ok_e and type(res) == "string" then
                if res:find("\127", 1, true) or res:find("UNIQ%-", 1) then
                    is_ext = true
                end
            end
        end
        ext_tag_probe_cache[tag_low] = is_ext
    end
    if ext_tag_probe_cache[tag_low] then
        Runtime.raise("NotImplementedError", "'<" .. tag_name .. ">' is a MediaWiki extension tag, not a standard HTML tag; please use frame.extensionTag('" .. tag_name .. "', ...) instead")
    end
end

-- ==========================================
-- 3. MarkupBuilderBase、HtmlObject 與 SvgObject 類別建構
-- ==========================================
local MarkupBuilderBaseClass, HtmlObjectClass, HtmlClassProxyClass, HtmlCssProxyClass
local SvgObjectClass, SvgImgAttributesClass

local function ensure_markup_classes()
    if MarkupBuilderBaseClass then return end

    -- [A] 抽象基底類別 MarkupBuilderBase (禁止直接 new)
    MarkupBuilderBaseClass = PyClass.make_class("MarkupBuilderBase", { B.object }, {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot instantiate abstract base class 'MarkupBuilderBase' directly")
                end
            }
        }
    })

    -- [B] HtmlClassProxy (綁定於 htmlObj.class_,禁止直接 new)
    local html_class_proxy_dict = {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot create 'HtmlClassProxy' instances directly; access via htmlObj.class_ or htmlObj.addClass()")
                end
            }
        },
        __call = {
            __type = "builtin_method", __name = "__call__",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "addClass")
                local priv = mw_private[args[1]]
                if not priv then Runtime.raise("TypeError", "invalid HtmlClassProxy instance") end
                if not is_none(args[2]) then
                    priv.lua_html:addClass(RUtils.to_str(args[2], false))
                    if priv.sync_svg then priv.sync_svg() end
                end
                return priv.owner
            end
        },
        add = {
            __type = "builtin_method", __name = "add",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "class.add")
                local priv = mw_private[args[1]]
                if not priv then Runtime.raise("TypeError", "invalid HtmlClassProxy instance") end
                if not is_none(args[2]) then
                    priv.lua_html:addClass(RUtils.to_str(args[2], false))
                    if priv.sync_svg then priv.sync_svg() end
                end
                return priv.owner
            end
        },
        __iadd__ = {
            __type = "builtin_method", __name = "__iadd__",
            __call = function(args)
                local priv = mw_private[args[1]]
                if not is_none(args[2]) then
                    priv.lua_html:addClass(RUtils.to_str(args[2], false))
                    if priv.sync_svg then priv.sync_svg() end
                end
                return args[1]
            end
        },
        __str__ = {
            __type = "builtin_method", __name = "__str__",
            __call = function(args)
                local priv = mw_private[args[1]]
                return (priv and priv.lua_html:getAttr("class")) or ""
            end
        },
        __repr__ = {
            __type = "builtin_method", __name = "__repr__",
            __call = function(args)
                local priv = mw_private[args[1]]
                local cls_str = (priv and priv.lua_html:getAttr("class")) or ""
                return string.format("<HtmlClassProxy class=%s>", RUtils.to_str(cls_str, true))
            end
        }
    }
    html_class_proxy_dict.__call__ = html_class_proxy_dict.__call
    HtmlClassProxyClass = PyClass.make_class("HtmlClassProxy", { B.object }, html_class_proxy_dict)

    -- [C] HtmlCssProxy (綁定於 htmlObj.css,禁止直接 new)
    local html_css_proxy_dict = {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot create 'HtmlCssProxy' instances directly; access via htmlObj.css")
                end
            }
        },
        __call = {
            __type = "builtin_method", __name = "__call__",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "css")
                local priv = mw_private[args[1]]
                if not priv then Runtime.raise("TypeError", "invalid HtmlCssProxy instance") end
                if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" then
                    local tbl = {}
                    for _, p in ipairs(args[2].pairs) do
                        local k = RUtils.to_str(p.key, false)
                        local v = is_none(p.value) and nil or RUtils.to_str(p.value, false)
                        tbl[k] = v
                        priv.css_cache[k] = v
                    end
                    priv.lua_html:css(tbl)
                elseif #args == 3 then
                    local k = RUtils.to_str(args[2], false)
                    local v = is_none(args[3]) and nil or RUtils.to_str(args[3], false)
                    priv.lua_html:css(k, v)
                    priv.css_cache[k] = v
                else
                    Runtime.raise("TypeError", "css() takes 1 dict argument or (name, value) arguments")
                end
                if priv.sync_svg then priv.sync_svg() end
                return priv.owner
            end
        },
        __getattr__ = {
            __type = "builtin_method", __name = "__getattr__",
            __call = function(args)
                local priv = mw_private[args[1]]
                local k = tostring(args[2]):gsub("_", "-")
                local v = priv and (priv.css_cache[k] or priv.css_cache[args[2]])
                return v ~= nil and v or B.None
            end
        },
        __setattr__ = {
            __type = "builtin_method", __name = "__setattr__",
            __call = function(args)
                local priv = mw_private[args[1]]
                if not priv then Runtime.raise("TypeError", "invalid HtmlCssProxy instance") end
                local k = tostring(args[2]):gsub("_", "-")
                local v = is_none(args[3]) and nil or RUtils.to_str(args[3], false)
                priv.lua_html:css(k, v)
                priv.css_cache[k] = v
                if priv.sync_svg then priv.sync_svg() end
                return B.None
            end
        },
        __getitem__ = {
            __type = "builtin_method", __name = "__getitem__",
            __call = function(args)
                local priv = mw_private[args[1]]
                local k = RUtils.to_str(args[2], false)
                local v = priv and priv.css_cache[k]
                return v ~= nil and v or B.None
            end
        },
        __setitem__ = {
            __type = "builtin_method", __name = "__setitem__",
            __call = function(args)
                local priv = mw_private[args[1]]
                local k = RUtils.to_str(args[2], false)
                local v = is_none(args[3]) and nil or RUtils.to_str(args[3], false)
                priv.lua_html:css(k, v)
                priv.css_cache[k] = v
                if priv.sync_svg then priv.sync_svg() end
                return B.None
            end
        },
        __str__ = {
            __type = "builtin_method", __name = "__str__",
            __call = function(args)
                local priv = mw_private[args[1]]
                return (priv and priv.lua_html:getAttr("style")) or ""
            end
        },
        __repr__ = {
            __type = "builtin_method", __name = "__repr__",
            __call = function(args)
                local priv = mw_private[args[1]]
                local st = (priv and priv.lua_html:getAttr("style")) or ""
                return string.format("<HtmlCssProxy style=%s>", RUtils.to_str(st, true))
            end
        }
    }
    html_css_proxy_dict.__call__ = html_css_proxy_dict.__call
    HtmlCssProxyClass = PyClass.make_class("HtmlCssProxy", { B.object }, html_css_proxy_dict)

    -- [D] HtmlObject 主類別
    local HTML_RESERVED_METHODS = {
        node = true, wikitext = true, newline = true, tag = true,
        attr = true, getAttr = true, addClass = true, css = true,
        cssText = true, done = true, allDone = true, class_ = true
    }

    local function get_html_priv(obj, caller)
        local priv = type(obj) == T_TABLE and mw_private[obj] or nil
        if not priv or priv.kind ~= "html" then
            Runtime.raise("TypeError", (caller or "HtmlObject") .. " requires a valid 'HtmlObject' instance")
        end
        return priv
    end

    local html_obj_dict = {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot create 'HtmlObject' instances directly; use mw.html.create()")
                end
            }
        },
        css = {
            __type = "property",
            fget = {
                __type = "builtin_method", __name = "css",
                __call = function(args)
                    local priv = get_html_priv(args[1], "css")
                    if not priv.css_proxy then
                        local cp = PyClass.make_instance(HtmlCssProxyClass)
                        mw_private[cp] = priv
                        priv.css_proxy = cp
                    end
                    return priv.css_proxy
                end
            }
        },
        class_ = {
            __type = "property",
            fget = {
                __type = "builtin_method", __name = "class_",
                __call = function(args)
                    local priv = get_html_priv(args[1], "class_")
                    if not priv.class_proxy then
                        local clp = PyClass.make_instance(HtmlClassProxyClass)
                        mw_private[clp] = priv
                        priv.class_proxy = clp
                    end
                    return priv.class_proxy
                end
            }
        },
        node = {
            __type = "builtin_method", __name = "node",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "node")
                local priv = get_html_priv(args[1], "node")
                local builder = args[2]
                if not is_none(builder) then
                    local b_priv = type(builder) == T_TABLE and mw_private[builder] or nil
                    if b_priv and b_priv.kind == "html" then
                        priv.lua_html:node(b_priv.lua_html)
                    else
                        priv.lua_html:node(RUtils.to_str(builder, false))
                    end
                    if priv.sync_svg then priv.sync_svg() end
                end
                return args[1]
            end
        },
        wikitext = {
            __type = "builtin_method", __name = "wikitext",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "wikitext")
                local priv = get_html_priv(args[1], "wikitext")
                local wt_args = {}
                for i = 2, #args do
                    if is_none(args[i]) then break end
                    wt_args[#wt_args + 1] = RUtils.to_str(args[i], false)
                end
                if #wt_args > 0 then
                    priv.lua_html:wikitext(unpack(wt_args))
                    if priv.sync_svg then priv.sync_svg() end
                end
                return args[1]
            end
        },
        newline = {
            __type = "builtin_method", __name = "newline",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "newline")
                local priv = get_html_priv(args[1], "newline")
                priv.lua_html:newline()
                if priv.sync_svg then priv.sync_svg() end
                return args[1]
            end
        },
        tag = {
            __type = "builtin_method", __name = "tag",
            __call = function(args, kwargs)
                local priv = get_html_priv(args[1], "tag")
                local tag_name = args[2]
                validate_html_tag(tag_name)
                local lua_opts = nil
                if args[3] and type(args[3]) == T_TABLE and args[3].__type == "dict" then
                    lua_opts = py2lua(args[3])
                elseif kwargs and next(kwargs) ~= nil then
                    lua_opts = py2lua(kwargs)
                end
                local ok, child_lua = pcall(priv.lua_html.tag, priv.lua_html, is_none(tag_name) and nil or tag_name, lua_opts)
                if not ok then
                    Runtime.raise("NotImplementedError", "HTML tag '<" .. tostring(tag_name) .. ">' is not supported: " .. tostring(child_lua))
                end
                if priv.sync_svg then priv.sync_svg() end
                return MwHtml.wrap_html_object(child_lua, args[1], priv.root or args[1], priv.sync_svg)
            end
        },
        attr = {
            __type = "builtin_method", __name = "attr",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "attr")
                local priv = get_html_priv(args[1], "attr")
                if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" then
                    local tbl = {}
                    for _, p in ipairs(args[2].pairs) do
                        local k = RUtils.to_str(p.key, false)
                        local v = is_none(p.value) and nil or RUtils.to_str(p.value, false)
                        tbl[k] = v
                    end
                    priv.lua_html:attr(tbl)
                elseif #args >= 2 then
                    local k = RUtils.to_str(args[2], false)
                    local v = is_none(args[3]) and nil or RUtils.to_str(args[3], false)
                    priv.lua_html:attr(k, v)
                else
                    Runtime.raise("TypeError", "attr() requires at least 1 argument")
                end
                if priv.sync_svg then priv.sync_svg() end
                return args[1]
            end
        },
        getAttr = {
            __type = "builtin_method", __name = "getAttr",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "getAttr")
                if #args ~= 2 then
                    Runtime.raise("TypeError", "getAttr() takes exactly 1 argument (" .. (#args - 1) .. " given)")
                end
                local priv = get_html_priv(args[1], "getAttr")
                local v = priv.lua_html:getAttr(RUtils.to_str(args[2], false))
                return v ~= nil and v or B.None
            end
        },
        addClass = {
            __type = "builtin_method", __name = "addClass",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "addClass")
                local priv = get_html_priv(args[1], "addClass")
                if not is_none(args[2]) then
                    priv.lua_html:addClass(RUtils.to_str(args[2], false))
                    if priv.sync_svg then priv.sync_svg() end
                end
                return args[1]
            end
        },
        cssText = {
            __type = "builtin_method", __name = "cssText",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "cssText")
                local priv = get_html_priv(args[1], "cssText")
                if not is_none(args[2]) then
                    priv.lua_html:cssText(RUtils.to_str(args[2], false))
                    if priv.sync_svg then priv.sync_svg() end
                end
                return args[1]
            end
        },
        done = {
            __type = "builtin_method", __name = "done",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "done")
                local priv = get_html_priv(args[1], "done")
                if priv.sync_svg then priv.sync_svg() end
                if priv.parent then return priv.parent end
                local p_lua = priv.lua_html:done()
                if p_lua == priv.lua_html or not p_lua then return args[1] end
                return MwHtml.wrap_html_object(p_lua, nil, priv.root, priv.sync_svg)
            end
        },
        allDone = {
            __type = "builtin_method", __name = "allDone",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "allDone")
                local priv = get_html_priv(args[1], "allDone")
                if priv.sync_svg then priv.sync_svg() end
                if priv.root then return priv.root end
                local r_lua = priv.lua_html:allDone()
                if r_lua == priv.lua_html or not r_lua then return args[1] end
                return MwHtml.wrap_html_object(r_lua, nil, nil, priv.sync_svg)
            end
        },
        __getattr__ = {
            __type = "builtin_method", __name = "__getattr__",
            __call = function(args)
                local self, attr = args[1], args[2]
                local priv = get_html_priv(self, "__getattr__")
                local real_attr = (attr == "class_") and "class" or attr
                local val = priv.lua_html:getAttr(real_attr)
                if val ~= nil then return val end
                local dash_attr = real_attr:gsub("_", "-")
                if dash_attr ~= real_attr then
                    val = priv.lua_html:getAttr(dash_attr)
                    if val ~= nil then return val end
                end
                if COMMON_HTML_ATTRS[real_attr] or COMMON_HTML_ATTRS[dash_attr] then
                    return B.None
                end
                Runtime.raise("AttributeError", "'HtmlObject' object has no attribute '" .. tostring(attr) .. "'")
            end
        },
        __setattr__ = {
            __type = "builtin_method", __name = "__setattr__",
            __call = function(args)
                local self, attr, val = args[1], args[2], args[3]
                local priv = get_html_priv(self, "__setattr__")
                if attr == "css" then
                    Runtime.raise("AttributeError", "can't overwrite 'css' proxy directly; use htmlObj.css.prop = val or htmlObj.css(name, val)")
                elseif attr == "class_" or attr == "class" then
                    priv.lua_html:attr("class", is_none(val) and nil or RUtils.to_str(val, false))
                    if priv.sync_svg then priv.sync_svg() end
                    return B.None
                elseif HTML_RESERVED_METHODS[attr] or attr:sub(1, 1) == "_" then
                    Runtime.raise("AttributeError", "'HtmlObject' attribute '" .. tostring(attr) .. "' is read-only or reserved")
                end
                local real_attr = attr:gsub("_", "-")
                priv.lua_html:attr(real_attr, is_none(val) and nil or RUtils.to_str(val, false))
                if priv.sync_svg then priv.sync_svg() end
                return B.None
            end
        },
        __str__ = {
            __type = "builtin_method", __name = "__str__",
            __call = function(args)
                local priv = get_html_priv(args[1], "__str__")
                return tostring(priv.lua_html)
            end
        }
    }
    html_obj_dict.__repr__ = html_obj_dict.__str__
    HtmlObjectClass = PyClass.make_class("HtmlObject", { MarkupBuilderBaseClass }, html_obj_dict)

    -- [E] SvgImgAttributes 與 SvgObject 類別
    local ALLOWED_IMG_ATTRS = {
        width = true, height = true, ["class"] = true,
        id = true, alt = true, title = true, style = true
    }
    local IMG_ATTR_ORDER = { "width", "height", "class", "id", "alt", "title", "style" }
    local DEFAULT_SVG_ATTRS = { "width", "height", "x", "y", "viewBox", "viewport", "xmlns" }

    local function coerce_svg_attr_val(val, func_name)
        if type(val) == T_TABLE and val.__type == "instance" and val.__builtin_value ~= nil then
            val = val.__builtin_value
        end
        if type(val) == "string" then return val end
        if type(val) == "number" or (type(val) == T_TABLE and val.__type == "int") then
            if type(val) == "boolean" then
                Runtime.raise("TypeError", func_name .. "() attribute value must be str or number, not bool")
            end
            return RUtils.to_str(val, false)
        end
        Runtime.raise("TypeError", func_name .. "() attribute value must be str or number, not '" .. RUtils.get_type_str(val) .. "'")
    end

    local function get_svg_priv(self_obj, func_name)
        local priv = type(self_obj) == T_TABLE and mw_private[self_obj] or nil
        if not priv or priv.kind ~= "svg" then
            Runtime.raise("TypeError", "descriptor '" .. func_name .. "' requires a valid 'SvgObject' instance")
        end
        return priv
    end

    local svg_img_dict = {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot create 'SvgImgAttributes' instances directly; access via svgObj.Img")
                end
            }
        },
        __getattr__ = {
            __type = "builtin_method", __name = "__getattr__",
            __call = function(args)
                local self, attr = args[1], args[2]
                if attr == "class_" then return self.__dict["class"] end
                Runtime.raise("AttributeError", "'SvgImgAttributes' object has no attribute '" .. tostring(attr) .. "'")
            end
        },
        __setattr__ = {
            __type = "builtin_method", __name = "__setattr__",
            __call = function(args)
                local self, attr, val = args[1], args[2], args[3]
                local priv = get_svg_priv(self, "SvgImgAttributes.__setattr__")
                if type(attr) ~= "string" then
                    Runtime.raise("TypeError", "attribute name must be string")
                end
                local real_attr = (attr == "class_") and "class" or attr
                if not ALLOWED_IMG_ATTRS[real_attr] then
                    Runtime.raise("AttributeError", "invalid img attribute '" .. attr .. "' (allowed: width, height, class, id, alt, title, style)")
                end
                local val_str = coerce_svg_attr_val(val, "Img." .. attr)
                priv.lua_svg:setImgAttribute(real_attr, val_str)
                priv.img_attrs[real_attr] = val_str
                self.__dict[real_attr] = val_str
                return B.None
            end
        },
        __delattr__ = {
            __type = "builtin_method", __name = "__delattr__",
            __call = function()
                Runtime.raise("TypeError", "'SvgImgAttributes' object does not support attribute deletion")
            end
        },
        __repr__ = {
            __type = "builtin_method", __name = "__repr__",
            __call = function(args)
                local priv = get_svg_priv(args[1], "__repr__")
                local parts = {}
                for _, k in ipairs(IMG_ATTR_ORDER) do
                    if priv.img_attrs[k] ~= nil then
                        table.insert(parts, k .. "=" .. RUtils.to_str(priv.img_attrs[k], true))
                    end
                end
                if #parts == 0 then return "<SvgImgAttributes (empty)>" end
                return "<SvgImgAttributes " .. table.concat(parts, " ") .. ">"
            end
        }
    }
    svg_img_dict.__str__ = svg_img_dict.__repr__
    SvgImgAttributesClass = PyClass.make_class("SvgImgAttributes", { B.object }, svg_img_dict)

    local PROTECTED_SVG_NAMES = {
        Content = true, Img = true, html = true,
        setContent = true, setAttribute = true, setImgAttribute = true, toImage = true,
        tag = true, node = true, wikitext = true, newline = true, done = true, allDone = true
    }

    local function set_svg_attr_internal(self, priv, name, val, caller_name)
        if type(name) ~= "string" or name == "" then
            Runtime.raise("TypeError", caller_name .. "() attribute name must be non-empty str, not '" .. RUtils.get_type_str(name) .. "'")
        end
        if name == "Content" then
            local v_priv = type(val) == T_TABLE and mw_private[val] or nil
            if v_priv and v_priv.kind == "html" then
                local html_str = tostring(v_priv.lua_html)
                priv.content = html_str
                if mw.html then
                    priv.html_builder = mw.html.create(nil)
                    priv.html_builder:wikitext(html_str)
                end
                priv.lua_svg:setContent(html_str)
                return
            end
            Runtime.raise("AttributeError", "can't set attribute 'Content' directly with str; use setContent() or assign an mw.html object")
        elseif name == "Img" then
            Runtime.raise("AttributeError", "can't set attribute 'Img' directly; modify attributes on svgObj.Img or use setImgAttribute()")
        elseif PROTECTED_SVG_NAMES[name] or name:sub(1, 1) == "_" then
            Runtime.raise("AttributeError", "'SvgObject' attribute '" .. name .. "' is read-only or reserved")
        end
        local val_str = coerce_svg_attr_val(val, caller_name)
        priv.lua_svg:setAttribute(name, val_str)
        priv.attrs[name] = val_str
        self.__dict[name] = val_str
    end

    local function set_img_attr_internal(priv, name, val, caller_name)
        if type(name) ~= "string" then
            Runtime.raise("TypeError", caller_name .. "() argument 1 must be str, not '" .. RUtils.get_type_str(name) .. "'")
        end
        local real_attr = (name == "class_") and "class" or name
        if not ALLOWED_IMG_ATTRS[real_attr] then
            Runtime.raise("ValueError", "invalid img attribute '" .. name .. "' (allowed: width, height, class, id, alt, title, style)")
        end
        local val_str = coerce_svg_attr_val(val, caller_name)
        priv.lua_svg:setImgAttribute(real_attr, val_str)
        priv.img_attrs[real_attr] = val_str
        priv.img_proxy.__dict[real_attr] = val_str
    end

    local function ensure_svg_html_proxy(self, priv)
        if not priv.html_proxy then
            if not priv.html_builder and mw.html then
                priv.html_builder = mw.html.create(nil)
                if priv.content ~= "" then priv.html_builder:wikitext(priv.content) end
            end
            priv.html_proxy = MwHtml.wrap_html_object(priv.html_builder, self, self, priv.sync_svg)
        end
        return priv.html_proxy
    end

    local svg_obj_dict = {
        __new__ = {
            __type = "staticmethod",
            __func = {
                __type = "builtin_method", __name = "__new__",
                __call = function()
                    Runtime.raise("TypeError", "cannot create 'SvgObject' instances directly; use mw.svg.new()")
                end
            }
        },
        Content = {
            __type = "property",
            fget = {
                __type = "builtin_method", __name = "Content",
                __call = function(args)
                    local priv = get_svg_priv(args[1], "Content")
                    if priv.sync_svg then priv.sync_svg() end
                    return priv.content
                end
            }
        },
        Img = {
            __type = "property",
            fget = {
                __type = "builtin_method", __name = "Img",
                __call = function(args)
                    local priv = get_svg_priv(args[1], "Img")
                    return priv.img_proxy
                end
            }
        },
        html = {
            __type = "property",
            fget = {
                __type = "builtin_method", __name = "html",
                __call = function(args)
                    local priv = get_svg_priv(args[1], "html")
                    return ensure_svg_html_proxy(args[1], priv)
                end
            }
        },
        setContent = {
            __type = "builtin_method", __name = "setContent",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "setContent")
                if #args ~= 2 then
                    Runtime.raise("TypeError", "setContent() takes exactly 1 positional argument (" .. math.max(0, #args - 1) .. " given)")
                end
                local self, content = args[1], args[2]
                local priv = get_svg_priv(self, "setContent")
                local c_priv = type(content) == T_TABLE and mw_private[content] or nil
                if c_priv and c_priv.kind == "html" then
                    content = tostring(c_priv.lua_html)
                elseif type(content) == T_TABLE and content.__type == "instance" and type(content.__builtin_value) == "string" then
                    content = content.__builtin_value
                end
                if type(content) ~= "string" then
                    Runtime.raise("TypeError", "setContent() argument must be str or HtmlObject, not '" .. RUtils.get_type_str(content) .. "'")
                end
                priv.content = content
                if mw.html then
                    priv.html_builder = mw.html.create(nil)
                    if content ~= "" then priv.html_builder:wikitext(content) end
                    if priv.html_proxy and mw_private[priv.html_proxy] then
                        mw_private[priv.html_proxy].lua_html = priv.html_builder
                    end
                end
                priv.lua_svg:setContent(content)
                return self
            end
        },
        setAttribute = {
            __type = "builtin_method", __name = "setAttribute",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "setAttribute")
                if #args ~= 3 then
                    Runtime.raise("TypeError", "setAttribute() takes exactly 2 positional arguments (" .. math.max(0, #args - 1) .. " given)")
                end
                local self, name, val = args[1], args[2], args[3]
                local priv = get_svg_priv(self, "setAttribute")
                set_svg_attr_internal(self, priv, name, val, "setAttribute")
                return self
            end
        },
        setImgAttribute = {
            __type = "builtin_method", __name = "setImgAttribute",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "setImgAttribute")
                if #args ~= 3 then
                    Runtime.raise("TypeError", "setImgAttribute() takes exactly 2 positional arguments (" .. math.max(0, #args - 1) .. " given)")
                end
                local self, name, val = args[1], args[2], args[3]
                local priv = get_svg_priv(self, "setImgAttribute")
                set_img_attr_internal(priv, name, val, "setImgAttribute")
                return self
            end
        },
        tag = {
            __type = "builtin_method", __name = "tag",
            __call = function(args, kwargs)
                local self = args[1]
                local priv = get_svg_priv(self, "tag")
                local hp = ensure_svg_html_proxy(self, priv)
                return HtmlObjectClass.__dict.tag.__call({ hp, args[2], args[3] }, kwargs)
            end
        },
        node = {
            __type = "builtin_method", __name = "node",
            __call = function(args, kwargs)
                local self = args[1]
                local priv = get_svg_priv(self, "node")
                local hp = ensure_svg_html_proxy(self, priv)
                HtmlObjectClass.__dict.node.__call({ hp, args[2] }, kwargs)
                return self
            end
        },
        wikitext = {
            __type = "builtin_method", __name = "wikitext",
            __call = function(args, kwargs)
                local self = args[1]
                local priv = get_svg_priv(self, "wikitext")
                local hp = ensure_svg_html_proxy(self, priv)
                local call_args = { hp }
                for i = 2, #args do call_args[i] = args[i] end
                HtmlObjectClass.__dict.wikitext.__call(call_args, kwargs)
                return self
            end
        },
        newline = {
            __type = "builtin_method", __name = "newline",
            __call = function(args, kwargs)
                local self = args[1]
                local priv = get_svg_priv(self, "newline")
                local hp = ensure_svg_html_proxy(self, priv)
                HtmlObjectClass.__dict.newline.__call({ hp }, kwargs)
                return self
            end
        },
        done = {
            __type = "builtin_method", __name = "done",
            __call = function(args) return args[1] end
        },
        allDone = {
            __type = "builtin_method", __name = "allDone",
            __call = function(args) return args[1] end
        },
        toImage = {
            __type = "builtin_method", __name = "toImage",
            __call = function(args, kwargs)
                check_no_kwargs(kwargs, "toImage")
                if #args ~= 1 then
                    Runtime.raise("TypeError", "toImage() takes 0 positional arguments (" .. (#args - 1) .. " given)")
                end
                local priv = get_svg_priv(args[1], "toImage")
                if mw.isSubsting() then
                    Runtime.raise("NotImplementedError", "SVG 圖表功能不支援在 subst (模板展開存成純 wikitext) 模式下使用")
                end
                if priv.sync_svg then priv.sync_svg() end
                return priv.lua_svg:toImage()
            end
        },
        __setattr__ = {
            __type = "builtin_method", __name = "__setattr__",
            __call = function(args)
                local self, attr, val = args[1], args[2], args[3]
                local priv = get_svg_priv(self, "__setattr__")
                set_svg_attr_internal(self, priv, attr, val, "SvgObject.__setattr__")
                return B.None
            end
        },
        __delattr__ = {
            __type = "builtin_method", __name = "__delattr__",
            __call = function()
                Runtime.raise("TypeError", "'SvgObject' object does not support attribute deletion")
            end
        },
        __repr__ = {
            __type = "builtin_method", __name = "__repr__",
            __call = function(args)
                local priv = get_svg_priv(args[1], "__repr__")
                if priv.sync_svg then priv.sync_svg() end
                local w = priv.img_attrs.width or priv.attrs.width
                local h = priv.img_attrs.height or priv.attrs.height
                local vb = priv.attrs.viewBox or priv.attrs.viewport
                local parts = {
                    "width=" .. (w and RUtils.to_str(w, true) or "None"),
                    "height=" .. (h and RUtils.to_str(h, true) or "None")
                }
                if vb then table.insert(parts, "viewBox=" .. RUtils.to_str(vb, true)) end
                if priv.attrs.x then table.insert(parts, "x=" .. RUtils.to_str(priv.attrs.x, true)) end
                if priv.attrs.y then table.insert(parts, "y=" .. RUtils.to_str(priv.attrs.y, true)) end
                if priv.img_attrs["class"] then
                    table.insert(parts, "img_class=" .. RUtils.to_str(priv.img_attrs["class"], true))
                end
                table.insert(parts, "content_len=" .. tostring(#priv.content))
                return "<SvgObject " .. table.concat(parts, " ") .. ">"
            end
        }
    }
    svg_obj_dict.__str__ = svg_obj_dict.__repr__
    SvgObjectClass = PyClass.make_class("SvgObject", { MarkupBuilderBaseClass }, svg_obj_dict)
end

-- ==========================================
-- 4. 對外封裝與 mw_proxy 掛載介面
-- ==========================================
function MwHtml.wrap_html_object(lua_html, parent_inst, root_inst, sync_svg_cb)
    ensure_markup_classes()
    if lua_html == nil then return B.None end
    if type(lua_html) == T_TABLE and mw_private[lua_html] and mw_private[lua_html].kind == "html" then
        return lua_html
    end
    local inst = PyClass.make_instance(HtmlObjectClass)
    mw_private[inst] = {
        kind = "html",
        owner = inst,
        lua_html = lua_html,
        parent = parent_inst,
        root = root_inst or inst,
        css_cache = {},
        sync_svg = sync_svg_cb
    }
    return inst
end

function MwHtml.wrap_svg_object(lua_svg_in)
    ensure_markup_classes()
    local DEFAULT_SVG_ATTRS = { "width", "height", "x", "y", "viewBox", "viewport", "xmlns" }
    local IMG_ATTR_ORDER = { "width", "height", "class", "id", "alt", "title", "style" }

    local inst = PyClass.make_instance(SvgObjectClass)
    local img_inst = PyClass.make_instance(SvgImgAttributesClass)
    for _, k in ipairs(DEFAULT_SVG_ATTRS) do
        inst.__dict[k] = (k == "xmlns") and "http://www.w3.org/2000/svg" or B.None
    end
    for _, k in ipairs(IMG_ATTR_ORDER) do
        img_inst.__dict[k] = B.None
    end
    local lua_svg = lua_svg_in or mw.svg.new()
    local html_builder = mw.html and mw.html.create(nil) or nil
    local priv_state
    priv_state = {
        kind = "svg",
        lua_svg = lua_svg,
        attrs = { xmlns = "http://www.w3.org/2000/svg" },
        img_attrs = {},
        content = "",
        img_proxy = img_inst,
        html_builder = html_builder,
        sync_svg = function()
            if priv_state.html_builder then
                local s = tostring(priv_state.html_builder)
                if s ~= "" then
                    priv_state.content = s
                    priv_state.lua_svg:setContent(s)
                end
            end
        end
    }
    mw_private[inst] = priv_state
    mw_private[img_inst] = priv_state
    return inst
end

function MwHtml.attach_to_mw_proxy(mw_proxy)
    ensure_markup_classes()

    if mw.svg then
        mw_proxy.svg = {
            new = {
                __type = "builtin_method", __name = "new",
                __call = function(args, kwargs)
                    check_no_kwargs(kwargs, "mw.svg.new")
                    if args and #args > 0 then
                        Runtime.raise("TypeError", "mw.svg.new() takes 0 positional arguments (" .. #args .. " given)")
                    end
                    return MwHtml.wrap_svg_object()
                end
            }
        }
    end

    if mw.html then
        mw_proxy.html = {
            create = {
                __type = "builtin_method", __name = "create",
                __call = function(args, kwargs)
                    local tag_name = args and args[1]
                    if type(tag_name) == "string" and tag_name:lower() == "svg" and mw.svg then
                        return MwHtml.wrap_svg_object()
                    end
                    validate_html_tag(tag_name)
                    local lua_opts = nil
                    if args and args[2] and type(args[2]) == T_TABLE and args[2].__type == "dict" then
                        lua_opts = py2lua(args[2])
                    elseif kwargs and next(kwargs) ~= nil then
                        lua_opts = py2lua(kwargs)
                    end
                    local ok, h = pcall(mw.html.create, is_none(tag_name) and nil or tag_name, lua_opts)
                    if not ok then
                        Runtime.raise("NotImplementedError", "HTML tag '<" .. tostring(tag_name) .. ">' is not supported: " .. tostring(h))
                    end
                    return MwHtml.wrap_html_object(h, nil, nil, nil)
                end
            }
        }
    end

    mw_proxy.MarkupBuilderBase = MarkupBuilderBaseClass
    mw_proxy.HtmlObject = HtmlObjectClass
    mw_proxy.SvgObject = SvgObjectClass
end

return MwHtml