Module:Excerpt: Difference between revisions

From WikiMD's Wellness Encyclopedia

m 1 revision imported
 
m 1 revision imported
 
(One intermediate revision by one other user not shown)
Line 1: Line 1:
local p = {}
-- Module:Excerpt implements the Excerpt template
local mRedirect = require('Module:Redirect')
-- Documentation and master version: https://en.wikipedia.org/wiki/Module:Excerpt
-- Authors: User:Sophivorus, User:Certes, User:Aidan9382 & others
-- License: CC-BY-SA-3.0


-- Get a redirect target (or nil if not a redirect) without using the expensive title object property .isRedirect
local Transcluder = require( 'Module:Transcluder' )
local function getRedirectTarget(titleObject)
local content = titleObject:getContent()
if not content then return nil end
return mRedirect.getTargetFromText(content)
end


local errors
local yesno = require( 'Module:Yesno' )
-- Return blank text, or an error message if requested
local function err(text)
if errors then error(text, 2) end
return ""
end


-- In text, match pre..list[1]..post or pre..list[2]..post or ...
local ok, config = pcall( require, 'Module:Excerpt/config' )
local function matchany(text, pre, list, post)
if not ok then config = {} end
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


-- Initialise this big table once to save time
local p = {}
local unwantedTemplates = {"[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?",
"[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"
}


-- Help gsub to remove unwanted templates and pseudo-templates such as #tag:ref and DEFAULTSORT
-- Helper function to get arguments
local function striptemplate(t)
local args
-- If template is unwanted then return "" (gsub will replace by nothing), else return nil (gsub will keep existing string)
local function getArg( key, default )
if matchany(t, "^{{%s*", unwantedTemplates, "%s*%f[|}]") then return "" end
local value = args[ key ]
 
if value and mw.text.trim( value ) ~= '' then
-- If template is wanted but produces an unwanted reference then return the string with |shortref or |ref removed
return value
local noref = mw.ustring.gsub(t, "|%s*shortref%s*%f[|}]", "")
end
noref = mw.ustring.gsub(noref, "|%s*ref%s*%f[|}]", "")
return default
 
-- 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, 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
 
local redir = getRedirectTarget(title)
if redir then title = mw.title.new(redir) end
 
return title:getContent(), redir or title.prefixedText
end
end


-- Check image for suitability
-- Helper function to handle errors
local function checkimage(image)
local function getError( message, value )
local page = matchany(image, "", {"[Ff]ile", "[Ii]mage"}, "%s*:[^|%]]*") -- match File:(name) or Image:(name)
if type( message ) == 'string' then
if not page then return false end
message = Transcluder.getError( message, value )
 
-- 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
end
 
if config.categories and config.categories.errors and mw.title.getCurrentTitle().isContentPage then
local desc, rtitle = getContent(page) -- get file description and title after following any redirect
message:node( '[[Category:' .. config.categories.errors .. ']]' )
if desc and desc ~= "" then -- found description on local wiki
if mw.ustring.match(desc, "[Nn]on%-free") then return false end
desc = mw.ustring.gsub(desc, "%b{}", striptemplate) -- remove DEFAULTSORT etc. to avoid side effects of frame:preprocess
elseif not rtitle then
return false
else
-- try commons
desc = "{{" .. rtitle .. "}}"
end
end
frame = frame or mw.getCurrentFrame()
return message
desc = frame:preprocess(desc)
 
return ( desc and desc ~= "" and not mw.ustring.match(desc, "[Nn]on%-free") ) and true or false -- hide non-free image
end
end


-- Attempt to parse [[File:...]] or [[Image:...]], either anywhere (start=false) or at the start only (start=true)
-- Helper function to get localized messages
local function parseimage(text, start)
local function getMessage( key )
local startre = ""
local ok, TNT = pcall( require, 'Module:TNT' )
if start then startre = "^" end -- a true flag restricts search to start of string
if not ok then return key end
local image = matchany(text, startre .. "%[%[%s*", {"[Ff]ile", "[Ii]mage"}, "%s*:.*") -- [[File: or [[Image: ...
return TNT.format( 'I18n/Module:Excerpt.tab', key )
if image then
image = mw.ustring.match(image, "%b[]%s*") -- matching [[...]] to handle wikilinks nested in caption
end
return image
end
end


-- Parse a caption, which ends at a | (end of parameter) or } (end of infobox) but may contain nested [..] and {..}
-- Main entry point for templates
local function parsecaption(caption)
function p.main( frame )
if not caption then return nil end
args = Transcluder.parseArgs( frame )
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


-- Attempt to construct a [[File:...]] block from {{infobox ... |image= ...}}
-- Make sure the requested page exists
local function argimage(text)
local page = getArg( 1 )
local token = nil
if not page or page == '{{{1}}}' then return getError( 'no-page' ) end
local hasNamedArgs = mw.ustring.find(text, "|") and mw.ustring.find(text, "=")
local title = mw.title.new(page)
if not hasNamedArgs then return nil end -- filter out any template that obviously doesn't contain an image
if not title then return getError( 'invalid-title', page ) end
if title.isRedirect then title = title.redirectTarget end
if not title.exists then return getError( 'page-not-found', page ) end
page = title.prefixedText


-- ensure image map is captured
-- 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


-- find all images
-- Build the hatnote
local hasImages = false
if hat and not inline then
local images = {}
if this then
local capture_from = 1
hat = this
while capture_from < mw.ustring.len(text) do
elseif quote then
local argname, position, image = mw.ustring.match(text, "|%s*([^=|]-[Ii][Mm][Aa][Gg][Ee][^=|]-)%s*=%s*()(.*)", capture_from)
hat = getMessage( 'this' )
if image then -- ImageCaption=, image_size=, image_upright=, etc. do not introduce an image
elseif only then
local lcArgname = mw.ustring.lower(argname)
hat = getMessage( only )
if mw.ustring.find(lcArgname, "caption")
else
or mw.ustring.find(lcArgname, "size")
hat = getMessage( 'section' )
or mw.ustring.find(lcArgname, "upright") then
image = nil
end
end
end
if image then
hat = hat .. ' ' .. getMessage( 'excerpt' ) .. ' '
hasImages = true
if section then
images[position] = image
hat = hat .. '[[:' .. page .. '#' .. mw.uri.anchorEncode( section ) .. '|' .. displaytitle
capture_from = position
.. ' § ' .. mw.ustring.gsub( section, '%[%[([^]|]+)|?[^]]*%]%]', '%1' ) .. ']].' -- remove nested links
else
else
capture_from = mw.ustring.len(text)
hat = hat .. '[[:' .. page .. '|' .. displaytitle .. ']].'
end
end
end
if edit then
capture_from = 1
hat = hat .. '<span class="mw-editsection-like plainlinks"><span class="mw-editsection-bracket">[</span>['
while capture_from < mw.ustring.len(text) do
hat = hat .. title:fullUrl( 'action=edit' ) .. ' ' .. mw.message.new( 'editsection' ):plain()
local position, image = mw.ustring.match(text, "|%s*[^=|]-[Pp][Hh][Oo][Tt][Oo][^=|]-%s*=%s*()(.*)", capture_from)
hat = hat .. ']<span class="mw-editsection-bracket">]</span></span>'
if image then
hasImages = true
images[position] = image
capture_from = position
else
capture_from = mw.ustring.len(text)
end
end
end
if config.hat then
capture_from = 1
hat = config.hat .. hat .. '}}'
while capture_from < mw.ustring.len(text) do
hat = frame:preprocess( hat )
local position, image = mw.ustring.match(text, "|%s*[^=|{}]-%s*=%s*()%[?%[?([^|{}]*%.%a%a%a%a?)%s*%f[|}]", capture_from)
if image then
hasImages = true
if not images[position] then
images[position] = image
end
capture_from = position
else
else
capture_from = mw.ustring.len(text)
hat = mw.html.create( 'div' ):addClass( 'dablink excerpt-hat' ):wikitext( hat )
end
end
else
hat = nil
end
end


if not hasImages then return nil end
-- Build the "Read more" link
 
if more and not inline then
-- find all captions
more = "'''[[" .. page .. '#' .. ( section or '' ) .. "|" .. getMessage( 'more' ) .. "]]'''"
local captions = {}
more = mw.html.create( 'div' ):addClass( 'noprint excerpt-more' ):wikitext( more )
capture_from = 1
else
while capture_from < mw.ustring.len(text) do
more = nil
local position, caption = mw.ustring.match(text, "|%s*[^=|]*[Cc][Aa][Pp][Tt][Ii][Oo][Nn][^=|]*%s*=%s*()([^\n]+)", capture_from)
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
capture_from = position
else
capture_from = mw.ustring.len(text)
end
end
end


-- find all alt text
-- Build the options for Module:Transcluder out of the template parameters and the desired defaults
local altTexts = {}
local options = {
for position, altText in mw.ustring.gmatch(text, "|%s*[Aa][Ll][Tt]%s*=%s*()([^\n]*)") do
files = files,
if altText then
lists = lists,
tables = tables,
paragraphs = paragraphs,
sections = subsections,
categories = 0,
references = references,
only = only and mw.text.trim( only, 's' ) .. 's',
noLinks = noLinks,
noBold = noBold,
noSelfLinks = true,
noNonFreeFiles = onlyFreeFiles,
noBehaviorSwitches = true,
fixReferences = true,
linkBold = true,
}


-- altText is terminated by }} or |, but first skip any matched [[...]] and {{...}}
-- Get the excerpt itself
local lookfrom = math.max( -- find position after whichever comes last: start of string, end of last ]] or end of last }}
local title = page .. '#' .. ( section or '' )
mw.ustring.match(altText, ".*{%b{}}()") or 1, -- if multiple {{...}}, .* consumes all but one, leaving the last for %b
local ok, excerpt = pcall( Transcluder.get, title, options )
mw.ustring.match(altText, ".*%[%b[]%]()") or 1)
if not ok then return getError( excerpt ) end
if mw.text.trim( excerpt ) == '' and not only then
if section then return getError( 'section-empty', section ) else return getError( 'lead-empty' ) end
end


local len = mw.ustring.len(altText)
-- Fix birth and death dates, but only in the first paragraph
local aftertext = math.min( -- find position after whichever comes first: end of string, }} or |
if briefDates then
mw.ustring.match(altText, "()}}", lookfrom) or len+1,
local startpos = 1 -- skip initial templates
mw.ustring.match(altText, "()|", lookfrom) or len+1)
local s
altText = mw.ustring.sub(altText, 1, aftertext-1) -- chop off |... or }}... which is not part of [[...]] or {{...}}
local e = 0
 
repeat
altText = mw.text.trim(altText)
startpos = e + 1
local altTextStart = mw.ustring.sub(altText, 1, 1)
s, e = mw.ustring.find( excerpt, "%s*%b{}%s*", startpos )
if altTextStart == '|' or altTextStart == '}' then altText = nil end
until not s or s > startpos
end
s, e = mw.ustring.find( excerpt, "%b()", startpos ) -- get (...), which may be (year–year)
if altText then
if s and s < startpos + 100 then -- look only near the start
-- find nearest image, and use same index for altTexts table
local year1, conjunction, year2 = mw.ustring.match( mw.ustring.sub( excerpt, s, e ), '(%d%d%d+)(.-)(%d%d%d+)' )
local i = position
if year1 and year2 and (mw.ustring.match( conjunction, '[%-–—]' ) or mw.ustring.match( conjunction, '{{%s*[sS]nd%s*}}' )) then
while i > 0 and not images[i] do
local y1 = tonumber(year1)
i = i - 1
local y2 = tonumber(year2)
if images[i] then
if y2 > y1 and y2 < y1 + 125 and y1 <= tonumber( os.date( "%Y" )) then
if not altTexts[i] then
excerpt = mw.ustring.sub( excerpt, 1, s ) .. year1 .. "–" .. year2 .. mw.ustring.sub( excerpt, e )
altTexts[i] = altText
end
end
end
end
end
Line 281: Line 167:
end
end


-- find all image sizes
-- If no file was found, try to get one from the infobox
local imageSizes = {}
local fileNamespaces = Transcluder.getNamespaces( 'File' )
for position, imageSizeMatch in mw.ustring.gmatch(text, "|%s*[Ii][Mm][Aa][Gg][Ee][ _]?[Ss][Ii][Zz][Ee]%s*=%s*()([^}|\n]*)") do
if ( ( only == 'file' or only == 'files' ) or ( not only and ( files ~= '0' or not files ) ) ) and -- caller asked for files
local imageSize = mw.ustring.match(imageSizeMatch, "=%s*([^}|\n]*)")
not Transcluder.matchAny( excerpt, '%[%[', fileNamespaces, ':' ) and -- and there are no files in Transcluder's output
if imageSize then
config.captions -- and we have the config option required to try finding files in templates
imageSize = mw.text.trim(imageSize )
then
local imageSizeStart = mw.ustring.sub(imageSize, 1, 1)
-- We cannot distinguish the infobox from the other templates so we search them all
if imageSizeStart == '|' or imageSizeStart == '}' then imageSize = nil end
local infobox = Transcluder.getTemplates( excerpt );
end
infobox = table.concat( infobox )
if imageSize then
local parameters = Transcluder.getParameters( infobox )
-- find nearest image, and use same index for imageSizes table
local file, captions, caption
local i = position
for _, pair in pairs( config.captions ) do
while i > 0 and not images[i] do
file = pair[1]
i = i - 1
file = parameters[file]
if images[i] then
if file and Transcluder.matchAny( file, '^.*%.', { '[Jj][Pp][Ee]?[Gg]', '[Pp][Nn][Gg]', '[Gg][Ii][Ff]', '[Ss][Vv][Gg]' }, '.*' ) then
if not imageSizes[i] then
file = mw.ustring.match( file, '%[?%[?.-:([^{|]+)%]?%]?' ) or file -- [[File:Example.jpg{{!}}upright=1.5]] to Example.jpg
imageSizes[i] = imageSize
captions = pair[2]
end
for _, p in pairs( captions ) do
if parameters[ p ] then caption = parameters[ p ] break end
end
excerpt = '[[File:' .. file .. '|thumb|' .. ( caption or '' ) .. ']]' .. excerpt
if ( onlyFreeFiles ) then
excerpt = Transcluder.removeNonFreeFiles( excerpt )
end
end
break
end
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
-- Unlike other elements, templates are filtered here
local keys = {}
-- because we had to search the infoboxes for files
for key, val in pairs(images) do
local trash
table.insert(keys, key)
if only and ( only == 'template' or only == 'templates' ) then
end
trash, excerpt = Transcluder.getTemplates( excerpt, templates );
table.sort(keys)
else -- Remove blacklisted templates
 
local blacklist = config.blacklist and table.concat( config.blacklist, ',' ) or ''
-- add in relevant optional parameters for each image: caption, alt text and image size
if templates then
local imageTokens = {}
if string.sub( templates, 1, 1 ) == '-' then --Unwanted templates. Append to blacklist
for _, index in ipairs(keys) do
blacklist = templates .. ',' .. blacklist
local image = images[index]
else --Wanted templates. Replaces blacklist and acts as whitelist
local token = parseimage(image, true) -- look for image=[[File:...]] etc.
blacklist = templates
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
end
token = token .. image
else
local caption = captions[index]
blacklist = '-' .. blacklist
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
return "" -- remove entire block if image can't be extracted
end
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
 
-- 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`
--  @param filesOnly : If set, only return the files and not the prose
local function parse(text, options, filesOnly)
local allparas = 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 allparas = false end -- if any para specifically requested, don't keep all
end
end
end
trash, excerpt = Transcluder.getTemplates( excerpt, blacklist );
if filesOnly then
allparas = false
options.paraflags = {}
end
end


local maxfile = 0 -- for efficiency, stop checking images after this many have been found
-- Remove extra line breaks but leave one before and after so the parser interprets lists, tables, etc. correctly
if options.fileflags then
excerpt = mw.text.trim( excerpt )
if type(options.fileflags) ~= "table" then options.fileflags = numberflags(options.fileflags) end
excerpt = string.gsub( excerpt, '\n\n\n+', '\n\n' )
for k, v in pairs(options.fileflags) do
excerpt = '\n' .. excerpt .. '\n'
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?
-- Remove nested categories
local t = "" -- the stripped down output text
excerpt = frame:preprocess( excerpt )
local filetext = "" -- output text with concatenated [[File:Foo|...]]\n entries
local categories, excerpt = Transcluder.getCategories( excerpt, options.categories )
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 tracking categories
repeat -- loop around parsing a template, image or paragraph
if config.categories then
local token = mw.ustring.match(text, "^%b{}%s*") or false -- {{Template}}
local contentCategory = config.categories.content
if not leadstart and not token then token = mw.ustring.match(text, "^%b<>%s*%b{}%s*") end -- allow <tag>{{template}} before lead has started
if contentCategory and mw.title.getCurrentTitle().isContentPage then
local line = mw.ustring.match(text, "[^\n]*")
excerpt = excerpt .. '[[Category:' .. contentCategory .. ']]'
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
end
 
local namespaceCategory = config.categories[ mw.title.getCurrentTitle().namespace ]
if token then -- found a template which is not the prefix to a line of text
if namespaceCategory then
if leadstart then -- lead has already started, so keep the template within the text, unless it's a whole line (navbox etc.)
excerpt = excerpt .. '[[Category:' .. namespaceCategory .. ']]'
if not filesOnly and not startLine then t = t .. token end
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 = mw.ustring.gsub(image, "|%s*frameless%s*%f[|%]]", "") -- make image a thumbnail, not frameless etc.
image = mw.ustring.gsub(image, "|%s*framed?%s*%f[|%]]", "")
if not matchany(image, "|%s*", {"thumb", "thumbnail"}, "%s*%f[|%]]") then
image = mw.ustring.gsub(image, "(%]%]%s*)$", "|thumb%1")
end
if fileargs then image = mw.ustring.gsub(image, "(%]%]%s*)$", "|" .. fileargs .. "%1") end
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
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
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 allparas 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, leadOnly)
text = mw.ustring.gsub(text, "<!%-%-.-%-%->","") -- remove HTML comments
if leadOnly then
text = mw.ustring.gsub(text, "\n==.*","") -- remove first ==Heading== and everything after it
text = mw.ustring.gsub(text, "^==.*","") -- ...even if it's the start of the article (blank lead)
end
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
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, "<%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, "%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
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 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
 
-- 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
 
-- 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
if not section then
section = mw.ustring.match(pagename, ".-#(.*)") -- parse redirect to Page#Section
end
if text and section then text = getsection(text, section) end
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
end
if not text then return err("Cannot read a valid page: first name is " .. firstpage) end
text = cleanupText(text, true)
local filetext
filetext, text = parse(text, options)


-- replace the bold title or synonym near the start of the article by a wikilink to the article
-- Load the styles
local lang = mw.language.getContentLanguage()
local styles
local pos = mw.ustring.find(text, "'''" .. lang:ucfirst(pagename) .. "'''", 1, true) -- look for "'''Foo''' is..." (uc) or "A '''foo''' is..." (lc)
if config.styles then
or mw.ustring.find(text, "'''" .. lang:lcfirst(pagename) .. "'''", 1, true) -- plain search: special characters in pagename represent themselves
styles = frame:extensionTag( 'templatestyles', '', { src = config.styles } )
if pos then
local len = mw.ustring.len(pagename)
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 -- 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 a < 100 and not mw.ustring.find(b, "%[") then ---if early in article and not wikilinked
return "'''[[" .. pagename .. "|" .. b .. "]]'''" -- replace '''Foo''' by '''[[pagename|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
end


text = filetext .. text
-- Combine and return the elements
 
if inline then
-- Seek and destroy unterminated templates and wikilinks
return mw.text.trim( excerpt )
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
repeat -- do similar for [[wikilink]]s
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{ → {{, ]E]E → ]], etc.
 
-- Ensure div tags match
text = fixtags(text, "div")
 
if options.more then text = text .. " '''[[" .. pagename .. "|" .. options.more .. "]]'''" end -- wikilink to article for more info
return text
end
 
-- Shared template invocation code for lead and random functions
local function invoke(frame, func)
-- 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
end
 
local tag = 'div'
local pagenames = {}
if quote then
if func == "lead" then
tag = 'blockquote'
pagenames = { args[1] }
elseif func == "linked" or func == "listitem" then
-- 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
if args["section"] then -- check relevant section only
text = getsection(text, args["section"], args["sectiononly"])
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
end
 
excerpt = mw.html.create( 'div' ):addClass( 'excerpt' ):wikitext( excerpt )
local options = args -- pick up miscellaneous options: more, errors, fileargs
local block = mw.html.create( tag ):addClass( 'excerpt-block' ):addClass( class )
options.paraflags = numberflags(args["paragraphs"] or "") -- parse paragraphs, e.g. "1,3-5" → {"1","3-5"}
return block:node( styles ):node( hat ):node( excerpt ):node( more )
options.fileflags = numberflags(args["files"] or "") -- parse file numbers
if options.more and options.more == "" then options.more = "Read more..." end -- more= is short for this default text
 
local text = main(pagenames, options)
return frame:preprocess(text)
end
end


-- Entry points for template callers using #invoke:
-- Entry points for backwards compatibility
function p.lead(frame) return invoke(frame, "lead") end -- {{Transclude lead excerpt}} reads the first and only article
function p.lead( frame ) return p.main( frame ) end
function p.linked(frame) return invoke(frame, "linked") end -- {{Transclude linked excerpt}} reads a randomly selected article linked from the given page
function p.excerpt( frame ) return p.main( frame ) end
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
return p

Latest revision as of 18:52, 16 December 2024

Documentation for this module may be created at Module:Excerpt/doc

-- Module:Excerpt implements the Excerpt template
-- Documentation and master version: https://en.wikipedia.org/wiki/Module:Excerpt
-- Authors: User:Sophivorus, User:Certes, User:Aidan9382 & others
-- License: CC-BY-SA-3.0

local Transcluder = require( 'Module:Transcluder' )

local yesno = require( 'Module:Yesno' )

local ok, config = pcall( require, 'Module:Excerpt/config' )
if not ok then config = {} end

local p = {}

-- Helper function to get arguments
local args
local function getArg( key, default )
	local value = args[ key ]
	if value and mw.text.trim( value ) ~= '' then
		return value
	end
	return default
end

-- Helper function to handle errors
local function getError( message, value )
	if type( message ) == 'string' then
		message = Transcluder.getError( message, value )
	end
	if config.categories and config.categories.errors and mw.title.getCurrentTitle().isContentPage then
		message:node( '[[Category:' .. config.categories.errors .. ']]' )
	end
	return message
end

-- Helper function to get localized messages
local function getMessage( key )
	local ok, TNT = pcall( require, 'Module:TNT' )
	if not ok then return key end
	return TNT.format( 'I18n/Module:Excerpt.tab', key )
end

-- Main entry point for templates
function p.main( frame )
	args = Transcluder.parseArgs( frame )

	-- Make sure the requested page exists
	local page = getArg( 1 )
	if not page or page == '{{{1}}}' then return getError( 'no-page' ) end
	local title = mw.title.new(page)
	if not title then return getError( 'invalid-title', page ) end
	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
	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

	-- Build the hatnote
	if hat and not inline then
		if this then
			hat = this
		elseif quote then
			hat = getMessage( 'this' )
		elseif only then
			hat = getMessage( only )
		else
			hat = getMessage( 'section' )
		end
		hat = hat .. ' ' .. getMessage( 'excerpt' ) .. ' '
		if section then
			hat = hat .. '[[:' .. page .. '#' .. mw.uri.anchorEncode( section ) .. '|' .. displaytitle
				.. ' § ' .. mw.ustring.gsub( section, '%[%[([^]|]+)|?[^]]*%]%]', '%1' ) .. ']].' -- remove nested links
		else
			hat = hat .. '[[:' .. page .. '|' .. displaytitle .. ']].'
		end
		if edit then
			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()
			hat = hat .. ']<span class="mw-editsection-bracket">]</span></span>'
		end
		if config.hat then
			hat = config.hat .. hat .. '}}'
			hat = frame:preprocess( hat )
		else
			hat = mw.html.create( 'div' ):addClass( 'dablink excerpt-hat' ):wikitext( hat )
		end
	else
		hat = nil
	end

	-- Build the "Read more" link
	if more and not inline then
		more = "'''[[" .. page .. '#' .. ( section or '' ) .. "|" .. getMessage( 'more' ) .. "]]'''"
		more = mw.html.create( 'div' ):addClass( 'noprint excerpt-more' ):wikitext( more )
	else
		more = nil
	end

	-- Build the options for Module:Transcluder out of the template parameters and the desired defaults
	local options = {
		files = files,
		lists = lists,
		tables = tables,
		paragraphs = paragraphs,
		sections = subsections,
		categories = 0,
		references = references,
		only = only and mw.text.trim( only, 's' ) .. 's',
		noLinks = noLinks,
		noBold = noBold,
		noSelfLinks = true,
		noNonFreeFiles = onlyFreeFiles,
		noBehaviorSwitches = true,
		fixReferences = true,
		linkBold = true,
	}

	-- Get the excerpt itself
	local title = page .. '#' .. ( section or '' )
	local ok, excerpt = pcall( Transcluder.get, title, options )
	if not ok then return getError( excerpt ) end
	if mw.text.trim( excerpt ) == '' and not only then
		if section then return getError( 'section-empty', section ) else return getError( 'lead-empty' ) end
	end

	-- Fix birth and death dates, but only in the first paragraph
	if briefDates then
		local startpos = 1 -- skip initial templates
		local s
		local e = 0
		repeat
			startpos = e + 1
			s, e = mw.ustring.find( excerpt, "%s*%b{}%s*", startpos )
		until not s or s > startpos
		s, e = mw.ustring.find( excerpt, "%b()", startpos ) -- get (...), which may be (year–year)
		if s and s < startpos + 100 then -- look only near the start
			local year1, conjunction, year2 = mw.ustring.match( mw.ustring.sub( excerpt, s, e ), '(%d%d%d+)(.-)(%d%d%d+)' )
			if year1 and year2 and (mw.ustring.match( conjunction, '[%-–—]' ) or mw.ustring.match( conjunction, '{{%s*[sS]nd%s*}}' )) then
				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 no file was found, try to get one from the infobox
	local fileNamespaces = Transcluder.getNamespaces( 'File' )
	if ( ( only == 'file' or only == 'files' ) or ( not only and ( files ~= '0' or not files ) ) ) and -- caller asked for files
		not Transcluder.matchAny( excerpt, '%[%[', fileNamespaces, ':' ) and -- and there are no files in Transcluder's output
		config.captions -- and we have the config option required to try finding files in templates
	then
		-- We cannot distinguish the infobox from the other templates so we search them all
		local infobox = Transcluder.getTemplates( excerpt );
		infobox = table.concat( infobox )
		local parameters = Transcluder.getParameters( infobox )
		local file, captions, caption
		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
				excerpt = '[[File:' .. file .. '|thumb|' .. ( caption or '' ) .. ']]' .. excerpt
				if ( onlyFreeFiles ) then
					excerpt = Transcluder.removeNonFreeFiles( excerpt )
				end
				break
			end
		end
	end

	-- Unlike other elements, templates are filtered here
	-- because we had to search the infoboxes for files
	local trash
	if only and ( only == 'template' or only == 'templates' ) then
		trash, excerpt = Transcluder.getTemplates( excerpt, templates );
	else -- Remove blacklisted templates
		local blacklist = config.blacklist and table.concat( config.blacklist, ',' ) or ''
		if templates then
			if string.sub( templates, 1, 1 ) == '-' then --Unwanted templates. Append to blacklist
				blacklist = templates .. ',' .. blacklist
			else --Wanted templates. Replaces blacklist and acts as whitelist
				blacklist = templates
			end
		else
			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
	excerpt = mw.text.trim( excerpt )
	excerpt = string.gsub( excerpt, '\n\n\n+', '\n\n' )
	excerpt = '\n' .. excerpt .. '\n'

	-- Remove nested categories
	excerpt = frame:preprocess( excerpt )
	local categories, excerpt = Transcluder.getCategories( excerpt, options.categories )

	-- Add tracking categories
	if config.categories then
		local contentCategory = config.categories.content
		if contentCategory and mw.title.getCurrentTitle().isContentPage then
			excerpt = excerpt .. '[[Category:' .. contentCategory .. ']]'
		end
		local namespaceCategory = config.categories[ mw.title.getCurrentTitle().namespace ]
		if namespaceCategory then
			excerpt = excerpt .. '[[Category:' .. namespaceCategory .. ']]'
		end
	end

	-- Load the styles
	local styles
	if config.styles then
		styles = frame:extensionTag( 'templatestyles', '', { src = config.styles } )
	end

	-- Combine and return the elements
	if inline then
		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 backwards compatibility
function p.lead( frame ) return p.main( frame ) end
function p.excerpt( frame ) return p.main( frame ) end

return p