Edge Rewrite
// 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: a2217c1c6e9df485

Jump to content

Module:Sandbox/Solidest/Episode table

From Wikipedia, the free encyclopedia
-- This module implements {{Episode table}} and {{Episode table/part}}.

-- Central configuration for names, labels, parameters, and tracking output.
-- The rendering code below should only contain behavior that cannot be described
-- as data here.
local config = {
	-- Required module pages. Keeping them here makes dependencies easy to audit
	-- and avoids repeated string literals in the implementation.
	modules = {
		arguments = 'Module:Arguments',
		checkUnknown = 'Module:Check for unknown parameters',
		colorContrast = 'Module:Color contrast',
		colorData = 'Module:Color contrast/colors',
		plainText = 'Module:Plain text',
	},

	-- Template and stylesheet pages expanded or emitted by this module.
	templates = {
		main = 'Template:Episode table',
		part = 'Template:Episode table/part',
		screenReaderOnly = 'Template:Screen reader-only',
		styles = 'Module:Episode table/styles.css',
	},

	-- Default header background used when no background is supplied.
	defaultColor = '#CCCCFF',

	-- Classes added to the root table, in order.
	classes = {'wikitable', 'plainrowheaders', 'wikiepisodetable'},

	-- Optional sticky header support. When enabled, the configured class is
	-- added to the table and the configured stylesheet is emitted with the
	-- module stylesheet.
	stickyHeader = {
		enabled = true,
		class = 'sticky-header',
		styles = 'Template:Sticky header/styles.css',
	},

	-- Contrast thresholds used for tracking invalid table colors and for making
	-- references readable on dark or low-contrast backgrounds.
	contrast = {
		invalidColor = 7,
		reference = 7,
	},

	-- Parameter groups used outside the header-column definitions. Each key is an
	-- internal group name; each value is the list of accepted template parameter
	-- names for that group, checked from left to right.
	params = {
		id = {'id'},
		background = {'background'},
		caption = {'caption'},
		showCaption = {'show_caption'},
		country = {'country'},
		episodes = {'episodes'},
		anchor = {'anchor'},
		totalWidth = {'total_width'},
		dontclose = {'dontclose'},
		forceoverall = {'forceoverall'},
		overallType = {'overall_type'},
		seasonType = {'season_type'},
		seriesType = {'series_type'},
		viewersType = {'viewers_type'},
		partColor = {'c'},
		partReference = {'r'},
		partSubtitle = {'subtitle'},
		partNobold = {'nobold'},
		refReference = {'r'},
		refBackground = {'b'},
		altTitle = {'alttitle', 'AltTitle'},
	},

	-- Text used by dynamic column headings. Fixed column headings live directly
	-- in config.columns; this section covers headings assembled from smaller
	-- pieces or from template parameters.
	headings = {
		number = {text = 'No.', title = 'Number'},
		overall = {defaultType = 'overall', separator = '<br>'},
		season = {defaultType = 'season', prefix = ' in<br>'},
		series = {defaultType = 'series', prefix = ' in<br>'},
		prodcode = {text = 'Prod.', title = 'Production', suffix = '<br>code'},
		viewers = {
			defaultLabel = ' Viewers',
			countrySuffix = ' viewers',
			defaultType = 'millions',
			typePrefix = '<br>(',
			typeSuffix = ')',
		},
	},

	-- Suffixes appended to column parameter names for custom heading text and
	-- heading references. For example, airdateT changes the airdate heading and
	-- airdateR adds a reference to it.
	suffixes = {
		text = {'T'},
		reference = {'R'},
	},

	-- Rows inserted by {{Episode table/part}}. params enables the row label;
	-- label is the text shown before the supplied value.
	partTypes = {
		{params = {'act'}, label = 'Act'},
		{params = {'chapter'}, label = 'Chapter'},
		{params = {'part'}, label = 'Part'},
		{params = {'volume'}, label = 'Volume'},
		{params = {'week'}, label = 'Week'},
	},

	-- {{Episode table/part}} spans across all possible columns.
	partRow = {
		colspan = 13,
	},

	-- Header columns, in render order.
	-- key: internal column identifier used by the rendering code.
	-- params: accepted parameter names; the first present parameter enables the column.
	-- heading: fixed heading text; omit it for headings built from config.headings.
	-- defaultWidth: width used when the enabling parameter is present but blank.
	-- title: appends the alternate-title heading below the title heading.
	-- skipIfSeasonUsed: suppresses this column when the season column is present.
	columns = {
		{
			key = 'overall',
			params = {'overall', 'EpisodeNumber'},
			defaultWidth = 5,
		},
		{
			key = 'overall2',
			params = {'overall2', '*'},
			defaultWidth = 5,
			heading = '',
		},
		{
			key = 'season',
			params = {'season', 'EpisodeNumber2'},
			defaultWidth = 5,
		},
		{
			key = 'series',
			params = {'series', 'EpisodeNumber2Series'},
			defaultWidth = 5,
			skipIfSeasonUsed = true,
		},
		{
			key = 'title',
			params = {'title', 'Title'},
			heading = 'Title',
			title = true,
		},
		{
			key = 'aux1',
			params = {'aux1', 'Aux1'},
			heading = '',
		},
		{
			key = 'director',
			params = {'director', 'DirectedBy'},
			heading = 'Directed by',
		},
		{
			key = 'writer',
			params = {'writer', 'WrittenBy'},
			heading = 'Written by',
		},
		{
			key = 'aux2',
			params = {'aux2', 'Aux2'},
			heading = '',
		},
		{
			key = 'aux3',
			params = {'aux3', 'Aux3'},
			heading = '',
		},
		{
			key = 'airdate',
			params = {'airdate', 'OriginalAirDate'},
			heading = 'Original release date',
		},
		{
			key = 'altdate',
			params = {'altdate', 'AltDate'},
			heading = '',
		},
		{
			key = 'guests',
			params = {'guests', 'Guests'},
			heading = 'Guest(s)',
		},
		{
			key = 'musicalguests',
			params = {'musicalguests', 'MusicalGuests'},
			heading = 'Musical/entertainment guest(s)',
		},
		{
			key = 'prodcode',
			params = {'prodcode', 'ProdCode'},
			defaultWidth = 7,
		},
		{
			key = 'viewers',
			params = {'viewers', 'Viewers'},
			defaultWidth = 10,
		},
		{
			key = 'aux4',
			params = {'aux4', 'Aux4'},
			heading = '',
		},
	},

	-- Tracking categories appended to rendered output. These should stay in one
	-- place because they are public maintenance signals, not rendering details.
	categories = {
		visibleCaption = '[[Category:Articles using Template:Episode table with a visible caption]]',
		invalidColor = '[[Category:Articles using Template:Episode table with invalid colour combination]]',
		noColumns = '[[Category:Articles using Template:Episode table with no column parameters]]',
		unknown = '[[Category:Pages using episode table with unknown parameters|_VALUE_%s]]',
	},

	-- Preview-only messages used by unknown-parameter tracking.
	preview = {
		unknown = 'Page using [[Template:Episode table]] with unknown parameter "_VALUE_"',
	},

	-- Unknown-parameter tracking is generated from config.params and
	-- config.columns so new parameters do not need to be listed twice.
	check = {
		-- Individual parameters accepted by the module but not listed as known
		-- parameters for unknown-parameter tracking.
		excludedParams = {'*'},

		-- Parameter groups from config.params that are not valid for the main
		-- template and should not be listed as known parameters.
		excludedParamGroups = {'partNobold'},

		-- Parameter groups from config.params that are only valid when one of
		-- the configured suffixes is appended.
		paramGroupsRequiringSuffix = {'altTitle'},

		-- Passed through to [[Module:Check for unknown parameters]].
		showblankpositional = 'y',

		-- Regular expressions passed through to [[Module:Check for unknown parameters]].
		regexps = {
			'[%w_]+NULL',
		},
	},
}

local HTMLcolor = mw.loadData(config.modules.colorData)
local contrast_ratio = require(config.modules.colorContrast)._ratio

--------------------------------------------------------------------------------
-- Local functions
--------------------------------------------------------------------------------

local function getArg(args, names)
	for _, name in ipairs(names) do
		if args[name] ~= nil then
			return args[name], name
		end
	end
end

local function hasArg(args, names)
	local value = getArg(args, names)
	return value ~= nil
end

local function getSuffixedArg(args, names, suffixes)
	for _, name in ipairs(names) do
		for _, suffix in ipairs(suffixes) do
			local argName = name .. suffix
			if args[argName] ~= nil then
				return args[argName], argName
			end
		end
	end
end

local function normalizeColor(color, default)
	if color == nil or color == '' or color == '#' then
		return default
	end
	if HTMLcolor[color] == nil then
		color = '#' .. (mw.ustring.match(color, '^[%s#]*([a-fA-F0-9]*)[%s]*$') or '')
	end
	return color
end

local function makeCellWidth(width, defaultWidth)
	if width == nil then
		return nil
	end
	if width == '' and defaultWidth then
		width = tostring(defaultWidth)
	end
	if width == 'auto' then
		return 'auto'
	end
	if tonumber(width) ~= nil then
		return width .. '%'
	end
	return nil
end

local function makeTableWidth(width)
	local tableWidth = string.gsub(width or '', '%%', '')
	if width == 'auto' or width == '' then
		return 'auto'
	end
	if tonumber(tableWidth) ~= nil then
		return tableWidth .. '%'
	end
	return '100%'
end

local function getTableClasses()
	local classes = {}
	for _, class in ipairs(config.classes) do
		table.insert(classes, class)
	end
	if config.stickyHeader.enabled then
		table.insert(classes, config.stickyHeader.class)
	end
	return classes
end

local function addClassList(node, classes)
	for _, class in ipairs(classes) do
		node:addClass(class)
	end
	return node
end

local function makeTemplateStyles()
	local frame = mw.getCurrentFrame()
	local output = ''
	if config.templates.styles then
		output = output .. frame:extensionTag{
			name = 'templatestyles',
			args = {src = config.templates.styles},
		}
	end
	if config.stickyHeader.enabled and config.stickyHeader.styles then
		output = output .. frame:extensionTag{
			name = 'templatestyles',
			args = {src = config.stickyHeader.styles},
		}
	end
	return output
end

local function appendParameter(list, seen, name)
	if name and not seen[name] then
		table.insert(list, name)
		seen[name] = true
	end
end

local function makeSet(list)
	local set = {}
	for _, item in ipairs(list or {}) do
		set[item] = true
	end
	return set
end

local function appendSuffixedParameters(list, seen, paramNames, suffixes, includeBase, excludedParams)
	excludedParams = excludedParams or {}
	for _, paramName in ipairs(paramNames) do
		if includeBase and not excludedParams[paramName] then
			appendParameter(list, seen, paramName)
		end
		if not excludedParams[paramName] then
			for _, suffix in ipairs(suffixes) do
				appendParameter(list, seen, paramName .. suffix)
			end
		end
	end
end

local function getSortedKeys(source)
	local keys = {}
	for key in pairs(source) do
		table.insert(keys, key)
	end
	table.sort(keys)
	return keys
end

local function buildCheckConfig(title)
	local checkConfig = {
		unknown = string.format(config.categories.unknown, title.text),
		preview = config.preview.unknown,
		showblankpositional = config.check.showblankpositional,
	}
	local seen = {}

	for i, regexp in ipairs(config.check.regexps) do
		checkConfig['regexp' .. i] = regexp
	end

	local excludedParams = makeSet(config.check.excludedParams)
	local excludedParamGroups = makeSet(config.check.excludedParamGroups)
	for _, paramGroup in ipairs(config.check.paramGroupsRequiringSuffix) do
		excludedParamGroups[paramGroup] = true
	end

	for _, paramGroup in ipairs(getSortedKeys(config.params)) do
		if not excludedParamGroups[paramGroup] then
			for _, param in ipairs(config.params[paramGroup]) do
				if not excludedParams[param] then
					appendParameter(checkConfig, seen, param)
				end
			end
		end
	end

	for _, column in ipairs(config.columns) do
		appendSuffixedParameters(checkConfig, seen, column.params, {}, true, excludedParams)
		appendSuffixedParameters(checkConfig, seen, column.textParams or column.params, config.suffixes.text, false, excludedParams)
		appendSuffixedParameters(checkConfig, seen, column.referenceParams or column.params, config.suffixes.reference, false, excludedParams)
		appendSuffixedParameters(checkConfig, seen, column.extraTextParams or {}, config.suffixes.extraText or {}, false, excludedParams)
		appendSuffixedParameters(checkConfig, seen, column.extraReferenceParams or {}, config.suffixes.extraReference or {}, false, excludedParams)
	end
	for _, paramGroup in ipairs(config.check.paramGroupsRequiringSuffix) do
		appendSuffixedParameters(checkConfig, seen, config.params[paramGroup], config.suffixes.text, false, excludedParams)
		appendSuffixedParameters(checkConfig, seen, config.params[paramGroup], config.suffixes.reference, false, excludedParams)
	end

	return checkConfig
end

--------------------------------------------------------------------------------
-- EpisodeTable class
-- The main class.
--------------------------------------------------------------------------------

local EpisodeTable = {}

function EpisodeTable.abbr(text, title)
	local abbr = mw.html.create('abbr')
		:attr('title', title)
		:wikitext(text)
	return tostring(abbr)
end

local function hasColumn(args, key)
	for _, column in ipairs(config.columns) do
		if column.key == key then
			return hasArg(args, column.params)
		end
	end
	return false
end

local function buildHeadings(args)
	local countryValue = getArg(args, config.params.country)
	local country = countryValue ~= '' and countryValue ~= nil
	local viewersType = getArg(args, config.params.viewersType)
	local number = config.headings.number
	local overall = config.headings.overall
	local season = config.headings.season
	local series = config.headings.series
	local prodcode = config.headings.prodcode
	local viewersConfig = config.headings.viewers
	local viewersLabel = country
		and (viewersConfig.countryPrefix or '') .. countryValue .. (viewersConfig.countrySuffix or '')
		or viewersConfig.defaultLabel
	local viewers = viewersLabel ..
		((not viewersType or viewersType ~= '') and viewersConfig.typePrefix .. (viewersType or viewersConfig.defaultType) .. viewersConfig.typeSuffix or '')

	return {
		overall = EpisodeTable.abbr(number.text, number.title) ..
			((hasColumn(args, 'season') or hasColumn(args, 'series') or hasArg(args, config.params.forceoverall)) and overall.separator .. (getArg(args, config.params.overallType) or overall.defaultType) or ''),
		season = EpisodeTable.abbr(number.text, number.title) .. season.prefix .. (getArg(args, config.params.seasonType) or season.defaultType),
		series = EpisodeTable.abbr(number.text, number.title) .. series.prefix .. (getArg(args, config.params.seriesType) or series.defaultType),
		prodcode = EpisodeTable.abbr(prodcode.text, prodcode.title) .. prodcode.suffix,
		viewers = viewers,
	}
end

local function getColumnHeading(column, headings)
	if column.heading ~= nil then
		return column.heading
	end
	return headings[column.key] or ''
end

local function getColumnText(args, column)
	local text = getSuffixedArg(args, column.textParams or column.params, config.suffixes.text)
	if text ~= nil then
		return text
	end
	return getSuffixedArg(args, column.extraTextParams or {}, config.suffixes.extraText or {})
end

local function getColumnReference(args, column)
	local reference = getSuffixedArg(args, column.referenceParams or column.params, config.suffixes.reference)
	if reference ~= nil then
		return reference
	end
	return getSuffixedArg(args, column.extraReferenceParams or {}, config.suffixes.extraReference or {})
end

local function getAltTitle(args)
	local text = getSuffixedArg(args, config.params.altTitle, config.suffixes.text)
	local reference = getSuffixedArg(args, config.params.altTitle, config.suffixes.reference)
	return text, reference
end

function EpisodeTable.cell(background, width, text, reference, textColor, defaultWidth)
	local cell = mw.html.create('th')

	cell:attr('scope', 'col')
		:css('background', background or config.defaultColor)
		:css('width', makeCellWidth(width, defaultWidth))
		:css('color', textColor)
		:wikitext(text)

	if reference and reference ~= '' then
		cell:wikitext('&#8202;' .. EpisodeTable.reference(reference, background))
	end

	return cell
end

function EpisodeTable.reference(reference, background)
	local link1_cr = contrast_ratio{'#0645AD', background or config.defaultColor, ['error'] = 0}
	local link2_cr = contrast_ratio{'#0B0080', background or config.defaultColor, ['error'] = 0}

	local refspan = mw.html.create('span')
		:wikitext(reference)

	if link1_cr < config.contrast.reference or link2_cr < config.contrast.reference then
		local severe = config.contrast.referenceSevere and
			link1_cr < config.contrast.referenceSevere and link2_cr < config.contrast.referenceSevere
		refspan
			:css('color', severe and (config.contrast.referenceSevereColor or 'white') or 'black')
			:css('background-color', severe and (config.contrast.referenceSevereBackground or '#f8f9fa') or (config.contrast.referenceBackground or 'white'))
			:css('padding', '1px')
			:css('display', 'inline-block')
			:css('line-height', '50%')
	end

	return tostring(refspan)
end

function EpisodeTable.part(args)
	local row = mw.html.create('tr')
	local cellColor = normalizeColor(getArg(args, config.params.partColor), config.defaultColor)
	local cellRef = getArg(args, config.params.partReference)
	local subTitle = getArg(args, config.params.partSubtitle)

	local black_cr = contrast_ratio{cellColor or config.defaultColor, 'black', ['error'] = 0}
	local white_cr = contrast_ratio{'white', cellColor or config.defaultColor, ['error'] = 0}

	local displaytext = ''
	local isAnyPartSet = false

	for _, partType in ipairs(config.partTypes) do
		local partValue = getArg(args, partType.params)
		if partValue then
			isAnyPartSet = true
			displaytext = partType.label .. ' ' .. partValue
		end
	end

	if subTitle then
		displaytext = displaytext .. ((isAnyPartSet and ': ' or '') .. subTitle)
	end

	local displayTextAnchor = require(config.modules.plainText)._main(displaytext)
	local text = (hasArg(args, config.params.partNobold) and displaytext or "'''" .. displaytext .. "'''") ..
		(cellRef and '&#8202;' .. EpisodeTable.reference(cellRef, cellColor) or '')

	row:tag('td')
		:attr('colspan', config.partRow.colspan)
		:attr('id', getArg(args, config.params.id) or displayTextAnchor)
		:css('text-align', 'center')
		:css('background-color', cellColor or config.defaultColor)
		:css('color', black_cr > white_cr and 'black' or 'white')
		:wikitext(text)

	return tostring(row)
end

function EpisodeTable.new(frame, args)
	args = args or {}
	local categories = ''
	local rawBackground = getArg(args, config.params.background)
	local background = normalizeColor(rawBackground, nil)

	local root = addClassList(mw.html.create('table'), getTableClasses())
		:css('width', makeTableWidth(getArg(args, config.params.totalWidth)))

	local caption = getArg(args, config.params.caption)
	if hasArg(args, config.params.showCaption) then
		root:tag('caption'):wikitext(caption)
		categories = categories .. config.categories.visibleCaption
	elseif caption then
		root:tag('caption'):wikitext(frame:expandTemplate{title = config.templates.screenReaderOnly, args = {caption}})
	end

	local title = mw.title.getCurrentTitle()
	local black_cr = contrast_ratio{background, 'black', ['error'] = 0}
	local white_cr = contrast_ratio{'white', background, ['error'] = 0}

	if title.namespace == 0 and rawBackground and rawBackground ~= '' and rawBackground ~= '#'
		and black_cr < config.contrast.invalidColor and white_cr < config.contrast.invalidColor
	then
		categories = categories .. config.categories.invalidColor
	end

	local textColor = background and (black_cr > white_cr and 'black' or 'white') or 'black'
	local mainRow = root:tag('tr')
	mainRow
		:css('color', textColor)
		:css('text-align', 'center')

	local usedColumn = false
	local usedSeason = false
	local headings = buildHeadings(args)

	for _, column in ipairs(config.columns) do
		local width = getArg(args, column.params)
		if width and not (column.skipIfSeasonUsed and usedSeason) then
			usedColumn = true
			if column.key == 'season' then
				usedSeason = true
			end

			local cellText = getColumnText(args, column)
			local cellRef = getColumnReference(args, column)
			cellText = cellText or getColumnHeading(column, headings)

			if column.title then
				cellText = cellText .. (cellRef and '&#8202;' .. EpisodeTable.reference(cellRef, background) or '')

				local altText, altRef = getAltTitle(args)
				if altText then
					cellText = cellText .. '<br> ' .. altText .. (altRef and '&#8202;' .. EpisodeTable.reference(altRef, background) or '')
				end

				mainRow:node(EpisodeTable.cell(background, width, cellText, false, textColor, column.defaultWidth))
			else
				mainRow:node(EpisodeTable.cell(background, width, cellText, cellRef, textColor, column.defaultWidth))
			end
		end
	end

	if title.namespace == 0 and usedColumn == false then
		categories = categories .. config.categories.noColumns
	end

	local episodes = getArg(args, config.params.episodes)
	if episodes then
		local anchor = getArg(args, config.params.anchor)
		if anchor then
			episodes = string.gsub(episodes, '(id=")(ep%w+")', '%1' .. anchor .. '%2')
		end
		root:node(episodes)
	end

	local templateStyles = makeTemplateStyles()
	local dontclose = getArg(args, config.params.dontclose)
	local output = ((dontclose or '') ~= '') and mw.ustring.gsub(tostring(root), '</table>', '') or tostring(root)
	return output .. categories .. templateStyles
end

--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------

local p = {}

function p.main(frame)
	local args = require(config.modules.arguments).getArgs(frame, {
		removeBlanks = false,
		wrappers = config.templates.main,
	})

	local tracking = ''
	local title = mw.title.getCurrentTitle()
	if title.namespace == 0 then
		tracking = require(config.modules.checkUnknown)._check(buildCheckConfig(title), args)
	end

	return EpisodeTable.new(frame, args) .. tracking
end

function p.part(frame)
	local args = require(config.modules.arguments).getArgs(frame, {
		removeBlanks = false,
		wrappers = config.templates.part,
	})
	return EpisodeTable.part(args)
end

function p.ref(frame)
	local args = require(config.modules.arguments).getArgs(frame, {
		removeBlanks = false,
	})
	return EpisodeTable.reference(getArg(args, config.params.refReference), getArg(args, config.params.refBackground))
end

return p