local p = {}
local parser = require('Module:WikitextParser')
local function getArg(args, key, fallback)
local v = args[key]
if v == nil or v == '' then
return fallback
end
return v
end
local function tonumberOrNil(v)
local n = tonumber(v)
if n then
return n
end
return nil
end
local function tonumberOrZero(v)
local n = tonumber(v)
if n then
return n
end
return 0
end
local function clamp(n, low, high)
if n < low then
return low
elseif n > high then
return high
end
return n
end
local function trim(s)
return mw.text.trim(tostring(s or ''))
end
local function isYes(v)
v = trim(v):lower()
return v == '1' or v == 'yes' or v == 'y' or v == 'true'
end
local function parseHeadingLine(line)
local eq1, title, eq2 = tostring(line):match('^(=+)%s*(.-)%s*(=+)%s*$')
if eq1 and eq2 and eq1 == eq2 then
return {
level = #eq1,
title = title
}
end
return nil
end
local function isMarkerOnlyLine(line)
return tostring(line):match('^%s*=+%s*$') ~= nil
end
local function rebuildHeading(title, level)
title = trim(title):gsub('[\r\n]+', ' ')
level = clamp(level, 2, 6)
local eq = string.rep('=', level)
return eq .. ' ' .. title .. ' ' .. eq
end
local function adjustHeading(line, shift)
local h = parseHeadingLine(line)
if not h then
return line
end
return rebuildHeading(h.title, h.level + shift)
end
local function adjustHeadingRelativeToTop(line, originalTopLevel, desiredTopLevel)
local h = parseHeadingLine(line)
if not h then
return line
end
local relativeDepth = h.level - originalTopLevel
return rebuildHeading(h.title, desiredTopLevel + relativeDepth)
end
local function makeTopHeading(title, level)
return rebuildHeading(title, level)
end
local function splitLines(text)
local lines = {}
text = tostring(text or '')
for line in (text .. '\n'):gmatch('(.-)\n') do
table.insert(lines, line)
end
return lines
end
local function stripLeadingGarbage(lines, sectionTitle)
local normalizedTitle = trim(sectionTitle)
local removedSomething = true
while removedSomething and #lines > 0 do
removedSomething = false
while #lines > 0 and trim(lines[1]) == '' do
table.remove(lines, 1)
removedSomething = true
end
if #lines == 0 then
return
end
local first = trim(lines[1])
local second = trim(lines[2] or '')
local h = parseHeadingLine(first)
if h and trim(h.title) == normalizedTitle then
table.remove(lines, 1)
removedSomething = true
elseif isMarkerOnlyLine(first) then
table.remove(lines, 1)
removedSomething = true
if #lines > 0 and trim(lines[1]) == normalizedTitle then
table.remove(lines, 1)
end
elseif first == normalizedTitle then
table.remove(lines, 1)
removedSomething = true
elseif second == normalizedTitle and isMarkerOnlyLine(first) then
table.remove(lines, 1)
table.remove(lines, 1)
removedSomething = true
end
end
end
local function stripTrailingBlankLines(lines)
while #lines > 0 and trim(lines[#lines]) == '' do
table.remove(lines, #lines)
end
end
local function makeExcerptHatnote(page, section)
local displaySection = section:gsub('%[%[([^]|]+)|?[^]]*%]%]', '%1')
local link = '[[:' .. page .. '#' .. mw.uri.anchorEncode(section) .. '|' .. page .. ' § ' .. displaySection .. ']]'
local title = mw.title.new(page)
local editUrl = title and title:fullUrl('action=edit') or nil
local hat = 'These paragraphs are an excerpt from ' .. link .. '.'
if editUrl then
hat = hat
.. '<span class="mw-editsection-like plainlinks"><span class="mw-editsection-bracket">[</span>['
.. tostring(editUrl) .. ' ' .. mw.message.new('editsection'):plain()
.. ']<span class="mw-editsection-bracket">]</span></span>'
end
return tostring(
mw.html.create('div')
:addClass('dablink')
:addClass('excerpt-hat')
:wikitext(hat)
)
end
local function escapeString(str)
return tostring(str):gsub('[%^%$%(%)%.%[%]%*%+%-%?%%]', '%%%0')
end
local function removeString(text, str)
local pattern = escapeString(str)
if #pattern > 9999 then
pattern = escapeString(mw.ustring.sub(str, 1, 999)) .. '.-' .. escapeString(mw.ustring.sub(str, -999))
end
return text:gsub(pattern, '')
end
local function parseFilter(filter)
local filters = {}
local isBlacklist = false
if string.sub(filter, 1, 1) == '-' then
isBlacklist = true
filter = string.sub(filter, 2)
end
local values = mw.text.split(filter, ',')
for _, value in pairs(values) do
value = mw.text.trim(value)
local min, max = mw.ustring.match(value, '^(%d+)%s*[-–—]%s*(%d+)$')
if not max then
min, max = string.match(value, '^((%d+))$')
end
if max then
for i = min, max do
filters[i] = true
end
else
filters[value] = true
end
end
return { cache = {}, terms = filters }, isBlacklist
end
local function matchFilter(value, filter)
if value == nil then
return false
elseif type(value) == 'number' then
return filter.terms[value]
else
local cached = filter.cache[value]
if cached ~= nil then
return cached
end
local lang = mw.language.getContentLanguage()
local lcvalue = lang:lcfirst(value)
local ucvalue = lang:ucfirst(value)
for term in pairs(filter.terms) do
if value == tostring(term)
or type(term) == 'string' and (
lcvalue == term
or ucvalue == term
or mw.ustring.match(value, term)
)
then
filter.cache[value] = true
return true
end
end
filter.cache[value] = false
return false
end
end
local function filterTemplates(wikitext, filter)
if not filter or filter == '' then
return wikitext
end
local filters, isBlacklist = parseFilter(filter)
local templates = parser.getTemplates(wikitext)
for index, template in pairs(templates) do
local name = parser.getTemplateName(template)
if
(isBlacklist and (matchFilter(index, filters) or matchFilter(name, filters)))
or
(not isBlacklist and (not matchFilter(index, filters) and not matchFilter(name, filters)))
then
wikitext = removeString(wikitext, template)
end
end
return wikitext
end
local function getRawSectionWikitext(page, section, includeSubsections)
local title = mw.title.new(page)
if not title then
return nil, 'invalid-title'
end
if title.isRedirect then
title = title.redirectTarget
end
if not title or not title.exists then
return nil, 'page-not-found'
end
local wikitext = title:getContent()
if not wikitext or wikitext == '' then
return nil, 'page-empty'
end
local excerpt = parser.getSectionTag(wikitext, section)
if not excerpt then
if includeSubsections then
excerpt = parser.getSection(wikitext, section)
else
local sections = parser.getSections(wikitext)
excerpt = sections[section]
end
end
if not excerpt or excerpt == '' then
return nil, 'section-not-found'
end
return excerpt
end
function p.main(frame)
local args = frame:getParent().args
local page = trim(getArg(args, 1, getArg(args, 'page', nil)))
local section = trim(getArg(args, 2, getArg(args, 'section', nil)))
if page == '' or section == '' then
return '<strong class="error">Template:ExcerptHeaderLevel requires both a page and a section heading.</strong>'
end
local includeTopHeader = isYes(getArg(args, 'include-top-header', 'yes'))
local showExcerptNotice = isYes(getArg(args, 'show-excerpt-notice', 'yes'))
local includeSubsections = isYes(getArg(args, 'subsections', 'yes'))
local templateFilter = getArg(args, 'templates', nil)
local topHeaderLevel = tonumberOrNil(getArg(args, 'top-header-level', nil))
local shift
if topHeaderLevel then
topHeaderLevel = clamp(topHeaderLevel, 2, 6)
else
shift = tonumberOrZero(getArg(args, 'shift', 0))
local higher = tonumberOrZero(getArg(args, 'higher', 0))
local lower = tonumberOrZero(getArg(args, 'lower', 0))
shift = shift - higher + lower
end
local raw, err = getRawSectionWikitext(page, section, includeSubsections)
if not raw then
return '<strong class="error">ExcerptHeaderLevel error: ' .. tostring(err) .. '.</strong>'
end
if templateFilter and templateFilter ~= '' then
raw = filterTemplates(raw, templateFilter)
end
raw = raw:gsub('<[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.-</[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>', '')
raw = raw:gsub('\n\n\n+', '\n\n')
raw = mw.text.trim(raw)
raw = '\n' .. raw .. '\n'
raw = frame:preprocess(raw)
local lines = splitLines(raw)
stripLeadingGarbage(lines, section)
local out = {}
if showExcerptNotice then
table.insert(out, makeExcerptHatnote(page, section))
end
if topHeaderLevel then
if includeTopHeader then
table.insert(out, makeTopHeading(section, topHeaderLevel))
end
for _, line in ipairs(lines) do
table.insert(out, adjustHeadingRelativeToTop(line, 2, topHeaderLevel + 1))
end
else
if includeTopHeader then
table.insert(out, makeTopHeading(section, clamp(2 + shift, 2, 6)))
end
for _, line in ipairs(lines) do
table.insert(out, adjustHeading(line, shift))
end
end
stripTrailingBlankLines(out)
return table.concat(out, '\n')
end
return p