模組:Reply to

-- MediaWiki后台配置项$wgEchoMaxMentionsCount。这个配置项决定一笔带签名的编辑最多可以同时提及多少使用者。
-- 维基媒体旗下的wiki站点将这个值设定为50。若未来该配置出现变更,或将此模块使用在其他wiki站点,需要修改此值,在本模组内也仅需修改此值。
local WG_ECHO_MAX_MENTIONS_COUNT = 50

-- 引入[[Module:Error]]
local mod_error = require('Module:Error')

--[[
	创建一个由于不当使用模组而产生的错误消息。

	`msg`:错误消息的内容。
]]
local function errormsg(msg)
	return mod_error.error{ [1] = '[[Module:Reply to]]錯誤:' .. msg }
end

--[[
	创建一个由于使用者不当使用模版、完全不提供任何使用者名称而产生的错误消息。

	`template`:使用者不当使用的模版名称。
]]
local function nouser(template)
	return mod_error.error{ [1] = '使用[[' ..  template .. ']]時出現錯誤:並無提供-{zh-hant:使用者名稱;zh-hans:用户名;}-。模板用法見於[[' ..  template .. ']]。' }
end

--[[
	创建一个由于使用者不当使用模版、提供了多于最大数量的使用者名称而产生的错误消息。

	`template`:使用者不当使用的模版名称。
	`count`:模版最多允许加入的使用者个数。
]]
local function maxuser(template, count)
	return mod_error.error{ [1] = '[[' ..  template .. ']]最多-{zh-hans:支持;zh-hant:支援}-提及' .. (count or WG_ECHO_MAX_MENTIONS_COUNT) .. '个-{zh-hant:使用者;zh-hans:用户;}-。如果需要提及超过' .. (count or WG_ECHO_MAX_MENTIONS_COUNT) .. '个-{zh-hant:使用者;zh-hans:用户;}-,必须多次留言。模板用法見於[[' ..  template .. ']]。' }
end

--[[
	创建一个由于使用者不当使用模版、试图以更大数值绕过设定的最大提及数量而产生的错误消息。

	`template`:使用者不当使用的模版名称。
	`max`:试图设定的最大值。
]]
local function invalidmaxoverride(template, max)
	return mod_error.error{ [1] = '试图让[[' ..  template .. ']]允许一次性提及' .. max .. '位-{zh-hant:使用者;zh-hans:用户;}-。MediaWiki限制一次性最多只能提及' .. WG_ECHO_MAX_MENTIONS_COUNT .. '位-{zh-hant:使用者;zh-hans:用户;}-。' }
end

--[[
	创建一个由于使用者不当使用模版、试图提及非法使用者名称而产生的错误消息。

	`username`:非法的使用者名称。
]]
local function invalidusername(username)
	return mod_error.error{ [1] = '试图提及的-{zh-hant:使用者名稱;zh-hans:用户名;}-「' .. username ..'」在技术上不合法。' }
end

--[[
	模块核心内部调用代码。输出字符串编码的提及内容,以及错误消息。

	`echo`:是否触发MediaWiki的echo机制。默认为true。
	`args`:参数列表。允许包含的内容有:
		`1`, `2`, ...:需要提及的使用者名称。只应当从模版参数间接获取。
		`label1`, `label2`, ...:使用者管道链接显示的标签。只应当从模版参数间接读取。
		`c`、`c2`:用作连接各个被提及的使用者的连接符号。如果只有一个使用者,没有连接符号;如果只有两个使用者,使用`c2`;如果有三个或更多使用者,最后一对使用者之间使用`c2`连接,其他使用`c`连接。默认`c`和`c2`都是「、」。只应当从模组参数直接获取。
			`c2`有fallback机制。如果没有指定`c2`,但是指定了`c`,那么`c2`将会使用`c`的值。只应当从模组参数直接获取。
		`@`:用作替代输出结果前缀「@」字样的文字。如不指定则为「@」。只应当从模组参数直接获取。
		`p`:用作替代输出结果后缀「:」字样的文字。如不指定则为「:」。只应当从模组参数直接获取。
		`max`:用作重新设定默认最大提及数量的值。只允许重新设定为更小的数字,否则该参数无效。只应当从模组参数直接获取。
		`template`:使用者调用该方法所使用的模版名称。用作错误消息输出。原则上调用此方法者都需要指定该值,但是该方法也提供一个绝对正确的默认值:「Template:Reply to」。只应当从模组参数直接获取。
		`example`:模组进入示范模式,示范模式会忽略所有提供的使用者名称,只展示User:Example。只应当从模组参数直接获取。
]]
local function reply_core(args, echo)
	local ret = {}			-- 返回内容
	local error = {}		-- 捕获的错误文字

	local template = args.template or 'Template:Reply to'
	if echo == nil then echo = true end

	-- 设定最大提及数量的override。
	local max = tonumber(args.max) or WG_ECHO_MAX_MENTIONS_COUNT
	if max > WG_ECHO_MAX_MENTIONS_COUNT then
		error[#error + 1] = invalidmaxoverride(template, max)
		max = WG_ECHO_MAX_MENTIONS_COUNT
	end

	-- 模版进入示范模式。
	if args.example then
		local link
		if echo then
			link = '-{[[User:' .. args.example .. '|' .. args.example .. ']]}-'
		else
			local url = mw.uri.fullUrl(mw.site.namespaces.User.name .. ':' .. args.example)
			url = tostring(url)
			link = string.format('-{[%s %s]}-', url, args.example)
		end
		return {
			['body'] = (args['@'] or '@') .. link .. (args['p'] or ':'),
			['error'] = '',
		}
	end

	-- 模版不在示范模式。
	-- 生成每一位使用者的wikilink,以table/array方式存储,暂存至ret当中。
	local i = 0
	while true do
		username = args[i + 1]  -- Lua的数组下标从1开始
		if (username ~= nil and username ~= '') then
			-- 检查是否超出数量
			if i >= max then
				error[#error + 1] = maxuser(template, max)
				break
			end

			-- 检查用户名是否合法
			local success, title = pcall(mw.title.new, username)
			-- 註:因為username不該包含命名空間,因此經過解析後必須在條目命名空間
			if not success or not title or title.namespace ~= 0 then
				error[#error + 1] = invalidusername(username)
			else
				if echo then
					ret[#ret + 1] = '-{[[User:' .. username .. '|' .. (args['label' .. (i + 1)] or username)  .. ']]}-'
				else
					local url = mw.uri.fullUrl(mw.site.namespaces.User.name .. ':' .. username)
					url = tostring(url)
					local label = args['label' .. tostring(i + 1)]
					url = string.format('-{[%s %s]}-', url, label or username)
					ret[#ret + 1] = url
				end
			end

			i = i + 1
		else
			break
		end
	end

	local ret_text = ''
	if #ret <= 0 then
		if i <= 0 then
			-- 完全没有提供使用者名称。抛出错误消息。
			error[#error + 1] = nouser(template)
		else
			-- 有提及使用者,但是没有一个使用者的名称是合法的。由于不合法使用者名称已经抛出了消息,所以这里不抛出错误消息。
		end
	else
		ret_text = mw.text.listToText(ret, (args['c'] or '、'), (args['c2'] or args['c'] or '、'))
		ret_text = (args['@'] or '@') .. ret_text .. (args['p'] or ':')
	end

	local ret_err = mw.text.listToText(error, '', '')

	return {
		['body'] = ret_text,
		['error'] = ret_err,
	}
end

----------- 导出方法 -----------

local p = {}

--[[
	对传入模版/模组的参数进行预先处理。
	处理原则:传入模版的参数`1`、`2`、`label1`、`label2`等,与传入模组的参数`c`、`c2`、`@`、`p`合并。
	此外,对于参数 `1`、`2` 和 `label1`、`label2` 等,该方法会将它们按自然数顺序重组。因此传入编号1、2、4、10的参数,会被重组为1、2、3、4。
]]
local function getargs(frame)
	local moduleargs = frame.args	-- 传入模组的参数,{{#invoke:Reply to|method|...}}
	local templateargs = {}			-- 传入模版的参数,{{Reply to|...}}
	if frame:getParent() then
		templateargs = frame:getParent().args
	end

	local tmp = {}
	-- 加入传入模版的参数
	for k, v in pairs(templateargs) do
		if type(k) == 'number' then
			tmp[k] = v
		elseif k:sub(0, 5) == 'label' then
			if v == '' then
				tmp[k] = '&#x200B;' -- 如果提供了labelx参数,但是参数数值留空,则改为不换行空格&nbsp;。
			else
				tmp[k] = v
			end
		end
	end
	-- 重组加入模版的参数
	local n = 1
	local merged = {}
	for k, v in pairs(tmp) do
		if type(k) == 'number' then
			merged[n] = v
			merged['label' .. n] = tmp['label' .. k]
			n = n + 1
		end
	end
	-- 加入传入模组的参数
	local allowedfixednameparam = {
		['c'] = true,
		['c2'] = true,
		['@'] = true,
		['p'] = true,
		['max'] = true,
		['template'] = true,
		['example'] = true,
	}
	for k, v in pairs(moduleargs) do
		if allowedfixednameparam[k] then
			merged[k] = v
		end
	end

	return merged
end

--[[
	{{Reply to}}的主入口。
]]
function p.replyto(frame)
	local args = getargs(frame)
	args['template'] = args['template'] or (frame:getParent() and frame:getParent():getTitle()) or 'Template:Reply to'

	local data = reply_core(args, true)
	return mw.text.tag('span', {['class']='template-ping'}, data['body']) .. data['error']
end

--[[
	p.replyto的兼容用别名。以函数指针实现,因而在调用此方法时可以少一层嵌套。
]]
p.ping = p.replyto

--[[
	{{Unping}}的主入口。
]]
function p.unping(frame)
	local args = getargs(frame)
	args['template'] = args['template'] or (frame:getParent() and frame:getParent():getTitle()) or 'Template:Unping'

	local data = reply_core(args, false)
	return mw.text.tag('span', {['class']='plainlinks'}, data['body']) .. data['error']
end

--[[
	{{Noping}}的主入口。
]]
function p.hidden_ping(frame)
	local args = getargs(frame)
	args['template'] = args['template'] or (frame:getParent() and frame:getParent():getTitle()) or 'Template:Noping'

	local data = reply_core(args, true)
	return mw.text.tag('span', {['style']='display:none'}, data['body']) .. data['error']
end

return p