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 = {
"[Ff]ile",
"[Ii]mage"
}

-- 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 = {
"[^=|][Cc]aption[^=|]",
"[^=|][Ll]egend[^=|]"
}

-- List of file types that are allowed to be transcluded
local fileTypes = {"[Gg][Ii][Ff]", "[Jj][Pp][Ee]?[Gg]", "[Pp][Nn][Gg]", "[Ss][Vv][Gg]", "[Tt][Ii][Ff][Ff]", "[Xx][Cc][Ff]"}

-- Regular expressions to match all inline templates that are undesirable in excerpts
local unwantedInlineTemplates = {
"[Ee]fn", "[Ee]fn%-[lu][arg]", "[Ee]fn [%a ]-", "[Ee]l[mn]", "[Rr]p?", "[Ss]fn[bmp]", "[Ss]f[bn]", "[Nn]ote[Tt]ag", "[Tt]ag:%s[Rr]ef", "[Rr]efn?",
"[CcDd]n", "[Cc]itation[%- _]needed", "[Dd]isambiguation needed", "[Ff]eatured article", "[Gg]ood article",
"[Dd]ISPLAYTITLE", "[Ss]hort[ _]+description", "[Cc]itation", "[Cc]ite[%- _]+[%w_%s]-", "[Cc]oor[%w_%s]-",
"[Uu]?n?[Rr]eliable source[%?%w_%s]-", "[Rr]s%??", "[Vv]c", "[Vv]erify credibility", "[Bb]y[ _][Ww]ho[m]%??", "[Ww]ikisource[ -_]multi", "[Ii]nflation[ _/-][Ff]n",
"[Bb]iblesource",
-- aliases for Clarification needed
"[Cc]f[ny]", "[Cc]larification[ _]+inline", "[Cc]larification[%- _]needed", "[Cc]larification", "[Cc]larify%-inline", "[Cc]larify%-?me",
"[Cc]larify[ _]+inline", "[Cc]larify", "[Cc]LARIFY", "[Cc]onfusing%-inline", "[Cc]onfusing%-short", "[Ee]xplainme", "[Hh]uh[ _]%??", "[Ww]hat%?",
"[Ii]nline[ _]+[Uu]nclear", "[Ii]n[ _]+what[ _]+sense", "[Oo]bscure", "[Pp]lease[ _]+clarify", "[Uu]nclear[ _]+inline", "[Ww]hat's[ _]+this%?",
"[Gg]eoQuelle", "[Nn]eed[s]+[%- _]+[Ii][Pp][Aa]", "[Ii]PA needed",
-- aliases for Clarification needed lead
"[Cc]itation needed %(?lea?de?%)?", "[Cc]nl", "[Ff]act %(?lea?de?%)?", "[Ll]ead citation needed", "[Nn]ot in body", "[Nn]ot verified in body",
-- Primary source etc.
"[Pp]s[ci]", "[Nn]psn", "[Nn]on%-primary[ _]+source[ _]+needed", "[Ss]elf%-published[%w_%s]-", "[Uu]ser%-generated[%w_%s]-",
"[Pp]rimary source[%w_%s]-", "[Ss]econdary source[%w_%s]-", "[Tt]ertiary source[%w_%s]-", "[Tt]hird%-party[%w_%s]-",
-- aliases for Disambiguation (page) and similar
"[Bb]egriffsklärung", "[Dd][Aa][Bb]", "[Dd]big", "[%w_%s]-%f[%w][Dd]isam[%w_%s]-", "[Hh][Nn][Dd][Ii][Ss]",
-- aliases for Failed verification
"[Bb]adref", "[Ff]aile?[ds] ?[rv][%w_%s]-", "[Ff][Vv]", "[Nn][Ii]?[Cc][Gg]", "[Nn]ot ?in ?[crs][%w_%s]-", "[Nn]ot specifically in source",
"[Vv]erification[%- _]failed",
-- aliases for When
"[Aa]s[ _]+of[ _]+when%??", "[Aa]s[ _%-]+of%??", "[Cc]larify date", "[Dd]ate[ _]needed", "[Nn]eeds?[ _]+date", "[Rr]ecently", "[Ss]ince[ _]+when%??",
"[Ww]HEN", "[Ww]hen%??",
-- aliases for Update
"[Nn]ot[ _]up[ _]to[ _]date","[Oo]u?[Tt][Dd]","[Oo]ut[%- _]o?f?[%- _]dated?", "[Uu]pdate", "[Uu]pdate[ _]+sect", "[Uu]pdate[ _]+Watch",
-- aliases for Pronunciation needed
"[Pp]ronunciation%??[%- _]n?e?e?d?e?d?", "[Pp]ronounce", "[Rr]equested[%- _]pronunciation", "[Rr]e?q?pron", "[Nn]eeds[%- _]pronunciation",
-- Chart, including Chart/start etc.
"[Cc]hart", "[Cc]hart/[%w_%s]-",
-- Cref and others
"[Cc]ref2?", "[Cc]note",
-- Explain and others
"[Ee]xplain", "[Ff]urther[ ]explanation[ ]needed", "[Ee]laboration[ ]needed", "[Ee]xplanation[ ]needed",
-- TOC templates
"[Cc][Oo][Mm][Pp][Aa][Cc][Tt][ _][Tt][Oo][Cc][8][5]", "[Tt][Oo][Cc]", "09[Aa][Zz]", "[Tt][Oo][Cc][ ][Cc][Oo][Mm][Pp][Aa][Cc][Tt]", "[Tt][Oo][Cc][ ][Ss][Mm][Aa][Ll][Ll]", "[Cc][Oo][Mm][Pp][Aa][Cc][Tt][ _][Aa][Ll][Pp][Hh][Aa][Bb][Ee][Tt][Ii][Cc][ _][Tt][Oo][Cc]",
"DEFAULTSORT:.-",
"[Oo]ne[ _]+source",
"[Cc]ontains[ _]+special[ _]+characters",
"[Ii]nfobox[ _]+[Cc]hinese"
}

-- Regular expressions to match all block templates that are desirable in excerpts
local wantedBlockTemplates = {
"[Bb]asketball[ _]roster[ _]header",
"[Cc]abinet[ _]table[^|}]",
"[Cc]hart[^|}]",
"[Cc]lear",
"[Cc]ol[^|}]", -- all column templates
"COVID-19[ _]pandemic[ _]data[^|}]",
"[Cc]ycling[ _]squad[^|}]",
"[Dd]ynamic[ _]list",
"[Ee]lection[ _]box[^|}]",
"[Gg]allery",
"[Gg]raph[^|}]",
"[Hh]idden",
"[Hh]istorical[ _]populations",
"[Ll]egend[ _]inline",
"[Pp]lainlist",
"[Pp]layer[^|}]",
"[Ss]eries[ _]overview",
"[Ss]ide[ _]box",
"[Ss]witcher",
"[Tt]ree[ _]chart[^|}]",
"[Tt]elevision[ _]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 = errorMessages[message] 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 = errorMessages[message] 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..list[1]..post or pre..list[2]..post or ...
local function matchAny(text, pre, list, post, init)
local match = {}
for i = 1, list do
match = { mw.ustring.match(text, pre .. list[i] .. post, init) }
if match[1] 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 flags[p] = 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 args[key] = value end
for key, value in pairs(frame.args) do args[key] = 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 s[1] ~= "N" and s[1] ~= "HL" and s[1] ~= "JA" then s[1]=s[1] .. "-" 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, "^{{%s", unwantedInlineTemplates, "%s%f[|}]") then return "" end

-- If template is wanted but produces an unwanted reference then return the string with |Note=, |ref or |shortref removed
local noRef = mw.ustring.gsub(t, "|%sNote%s=.-%f[|}]", "")
noRef = mw.ustring.gsub(noRef, "|%sref%s%f[|}]", "")
noRef = mw.ustring.gsub(noRef, "|%sshortref%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
noRef = mw.ustring.gsub(noRef, "^{{%s[Aa]udio.-|.-|(.-)%f[|}].", "%1")

-- Replace by its text parameter: → English
noRef = mw.ustring.gsub(noRef, "^{{%s[Nn]ihongo[ _]+foot%s|(.-)%f[|}].", "%1")

-- Replace by its text parameter: → N485US,
noRef = mw.ustring.gsub(noRef, "^", airreg)

if noRef ~= t then return noRef end

return nil -- not an unwanted template: keep
end

-- Get a page's content, following redirects
-- Also returns the page name, or the target page name if a redirect was followed, or false if no page found
-- For file pages, returns the content of the file description page
local function getContent(page)
local title = mw.title.new(page)
if not title then return false, false end

local target = title.redirectTarget
if target then title = target end

return title:getContent(), title.prefixedText
end

-- Get the tables only
local function getTables(text, options)
local tables = {}
for candidate in mw.ustring.gmatch(text, "%b{}") do
if mw.ustring.sub(candidate, 1, 2) == ']-|(.+)}}')
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(parts[1])
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, '@@@', '|')
parameters[key] = value
paramOrder[paramCount] = key
paramCount = paramCount + 1
end
end
end
return parameters, paramOrder
end

-- Turns an infobox file table into a
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 "\n"
end
end
-- Attempt to construct a block from
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 = parameters[key]
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
positionalImages[initialPosition+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 = positionalImages[scanPosition]
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)
positionalImages[position] = {file=filename,caption="",args=fileargs}

elseif mw.ustring.match(key, "[Ii][Mm][Aa][Gg][Ee]") or mw.ustring.match(key, "[Pp][Hh][Oo][Tt][Oo]") or mw.ustring.match(key, "[Ss][Yy][Mm][Bb][Oo][Ll]") 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)
positionalImages[position] = {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 = positionalImages[i]
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%dx?%d+%spx%s.-)|%s%dx?%d+%spx%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, "^%sFile%s:%s", "", 1)
f = mw.ustring.gsub(f, "^%sImage%s:%s", "", 1)
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", { "{{", "%[%[%s[Ff]ile:", "%[%[%s[Ii]mage:" }, "") then
token = nil
end
end

if token then -- found a template which is not the prefix to a line of text

if is(options.keepTables) and mw.ustring.sub(token, 1, 2) == ']") then
t = t .. token -- keep wanted block templates

elseif files ","") -- remove HTML comments
text = mw.ustring.gsub(text, ".-", "") -- remove noinclude bits
if mw.ustring.find(text, "[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]") then -- avoid expensive search if possible
text = mw.ustring.gsub(text, ".-", "") -- remove text between onlyinclude sections
text = mw.ustring.gsub(text, "^.-", "") -- remove text before first onlyinclude section
text = mw.ustring.gsub(text, ".", "") -- remove text after last onlyinclude section
end
if not is(options.keepSubsections) then
text = mw.ustring.gsub(text, "\n.","") -- remove first Heading== and everything after it
text = mw.ustring.gsub(text, "^==.","") -- ...even if the lead is empty
end
if not is(options.keepRefs) then
text = mw.ustring.gsub(text, "]-/%s>", "") -- remove refs cited elsewhere
text = mw.ustring.gsub(text, ".-", "") -- remove refs
text = mw.ustring.gsub(text, "{%b{}}", stripTemplate) -- remove unwanted templates such as references
end
text = mw.ustring.gsub(text, ".-", "") -- remove musical scores
text = mw.ustring.gsub(text, ".-", convertImageMap) -- convert imagemaps into standard images
text = mw.ustring.gsub(text, "%s", "") -- remove most common tables of contents
text = mw.ustring.gsub(text, "%s__[A-Z]TOC__", "") -- remove TOC behavior switches
text = mw.ustring.gsub(text, "\n%s", "\n") -- remove protection templates
text = mw.ustring.gsub(text, "%s{{[^]%-[Ss]tub%s}}", "") -- remove most stub templates
text = mw.ustring.gsub(text, "%s%[%[%s:?[Cc]ategory:.-%]%]", "") -- remove categories
text = mw.ustring.gsub(text, "^:[^\n]+\n","") -- remove DIY hatnote indented with a colon
return text
end

-- Parse a Section from a page
local function getSection(text, section, mainOnly)
local escapedSection = mw.ustring.gsub(mw.uri.decode(section), "([%^%$%(%)%%%.%[%]%%+%-%?])", "%%%1") -- %26 → & etc, then ^ → %^ etc.
local level, content = mw.ustring.match(text .. "\n", "\n(+)%s" .. escapedSection .. "%s.-\n(.)")
if not content then return luaError("sectionNotFound", section) end
local nextSection
if mainOnly then
nextSection = "\n==." -- Main part of section terminates at any level of header
else
nextSection = "\n==" .. mw.ustring.rep("=?", level - 2) .. "[^=]." -- "" → "\n?[^=].", matching "" or "" but not "===="
end
content = mw.ustring.gsub(content, nextSection, "") -- remove later sections with headings at this level or higher
if mw.ustring.match(content, "^%s$") then return luaError("sectionEmpty", section) end
return content
end

-- Parse a
-- @todo Implement custom parsing of fragments rather than relying on lst
local function getFragment(page, fragment)
local frame = mw.getCurrentFrame()
local text = frame:callParserFunction('lst', page, fragment)
if mw.ustring.match(text, "^%s$") then return luaError("fragmentEmpty", fragment) end
return text
end

-- Remove unmatched or tags
local function fixTags(text, tag)
local startCount = 0
for i in mw.ustring.gmatch(text, "") do startCount = startCount + 1 end

local endCount = 0
for i in mw.ustring.gmatch(text, "") do endCount = endCount + 1 end

if startCount > endCount then -- more than : remove the last few s
local i = 0
text = mw.ustring.gsub(text, "", function(t)
i = i + 1
if i > endCount then return "" else return nil end
end) -- "end" here terminates the anonymous replacement function(t) passed to gsub
elseif endCount > startCount then -- more than : remove the first few s
text = mw.ustring.gsub(text, "", "", endCount - startCount)
end
return text
end

local function fixTemplates(text)
repeat -- hide matched s including nested templates
local t = text
text = mw.ustring.gsub(text, "{(%b{})}", "\27{\27%1\27}\27") -- → E{Esometemplate}E}E where E represents escape
text = mw.ustring.gsub(text, "(]>[^)", "%1}\27}\27%2") -- \{sqrt\{hat{x}} → \{sqrt\{hat{x}E}E
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{ → {{, etc.
return text
end

local function fixLinks(text)
repeat -- hide matched wikilinks including nested links like link.]]
local t = text
text = mw.ustring.gsub(text, "%[(%b[])%]", "\27[\27%1\27]\27")
until text == t
text = text.gsub(text, "([%[%]])%1[^\27].", "") -- remove unmatched or and everything thereafter, avoiding ]E]E etc.
text = text.gsub(text, "([%[%]])%1$", "") -- remove unmatched or at end of text
text = mw.ustring.gsub(text, "\27", "") -- unhide matched pairs: ]E]E → ]], etc.
return text
end

-- Replace the first call to each reference defined outside of the text for the full reference, to prevent undefined references
-- Then prefix the page title to the reference names to prevent conflicts
-- that is, replace for
-- also remove reference groups: for
-- and for
-- @todo The current regex may fail in cases with both kinds of quotes, like
local function fixRefs(text, page, full)
if not full then full = getContent(page) end
local refNames = {}
local refName
local refBody
local position = 1
while position ]name%s=%s[\"']?([^\"'>]+)[\"']?[^>]/%s>()", position)
if refName then
refName = mw.text.trim(refName)
if not refNames[refName] then -- make sure we process each ref name only once
table.insert(refNames, refName)
refName = mw.ustring.gsub(refName, "[%^%$%(%)%.%[%]%%+%-%?%%]", "%%%0") -- escape special characters
refBody = mw.ustring.match(text, "]name%s=%s[\"']?%s" .. refName .. "%s[\"']?[^>/]>.-")
if not refBody then -- the ref body is not in the excerpt
refBody = mw.ustring.match(full, "]name%s=%s[\"']?%s" .. refName .. "%s[\"']?[^/>]>.-")
if refBody then -- the ref body was found elsewhere
text = mw.ustring.gsub(text, "]name%s=%s[\"']?%s" .. refName .. "%s[\"']?[^>]/?%s>", refBody, 1)
end
end
end
else
position = mw.ustring.len(text)
end
end
text = mw.ustring.gsub(text, "]name%s=%s[\"']?([^\"'>/]+)[\"']?[^>/](/?)%s>", )
text = mw.ustring.gsub(text, "]group%s=%s[\"']?[^\"'>/]+[\"']%s>", )
return text
end

-- Replace the bold title or synonym near the start of the article by a wikilink to the article
function linkBold(text, page)
local lang = mw.language.getContentLanguage()
local position = mw.ustring.find(text, "" .. lang:ucfirst(page) .. "", 1, true) -- look for "Foo is..." (uc) or "A foo is..." (lc)
or mw.ustring.find(text, "" .. lang:lcfirst(page) .. "", 1, true) -- plain search: special characters in page represent themselves
if position then
local length = mw.ustring.len(page)
text = mw.ustring.sub(text, 1, position + 2) .. "" .. mw.ustring.sub(text, position + 3, position + length + 2) .. "" .. mw.ustring.sub(text, position + length + 3, -1) -- link it
else -- look for anything unlinked in bold, assumed to be a synonym of the title (e.g. a person's birth name)
text = mw.ustring.gsub(text, "()(.-')", function(a, b)
if not mw.ustring.find(b, "%[") then -- if not wikilinked
return "" .. b .. "" -- replace Foo by Foo
else
return nil -- instruct gsub to make no change
end
end, 1) -- "end" here terminates the anonymous replacement function(a, b) passed to gsub
end
return text
end

-- Main function for modules
local function get(page, options)
if options.errors then errors = options.errors end

if not page or page == "" then return luaError("noPage") end

local text
page, section = mw.ustring.match(page, "([^]+)?([^])")
text, page = getContent(page)
if not page then return luaError("noPage") end
if not text then return luaError("pageNotFound", page) end
local full = text -- save the full text for later

if is(options.fragment) then
text = getFragment(page, options.fragment)
end

if is(section) then
text = getSection(text, section)
end

-- Strip text of all undersirables
text = cleanupText(text, options)
text = parse(text, options)

-- Replace the bold title or synonym near the start of the article by a wikilink to the article
text = linkBold(text, page)

-- Remove bold text if requested
if is(options.nobold) then text = mw.ustring.gsub(text, "", "") end

-- Keep only tables if requested
if is(options.tablesOnly) then text = getTables(text) end

-- Keep only lists if requested
if is(options.listsOnly) then text = getLists(text) end

-- Seek and destroy unterminated templates, links and tags
text = fixTemplates(text)
text = fixLinks(text)
text = fixTags(text, "div")

-- Fix broken references
if is(options.keepRefs) then text = fixRefs(text, page, full) end

-- Add (Full article...) link
if options.moreLinkText then
text = text .. " (" .. options.moreLinkText .. ")"
end

return text
end

-- Main invocation function for templates
local function main(frame)
local args = parseArgs(frame)
local page = args[1]
local ok, text = pcall(get, page, args)
if not ok then
text = errorMessages.prefix .. text
if errorCategory and errorCategory ~= and mw.title.getCurrentTitle().isContentPage then
text = text .. .. errorCategory ..
end
return mw.html.create('div'):addClass('error'):wikitext(text)
end
return frame:preprocess(text)
end

local function getMoreLinkText(more)
local defaultText = "Đọc thêm..." -- default text, same as in Template:TFAFULL
if not more or more == then -- nil/empty => use default
return defaultText
end
if not yesno(more, true) then -- falsy values => suppress the link
return nil
end
return more
end

-- Shared invocation function used by templates meant for portals
local function portal(frame, template)
local args = parseArgs(frame)

errors = args['errors'] or false -- disable error reporting unless requested

-- There should be at least one argument except with selected=Foo and Foo=Somepage
if args 0 then
local candidateKey = 1
local candidateString
local candidateArgs
if candidateCount > 1 then math.randomseed(os.time()) end
while (not text or text == "") and candidateCount > 0 do
if candidateCount > 1 then candidateKey = math.random(candidateCount) end -- pick a random candidate
candidateString = candidates[candidateKey]
if candidateString and candidateString ~= "" then
-- We have page or page or text, possibly followed by |opt1|opt2...
page, candidateArgs = mw.ustring.match(candidateString, "^%s(%[%b[]%])%s|?(.)")
if page and page ~= "" then
page = mw.ustring.match(page, "%[%[([^|%]])") -- turn text into page, discarding text
else -- we have page or page|opt...
page, candidateArgs = mw.ustring.match(candidateString, "%s([^|][^|%s])%s|?(.)")
end
-- candidate arguments (even if value is "") have priority over global arguments
if candidateArgs and candidateArgs ~= "" then
for _, t in pairs(mw.text.split(candidateArgs, "|")) do
local k, v = mw.ustring.match(t, "%s([^=]-)%s=(.-)%s$")
if k == 'files' then options.fileflags = numberFlags(v)
elseif k == 'paragraphs' then options.paraflags = numberFlags(v)
elseif k == 'more' then args.more = v
else options[k] = v end
end
end
if page and page ~= "" then
local section = mw.ustring.match(page, "[^]+([^]+)") -- save the section
text, page = getContent(page) -- make sure the page exists
if page and page ~= "" and text and text ~= "" then
if args.nostubs then
local isStub = mw.ustring.find(text, "%s{{[^{|}]%-[Ss]tub%s}}")
if isStub then text = nil end
end
if section and section ~= "" then
page = page .. '' .. section -- restore the section
end
text = get(page, options)
end
end
end
table.remove(candidates, candidateKey) -- candidate processed
candidateCount = candidateCount - 1 -- ensure that we exit the loop after all candidates are done
end
end
if not text or text == "" then return wikiError("No valid pages found") end

if args.showall then
local separator = args.showall
if separator == "" then separator = "" end
for _, candidate in pairs(candidates) do
local t = get(candidate, options)
if t ~= "" then
text = text .. separator .. t
end
end
end

-- Add a collapsed list of pages which might appear
if args.list and not args.showall then
local list = args.list
if list == "" then list = "Other articles" end
text = text .. "|bg=fff}}\n"
end

return frame:preprocess(text)
end

-- Old invocation function used by
local function excerpt(frame)
local args = parseArgs(frame)

-- Make sure the requested page exists
local page = args[1] or args.article or args.source or args.page
if not page then return wikiError("noPage") end
local title = mw.title.new(page)
if not title then return wikiError("noPage") end
if title.isRedirect then title = title.redirectTarget end
if not title.exists then return wikiError("pageNotFound", page) end
page = title.prefixedText

-- Define some useful variables
local section = args[2] or args.section or mw.ustring.match(args[1], "[^]+([^]+)")
local tag = args.tag or 'div'

-- Define the HTML elements
local block = mw.html.create(tag):addClass('excerpt-block')
if is(args.indicator) then block:addClass('excerpt-indicator') end

local style = frame:extensionTag{ name = 'templatestyles', args = { src = 'Excerpt/styles.css' } }

local hatnote
if not args.nohat then
if args.this then
hatnote = args.this
elseif args.indicator then
hatnote = 'This is'
elseif args.only == 'file' then
hatnote = 'This file is'
elseif args.only == 'file' then
hatnote = 'These files are'
elseif args.only == 'list' then
hatnote = 'This list is'
elseif args.only == 'lists' then
hatnote = 'These lists are'
elseif args.only == 'table' then
hatnote = 'This table is'
elseif args.only == 'tables' then
hatnote = 'These tables are'
else
hatnote = 'This section is'
end
hatnote = hatnote .. ' an excerpt from '
if section then
hatnote = hatnote .. .. page .. ' § ' .. section ..
else
hatnote = hatnote .. .. page ..
end
hatnote = hatnote .. "" ..
hatnote = hatnote .. title:fullUrl('action=edit') .. ' edit'
hatnote = hatnote .. .. ""
hatnote = require('Module:Hatnote')._hatnote(hatnote, {selfref=true}) or wikiError('Error generating hatnote')
end

-- Build the module options out of the template arguments and the desired defaults
local options = {
fileflags = numberFlags( args['files'] or 1 ),
paraflags = numberFlags( args['paragraphs'] ),
filesOnly = is( args['only'] 'file' or args['only'] 'files' ),
listsOnly = is( args['only'] 'list' or args['only'] 'lists'),
tablesOnly = is( args['only'] 'table' or args['only'] 'tables' ),
keepTables = is( args['tables'] or true ),
keepRefs = is( args['references'] or true ),
keepSubsections = is( args['subsections'] ),
nobold = not is( args['bold'] ),
fragment = args['fragment']
}

-- Get the excerpt itself
if section then page = page .. '' .. section end
local ok, excerpt = pcall(e.get, page, options)
if not ok then return wikiError(excerpt) end
excerpt = "\n" .. excerpt -- line break is necessary to prevent broken tables and lists
if mw.title.getCurrentTitle().isContentPage then excerpt = excerpt .. end
excerpt = frame:preprocess(excerpt)
excerpt = mw.html.create(tag):addClass('excerpt'):wikitext(excerpt)

-- Combine and return the elements
return block:node(style):node(hatnote):node(excerpt)
end

-- Entry points for templates
function p.main(frame) return main(frame) end
function p.lead(frame) return portal(frame, "lead") end -- reads a randomly selected article linked from the given page
function p.linked(frame) return portal(frame, "linked") end -- reads a randomly selected article linked from the given page
function p.listitem(frame) return portal(frame, "listitem") end -- reads a randomly selected article listed on the given page
function p.random(frame) return portal(frame, "random") end -- reads any article (default for invoke with one argument)
function p.selected(frame) return portal(frame, "selected") end -- reads the article whose key is in the selected= parameter
function p.excerpt(frame) return excerpt(frame) end -- transcludes part of an article into another article

-- Entry points for other Lua modules
function p.get(page, options) return get(page, options) end
function p.getContent(page) return getContent(page) end
function p.getSection(text, section) return getSection(text, section) end
function p.getTables(text, options) return getTables(text, options) end
function p.getLists(text, options) return getLists(text, options) end
function p.parse(text, options) return parse(text, options) end
function p.parseImage(text, start) return parseImage(text, start) end
function p.parseArgs(frame) return parseArgs(frame) end
function p.templateImages(text) return templateImages(text) end
function p.checkImage(image) return checkImage(image) end
function p.cleanupText(text, options) return cleanupText(text, options) end
function p.numberFlags(str) return numberFlags(str) end
function p.getMoreLinkText(more) return getMoreLinkText(more) end

return p