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) "'" → "'"
-- 2) ';' and ':' → "_-"
-- 3) '/' → "_-_"
-- 4) ' ' → '_'
local function normalize(name)
name = mw.ustring.gsub(name, "'", "'")
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