-- Module:Excerpt implements the Excerpt template
local p = {}
-- Documentation and master version: https://en.wikipedia.org/wiki/Module:Excerpt
local mRedirect = require('Module:Redirect')
-- Authors: User:Sophivorus, User:Certes, User:Aidan9382 & others
-- License: CC-BY-SA-3.0
local Transcluder = require( 'Module:Transcluder' )
-- Get a redirect target (or nil if not a redirect) without using the expensive title object property .isRedirect
local function getRedirectTarget(titleObject)
local content = titleObject:getContent()
if not content then return nil end
return mRedirect.getTargetFromText(content)
end
local yesno = require( 'Module:Yesno' )
local errors
-- Return blank text, or an error message if requested
local function err(text)
if errors then error(text, 2) end
return ""
end
local ok, config = pcall( require, 'Module:Excerpt/config' )
-- In text, match pre..list[1]..post or pre..list[2]..post or ...
if not ok then config = {} end
local function matchany(text, pre, list, post)
local match
for i = 1, #list do
match = mw.ustring.match(text, pre .. list[i] .. post)
if match then return match end
end
return nil
end
local p = {}
-- Get a page's content, following redirects, and processing file description pages for files.
-- Also returns the page name, or the target page name if a redirect was followed, or false if no page found
local function getContent(page, frame)
local title = mw.title.new(page) -- Read description page (for :File:Foo rather than File:Foo)
if not title then return false, false end
-- Helper function to get arguments
local redir = getRedirectTarget(title)
local args
if redir then title = mw.title.new(redir) end
local function getArg( key, default )
local value = args[ key ]
if title.namespace == 6 then
if value and mw.text.trim( value ) ~= '' then
frame = frame or mw.getCurrentFrame()
return value
return frame:preprocess("{{" .. title.prefixedText .. "}}"), redir or title.prefixedText
else
return title:getContent(), redir or title.prefixedText
end
return default
end
-- Helper function to handle errors
-- Check image for suitability
local function checkimagegetError(image message, value )
if type( message ) == 'string' then
local page = matchany(image, "", {"[Ff]ile", "[Ii]mage"}, "%s*:[^|%]]*") -- match File:(name) or Image:(name)
message = Transcluder.getError( message, value )
if not page then return false end
-- Limit to image types: .gif, .jpg, .jpeg, .png, .svg, .tiff, .xcf (exclude .ogg audio etc.)
if not matchany(page, "%.", {"[Gg][Ii][Ff]", "[Jj][Pp][Ee]?[Gg]", "[Pp][Nn][Gg]", "[Ss][Vv][Gg]", "[Tt][Ii][Ff][Ff]", "[Xx][Cc][Ff]"}, "%s*$") then
return false
end
if config.categories and config.categories.errors and mw.title.getCurrentTitle().isContentPage then
message:node( '[[Category:' .. config.categories.errors .. ']]' )
local desc = getContent(page)
end
return ( desc and desc ~= "" and not mw.ustring.match(desc, "[Nn]on%-free") ) and true or false -- hide non-free image
return message
end
-- Helper function to get localized messages
-- Attempt to parse [[File:...]] or [[Image:...]], either anywhere (start=false) or at the start only (start=true)
local function parseimagegetMessage(text, startkey )
local startreok, TNT = ""pcall( require, 'Module:TNT' )
if not ok then return key end
if start then startre = "^" end -- a true flag restricts search to start of string
return TNT.format( 'I18n/Module:Excerpt.tab', key )
local image = matchany(text, startre .. "%[%[%s*", {"[Ff]ile", "[Ii]mage"}, "%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
-- Main entry point for templates
-- Parse a caption, which ends at a | (end of parameter) or } (end of infobox) but may contain nested [..] and {..}
local function parsecaptionp.main(caption frame )
args = Transcluder.parseArgs( frame )
if not caption then return nil end
local len = mw.ustring.len(caption)
local pos = 1
while pos <= len do
local linkstart, linkend = mw.ustring.find(caption, "%b[]", pos)
linkstart = linkstart or len + 1 -- avoid comparison with nil when no link
local templatestart, templateend = mw.ustring.find(caption, "%b{}", pos)
templatestart = templatestart or len + 1 -- avoid comparison with nil when no template
local argend = mw.ustring.find(caption, "[|}]", pos) or len + 1
if linkstart < templatestart and linkstart < argend then
pos = linkend + 1 -- skip wikilink
elseif templatestart < argend then
pos = templateend + 1 -- skip template
else -- argument ends before the next wikilink or template
return mw.ustring.sub(caption, 1, argend - 1)
end
end
end
-- Make sure the requested page exists
-- Attempt to construct a [[File:...]] block from {{infobox ... |image= ...}}
local functionpage argimage= getArg(text 1 )
if not page or page == '{{{1}}}' then return getError( 'no-page' ) end
local token = nil
local hasNamedArgstitle = mw.ustringtitle.findnew(text, "|") and mw.ustring.find(text, "="page)
if not hasNamedArgstitle then return nil endgetError( 'invalid-- filter out any template that obviously doesntitle't, containpage an) imageend
if title.isRedirect then title = title.redirectTarget end
if not title.exists then return getError( 'page-not-found', page ) end
page = title.prefixedText
-- Set variables from the template parameters
text = mw.ustring.gsub(text, '<!%-%-imagemap%-%->', '|imagemap=')
local section = getArg( 2, mw.ustring.match( getArg( 1 ), '[^#]+#(.+)' ) )
local hat = yesno( getArg( 'hat', true ) )
local edit = yesno( getArg( 'edit', true ) )
local this = getArg( 'this' )
local only = getArg( 'only' )
local files = getArg( 'files', getArg( 'file', ( only == 'file' and 1 ) ) )
local lists = getArg( 'lists', getArg( 'list', ( only == 'list' and 1 ) ) )
local tables = getArg( 'tables', getArg( 'table', ( only == 'table' and 1 ) ) )
local templates = getArg( 'templates', getArg( 'template', ( only == 'template' and 1 ) ) )
local paragraphs = getArg( 'paragraphs', getArg( 'paragraph', ( only == 'paragraph' and 1 ) ) )
local references = getArg( 'references' )
local subsections = not yesno( getArg( 'subsections' ) )
local noLinks = not yesno( getArg( 'links', true ) )
local noBold = not yesno( getArg( 'bold' ) )
local onlyFreeFiles = yesno( getArg( 'onlyfreefiles', true ) )
local briefDates = yesno( getArg( 'briefdates', false ) )
local inline = yesno( getArg( 'inline' ) )
local quote = yesno( getArg( 'quote' ) )
local more = yesno( getArg( 'more' ) )
local class = getArg( 'class' )
local displaytitle = getArg( 'displaytitle' ) or page
-- findBuild allthe imageshatnote
if hat and not inline then
local hasImages = false
if this then
local images = {}
hat = this
for position, image in mw.ustring.gmatch(text, "|%s*[^=|]-[Ii][Mm][Aa][Gg][Ee][^=|]-%s*=%s*()(.*)") do
elseif quote then
hasImages = true
hat = getMessage( 'this' )
images[position] = image
elseif only then
end
hat = getMessage( only )
for position, image in mw.ustring.gmatch(text, "|%s*[^=|]-[Pp][Hh][Oo][Tt][Oo][^=|]-%s*=%s*()(.*)") do
else
hasImages = true
hat = getMessage( 'section' )
images[position] = image
end
for position, image in mw.ustring.gmatch(text, "|%s*[^=|{}]-%s*=%s*()%[?%[?([^|{}]*%.%a%a%a%a?)%s*%f[|}]") do
hasImages = true
if not images[position] then
images[position] = image
end
hat = hat .. ' ' .. getMessage( 'excerpt' ) .. ' '
end
if section then
hat = hat .. '[[:' .. page .. '#' .. mw.uri.anchorEncode( section ) .. '|' .. displaytitle
if not hasImages then return nil end
.. ' § ' .. mw.ustring.gsub( section, '%[%[([^]|]+)|?[^]]*%]%]', '%1' ) .. ']].' -- remove nested links
-- find all captions
local captions = {}
local capture_from = 0
while capture_from < #text do
local position, caption = mw.ustring.match(text, "|%s*[^=|]*[Cc][Aa][Pp][Tt][Ii][Oo][Nn][^=|]*%s*=%s*()([^\n]+)", capture_from)
if caption then
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
capture_from = position
else
hat = hat .. '[[:' .. page .. '|' .. displaytitle .. ']].'
capture_from = #text
end
if edit then
end
hat = hat .. '<span class="mw-editsection-like plainlinks"><span class="mw-editsection-bracket">[</span>['
hat = hat .. title:fullUrl( 'action=edit' ) .. ' ' .. mw.message.new( 'editsection' ):plain()
-- find all alt text
hat = hat .. ']<span class="mw-editsection-bracket">]</span></span>'
local altTexts = {}
for position, altText in mw.ustring.gmatch(text, "|%s*[Aa][Ll][Tt]%s*=%s*()([^\n]*)") do
if altText then
altText = mw.text.trim(altText)
local altTextStart = mw.ustring.sub(altText, 1, 1)
if altTextStart == '|' or altTextStart == '}' then altText = nil end
end
if altTextconfig.hat then
hat = config.hat .. hat .. '}}'
-- find nearest image, and use same index for altTexts table
hat = frame:preprocess( hat )
local i = position
else
while i > 0 and not images[i] do
hat = mw.html.create( 'div' ):addClass( 'dablink excerpt-hat' ):wikitext( hat )
i = i - 1
if images[i] then
if not altTexts[i] then
altTexts[i] = altText
end
end
end
end
else
hat = nil
end
-- findBuild allthe image"Read sizesmore" link
if more and not inline then
local imageSizes = {}
more = "'''[[" .. page .. '#' .. ( section or '' ) .. "|" .. getMessage( 'more' ) .. "]]'''"
for position, imageSizeMatch in mw.ustring.gmatch(text, "|%s*[Ii][Mm][Aa][Gg][Ee][ _]?[Ss][Ii][Zz][Ee]%s*=%s*()([^}|\n]*)") do
more = mw.html.create( 'div' ):addClass( 'noprint excerpt-more' ):wikitext( more )
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, "^", {"[Ff]ile", "[Ii]mage"}, "%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
-- Help gsub convert imagemaps into standard images
local function convertImagemap(imagemap)
local image = matchany(imagemap, "[>\n]%s*", {"[Ii]mage:", "[Ff]ile:"}, "[^\n]*")
if image then
return "<!--imagemap-->[[" .. mw.ustring.gsub(image, "[>\n]%s*", "", 1) .. "]]"
else
more = nil
return "" -- remove entire block if image can't be extracted
end
end
-- Build the options for Module:Transcluder out of the template parameters and the desired defaults
-- Help gsub to remove unwanted templates
local functionoptions striptemplate(t)= {
files = files,
-- If template is unwanted then return "" (gsub will replace by nothing), else return nil (gsub will keep existing string)
lists = lists,
local unwanted = {"[Ee]fn", "[Ee]fn%-[lu][arg]", "[Ee]l[mn]", "[Rr]p?", "[Ss]fn[bmp]", "[Ss]f[bn]", "NoteTag", "#[Tt]ag:%s*[Rr]ef", "[Rr]efn?",
tables = tables,
"[CcDd]n", "[Cc]itation needed", "[Dd]isambiguation needed", "[Ff]eatured article", "[Gg]ood article",
paragraphs = paragraphs,
"[Dd]ISPLAYTITLE", "[Ss]hort[ _]+description", "[Cc]itation", "[Cc]ite[%- _]+[%w_%s]-", "[Cc]oor[%w_%s]-",
sections = subsections,
"[Uu]?n?[Rr]eliable source[%?%w_%s]-", "[Rr]s%??", "[Vv]c", "[Vv]erify credibility",
categories = 0,
-- aliases for Clarification needed
references = references,
"[Cc]f[ny]", "[Cc]larification[ _]+inline", "[Cc]larification[%- _]*needed", "[Cc]larification", "[Cc]larify%-inline", "[Cc]larify%-?me",
only = only and mw.text.trim( only, 's' ) .. 's',
"[Cc]larify[ _]+inline", "[Cc]larify", "[Cc]LARIFY", "[Cc]onfusing%-inline", "[Cc]onfusing%-short", "[Ee]xplainme", "[Hh]uh[ _]*%??", "[Ww]hat%?",
noLinks = noLinks,
"[Ii]nline[ _]+[Uu]nclear", "[Ii]n[ _]+what[ _]+sense", "[Oo]bscure", "[Pp]lease[ _]+clarify", "[Uu]nclear[ _]+inline", "[Ww]hat's[ _]+this%?",
noBold = noBold,
"[Gg]eoQuelle",
noSelfLinks = true,
-- Primary source etc.
noNonFreeFiles = onlyFreeFiles,
"[Pp]s[ci]", "[Nn]psn", "[Nn]on%-primary[ _]+source[ _]+needed", "[Ss]elf-published[%w_%s]-", "[Uu]ser-generated[%w_%s]-",
noBehaviorSwitches = true,
"[Pp]rimary source[%w_%s]-", "[Ss]econdary source[%w_%s]-", "[Tt]ertiary source[%w_%s]-", "[Tt]hird-party[%w_%s]-",
fixReferences = true,
-- aliases for Disambiguation (page) and similar
linkBold = true,
"[Bb]egriffsklärung", "[Dd][Aa][Bb]", "[Dd]big", "[%w_%s]-%f[%w][Dd]isam[%w_%s]-", "[Hh][Nn][Dd][Ii][Ss]"}
}
-- Get the excerpt itself
if matchany(t, "^{{%s*", unwanted, "%s*%f[|}]") then return "" end
local title = page .. '#' .. ( section or '' )
local ok, excerpt = pcall( Transcluder.get, title, options )
-- If template is wanted but produces an unwanted reference then return the string with |shortref or |ref removed
if not ok then return getError( excerpt ) end
local noref = mw.ustring.gsub(t, "|%s*shortref%s*%f[|}]", "")
if mw.text.trim( excerpt ) == '' and not only then
noref = mw.ustring.gsub(noref, "|%s*ref%s*%f[|}]", "")
if section then return getError( 'section-empty', section ) else return getError( 'lead-empty' ) end
-- If a wanted template has unwanted nested templates, purge them too
noref = mw.ustring.sub(noref, 1, 2) .. mw.ustring.gsub(mw.ustring.sub(t, 3), "%b{}", striptemplate)
if noref ~= t then return noref end
return nil -- not an unwanted template: keep
end
-- Convert a comma-separated list of numbers or min-max ranges into a list of booleans, e.g. "1,3-5" → {1=true,2=false,3=true,4=true,5=true}
local function numberflags(str)
local ranges = mw.text.split(str, ",") -- parse ranges, e.g. "1,3-5" → {"1","3-5"}
local flags = {}
for _, r in pairs(ranges) do
local min, max = mw.ustring.match(r, "^%s*(%d+)%s*%-%s*(%d+)%s*$") -- "3-5" → min=3 max=5
if not max then min, max = mw.ustring.match(r, "^%s*((%d+))%s*$") end -- "1" → min=1 max=1
if max then
for p = min, max do flags[p] = true end
end
end
return flags
end
-- Fix birth and death dates, but only in the first paragraph
-- a basic parser to trim down extracted wikitext
if briefDates then
-- @param text : Wikitext to be processed
local startpos = 1 -- skip initial templates
-- @param options : A table of options...
local s
-- 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.
local e = 0
-- 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}`
repeat
-- options.fileargs : args for the [[File:]] syntax, such as `left`
startpos = e + 1
-- @param filesOnly : If set, only return the files and not the prose
s, e = mw.ustring.find( excerpt, "%s*%b{}%s*", startpos )
local function parse(text, options, filesOnly)
until not s or s > startpos
local allparas = true -- keep all paragraphs?
s, e = mw.ustring.find( excerpt, "%b()", startpos ) -- get (...), which may be (year–year)
if options.paraflags then
if s and s < startpos + 100 then -- look only near the start
if type(options.paraflags) ~= "table" then options.paraflags = numberflags(options.paraflags) end
local year1, conjunction, year2 = mw.ustring.match( mw.ustring.sub( excerpt, s, e ), '(%d%d%d+)(.-)(%d%d%d+)' )
for _, v in pairs(options.paraflags) do
if year1 and year2 and (mw.ustring.match( conjunction, '[%-–—]' ) or mw.ustring.match( conjunction, '{{%s*[sS]nd%s*}}' )) then
if v then allparas = false end -- if any para specifically requested, don't keep all
local y1 = tonumber(year1)
local y2 = tonumber(year2)
if y2 > y1 and y2 < y1 + 125 and y1 <= tonumber( os.date( "%Y" )) then
excerpt = mw.ustring.sub( excerpt, 1, s ) .. year1 .. "–" .. year2 .. mw.ustring.sub( excerpt, e )
end
end
end
end
if filesOnly then
allparas = 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
text = mw.ustring.gsub(text,"^%s*","") -- remove initial white space
repeat -- loop around parsing a template, image or paragraph
local token = mw.ustring.match(text, "^%b{}%s*") or false -- {{Template}}
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
-- 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 no file was found, try to get one from the infobox
if token then -- found a template which is not the prefix to a line of text
local fileNamespaces = Transcluder.getNamespaces( 'File' )
if leadstart then -- lead has already started, so keep the template within the text
if ( ( only == 'file' or only == 'files' ) or ( not only and ( files ~= '0' or not files ) ) ) and -- caller asked for files
if not filesOnly then t = t .. token end
not Transcluder.matchAny( excerpt, '%[%[', fileNamespaces, ':' ) and -- and there are no files in Transcluder's output
elseif files < maxfile then -- discard template, but if we are still collecting images...
config.captions -- and we have the config option required to try finding files in templates
local images = argimage(token) or {}
then
if not images then
-- We cannot distinguish the infobox from the other templates so we search them all
local image = parseimage(token, false) -- look for embedded [[File:...]], |image=, etc.
local infobox = Transcluder.getTemplates( excerpt );
if image then table.insert(images, image) end
infobox = table.concat( infobox )
local parameters = Transcluder.getParameters( infobox )
local file, captions, caption, cssclasses, cssclass
for _, pair in pairs( config.captions ) do
file = pair[1]
file = parameters[file]
if file and Transcluder.matchAny( file, '^.*%.', { '[Jj][Pp][Ee]?[Gg]', '[Pp][Nn][Gg]', '[Gg][Ii][Ff]', '[Ss][Vv][Gg]' }, '.*' ) then
file = mw.ustring.match( file, '%[?%[?.-:([^{|]+)%]?%]?' ) or file -- [[File:Example.jpg{{!}}upright=1.5]] to Example.jpg
captions = pair[2]
for _, p in pairs( captions ) do
if parameters[ p ] then caption = parameters[ p ] break end
end
for _, image in ipairs(images) do
-- Check for CSS classes
if files < maxfile and checkimage(image) then -- if image is found and qualifies (not a sound file, non-free, etc.)
-- We opt to use skin-invert-image instead of skin-invert
files = files + 1 -- count the file, whether displaying it or not
-- in all other cases, the CSS provided in the infobox is used
if options.fileflags and options.fileflags[files] then -- if displaying this image
if pair[3] then
image = mw.ustring.gsub(image, "|%s*frameless%s*%f[|%]]", "") -- make image a thumbnail, not frameless etc.
cssclasses = pair[3]
image = mw.ustring.gsub(image, "|%s*framed?%s*%f[|%]]", "")
for _, p in pairs(cssclasses) do
if not matchany(image, "|%s*", {"thumb", "thumbnail"}, "%s*%f[|%]]") then
if parameters[p] then
image = mw.ustring.gsub(image, "(%]%]%s*)$", "|thumb%1")
cssclass = ((parameters[p] == 'skin-invert') and 'skin-invert-image' or parameters[p])
end
break
if fileargs then image = mw.ustring.gsub(image, "(%]%]%s*)$", "|" .. fileargs .. "%1") end
filetext = filetext .. image
end
end
end
end
excerpt = '[[File:' .. file ..
else -- the next token in text is not a template
(cssclass and ('|class=' .. cssclass) or '') ..
token = parseimage(text, true)
'|thumb|' .. (caption or '') .. ']]' .. excerpt
if token then -- the next token in text looks like an image
if ( onlyFreeFiles ) then
if files < maxfile and checkimage(token) then -- if more images are wanted and this is a wanted image
excerpt = Transcluder.removeNonFreeFiles( excerpt )
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
if fileargs then image = mw.ustring.gsub(image, "(%]%]%s*)$", "|" .. fileargs .. "%1") end
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 blankpos = mw.ustring.find(text, "\n%s*\n") or afterend -- position of next paragraph delimiter (or end of text)
local endpos = 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,
blankpos)
token = mw.ustring.sub(text, 1, endpos-1)
if blankpos < afterend and blankpos == endpos then -- paragraph ends with a blank line
token = token .. mw.ustring.match(text, "\n%s*\n", blankpos)
end
leadstart = leadstart or mw.ustring.len(t) + 1 -- we got a paragraph, so mark the start of the lead section
paras = paras + 1
if allparas or (options.paraflags and options.paraflags[paras]) then t = t .. token end -- add if this paragraph wanted
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
until not text or text == "" or not token or token == "" -- loop until all text parsed
text = mw.ustring.gsub(filetext .. t, "\n+$", "") -- remove trailing line feeds, so "{{Transclude text excerpt|Foo}} more" flows on one line
return text, leadstart
end
local function cleanupText(text, leadOnly)
text = mw.ustring.gsub(text, "<!%-%-.-%-%->","") -- remove HTML comments
if leadOnly then
text = mw.ustring.gsub(text, "%c%s*==.*","") -- remove first ==Heading== and everything after it
end
text = mw.ustring.gsub(text, "<noinclude>.-</noinclude>", "") -- remove noinclude bits
text = mw.ustring.gsub(text, "<%s*[Rr]ef[^>]-/%s*>", "") -- remove refs cited elsewhere
text = mw.ustring.gsub(text, "<%s*[Rr]ef.->.-<%s*/%s*ref%s*>", "") -- remove refs
text = mw.ustring.gsub(text, "<%s*[Ii]magemap.->.-<%s*/%s*imagemap%s*>", convertImagemap) -- convert imagemaps into standard images
text = mw.ustring.gsub(text, "%b{}", striptemplate) -- remove unwanted templates such as references
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
return text
end
-- Parse a ==Section== from a page
local function getsection(text, section, mainonly)
local level, content = mw.ustring.match(text .. "\n", "\n(==+)%s*" .. section .. "%s*==.-\n(.*)")
if not content then return nil end -- no such section
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
return content
end
-- Main function returns a string value: text of the lead of a page
local function main(pagenames, options)
if not pagenames or #pagenames < 1 then return err("No page names given") end
local pagename
local text
local pagecount = #pagenames
local firstpage = pagenames[1] or "(nil)" -- save for error message, as it the name will be deleted
-- read the page, or a random one if multiple pages were provided
if pagecount > 1 then math.randomseed(os.time()) end
while not text and pagecount > 0 do
local pagenum = 1
if pagecount > 1 then pagenum = math.random(pagecount) end -- pick a random title
pagename = pagenames[pagenum]
if pagename and pagename ~= "" then
pagename = mw.ustring.match(pagename, "%[%[%s*(.-)[%]|]") or pagename -- "[[Foo|Bar]]" → "Foo"
pagename = mw.ustring.gsub(pagename, "^%s+", "") -- strip leading ...
pagename = mw.ustring.gsub(pagename, "%s+$", "") -- ...and trailing white space
if pagename and pagename ~= "" then
local pn, section = mw.ustring.match(pagename, "(.-)#(.*)")
pagename = pn or pagename
text, normalisedPagename = getContent(pagename)
if not normalisedPagename then
return err("No title for page name " .. pagename)
else
pagename = normalisedPagename
end
if text and options.nostubs then
local isStub = mw.ustring.find(text, "%s*{{[^{|}]*%-[Ss]tub%s*}}")
if isStub then text = nil end
end
break
if text and section then text = getsection(text, section) end
end
end
if not text then table.remove(pagenames, pagenum) end -- this one didn't work; try another
pagecount = pagecount - 1 -- ensure that we exit the loop after at most #pagenames iterations
end
if not text then return err("Cannot read a valid page: first name is " .. firstpage) end
-- Unlike other elements, templates are filtered here
text = cleanupText(text, true)
-- because we had to search the infoboxes for files
text, leadstart = parse(text, options)
local trash
if only and ( only == 'template' or only == 'templates' ) then
-- replace the bold title or synonym near the start of the article by a wikilink to the article
trash, excerpt = Transcluder.getTemplates( excerpt, templates );
local lang = mw.language.getContentLanguage()
else -- Remove blacklisted templates
local pos = mw.ustring.find(text, "'''" .. lang:ucfirst(pagename) .. "'''", 1, true) -- look for "'''Foo''' is..." (uc) or "A '''foo''' is..." (lc)
local blacklist = config.blacklist and table.concat( config.blacklist, ',' ) or ''
or mw.ustring.find(text, "'''" .. lang:lcfirst(pagename) .. "'''", 1, true) -- plain search: special characters in pagename represent themselves
if postemplates then
if string.sub( templates, 1, 1 ) == '-' then --Unwanted templates. Append to blacklist
local len = mw.ustring.len(pagename)
blacklist = templates .. ',' .. blacklist
text = mw.ustring.sub(text, 1, pos + 2) .. "[[" .. mw.ustring.sub(text, pos + 3, pos + len + 2) .. "]]" .. mw.ustring.sub(text, pos + len + 3, -1) -- link it
else --Wanted templates. Replaces blacklist and acts as whitelist
else -- look for anything unlinked in bold, assumed to be a synonym of the title (e.g. a person's birth name)
blacklist = templates
text = mw.ustring.gsub(text, "(.-''')(.-'*)'''", function(a, b)
if mw.ustring.len(a) < 100 + (leadstart or 0) and not mw.ustring.find(b, "%[") then ---if early in article and not wikilinked
return a .. "[[" .. pagename .. "|" .. b .. "]]'''" -- replace '''Foo''' by '''[[pagename|Foo]]
else
return nil -- instruct gsub to make no change
end
else
end, 1) -- "end" here terminates the anonymous replacement function(a, b) passed to gsub
blacklist = '-' .. blacklist
end
trash, excerpt = Transcluder.getTemplates( excerpt, blacklist );
end
-- Remove extra line breaks but leave one before and after so the parser interprets lists, tables, etc. correctly
if options.more then text = text .. " '''[[" .. pagename .. "|" .. options.more .. "]]'''" end -- wikilink to article for more info
excerpt = mw.text.trim( excerpt )
return text
excerpt = string.gsub( excerpt, '\n\n\n+', '\n\n' )
end
excerpt = '\n' .. excerpt .. '\n'
-- Remove nested categories
-- Shared template invocation code for lead and random functions
excerpt = frame:preprocess( excerpt )
local function invoke(frame, func)
local categories, excerpt = Transcluder.getCategories( excerpt, options.categories )
-- args = { 1,2,... = page names, paragraphs = list e.g. "1,3-5", files = list, more = text}
local args = {} -- args[k] = frame.args[k] or frame:getParent().args[k] for all k in either (numeric or not)
for k, v in pairs(frame:getParent().args) do args[k] = v end
for k, v in pairs(frame.args) do args[k] = v end -- args from a Lua call have priority over parent args from template
errors = args["errors"] -- set the module level boolean used in local function err
local articlecount = #args -- must be 1 except with selected=Foo and Foo=Somepage
if articlecount < 1 and not (func == "selected" and args[func] and args[args[func]]) then
return err("No articles provided")
end
-- Add tracking categories
local pagenames = {}
if func == "lead"config.categories then
local contentCategory = config.categories.content
pagenames = { args[1] }
if contentCategory and mw.title.getCurrentTitle().isContentPage then
elseif func == "linked" or func == "listitem" then
excerpt = excerpt .. '[[Category:' .. contentCategory .. ']]'
-- Read named page and find its wikilinks
local page = args[1]
local text, title = getContent(page)
if not title then
return err("No title for page name " .. page)
elseif not text then
return err("No content for page name " .. page)
end
local namespaceCategory = config.categories[ mw.title.getCurrentTitle().namespace ]
if args["section"] then -- check relevant section only
if namespaceCategory then
text = getsection(text, args["section"], args["sectiononly"])
excerpt = excerpt .. '[[Category:' .. namespaceCategory .. ']]'
if not text then return err("No section " .. args["section"] .. " in page " .. page) end
end
-- replace annotated links with real links
text = mw.ustring.gsub(text, "{{%s*[Aa]nnotated[ _]link%s*|%s*(.-)%s*}}", "[[%1]]")
if func == "linked" then
for p in mw.ustring.gmatch(text, "%[%[%s*([^%]|\n]*)") do table.insert(pagenames, p) 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 p in mw.ustring.gmatch(text, "\n:*[%*#][^\n]-%[%[%s*([^%]|\n]*)") do table.insert(pagenames, p) end
end
elseif func == "random" then
-- accept any number of page names. If more than one, we'll pick one randomly
for i, p in pairs(args) do
if p and type(i) == 'number' then table.insert(pagenames, p) end
end
elseif func == "selected" then
local articlekey = args[func]
if tonumber(articlekey) then -- normalise article number into the range 1..#args
articlekey = articlekey % articlecount
if articlekey == 0 then articlekey = articlecount end
end
pagenames = { args[articlekey] }
end
-- Load the styles
local options = args -- pick up miscellaneous options: more, errors, fileargs
local styles
options.paraflags = numberflags(args["paragraphs"] or "") -- parse paragraphs, e.g. "1,3-5" → {"1","3-5"}
if config.styles then
options.fileflags = numberflags(args["files"] or "") -- parse file numbers
styles = frame:extensionTag( 'templatestyles', '', { src = config.styles } )
if options.more and options.more == "" then options.more = "Read more..." end -- more= is short for this default text
end
-- Combine and return the elements
local text = main(pagenames, options)
if inline then
return frame:preprocess(text)
return mw.text.trim( excerpt )
end
local tag = 'div'
if quote then
tag = 'blockquote'
end
excerpt = mw.html.create( 'div' ):addClass( 'excerpt' ):wikitext( excerpt )
local block = mw.html.create( tag ):addClass( 'excerpt-block' ):addClass( class )
return block:node( styles ):node( hat ):node( excerpt ):node( more )
end
-- Entry points for templatebackwards callers using #invoke:compatibility
function p.lead( frame ) return invokep.main( frame, "lead") end -- {{Transclude lead excerpt}} reads the first and only article
function p.linkedexcerpt( frame ) return invokep.main( frame, "linked") end -- {{Transclude linked excerpt}} reads a randomly selected article linked from the given page
function p.listitem(frame) return invoke(frame, "listitem") end -- {{Transclude list item excerpt}} reads a randomly selected article listed on the given page
function p.random(frame) return invoke(frame, "random") end -- {{Transclude random excerpt}} reads any article (default for invoke with one argument)
function p.selected(frame) return invoke(frame, "selected") end -- {{Transclude selected excerpt}} reads the article whose key is in the selected= parameter
-- Entry points for other Lua modules
function p.getContent(page, frame) return getContent(page, frame) end
function p.getsection(text, section) return getsection(text, section) end
function p.parse(text, options, filesOnly) return parse(text, options, filesOnly) end
function p.argimage(text) return argimage(text) end
function p.checkimage(image) return checkimage(image) end
function p.parseimage(text, start) return parseimage(text, start) end
function p.cleanupText(text, leadOnly) return cleanupText(text, leadOnly) end
function p.main(pagenames, options) return main(pagenames, options) end
function p.numberflags(str) return numberflags(str) end
return p
|