모듈: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:
--   Q833639
-- Preview the following to dump a built-in table:
--   testcase
local Collection  -- a table to hold items
Collection = {
	add = function (self, item)
		if item ~= nil then
			self.n = self.n + 1
			selfself.n = item
		end
	end,
	join = function (self, sep)
		return table.concat(self, sep)
	end,
	remove = function (self, pos)
		if self.n > 0 and (pos == nil or (0 ....
	return '\n' ..
		mw.text.nowiki(text) ..
		(text:sub(-1) == '\n' and  or '\n') ..
		'\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 " not "".
	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 > 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 *>', pos)
			if s then
				if s > 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+)>', s+2) or 'NOTAG'
					if tag == currenttag then
						local indent = has_child and indent_pad(depth - 1) or 
						table.insert(result, indent .. )
					else
						-- Should never happen.
						table.insert(result, '\n')
					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 selfClosingTagstag 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('>%s+= control.limitdepth then
			put(this)
		elseif parents and parentsthis then
			control.neededthis = 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 {}
			parentsthis = 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 > maxsize then
					put('...more...')
					break
				end
				vardump(varkey, 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 onamesvar then
			local name = type(var)
			tcountsname = (tcountsname or 0) + 1
			onamesvar = name .. '_' .. tcountsname
		end
		return onamesvar
	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.needednote 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.args1
	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 EXPRESSION.
	-- 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.args1 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 = objectitem
		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 .. ' = ' .. mw.text.nowiki(frame.argskey) .. )
	end
	return lines:join('\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('  ' .. mw.text.nowiki(keyrep .. '=' .. f.argskey))
		end
	end
	lines:add()
	return lines:join('\n')
end
local function wikidata(frame)
	local item = frame.args1
	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 TEXT
	-- 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('^Ddata:') 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)