模块:PythonScript/Mw
外观
-- Module:PythonScript/Mw
-- MediaWiki 橋接主模組:負責雙向資料轉譯 (py2lua/lua2py)、LuaInstance、FrameObject 與 TitleObject 特化
-- (MarkupBuilderBase、HtmlObject 與 SvgObject 拆分至子模組 Module:PythonScript/Mw/Html)
local MwAPI = {}
local PyClass = require("Module:PythonScript/Class")
local Runtime = require("Module:PythonScript/Runtime")
local RUtils = require("Module:PythonScript/Runtime/Utils")
local MwHtml = require("Module:PythonScript/Mw/Html")
local T_FUNC = type(function() end)
local T_TABLE = type({})
local B = Runtime.builtins
-- 私有弱引用表:集中隱藏所有特化物件的底層 Lua 實例與快取,使 Python 端 dir() / vars() 完全不可見
local mw_private = setmetatable({}, { __mode = "k" })
local py2lua, lua2py, wrap_lua_instance, make_lazy_namespace, make_py_iterator
local wrap_frame_object, wrap_title_object
local function is_none(v)
return v == nil or v == B.None or (type(v) == T_TABLE and v.__class and v.__class.__name == "NoneType")
end
local function check_no_kwargs(kwargs, func_name)
if kwargs and next(kwargs) ~= nil then
Runtime.raise("TypeError", func_name .. "() takes no keyword arguments")
end
end
-- 1. 偵測是否為 Scribunto Frame 物件 (全成員型別比對)
local function is_lua_frame(frame)
return type(frame) == T_TABLE
and (frame.args and type(frame.args) == T_TABLE)
and (frame.callParserFunction and type(frame.callParserFunction) == T_FUNC)
and (frame.expandTemplate and type(frame.expandTemplate) == T_FUNC)
and (frame.extensionTag and type(frame.extensionTag) == T_FUNC)
and (frame.getParent and type(frame.getParent) == T_FUNC)
and (frame.getTitle and type(frame.getTitle) == T_FUNC)
and (frame.newChild and type(frame.newChild) == T_FUNC)
and (frame.preprocess and type(frame.preprocess) == T_FUNC)
and (frame.getArgument and type(frame.getArgument) == T_FUNC)
and (frame.newParserValue and type(frame.newParserValue) == T_FUNC)
and (frame.newTemplateParserValue and type(frame.newTemplateParserValue) == T_FUNC)
and (frame.argumentPairs and type(frame.argumentPairs) == T_FUNC)
end
-- 2. 偵測是否為 Scribunto Title 物件 (僅檢查非高開銷之方法成員,避免觸發昂貴查詢)
local function is_lua_title(obj)
if type(obj) ~= T_TABLE or obj.__type ~= nil then return false end
local ok, res = pcall(function()
return type(obj.inNamespace) == T_FUNC
and type(obj.inNamespaces) == T_FUNC
and type(obj.hasSubjectNamespace) == T_FUNC
and type(obj.subPageTitle) == T_FUNC
and type(obj.fullUrl) == T_FUNC
and type(obj.localUrl) == T_FUNC
and type(obj.canonicalUrl) == T_FUNC
and type(obj.getContent) == T_FUNC
end)
return ok and res == true
end
local is_lua_html = MwHtml.is_lua_html
local is_lua_svg = MwHtml.is_lua_svg
local wrap_html_object = MwHtml.wrap_html_object
local wrap_svg_object = MwHtml.wrap_svg_object
-- ==========================================
-- 核心轉譯引擎:雙向資料與型態映射
-- ==========================================
py2lua = function(val)
if type(val) == T_TABLE then
if val.__is_py_proxy and val.__py_obj ~= nil then
val = val.__py_obj
end
local priv = mw_private[val]
if priv then
return priv.lua_frame or priv.lua_title or priv.lua_html or priv.lua_svg or priv.lua_obj
end
if val.__class and val.__class.__name == "NoneType" then
return nil
elseif val.__type == "LuaInstance" or (val.__class and val.__class.__name == "LuaInstance") then
return val.__dict._lua_obj
elseif val.__type == "bytes" or val.__type == "bytearray" or val.__type == "memoryview" then
return val._data
elseif val.__type == "int" and val.is_big then
local MathCore = require("Module:PythonScript/Runtime/Cmath")
return tonumber(MathCore.BigInt.to_string(val.sign, val.limbs))
elseif val.__type == "dict" then
local t = {}
for _, p in ipairs(val.pairs) do t[py2lua(p.key)] = py2lua(p.value) end
return t
elseif val.__type == "list" or val.__type == "tuple" or val.__type == "set" then
local t = {}
for i, v in ipairs(val.elements) do t[i] = py2lua(v) end
return t
elseif val.__type == "function" or val.__type == "bound_method" or val.__type == "builtin_method" then
return function(...)
local Evaluator = require("Module:PythonScript/Evaluator")
local args = {...}
local py_args = {}
for _, a in ipairs(args) do table.insert(py_args, lua2py(a)) end
local success, res = pcall(Evaluator.invoke, val, py_args, {})
if not success then error(res, 0) end
return py2lua(res)
end
end
end
return val
end
lua2py = function(val)
if val == nil then return B.None end
if type(val) == "number" then
if math.floor(val) == val and math.abs(val) > 9007199254740990 then
return B.int.__call({val})
end
return val
end
if type(val) == T_TABLE then
if val.__is_py_proxy and val.__py_obj ~= nil then
return val.__py_obj
end
if val.__type then return val end
if is_lua_frame(val) then return wrap_frame_object(val) end
if val.__type then return val end
if is_lua_frame(val) then return wrap_frame_object(val) end
if is_lua_title(val) then return wrap_title_object(val) end
if is_lua_html(val) then return wrap_html_object(val) end
if is_lua_svg(val) then return wrap_svg_object(val) end
local has_func = false
local max_k, count, is_array = 0, 0, true
for k, v in pairs(val) do
if type(v) == T_FUNC then has_func = true end
if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then is_array = false end
if type(k) == "number" and k > max_k then max_k = k end
count = count + 1
end
if has_func or getmetatable(val) then
return wrap_lua_instance(val)
end
if is_array and max_k == count and count > 0 then
local elts = {}
for i = 1, max_k do table.insert(elts, lua2py(val[i])) end
return {__type="list", elements=elts}
else
local kv_pairs = {}
for k, v in pairs(val) do table.insert(kv_pairs, {key=lua2py(k), value=lua2py(v)}) end
return {__type="dict", pairs=kv_pairs}
end
end
return val
end
-- 單向注入共用參照至 Mw/Html 子模組,徹底打破循環依賴
MwHtml.bind({
mw_private = mw_private,
py2lua = py2lua,
is_none = is_none,
check_no_kwargs = check_no_kwargs
})
-- ==========================================
-- 迭代器包裝器與通用 LuaInstance 代理
-- ==========================================
make_py_iterator = function(lua_iter)
local dict = {
__iter__ = { __type = "builtin_method", __call = function(args) return args[1] end },
__next__ = {
__type = "builtin_method",
__call = function(args)
local res = { args[1].__dict._lua_iter() }
if res[1] == nil then Runtime.raise("StopIteration") end
if #res == 1 then return lua2py(res[1]) end
local tup = {}
for i = 1, #res do table.insert(tup, lua2py(res[i])) end
return { __type = "tuple", elements = tup }
end
}
}
local inst = PyClass.make_instance(PyClass.make_class("LuaIterator", {}, dict))
inst.__dict._lua_iter = lua_iter
return inst
end
local LuaInstanceClass = PyClass.make_class("LuaInstance", {}, {
__getattr__ = {
__type = "builtin_method",
__call = function(args)
local self, attr = args[1], args[2]
local lua_obj = self.__dict._lua_obj
local val = lua_obj[attr]
if type(val) == T_FUNC then
return {
__type = "builtin_method",
__call = function(m_args, m_kwargs)
local lua_args = { lua_obj }
for i = 1, #m_args do table.insert(lua_args, py2lua(m_args[i])) end
if m_kwargs and next(m_kwargs) then
local kw = {}
for key, kw_val in pairs(m_kwargs) do kw[key] = py2lua(kw_val) end
table.insert(lua_args, kw)
end
local results = { pcall(val, unpack(lua_args)) }
if not results[1] then error(results[2], 0) end
table.remove(results, 1)
if #results == 0 then return B.None end
if #results == 1 then return lua2py(results[1]) end
local tup = {}
for _, r in ipairs(results) do table.insert(tup, lua2py(r)) end
return {__type="tuple", elements=tup}
end
}
end
return lua2py(val)
end
},
__getitem__ = {
__type = "builtin_method",
__call = function(args) return lua2py(args[1].__dict._lua_obj[py2lua(args[2])]) end
},
__iter__ = {
__type = "builtin_method",
__call = function(args)
local lua_obj = args[1].__dict._lua_obj
local k
return make_py_iterator(function()
k = next(lua_obj, k)
return k
end)
end
},
__str__ = {
__type = "builtin_method",
__call = function(args)
local success, res = pcall(tostring, args[1].__dict._lua_obj)
return success and res or "<LuaInstance>"
end
}
})
wrap_lua_instance = function(lua_obj)
if type(lua_obj) ~= T_TABLE then return lua2py(lua_obj) end
if is_lua_frame(lua_obj) then return wrap_frame_object(lua_obj) end
if is_lua_title(lua_obj) then return wrap_title_object(lua_obj) end
if is_lua_html(lua_obj) then return wrap_html_object(lua_obj) end
if is_lua_svg(lua_obj) then return wrap_svg_object(lua_obj) end
local inst = PyClass.make_instance(LuaInstanceClass)
inst.__dict._lua_obj = lua_obj
return inst
end
-- ==========================================
-- 特化一:TitleObject、TitleBatchObject 與 TitleFileMetadata
-- ==========================================
local TitleObjectClass, TitleBatchObjectClass, TitleFileMetadataClass
local function get_title_priv(obj, caller)
local priv = type(obj) == T_TABLE and mw_private[obj] or nil
if not priv or priv.kind ~= "title" then
Runtime.raise("TypeError", (caller or "TitleObject") .. " requires a valid 'TitleObject' instance")
end
return priv
end
local function extract_title_str(v)
if type(v) == T_TABLE and mw_private[v] and mw_private[v].kind == "title" then
return mw_private[v].lua_title.fullText or mw_private[v].lua_title.prefixedText
end
return RUtils.to_str(v, false)
end
local TITLE_SCALAR_PROPS = {
id = true, interwiki = true, namespace = true, fragment = true,
nsText = true, subjectNsText = true, talkNsText = true,
text = true, prefixedText = true, fullText = true,
rootText = true, baseText = true, subpageText = true,
canTalk = true, exists = true, fileExists = true,
isContentPage = true, isExternal = true, isLocal = true,
isRedirect = true, isSpecialPage = true, isSubpage = true,
isTalkPage = true, contentModel = true, content = true,
isDisambiguationPage = true
}
local TITLE_OBJ_PROPS = {
basePageTitle = true, rootPageTitle = true,
talkPageTitle = true, subjectPageTitle = true
}
local function wrap_title_file_metadata(lua_file)
if not lua_file then return B.None end
if not TitleFileMetadataClass then
local tf_dict = {
__new__ = {
__type = "staticmethod",
__func = {
__type = "builtin_method", __name = "__new__",
__call = function()
Runtime.raise("TypeError", "cannot create 'TitleFileMetadata' instances directly")
end
}
},
__getattr__ = {
__type = "builtin_method", __name = "__getattr__",
__call = function(args)
local priv = mw_private[args[1]]
if not priv or priv.kind ~= "title_file" then
Runtime.raise("TypeError", "invalid TitleFileMetadata instance")
end
local k = args[2]
local val = priv.lua_file[k]
if val == nil then return B.None end
return lua2py(val)
end
},
__getitem__ = {
__type = "builtin_method", __name = "__getitem__",
__call = function(args)
local priv = mw_private[args[1]]
local val = priv and priv.lua_file[py2lua(args[2])]
if val == nil then return B.None end
return lua2py(val)
end
},
__setattr__ = {
__type = "builtin_method", __name = "__setattr__",
__call = function()
Runtime.raise("AttributeError", "'TitleFileMetadata' object attributes are read-only")
end
},
__repr__ = {
__type = "builtin_method", __name = "__repr__",
__call = function(args)
local priv = mw_private[args[1]]
local lf = priv and priv.lua_file
if not lf then return "<TitleFileMetadata>" end
if not lf.exists then return "<TitleFileMetadata exists=False>" end
return string.format("<TitleFileMetadata exists=True size=%s mimeType=%s width=%s height=%s>",
tostring(lf.size), RUtils.to_str(lf.mimeType, true), tostring(lf.width), tostring(lf.height))
end
}
}
tf_dict.__str__ = tf_dict.__repr__
TitleFileMetadataClass = PyClass.make_class("TitleFileMetadata", { B.object }, tf_dict)
end
local inst = PyClass.make_instance(TitleFileMetadataClass)
mw_private[inst] = { kind = "title_file", lua_file = lua_file }
return inst
end
local function ensure_title_class()
if TitleObjectClass then return TitleObjectClass end
local function coerce_url_query(q)
if is_none(q) then return nil end
if type(q) == "string" then return q end
if type(q) == T_TABLE and q.__type == "dict" then
local t = {}
for _, p in ipairs(q.pairs) do
t[RUtils.to_str(p.key, false)] = RUtils.to_str(p.value, false)
end
return t
end
return py2lua(q)
end
local title_dict = {
__new__ = {
__type = "staticmethod",
__func = {
__type = "builtin_method", __name = "__new__",
__call = function()
Runtime.raise("TypeError", "cannot create 'TitleObject' instances directly; use mw.title.new(), mw.title.makeTitle(), or mw.title.getCurrentTitle()")
end
}
},
isSubpageOf = {
__type = "builtin_method", __name = "isSubpageOf",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "isSubpageOf")
if #args ~= 2 then
Runtime.raise("TypeError", "isSubpageOf() takes exactly 1 argument (" .. (#args - 1) .. " given)")
end
local priv = get_title_priv(args[1], "isSubpageOf")
local t2 = args[2]
if type(t2) == T_TABLE and mw_private[t2] and mw_private[t2].kind == "title" then
t2 = mw_private[t2].lua_title
elseif type(t2) == "string" and mw.title and mw.title.new then
t2 = mw.title.new(t2) or t2
end
return priv.lua_title:isSubpageOf(t2) == true
end
},
inNamespace = {
__type = "builtin_method", __name = "inNamespace",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "inNamespace")
if #args ~= 2 then
Runtime.raise("TypeError", "inNamespace() takes exactly 1 argument (" .. (#args - 1) .. " given)")
end
local priv = get_title_priv(args[1], "inNamespace")
return priv.lua_title:inNamespace(py2lua(args[2])) == true
end
},
inNamespaces = {
__type = "builtin_method", __name = "inNamespaces",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "inNamespaces")
local priv = get_title_priv(args[1], "inNamespaces")
local ns_list = {}
for i = 2, #args do ns_list[#ns_list + 1] = py2lua(args[i]) end
return priv.lua_title:inNamespaces(unpack(ns_list)) == true
end
},
hasSubjectNamespace = {
__type = "builtin_method", __name = "hasSubjectNamespace",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "hasSubjectNamespace")
if #args ~= 2 then
Runtime.raise("TypeError", "hasSubjectNamespace() takes exactly 1 argument (" .. (#args - 1) .. " given)")
end
local priv = get_title_priv(args[1], "hasSubjectNamespace")
return priv.lua_title:hasSubjectNamespace(py2lua(args[2])) == true
end
},
subPageTitle = {
__type = "builtin_method", __name = "subPageTitle",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "subPageTitle")
if #args ~= 2 then
Runtime.raise("TypeError", "subPageTitle() takes exactly 1 argument (" .. (#args - 1) .. " given)")
end
local priv = get_title_priv(args[1], "subPageTitle")
local sub = priv.lua_title:subPageTitle(RUtils.to_str(args[2], false))
return sub and wrap_title_object(sub) or B.None
end
},
partialUrl = {
__type = "builtin_method", __name = "partialUrl",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "partialUrl")
local priv = get_title_priv(args[1], "partialUrl")
return priv.lua_title:partialUrl()
end
},
fullUrl = {
__type = "builtin_method", __name = "fullUrl",
__call = function(args, kwargs)
local priv = get_title_priv(args[1], "fullUrl")
local q = coerce_url_query(args[2] or (kwargs and kwargs.query))
local proto = args[3] or (kwargs and kwargs.proto)
proto = not is_none(proto) and RUtils.to_str(proto, false) or nil
return priv.lua_title:fullUrl(q, proto)
end
},
localUrl = {
__type = "builtin_method", __name = "localUrl",
__call = function(args, kwargs)
local priv = get_title_priv(args[1], "localUrl")
local q = coerce_url_query(args[2] or (kwargs and kwargs.query))
return priv.lua_title:localUrl(q)
end
},
canonicalUrl = {
__type = "builtin_method", __name = "canonicalUrl",
__call = function(args, kwargs)
local priv = get_title_priv(args[1], "canonicalUrl")
local q = coerce_url_query(args[2] or (kwargs and kwargs.query))
return priv.lua_title:canonicalUrl(q)
end
},
getContent = {
__type = "builtin_method", __name = "getContent",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "getContent")
local priv = get_title_priv(args[1], "getContent")
local c = priv.lua_title:getContent()
return c ~= nil and c or B.None
end
},
__getattr__ = {
__type = "builtin_method", __name = "__getattr__",
__call = function(args)
local self, attr = args[1], args[2]
local priv = get_title_priv(self, "__getattr__")
local lt = priv.lua_title
if TITLE_SCALAR_PROPS[attr] then
local v = lt[attr]
if v == nil then return B.None end
return v
elseif TITLE_OBJ_PROPS[attr] then
local t_sub = lt[attr]
if not t_sub then return B.None end
return wrap_title_object(t_sub)
elseif attr == "redirectTarget" then
local rt = lt.redirectTarget
if rt == false or rt == nil then return false end
return wrap_title_object(rt)
elseif attr == "file" then
if priv.cached_file == nil then
priv.cached_file = wrap_title_file_metadata(lt.file)
end
return priv.cached_file
elseif attr == "protectionLevels" or attr == "cascadingProtection" or attr == "categories" or attr == "visibleCategories" or attr == "pageAssessments" or attr == "pageLang" then
local v = lt[attr]
if v == nil then return B.None end
return lua2py(v)
end
Runtime.raise("AttributeError", "'TitleObject' 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_title_priv(self, "__setattr__")
if attr == "fragment" then
priv.lua_title.fragment = is_none(val) and "" or RUtils.to_str(val, false)
priv.cached_repr = nil
return B.None
end
Runtime.raise("AttributeError", "'TitleObject' attribute '" .. tostring(attr) .. "' is read-only")
end
},
__eq__ = {
__type = "builtin_method", __name = "__eq__",
__call = function(args)
local p1 = mw_private[args[1]]
local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
if p1 and p1.kind == "title" and p2 and p2.kind == "title" then
if mw.title and mw.title.equals then
return mw.title.equals(p1.lua_title, p2.lua_title)
end
return p1.lua_title == p2.lua_title
end
return B.NotImplemented
end
},
__ne__ = {
__type = "builtin_method", __name = "__ne__",
__call = function(args)
local p1 = mw_private[args[1]]
local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
if p1 and p1.kind == "title" and p2 and p2.kind == "title" then
if mw.title and mw.title.equals then
return not mw.title.equals(p1.lua_title, p2.lua_title)
end
return p1.lua_title ~= p2.lua_title
end
return B.NotImplemented
end
},
__lt__ = {
__type = "builtin_method", __name = "__lt__",
__call = function(args)
local p1 = get_title_priv(args[1], "__lt__")
local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
if not p2 or p2.kind ~= "title" then return B.NotImplemented end
return mw.title.compare(p1.lua_title, p2.lua_title) < 0
end
},
__le__ = {
__type = "builtin_method", __name = "__le__",
__call = function(args)
local p1 = get_title_priv(args[1], "__le__")
local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
if not p2 or p2.kind ~= "title" then return B.NotImplemented end
return mw.title.compare(p1.lua_title, p2.lua_title) <= 0
end
},
__gt__ = {
__type = "builtin_method", __name = "__gt__",
__call = function(args)
local p1 = get_title_priv(args[1], "__gt__")
local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
if not p2 or p2.kind ~= "title" then return B.NotImplemented end
return mw.title.compare(p1.lua_title, p2.lua_title) > 0
end
},
__ge__ = {
__type = "builtin_method", __name = "__ge__",
__call = function(args)
local p1 = get_title_priv(args[1], "__ge__")
local p2 = type(args[2]) == T_TABLE and mw_private[args[2]] or nil
if not p2 or p2.kind ~= "title" then return B.NotImplemented end
return mw.title.compare(p1.lua_title, p2.lua_title) >= 0
end
},
__hash__ = {
__type = "builtin_method", __name = "__hash__",
__call = function(args)
local priv = get_title_priv(args[1], "__hash__")
return RUtils.hash_impl({ priv.lua_title.prefixedText or "" })
end
},
__repr__ = {
__type = "builtin_method", __name = "__repr__",
__call = function(args)
local priv = get_title_priv(args[1], "__repr__")
if not priv.cached_repr then
local ft = priv.lua_title.fullText or priv.lua_title.prefixedText or tostring(priv.lua_title)
priv.cached_repr = string.format("<TitleObject title='%s'>", tostring(ft))
end
return priv.cached_repr
end
}
}
title_dict.__str__ = title_dict.__repr__
TitleObjectClass = PyClass.make_class("TitleObject", { B.object }, title_dict)
return TitleObjectClass
end
wrap_title_object = function(lua_title)
if lua_title == nil then return B.None end
if type(lua_title) == T_TABLE and mw_private[lua_title] and mw_private[lua_title].kind == "title" then
return lua_title
end
local inst = PyClass.make_instance(ensure_title_class())
mw_private[inst] = {
kind = "title",
lua_title = lua_title,
cached_repr = nil,
cached_file = nil
}
return inst
end
local function wrap_title_batch_object(lua_batch)
if not TitleBatchObjectClass then
local tb_dict = {
__new__ = {
__type = "staticmethod",
__func = {
__type = "builtin_method", __name = "__new__",
__call = function()
Runtime.raise("TypeError", "cannot create 'TitleBatchObject' instances directly; use mw.title.newBatch()")
end
}
},
lookupExistence = {
__type = "builtin_method", __name = "lookupExistence",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "lookupExistence")
local priv = mw_private[args[1]]
if not priv or priv.kind ~= "title_batch" then
Runtime.raise("TypeError", "invalid TitleBatchObject instance")
end
priv.lua_batch:lookupExistence()
return args[1]
end
},
getTitles = {
__type = "builtin_method", __name = "getTitles",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "getTitles")
local priv = mw_private[args[1]]
if not priv or priv.kind ~= "title_batch" then
Runtime.raise("TypeError", "invalid TitleBatchObject instance")
end
local raw_titles = priv.lua_batch:getTitles() or {}
local max_i = #raw_titles
for k, _ in pairs(raw_titles) do
if type(k) == "number" and k > max_i then max_i = k end
end
local out = {}
for i = 1, max_i do
out[i] = raw_titles[i] and wrap_title_object(raw_titles[i]) or B.None
end
return { __type = "list", elements = out }
end
},
__repr__ = {
__type = "builtin_method", __name = "__repr__",
__call = function() return "<TitleBatchObject>" end
}
}
tb_dict.__str__ = tb_dict.__repr__
TitleBatchObjectClass = PyClass.make_class("TitleBatchObject", { B.object }, tb_dict)
end
local inst = PyClass.make_instance(TitleBatchObjectClass)
mw_private[inst] = { kind = "title_batch", lua_batch = lua_batch }
return inst
end
-- ==========================================
-- 特化二:FrameObject 與 FrameArgumentType
-- ==========================================
local FrameObjectClass, FrameArgumentTypeClass
local function wrap_frame_argument(lua_arg_obj)
if lua_arg_obj == nil then return B.None end
if not FrameArgumentTypeClass then
local fa_dict = {
__new__ = {
__type = "staticmethod",
__func = {
__type = "builtin_method", __name = "__new__",
__call = function()
Runtime.raise("TypeError", "cannot create 'FrameArgumentType' instances directly")
end
}
},
expand = {
__type = "builtin_method", __name = "expand",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "expand")
if #args ~= 1 then
Runtime.raise("TypeError", "expand() takes 0 positional arguments (" .. (#args - 1) .. " given)")
end
local priv = mw_private[args[1]]
if not priv or priv.kind ~= "frame_arg" then
Runtime.raise("TypeError", "invalid FrameArgumentType instance")
end
local res = priv.lua_obj:expand()
return res ~= nil and res or B.None
end
},
__repr__ = {
__type = "builtin_method", __name = "__repr__",
__call = function() return "<FrameArgumentType>" end
}
}
fa_dict.__str__ = fa_dict.__repr__
FrameArgumentTypeClass = PyClass.make_class("FrameArgumentType", { B.object }, fa_dict)
end
local inst = PyClass.make_instance(FrameArgumentTypeClass)
mw_private[inst] = { kind = "frame_arg", lua_obj = lua_arg_obj }
return inst
end
local function get_frame_priv(obj, caller)
local priv = type(obj) == T_TABLE and mw_private[obj] or nil
if not priv or priv.kind ~= "frame" then
Runtime.raise("TypeError", (caller or "FrameObject") .. " requires a valid 'FrameObject' instance")
end
return priv
end
local function parse_title_and_args(args, kwargs, func_name, default_title)
kwargs = kwargs or {}
if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" and next(kwargs) == nil then
local d_lua = py2lua(args[2])
if d_lua.title ~= nil or d_lua.args ~= nil then
local t_str = d_lua.title ~= nil and extract_title_str(d_lua.title) or default_title
local a_tbl = {}
if type(d_lua.args) == T_TABLE then
for k, v in pairs(d_lua.args) do a_tbl[k] = RUtils.to_str(v, false) end
end
return t_str, a_tbl
end
end
local title_val = kwargs.title
local has_kw_title = not is_none(title_val)
local explicit_args_tbl = kwargs.args
local out_args = {}
if not is_none(explicit_args_tbl) and type(explicit_args_tbl) == T_TABLE then
if explicit_args_tbl.__type == "dict" then
for _, p in ipairs(explicit_args_tbl.pairs) do
out_args[py2lua(p.key)] = RUtils.to_str(p.value, false)
end
elseif explicit_args_tbl.__type == "list" or explicit_args_tbl.__type == "tuple" then
for i, v in ipairs(explicit_args_tbl.elements) do
out_args[i] = RUtils.to_str(v, false)
end
end
end
if has_kw_title then
title_val = extract_title_str(title_val)
for i = 2, #args do
out_args[i - 1] = RUtils.to_str(args[i], false)
end
else
if #args >= 2 and not is_none(args[2]) then
title_val = extract_title_str(args[2])
else
title_val = default_title
end
if #args == 3 and type(args[3]) == T_TABLE and (args[3].__type == "dict" or args[3].__type == "list" or args[3].__type == "tuple") and is_none(explicit_args_tbl) then
if args[3].__type == "dict" then
for _, p in ipairs(args[3].pairs) do
out_args[py2lua(p.key)] = RUtils.to_str(p.value, false)
end
else
for i, v in ipairs(args[3].elements) do
out_args[i] = RUtils.to_str(v, false)
end
end
else
for i = 3, #args do
out_args[i - 2] = RUtils.to_str(args[i], false)
end
end
end
for k, v in pairs(kwargs) do
if k ~= "title" and (k ~= "args" or type(explicit_args_tbl) ~= T_TABLE) then
out_args[k] = RUtils.to_str(v, false)
end
end
return title_val, out_args
end
local function ensure_frame_class()
if FrameObjectClass then return FrameObjectClass end
local frame_dict = {
__new__ = {
__type = "staticmethod",
__func = {
__type = "builtin_method", __name = "__new__",
__call = function()
Runtime.raise("TypeError", "cannot create 'FrameObject' instances directly; use mw.getCurrentFrame() or frame.newChild()")
end
}
},
args = {
__type = "property",
fget = {
__type = "builtin_method", __name = "args",
__call = function(args)
local priv = get_frame_priv(args[1], "args")
if not priv.cached_args then
local kv_pairs = {}
for k, v in pairs(priv.lua_frame.args or {}) do
table.insert(kv_pairs, { key = lua2py(k), value = lua2py(v) })
end
priv.cached_args = { __type = "dict", pairs = kv_pairs }
end
return priv.cached_args
end
}
},
callParserFunction = {
__type = "builtin_method", __name = "callParserFunction",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "callParserFunction")
kwargs = kwargs or {}
if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" and next(kwargs) == nil then
local d_lua = py2lua(args[2])
if d_lua.name ~= nil then
local fn_name = RUtils.to_str(d_lua.name, false)
local raw_a = d_lua.args
if type(raw_a) == T_TABLE then
return priv.lua_frame:callParserFunction{ name = fn_name, args = raw_a }
elseif raw_a ~= nil then
return priv.lua_frame:callParserFunction(fn_name, RUtils.to_str(raw_a, false))
else
return priv.lua_frame:callParserFunction(fn_name)
end
end
end
local fn_name = args[2] or kwargs.name
if is_none(fn_name) then
Runtime.raise("TypeError", "callParserFunction() missing required argument 'name'")
end
fn_name = RUtils.to_str(fn_name, false)
local pos_args = {}
local named_args = {}
local has_named = false
if #args == 3 and type(args[3]) == T_TABLE and (args[3].__type == "list" or args[3].__type == "tuple" or args[3].__type == "dict") and next(kwargs) == nil then
if args[3].__type == "dict" then
for _, p in ipairs(args[3].pairs) do
local k = py2lua(p.key)
local v = RUtils.to_str(p.value, false)
if type(k) == "number" then pos_args[k] = v
else named_args[tostring(k)] = v; has_named = true end
end
else
for i, v in ipairs(args[3].elements) do
pos_args[i] = RUtils.to_str(v, false)
end
end
else
for i = 3, #args do
table.insert(pos_args, RUtils.to_str(args[i], false))
end
end
for k, v in pairs(kwargs) do
if k ~= "name" or args[2] ~= nil then
if k == "args" and type(v) == T_TABLE and #pos_args == 0 then
if v.__type == "dict" then
for _, p in ipairs(v.pairs) do
named_args[RUtils.to_str(p.key, false)] = RUtils.to_str(p.value, false)
has_named = true
end
elseif v.__type == "list" or v.__type == "tuple" then
for i, el in ipairs(v.elements) do pos_args[i] = RUtils.to_str(el, false) end
end
else
named_args[tostring(k)] = RUtils.to_str(v, false)
has_named = true
end
end
end
if not has_named then
return priv.lua_frame:callParserFunction(fn_name, unpack(pos_args))
else
local combined = {}
for i, v in ipairs(pos_args) do combined[i] = v end
for k, v in pairs(named_args) do combined[k] = v end
return priv.lua_frame:callParserFunction(fn_name, combined)
end
end
},
expandTemplate = {
__type = "builtin_method", __name = "expandTemplate",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "expandTemplate")
local t_str, a_tbl = parse_title_and_args(args, kwargs, "expandTemplate", nil)
if not t_str then
Runtime.raise("TypeError", "expandTemplate() missing required argument 'title'")
end
return priv.lua_frame:expandTemplate{ title = t_str, args = a_tbl }
end
},
extensionTag = {
__type = "builtin_method", __name = "extensionTag",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "extensionTag")
kwargs = kwargs or {}
if #args == 2 and type(args[2]) == T_TABLE and args[2].__type == "dict" and next(kwargs) == nil then
local d_lua = py2lua(args[2])
return priv.lua_frame:extensionTag{
name = RUtils.to_str(d_lua.name or "", false),
content = d_lua.content ~= nil and RUtils.to_str(d_lua.content, false) or nil,
args = d_lua.args
}
end
local name = args[2] or kwargs.name
if is_none(name) then
Runtime.raise("TypeError", "extensionTag() missing required argument 'name'")
end
local content = args[3] ~= nil and args[3] or kwargs.content
local ext_args = args[4] ~= nil and args[4] or kwargs.args
local lua_ext_args = nil
if not is_none(ext_args) then
lua_ext_args = py2lua(ext_args)
else
local extra_kw = {}
for k, v in pairs(kwargs) do
if k ~= "name" and k ~= "content" and k ~= "args" then
extra_kw[k] = RUtils.to_str(v, false)
end
end
if next(extra_kw) ~= nil then lua_ext_args = extra_kw end
end
return priv.lua_frame:extensionTag{
name = RUtils.to_str(name, false),
content = not is_none(content) and RUtils.to_str(content, false) or nil,
args = lua_ext_args
}
end
},
getParent = {
__type = "builtin_method", __name = "getParent",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "getParent")
local priv = get_frame_priv(args[1], "getParent")
local parent = priv.lua_frame:getParent()
if not parent then return B.None end
return wrap_frame_object(parent)
end
},
getTitle = {
__type = "builtin_method", __name = "getTitle",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "getTitle")
local priv = get_frame_priv(args[1], "getTitle")
local raw_t = priv.lua_frame:getTitle()
if is_lua_title(raw_t) then return wrap_title_object(raw_t) end
return raw_t
end
},
newChild = {
__type = "builtin_method", __name = "newChild",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "newChild")
local t_str, a_tbl = parse_title_and_args(args, kwargs, "newChild", priv.lua_frame:getTitle())
local child = priv.lua_frame:newChild{ title = t_str, args = a_tbl }
return wrap_frame_object(child)
end
},
preprocess = {
__type = "builtin_method", __name = "preprocess",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "preprocess")
local text = args[2] or (kwargs and kwargs.text)
if type(text) == T_TABLE and text.__type == "dict" then
text = py2lua(text).text
end
if is_none(text) then
Runtime.raise("TypeError", "preprocess() missing required argument 'text'")
end
return priv.lua_frame:preprocess(RUtils.to_str(text, false))
end
},
getArgument = {
__type = "builtin_method", __name = "getArgument",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "getArgument")
local arg_key = args[2] or (kwargs and kwargs.name)
if type(arg_key) == T_TABLE and arg_key.__type == "dict" then
arg_key = py2lua(arg_key).name
end
if is_none(arg_key) then
Runtime.raise("TypeError", "getArgument() missing required argument 'name'")
end
local res = priv.lua_frame:getArgument(py2lua(arg_key))
return wrap_frame_argument(res)
end
},
newParserValue = {
__type = "builtin_method", __name = "newParserValue",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "newParserValue")
local text = args[2] or (kwargs and kwargs.text)
if type(text) == T_TABLE and text.__type == "dict" then
text = py2lua(text).text
end
if is_none(text) then
Runtime.raise("TypeError", "newParserValue() missing required argument 'text'")
end
local res = priv.lua_frame:newParserValue(RUtils.to_str(text, false))
return wrap_frame_argument(res)
end
},
newTemplateParserValue = {
__type = "builtin_method", __name = "newTemplateParserValue",
__call = function(args, kwargs)
local priv = get_frame_priv(args[1], "newTemplateParserValue")
local t_str, a_tbl = parse_title_and_args(args, kwargs, "newTemplateParserValue", nil)
if not t_str then
Runtime.raise("TypeError", "newTemplateParserValue() missing required argument 'title'")
end
local res = priv.lua_frame:newTemplateParserValue{ title = t_str, args = a_tbl }
return wrap_frame_argument(res)
end
},
argumentPairs = {
__type = "builtin_method", __name = "argumentPairs",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "argumentPairs")
local priv = get_frame_priv(args[1], "argumentPairs")
local iter_fn, state, var = priv.lua_frame:argumentPairs()
return make_py_iterator(function()
local k, v = iter_fn(state, var)
var = k
if k == nil then return nil end
return k, v
end)
end
},
__setattr__ = {
__type = "builtin_method", __name = "__setattr__",
__call = function()
Runtime.raise("AttributeError", "'FrameObject' object attributes are read-only")
end
},
__repr__ = {
__type = "builtin_method", __name = "__repr__",
__call = function(args)
local priv = get_frame_priv(args[1], "__repr__")
if not priv.cached_title then
local raw_t = priv.lua_frame:getTitle()
local full_text = tostring(raw_t or "")
if is_lua_title(raw_t) then
full_text = raw_t.fullText or raw_t.prefixedText or full_text
elseif mw.title and mw.title.new and full_text ~= "" then
local ok, t_obj = pcall(mw.title.new, full_text)
if ok and t_obj and t_obj.fullText then
full_text = t_obj.fullText
end
end
priv.cached_title = string.format('<FrameObject title="%s">', full_text)
end
return priv.cached_title
end
}
}
frame_dict.__str__ = frame_dict.__repr__
FrameObjectClass = PyClass.make_class("FrameObject", { B.object }, frame_dict)
return FrameObjectClass
end
wrap_frame_object = function(lua_frame)
if lua_frame == nil then return B.None end
if type(lua_frame) == T_TABLE and mw_private[lua_frame] and mw_private[lua_frame].kind == "frame" then
return lua_frame
end
local inst = PyClass.make_instance(ensure_frame_class())
mw_private[inst] = {
kind = "frame",
lua_frame = lua_frame,
cached_title = nil,
cached_args = nil
}
return inst
end
-- ==========================================
-- 延遲載入命名空間 (Lazy Namespace)
-- ==========================================
local LazyNamespaceClass = PyClass.make_class("module", {}, {
__getattr__ = {
__type = "builtin_method",
__call = function(args)
local self, attr = args[1], args[2]
local raw_tbl = self.__dict._raw_lua_tbl
local val = raw_tbl[attr]
if val == nil then
Runtime.raise("AttributeError", "module '" .. self.__dict._name .. "' has no attribute '" .. attr .. "'")
end
local wrapped_val
if type(val) == T_TABLE and (val.__type == "builtin_method" or val.__type == "class" or val.__type == "instance") then
wrapped_val = val
elseif type(val) == T_FUNC then
wrapped_val = {
__type = self.__dict._is_external and "function" or "builtin_method",
__name = attr,
__qualname__ = attr,
__module__ = self.__dict._name,
__call = function(py_args, py_kwargs)
local lua_args = {}
for _, a in ipairs(py_args or {}) do table.insert(lua_args, py2lua(a)) end
if py_kwargs and next(py_kwargs) then
local kw = {}
for key, kw_val in pairs(py_kwargs) do kw[key] = py2lua(kw_val) end
table.insert(lua_args, kw)
end
local results = { pcall(val, unpack(lua_args)) }
if not results[1] then error(results[2], 0) end
table.remove(results, 1)
if #results == 0 then return B.None end
if #results == 1 then return lua2py(results[1]) end
local tup = {}
for _, r in ipairs(results) do table.insert(tup, lua2py(r)) end
return {__type="tuple", elements=tup}
end
}
elseif type(val) == T_TABLE then
if is_lua_frame(val) then wrapped_val = wrap_frame_object(val)
elseif is_lua_title(val) then wrapped_val = wrap_title_object(val)
elseif is_lua_html(val) then wrapped_val = wrap_html_object(val)
elseif is_lua_svg(val) then wrapped_val = wrap_svg_object(val)
else wrapped_val = make_lazy_namespace(self.__dict._name .. "." .. attr, val) end
else
wrapped_val = lua2py(val)
end
self.__dict[attr] = wrapped_val
return wrapped_val
end
}
})
make_lazy_namespace = function(name, tbl, is_external)
local inst = PyClass.make_instance(LazyNamespaceClass)
inst.__dict._name = name
inst.__dict._raw_lua_tbl = tbl
inst.__dict._is_external = is_external
return inst
end
-- ==========================================
-- 啟動模組匯出與功能性物件特化註冊
-- ==========================================
function MwAPI.get_module()
ensure_title_class()
ensure_frame_class()
local mw_proxy = {}
for k, v in pairs(mw) do mw_proxy[k] = v end
mw_proxy.getCurrentFrame = {
__type = "builtin_method", __name = "getCurrentFrame",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "mw.getCurrentFrame")
if args and #args > 0 then
Runtime.raise("TypeError", "mw.getCurrentFrame() takes 0 positional arguments")
end
local f = mw.getCurrentFrame()
return f and wrap_frame_object(f) or B.None
end
}
mw_proxy.log = {
__type = "builtin_method", __name = "log",
__call = function(py_args)
local out = {}
for _, v in ipairs(py_args or {}) do
table.insert(out, RUtils.to_str(v, false))
end
mw.log(unpack(out))
return B.None
end
}
local function clean_for_log(val, seen)
local t = type(val)
if t == T_TABLE then
seen = seen or {}
if seen[val] then
if val.__type == "list" then return "[...]" end
if val.__type == "dict" then return "{...}" end
if val.__type == "set" then return "set(...)" end
if val.__type == "tuple" then return "(...)" end
return "<...>"
end
seen[val] = true
end
if t ~= T_TABLE then return val end
if val.__type == "list" or val.__type == "tuple" or val.__type == "set" then
local out = {}
for i, v in ipairs(val.elements) do out[i] = clean_for_log(v, seen) end
return out
elseif val.__type == "dict" then
local out = {}
for _, p in ipairs(val.pairs) do
local k = type(p.key) == T_TABLE and RUtils.to_str(p.key, false) or p.key
out[k] = clean_for_log(p.value, seen)
end
return out
elseif val.__type == "LuaInstance" then
return val.__dict._lua_obj
else
return RUtils.to_str(val, false)
end
end
mw_proxy.logObject = {
__type = "builtin_method", __name = "logObject",
__call = function(py_args)
if not py_args or #py_args == 0 then return B.None end
local obj, prefix = py_args[1], py_args[2]
local clean_obj = clean_for_log(obj)
if not is_none(prefix) then
mw.logObject(clean_obj, RUtils.to_str(prefix, false))
else
mw.logObject(clean_obj)
end
return B.None
end
}
mw_proxy.ustring = {}
for k, v in pairs(mw.ustring) do mw_proxy.ustring[k] = v end
mw_proxy.ustring.gmatch = function(s, pattern) return make_py_iterator(mw.ustring.gmatch(s, pattern)) end
mw_proxy.ustring.gcodepoint = function(s, i, j) return make_py_iterator(mw.ustring.gcodepoint(s, i, j)) end
mw_proxy.text = {}
for k, v in pairs(mw.text) do mw_proxy.text[k] = v end
mw_proxy.text.gsplit = function(s, pattern, plain) return make_py_iterator(mw.text.gsplit(s, pattern, plain)) end
-- 1. mw.title 庫特化
if mw.title then
mw_proxy.title = {
equals = {
__type = "builtin_method", __name = "equals",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "mw.title.equals")
if #args ~= 2 then
Runtime.raise("TypeError", "mw.title.equals() takes exactly 2 arguments")
end
return mw.title.equals(py2lua(args[1]), py2lua(args[2]))
end
},
compare = {
__type = "builtin_method", __name = "compare",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "mw.title.compare")
if #args ~= 2 then
Runtime.raise("TypeError", "mw.title.compare() takes exactly 2 arguments")
end
return mw.title.compare(py2lua(args[1]), py2lua(args[2]))
end
},
getCurrentTitle = {
__type = "builtin_method", __name = "getCurrentTitle",
__call = function(args, kwargs)
check_no_kwargs(kwargs, "mw.title.getCurrentTitle")
local t = mw.title.getCurrentTitle()
return t and wrap_title_object(t) or B.None
end
},
new = {
__type = "builtin_method", __name = "new",
__call = function(args, kwargs)
local text_or_id = args[1] or (kwargs and (kwargs.text or kwargs.id))
local ns = args[2] or (kwargs and kwargs.namespace)
if is_none(text_or_id) then return B.None end
local ok, t = pcall(mw.title.new, py2lua(text_or_id), not is_none(ns) and py2lua(ns) or nil)
if not ok or not t then return B.None end
return wrap_title_object(t)
end
},
makeTitle = {
__type = "builtin_method", __name = "makeTitle",
__call = function(args, kwargs)
local ns = args[1] or (kwargs and kwargs.namespace)
local title = args[2] or (kwargs and kwargs.title)
local frag = args[3] or (kwargs and kwargs.fragment)
local iw = args[4] or (kwargs and kwargs.interwiki)
if is_none(ns) or is_none(title) then
Runtime.raise("TypeError", "mw.title.makeTitle() requires at least (namespace, title)")
end
local ok, t = pcall(
mw.title.makeTitle,
py2lua(ns),
RUtils.to_str(title, false),
not is_none(frag) and RUtils.to_str(frag, false) or nil,
not is_none(iw) and RUtils.to_str(iw, false) or nil
)
if not ok or not t then return B.None end
return wrap_title_object(t)
end
},
newBatch = {
__type = "builtin_method", __name = "newBatch",
__call = function(args, kwargs)
if not mw.title.newBatch then
Runtime.raise("NotImplementedError", "mw.title.newBatch is not available in this MediaWiki version")
end
local list_arg = args[1] or (kwargs and kwargs.list)
local def_ns = args[2] or (kwargs and kwargs.defaultNamespace)
if is_none(list_arg) then
Runtime.raise("TypeError", "mw.title.newBatch() missing required list of page titles")
end
local pages = {}
local iter_fn = RUtils.get_iterator(list_arg)
while true do
local hn, item = iter_fn()
if not hn then break end
if type(item) ~= "string" then
Runtime.raise("TypeError", "mw.title.newBatch() list must only contain strings")
end
table.insert(pages, item)
end
local b_obj = mw.title.newBatch(pages, not is_none(def_ns) and py2lua(def_ns) or nil)
return wrap_title_batch_object(b_obj)
end
}
}
end
-- 2. 委派子模組 Module:PythonScript/Mw/Html 掛載 mw.html、mw.svg 與 MarkupBuilderBase 類別家族
MwHtml.attach_to_mw_proxy(mw_proxy)
mw_proxy.TitleObject = TitleObjectClass
mw_proxy.FrameObject = FrameObjectClass
return make_lazy_namespace("mw", mw_proxy)
end
MwAPI.py2lua = py2lua
MwAPI.lua2py = lua2py
MwAPI.wrap_lua_instance = wrap_lua_instance
MwAPI.make_lazy_namespace = make_lazy_namespace
MwAPI.make_py_iterator = make_py_iterator
MwAPI.wrap_frame_object = wrap_frame_object
MwAPI.wrap_title_object = wrap_title_object
MwAPI.wrap_html_object = wrap_html_object
MwAPI.wrap_svg_object = wrap_svg_object
return MwAPI