diff --git a/src/wikitextprocessor/core.py b/src/wikitextprocessor/core.py index caafefec..ee35f0fc 100644 --- a/src/wikitextprocessor/core.py +++ b/src/wikitextprocessor/core.py @@ -1212,7 +1212,7 @@ def expand_args(coded: str, argmap: TemplateArgs) -> str: expand_args(args[0], argmap), parent, True ).strip() self.expand_stack.pop() - if k.isdigit(): + if k.isdigit() and int(k) > 0: k = int(k) else: k = re.sub(r"\s+", " ", k).strip() @@ -1428,7 +1428,7 @@ def expander(arg: str) -> str: # https://en.wikipedia.org/wiki/Help:Template # (but not around unnamed parameters) k, arg = m2.groups() - if k.isdigit(): + if k.isdigit() and int(k) > 0: k = int(k) else: self.expand_stack.append("ARGNAME") diff --git a/src/wikitextprocessor/lua/_sandbox_phase2.lua b/src/wikitextprocessor/lua/_sandbox_phase2.lua index fd400836..bbc0fbc3 100644 --- a/src/wikitextprocessor/lua/_sandbox_phase2.lua +++ b/src/wikitextprocessor/lua/_sandbox_phase2.lua @@ -10,22 +10,37 @@ assert(_new_loadData ~= nil) local function frame_args_index(new_args, key) -- print("frame_args_index", key) - local i = tonumber(key) - if key ~= "inf" and key ~= "nan" and i ~= nil then - key = i - end local v = new_args._orig[key] if v == nil then - return nil + local i = tonumber(key) + if i ~= nil then + key = i + else + return nil + end + v = new_args._orig[key] + if v == nil then + return nil + end end if not new_args._preprocessed[key] then local frame = new_args._frame - v = frame:preprocess(v) + if type(v) == "userdata" then + -- Python tuple in luaexec.call_lua_sandbox.make_frame() + local is_named = v[1] + v = frame:preprocess(v[0]) + -- https://en.wikipedia.org/wiki/Help:Template#Whitespace_handling + if is_named then + v = v:match "^%s*(.-)%s*$" + end + else + v = frame:preprocess(v) + end -- Cache preprocessed value so we only preprocess each argument once new_args._preprocessed[key] = true new_args._orig[key] = v end - -- print("frame_args_index", key, "->", v) + -- print("frame_args_index", key, "->", "'"..v.."'") return v end diff --git a/src/wikitextprocessor/lua/mw.lua b/src/wikitextprocessor/lua/mw.lua index e3b82943..9db0d52d 100644 --- a/src/wikitextprocessor/lua/mw.lua +++ b/src/wikitextprocessor/lua/mw.lua @@ -83,7 +83,90 @@ function mw.clone(v) end function mw.dumpObject(obj) - print("mw.dumpObject", obj) + -- https://github.com/wikimedia/mediawiki-extensions-Scribunto/blob/184216759e22635bd25a0844e6f68979ecf7bc2a/includes/Engines/LuaCommon/lualib/mw.lua#L551 + local doneTable = {} + local doneObj = {} + local ct = {} + local function sorter( a, b ) + local ta, tb = type( a ), type( b ) + if ta ~= tb then + return ta < tb + end + if ta == 'string' or ta == 'number' then + return a < b + end + if ta == 'boolean' then + return tostring( a ) < tostring( b ) + end + return false -- Incomparable + end + local function _dumpObject( object, indent, expandTable ) + local tp = type( object ) + if tp == 'number' or tp == 'nil' or tp == 'boolean' then + return tostring( object ) + elseif tp == 'string' then + return string.format( "%q", object ) + elseif tp == 'table' then + if not doneObj[object] then + local s = tostring( object ) + if string.sub(s, 1, 6) == 'table:' then -- this line changed + ct[tp] = ( ct[tp] or 0 ) + 1 + doneObj[object] = 'table#' .. ct[tp] + else + doneObj[object] = s + doneTable[object] = true + end + end + if doneTable[object] or not expandTable then + return doneObj[object] + end + doneTable[object] = true + + local ret = { doneObj[object], ' {\n' } + local mt = getmetatable( object ) + local indentString = " " + if mt then + ret[#ret + 1] = string.rep( indentString, indent + 2 ) + ret[#ret + 1] = 'metatable = ' + ret[#ret + 1] = _dumpObject( mt, indent + 2, false ) + ret[#ret + 1] = "\n" + end + + local doneKeys = {} + for key, value in ipairs( object ) do + doneKeys[key] = true + ret[#ret + 1] = string.rep( indentString, indent + 2 ) + ret[#ret + 1] = _dumpObject( value, indent + 2, true ) + ret[#ret + 1] = ',\n' + end + local keys = {} + for key in pairs( object ) do + if not doneKeys[key] then + keys[#keys + 1] = key + end + end + table.sort( keys, sorter ) + for i = 1, #keys do + local key = keys[i] + ret[#ret + 1] = string.rep( indentString, indent + 2 ) + ret[#ret + 1] = '[' + ret[#ret + 1] = _dumpObject( key, indent + 3, false ) + ret[#ret + 1] = '] = ' + ret[#ret + 1] = _dumpObject( object[key], indent + 2, true ) + ret[#ret + 1] = ",\n" + end + ret[#ret + 1] = string.rep( indentString, indent ) + ret[#ret + 1] = '}' + return table.concat( ret ) + else + if not doneObj[object] then + ct[tp] = ( ct[tp] or 0 ) + 1 + doneObj[object] = tostring( object ) .. '#' .. ct[tp] + end + return doneObj[object] + end + end + return _dumpObject( obj, 0, true ) end function mw.incrementExpensiveFunctionCount() diff --git a/src/wikitextprocessor/luaexec.py b/src/wikitextprocessor/luaexec.py index 29794b82..9ab9366b 100644 --- a/src/wikitextprocessor/luaexec.py +++ b/src/wikitextprocessor/luaexec.py @@ -135,7 +135,7 @@ def recurse(x: Union[list, tuple, dict]) -> Any: # Convert numeric keys to integers and see if we can make it a # table with sequential integer keys. for k, v in list(x.items()): - if k.isdigit(): + if k.isdigit() and int(k) > 0: del x[k] x[int(k)] = recurse(v) else: @@ -429,7 +429,7 @@ def make_frame( frame_args = {} for k, arg in args.items(): arg = re.sub(r"(?si)(<\s*noinclude\s*/\s*>|\n$)", "", arg) - frame_args[k] = arg + frame_args[k] = (arg, False) else: assert isinstance(args, (list, tuple)) frame_args = {} @@ -437,21 +437,23 @@ def make_frame( for arg in args: # |-separated strings in {{templates|arg=value|...}} m = re.match(r"""(?s)^\s*([^<>="']+?)\s*=\s*(.*?)\s*$""", arg) - if m: - # Have argument name + if m is not None: + # named parameter k, arg = m.groups() - if k.isdigit(): + if k.isdigit() and int(k) > 0: + # Greek wiktionary uses '0', '00' and '000' as + # parameter names... k = int(k) - if k < 1 or k > 1000: + if k > 1000: ctx.warning( - f"Template argument index <1 or >1000: {k=!r}", + f"Template argument index >1000: {k=!r}", sortid="luaexec/477/20230710", ) k = 1000 if num <= k: num = k + 1 else: - # No argument name + # unnamed parameter k = num num += 1 if k in frame_args: @@ -465,7 +467,7 @@ def make_frame( # does not always like them (e.g., remove_links() in # Module:links). arg = re.sub(r"(?si)(<\s*noinclude\s*/\s*>|\n$)", "", arg) - frame_args[k] = arg + frame_args[k] = (arg, m is not None) frame_args_lt: "_LuaTable" = lua.table_from(frame_args) # type: ignore[union-attr] def extensionTag(frame: "_LuaTable", *args: Any) -> str: diff --git a/src/wikitextprocessor/parser.py b/src/wikitextprocessor/parser.py index 3b85013d..a297331c 100644 --- a/src/wikitextprocessor/parser.py +++ b/src/wikitextprocessor/parser.py @@ -632,7 +632,10 @@ def template_parameters(self) -> TemplateParameters: parameter_value = parameter[ equal_sign_index + 1 : ].strip() - if parameter_name.isdigit(): # value contains "=" + if ( + parameter_name.isdigit() + and int(parameter_name) > 0 + ): # value contains "=" parameter_name = int(parameter_name) is_named = False if len(parameter_value) > 0: diff --git a/tests/test_lua.py b/tests/test_lua.py index 4fa12213..f2abc2cf 100644 --- a/tests/test_lua.py +++ b/tests/test_lua.py @@ -598,3 +598,45 @@ def test_mw_uri_anchorEncode(self): return export""", ) self.assertEqual(self.wtp.expand("{{#invoke:test|test}}"), "**") + + def test_el_zero_arg(self): + # https://el.wiktionary.org/wiki/Πρότυπο:ετ + # Unnamed template parameters and numbered parameters can only + # be positive non-zero integers; zero or "00" or negative is a string + self.wtp.start_page("θηλυκός") + self.wtp.add_page( + "Module:test", + 828, + """local export = {} +function export.test(frame) + return tostring(frame.args['0']) .. "|" .. + --tostring(frame.args[0]) .. "|" .. + tostring(frame.args['00']) .. "|" .. + tostring(frame.args[1]) .. "|" .. + tostring(frame.args[2]) .. "|" .. + tostring(frame.args['named']) +end +return export""", + ) + self.assertEqual( + self.wtp.expand( + "{{#invoke:test|test|0= 0 |00= 00 | first |2= second |named= named }}" # noqa: E501 + ), + "0|00| first |second|named", + ) + + def test_el_strip_arg(self): + self.wtp.start_page("θηλυκός") + self.wtp.add_page( + "Module:test", + 828, + """local export = {} +function export.test(frame) + return tostring(frame.args['foo']) +end +return export""", + ) + self.assertEqual( + self.wtp.expand("{{#invoke:test|test|foo= {{#if||}} {{#if||}} }}"), + "", + )