|
|
| Line 1: |
Line 1: |
| -- ATTENTION !
| |
| -- This module is deprecated
| |
| -- Prefer Module:Excerpt whenever possible
| |
|
| |
|
| -- 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 "<!--imagemap-->[[" .. 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 {{audio}} by its text parameter: {{Audio|Foo.ogg|Bar}} → Bar
| |
| noRef = mw.ustring.gsub(noRef, "^{{%s*[Aa]udio.-|.-|(.-)%f[|}].*", "%1")
| |
|
| |
| -- Replace {{Nihongo foot}} by its text parameter: {{Nihongo foot|English|英語|eigo}} → 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("{{" .. fileTitle .. "}}")
| |
| 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 [[File:...]] or [[Image:...]], 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 [[File:...]] block from {{infobox ... |image= ...}}
| |
| 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 {{Template\n on\n multiple lines}} 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=[[File:...]] etc.
| |
| if not token then
| |
| image = mw.ustring.match(image, "^[^}|\n]*") -- remove later arguments
| |
| token = "[[" -- Add File: unless name already begins File: or Image:
| |
| if not matchAny(image, "^", fileNamespaces, "%s*:") then
| |
| token = token .. "File:"
| |
| end
| |
| token = token .. image
| |
| local caption = captions[index]
| |
| if caption and mw.ustring.match(caption, "%S") then token = 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 [[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 leadStart = nil -- have we found some text yet?
| |
| local t = "" -- the stripped down output text
| |
| local fileText = "" -- output text with concatenated [[File:Foo|...]]\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 -- {{Template}} or {| Table |}
| |
| if not leadStart and not token then token = mw.ustring.match(text, "^%b<>%s*%b{}%s*") end -- allow <tag>{{template}} 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) == '{{#' then
| |
| t = t .. token -- keep parser functions
| |
|
| |
| elseif leadStart then -- lead has already started, so keep the template within the text, unless it's a whole line (navbox etc.)
| |
| if not is(options.filesOnly) and not startLine then t = t .. token end
| |
|
| |
| elseif matchAny(token, "^{{%s*", wantedBlockTemplates, "%s*%f[|}]") then
| |
| t = t .. token -- keep wanted block templates
| |
|
| |
| elseif files < maxfile then -- discard template, but if we are still collecting images...
| |
| local images = argImage(token) or {}
| |
| if not images then
| |
| local image = parseImage(token, false) -- look for embedded [[File:...]], |image=, etc.
| |
| if image then table.insert(images, image) end
| |
| end
| |
| for _, image in ipairs(images) do
| |
| if files < maxfile and checkImage(image) then -- if image is found and qualifies (not a sound file, non-free, etc.)
| |
| files = files + 1 -- count the file, whether displaying it or not
| |
| if options.fileflags and options.fileflags[files] then -- if displaying this image
| |
| image = modifyImage(image, "thumb")
| |
| image = modifyImage(image, fileArgs)
| |
| fileText = fileText .. image
| |
| end
| |
| end
| |
| end
| |
| end
| |
| else -- the next token in text is not a template
| |
| token = parseImage(text, true)
| |
| if token then -- the next token in text looks like an image
| |
| if files < maxfile and checkImage(token) then -- if more images are wanted and this is a wanted image
| |
| files = files + 1
| |
| if options.fileflags and options.fileflags[files] then
| |
| local image = token -- copy token for manipulation by adding |right etc. without changing the original
| |
| image = modifyImage(image, fileArgs)
| |
| fileText = fileText .. image
| |
| end
| |
| end
| |
| else -- got a paragraph, which ends at a file, image, blank line or end of text
| |
| local afterEnd = mw.ustring.len(text) + 1
| |
| local blankPosition = mw.ustring.find(text, "\n%s*\n") or afterEnd -- position of next paragraph delimiter (or end of text)
| |
| local endPosition = math.min( -- find position of whichever comes first: [[File:, [[Image: or paragraph delimiter
| |
| mw.ustring.find(text, "%[%[%s*[Ff]ile%s*:") or afterEnd,
| |
| mw.ustring.find(text, "%[%[%s*[Ii]mage%s*:") or afterEnd,
| |
| blankPosition)
| |
| token = mw.ustring.sub(text, 1, endPosition-1)
| |
| if blankPosition < afterEnd and blankPosition == endPosition then -- paragraph ends with a blank line
| |
| token = token .. mw.ustring.match(text, "\n%s*\n", blankPosition)
| |
| end
| |
| local isHatnote = not(leadStart) and mw.ustring.sub(token, 1, 1) == ':'
| |
| if not isHatnote then
| |
| leadStart = leadStart or mw.ustring.len(t) + 1 -- we got a paragraph, so mark the start of the lead section
| |
| paras = paras + 1
| |
| if allParagraphs or (options.paraflags and options.paraflags[paras]) then t = t .. token end -- add if this paragraph wanted
| |
| end
| |
| end -- of "else got a paragraph"
| |
| end -- of "else not a template"
| |
|
| |
| if token then text = mw.ustring.sub(text, mw.ustring.len(token)+1) end -- remove parsed token from remaining text
| |
| startLine = mw.ustring.find(token, "\n%s*$") -- will the next token be the first non-space on a line?
| |
| until not text or text == "" or not token or token == "" -- loop until all text parsed
| |
|
| |
| text = mw.ustring.gsub(t, "\n+$", "") -- remove trailing line feeds, so "{{Transclude text excerpt|Foo}} more" flows on one line
| |
|
| |
| return fileText .. text
| |
| end
| |
|
| |
| local function cleanupText(text, options)
| |
| text = mw.ustring.gsub(text, "<!%-%-.-%-%->","") -- remove HTML comments
| |
| text = mw.ustring.gsub(text, "<[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.-</[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>", "") -- 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, "</[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.-<[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>", "") -- remove text between onlyinclude sections
| |
| text = mw.ustring.gsub(text, "^.-<[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>", "") -- remove text before first onlyinclude section
| |
| text = mw.ustring.gsub(text, "</[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.*", "") -- 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*[Rr][Ee][Ff][^>]-/%s*>", "") -- remove refs cited elsewhere
| |
| text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff].->.-<%s*/%s*[Rr][Ee][Ff]%s*>", "") -- remove refs
| |
| text = mw.ustring.gsub(text, "%b{}", stripTemplate) -- remove unwanted templates such as references
| |
| end
| |
| text = mw.ustring.gsub(text, "<%s*[Ss][Cc][Oo][Rr][Ee].->.-<%s*/%s*[Ss][Cc][Oo][Rr][Ee]%s*>", "") -- remove musical scores
| |
| text = mw.ustring.gsub(text, "<%s*[Ii][Mm][Aa][Gg][Ee][Mm][Aa][Pp].->.-<%s*/%s*[Ii][Mm][Aa][Gg][Ee][Mm][Aa][Pp]%s*>", convertImageMap) -- convert imagemaps into standard images
| |
| text = mw.ustring.gsub(text, "%s*{{%s*[Tt][Oo][Cc].-}}", "") -- 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*{{%s*[Pp]p%-.-}}", "\n") -- remove protection templates
| |
| text = mw.ustring.gsub(text, "%s*{{[^{|}]*[Ss]idebar%s*}}", "") -- remove most sidebars
| |
| 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 <section begin="Name of the fragment">
| |
| -- @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 <tag> or </tag> tags
| |
| local function fixTags(text, tag)
| |
| local startCount = 0
| |
| for i in mw.ustring.gmatch(text, "<%s*" .. tag .. "%f[^%w_].->") do startCount = startCount + 1 end
| |
|
| |
| local endCount = 0
| |
| for i in mw.ustring.gmatch(text, "<%s*/" .. tag .. "%f[^%w_].->") do endCount = endCount + 1 end
| |
|
| |
| if startCount > endCount then -- more <tag> than </tag>: remove the last few <tag>s
| |
| local i = 0
| |
| text = mw.ustring.gsub(text, "<%s*" .. tag .. "%f[^%w_].->", 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 </tag> than <tag>: remove the first few </tag>s
| |
| text = mw.ustring.gsub(text, "<%s*/" .. tag .. "%f[^%w_].->", "", endCount - startCount)
| |
| end
| |
| return text
| |
| end
| |
|
| |
| local function fixTemplates(text)
| |
| repeat -- hide matched {{template}}s including nested templates
| |
| local t = text
| |
| text = mw.ustring.gsub(text, "{(%b{})}", "\27{\27%1\27}\27") -- {{sometemplate}} → E{Esometemplate}E}E where E represents escape
| |
| text = mw.ustring.gsub(text, "(< *math[^>]*>[^<]-)}}(.-< */math *>)", "%1}\27}\27%2") -- <math>\{sqrt\{hat{x}}</math> → <math>\{sqrt\{hat{x}E}E</math>
| |
| 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 [[wikilink]]s including nested links like [[File:Example.jpg|Some [[nested]] 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 <ref name="Foo"> for <ref name="Title of the article Foo">
| |
| -- and also <ref name="Foo" /> for <ref name="Title of the article Foo" />
| |
| -- also remove reference groups: <ref name="Foo" group="Bar"> for <ref name="Title of the article Foo">
| |
| -- and <ref group="Bar"> for <ref>
| |
| -- @todo The current regex may fail in cases with both kinds of quotes, like <ref name="Darwin's book">
| |
| 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*>", '<ref name="' .. page .. ' %1" %2>')
| |
| text = mw.ustring.gsub(text, "<%s*[Rr][Ee][Ff][^>]*group%s*=%s*[\"']?[^\"'>/]+[\"']%s*>", '<ref>')
| |
| 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 "'''[[" .. page .. "|" .. b .. "]]'''" -- replace '''Foo''' by '''[[page|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 .. " ('''[[" .. page .. "|" .. 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 [[page|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 [[page|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 .. '|' .. page .. ' § ' .. section .. ']]'
| |
| else
| |
| hatnote = hatnote .. '[[' .. page .. ']]'
| |
| end
| |
| hatnote = hatnote .. "''" .. '<span class="mw-editsection-like plainlinks"><span class="mw-editsection-bracket">[</span>['
| |
| hatnote = hatnote .. title:fullUrl('action=edit') .. ' edit'
| |
| hatnote = hatnote .. ']<span class="mw-editsection-bracket">]</span></span>' .. "''"
| |
| 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 .. '[[Category:Articles with excerpts]]' 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
| |