Module:Dump

-- Dump a table to help develop other modules. -- It is also possible to use mw.dumpObject but the result from this -- module is clearer and is close to valid Lua source. -- The main purpose is to allow easy inspection of Wikidata items. -- Preview the following in a sandbox to see entity Q833639 as a Lua table: --  -- Preview the following to dump a built-in table: --

local Collection -- a table to hold items Collection = { add = function (self, item) if item ~= nil then self.n = self.n + 1 self[self.n] = item end end, join = function (self, sep) return table.concat(self, sep) end, remove = function (self, pos) if self.n &#x3e; 0 and (pos == nil or (0 &#x3c; pos and pos &#x3c;= self.n)) then self.n = self.n - 1 return table.remove(self, pos) end end, sort = function (self, comp) table.sort(self, comp) end, new = function return setmetatable({n = 0}, Collection) end } Collection.__index = Collection

local function pre_block(text) -- Pre tags returned by a module do not act like wikitext &#x3c;pre&#x3e;...&#x3c;/pre&#x3e;. return '&#x3c;pre&#x3e;\n' .. mw.text.nowiki(text) .. (text:sub(-1) == '\n' and '' or '\n') .. '&#x3c;/pre&#x3e;\n' end

local function make_tabstr(indent) -- Return a string to generate one level of indent. if indent == 'tab' then -- Tabs do not work well in a browser edit window, but can force them. return '\t' end indent = tonumber(indent) if not (type(indent) == 'number' and 1 &#x3c;= indent and indent &#x3c;= 32) then indent = 4 end return string.rep(' ', indent) end

local function _dumphtml(html, tabwidth) -- Return a pretty-text formatted dump of an html string. -- This assumes clean html, for example, tag "&#x3c;table&#x3e;" not "&#x3c; table &#x3e;". if type(html) ~= 'string' then return '' end local selfClosingTags = { -- from mw.html.lua area = true, base = true, br = true, col = true, command = true, embed = true, hr = true, img = true, input = true, keygen = true, link = true, meta = true, param = true, source = true, track = true, wbr = true, }	local tabstr = make_tabstr(tabwidth) local function indent_pad(depth, isfirst) -- Return a string with an indent to match depth. if depth &#x3e; 0 then return '\n' .. string.rep(tabstr, depth) end return isfirst and '' or '\n' end local function extract(result, html, pos, len, depth, currenttag) -- Dump more of html into table result and return new pos. local has_child while pos &#x3c;= len do			local s, e = html:find('&#x3c;[^&#x3c;&#x3e;]*&#x3e;', pos) if s then if s &#x3e; pos then table.insert(result, html:sub(pos, s-1)) end if html:sub(s+1, s+1) == '/' then -- A closing tag. local tag = html:match('^([a-zA-Z0-9]+)&#x3e;', s+2) or 'NOTAG' if tag == currenttag then local indent = has_child and indent_pad(depth - 1) or '' table.insert(result, indent .. '&#x3c;/' .. tag .. '&#x3e;') else -- Should never happen. table.insert(result, '\n&#x3c;/' .. tag .. '&#x3e;') end return e + 1 end local tag = html:match('^[a-zA-Z0-9]+', s+1) or 'NOTAG' if html:sub(e-1, e-1) == '/' or selfClosingTags[tag] then -- A self-closing tag. table.insert(result, html:sub(s, e)) pos = e + 1 else -- An opening tag. table.insert(result, indent_pad(depth, pos == 1) .. html:sub(s, e)) pos = extract(result, html, e+1, len, depth+1, tag) has_child = true end else table.insert(result, html:sub(pos)) break end end return len + 1 end local result = {} html = html:gsub('&#x3e;%s+&#x3c;', '&#x3e;&#x3c;'):gsub('\n%s*', ' ') extract(result, html, 1, #html, 0) return pre_block(table.concat(result)) end

local function dumphtml(frame) local args = frame.args local pargs = frame:getParent.args local text = args[1] or pargs[1] local indent = args.indent or pargs.indent return _dumphtml(text, indent) end

local function quoted(str) return (string.format('%q', str):gsub('\\\n', '\\n')) end

local function iterkeys(var, control) -- Return an iterator over the keys of var (which should be a table). -- The keys are sorted with numbered keys first, then other types. -- The iterator returns key, repr where key is the actual key, and -- repr is its representation: a number for the ipairs keys, or -- a string, including for number keys above the table length. if type(var) ~= 'table' then return function return nil end end local nums = {} local results = Collection.new for i, _ in ipairs(var) do		nums[i] = true results:add({ i, i }) end local keys = Collection.new for k, _ in pairs(var) do		if not nums[k] then keys:add(k) end end local autoname = control.autoname keys:sort(function (a, b)			local ta, tb = type(a), type(b)			if ta == tb then				if ta == 'number' or ta == 'string' then					return a &#x3c; b				end				if ta == 'boolean' then					return b and not a				end				return autoname(a) &#x3c; autoname(b)			end			if ta == 'number' then				return true			elseif tb == 'number' then				return false			else				return ta &#x3c; tb			end		end) for _, k in ipairs(keys) do		local repr local tk = type(k) if tk == 'number' then repr = '[' .. k .. ']'		elseif tk == 'string' then if k:match('^[%a_][%w_]*$') then repr = k			else repr = '[' .. quoted(k) .. ']'			end elseif tk == 'boolean' then repr = '[' .. tostring(k) .. ']'		else repr = autoname(k) control.needed[repr] = true end results:add({ k, repr }) end local last = 0 return function if last &#x3c; results.n then last = last + 1 return unpack(results[last]) end end end

local function vardump(var, vname, depth, control, self, parents) -- Update items in control with results from dumping a variable. local function put(value, options) options = options or {} local indent = options.indent or depth local comma = (options.kind == 'open' or indent == 0) and '' or ',' control.items:add({			key = (type(vname) == 'string' and options.kind ~= 'close') and vname or nil,			value = value .. comma,			depth = indent,			note = options.note		}) end if var == nil then put('nil') elseif type(var) == 'string' then put(quoted(var)) elseif type(var) == 'table' then local this = control.autoname(var) if depth &#x3e;= control.limitdepth then put(this) elseif parents and parents[this] then control.needed[this] = true if self == this then put(this, {note = 'self'}) control.needed['self'] = true else put(this, {note = 'repeat'}) control.needed['repeat'] = true end else parents = parents or {} parents[this] = true self = this put('{', {kind = 'open', note = this}) local mt = getmetatable(var) if mt then vardump(mt, '__metatable', depth + 1, control, self, parents) end local maxsize = control.items.n + control.limititems for key, keyrep in iterkeys(var, control) do				if control.items.n &#x3e; maxsize then put('...more...') break end vardump(var[key], keyrep, depth + 1, control, self, parents) end put('}', { kind = 'close' }) end elseif type(var) == 'boolean' or type(var) == 'number' then put(tostring(var)) else -- function (or userdata or thread) put(control.autoname(var)) end end

local function dumper(var, vname, tabwidth, wantraw, limititems, limitdepth) -- Return a string representing var in almost-correct Lua syntax. -- There is no newline at the end of the result. local onames = {} local tcounts = {} local function autoname(var) -- Return a string that is a unique name for var, given it is not -- a number or string. if not onames[var] then local name = type(var) tcounts[name] = (tcounts[name] or 0) + 1 onames[var] = name .. '_' .. tcounts[name] end return onames[var] end local control = { autoname = autoname, limititems = limititems or 10000, limitdepth = limitdepth or 50, items = Collection.new, needed = {}, }	vardump(var, tostring(vname or 'variable'), 0, control) local tabstr = make_tabstr(tabwidth) local lines = Collection.new for i, v in ipairs(control.items) do		local indent = string.rep(tabstr, v.depth) local note = v.note if note and control.needed[note] then note = ' -- ' .. note else note = '' end local k = v.key and (v.key .. ' = ') or '' lines:add(indent .. k .. v.value .. note) end local raw = lines:join('\n') return wantraw and raw or pre_block(raw) end

local function dump_testcase(frame) local item if type(frame) == 'table' then item = frame.args[1] else item = frame end if item == 'G' or item == '_G' then return dumper(_G, '_G', frame.args.indent) end local fruit = { 'apple', 'banana', [0] = 'zero', [{'anon'}] = 'anon' } local testcase = { [100] = 'one hundred', [99] = 'ninety nine', [0.5] = 'one half', [-1] = 'negative one', 'one', 'two', [' '] = 'space', ['1 –◆— z'] = 'unicode', alpha = 'aaa', beta = 'bbb', c = 123, data = { dumper = dumper, [dumper] = 'dumper', 'three', 'four', T = true, [true] = 'T', alpha2 = 'aaa2', beta2 = 'bbb2', F = false, [false] = 'F', c2 = 1234, data2 = { 'five', 'six', alpha3 = 'aaa3', beta3 = 'bbb3', c3 = 12345, fruit = fruit, [fruit] = 'fruit', },		},		z = 'zoo', }	testcase.testcase = testcase testcase.data.me = testcase.data testcase.data.data2.me = testcase testcase.data.data2.fruit.back = testcase.data setmetatable(testcase.data, {		__index = function (self, key) return type(key) == 'string' and #key or nil end,		__tostring = function (self) return tostring(#self) end,	}) if item == 'return table' then return testcase end return dumper(testcase, 'testcase', frame.args.indent) end

local function execute(frame) -- Return a dump of the result from executing. -- In general that is not possible in Scribunto so this has built-in code -- to parse some expressions of interest. -- The primary aim is to test the result of calling a Wikidata function -- while previewing an edit in an article. -- Examples of EXPRESSION: --  mw.wikibase.getEntityIdForCurrentPage --  mw.wikibase.getBestStatements('Q868', 'P214') --  mw.wikibase.getBestStatements(Q868, P214)       -- also accepted --  mw.wikibase.getEntity:getDescription('de') --  mw.wikibase.getEntity('Q868'):getDescription('de') -- getEntityObject is an alias for getEntity. -- Using the following gives an "out of memory" error presumably because -- the result is a table with a metatable that dump repeatedly expands. --  mw.title.getCurrentTitle local function params(ptext, first) local p = { first } for item in (ptext .. ','):gmatch('(%S.-)%s*,') do -- Remove any quotes around each parameter because it is already a string. local _, s = item:match(^%s*(['"])(.*)%1%s*$)			table.insert(p, s or tonumber(item) or item)		end		return unpack(p)	end	local expression = frame.args[1] or ''	local text = expression:match('^%s*mw(%..-)%s*$')	if not text then		return 'Expression not recognized: "' .. expression .. '"'	end	-- Look for a supported expression of form 'mw.a.b(c):d.e(f)'.	local entity	local object = mw	local item, ptext, rest = text:match('^%.wikibase%.(%w+)%s*%((.*)%):(.*)$')	if item == 'getEntity' or item == 'getEntityObject' then		entity = mw.wikibase.getEntity(params(ptext))		if not entity then			return 'No entity found for (' .. ptext .. ')'		end		object = entity		text = '.' .. rest -- treat ':' as '.'	end	local upto = 1	for i1, item, i2 in text:gmatch('%.(%w+)') do		if i1 == upto and type(object) == 'table' then			object = object[item]		else			object = nil		end		if object == nil then			return 'Invalid item "' .. item .. '"'		end		if type(object) == 'function' then			if text:sub(i2, i2 + 1) == '' then				object = object				i2 = i2 + 2			end		end		upto = i2	end	local parm = text:sub(upto):match('^%((.*)%)%s*$')	if parm then		object = object(params(parm, entity))	end	return dumper(object, expression) end

local function dumpargs(frame) -- Return text dump of frame.args. -- → display ref strip marker local control = { autoname = function (var) return tostring(var) end, -- should not be called since keys should be numbers or strings }	local lines = Collection.new for key, keyrep in iterkeys(frame.args, control) do lines:add(keyrep .. ' = &#x3c;code&#x3e;' .. mw.text.nowiki(frame.args[key]) .. '&#x3c;/code&#x3e;') end return lines:join('&#x3c;br&#x3e;\n') end

local function parameters(frame) -- Return text dump of args and parent args from frame. -- This is for debugging a module to show what parameters it received. local control = { autoname = function (var) return tostring(var) end, -- should not be called since keys should be numbers or strings }	local lines = Collection.new lines:add('') for _, f in ipairs({ frame, frame:getParent }) do lines:add( .. f:getTitle .. ) for key, keyrep in iterkeys(f.args, control) do lines:add('&#x26;nbsp;&#x26;nbsp;' .. mw.text.nowiki(keyrep .. '=' .. f.args[key])) end end lines:add('') return lines:join('&#x3c;br&#x3e;\n') end

local function wikidata(frame) local item = frame.args[1] if item then local id = item:match('^%s*([PQ]%d+)%s*$') if id then local entity = mw.wikibase.getEntity(id) return dumper(entity, id, frame.args.indent) end end return 'Parameter should be a Wikidata identifier such as P2386 or Q833639' end

local builtins = { -- Handle preview of wikitext like -- where TEXT is a built-in value that can be dumped. __index = function (self, key) local result local function caller return result end if type(key) == 'string' then local title = key:match('^%s*[\'"]?(.*%.tab)[\'"]?%s*$') if title then -- Assume structured data from Commons at c:Data:&. if title:match('^[Dd]ata:') then title = title:sub(6) end local data = mw.ext.data.get(title) -- false if page does not exist result = dumper(data, 'c:Data:' .. title .. '') end end result = result or ('UNKNOWN: ' .. tostring(key)) return caller end }

return setmetatable({	args = dumpargs,	_dump = dumper,	_dumphtml = _dumphtml,	dumphtml = dumphtml,	execute = execute,	parameters = parameters,	testcase = dump_testcase,	wikidata = wikidata, }, builtins)