--                            Article history -- -- This module allows editors to link to all the significant events in an -- article's history, such as good article nominations and featured article -- nominations. It also displays its current status, as well as other -- information, such as the date it was featured on the main page.  local CONFIG_PAGE = 'মডিউল:নিবন্ধের ইতিহাস/রূপরেখা' local WRAPPER_TEMPLATE = 'টেমপ্লেট:নিবন্ধের ইতিহাস' local DEBUG_MODE = false -- If true, errors are not caught.  -- Load required modules. require('strict') local Category = require('মডিউল:নিবন্ধের ইতিহাস/বিষয়শ্রেণী') local yesno = require('Module:Yesno') local lang = mw.language.getContentLanguage()  -- Helper functions  local function isPositiveInteger(num) 	return type(num) == 'number' 		and math.floor(num) == num 		and num > 0 		and num < math.huge end  local function substituteParams(msg, ...) 	return mw.message.newRawMessage(msg, ...):plain() end  local function makeUrlLink(url, display) 	return string.format('[%s %s]', url, display) end  local function maybeCallFunc(val, ...) 	-- Checks whether val is a function, and if so calls it with the specified 	-- arguments. Otherwise val is returned as-is. 	if type(val) == 'function' then 		return val(...) 	else 		return val 	end end  local function renderImage(image, caption, size) 	if caption then 		caption = '|' .. caption 	else 		caption = 	end 	return string.format('%s%s', image, size, caption) end  local function addMixin(class, mixin) 	-- Add a mixin to a class. The functions will be shared across classes, so 	-- don't use it for functions that keep state. 	for name, method in pairs(mixin) do 		class[name] = method 	end end  -- Message mixin -- This mixin is used by all classes to add message-related methods.  local Message = {}  function Message:message(key, ...) 	-- This fetches the message from the config with the specified key, and 	-- substitutes parameters $1, $2 etc. with the subsequent values it is 	-- passed. 	local msg = self.cfg.msg[key] 	if select('#', ...) > 0 then 		return substituteParams(msg, ...) 	else 		return msg 	end end  function Message:raiseError(msg, help) 	-- Raises an error with the specified message and help link. Execution 	-- stops unless the error is caught. This is used for errors where 	-- subsequent processing becomes impossible. 	local errorText 	if help then 		errorText = self:message('error-message-help', msg, help) 	else 		errorText = self:message('error-message-nohelp', msg) 	end 	error(errorText, 0) end  function Message:addWarning(msg, help) 	-- Adds a warning to the object's warnings table. Execution continues as 	-- normal. This is used for errors that should be fixed but that do not 	-- prevent the module from outputting something useful. 	self.warnings = self.warnings or {} 	local warningText 	if help then 		warningText = self:message('warning-help', msg, help) 	else 		warningText = self:message('warning-nohelp', msg) 	end 	table.insert(self.warnings, warningText) end  function Message:getWarnings() 	return self.warnings or {} end  -- Row class -- This class represents one row in the template.  local Row = {} Row.__index = Row addMixin(Row, Message)  function Row.new(data) 	local obj = setmetatable({}, Row) 	obj.cfg = data.cfg 	obj.currentTitle = data.currentTitle 	obj.makeData = data.makeData -- used by Row:getData 	return obj end  function Row:_cachedTry(cacheKey, errorCacheKey, func) 	-- This method is for use in Row object methods that are called more than 	-- once. The results of such methods should be cached to avoid unnecessary 	-- processing. We also cache any errors found and abort if an error was 	-- raised previously, otherwise error messages could be displayed multiple 	-- times. 	-- 	-- We use false as a key to cache nil results, so func cannot return false. 	-- 	-- @param cacheKey The key to cache successful results with 	-- @param errorCacheKey The key to cache errors with 	-- @param func an anonymous function that returns the method result 	if self[errorCacheKey] then 		return nil 	end 	local ret = self[cacheKey] 	if ret then 		return ret 	elseif ret == false then 		return nil 	end 	local success 	if DEBUG_MODE then 		success = true 		ret = func() 	else 		success, ret = pcall(func) 	end 	if success then 		if ret then 			self[cacheKey] = ret 			return ret 		else 			self[cacheKey] = false 			return nil 		end 	else 		self[errorCacheKey] = true 		-- We have already formatted the error message, so no need to format it 		-- again. 		error(ret, 0) 	end end  function Row:getData(articleHistoryObj) 	return self:_cachedTry('_dataCache', '_isDataError', function () 		return self.makeData(articleHistoryObj) 	end) end  function Row:setIconValues(icon, caption, size) 	self.icon = icon 	self.iconCaption = caption 	self.iconSize = size end  function Row:getIcon(articleHistoryObj) 	return maybeCallFunc(self.icon, articleHistoryObj, self) end  function Row:getIconCaption(articleHistoryObj) 	return maybeCallFunc(self.iconCaption, articleHistoryObj, self) end  function Row:getIconSize() 	return self.iconSize or self.cfg.defaultIconSize or '30px' end  function Row:renderIcon(articleHistoryObj) 	local icon = self:getIcon(articleHistoryObj) 	if not icon then 		return nil 	end 	return renderImage( 		icon, 		self:getIconCaption(articleHistoryObj), 		self:getIconSize() 	) end  function Row:setNoticeBarIconValues(icon, caption, size) 	self.noticeBarIcon = icon 	self.noticeBarIconCaption = caption 	self.noticeBarIconSize = size end  function Row:getNoticeBarIcon(articleHistoryObj) 	local icon = maybeCallFunc(self.noticeBarIcon, articleHistoryObj, self) 	if icon == true then 		icon = self:getIcon(articleHistoryObj) 		if not icon then 			self:raiseError( 				self:message('row-error-missing-icon'), 				self:message('row-error-missing-icon-help') 			) 		end 	end 	return icon end  function Row:getNoticeBarIconCaption(articleHistoryObj) 	local caption = maybeCallFunc( 		self.noticeBarIconCaption, 		articleHistoryObj, 		self 	) 	if not caption then 		caption = self:getIconCaption(articleHistoryObj) 	end 	return caption end  function Row:getNoticeBarIconSize() 	return self.noticeBarIconSize or self.cfg.defaultNoticeBarIconSize or '15px' end  function Row:exportNoticeBarIcon(articleHistoryObj) 	local icon = self:getNoticeBarIcon(articleHistoryObj) 	if not icon then 		return nil 	end 	return renderImage( 		icon, 		self:getNoticeBarIconCaption(articleHistoryObj), 		self:getNoticeBarIconSize() 	) end  function Row:setText(text) 	self.text = text end  function Row:getText(articleHistoryObj) 	return maybeCallFunc(self.text, articleHistoryObj, self) end  function Row:exportHtml(articleHistoryObj) 	if self._html then 		return self._html 	end 	local text = self:getText(articleHistoryObj) 	if not text then 		return nil 	end 	local html = mw.html.create('tr') 	html 		:tag('td') 			:addClass('mbox-image') 			:wikitext(self:renderIcon(articleHistoryObj)) 			:done() 		:tag('td') 			:addClass('mbox-text') 			:wikitext(text) 	self._html = html 	return html end  function Row:setCategories(val) 	-- Set the categories from the object's config. val can be either an array 	-- of strings or a function returning an array of category objects. 	self.categories = val end  function Row:getCategories(articleHistoryObj) 	local ret = {} 	if type(self.categories) == 'table' then 		for _, cat in ipairs(self.categories) do 			ret[#ret + 1] = Category.new(cat) 		end 	elseif type(self.categories) == 'function' then 		local t = self.categories(articleHistoryObj, self) or {} 		for _, categoryObj in ipairs(t) do 			ret[#ret + 1] = categoryObj 		end 	end 	return ret end  -- Status class -- Status objects deal with possible current statuses of the article.  local Status = setmetatable({}, Row) Status.__index = Status  function Status.new(data) 	local obj = Row.new(data) 	setmetatable(obj, Status)  	obj.id = data.id 	obj.statusCfg = obj.cfg.statuses[obj.id] 	obj.name = obj.statusCfg.name 	obj:setIconValues( 		obj.statusCfg.icon, 		obj.statusCfg.iconCaption or obj.name, 		data.iconSize 	) 	obj:setNoticeBarIconValues( 		obj.statusCfg.noticeBarIcon, 		obj.statusCfg.noticeBarIconCaption or obj.name, 		obj.statusCfg.noticeBarIconSize 	) 	obj:setText(obj.statusCfg.text) 	obj:setCategories(obj.statusCfg.categories)  	return obj end  function Status:getIconSize() 	return self.iconSize 		or self.statusCfg.iconSize 		or self.cfg.defaultStatusIconSize 		or '50px' end  function Status:getText(articleHistoryObj) 	local text = Row.getText(self, articleHistoryObj) 	if text then 		return substituteParams( 			text, 			self.currentTitle.subjectPageTitle.prefixedText, 			self.currentTitle.text 		) 	end end  -- MultiStatus class -- For when an article can have multiple distinct statuses, e.g. former -- featured article status and good article status.  local MultiStatus = setmetatable({}, Row) MultiStatus.__index = MultiStatus  function MultiStatus.new(data) 	local obj = Row.new(data) 	setmetatable(obj, MultiStatus)  	obj.id = data.id 	obj.statusCfg = obj.cfg.statuses[data.id] 	obj.name = obj.statusCfg.name  	-- Set child status objects 	local function getChildStatusData(data, id, iconSize) 		local ret = {} 		for k, v in pairs(data) do 			ret[k] = v 		end 		ret.id = id 		ret.iconSize = iconSize 		return ret 	end 	obj.statuses = {} 	local defaultIconSize = obj.cfg.defaultMultiStatusIconSize or '30px' 	for _, id in ipairs(obj.statusCfg.statuses) do 		table.insert(obj.statuses, Status.new(getChildStatusData( 			data, 			id, 			obj.cfg.statuses[id].iconMultiSize or defaultIconSize 		))) 	end  	return obj end  function MultiStatus:exportHtml(articleHistoryObj) 	local ret = mw.html.create() 	for _, obj in ipairs(self.statuses) do 		ret:node(obj:exportHtml(articleHistoryObj)) 	end 	return ret end  function MultiStatus:getCategories(articleHistoryObj) 	local ret = {} 	for _, obj in ipairs(self.statuses) do 		for _, categoryObj in ipairs(obj:getCategories(articleHistoryObj)) do 			ret[#ret + 1] = categoryObj 		end 	end 	return ret end  function MultiStatus:exportNoticeBarIcon() 	local ret = {} 	for _, obj in ipairs(self.statuses) do 		ret[#ret + 1] = obj:exportNoticeBarIcon() 	end 	return table.concat(ret) end  function MultiStatus:getWarnings() 	local ret = {} 	for _, obj in ipairs(self.statuses) do 		for _, msg in ipairs(obj:getWarnings()) do 			ret[#ret + 1] = msg 		end 	end 	return ret end  -- Notice class -- Notice objects contain notices about an article that aren't part of its -- current status, e.g. the date an article was featured on the main page.  local Notice = setmetatable({}, Row) Notice.__index = Notice  function Notice.new(data) 	local obj = Row.new(data) 	setmetatable(obj, Notice)  	obj:setIconValues( 		data.icon, 		data.iconCaption, 		data.iconSize 	) 	obj:setNoticeBarIconValues( 		data.noticeBarIcon, 		data.noticeBarIconCaption, 		data.noticeBarIconSize 	) 	obj:setText(data.text) 	obj:setCategories(data.categories)  	return obj end  -- Action class -- Action objects deal with a single action in the history of the article. We -- use getter methods rather than properties for the name and result, etc., as -- their processing needs to be delayed until after the status object has been -- initialised. The status object needs to parse the action objects when it is -- initialised, and the value of some names, etc., in the action objects depend -- on the status object, so this is necessary to avoid errors/infinite loops.  local Action = setmetatable({}, Row) Action.__index = Action  function Action.new(data) 	local obj = Row.new(data) 	setmetatable(obj, Action)  	obj.paramNum = data.paramNum  	-- Set the ID 	do 		if not data.code then 			obj:raiseError( 				obj:message('action-error-no-code', obj:getParameter('code')), 				obj:message('action-error-no-code-help') 			) 		end 		local code = mw.ustring.upper(data.code) 		obj.id = obj.cfg.actions[code] and obj.cfg.actions[code].id 		if not obj.id then 			obj:raiseError( 				obj:message( 					'action-error-invalid-code', 					data.code, 					obj:getParameter('code') 				), 				obj:message('action-error-invalid-code-help') 			) 		end 	end  	-- Add a shortcut for this action's config. 	obj.actionCfg = obj.cfg.actions[obj.id]  	-- Set the link 	obj.link = data.link or obj.currentTitle.talkPageTitle.prefixedText  	-- Set the result ID 	do 		local resultCode = data.resultCode 			and mw.ustring.lower(data.resultCode) 			or '_BLANK' 		if obj.actionCfg.results[resultCode] then 			obj.resultId = obj.actionCfg.results[resultCode].id 		elseif resultCode == '_BLANK' then 			obj:raiseError( 				obj:message( 					'action-error-blank-result', 					obj.id, 					obj:getParameter('resultCode') 				), 				obj:message('action-error-blank-result-help') 			) 		else 			obj:raiseError( 				obj:message( 					'action-error-invalid-result', 					data.resultCode, 					obj.id, 					obj:getParameter('resultCode') 				), 				obj:message('action-error-invalid-result-help') 			) 		end 	end  	-- Set the date 	if data.date then 		local success, date = pcall( 			lang.formatDate, 			lang, 			obj:message('action-date-format'), 			data.date 		) 		if success and date then 			obj.date = date 		else 			obj:addWarning( 				obj:message( 					'action-warning-invalid-date', 					data.date, 					obj:getParameter('date') 				), 				obj:message('action-warning-invalid-date-help') 			) 		end 	else 		obj:addWarning( 			obj:message( 				'action-warning-no-date', 				obj.paramNum, 				obj:getParameter('date'), 				obj:getParameter('code') 			), 			obj:message('action-warning-no-date-help') 		) 	end 	obj.date = obj.date or obj:message('action-date-missing')  	-- Set the oldid 	obj.oldid = tonumber(data.oldid) 	if data.oldid and (not obj.oldid or not isPositiveInteger(obj.oldid)) then 		obj.oldid = nil 		obj:addWarning( 			obj:message( 				'action-warning-invalid-oldid', 				data.oldid, 				obj:getParameter('oldid') 			), 			obj:message('action-warning-invalid-oldid-help') 		) 	end  	-- Set the notice bar icon values 	obj:setNoticeBarIconValues( 		data.noticeBarIcon, 		data.noticeBarIconCaption, 		data.noticeBarIconSize 	)  	-- Set the categories 	obj:setCategories(obj.actionCfg.categories)  	return obj end  function Action:getParameter(key) 	-- Finds the original parameter name for the given key that was passed to 	-- Action.new. 	local prefix = self.cfg.actionParamPrefix 	local suffix 	for k, v in pairs(self.cfg.actionParamSuffixes) do 		if v == key then 			suffix = k 			break 		end 	end 	if not suffix then 		error('invalid key "' .. tostring(key) .. '" passed to Action:getParameter', 2) 	end 	return prefix .. tostring(self.paramNum) .. suffix end  function Action:getName(articleHistoryObj) 	return maybeCallFunc(self.actionCfg.name, articleHistoryObj, self) end  function Action:getResult(articleHistoryObj) 	return maybeCallFunc( 		self.actionCfg.results[self.resultId].text, 		articleHistoryObj, 		self 	) end  function Action:exportHtml(articleHistoryObj) 	if self._html then 		return self._html 	end  	local row = mw.html.create('tr')  	-- Date cell 	local dateCell = row:tag('td') 	if self.oldid then 		dateCell 			:tag('span') 				:addClass('plainlinks') 				:wikitext(makeUrlLink( 					self.currentTitle.subjectPageTitle:fullUrl{oldid = self.oldid}, 					self.date 				)) 	else 		dateCell:wikitext(self.date) 	end  	-- Process cell 	row 		:tag('td') 			:wikitext(string.format( 				"%s", 				self.link, 				self:getName(articleHistoryObj) 			))  	-- Result cell 	row 		:tag('td') 			:wikitext(self:getResult(articleHistoryObj))  	self._html = row 	return row end  -- CollapsibleNotice class -- This class makes notices that go in the collapsible part of the template, -- underneath the list of actions.  local CollapsibleNotice = setmetatable({}, Row) CollapsibleNotice.__index = CollapsibleNotice  function CollapsibleNotice.new(data) 	local obj = Row.new(data) 	setmetatable(obj, CollapsibleNotice)  	obj:setIconValues( 		data.icon, 		data.iconCaption, 		data.iconSize 	) 	obj:setNoticeBarIconValues( 		data.noticeBarIcon, 		data.noticeBarIconCaption, 		data.noticeBarIconSize 	) 	obj:setText(data.text) 	obj:setCollapsibleText(data.collapsibleText) 	obj:setCategories(data.categories)  	return obj end  function CollapsibleNotice:setCollapsibleText(s) 	self.collapsibleText = s end  function CollapsibleNotice:getCollapsibleText(articleHistoryObj) 	return maybeCallFunc(self.collapsibleText, articleHistoryObj, self) end  function CollapsibleNotice:getIconSize() 	return self.iconSize 		or self.cfg.defaultCollapsibleNoticeIconSize 		or '20px' end  function CollapsibleNotice:exportHtml(articleHistoryObj, isInCollapsibleTable) 	local cacheKey = isInCollapsibleTable 		and '_htmlCacheCollapsible' 		or '_htmlCacheDefault' 	return self:_cachedTry(cacheKey, '_isHtmlError', function () 		local text = self:getText(articleHistoryObj) 		if not text then 			return nil 		end  		local function maybeMakeCollapsibleTable(cell, text, collapsibleText) 			-- If collapsible text is specified, makes a collapsible table 			-- inside the cell with two rows, a header row with one cell and a 			-- collapsed row with one cell. These are filled with text and 			-- collapsedText, respectively. If no collapsible text is 			-- specified, the text is added to the cell as-is. 			if collapsibleText then 				cell 					:tag('div') 						:addClass('mw-collapsible mw-collapsed') 						:tag('div') 							:wikitext(text) 							:done() 						:tag('div') 							:addClass('mw-collapsible-content') 							:css('border', '1px silver solid') 							:wikitext(collapsibleText) 			else 				cell:wikitext(text) 			end 		end  		local html = mw.html.create('tr') 		local icon = self:renderIcon(articleHistoryObj) 		local collapsibleText = self:getCollapsibleText(articleHistoryObj) 		if isInCollapsibleTable then 			local textCell = html:tag('td') 				:attr('colspan', 3) 				:css('width', '100%') 			local rowText 			if icon then 				rowText = icon .. ' ' .. text 			else 				rowText = text 			end 			maybeMakeCollapsibleTable(textCell, rowText, collapsibleText) 		else 			local textCell = html 				:tag('td') 					:addClass('mbox-image') 					:wikitext(icon) 					:done() 				:tag('td') 					:addClass('mbox-text') 			maybeMakeCollapsibleTable(textCell, text, collapsibleText) 		end  		return html 	end) end  -- ArticleHistory class -- This class represents the whole template.  local ArticleHistory = {} ArticleHistory.__index = ArticleHistory addMixin(ArticleHistory, Message)  function ArticleHistory.new(args, cfg, currentTitle) 	local obj = setmetatable({}, ArticleHistory)  	-- Set input 	obj.args = args or {} 	obj.currentTitle = currentTitle or mw.title.getCurrentTitle()  	-- Define object structure. 	obj._errors = {} 	obj._allObjectsCache = {}  	-- Format the config 	local function substituteAliases(t, ret) 		-- This function substitutes strings found in an "aliases" subtable 		-- as keys in the parent table. It works recursively, so "aliases" 		-- subtables can be placed at any level. It assumes that tables will 		-- not be nested recursively, which should be true in the case of our 		-- config file. 		ret = ret or {} 		for k, v in pairs(t) do 			if k ~= 'aliases' then 				if type(v) == 'table' then 					local newRet = {} 					ret[k] = newRet 					if v.aliases then 						for _, alias in ipairs(v.aliases) do 							ret[alias] = newRet 						end 					end 					substituteAliases(v, newRet) 				else 					ret[k] = v 				end 			end 		end 		return ret 	end 	obj.cfg = substituteAliases(cfg or require(CONFIG_PAGE))  	--[[ 	-- Get a table of the arguments sorted by prefix and number. Non-string 	-- keys and keys that don't contain a number are ignored. (This means that 	-- positional parameters are ignored, as they are numbers, not strings.) 	-- The parameter numbers are stored in the first positional parameter of 	-- the subtables, and any gaps are removed so that the tables can be 	-- iterated over with ipairs. 	-- 	-- For example, these arguments: 	--   {a1x = 'eggs', a1y = 'spam', a2x = 'chips', b1z = 'beans', b3x = 'bacon'} 	-- would translate into this prefixArgs table. 	--   { 	--     a = { 	--       {1, x = 'eggs', y = 'spam'}, 	--       {2, x = 'chips'} 	--     }, 	--     b = { 	--       {1, z = 'beans'}, 	--       {3, x = 'bacon'} 	--     } 	--   } 	--]] 	do 		local prefixArgs = {} 		for k, v in pairs(obj.args) do 			if type(k) == 'string' then 				local prefix, num, suffix = k:match('^(.-)([1-9][0-9]*)(.*)$') 				if prefix then 					num = tonumber(num) 					prefixArgs[prefix] = prefixArgs[prefix] or {} 					prefixArgs[prefix][num] = prefixArgs[prefix][num] or {} 					prefixArgs[prefix][num][suffix] = v 					prefixArgs[prefix][num][1] = num 				end 			end 		end 		-- Remove the gaps 		local prefixArrays = {} 		for prefix, prefixTable in pairs(prefixArgs) do 			prefixArrays[prefix] = {} 			local numKeys = {} 			for num in pairs(prefixTable) do 				numKeys[#numKeys + 1] = num 			end 			table.sort(numKeys) 			for _, num in ipairs(numKeys) do 				table.insert(prefixArrays[prefix], prefixTable[num]) 			end 		end 		obj.prefixArgs = prefixArrays 	end  	return obj end  function ArticleHistory:try(func, ...) 	if DEBUG_MODE then 		local val = func(...) 		return val 	else 		local success, val = pcall(func, ...) 		if success then 			return val 		else 			table.insert(self._errors, val) 			return nil 		end 	end end  function ArticleHistory:getActionObjects() 	-- Gets an array of action objects for the parameters specified by the 	-- user. We memoise this so that the parameters only have to be processed 	-- once. 	if self.actions then 		return self.actions 	end  	-- Get the action args, and exit if they don't exist. 	local actionArgs = self.prefixArgs[self.cfg.actionParamPrefix] 	if not actionArgs then 		self.actions = {} 		return self.actions 	end  	-- Make the objects. 	local actions = {} 	local suffixes = self.cfg.actionParamSuffixes 	for _, t in ipairs(actionArgs) do 		local objArgs = {} 		for k, v in pairs(t) do 			local newK = suffixes[k] 			if newK then 				objArgs[newK] = v 			end 		end 		objArgs.paramNum = t[1] 		objArgs.cfg = self.cfg 		objArgs.currentTitle = self.currentTitle 		local actionObj = self:try(Action.new, objArgs) 		table.insert(actions, actionObj) 	end 	self.actions = actions 	return actions end  function ArticleHistory:getStatusIdForCode(code) 	-- Gets a status ID given a status code. If no code is specified, returns 	-- nil, and if the code is invalid, raises an error. 	if not code then 		return nil 	end 	local statuses = self.cfg.statuses 	local codeUpper = mw.ustring.upper(code) 	if statuses[codeUpper] then 		return statuses[codeUpper].id 	else 		self:addWarning( 			self:message('articlehistory-warning-invalid-status', code), 			self:message('articlehistory-warning-invalid-status-help') 		) 		return nil 	end end  function ArticleHistory:getStatusObj() 	-- Get the status object for the current status. 	if self.statusObj == false then 		return nil 	elseif self.statusObj ~= nil then 		return self.statusObj 	end 	local statusId 	if self.cfg.getStatusIdFunction then 		statusId = self:try(self.cfg.getStatusIdFunction, self) 	else 		statusId = self:try( 			self.getStatusIdForCode, self, 			self.args[self.cfg.currentStatusParam] 		) 	end 	if not statusId then 		self.statusObj = false 		return nil 	end  	-- Check that some actions were specified, and if not add a warning. 	local actions = self:getActionObjects() 	if #actions < 1 then 		self:addWarning( 			self:message('articlehistory-warning-status-no-actions'), 			self:message('articlehistory-warning-status-no-actions-help') 		) 	end  	-- Make a new status object. 	local statusObjData = { 		id = statusId, 		currentTitle = self.currentTitle, 		cfg = self.cfg 	} 	local isMulti = self.cfg.statuses[statusId].isMulti 	local initFunc = isMulti and MultiStatus.new or Status.new 	local statusObj = self:try(initFunc, statusObjData) 	self.statusObj = statusObj or false 	return self.statusObj or nil end  function ArticleHistory:getStatusId() 	local statusObj = self:getStatusObj() 	return statusObj and statusObj.id end  function ArticleHistory:_noticeFactory(memoizeKey, configKey, class) 	-- This holds the logic for fetching tables of Notice and CollapsibleNotice 	-- objects. 	if self[memoizeKey] then 		return self[memoizeKey] 	end 	local ret = {} 	for _, t in ipairs(self.cfg[configKey] or {}) do 		if t.isActive(self) then 			local data = {} 			for k, v in pairs(t) do 				if k ~= 'isActive' then 					data[k] = v 				end 			end 			data.cfg = self.cfg 			data.currentTitle = self.currentTitle 			ret[#ret + 1] = class.new(data) 		end 	end 	self[memoizeKey] = ret 	return ret end  function ArticleHistory:getNoticeObjects() 	return self:_noticeFactory('notices', 'notices', Notice) end  function ArticleHistory:getCollapsibleNoticeObjects() 	return self:_noticeFactory( 		'collapsibleNotices', 		'collapsibleNotices', 		CollapsibleNotice 	) end  function ArticleHistory:getAllObjects(addSelf) 	local cacheKey = addSelf and 'addSelf' or 'default' 	local ret = self._allObjectsCache[cacheKey] 	if not ret then 		ret = {} 		local statusObj = self:getStatusObj() 		if statusObj then 			ret[#ret + 1] = statusObj 		end 		local objTables = { 			self:getNoticeObjects(), 			self:getActionObjects(), 			self:getCollapsibleNoticeObjects() 		} 		for _, t in ipairs(objTables) do 			for _, obj in ipairs(t) do 				ret[#ret + 1] = obj 			end 		end 		if addSelf then 			ret[#ret + 1] = self 		end 		self._allObjectsCache[cacheKey] = ret 	end 	return ret end  function ArticleHistory:getNoticeBarIcons() 	local ret = {} 	-- Icons that aren't part of a row. 	if self.cfg.noticeBarIcons then 		for _, data in ipairs(self.cfg.noticeBarIcons) do 			if data.isActive(self) then 				ret[#ret + 1] = renderImage( 					data.icon, 					nil, 					data.size or self.cfg.defaultNoticeBarIconSize 				) 			end 		end 	end 	-- Icons in row objects. 	for _, obj in ipairs(self:getAllObjects()) do 		ret[#ret + 1] = obj:exportNoticeBarIcon(self) 	end 	return ret end  function ArticleHistory:getErrorMessages() 	-- Returns an array of error/warning strings. Error strings come first. 	local ret = {} 	for _, msg in ipairs(self._errors) do 		ret[#ret + 1] = msg 	end 	for _, obj in ipairs(self:getAllObjects(true)) do 		for _, msg in ipairs(obj:getWarnings()) do 			ret[#ret + 1] = msg 		end 	end 	return ret end  function ArticleHistory:categoriesAreActive() 	-- Returns a boolean indicating whether categories should be output or not. 	local title = self.currentTitle 	local ns = title.namespace 	return title.isTalkPage 		and ns ~= 3 -- not user talk 		and ns ~= 119 -- not draft talk end  function ArticleHistory:renderCategories() 	local ret = {}  	if self:categoriesAreActive() then 		-- Child object categories 		for _, obj in ipairs(self:getAllObjects()) do 			local categories = self:try(obj.getCategories, obj, self) 			for _, categoryObj in ipairs(categories or {}) do 				ret[#ret + 1] = tostring(categoryObj) 			end 		end  		-- Extra categories 		for _, func in ipairs(self.cfg.extraCategories or {}) do 			local cats = func(self) or {} 			for _, categoryObj in ipairs(cats) do 				ret[#ret + 1] = tostring(categoryObj) 			end 		end 	end  	return table.concat(ret) end  function ArticleHistory:__tostring() 	local root = mw.html.create()  	-- Table root 	local tableRoot = root:tag('table') 	tableRoot:addClass('article-history tmbox tmbox-notice')  	-- Status 	local statusObj = self:getStatusObj() 	if statusObj then 		tableRoot:node(self:try(statusObj.exportHtml, statusObj, self)) 	end  	-- Notices 	local notices = self:getNoticeObjects() 	for _, noticeObj in ipairs(notices) do 		tableRoot:node(self:try(noticeObj.exportHtml, noticeObj, self)) 	end  	-- Get action objects and the collapsible notice objects, and generate the 	-- HTML objects for the action objects. We need the action HTML objects so 	-- that we can accurately calculate the number of collapsible rows, as some 	-- action objects may generate errors when the HTML is generated. 	local actions = self:getActionObjects() or {} 	local collapsibleNotices = self:getCollapsibleNoticeObjects() or {} 	local collapsibleNoticeHtmlObjects, actionHtmlObjects = {}, {} 	for _, obj in ipairs(actions) do 		table.insert( 			actionHtmlObjects, 			self:try(obj.exportHtml, obj, self) 		) 	end 	for _, obj in ipairs(collapsibleNotices) do 		table.insert( 			collapsibleNoticeHtmlObjects, 			self:try(obj.exportHtml, obj, self, true) -- Render the collapsed version 		) 	end 	local nActionRows = #actionHtmlObjects 	local nCollapsibleRows = nActionRows + #collapsibleNoticeHtmlObjects  	-- Find out if we are collapsed or not. 	local isCollapsed = yesno(self.args.collapse) 	if isCollapsed == nil then 		if self.cfg.uncollapsedRows == 'all' then 			isCollapsed = false 		elseif nCollapsibleRows == 1 then 			isCollapsed = false 		else 			isCollapsed = nCollapsibleRows > (tonumber(self.cfg.uncollapsedRows) or 3) 		end 	end  	-- If we are not collapsed, re-render the collapsible notices in the 	-- non-collapsed version. 	if not isCollapsed then 		collapsibleNoticeHtmlObjects = {} 		for _, obj in ipairs(collapsibleNotices) do 			table.insert( 				collapsibleNoticeHtmlObjects, 				self:try(obj.exportHtml, obj, self, false) 			) 		end 	end  	-- Collapsible table for actions and collapsible notices. Collapsible 	-- notices are only included in the table if it is collapsed. Action rows 	-- are always included. 	local collapsibleTable 	if isCollapsed or nActionRows > 0 then 		-- Collapsible table base 		collapsibleTable = tableRoot 			:tag('tr') 				:tag('td') 					:attr('colspan', 2) 					:css('width', '100%') 					:tag('table') 						:addClass('article-history-milestones') 						:addClass(isCollapsed and 'mw-collapsible mw-collapsed' or nil) 						:css('width', '100%') 						:css('font-size', '90%')  		-- Header row 		local ctHeader = collapsibleTable 			:tag('tr') 				:tag('th') 					:attr('colspan', 3) 					:css('font-size', '110%')  		-- Notice bar 		if isCollapsed then 			local noticeBarIcons = self:getNoticeBarIcons() 			if #noticeBarIcons > 0 then 				local noticeBar = ctHeader:tag('span'):css('float', 'left') 				for _, icon in ipairs(noticeBarIcons) do 					noticeBar:wikitext(icon) 				end 				ctHeader:wikitext(' ') 			end 		end  		-- Header text 		if mw.site.namespaces[self.currentTitle.namespace].subject.id == 0 then 			ctHeader:wikitext(self:message('milestones-header')) 		else 			ctHeader:wikitext(self:message( 				'milestones-header-other-ns', 				self.currentTitle.subjectNsText 			)) 		end  		-- Subheadings 		if nActionRows > 0 then 			collapsibleTable 				:tag('tr') 					:css('text-align', 'left') 					:tag('th') 						:wikitext(self:message('milestones-date-header')) 						:done() 					:tag('th') 						:wikitext(self:message('milestones-process-header')) 						:done() 					:tag('th') 						:wikitext(self:message('milestones-result-header')) 		end  		-- Actions 		for _, htmlObj in ipairs(actionHtmlObjects) do 			collapsibleTable:node(htmlObj) 		end 	end  	-- Collapsible notices and current status 	-- These are only included in the collapsible table if it is collapsed. 	-- Otherwise, they are added afterwards, so that they align with the 	-- notices. 	do 		local tableNode, statusColspan 		if isCollapsed then 			tableNode = collapsibleTable 			statusColspan = 3 		else 			tableNode = tableRoot 			statusColspan = 2 		end  		-- Collapsible notices 		for _, obj in ipairs(collapsibleNotices) do 			tableNode:node(self:try(obj.exportHtml, obj, self, isCollapsed)) 		end  		-- Current status 		if statusObj and nActionRows > 1 then 			tableNode 				:tag('tr') 					:tag('td') 						:attr('colspan', statusColspan) 						:wikitext(self:message('status-blurb', statusObj.name)) 		end 	end  	-- Get the categories. We have to do this before the error row, so that 	-- category errors display. 	local categories = self:renderCategories()  	-- Error row and error category 	local errors = self:getErrorMessages() 	local errorCategory 	if #errors > 0 then 		local errorList = tableRoot 			:tag('tr') 				:tag('td') 					:attr('colspan', 2) 					:addClass('mbox-text') 					:tag('ul') 						:addClass('error') 						:css('font-weight', 'bold') 		for _, msg in ipairs(errors) do 			errorList:tag('li'):wikitext(msg) 		end 		if self:categoriesAreActive() then 			errorCategory = tostring(Category.new(self:message( 				'error-category' 			))) 		end  	-- If there are no errors and no active objects, then exit. We can't make 	-- this check earlier as we don't know where the errors may be until we 	-- have finished rendering the banner. 	elseif #self:getAllObjects() < 1 then 		return  	end  	-- Add the categories 	root:wikitext(categories) 	root:wikitext(errorCategory) 	 	local frame = mw.getCurrentFrame() 	return frame:extensionTag{ 		name = 'templatestyles', args = { src = 'Module:Message box/tmbox.css' } 	} .. frame:extensionTag{ 		name = 'templatestyles', args = { src = 'মডিউল:নিবন্ধের ইতিহাস/শৈলী.css' } 	} .. tostring(root) end  -- Exports -- These functions are called from Lua and from wikitext.  local p = {}  function p._main(args, cfg, currentTitle) 	local articleHistoryObj = ArticleHistory.new(args, cfg, currentTitle) 	return tostring(articleHistoryObj) end  function p.main(frame) 	local args = require('Module:Arguments').getArgs(frame, { 		wrappers = WRAPPER_TEMPLATE 	}) 	if frame:getTitle():find('sandbox', 1, true) then 		CONFIG_PAGE = CONFIG_PAGE .. '/sandbox' 	end 	return p._main(args) end  function p._exportClasses() 	return { 		Message = Message, 		Row = Row, 		Status = Status, 		MultiStatus = MultiStatus, 		Notice = Notice, 		Action = Action, 		CollapsibleNotice = CollapsibleNotice, 		ArticleHistory = ArticleHistory 	} end  return p