Module:Excerpt: Difference between revisions

Content deleted Content added
Handle redirects by transcluding the target
Use infobox CSS class if it exists (mainly for dark mode compatibility)
 
(169 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
-- Entry point for Lua callers
local args
-- Returns a string value: text of the lead of a page
local function p._leadgetArg(pagename key, optionsdefault )
local value = args[ key ]
if not pagename then return "" end -- Return blank text rather than an error splurge
if value and mw.text.trim( value ) ~= '' then
local title = mw.title.new(pagename) -- Find the lead section of the named page
if not title then return "" endvalue
end
local redir = mRedirect.getTarget(title)
return default
if redir then title = mw.title.new(redir) end
end
 
-- Helper function to handle errors
local text = title:getContent() or ""
local function getError( message, value )
text = mw.ustring.gsub(text, "%c%s*==.*","") -- remove first heading and everything after it
if type( message ) == 'string' then
text = mw.ustring.gsub(text, "<noinclude>.-</noinclude>", "") -- remove noinclude bits
message = Transcluder.getError( message, value )
text = mw.ustring.gsub(text, "<%s*ref[^>]-/%s*>", "") -- remove refs cited elsewhere
text = mw.ustring.gsub(text, "<%s*ref.->.-<%s*/%s*ref%s*>", "") -- remove refs
for _, t in pairs {"[Ee]fn", "[Ee]fn-la", "[Ee]l[mn]", "[Rr]", "[Ss]fn[bp]", "[Ss]f[bn]", "NoteTag"} do
text = mw.ustring.gsub(text, "{{%s*" .. t .. "%s*|.-}}", "") -- remove ref and footnote templates
end
if config.categories and config.categories.errors and mw.title.getCurrentTitle().isContentPage then
text = mw.ustring.gsub(text, "\n%s*{{%s*[Tt][Oo][Cc].-}}", "\n") -- remove most common tables of contents
message:node( '[[Category:' .. config.categories.errors .. ']]' )
end
return message
end
 
-- Helper function to get localized messages
local allparas = true -- keep all paragraphs?
local function getMessage( key )
if options.paraflags then
local ok, TNT = pcall( require, 'Module:TNT' )
for _, v in pairs(options.paraflags) do
if not ok then return key end
if v then allparas = false end -- if any para specifically requested, don't keep all
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
-- a basic parser to trim down the lead
if more and not inline then
local inlead = false -- have we found some text yet?
more = "'''[[" .. page .. '#' .. ( section or '' ) .. "|" .. getMessage( 'more' ) .. "]]'''"
local t = "" -- the stripped down output text
more = mw.html.create( 'div' ):addClass( 'noprint excerpt-more' ):wikitext( more )
local files = 0 -- how many [[Image: or [[File: so far
else
local paras = 0 -- how many paragraphs so far
more = nil
end
 
-- Build the options for Module:Transcluder out of the template parameters and the desired defaults
text = mw.ustring.gsub(text,"^%s*","") -- remove initial white space
local options = {
repeat -- loop around parsing a comment, template, image or paragraph
files = files,
local token = mw.ustring.match(text, "^%s*<!%-%-.-%-%->%s*") -- <!--HTML comment-->
lists = lists,
or mw.ustring.match(text, "^%b{}%s*") or false -- or {{Template}}
tables = tables,
if token then
paragraphs = paragraphs,
if inlead then t = t .. token end -- keep comments and templates only within text body
sections = subsections,
else
categories = 0,
token = mw.ustring.match(text, "^%[%[%s*[Ff]ile%s*:%C*%]%]%s*") -- [[File: ... ]]
references = references,
or mw.ustring.match(text, "^%[%[%s*[Ii]mage%s*:%C*%]%]%s*") -- or [[Image: ... ]]
only = only and mw.text.trim( only, 's' ) .. 's',
if token then
filesnoLinks = files + 1noLinks,
noBold = noBold,
if options.fileflags and options.fileflags[files] then t = t .. token end
noSelfLinks = true,
else -- got a paragraph, which ends at a file, image, blank line or end of text
noNonFreeFiles = onlyFreeFiles,
local afterend = mw.ustring.len(text) + 1
noBehaviorSwitches = true,
local blankpos = mw.ustring.find(text, "\n%s*\n") or afterend
fixReferences = true,
local endpos = math.min(
linkBold = true,
mw.ustring.find(text, "%[%[%s*[Ff]ile%s*:") or afterend,
}
mw.ustring.find(text, "%[%[%s*[Ff]ile%s*:") or afterend,
 
blankpos)
-- Get the excerpt itself
token = mw.ustring.sub(text, 1, endpos-1)
local title = page .. '#' .. ( section or '' )
if blankpos < afterend and blankpos == endpos then -- paragraph ends with a blank line
local ok, excerpt = pcall( Transcluder.get, title, options )
token = token .. mw.ustring.match(text, "\n%s*\n", blankpos)
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
inlead = true
paras = paras + 1
if allparas or (options.paraflags and options.paraflags[paras]) then t = t .. token end
end
end
end
 
-- If no file was found, try to get one from the infobox
if token then text = mw.ustring.sub(text, mw.ustring.len(token)+1) end
local fileNamespaces = Transcluder.getNamespaces( 'File' )
until not text or text == "" or not token or token == ""
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, 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
-- Check for CSS classes
-- We opt to use skin-invert-image instead of skin-invert
-- 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
text = mw.ustring.gsub(t, "\n+$", "") -- remove trailing line feeds, so "{{Transclude text excerpt|Foo}} more" flows on one line
-- because we had to search the infoboxes for files
 
local trash
if options.more then text = text .. " '''[[" .. pagename .. "|" .. options.more .. "]]'''" end
if only and ( only == 'template' or only == 'templates' ) then
return text
trash, excerpt = Transcluder.getTemplates( excerpt, templates );
end
else -- Remove blacklisted templates
 
local blacklist = config.blacklist and table.concat( config.blacklist, ',' ) or ''
-- 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}
if templates then
function p.numberflags(str)
if string.sub( templates, 1, 1 ) == '-' then --Unwanted templates. Append to blacklist
local ranges = mw.text.split(str, ",") -- parse ranges, e.g. "1,3-5" → {"1","3-5"}
blacklist = templates .. ',' .. blacklist
local flags = {}
else --Wanted templates. Replaces blacklist and acts as whitelist
for _, r in pairs(ranges) do
blacklist = templates
local min, max = mw.ustring.match(r, "^%s*(%d+)%s*%-%s*(%d+)%s*$") -- "3-5" → min=3 max=5
end
if not max then min, max = mw.ustring.match(r, "^%s*((%d+))%s*$") end -- "1" → min=1 max=1
else
if max then
blacklist = '-' .. blacklist
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
-- Entry point for template callers using #invoke:
excerpt = mw.text.trim( excerpt )
function p.lead(frame)
excerpt = string.gsub( excerpt, '\n\n\n+', '\n\n' )
-- args = { 1 = page name, 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 pagename = args[1] or pargs[1] or ""
excerpt = frame:preprocess( excerpt )
pagename = mw.ustring.match(pagename, "%[%[%s*(.-)[]|#]") or pagename -- "[[Foo|Bar]]" → "Foo"
local categories, excerpt = Transcluder.getCategories( excerpt, options.categories )
 
-- Add tracking categories
local options = {}
if config.categories then
options.paraflags = p.numberflags(args["paragraphs"] or pargs["paragraphs"] or "") -- parse paragraphs, e.g. "1,3-5" → {"1","3-5"}
local contentCategory = config.categories.content
options.fileflags = p.numberflags(args["files"] or pargs["files"] or "") -- parse file numbers
if contentCategory and mw.title.getCurrentTitle().isContentPage then
options.more = args["more"] or pargs["more"]
excerpt = excerpt .. '[[Category:' .. contentCategory .. ']]'
if options.more and options.more == "" then options.more = "Read more..." end -- more= is short for this default text
end
local namespaceCategory = config.categories[ mw.title.getCurrentTitle().namespace ]
if namespaceCategory then
excerpt = excerpt .. '[[Category:' .. namespaceCategory .. ']]'
end
end
 
-- Load the styles
local text = p._lead(pagename, options)
local styles
return frame:preprocess(text)
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