Module:Excerpt: Difference between revisions

Content deleted Content added
Thumb sized images
Use infobox CSS class if it exists (mainly for dark mode compatibility)
 
(157 intermediate revisions by 10 users not shown)
Line 1:
-- 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 = {}
local mRedirect = require('Module:Redirect')
 
-- Helper function to get arguments
local errors
local args
-- Return blank text, or an error message if requested
local function errgetArg(text key, default )
local value = args[ key ]
if errors then error(text, 2) end
if value and mw.text.trim( value ) ~= '' then
return ""
return value
end
return default
end
 
-- Helper function to handle errors
-- Attempt to parse [[File:...]] or [[Image:...]], either anywhere or at the start
local function parseimagegetError(text message, startvalue )
if type( message ) == 'string' then
local startre = ""
message = Transcluder.getError( message, value )
if start then startre = "^" end -- a true flag restricts search to start of string
end
local image = mw.ustring.match(text, startre .. "%[%[%s*[Ff]ile%s*:.*") -- [[File: ...
if config.categories and config.categories.errors and mw.title.getCurrentTitle().isContentPage then
or mw.ustring.match(text, startre .. "%[%[%s*[Ii]mage%s*:.*") -- or [[Image: ...
message:node( '[[Category:' .. config.categories.errors .. ']]' )
if image then
image = mw.ustring.match(image, "%b[]%s*") -- match [[...]] to handle nesting
end
return imagemessage
end
 
-- Helper function to get localized messages
-- Attempt to construct a [[File:...]] block from {{infobox ... |image= ...}}
local function argimagegetMessage(text key )
local tokenok, TNT = nilpcall( require, 'Module:TNT' )
if not ok then return key end
if mw.ustring.match(text, "%{%{%s*[Ii]nfobox") then
return TNT.format( 'I18n/Module:Excerpt.tab', key )
local image = mw.ustring.match(text, "|%s*image%s*=%s*([^%}|]*)") -- parse image= argument...
or mw.ustring.match(text, "|%s*Cover%s*=%s*([^%}|]-)") -- or Cover= from Infobox album
if image then -- add in relevant parameters: caption, alt text and image size
token = "[[File:" .. image
local caption = mw.ustring.match(text, "|%s*[Cc]aption%s*=%s*([^%}|]*)")
if caption then token = token .. "|caption=" .. caption end
local alt = mw.ustring.match(text, "|%s*alt%s*=%s*([^%}|]*)")
if alt then token = token .. "|alt=" .. alt end
local image_size = mw.ustring.match(text, "|%s*image_size%s*=%s*([^%}|]*)")
if image_size then token = token .. "|" .. image_size end
token = mw.ustring.gsub(token, "\n","") .. "|thumb]]\n"
end
end
 
return token
end
 
-- EntryMain entry point for Lua callerstemplates
function p.main( frame )
-- Returns a string value: text of the lead of a page
args = Transcluder.parseArgs( frame )
function p._lead(pagenames, options)
errors = options.errors
 
-- Make sure the requested page exists
if not pagenames or #pagenames < 1 then return err("No page names given") end
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 pagename
local section = getArg( 2, mw.ustring.match( getArg( 1 ), '[^#]+#(.+)' ) )
local text
local pagecounthat = #pagenamesyesno( getArg( 'hat', true ) )
local edit = yesno( getArg( 'edit', true ) )
local firstpage = pagenames[1] or "(nil)" -- save for error message, as it the name will be deleted
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
-- read the page, or a random one if multiple pages were provided
if hat and not inline then
if pagecount > 1 then math.randomseed(os.time()) end
if this then
while not text and pagecount > 0 do
local pagenum hat = 1this
elseif quote then
if pagecount > 1 then pagenum = math.random(pagecount) end -- pick a random title
hat = getMessage( 'this' )
pagename = pagenames[pagenum]
elseif only then
if pagename and pagename ~= "" then
hat = getMessage( only )
pagename = mw.ustring.match(pagename, "%[%[%s*(.-)[]|#]") or pagename -- "[[Foo|Bar]]" → "Foo"
else
pagename = mw.ustring.match(pagename, "%S.*%S") -- strip leading and trailing white space
hat = getMessage( 'section' )
 
if pagename and pagename ~= "" then
local title = mw.title.new(pagename) -- Find the lead section of the named page
if not title then return err("No title for page name " .. pagename) end
local redir = mRedirect.getTarget(title)
if redir then title = mw.title.new(redir) end
 
text = title:getContent()
end
end
hat = hat .. ' ' .. getMessage( 'excerpt' ) .. ' '
if not text then table.remove(pagenames, pagenum) end -- this one didn't work; try another
if section then
pagecount = pagecount - 1 -- ensure that we exit the loop eventually
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
if not text then return err("Cannot read a valid page: first name is " .. firstpage) end
 
-- Build the "Read more" link
text = mw.ustring.gsub(text, "<!%-%-.-%-%->","") -- remove HTML comments
if more and not inline then
text = mw.ustring.gsub(text, "%c%s*==.*","") -- remove first heading and everything after it
more = "'''[[" .. page .. '#' .. ( section or '' ) .. "|" .. getMessage( 'more' ) .. "]]'''"
text = mw.ustring.gsub(text, "<noinclude>.-</noinclude>", "") -- remove noinclude bits
more = mw.html.create( 'div' ):addClass( 'noprint excerpt-more' ):wikitext( more )
text = mw.ustring.gsub(text, "<%s*ref[^>]-/%s*>", "") -- remove refs cited elsewhere
else
text = mw.ustring.gsub(text, "<%s*ref.->.-<%s*/%s*ref%s*>", "") -- remove refs
more = nil
text = mw.ustring.gsub(text, "<%s*imagemap.->.-<%s*/%s*imagemap%s*>", "") -- remove imagemaps
for _, t in pairs {"[Ee]fn", "[Ee]fn-la", "[Ee]l[mn]", "[Rr]p?", "[Ss]fn[bp]", "[Ss]f[bn]", "NoteTag", "#[Tt]ag:%s*[Rr]ef",
"[CcDd]n", "Citation needed", "Disambiguation needed"} do
text = mw.ustring.gsub(text, "{{%s*" .. t .. "%s*|.-}}", "") -- remove ref and footnote templates
end
text = mw.ustring.gsub(text, "\n%s*{{%s*[Tt][Oo][Cc].-}}", "\n") -- remove most common tables of contents
 
-- Build the options for Module:Transcluder out of the template parameters and the desired defaults
local allparas = true -- keep all paragraphs?
iflocal options.paraflags then= {
files = files,
for _, v in pairs(options.paraflags) do
lists = lists,
if v then allparas = false end -- if any para specifically requested, don't keep all
tables = tables,
end
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
 
-- aFix basicbirth parserand todeath trimdates, downbut only in the leadfirst paragraph
if briefDates then
local inlead = false -- have we found some text yet?
local tstartpos = ""1 -- theskip strippedinitial down output texttemplates
local s
local files = 0 -- how many [[Image: or [[File: so far
local parase = 0 -- how many paragraphs so far
repeat
 
startpos = e + 1
text = mw.ustring.gsub(text,"^%s*","") -- remove initial white space
s, e = mw.ustring.find( excerpt, "%s*%b{}%s*", startpos )
repeat -- loop around parsing a template, image or paragraph
until not s or s > startpos
local token = mw.ustring.match(text, "^%b{}%s*") or false -- {{Template}}
s, e = mw.ustring.find( excerpt, "%b()", startpos ) -- get (...), which may be (year–year)
if token then
if inleads thenand --s keep< commentsstartpos and+ 100 then -- templateslook only withinnear textthe bodystart
local year1, conjunction, year2 = mw.ustring.match( mw.ustring.sub( excerpt, s, e ), '(%d%d%d+)(.-)(%d%d%d+)' )
t = t .. token
if year1 and year2 and (mw.ustring.match( conjunction, '[%-–—]' ) or mw.ustring.match( conjunction, '{{%s*[sS]nd%s*}}' )) then
else -- look for [[File:... embedded in an infobox etc. in the preamble
local imagey1 = parseimagetonumber(token, false) or argimage(tokenyear1)
local y2 = tonumber(year2)
if image then -- keep comments and templates only within text body
if y2 > y1 and y2 < y1 + 125 and y1 <= tonumber( os.date( "%Y" )) then
image = mw.ustring.gsub(image, "|%s*frameless", "|frame") -- excerpt needs a frame to flow around, even if infobox doesn't
excerpt = mw.ustring.sub( excerpt, 1, s ) .. year1 .. "–" .. year2 .. mw.ustring.sub( excerpt, e )
files = files + 1
if options.fileflags and options.fileflags[files] then t = t .. image end
end
end
elseend
end
token = parseimage(text, true)
 
if token then
-- If no file was found, try to get one from the infobox
files = files + 1
local fileNamespaces = Transcluder.getNamespaces( 'File' )
if options.fileflags and options.fileflags[files] then t = t .. token end
if ( ( only == 'file' or only == 'files' ) or ( not only and ( files ~= '0' or not files ) ) ) and -- caller asked for files
else -- got a paragraph, which ends at a file, image, blank line or end of text
not Transcluder.matchAny( excerpt, '%[%[', fileNamespaces, ':' ) and -- and there are no files in Transcluder's output
local afterend = mw.ustring.len(text) + 1
config.captions -- and we have the config option required to try finding files in templates
local blankpos = mw.ustring.find(text, "\n%s*\n") or afterend
then
local endpos = math.min(
-- We cannot distinguish the infobox from the other templates so we search them all
mw.ustring.find(text, "%[%[%s*[Ff]ile%s*:") or afterend,
local infobox = Transcluder.getTemplates( excerpt );
mw.ustring.find(text, "%[%[%s*[Ff]ile%s*:") or afterend,
infobox = table.concat( infobox )
blankpos)
local parameters = Transcluder.getParameters( infobox )
token = mw.ustring.sub(text, 1, endpos-1)
local file, captions, caption, cssclasses, cssclass
if blankpos < afterend and blankpos == endpos then -- paragraph ends with a blank line
for _, pair in pairs( config.captions ) do
token = token .. mw.ustring.match(text, "\n%s*\n", blankpos)
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
inlead = true
paras-- =Check parasfor +CSS 1classes
-- We opt to use skin-invert-image instead of skin-invert
if allparas or (options.paraflags and options.paraflags[paras]) then t = t .. token end
-- in all other cases, the CSS provided in the infobox is used
if pair[3] then
cssclasses = pair[3]
for _, p in pairs(cssclasses) do
if parameters[p] then
cssclass = ((parameters[p] == 'skin-invert') and 'skin-invert-image' or parameters[p])
break
end
end
end
excerpt = '[[File:' .. file ..
(cssclass and ('|class=' .. cssclass) or '') ..
'|thumb|' .. (caption or '') .. ']]' .. excerpt
if ( onlyFreeFiles ) then
excerpt = Transcluder.removeNonFreeFiles( excerpt )
end
break
end
end
end
 
-- Unlike other elements, templates are filtered here
if token then text = mw.ustring.sub(text, mw.ustring.len(token)+1) end
-- because we had to search the infoboxes for files
until not text or text == "" or not token or token == ""
local trash
 
if only and ( only == 'template' or only == 'templates' ) then
text = mw.ustring.gsub(t, "\n+$", "") -- remove trailing line feeds, so "{{Transclude text excerpt|Foo}} more" flows on one line
trash, excerpt = Transcluder.getTemplates( excerpt, templates );
 
else -- Remove blacklisted templates
if options.more then text = text .. " '''[[" .. pagename .. "|" .. options.more .. "]]'''" end
local blacklist = config.blacklist and table.concat( config.blacklist, ',' ) or ''
return text
if templates then
end
if string.sub( templates, 1, 1 ) == '-' then --Unwanted templates. Append to blacklist
 
blacklist = templates .. ',' .. blacklist
-- 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}
else --Wanted templates. Replaces blacklist and acts as whitelist
local function numberflags(str)
blacklist = templates
local ranges = mw.text.split(str, ",") -- parse ranges, e.g. "1,3-5" → {"1","3-5"}
end
local flags = {}
else
for _, r in pairs(ranges) do
blacklist = '-' .. blacklist
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
trash, excerpt = Transcluder.getTemplates( excerpt, blacklist );
end
return flags
end
 
-- Remove extra line breaks but leave one before and after so the parser interprets lists, tables, etc. correctly
-- Shared template invocation code for lead and random functions
excerpt = mw.text.trim( excerpt )
local function leadrandom(frame, israndom)
excerpt = string.gsub( excerpt, '\n\n\n+', '\n\n' )
-- args = { 1,2,... = page names, paragraphs = list e.g. "1,3-5", files = list, more = text}
excerpt = '\n' .. excerpt .. '\n'
local args = frame.args -- from calling module
local pargs = frame:getParent().args -- from template
 
-- Remove nested categories
local pagenames = { args[1] or pargs[1] } -- For lead, ignore all but the first unnamed argument
excerpt = frame:preprocess( excerpt )
if israndom then
local categories, excerpt = Transcluder.getCategories( excerpt, options.categories )
-- For random, accept any number of page names. If more than one, we'll pick one randomly
 
for i, p in pairs(args) do
-- Add tracking categories
if p and type(i) == 'number' and i > 1 then table.insert(pagenames, p) end
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 ]
for i, p in pairs(pargs) do
if namespaceCategory then
if p and type(i) == 'number' and i > 1 and not args[i] then table.insert(pagenames, p) end
excerpt = excerpt .. '[[Category:' .. namespaceCategory .. ']]'
end
end
 
-- Load the styles
local options = {}
local styles
options.paraflags = numberflags(args["paragraphs"] or pargs["paragraphs"] or "") -- parse paragraphs, e.g. "1,3-5" → {"1","3-5"}
if config.styles then
options.fileflags = numberflags(args["files"] or pargs["files"] or "") -- parse file numbers
styles = frame:extensionTag( 'templatestyles', '', { src = config.styles } )
options.more = args["more"] or pargs["more"]
end
if options.more and options.more == "" then options.more = "Read more..." end -- more= is short for this default text
options.errors = args["errors"] or pargs["errors"]
 
-- Combine and return the elements
local text = p._lead(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 leadrandomp.main( frame, false) end
function p.randomexcerpt( frame ) return leadrandomp.main( frame, true) end
 
return p