模組:TemplateExist/sandbox

local p = {}
local yesno = require("Module:Yesno")
local func_removeNoinclude = {}
function p.templateExist(input_data, _depth, _max_depth, _recursion_list, _removeNoinclude)
	--===讀取Lua調用之參數 和 提供預設值===
	local template_name = input_data --待判斷模板名稱
	local count = 1 --幾個該模板視為存在 (預設為有1個就視為存在)
	local return_count = false --返回值是否為模板數量,"是" 返回模板數量、"否" 返回存在與否
	local depth = _depth or 0 --目前搜索深度,預設為0
	local max_depth = _max_depth or 1 --最大搜索深度,預設為一層
	local recursion_list = {} --已造訪過的模板
	for key, value in pairs(_recursion_list or {}) do recursion_list[key] = value end --寫入先前已造訪過的模板
	local removeNoinclude = _removeNoinclude or (function(_str)return _str end) --內文前處理函數
	local currentTitle = mw.title.getCurrentTitle() --要找模板的頁面,預設為所在頁面
	--===讀取模板調用之參數===
	if type(input_data) == type({}) and type(input_data.args) == type({}) then
		--參數 |1= 模板名稱
		template_name = input_data.args[1] or input_data.args['1'] or ''
		--參數 |count= 多少數量視為存在;或是否返回數量
		local _count = tonumber(input_data.args.count)
		if type(_count) == type(nil) then --count參數不是數字時
			--檢查count參數是不是布林值
			return_count = yesno(tostring(input_data.args.count))
		end
		count = _count or count --多少數量模板視為存在
		--參數 |depth= 最大搜尋深度
		max_depth = tonumber(input_data.args.depth) or max_depth
		--參數 |page= 要尋找模板的頁面。未輸入使用本調用所在頁面
		local page = input_data.args.page --是否檢查特定頁面
		if mw.text.trim(page or '') ~= '' then --有輸入頁面名稱
			--檢查頁面名稱是否合法
			local success, page_obj = pcall(mw.title.new, page)
			if success and type((page_obj or {}).getContent) == type(tostring) then
				--頁面名稱是否合法的話,就檢查參數 |page= 提供的頁面
				currentTitle = page_obj
			end
		end
	end
	local function _return(_value) -- 返回值模式
		return return_count
			and _value --如果是模板計數,返回指定模板數量
			or (_value >= count) --否則返回模板存在與否
	end
	--===開始判斷模板是否存在===
	--取得判斷目標的模板
	local template_title = p.getTemplateTitle(template_name)
	--模板計數 (還沒開始找模板,所以是0)
	local template_count = 0
	if --如果模板和待判斷頁面都合法
		type((template_title or {}).getContent) == type(tostring) and
		type((currentTitle or {}).getContent) == type(tostring) 
	then --開始尋找模板
		--如發現模板循環,直接退出
		if mw.title.equals(template_title, currentTitle) then return _return(0) end
		if recursion_list[tostring(currentTitle)] then return _return(0) end
		recursion_list[tostring(currentTitle)] = true
		--取得待判斷頁面內容
		local function _remove_html_comments(html)
		    local comment_pattern = "<!--.-?-->"
		    local result = string.gsub(html, comment_pattern, "")
		    return result
		end
		local content = removeNoinclude(currentTitle:getContent() or '')
		content = _remove_html_comments(content)
		--檢查待判斷頁面中所有格式為 {{XXX 的內容
		for capture in mw.ustring.gmatch('-'..content, "[^%{]%{%{%s*([^%{%}%|:]+)%s*[%}%|]") do
			--解析出 {{XXX 的模板名稱
			local check_template_name = mw.text.trim(capture or '')
			local check_module = mw.ustring.match(check_template_name, "%s*#%s*[Ii][Nn][Vv][Oo][Kk][Ee]%s*:%s*([^%{%}%|:]+)%s*$")
			if check_module then check_template_name = "Module:"..check_module end
			--取得該模板名稱對應的模板物件
			local check_template_title = p.getTemplateTitle(check_template_name)
			if check_template_title then --模板物件存在
				--看看該模板跟待判斷的模板是否為同一個
				if mw.title.equals(check_template_title, template_title) then
					--如果是,模板計數遞增
					template_count = template_count + 1
				elseif depth + 1 < max_depth then --是否深度搜索
					func_removeNoinclude = (type(func_removeNoinclude) == type(tostring)) and func_removeNoinclude or require("Module:Special_wikitext/Custom_Module/tools").removeNoinclude
					template_count = template_count --看看待判斷的模板是否在其他模板裡面
						+ p.templateExist( --進入模板判斷
							{args={
								tostring(template_title), --要判斷的模板保持不變
								count=true, --計數模式
								page=tostring(check_template_title) --進入模板
							}},
							depth + 1, max_depth, recursion_list, --增加深度
							func_removeNoinclude
						)
					--紀錄已進入過的模板,避免模板循環
					recursion_list[tostring(check_template_title)] = true
				end --模板跟待判斷的模板不是同一個,不做動作
			end --判斷下一個模板
		end --所有模板檢查完畢
	end
	return _return(template_count) --偵測完畢,返回結果
end

-- https://doc.wikimedia.org/mediawiki-core/1.31.5/php/MessagesZh__hans_8php.html#af2f5c876bcf2c2f36d673e49a40965a3 中提取。不包括名称包含#、__、:,或在站内未生效的部分魔术字。
-- TODO: https://doc.wikimedia.org/mediawiki-core/1.31.5/php/MessagesZh__hant_8php.html#af2f5c876bcf2c2f36d673e49a40965a3 未加入。需更好检测方案。可能有扩展的魔术字未加入?
local mwFunctionNames = { 
	'本月', '本月2', 'CURRENTMONTH', 'CURRENTMONTH2',
	'本月1', 'CURRENTMONTH1',
	'本月名', '本月名称', 'CURRENTMONTHNAME',
	'本月名属格', '本月名称属格', 'CURRENTMONTHNAMEGEN',
	'本月简称', 'CURRENTMONTHABBREV',
	'今天', 'CURRENTDAY',
	'今天2', 'CURRENTDAY2',
	'星期', '今天名', '今天名称', 'CURRENTDAYNAME',
	'今年', 'CURRENTYEAR',
	'当前时间', '此时', 'CURRENTTIME',
	'当前小时', 'CURRENTHOUR',
	'本地月', '本地月2', 'LOCALMONTH', 'LOCALMONTH2',
	'本地月1', 'LOCALMONTH1',
	'本地月份名', 'LOCALMONTHNAME',
	'本地月历', 'LOCALMONTHNAMEGEN',
	'本地月缩写', 'LOCALMONTHABBREV',
	'本地日', 'LOCALDAY',
	'本地日2', 'LOCALDAY2',
	'本地日名', 'LOCALDAYNAME',
	'本地年', 'LOCALYEAR',
	'本地时间', 'LOCALTIME',
	'本地小时', 'LOCALHOUR',
	'页面数', 'NUMBEROFPAGES',
	'条目数', 'NUMBEROFARTICLES',
	'文件数', 'NUMBEROFFILES',
	'用户数', 'NUMBEROFUSERS',
	'活跃用户数', 'NUMBEROFACTIVEUSERS',
	'编辑数', 'NUMBEROFEDITS',
	'页名', '页面名', '页面名称', 'PAGENAME',
	'页面名等同', '页面名称等同', 'PAGENAMEE',
	'名字空间', 'NAMESPACE',
	'名字空间等同', 'NAMESPACEE',
	'名字空间编号', 'NAMESPACENUMBER',
	'讨论空间', '讨论名字空间', 'TALKSPACE',
	'讨论空间等同', '讨论名字空间等同', 'TALKSPACEE',
	'主名字空间', '条目名字空间', 'SUBJECTSPACE', 'ARTICLESPACE',
	'主名字空间等同', '条目名字空间等同', 'SUBJECTSPACEE', 'ARTICLESPACEE',
	'页面全称', '完整页面名称', 'FULLPAGENAME',
	'完整页面名称等同', 'FULLPAGENAMEE',
	'子页面名称', 'SUBPAGENAME',
	'子页面名称等同', 'SUBPAGENAMEE',
	'根页面名称', 'ROOTPAGENAME',
	'根页面名称等同', 'ROOTPAGENAMEE',
	'基础页面名称', 'BASEPAGENAME',
	'基础页面名称等同', 'BASEPAGENAMEE',
	'讨论页面名称', '对话页面名称', 'TALKPAGENAME',
	'讨论页面名称等同', '对话页面名称等同', 'TALKPAGENAMEE',
	'主名字空间页面名称', '条目页面名称', 'SUBJECTPAGENAME', 'ARTICLEPAGENAME',
	'主名字空间页面名称等同', '条目页面名称等同', 'SUBJECTPAGENAMEE', 'ARTICLEPAGENAMEE',
	'站点名称', 'SITENAME',
	'条目路径', 'ARTICLEPATH',
	'页面ID', 'PAGEID',
	'服务器', 'SERVER',
	'服务器名', 'SERVERNAME',
	'脚本路径', 'SCRIPTPATH',
	'样式路径', 'STYLEPATH',
	'本周', 'CURRENTWEEK',
	'当前DOW', 'CURRENTDOW',
	'本地周', 'LOCALWEEK',
	'本地DOW', 'LOCALDOW',
	'修订ID', 'REVISIONID',
	'修订日', 'REVISIONDAY',
	'修订日2', 'REVISIONDAY2',
	'修订月', 'REVISIONMONTH',
	'修订月1', 'REVISIONMONTH1',
	'修订年', 'REVISIONYEAR',
	'修订时间戳', 'REVISIONTIMESTAMP',
	'修订用户', 'REVISIONUSER',
	'修订大小', 'REVISIONSIZE',
	'显示标题', 'DISPLAYTITLE',
	'当前版本', 'CURRENTVERSION',
	'当前时间戳', 'CURRENTTIMESTAMP',
	'本地时间戳', 'LOCALTIMESTAMP',
	'方向标记', 'DIRECTIONMARK', 'DIRMARK',
	'内容语言', 'CONTENTLANGUAGE', 'CONTENTLANG',
	'管理员数', 'NUMBEROFADMINS',
	'分类中页面数', 'PAGESINCATEGORY', 'PAGESINCAT',
	'页面大小', 'PAGESIZE',
	'组中用户数', 'NUMBERINGROUP', 'NUMINGROUP',
	'保护级别', 'PROTECTIONLEVEL',
	'级联来源', 'CASCADINGSOURCES',
}
local function _ismwFun(input)
    for _, pattern in ipairs(mwFunctionNames) do
        if input == pattern then
            return true
        end
    end
    return false
end

function p.getTemplateTitle(input_data)
	local template_name = input_data
	local is_lua = true
	if type(input_data) == type({}) and type(input_data.args) == type({}) then
		--讀取模板呼叫的參數
		template_name = input_data.args[1] or input_data.args['1'] or ''
	end
	local function _return(_value) --區分模板呼叫或模組呼叫
		if is_lua then return _value
		else return _value and tostring(_value) or '' end
	end
	template_name = mw.text.trim(template_name or '')
	--未輸入,返回空
	if template_name == '' then return _return() end

	template_name = template_name:gsub("^%l", string.upper) -- 首字母大写 -- TODO: STYLEPATH等部分魔术字为大小写不敏感(定义为'0')
	if _ismwFun(template_name) then return _return() end -- 调用的是MediaWiki变量或解析器函数。返回空

	--嘗試取得待偵測模板
	local success, template_obj = pcall(mw.title.new, template_name, "Template")
	--取得失敗返回空
	if (not success) or type((template_obj or {}).getContent) ~= type(tostring) then return _return() end
	--取得模板原名 (模板可能是重定向)
	local page_name = tostring(template_obj)
	local template_title = (template_obj or{}).redirectTarget or template_obj
	local redircct_name = tostring(template_title)
	--如果模板不能讀取內容 (它是假的模板) 返回空
	if type((template_title or{}).getContent) ~= type(tostring)then return _return() end
	--如果模板存在,返回模板物件
	if type(template_title:getContent()) == type("string") then
		return _return(template_title)
	end
	local lib_zhcvt = require('Module:ZhConversion') --繁簡轉換
	for _, check_title in ipairs({ --列舉繁簡的可能
		lib_zhcvt.to_hant(page_name), lib_zhcvt.to_hans(page_name),
		lib_zhcvt.to_hant(redircct_name), lib_zhcvt.to_hans(redircct_name),
	}) do --檢查各種繁簡模式
		--嘗試取得該繁簡模式的模板
		local _success, check_page = pcall(mw.title.new, check_title, "Template")
		--如果是可讀取的模板
		if _success and type(check_page.getContent) == type(tostring) then
			--排除重新導向
			check_page = check_page.redirectTarget or check_page
			--模板存在,返回模板物件
			if type(check_page:getContent()) == type("string") then
				return _return(check_page)
			end
		end
	end
	--測試不通過,返回空
	return _return()
end
return p