模組: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