Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a21ed614afc9cf67

Jump to content

Module:ExcerptHeaderLevel

From Wikipedia, the free encyclopedia
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 showExcerptNoticeDefault = includeTopHeader and 'yes' or 'no'
	local showExcerptNotice = isYes(getArg(args, 'show-excerpt-notice', showExcerptNoticeDefault))
	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