Module:Excerpt

-- Name of the category to track content pages with errors local errorCategory = "Articles with broken excerpts"

-- Error messages local errorMessages = { prefix = "Excerpt error: ", noPage = "No page given", pageNotFound = "Page '%s' not found", leadEmpty = "Lead section is empty", sectionEmpty = "Section '%s' is empty", sectionNotFound = "Section '%s' not found", fragmentEmpty = "Fragment '%s' is empty", fragmentNotFound = "Fragment '%s' not found" }

-- 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[^=|]*" }

-- Regular expressions to match all inline templates that are undesirable in excerpts local unwantedInlineTemplates = { "[Ee]fn", "[Ee]fn%-[lu][arg]", "[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" }

-- 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 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

-- 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 |shortref or |ref removed local noRef = mw.ustring.gsub(t, "|%s*shortref%s*%f[|}]", "") noRef = mw.ustring.gsub(noRef, "|%s*ref%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 (英語) → English noRef = mw.ustring.gsub(noRef, "^{{%s*[Nn]ihongo[ _]+foot%s*|(.-)%f[|}].*", "%1")

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) == '{|' then table.insert(tables, candidate) end end return table.concat(tables, '\n') end

-- Get the lists only local function getLists(text, options) local lists = {} for list in mw.ustring.gmatch(text, "\n[*#][^\n]+") do		table.insert(lists, list) end return table.concat(lists, '\n') end

-- Check image for suitability local function checkImage(image) local page = matchAny(image, "", fileNamespaces, "%s*:[^|%]]*") -- match File:(name) or Image:(name) if not page then return false end

-- Limit to image types: .gif, .jpg, .jpeg, .png, .svg, .tiff, .xcf (exclude .ogg, audio, etc.) local fileTypes = {"[Gg][Ii][Ff]", "[Jj][Pp][Ee]?[Gg]", "[Pp][Nn][Gg]", "[Ss][Vv][Gg]", "[Tt][Ii][Ff][Ff]", "[Xx][Cc][Ff]"} if not matchAny(page, "%.", fileTypes, "%s*$") then return false end

-- Check the local wiki local fileDescription, fileTitle = getContent(page) -- get file description and title after following any redirect if not fileTitle or fileTitle == "" then return false end -- the image doesn't exist

-- Check Commons if not fileDescription or fileDescription == "" then local frame = mw.getCurrentFrame fileDescription = frame:preprocess("") end

-- Filter non-free images if not fileDescription or fileDescription == "" or mw.ustring.match(fileDescription, "[Nn]on%-free") then return false end

return true end

-- Attempt to parse or, either anywhere (start=false) or at the start only (start=true) local function parseImage(text, start) local startre = "" if start then startre = "^" end -- a true flag restricts search to start of string local image = matchAny(text, startre .. "%[%[%s*", fileNamespaces, "%s*:.*") -- [[File: or [[Image: ...	if image then		image = mw.ustring.match(image, "%b[]%s*") -- matching [[...]] to handle wikilinks nested in caption	end	return image end

-- Parse a caption, which ends at a | (end of parameter) or } (end of infobox) but may contain nested [..] and {..} local function parseCaption(caption) if not caption then return nil end local length = mw.ustring.len(caption) local position = 1 while position <= length do		local linkStart, linkEnd = mw.ustring.find(caption, "%b[]", position) linkStart = linkStart or length + 1 -- avoid comparison with nil when no link local templateStart, templateEnd = mw.ustring.find(caption, "%b{}", position) templateStart = templateStart or length + 1 -- avoid comparison with nil when no template local argEnd = mw.ustring.find(caption, "[|}]", position) or length + 1 if linkStart < templateStart and linkStart < argEnd then position = linkEnd + 1 -- skip wikilink elseif templateStart < argEnd then position = templateEnd + 1 -- skip template else -- argument ends before the next wikilink or template return mw.ustring.sub(caption, 1, argEnd - 1) end end return caption -- No terminator found: return entire caption end

-- Attempt to construct a block from local function argImage(text) local token = nil 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 text = mw.ustring.gsub(text, '<!%-%-imagemap%-%->', '|imagemap=')

-- find all images local hasImages = false local images = {} local captureFrom = 1 while captureFrom < mw.ustring.len(text) do		local argname, position, image = mw.ustring.match(text, "|%s*([^=|]-[Ii][Mm][Aa][Gg][Ee][^=|]-)%s*=%s*(.*)", captureFrom) if image then -- ImageCaption=, image_size=, image_upright=, etc. do not introduce an image local lcArgName = mw.ustring.lower(argname) if mw.ustring.find(lcArgName, "caption") or mw.ustring.find(lcArgName, "size") or mw.ustring.find(lcArgName, "upright") then image = nil end end if image then hasImages = true images[position] = image captureFrom = position else captureFrom = mw.ustring.len(text) end end captureFrom = 1 while captureFrom < mw.ustring.len(text) do		local position, image = mw.ustring.match(text, "|%s*[^=|]-[Pp][Hh][Oo][Tt][Oo][^=|]-%s*=%s*(.*)", captureFrom) if image then hasImages = true images[position] = image captureFrom = position else captureFrom = mw.ustring.len(text) end end captureFrom = 1 while captureFrom < mw.ustring.len(text) do		local position, image = mw.ustring.match(text, "|%s*[^=|{}]-%s*=%s*%[?%[?([^|{}]*%.%a%a%a%a?)%s*%f[|}]", captureFrom) if image then hasImages = true if not images[position] then images[position] = image end captureFrom = position else captureFrom = mw.ustring.len(text) end end

if not hasImages then return nil end

-- find all captions local captions = {} captureFrom = 1 while captureFrom < mw.ustring.len(text) do		local position, caption = matchAny(text, "|%s*", captionParams, "%s*=%s*([^\n]+)", captureFrom) if caption then -- extend caption to parse "| caption = Foo Bar\n" local bracedCaption = mw.ustring.match(text, "^[^\n]-%b{}[^\n]+", position) if bracedCaption and bracedCaption ~= "" then caption = bracedCaption end caption = mw.text.trim(caption) local captionStart = mw.ustring.sub(caption, 1, 1) if captionStart == '|' or captionStart == '}' then caption = nil end end if caption then -- find nearest image, and use same index for captions table local i = position while i > 0 and not images[i] do				i = i - 1 if images[i] then if not captions[i] then captions[i] = parseCaption(caption) end end end captureFrom = position else captureFrom = mw.ustring.len(text) end end

-- find all alt text local altTexts = {} for position, altText in mw.ustring.gmatch(text, "|%s*[Aa][Ll][Tt]%s*=%s*([^\n]*)") do		if altText then

-- altText is terminated by }} or |, but first skip any matched ... and local lookFrom = math.max( -- find position after whichever comes last: start of string, end of last ]] or end of last }}			 mw.ustring.match(altText, ".*{%b{}}") or 1, -- if multiple, .* consumes all but one, leaving the last for %b			 mw.ustring.match(altText, ".*%[%b[]%]") or 1)

local length = mw.ustring.len(altText) local afterText = math.min( -- find position after whichever comes first: end of string, }} or |			 mw.ustring.match(altText, "}}", lookFrom) or length+1,			 mw.ustring.match(altText, "|", lookFrom) or length+1) altText = mw.ustring.sub(altText, 1, afterText-1) -- chop off |... or }}... which is not part of ... or

altText = mw.text.trim(altText) local altTextStart = mw.ustring.sub(altText, 1, 1) if altTextStart == '|' or altTextStart == '}' then altText = nil end end if altText then -- find nearest image, and use same index for altTexts table local i = position while i > 0 and not images[i] do				i = i - 1 if images[i] then if not altTexts[i] then altTexts[i] = altText end end end end end

-- find all image sizes local imageSizes = {} for position, imageSizeMatch in mw.ustring.gmatch(text, "|%s*[Ii][Mm][Aa][Gg][Ee][ _]?[Ss][Ii][Zz][Ee]%s*=%s*([^}|\n]*)") do		local imageSize = mw.ustring.match(imageSizeMatch, "=%s*([^}|\n]*)") if imageSize then imageSize = mw.text.trim(imageSize ) local imageSizeStart = mw.ustring.sub(imageSize, 1, 1) if imageSizeStart == '|' or imageSizeStart == '}' then imageSize = nil end end if imageSize then -- find nearest image, and use same index for imageSizes table local i = position while i > 0 and not images[i] do				i = i - 1 if images[i] then if not imageSizes[i] then imageSizes[i] = imageSize end end end end end

-- sort the keys of the images table (in a table sequence), so that images can be iterated over in order local keys = {} for key, val in pairs(images) do		table.insert(keys, key) end table.sort(keys)

-- add in relevant optional parameters for each image: caption, alt text and image size local imageTokens = {} for _, index in ipairs(keys) do		local image = images[index] local token = parseImage(image, true) -- look for image= etc.		if not token then image = mw.ustring.match(image, "^[^}|\n]*") -- remove later arguments token = "" .. caption end			local alt = altTexts[index]			if alt then token = token .. "|alt=" .. alt end			local image_size = imageSizes[index]			if image_size and mw.ustring.match(image_size, "%S") then token = token .. "|" .. image_size end			token = token .. "" end token = mw.ustring.gsub(token, "\n","") .. "\n" table.insert(imageTokens, token) end return imageTokens end

local function modifyImage(image, fileArgs) 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  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 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 = "" 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 {| Table |} 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) < mw.ustring.len(line) then -- template is followed by text (but it may just be other templates) line = mw.ustring.gsub(line, "%b{}", "") -- remove all templates from this line line = mw.ustring.gsub(line, "%b<>", "") -- 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 tables

elseif mw.ustring.sub(token, 1, 3) == '(.-< */math *>)", "%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 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  -- and also 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 < mw.ustring.len(text) do		refName, position = mw.ustring.match(text, "<%s*[Rr][Ee][Ff][^>]*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, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^>/]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>")				if not refBody then -- the ref body is not in the excerpt					refBody = mw.ustring.match(full, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?%s*" .. refName .. "%s*[\"']?[^/>]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>")					if refBody then -- the ref body was found elsewhere						text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*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, "<%s*[Rr][Ee][Ff][^>]*name%s*=%s*[\"']?([^\"'>/]+)[\"']?[^>/]*(/?)%s*>", '')	text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*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 = "Full article..." -- 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 < 1 and not (template == "selected" and args[template] and args[args[template]]) then return wikiError("noPage") end

-- Figure out the page to excerpt local page local candidates = {}

if template == "lead" then page = args[1] page = mw.text.trim(page) if not page or page == "" then return wikiError("noPage") end candidates = { page }

elseif template == "selected" then local key = args[template] local count = #args if tonumber(key) then -- normalise article number into the range 1..#args key = key % count if key == 0 then key = count end end page = args[key] page = mw.text.trim(page) if not page or page == "" then return wikiError("noPage") end candidates = { page }

elseif template == "linked" or template == "listitem" then local source = args[1] local text, source = getContent(source) if not source then return wikiError("noPage") elseif not text then return wikiError("noPage") end local section = args.section if section then -- check relevant section only text = getSection(text, section) if not text then return wikiError("sectionNotFound", section) end end -- Replace annotated links with real links text = mw.ustring.gsub(text, "{{%s*[Aa]nnotated[ _]link%s*|%s*(.-)%s*}}", "%1") if template == "linked" then for candidate in mw.ustring.gmatch(text, "%[%[%s*([^%]|\n]*)") do table.insert(candidates, candidate) end else -- listitem: first wikilink on a line beginning *, :#, etc. except in "See also" or later section text = mw.ustring.gsub(text, "\n== *See also.*", "") for candidate in mw.ustring.gmatch(text, "\n:*[%*#][^\n]-%[%[%s*([^%]|\n]*)") do table.insert(candidates, candidate) end end

elseif template == "random" then for key, value in pairs(args) do			if value and type(key) == "number" then table.insert(candidates, mw.text.trim(value)) end end end

-- Build an options array for the Excerpt module out of the arguments and the desired defaults local options = { errors = args['errors'] or false, fileargs = args['fileargs'], fileflags = numberFlags( args['files'] ), paraflags = numberFlags( args['paragraphs'] ), moreLinkText = getMoreLinkText(args['more']) }

-- Select a random candidate and make sure its valid local text local candidateCount = #candidates if candidateCount > 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 = "{{clear}}{{hr}}" 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 .. "{{collapse top|title={{resize|85%|" ..list .. "}}|bg=fff}}{{hlist" for _, candidate in pairs(candidates) do if mw.ustring.match(candidate, "%S") then text = text .. "|" .. mw.text.trim(candidate) .. "" end end text = text .. "}}\n{{collapse bottom}}" end

return frame:preprocess(text) end

-- Old invocation function used by {{Excerpt}} 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.wikiError(message, value) return wikiError(message, value) end function p.lead(frame) return portal(frame, "lead") end -- {{Transclude lead excerpt}} reads a randomly selected article linked from the given page function p.linked(frame) return portal(frame, "linked") end -- {{Transclude linked excerpt}} reads a randomly selected article linked from the given page function p.listitem(frame) return portal(frame, "listitem") end -- {{Transclude list item excerpt}} reads a randomly selected article listed on the given page function p.random(frame) return portal(frame, "random") end -- {{Transclude random excerpt}} reads any article (default for invoke with one argument) function p.selected(frame) return portal(frame, "selected") end -- {{Transclude selected excerpt}} reads the article whose key is in the selected= parameter function p.excerpt(frame) return excerpt(frame) end -- {{Excerpt}} 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.argImage(text) return argImage(text) end function p.checkImage(image) return checkImage(image) end function p.cleanupText(text, options) return cleanupText(text, options) end function p.luaError(message, value) return luaError(message, value) end function p.is(value) return is(value) end function p.numberFlags(str) return numberFlags(str) end function p.getMoreLinkText(more) return getMoreLinkText(more) end

-- Entry points for backwards compatibility function p.getsection(text, section) return getSection(text, section) end function p.parseimage(text, start) return parseImage(text, start) end function p.checkimage(image) return checkImage(image) end function p.argimage(text) return argImage(text) end function p.numberflags(str) return numberFlags(str) end

return p