モジュール:削除依頼ログ

local yesno = require('Module:Yesno')
---Check whether an array includes a certain value
---@param array table
---@param value any
---@return boolean
local function includes(array, value)
	for _, v in ipairs(array) do
		if v == value then
			return true
		end
	end
	return false
end
---Push a value into an array if the array doesn't already have the value
---@param array table
---@param value any
local function push(array, value)
	if not includes(array, value) then
		table.insert(array, value)
	end
end
-------------------------------------------------------------------------------
-- AfDLog class
-------------------------------------------------------------------------------
---@class AfDLog
---@field talk boolean Whether the log is for the talk page
---@field collapse boolean|number Whether to collapse some data rows
---@field numbered boolean **Disabled.** Whether to number up the list
---@field reversed boolean Whether to reverse the order of data rows
---@field currentTitle table The title object for the current page
---@field omitCat boolean Whether to omit error categories
---@field pageList table Keyed by pagetitles and valued by boolean values (true for existing pages)
---@field errors {invalidPage:boolean,invalidDate:boolean,badParameters:string,cats:string} Used to add error categories
---@field _suppressPageError boolean **Private parameter** Whether to supuress page-related errors
---@field pageType string 「この`pageType`は過去に削除依頼の...」
---@field rowData table An array of objects to render row data
local AfDLog = {}
AfDLog.__index = AfDLog
---Create a new AfDLog object instance.
---@param args table
---@return AfDLog
function AfDLog.new(args)
	local self = setmetatable({}, AfDLog)
	-- Set class properties
	self.talk = not not yesno(args.talk)
	self.collapse = tonumber(args.collapse) or not not yesno(args.collapse)
	self.numbered = true --yesno(args.numbered) == nil or not not yesno(args.numbered)
	self.reversed = yesno(args.reversed) == nil or not not yesno(args.reversed)
	self.currentTitle = mw.title.getCurrentTitle()
	self.omitCat = (function()
		local patterns = {
			'^Template:(削除依頼ログ)$',
			'^Template:(削除依頼ログ/.+)$',
			'^Template:(削除済みノート23?)$',
			'^Template:(削除済みノート23?/.+)$',
			'^Template:(不削除ノート)$',
			'^Template:(不削除ノート/.+)$',
			'^Template:(特定版削除済みノート)$',
			'^Template:(特定版削除済みノート/.+)$',
			'^Template:(版指定削除済みノート)$',
			'^Template:(版指定削除済みノート/.+)$',
			'^Template:(削除済みノートページ2?)$',
			'^Template:(削除済みノートページ2?/.+)$'
		}
		for _, pat in ipairs(patterns) do
			if string.find(self.currentTitle.prefixedText, pat) then
				return true
			end
		end
		return false
	end)()
	self.pageList = {}
	self.errors = {
		invalidPage = false, -- True if some page doesn't exist
		invalidDate = false, -- True if some date parameter has an unsupported format
		badParameters = {}, -- An array of names of undefined parameter keys
		cats = {} -- An array of error categories (`%s` in `Category:削除依頼ログエラー/%s`)
	}
	self._suppressPageError = args._suppressPageError == 'true'
	self.pageType = 'ページ'
	self.rowData = {}
	-- Get page type
	if self.talk then
		self.pageType = 'ノート'
	else
		local subjTitle = self.currentTitle.subjectPageTitle
		local pageTypes = {
			0 = '記事',
			2 = '利用者ページ',
			4 = 'プロジェクトページ',
			6 = 'ファイル',
			8 = 'インターフェースページ',
			10 = 'テンプレート',
			12 = 'ヘルプページ',
			14 = 'カテゴリ',
			100 = 'ポータル',
			102 = 'プロジェクト',
			828 = 'モジュール'
		}
		if pageTypessubjTitle.namespace then
			self.pageType = pageTypessubjTitle.namespace
		end
	end
	-- Process data paremeters (result, page, date)
	---@param date string
	---@return string|nil YYYY年MM月DD日 Nil if the input date has an unsupported format
	local function formatDate(date)
		local patterns = {
			'^(%d%d%d%d)年(%d%d?)月(%d%d?)日$',
			'^(%d%d%d%d)年(%d%d?)月$',
			'^(%d%d%d%d)%-(%d%d)%-(%d%d)T%d%d:%d%d:%d%dZ?$',
			'^(%d%d%d%d)%-(%d%d)%-(%d%d)$',
			'^(%d%d%d%d)%-(%d%d)$'
		}
		for _, pat in ipairs(patterns) do
			local y, m, d = string.match(date, pat)
			if y and m then
				return string.format('%s%s%s',
					y:gsub('^0', ) .. '年',
					m:gsub('^0', ) .. '月',
					d and d:gsub('^0', ) .. '日' or 
				)
			end
		end
		return nil
	end
	---@param prefix "result" | "page" | "fullpage" | "display" | "date" | "note"
	---@param num number
	---@param val string
	local function setRowData(prefix, num, val)
		if not self.rowDatanum then -- Set default data
			self.rowDatanum = {
				result = '削除',
				page = 'Wikipedia:削除依頼/', -- No automatic substitution: Causes problems if the page is moved afterwards
				fullpage = nil,
				display = nil,
				date = nil,
				note = nil
			}
		end
		if prefix == 'page' then
			-- Remove leading/trailing brackets and spaces
			-- Note: 'page' is a subpage name and leading spaces shouldn't be removed (XXX/A and XXX/_A are different)
			local m = string.match(val, '^%*(.-)%s_%*$')
			if m then
				val = 'Wikipedia:削除依頼/' .. m
			end
		elseif prefix == 'fullpage' then
			-- Remove leading/trailing brackets and spaces
			local m = string.match(val, '^%s_%*(.-)%s_%*$')
			if m then
				val = m
			end
		elseif prefix == 'date' then
			local date = formatDate(val)
			if date then
				val = date
			else
				self.errors.invalidDate = true
				return
			end
		end
		self.rowDatanumprefix = val -- Set the specified data
	end
	-- List of accepted parameter names; true for ordered parameters and false for unordered ones
	local acceptedPrefixes = {
		result = true,
		page = true,
		fullpage = true,
		display = true,
		date = true,
		note = true,
		talk = false,
		collapse = false,
		reversed = false,
		_suppresspageerror = false, -- Private parameter
		-- numbered = false -- Disabled parameter
	}
	for k, v in pairs(args) do
		local key = mw.ustring.lower(k)
		local prefix, num = key:match('^(.-)(%d*)$')
		if prefix and acceptedPrefixesprefix ~= nil and num ~= '0' then
			num = tonumber(num)
			if acceptedPrefixesprefix == true and num then
				setRowData(prefix, num, v)
			elseif acceptedPrefixesprefix == false and not num then
				-- Do nothing
			else
				push(self.errors.badParameters, k)
			end
		else
			push(self.errors.badParameters, k)
		end
	end
	---Remove any gaps in the array we made.
	---@param t table
	---@return table
	local function compressSparseArray(t)
		local ret, nums = {}, {}
		for num, _ in pairs(t) do
			nums#nums + 1 = num
		end
		table.sort(nums)
		for i, num in ipairs(nums) do
			reti = tnum
		end
		return ret
	end
	self.rowData = compressSparseArray(self.rowData)
	return self
end
---Render the AfDLog instance as a message box.
---@return string
function AfDLog:renderBox()
	return require('Module:Message box').main('tmbox', {
		type = 'notice',
		image = '削除依頼',
		text = self:renderBoxText()
	})
end
function AfDLog:renderBoxText()
	local nRows = #self.rowData
	local ret = {}
	ret#ret + 1 = string.format(
		'この%sは%sに削除依頼の審議対象になりました。',
		self.pageType,
		nRows == 1 and self.rowData1.date or '過去'
	)
	if nRows > 1 then
		ret#ret + 1 = '新しく依頼を提出する場合、以下を参考にしてください。\n'
		ret#ret + 1 = self:renderMultipleRows()
	elseif nRows == 1 then
		ret#ret + 1 = self:renderFirstRow()
	else -- There's no rowData
		-- Do nothing
	end
	ret#ret + 1 = self:renderErrors()
	return table.concat(ret)
end
function AfDLog:renderMultipleRows()
	local root = mw.html.create()
	local nRows = #self.rowData
	local i = self.reversed and nRows or 1
	local nCollapsedRows
	if type(self.collapse) == 'number' then
		self.collapse = math.ceil(self.collapse) -- Ensure that the number is an integer
		if self.collapse >= 0 then
			-- If the number is positive, collapse `collapse`-many rows
			if self.collapse > nRows then
				self.collapse = nRows
			end
			nCollapsedRows = self.collapse
		else
			-- If the number is negative, expand `collapse`-many rows
			if self.collapse  0
	local function makeList(isCollapsed, header)
		local tableRoot = root:tag('table')
		tableRoot
			:addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil)
			:css('width', '100%')
		if header then
			tableRoot
				:tag('tr')
					:tag('th')
						:wikitext(header)
		end
		return tableRoot
			:tag('tr')
				:tag('td')
					:tag(self.numbered and 'ol' or 'ul')
	end
	-- Render normal rows
	if hasNormalRows then
		local normalList = makeList(false)
		if self.reversed then
			while i >= 1 and i > nCollapsedRows do
				self:renderRow(i, normalList)
				i = i - 1
			end
		else
			while i  0 then
		local header
		if hasNormalRows then
			header = '過去の削除依頼:'
		else
			header = '削除依頼:'
		end
		local collapsedList = makeList(true, header)
		if self.reversed then
			while i >= 1 do
				self:renderRow(i, collapsedList)
				i = i - 1
			end
		else
			while i %s %s (%s; %s)', data.result, link, data.date, data.note)
	elseif data.date or data.note then
		wkt = string.format('%s %s (%s)', data.result, link, data.date or data.note)
	else
		wkt = string.format('%s %s', data.result, link)
	end
	html
		:tag('li')
			:attr('value', self.numbered and rowNum or nil)
			:wikitext(wkt)
end
function AfDLog:renderFirstRow()
	local data = self.rowData1
	local ret = {}
	if data.fullpage then
		ret#ret + 1 = string.format(
			'議論の結果、%sとなりました。詳細は%sをご覧ください。',
			data.result,
			self:addPage(data)
		)
	else
		ret#ret + 1 = string.format(
			'%sの結果、%sとなりました。',
			self:addPage(data, '議論'),
			data.result
		)
	end
	if data.note then
		ret#ret + 1 = string.format('(%s)', data.note)
	end
	return table.concat(ret)
end
---Clean up pagetitle, check its existence, and set the 'invalidPage' parameter to true if there's a problem.
---@param data table rowData object
---@param caption? string Priority: caption > data.display > data.fullpage > data.page (default)
---@return string wikilink
function AfDLog:addPage(data, caption)
	caption = string.gsub(caption or data.display or data.fullpage or data.page, '_', ' ')
	local key = data.fullpage and 'fullpage' or 'page' -- Override 'page' by 'fullpage' if the latter is specified
	local title = mw.title.new(datakey)
	local isValidPage = title and datakey ~= 'Wikipedia:削除依頼/'
	if isValidPage then
		datakey = title.prefixedText
		if self.pageListdatakey == nil and not self._suppressPageError then
			self.pageListdatakey = title.exists
		else
			self.pageListdatakey = true
		end
	else
		self.pageListdatakey = false
	end
	if not self.errors.invalidPage and not self.pageListdatakey then
		self.errors.invalidPage = true
	end
	if isValidPage then
		return string.format(
			'%s',
			title.prefixedText .. (title.fragment ~=  and ('#' .. title.fragment) or ),
			caption
		)
	else
		return string.format(
			'%s%s%s',
			mw.text.nowiki(),
			datakey,
			mw.text.nowiki()
		)
	end
end
function AfDLog:renderErrors()
	local function errorMessage(code)
		return string.format('エラー: %s', code)
	end
	local ret = {}
	if not self.rowData1 then
		ret#ret + 1 = errorMessage('必須引数が指定されていません')
		self:addCategory('引数未指定')
	end
	if #self.errors.badParameters > 0 then
		ret#ret + 1 = errorMessage(string.format(
			'不正な引数が指定されています (%s)',
			table.concat(self.errors.badParameters, ', ')
		))
		self:addCategory('不正な引数')
	end
	if self.errors.invalidPage then
		ret#ret + 1 = errorMessage('不正なページ名が指定されています')
		self:addCategory('不正なページ名')
	end
	if self.errors.invalidDate then
		self:addCategory('不正なdate引数')
	end
	ret#ret + 1 = self:renderCategories()
	return table.concat(ret)
end
function AfDLog:addCategory(subcat)
	push(self.errors.cats, subcat)
end
function AfDLog:renderCategories()
	if self.omitCat then
		return 
	end
	local ret = {}
	for _, subcat in ipairs(self.errors.cats) do
		ret#ret + 1 = string.format(, subcat)
	end
	if #ret > 0 then
		ret#ret + 1 = 
	end
	return table.concat(ret)
end
---@param rowNum? number If given, return a row, not a template
---@return string
function AfDLog:renderRawTemplate(rowNum)
	local ret = {}
	local function pushIfRowNumIsNil(str)
		if not rowNum then
			table.insert(ret, str)
		end
	end
	pushIfRowNumIsNil()
	return table.concat(ret)
end
-------------------------------------------------------------------------------
-- Exports
-------------------------------------------------------------------------------
local p = {}
function p._main(args)
	local afd = AfDLog.new(args)
	local caller = mw.getCurrentFrame():getParent():getTitle()
	local substPatterns = { -- Templates that must be transcluded with `subst:`
		'^Template:(削除済みノート23?)$',
		'^Template:(削除済みノート23?/.+)$',
		'^Template:(不削除ノート)$',
		'^Template:(不削除ノート/.+)$',
		'^Template:(特定版削除済みノート)$',
		'^Template:(特定版削除済みノート/.+)$',
		'^Template:(版指定削除済みノート)$',
		'^Template:(版指定削除済みノート/.+)$',
		'^Template:(削除済みノートページ2?)$',
		'^Template:(削除済みノートページ2?/.+)$'
	}
	local callerTitle
	for _, pat in ipairs(substPatterns) do
		callerTitle = caller:match(pat)
		if callerTitle then break end
	end
	if mw.isSubsting() then
		local rowNum
		if args.n then
			rowNum = string.match(args.n, '^(%d+)$') or 2
		end
		return afd:renderRawTemplate(rowNum)
	elseif callerTitle then
		local ret = {}
		ret#ret + 1 = 'エラー: '
		ret#ret + 1 = 'subst: がありません。'
		ret#ret + 1 = string.format('「%s」ではなく「subst:%s」としてください。', callerTitle, callerTitle)
		ret#ret + 1 = 
		afd:addCategory('subst違反')
		ret#ret + 1 = afd:renderCategories()
		return table.concat(ret)
	else
		return afd:renderBox()
	end
end
function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		wrappers = {
			'Template:削除依頼ログ'
		}
	})
	return p._main(args)
end
return p