Actions

Module

Documentation for this module may be created at Module:CoverVariants/doc

-- Module:CoverVariants
-- Renders additional variant covers for an issue:
-- * Lettered variants: A..Z, then AA..AZ  (A = base: <base>.png)
-- * Optional printings after covers: (2nd_Printing)...(Nth_Printing)
-- * Caption shows "Cover X" or "2nd Printing" + transcluded credits from the File page
-- * Auto-enlarges wraparound images by aspect ratio and/or |wraparound=yes on the File page
-- * Outputs a <table class="variantcovers"> by default, or a <gallery> if layout=gallery
-- * Returns empty string if nothing is found

local p = {}

----------------------------------------------------------------
-- Normalization and helpers
----------------------------------------------------------------

-- Match your template replacements:
-- 1) "&#39;" → "'"
-- 2) ';' and ':' → "_-"
-- 3) '/' → "_-_"
-- 4) ' ' → '_'
local function normalize(name)
	name = mw.ustring.gsub(name, "&#39;", "'")
	name = mw.ustring.gsub(name, ";", "_-")
	name = mw.ustring.gsub(name, ":", "_-")
	name = mw.ustring.gsub(name, "/", "_-_")
	name = mw.ustring.gsub(name, " ", "_")
	return name
end

local function up(s) return (s or ""):upper() end

-- Lettered cover codes: start..Z, then AA..AZ (extendable later if needed)
local function coverCodes(startCode)
	local codes = {}
	local startB = string.byte(up(startCode or "E"))
	if startB < string.byte("A") then startB = string.byte("A") end
	if startB > string.byte("Z") then startB = string.byte("Z") end
	for b = startB, string.byte("Z") do
		table.insert(codes, string.char(b))
	end
	for b = string.byte("A"), string.byte("Z") do
		table.insert(codes, "A" .. string.char(b))
	end
	return codes
end

-- English ordinals (2nd, 3rd, 4th... inc. teen rule)
local function ordinal(n)
	local teen = n % 100
	if teen >= 11 and teen <= 13 then return tostring(n) .. "th" end
	local last = n % 10
	if last == 1 then return tostring(n) .. "st"
	elseif last == 2 then return tostring(n) .. "nd"
	elseif last == 3 then return tostring(n) .. "rd"
	else return tostring(n) .. "th" end
end

-- Printing codes: "2nd_Printing".."Nth_Printing"
local function printingCodes(startN, maxN)
	local out = {}
	startN = startN or 2
	maxN = maxN or 0
	for n = startN, maxN do
		table.insert(out, ordinal(n) .. "_Printing")
	end
	return out
end

----------------------------------------------------------------
-- Wraparound detection helpers
----------------------------------------------------------------

-- (Args are read inside render; these helpers reference upvalues set there)
local wrapWidthArg   -- string like "300px" (if provided)
local wrapMult       -- number multiplier for base width
local wrapRatio      -- number threshold (e.g., 1.7)
local wrapDetect     -- "yes"/"no"
local wrapScanParam  -- "yes"/"no"

local function pickWidthStr(normalWidthStr, isWrap)
	if not isWrap then return normalWidthStr end
	if wrapWidthArg ~= "" then return wrapWidthArg end
	local n = tonumber((normalWidthStr or ""):match("^(%d+)"))
	if n then return tostring(math.floor(n * wrapMult + 0.5)) .. "px" end
	return normalWidthStr
end

local function fileIsWraparoundByRatio(titleObj)
	if not titleObj or not titleObj.file then return false end
	local w, h = titleObj.file.width, titleObj.file.height
	if not w or not h or h == 0 then return false end
	return (w / h) >= wrapRatio
end

local function fileHasWrapParam(titleObj)
	if not titleObj then return false end
	local content = titleObj:getContent()
	if not content then return false end
	local s = content:lower()
	return s:match("|%s*wraparound%s*=%s*yes") ~= nil
end

----------------------------------------------------------------
-- Caption builder
----------------------------------------------------------------

-- transMode: "filetemplate" → {{File:...}} ; "filepage" → {{:File:...}}
local function buildCaption(frame, firstLineHtml, filename, transMode)
	local credits
	if transMode == "filetemplate" then
		credits = frame:preprocess(string.format("{{File:%s}}", filename))
	else
		credits = frame:preprocess(string.format("{{:File:%s}}", filename))
	end
	return (firstLineHtml or "") .. (credits or "")
end

----------------------------------------------------------------
-- Renderers
----------------------------------------------------------------

local function renderTable(itemsHtml, addHeader)
	local out = {}
	if addHeader then
		table.insert(out, '<h2 class="guide">Additional Variant Covers</h2>')
	end
	table.insert(out, '<table class="variantcovers">')
	for _, cell in ipairs(itemsHtml) do
		table.insert(out, "<tr><td valign=top>" .. cell .. "</td></tr>")
	end
	table.insert(out, "</table>")
	return table.concat(out, "\n")
end

-- For gallery layout, items should be lines like:
--   File:Name.png|200px|<center><b>Cover X</b></center>...credits...
local function renderGallery(itemsGalleryLines, addHeader)
	local out = {}
	if addHeader then
		table.insert(out, '<h2 class="guide">Additional Variant Covers</h2>')
	end
	table.insert(out, "<gallery>")
	for _, line in ipairs(itemsGalleryLines) do
		table.insert(out, line)
	end
	table.insert(out, "</gallery>")
	return table.concat(out, "\n")
end

----------------------------------------------------------------
-- Main entry
----------------------------------------------------------------

function p.render(frame)
    local parent = frame:getParent()
    local raw_parent = (parent and parent.args) or {}
    local raw_frame  = frame.args or {}

    -- merge: start with wrapper (frame) defaults, then overlay caller (parent)
    local args = {}
    for k,v in pairs(raw_frame)  do if v ~= "" then args[k] = v end end
    for k,v in pairs(raw_parent) do if v ~= "" then args[k] = v end end

	-- Base title (strip stray ".png" if someone passes it)
	local base = args.base and mw.text.trim(args.base) or mw.title.getCurrentTitle().text
	base = base:gsub("%.(png)$", "")
	base = normalize(base)

	-- Options
	local start        = up(args.start or "E")
	local baseWidth    = args.width or "150px"
	local layout       = (args.layout or "table"):lower()   -- "table" | "gallery"
	local addHeader    = not (args.noheader == "1" or args.noheader == "yes")
	local max_gap      = tonumber(args.max_gap or "2") or 2
	local transMode    = (args.transclude or "filetemplate"):lower() -- default to your {{File:...}} usage

	-- Printing options
	local includePrint = (args.include_printings or ""):lower()  -- "yes"/"1" to enable
	local maxPrinting  = tonumber(args.max_printing or "0") or 0
	local printStart   = tonumber(args.printing_start or "2") or 2

	-- Wrap detection options (set upvalues used by helpers)
	wrapWidthArg  = args.wrap_width or ""             -- "300px" or ""
	wrapMult      = tonumber(args.wrap_multiplier or "2") or 2
	wrapRatio     = tonumber(args.wrap_ratio or "1.7") or 1.7
	wrapDetect    = (args.wrap_detect or "yes"):lower()
	wrapScanParam = (args.wrap_param_scan or "yes"):lower()

	-- Build code lists
	local cover_list = coverCodes(start)                         -- A..Z, AA..AZ (start is configurable)
	local print_list = {}
	if includePrint == "yes" or includePrint == "1" then
		print_list = printingCodes(printStart, maxPrinting)     -- 2nd..Nth
	end

	local itemsForTable = {}
	local itemsForGallery = {}
	local seen_any = false
	local gap = 0

	-- Helper to add one item for both layouts
	local function addItem(filename, firstLineHtml, titleObj)
		-- wraparound detection
		local isWrap = false
		if wrapDetect ~= "no" then
			isWrap = fileIsWraparoundByRatio(titleObj)
		end
		if not isWrap and wrapScanParam ~= "no" then
			isWrap = fileHasWrapParam(titleObj)
		end

		local widthToUse = pickWidthStr(baseWidth, isWrap)
		local caption = buildCaption(frame, firstLineHtml, filename, transMode)

		-- Table layout: use thumb syntax
		local thumb = string.format("[[File:%s|thumb|center|%s|%s]]", filename, widthToUse, caption)
		table.insert(itemsForTable, thumb)

		-- Gallery layout: use gallery line syntax (no brackets)
		local galleryLine = string.format("File:%s|%s|%s", filename, widthToUse, caption)
		table.insert(itemsForGallery, galleryLine)
	end

	-- 1) Covers (always before printings)
	for _, code in ipairs(cover_list) do
		local filename
		if code == "A" then
			-- Cover A is the base file
			filename = string.format("%s.png", base)
		else
			-- Lettered variants use "(Cover X)"
			filename = string.format("%s (Cover %s).png", base, code)
		end

		local title = mw.title.new("File:" .. filename)
		if title and title.exists then
			seen_any, gap = true, 0
			local first = string.format("<center><b>Cover %s</b></center>", mw.text.nowiki(code))
			addItem(filename, first, title)
		else
			if seen_any then
				gap = gap + 1
				if gap > max_gap then break end
			end
		end
	end

	-- Reset the gap before checking printings so missing covers don't suppress printings
	gap = 0

	-- 2) Printings (always after covers)
	for _, code in ipairs(print_list) do
		local filename = string.format("%s (%s).png", base, code) -- e.g., (2nd_Printing)
		local title = mw.title.new("File:" .. filename)
		if title and title.exists then
			seen_any, gap = true, 0
			local label = code:gsub("_", " ") -- "2nd Printing"
			local first = string.format("<center><b>%s</b></center>", mw.text.nowiki(label))
			addItem(filename, first, title)
		else
			if seen_any then
				gap = gap + 1
				if gap > max_gap then break end
			end
		end
	end

	-- Nothing found → render nothing
	if #itemsForTable == 0 then
		return ""
	end

	-- Render chosen layout
	if layout == "gallery" then
		return renderGallery(itemsForGallery, addHeader)
	else
		return renderTable(itemsForTable, addHeader)
	end
end

return p