Mô đun:Excerpt/portals

-- ATTENTION !
-- This version of Excerpt is designed specifically for the portal namespace and its associated templates
-- Prefer Module:Excerpt whenever possible
-- Name of the category to track content pages with errors
local errorCategory = "Bài viết có đoạn trích bị hỏng"
-- Error messages
local errorMessages = {
	prefix = "Lỗi trích xuất: ",
	noPage = "Không có trang nào được đưa ra",
	pageNotFound = "Không tìm thấy trang '%s'",
	leadEmpty = "Mục dẫn trống",
	sectionEmpty = "Mục '%s' trống",
	sectionNotFound = "Không tìm thấy mục '%s'",
	fragmentEmpty = "Đoạn '%s' trống",
	fragmentNotFound = "Không tìm thấy đoạn '%s'"
}
-- Regular expressions to match all aliases of the file namespace
local fileNamespaces = {
	"Ffile",
	"Iimage"
}
-- Regular expressions to match all image parameters
local imageParams = {
	{"thumb", "thumbnail", "frame", "framed", "frameless"},
	{"right", "left", "center", "none"},
	{"baseline", "middle", "sub", "super", "text-top", "text-bottom", "top", "bottom"}
}
-- Regular expressions to match all infobox parameters for image captions
local captionParams = {
	"*Ccaption*",
	"*Llegend*"
}
-- List of file types that are allowed to be transcluded
local fileTypes = {"GgIiFf", "JjPpEe?Gg", "PpNnGg", "SsVvGg", "TtIiFfFf", "XxCcFf"}
-- Regular expressions to match all inline templates that are undesirable in excerpts
local unwantedInlineTemplates = {
	"Eefn", "Eefn%-luarg", "Eefn %a -", "Eelmn", "Rrp?", "Ssfnbmp", "Ssfbn", "NnoteTtag", "#Ttag:%s*Rref", "Rrefn?",
	"CcDdn", "Ccitation%- _needed", "Ddisambiguation needed", "Ffeatured article", "Ggood article",
	"DdISPLAYTITLE", "Sshort _+description", "Ccitation", "Ccite%- _+%w_%s-", "Ccoor%w_%s-",
	"Uu?n?Rreliable source%?%w_%s-", "Rrs%??", "Vvc", "Vverify credibility", "Bby _*Wwhom*%??", "Wwikisource -_*multi", "Iinflation _/-*Ffn",
	"Bbiblesource",
	-- aliases for Clarification needed
	"Ccfny", "Cclarification _+inline", "Cclarification%- _*needed", "Cclarification", "Cclarify%-inline", "Cclarify%-?me",
	"Cclarify _+inline", "Cclarify", "CcLARIFY", "Cconfusing%-inline", "Cconfusing%-short", "Eexplainme", "Hhuh _*%??", "Wwhat%?",
	"Iinline _+Uunclear", "Iin _+what _+sense", "Oobscure", "Pplease _+clarify", "Uunclear _+inline", "Wwhat's _+this%?",
	"GgeoQuelle", "Nneeds+%- _+IiPpAa", "IiPA needed",
	-- aliases for Clarification needed lead
	"Ccitation needed %(?lea?de?%)?", "Ccnl", "Ffact %(?lea?de?%)?", "Llead citation needed", "Nnot in body", "Nnot verified in body",
	-- Primary source etc.
	"Ppsci", "Nnpsn", "Nnon%-primary _+source _+needed", "Sself%-published%w_%s-", "Uuser%-generated%w_%s-",
	"Pprimary source%w_%s-", "Ssecondary source%w_%s-", "Ttertiary source%w_%s-", "Tthird%-party%w_%s-",
	-- aliases for Disambiguation (page) and similar
	"Bbegriffsklärung", "DdAaBb", "Ddbig", "%w_%s-%f%wDdisam%w_%s-", "HhNnDdIiSs",
	-- aliases for Failed verification
	"Bbadref", "Ffaile?ds ?rv%w_%s-", "FfVv", "NnIi?CcGg", "Nnot ?in ?crs%w_%s-", "Nnot specifically in source",
	"Vverification%- _failed",
	-- aliases for When
	"Aas _+of _+when%??", "Aas _%-+of%??", "Cclarify date", "Ddate _*needed", "Nneeds? _+date", "Rrecently", "Ssince _+when%??",
	"WwHEN", "Wwhen%??",
	-- aliases for Update
	"Nnot _*up _*to _*date","Oou?TtDd","Oout%- _*o?f?%- _*dated?", "Uupdate",  "Uupdate _+sect", "Uupdate _+Watch",
	-- aliases for Pronunciation needed
	"Ppronunciation%??%- _*n?e?e?d?e?d?", "Ppronounce", "Rrequested%- _*pronunciation", "Rre?q?pron", "Nneeds%- _*pronunciation",
	-- Chart, including Chart/start etc.
	"Cchart", "Cchart/%w_%s-",
	-- Cref and others
	"Ccref2?", "Ccnote",
	-- Explain and others
	"Eexplain", "Ffurther *explanation *needed", "Eelaboration *needed", "Eexplanation *needed",
	-- TOC templates
	"CcOoMmPpAaCcTt _*TtOoCc8*5*", "TtOoCc", "09AaZz", "TtOoCc *CcOoMmPpAaCcTt", "TtOoCc *SsMmAaLlLl", "CcOoMmPpAaCcTt _*AaLlPpHhAaBbEeTtIiCc _*TtOoCc",
	"DEFAULTSORT:.-",
	"Oone _+source",
	"Ccontains _+special _+characters",
	"Iinfobox _+Cchinese"
}
-- Regular expressions to match all block templates that are desirable in excerpts
local wantedBlockTemplates = {
	"Bbasketball _roster _header",
	"Ccabinet _table}*",
	"Cchart}*",
	"Cclear",
	"Ccol}*", -- all column templates
	"COVID-19 _pandemic _data}*",
	"Ccycling _squad}*",
	"Ddynamic _list",
	"Eelection _box}*",
	"Ggallery",
	"Ggraph}*",
	"Hhidden",
	"Hhistorical _populations",
	"Llegend _inline",
	"Pplainlist",
	"Pplayer}*",
	"Sseries _overview",
	"Sside _box",
	"Sswitcher",
	"Ttree _chart}*",
	"Ttelevision _ratings _graph"
}
local yesno = require('Module:Yesno')
local p = {}
-- Helper function to test for truthy and falsy values
local function is(value)
	if not value or value == "" or value == "0" or value == "false" or value == "no" then
		return false
	end
	return true
end
-- Error handling function
-- Throws a Lua error or returns an empty string if error reporting is disabled
local errors = true -- show errors by default
local function luaError(message, value)
	if not is(errors) then return  end -- error reporting is disabled
	message = errorMessagesmessage or message or 
	message = mw.ustring.format(message, value)
	error(message, 2)
end
-- Error handling function
-- Returns a wiki friendly error or an empty string if error reporting is disabled
local function wikiError(message, value)
	if not is(errors) then return  end -- error reporting is disabled
	message = errorMessagesmessage or message or 
	message = mw.ustring.format(message, value)
	message = errorMessages.prefix .. message
	if mw.title.getCurrentTitle().isContentPage then
		local errorCategory = mw.title.new(errorCategory, 'Category')
		if errorCategory then message = message ..  .. errorCategory.prefixedText ..  end
	end
	message = mw.html.create('div'):addClass('error'):wikitext(message)
	return message
end
-- Helper function to match from a list regular expressions
-- Like so: match pre..list1..post or pre..list2..post or ...
local function matchAny(text, pre, list, post, init)
	local match = {}
	for i = 1, #list do
		match = { mw.ustring.match(text, pre .. listi .. post, init) }
		if match1 then return unpack(match) end
	end
	return nil
end
-- Helper function to convert imagemaps into standard images
local function convertImageMap(imagemap)
	local image = matchAny(imagemap, ">\n%s*", fileNamespaces, "^\n*")
	if image then
		return "" .. mw.ustring.gsub(image, ">\n%s*", "", 1) .. ""
	else
		return "" -- remove entire block if image can't be extracted
	end
end
-- Helper function to convert a comma-separated list of numbers or min-max ranges into a list of booleans
-- For example: "1,3-5" to {1=true,2=false,3=true,4=true,5=true}
local function numberFlags(str)
	if not str then return {} end
	local flags = {}
	local ranges = mw.text.split(str, ",") -- parse ranges: "1,3-5" to {"1","3-5"}
	for _, r in pairs(ranges) do
		local min, max = mw.ustring.match(r, "^%s*(%d+)%s*-–—%s*(%d+)%s*$") -- "3-5" to min=3 max=5
		if not max then	min, max = mw.ustring.match(r, "^%s*((%d+))%s*$") end -- "1" to min=1 max=1
		if max then
			for p = min, max do flagsp = true end
		end
	end
	return flags
end
-- Helper function to convert template arguments into an array of arguments fit for get()
local function parseArgs(frame)
	local args = {}
	for key, value in pairs(frame:getParent().args) do argskey = value end
	for key, value in pairs(frame.args) do argskey = value end -- args from a Lua call have priority over parent args from template
	args.paraflags = numberFlags(args"paragraphs" or "") -- parse paragraphs: "1,3-5" to {"1","3-5"}
	args.fileflags = numberFlags(args"files" or "") -- parse file numbers
	return args
end
-- simulate  without the footnote, given "N|485US|," or similar
local function airreg(p)
	local s = mw.text.split(p, "%s*|%s*")
	if s1 ~= "N" and s1 ~= "HL" and s1 ~= "JA" then s1=s1 .. "-" end
	return table.concat(s, "")
end
-- Helper function to remove unwanted templates and pseudo-templates such as #tag:ref and DEFAULTSORT
local function stripTemplate(t)
	-- If template is unwanted then return "" (gsub will replace by nothing), else return nil (gsub will keep existing string)
	if matchAny(t, "^", "")
	noRef = mw.ustring.gsub(noRef, "|%s*ref%s*%f}", "")
	noRef = mw.ustring.gsub(noRef, "|%s*shortref%s*%f}", "")
	-- If a wanted template has unwanted nested templates, purge them too
	noRef = mw.ustring.sub(noRef, 1, 2) .. mw.ustring.gsub(mw.ustring.sub(noRef, 3), "%b{}", stripTemplate)
	-- Replace  by its text parameter: Bar → Bar
	noRef = mw.ustring.gsub(noRef, "^')
		if params then
			count = 0
			-- Temporarily replace pipes in subtemplates, tables and links to avoid chaos
			for subtemplate in string.gmatch(params, '%b{}') do
				params = string.gsub(params, escapeString(subtemplate), string.gsub(string.gsub(subtemplate, '%%', '%%%%'), '|', '@@@') )
			end
			for link in string.gmatch(params, '%b') do
				params = string.gsub(params, escapeString( mw.ustring.gsub(link, '%%', '%%%%') ), string.gsub(link, '|', '@@@') )
			end
			for parameter in mw.text.gsplit(params, '|') do
				parts = mw.text.split(parameter, '=')
				key = mw.text.trim(parts1)
				value = table.concat(parts, '=', 2)
				if value ==  then
					value = key
					count = count + 1
					key = count
				else
					value = mw.text.trim(value)
				end
				value = string.gsub(value, '@@@', '|')
				parameterskey = value
				paramOrderparamCount = key
				paramCount = paramCount + 1
			end
		end
	end
	return parameters, paramOrder
end
-- Turns an infobox file table into a File:...
local function stringifytemplateImage(image,allowFancy)
	--Certain positional elements may need to apply to the containing infobox, and not the file itself, so we should check that here
	if is(image.caption) and allowFancy then --Will be displayed like an infobox
		--Do not allow positioning the internal image. Instead, apply any positions we care about to the container
		local args,replacecount = string.gsub(image.args,"|left","") --We care if its formatted to the left, so track this
		args = mw.ustring.gsub(args,"|right","")
		args = mw.ustring.gsub(args,"|center","")
		local isleft = (replacecount > 0 and "y") or ""
		
		return mw.getCurrentFrame():expandTemplate({
			title="Infobox file display",
			args={image.file..args,image.caption,left=isleft}
		}) .. "\n"
	else
		local captionText = (is(image.caption) and "|"..image.caption) or ""
		return "thumb\n"
	end
end
-- Attempt to construct a File:... block from image= ...
local function templateImages(text)
	local hasNamedArgs = mw.ustring.find(text, "|") and mw.ustring.find(text, "=")
	if not hasNamedArgs then return nil end -- filter out any template that obviously doesn't contain an image
	-- ensure image map is captured, while removing anything beyond it
	text = mw.ustring.gsub(text, '(%%b%)+', '|imagemap=%1')
	-- filter through parameters for image related ones
	local images = {}
	local parameters, parameterOrder = getParameters(text)
	
	--Search all template parameters for file-like objects
	local positionalImages = {}
	local position = 1
	for _,key in ipairs(parameterOrder) do
		position = position + 1 --Cant rely on ipairs due to potentially weird manipulation later
		local value = parameterskey
		if is(value) then --Ensure its not empty
			if string.sub(value,1,2) == "" then --Template in a template
				--Extract files from the template and insert files if any appear
				local internalImages = templateImages(value) or {}
				local initialPosition = position
				for index,image in ipairs(internalImages) do
					positionalImagesinitialPosition+index = image --Still positional, technically
					position = position + 1 --Advance our own counter to avoid overlap
				end
			else
				if matchAny(key, "", captionParams, "%s*") then
					--Caption-like parameter name, try to associate it with an image
					local scanPosition = position
					while scanPosition > 0 do
						scanPosition = scanPosition - 1
						local image = positionalImagesscanPosition
						if image and image.caption == "" then
							image.caption = mw.getCurrentFrame():preprocess(value) --Assign caption to most recently defined image
							break
						end
					end
				
				elseif matchAny(value, "%.", fileTypes, "%s*$") then
					--File-like value, assume its an image
					local filename,fileargs = extractFileData(value)
					positionalImagesposition = {file=filename,caption="",args=fileargs}
				
				elseif mw.ustring.match(key, "IiMmAaGgEe") or mw.ustring.match(key, "PpHhOoTtOo") or mw.ustring.match(key, "SsYyMmBbOoLl") then
					--File-like parameter name, assume its an image after some scrutinization
					local keyLower = string.lower(key)
					if string.find(keyLower,"caption")
					 or string.find(keyLower,"size") or string.find(keyLower,"width")
					 or string.find(keyLower,"upright")
					 or string.find(keyLower,"alt") then --Argument is defining image settings, not an image
						--Do nothing for now
						--TODO: we really should extract some of this for later use
					else
						local filename,fileargs = extractFileData(value)
						positionalImagesposition = {file=filename,caption="",args=fileargs}
					end
				end
			end --End of "Is template in template" check
		end --End of "is(value)" check
	end
	
	--Append entries from positionalImages into the main images table
	for i = 1,position do
		local value = positionalImagesi
		if value then
			table.insert(images,value)
		end
	end
	return images
end
local function modifyImage(image, fileArgs)
	if type(image) == "table" then
		--Pass in a dummy string version and use that to handle modification
		local newversion = modifyImage("").."",fileArgs)
		--Since we know the format is strictly controlled, we can do a lazy sub grab for the args
		image.args = string.sub(newversion,8+#image.file,-3)
		return image
	end
	if fileArgs then
		for _, filearg in pairs(mw.text.split(fileArgs, "|")) do -- handle fileArgs=left|border etc.
			local fa = mw.ustring.gsub(filearg, "=.*", "") -- "upright=0.75" → "upright"
			local group = {fa} -- group of "border" is "border"...
			for _, g in pairs(imageParams) do
				for _, a in pairs(g) do
					if fa == a then group = g end -- ...but group of "left" is "right", "left", "center", "none"
				end
			end
			for _, a in pairs(group) do
				image = mw.ustring.gsub(image, "|%s*" .. a .. "%f%A%s*=%*", "") -- remove "|upright=0.75" etc.
				image = mw.ustring.gsub(image, "|%s*" .. a .. "%s*(%)", "%1") -- replace "|left|" by "|" etc.
			end
			image = mw.ustring.gsub(image, "(%)", "|" .. filearg .. "%1", 1) -- replace "|" by "|left|" etc.
		end
	end
	image = mw.ustring.gsub(image, "(|%s*%d*x?%d+%s*px%s*.-)|%s*%d*x?%d+%s*px%s*(%)", "%1%2") -- double px args
	return image
end
-- a basic parser to trim down extracted wikitext
--   @param text : Wikitext to be processed
--   @param options : A table of options...
--          options.paraflags : Which number paragraphs to keep, as either a string (e.g. '1,3-5') or a table (e.g. {1=true,2=false,3=true,4=true,5=true}. If not present, all paragraphs will be kept.
--          options.fileflags : table of which files to keep, as either a string (e.g. '1,3-5') or a table (e.g. {1=true,2=false,3=true,4=true,5=true}
--          options.fileargs : args for the File: syntax, such as 'left'
--			options.filesOnly : only return the files and not the prose
local function parse(text, options)
	local allParagraphs = true -- keep all paragraphs?
	if options.paraflags then
		if type(options.paraflags) ~= "table" then options.paraflags = numberFlags(options.paraflags) end
		for _, v in pairs(options.paraflags) do
			if v then allParagraphs = false end -- if any para specifically requested, don't keep all
		end
	end
	if is(options.filesOnly) then
		allParagraphs = false
		options.paraflags = {}
	end
	local maxfile = 0 -- for efficiency, stop checking images after this many have been found
	if options.fileflags then
		if type(options.fileflags) ~= "table" then options.fileflags = numberFlags(options.fileflags) end
		for k, v in pairs(options.fileflags) do
			if v and k > maxfile then maxfile = k end -- set maxfile = highest key in fileflags
		end
	end
	
	local fileArgs = options.fileargs and mw.text.trim(options.fileargs)
	if fileArgs ==  then fileArgs = nil end
	local doFancyFiles = yesno(options.doFancyFiles)
	if doFancyFiles == nil then doFancyFiles = true end
	local leadStart = nil -- have we found some text yet?
	local t = "" -- the stripped down output text
	local fileText = "" -- output text with concatenated ...\n entries
	local files = 0 -- how many images so far
	local paras = 0 -- how many paragraphs so far
	local startLine = true -- at the start of a line (no non-spaces found since last \n)?
	text = mw.ustring.gsub(text,"^%s*","") -- remove initial white space
	-- Add named files
	local f = options.files
	if f and mw.ustring.match(f, "^%d%s%-,") then -- filename rather than number list
		f = mw.ustring.gsub(f, "^%s*File%s*:%s*", "", 1)
		f = mw.ustring.gsub(f, "^%s*Image%s*:%s*", "", 1)
		f = "File:" .. f .. ""
		f = modifyImage(f, "thumb")
		f = modifyImage(f, fileArgs)
		if checkImage(f) then fileText = fileText .. f .. "\n" end
	end
	repeat -- loop around parsing a template, image or paragraph
		local token = mw.ustring.match(text, "^%b{}%s*") or false --  or 
		if not leadStart and not token then token = mw.ustring.match(text, "^%b%s*%b{}%s*") end -- allow  before lead has started
		local line = mw.ustring.match(text, "^\n*")
		if token and line and mw.ustring.len(token) ", "") -- remove all HTML tags from this line
			-- if anything is left, other than an incomplete further template or an image, keep the template: it counts as part of the line
			if mw.ustring.find(line, "%S") and not matchAny(line, "^%s*", { "E where E represents escape
		text = mw.ustring.gsub(text, "(*>^)", "%1}\27}\27%2") --  → 
	until text == t
	text = text.gsub(text, "({})%1^\27.*", "") -- remove unmatched  and everything thereafter, avoiding }E}E etc.
	text = text.gsub(text, "({})%1$", "") -- remove unmatched  at end of text
	text = mw.ustring.gsub(text, "\27", "") -- unhide matched pairs: E{E{ →