Module:Infobox television episode

--- @module local p = {}

local maintenance_categories = { incorrectly_formatted = "", unlinked_values = "", image_values_without_an_image = "", unnecessary_title_parameter = "", non_matching_title = "", flag_icon = "", dates_incorrectly_formatted = "", manual_display_title = "", list_markup = "", }

--- Returns the text after removing line breaks ( tags) and additional spaces as a result. --- --- @param text string --- @return string local function get_name_with_br_fixes(text) local title, _ = string.gsub(text, "", "") title, _ = string.gsub(title, " ", " ") return title end

--- Returns the page name after replacing quotation marks for single quotes and fixing it to use the --- "Space+single" and "Single+space" templates if a leading or trailing apostrophe or quotation mark is used. --- --- Note: per MOS:QWQ an episode title with quotation marks should be replaced with single quotes. --- --- @param frame table --- @param article_title string --- @return string local function get_page_name_with_apostrophe_quotation_fixes(frame, article_title) local page_name, _ = string.gsub(article_title, '"', "'")

local left_side_template = frame:expandTemplate{title = "Space+single"} local right_side_template = frame:expandTemplate{title = "Single+space"} page_name, _ = string.gsub(string.gsub(page_name, "^'", left_side_template), "'$", right_side_template)

return page_name end

--- Returns the series link. --- --- @param series string --- @return string local function get_series_link(series) local delink = require("Module:Delink")._delink return delink({series, wikilinks = "target"}) end

--- Returns two strings: The series name after de-linking it and escaping "-". The series name after de-linking it. --- --- @param series string --- @return string, string local function get_series_name(series) local delink = require("Module:Delink")._delink local series_name = delink({series})

-- Escape the character "-" as it is needed for string.find to work. local _ local series_name_escaped, _ = string.gsub(series_name, "-", "%%-") return series_name_escaped, series_name end

--- Returns a table consisting of the episode's title parts. --- --- The return table's properties: --- - title - The episode's title. --- - disambiguation - the disambiguation text without parentheses. --- --- Note: could potentially be moved to an outside module for other template and module uses. --- --- @param text string --- @return table local function get_title_parts(text) local title, disambiguation = string.match(text, "^(.+) (%b)$")

local titleString = title -- TODO: needed until https://github.com/Benjamin-Dobell/IntelliJ-Luanalysis/issues/63 is resolved. if not title or type(title) ~= "string" then titleString = text end

---@type table local title_parts = {title = -- nil titleString, disambiguation = nil}

if not disambiguation or type(disambiguation) ~= "string" then return title_parts end

-- Remove outside parentheses from names which use parentheses as part of the name such as "episode (Randall and Hopkirk (Deceased))". disambiguation = string.sub(-- nil disambiguation, 2, -2) title_parts.disambiguation = -----@not number disambiguation return title_parts end

--- Returns the title used in the template and an optional maintenance category. --- --- @param page_text string --- @param args table --- @param title_parts table --- @return string | nil local function get_lowercase_template_status(page_text, args, title_parts) local lowercase_template = string.match(page_text, "") if lowercase_template then local lowercase_title, _ = string.gsub(title_parts.title,"^%u", string.lower) if args.title then if args.title == lowercase_title then return maintenance_categories.unnecessary_title_parameter else return maintenance_categories.non_matching_title end return "" end return lowercase_title end

return nil end

--- Returns the title used in the template and an optional maintenance category. --- --- @param page_text string --- @param args table --- @param return_category boolean --- @return string | nil local function get_correct_title_value(page_text, args, return_category) local correct_title_template_pattern = ""

local correct_title = string.match(page_text, correct_title_template_pattern)

if not correct_title then correct_title_template_pattern = "" correct_title = string.match(page_text, correct_title_template_pattern) end

if not correct_title and type(correct_title) ~= "string" then return nil end

local correct_title_title_parts = get_title_parts(correct_title)

if not correct_title_title_parts.disambiguation then -- If the correct title value has no disambiguation, check if the title used in the infobox is the same as the title used for the correct title value. if return_category and args.title then if args.title == correct_title_title_parts.title then return maintenance_categories.unnecessary_title_parameter else return maintenance_categories.non_matching_title end end return correct_title_title_parts.title end

local series_name_escaped, _ = get_series_name(args.series)

if series_name_escaped ~= "" and (correct_title_title_parts.disambiguation == series_name_escaped or string.find(correct_title_title_parts.disambiguation, series_name_escaped)) then if return_category and args.title then if args.title == correct_title_title_parts.title then return maintenance_categories.unnecessary_title_parameter else return maintenance_categories.non_matching_title end end return correct_title_title_parts.title end

-- Can't determine if the text in parentheses is disambiguation or part of the title since |series= isn't used. if return_category then return "" end

return correct_title end

--- Returns the display title text used in either the or  templates. --- --- @param page_text string --- @param article_title string --- @return string | nil local function get_display_title_text(page_text, article_title) local title_modification = string.match(page_text, "") if title_modification and type(title_modification) == "string" then return -- nil title_modification end

title_modification = string.match(page_text, "") if title_modification and type(title_modification) == "string" then local italic_title_text, _ = string.gsub(article_title, -- nil title_modification, "" .. title_modification .. "") return italic_title_text end

return nil end

--- Returns a maintenance category if the italic_title value is not "no". --- --- Infobox parameters checked: --- - |italic_title= --- --- @param args table --- @return string local function is_italic_title_valid_value(args) if args.italic_title and args.italic_title ~= "no" then return string.format(maintenance_categories.incorrectly_formatted, "italic_title") end return "" end

--- Returns a maintenance category if the date is not formatted correctly with a template. --- Allow "Unaired" as a valid value for unaired television episodes. --- --- Infobox parameters checked: --- - |airdate= --- - |released= --- - |airdate_overall= --- --- @param start_date string --- @return string local function are_dates_formatted_correctly(start_date) if start_date and (string.find(start_date, "film%-date") or not string.find(start_date, "itvstart") and start_date ~= "Unaired") then return maintenance_categories.dates_incorrectly_formatted end return "" end

--- Returns a maintenance category if list markup is used. The infobox can handle list markup correctly. --- --- Note: the code here is temporarily checking only the parameters which have been converted --- to use the plainlist class directly. Once current uses will be converted, the function will check all parameters --- for incorrect usage. --- --- Infobox parameters checked: ---	- Parameters listed below. --- --- Currently checks for the following list markup: --- - tags - per MOS:NOBR. --- -  tags. --- - "plainlist" class. --- - "hlist" class. --- --- @param args table --- @return string local function uses_list_markup(args) local invalid_tags = { ["br"] = "<[bB][rR]%s?/?>", ["li"] = "", ["plainlist"] = "plainlist", ["hlist"] = "hlist", }

---@type table local parameters = { director = true, writer = true, story = true, teleplay = true, narrator = true, presenter = true, producer = true, music = true, photographer = true, editor = true, production = true, airdate = true, guests = true, commentary = true, }

for parameter_name, _ in pairs(parameters) do		for _, list_pattern in pairs(invalid_tags) do			local parameter_value = args[parameter_name] if parameter_value and string.find(parameter_value, list_pattern) then return maintenance_categories.list_markup end end end return "" end

--- Returns a maintenance category if a flag icon is used. --- --- All of the infobox values are checked. --- --- @param args table --- @return string local function has_flag_icon(args) for _, value in pairs(args) do		if string.find(value, "flagicon") then return maintenance_categories.flag_icon end end return "" end

--- Returns a maintenance category if the values are linked. --- --- Infobox parameters checked: --- - |episode= --- - |season= --- - |series_no= --- - |episode_list= --- --- The function currently checks if the following values are present: --- - ]] - links. --- --- @param args table --- @return string local function are_values_linked(args) local parameters = { episode = args.episode, season = args.season, series_no = args.series_no, episode_list = args.episode_list, }

for key, value in pairs(parameters) do		if string.find(value, "]]", 1, true) then return string.format(maintenance_categories.incorrectly_formatted, key) end end return "" end

--- Returns a maintenance category if the values are formatted. --- --- Most of the infobox values are checked. Not included are: --- - |title= - is handled in is_infobox_title_equal_to_article_title --- - |series= - is handled in are_values_links_only --- - |next= - is handled in are_values_links_only --- - |prev= is handled in are_values_links_only --- - |rtitle= --- - |rprev= --- - |rnext= --- - |image_alt= --- - |alt= --- - |caption= --- - |based_on= --- - |music= --- - |guests= --- - |module= --- --- The function currently checks if the following values are present: --- - '' - italics or bold. --- --- Note: --- If the series is American Horror Story then the season_article value is allowed to be formatted. --- If in the future more series need this exception then the hardcoded value in the function should be taken out into a list. --- --- @param args table --- @return string local function are_values_formatted(args) ---@type table local ignore_parameters = { title = true, series = true, prev = true, next = true, rtitle = true, rprev = true, rnext = true, image_alt = true, alt = true, caption = true, based_on = true, music = true, guests = true, module = true, }

for key, value in pairs(args) do		if not ignore_parameters[key] and string.find(value, "''", 1, true) then if key == "season_article" and args.series == "American Horror Story" then --TODO: This is hardcoded for now. -- Do nothing. else return string.format(maintenance_categories.incorrectly_formatted, key) end end end return "" end

--- Returns a maintenance category if the values use additional overall numbering. --- --- Infobox parameters checked: --- - |episode= --- - |season= --- - |series_no= --- --- The function currently checks if the following values are present: --- - overall - unsupported series overall numbering. --- --- @param args table --- @return string local function are_values_using_overall(args) local parameters = { episode = args.episode, season = args.season, series_no = args.series_no, }

for key, value in pairs(parameters) do		if string.find(value, "overall") then return string.format(maintenance_categories.incorrectly_formatted, key) end end

return "" end

--- Returns a maintenance category if the values are unlinked and if additional characters are found in the text. --- --- Infobox parameters checked: --- - |series= --- - |prev= --- - |next= --- --- The function currently checks if a value is unlinked or if there is any additional character --- before or after the linked text. --- --- @param args table --- @return string local function are_values_links_only(args) local parameters = { series = args.series, prev = args.prev, next = args.next, }

for key, value in pairs(parameters) do -- Check whether the values are linked. if not string.find(value, "%[%[.*%]%]") then return string.format(maintenance_categories.unlinked_values, key) end

-- Check whether the values have anything before or after link brackets. if string.gsub(value, "(%[%[.*%]%])", "") ~= "" then return string.format(maintenance_categories.incorrectly_formatted, key) end end

return "" end

--- Returns a maintenance category if the |image= value includes the "File:" or "Image:" prefix. --- --- Infobox parameters checked: --- - |image= --- --- @param image string --- @return string local function is_image_using_incorrect_syntax(image) if not image then return "" end

if string.find(image, "[Ff]ile:") or string.find(image, "[Ii]mage:") then return string.format(maintenance_categories.incorrectly_formatted, "image") end

return "" end

--- Returns a maintenance category if the |image_size= value includes "px". --- --- Infobox parameters checked: --- - |image_size= --- --- @param image_size string --- @return string local function is_image_size_using_px(image_size) if image_size and string.find(image_size, "px") then return string.format(maintenance_categories.incorrectly_formatted, "image_size") end return "" end

--- Returns a maintenance category if there is no image file while image auxiliary values are present. --- --- Infobox parameters checked: --- - |image= --- - |image_size= --- - |image_upright= --- - |image_alt= --- - |alt= --- - |caption= --- --- @param args table --- @return string local function are_image_auxiliary_values_used_for_no_image(args) if args.image then return "" end

if args.image_size or args.image_upright or args.image_alt or args.alt or args.caption then return maintenance_categories.image_values_without_an_image end

return "" end

--- Returns a maintenance category if the infobox title is equal to the article title. --- --- Infobox parameters checked: --- - |title= --- - |series= --- - |italic_title --- --- The function currently checks if the infobox title is equal to the article title while ignoring styling such as: --- - Nowrap spans. --- - Line breaks. --- - Leading and trailing apostrophe spaces. --- --- A return value can be one of three options: --- - The value of maintenance_categories.non_matching_title - when the args.title does not match the article title. --- - The value of maintenance_categories.unnecessary_title_parameter - when the args.title matches the article title. --- - An empty string - when args.title isn't used or the args.title uses an allowed modification --- (such as a nowrap template) while the rest of the args.title matchs the article title. --- --- Testing parameters: --- - |page_test= - a real Wikipedia page to read the content of the page. --- - |page_title_test= - the title of the page being checked. --- --- @param frame table --- @param args table --- @return string local function is_infobox_title_equal_to_article_title(frame, args) if not args.title then return "" end

local page_text if args.page_test then page_text = mw.title.new(args.page_test):getContent else page_text = mw.title.getCurrentTitle:getContent end

-- Check if the article is using a template. local correct_title = get_correct_title_value(page_text, args, true) if correct_title then return correct_title end

local article_title = args.page_title_test if not args.page_title_test then article_title = mw.title.getCurrentTitle.text end

local title_parts = get_title_parts(article_title)

-- Check if the article is using a template. local lowercase_title = get_lowercase_template_status(page_text, args, title_parts) if lowercase_title then return lowercase_title end

if title_parts.disambiguation then local series_name_escaped, _ = get_series_name(args.series) series_name_escaped = get_name_with_br_fixes(series_name_escaped) if series_name_escaped ~= "" and (title_parts.disambiguation == series_name_escaped or string.find(title_parts.disambiguation, series_name_escaped)) then -- Remove disambiguation. article_title = title_parts.title end end

if args.italic_title then -- Check if the article is using a or  template. local title_modification = get_display_title_text(page_text, article_title) if title_modification then if title_modification == args.title then return maintenance_categories.unnecessary_title_parameter else return maintenance_categories.non_matching_title end end end

local page_name = get_page_name_with_apostrophe_quotation_fixes(frame, article_title)

-- Remove nowrap span. if string.find(args.title, "nowrap") then local title = frame:expandTemplate{title = "Strip tags", args = {args.title}} if title == page_name then return "" end return maintenance_categories.non_matching_title end

-- Remove line breaks and additional spaces as a result. if string.find(args.title, "") then local title = get_name_with_br_fixes(args.title) if title == page_name then return "" end return maintenance_categories.non_matching_title end

if args.title == page_name then return maintenance_categories.unnecessary_title_parameter end

-- Article and infobox titles do not match. return maintenance_categories.non_matching_title end

--- Returns the relevant maintenance categories based on the values validated. --- --- @param frame table --- @return string function p.validate_values(frame) local getArgs = require("Module:Arguments").getArgs local args = getArgs(frame)

---@type string[] local categories = {} table.insert(categories, is_infobox_title_equal_to_article_title(frame, args)) table.insert(categories, are_image_auxiliary_values_used_for_no_image(args)) table.insert(categories, is_image_using_incorrect_syntax(args.image)) table.insert(categories, is_image_size_using_px(args.image_size)) table.insert(categories, are_values_links_only(args)) table.insert(categories, are_values_using_overall(args)) table.insert(categories, are_values_formatted(args)) table.insert(categories, are_values_linked(args)) table.insert(categories, has_flag_icon(args)) table.insert(categories, uses_list_markup(args)) table.insert(categories, are_dates_formatted_correctly(args.airdate or args.released)) table.insert(categories, is_italic_title_valid_value(args))

return table.concat(categories) end

--- Returns an instance if title qualifies. Also returns a maintenance category if conditions are met. --- --- The article's title is italicized if the series name is included in the article's title disambiguation. --- No italicization happens if one of the following conditions is met: --- --- - |italic_title= is set to "no". --- - The article's title does not use disambiguation. --- - No |series= value is set. --- - The article's disambiguation is not equal or does not include the series name. --- --- The page is added to a maintenance category if the title is italicized and there is already an ---, or  template. --- --- Infobox parameters checked: --- - |series= --- - |italic_title= --- --- Testing parameters: --- - |page_test= - a real Wikipedia page to read the content of the page. --- - |page_title_test= - the title of the page being checked. --- --- @param frame table --- @return string, string function p.italic_title(frame) local getArgs = require("Module:Arguments").getArgs local args = getArgs(frame)

local page_text if args.page_test then page_text = mw.title.new(args.page_test):getContent else page_text = mw.title.getCurrentTitle:getContent end

local maintenance_category = "" -- In case the page does not need to be italicized or can't be automatically done, a "no" value will disable both -- the italicization and the error handling. if args.italic_title == "no" then return "", maintenance_category end

local article_title = args.page_title_test if not args.page_title_test then article_title = mw.title.getCurrentTitle.text end

-- Check if the page already has an, or  template. local has_italic_dab, _ = string.find(page_text, "{{[Ii]talic dab") local has_italic_title, _ = string.find(page_text, "{{[Ii]talic title") local has_display_title, _ = string.find(page_text, "{{DISPLAYTITLE")

if has_italic_dab or has_italic_title or has_display_title then maintenance_category = maintenance_categories.manual_display_title end

local title_parts = get_title_parts(article_title)

-- The title is not italicized if the title does not use disambiguation or if the series parameter isn't used. if not title_parts.disambiguation or not args.series then return "", maintenance_category end

local series_name_escaped, series_name = get_series_name(args.series) series_name_escaped = get_name_with_br_fixes(series_name_escaped) series_name = get_name_with_br_fixes(series_name)

-- Check if the disambiguation equals the series name or if the series name can be found in the disambiguation. local italic_dab if title_parts.disambiguation == series_name then italic_dab = frame:expandTemplate{title = "Italic dab2"} elseif string.find(title_parts.disambiguation, series_name_escaped) then italic_dab = frame:expandTemplate{title = "Italic dab2", args = {string = series_name}} else return "", maintenance_category end

if args.page_title_test and italic_dab then italic_dab = "italic_dab" end return italic_dab, maintenance_category

end

--- Returns a formatted title string. --- --- @param rtitle string --- @return string local function create_title_with_rtitle_value(rtitle) local title_pattern = '"(.*)" and "(.*)"' if string.find(rtitle, title_pattern) then local episode1, episode2 = string.match(rtitle, title_pattern) local title_format = "\"%s\" and \"%s\"" return string.format(title_format, episode1, episode2) end

local title_pattern_br = '"(.*)" and%s?%s?"(.*)"' if string.find(rtitle, title_pattern_br) then local episode1, episode2 = string.match(rtitle, title_pattern_br) local title_format = "\"%s\" and \"%s\"" return string.format(title_format, episode1, episode2) end

return string.format("%s", rtitle) end

--- Returns the text used for the |above= field of the infobox. --- --- Infobox parameters checked: --- - |rtitle= --- - |title= --- - |series= --- --- Testing parameters: --- - |page_test= - a real Wikipedia page to read the content of the page. --- - |page_title_test= - the title of the page being checked. --- --- @param frame table --- @return string local function _above_title(frame, args) if args.rtitle then return create_title_with_rtitle_value(args.rtitle) end

local page if args.page_test then page = mw.title.new(args.page_test) else page = mw.title.getCurrentTitle end

local page_text = page:getContent

local article_title = args.page_title_test if not args.page_title_test then article_title = page.text end

local title_format = "\"%s\""

local correct_title = get_correct_title_value(page_text, args, false) if correct_title then return string.format(title_format, correct_title) end

local title_parts = get_title_parts(article_title)

local lowercase_title = get_lowercase_template_status(page_text, args, title_parts) if lowercase_title then return string.format(title_format, lowercase_title) end

local series_name_escaped, _ = get_series_name(args.series)

-- args.no_bold is used from IMDb episode so it requires this correction here also. if (args.italic_title and not args.rtitle) or args.no_bold then local title_modification = get_display_title_text(page_text, article_title)

if title_modification then if title_parts.disambiguation == series_name_escaped then local correct_title_title_parts = get_title_parts(title_modification) title_modification = correct_title_title_parts.title end return string.format(title_format, title_modification) end end

if args.title then return string.format(title_format, args.title) end

if not title_parts.disambiguation or (series_name_escaped ~= "" and (title_parts.disambiguation == series_name_escaped or string.find(title_parts.disambiguation, series_name_escaped))) or args.no_bold then return string.format(title_format, get_page_name_with_apostrophe_quotation_fixes(frame, title_parts.title)) end

return string.format(title_format, get_page_name_with_apostrophe_quotation_fixes(frame, article_title)) end

--- Returns the episode title from the article title, with textual fixes if needed. --- --- Used by and {{IMDb episode}} to automatically style the title without needing manual input. --- --- @param frame table --- @return string function p.above_title(frame) local getArgs = require("Module:Arguments").getArgs local args = getArgs(frame) local title = _above_title(frame, args) -- The title used by {{IMDb episode}} should not be in bold. if args.no_bold then title = string.gsub(title, "'''", "") end return title end

--- Returns a list of episodes link if not formatted, otherwise returns the text used for args.episode_list. --- --- Infobox parameters checked: --- - |episode_list= --- - |series= --- --- @param frame table --- @return string function p.episode_list(frame) local getArgs = require("Module:Arguments").getArgs ---@type table local args = getArgs(frame)

if args.episode_list then for _, v in pairs({"]]", "''"}) do			if string.find(args.episode_list, v) then return args.episode_list end end

if string.find(args.episode_list, "[Ss]toryline") then return "Storylines" end

return "List of episodes" end

if args.series then local series_name = get_series_link(args.series) local list_of_episodes = "List of " .. series_name .. " episodes"

if mw.title.new(list_of_episodes):getContent then return "List of episodes" end end end

--- Returns the relevant maintenance categories based on the {{Infobox television crossover episode}} values validated. --- --- @param frame table --- @return string function p.validate_values_crossover(frame) local getArgs = require("Module:Arguments").getArgs local args = getArgs(frame)

---@type string[] local categories = {} table.insert(categories, are_image_auxiliary_values_used_for_no_image(args)) table.insert(categories, is_image_using_incorrect_syntax(args.image)) table.insert(categories, is_image_size_using_px(args.image_size)) table.insert(categories, has_flag_icon(args)) table.insert(categories, are_dates_formatted_correctly(args.airdate_overall))

for i = 1, 5 do if not args["series" .. i] then break end

local nested_args = { series = args["series" .. i], episode = args["episode_no" .. i], season = args["season" .. i], airdate = args["airdate" .. i], prev = args["prev" .. i], next = args["next" .. i], episode_list = args["episode_list" .. i], }

table.insert(categories, are_values_links_only(nested_args)) table.insert(categories, are_values_using_overall(nested_args)) table.insert(categories, are_values_formatted(nested_args)) table.insert(categories, are_values_linked(nested_args)) table.insert(categories, are_dates_formatted_correctly(nested_args.airdate)) end

return table.concat(categories, "") end

return p