Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:ArchiveLink

From the only original and legitimate Battlestar Wiki: the free-as-in-beer, non-corporate, open-content encyclopedia, analytical reference, and episode guide on all things Battlestar Galactica. Accept neither subpar substitutes nor subpar clones.

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

--[[
================================================================================
 Module:ArchiveLink  —  BattlestarWiki ArchiveLink extension
================================================================================

 Public entry points  ({{#invoke:ArchiveLink|functionName|...}}):

   linkStatus     inline status badge for a single URL
   statusIcon     just the emoji icon, no label
   sourcesLink    wikilink to the Sources: archive page
   captureStatus  "✅ Archived" / "❌ Not archived" indicator
   urlHealth      compact one-liner: icon + status + archive note
   linkTable      wikitable of all tracked URLs on a page

 All reads go through the DB cache (mw.ext.ArchiveLink PHP bridge).
 No live HTTP checks happen at render time.
================================================================================
--]]

local ArchiveLink = {}

-- ---------------------------------------------------------------------------
-- Bridge accessor
-- ---------------------------------------------------------------------------
local function bridge()
	return mw.ext and mw.ext.ArchiveLink or nil
end

-- ---------------------------------------------------------------------------
-- Constants
-- ---------------------------------------------------------------------------
local ICON  = { live='🟢', dead='🔴', redirected='🟡', unknown='⚪' }
local CLS   = { live='archivelink-status--live', dead='archivelink-status--dead',
                redirected='archivelink-status--redirect', unknown='archivelink-status--unknown' }
local LABEL = { live='Live', dead='Dead', redirected='Redirect', unknown='Unknown' }
local CICON = { live='📦', wayback='📅' }

-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
local function trim(s)   return s and s:match('^%s*(.-)%s*$') or '' end
local function esc(s)
	s = tostring(s or '')
	return s:gsub('&','&amp;'):gsub('<','&lt;'):gsub('>','&gt;')
		     :gsub('%[%[','&#91;&#91;'):gsub('{{','&#123;&#123;')
end

local function param(args, ...)
	for _,k in ipairs{...} do
		local v = trim(args[k] or '')
		if v ~= '' then return v end
	end
	return nil
end

local MONTHS = {'January','February','March','April','May','June',
                'July','August','September','October','November','December'}

local function fmtDate(ts)
	if not ts or ts == '' or ts == false then return '' end
	local y,m,d = tostring(ts):match('^(%d%d%d%d)-(%d%d)-(%d%d)')
	if not y then y,m,d = tostring(ts):match('^(%d%d%d%d)(%d%d)(%d%d)') end
	if not y then return tostring(ts) end
	return tostring(tonumber(d))..' '..(MONTHS[tonumber(m)] or m)..' '..y
end

local function srcLink(slug, label)
	if not slug or slug == '' then return '' end
	return '[[Sources:'..slug..'|'..(label or ('Sources:'..slug))..']]'
end

local function getStatus(url)
	local b = bridge()
	if not b then return {status='unknown'} end
	return b.getStatus(url) or {status='unknown'}
end

local function getCapture(url)
	local b = bridge()
	if not b then return nil end
	return b.getCapture(url)
end

-- ---------------------------------------------------------------------------
-- 1. linkStatus
-- {{#invoke:ArchiveLink|linkStatus|url=...|label=...|noicon=1|nolink=1}}
-- ---------------------------------------------------------------------------
function ArchiveLink.linkStatus(frame)
	local args    = (frame:getParent() and frame:getParent().args) or frame.args
	local url     = trim(args.url or args[1] or '')
	if url == '' then return '<span class="error">ArchiveLink: no URL</span>' end

	local showIcon = not param(args,'noicon')
	local showLink = not param(args,'nolink')
	local custLbl  = param(args,'label')

	local row    = getStatus(url)
	local status = row.status or 'unknown'
	local icon   = ICON[status]  or '⚪'
	local cls    = CLS[status]   or CLS.unknown
	local lbl    = custLbl or LABEL[status] or status

	local codeSfx = ''
	if row.http_code and row.http_code ~= false and row.http_code ~= 0 then
		codeSfx = '&#8201;<small>('..tostring(row.http_code)..')</small>'
	end

	local badge = '<span class="archivelink-status-badge '..cls..'">'
		..(showIcon and (icon..'&#8201;') or '')
		..esc(lbl)..codeSfx..'</span>'

	local note = ''
	if showLink then
		local cap = getCapture(url)
		if cap and cap.sources_page then
			local ci   = CICON[cap.capture_type] or '📦'
			local dt   = fmtDate(cap.captured_at or '')
			local link = srcLink(cap.sources_page)
			note = ' <span class="archivelink-archived-note">'..ci..'&#160;'..link
				..(dt~='' and (' <small>('..dt..')</small>') or '')..'</span>'
		elseif status == 'dead' then
			if row.wayback_available then
				note = ' <span class="archivelink-wayback-note">📅&#160;<small>available on Wayback</small></span>'
			else
				note = ' <span class="archivelink-unarchived-note"><small>(not archived)</small></span>'
			end
		end
	end

	return frame:preprocess(badge..note)
end

-- ---------------------------------------------------------------------------
-- 2. statusIcon  — just the emoji
-- {{#invoke:ArchiveLink|statusIcon|url=...}}
-- ---------------------------------------------------------------------------
function ArchiveLink.statusIcon(frame)
	local args = (frame:getParent() and frame:getParent().args) or frame.args
	local url  = trim(args.url or args[1] or '')
	if url == '' then return '⚪' end
	return ICON[getStatus(url).status] or '⚪'
end

-- ---------------------------------------------------------------------------
-- 3. sourcesLink  — wikilink to Sources: page
-- {{#invoke:ArchiveLink|sourcesLink|url=...|label=...}}
-- ---------------------------------------------------------------------------
function ArchiveLink.sourcesLink(frame)
	local args  = (frame:getParent() and frame:getParent().args) or frame.args
	local url   = trim(args.url or args[1] or '')
	local label = param(args,'label')
	if url == '' then return '' end
	local cap = getCapture(url)
	if not cap or not cap.sources_page then return '' end
	return frame:preprocess(srcLink(cap.sources_page, label))
end

-- ---------------------------------------------------------------------------
-- 4. captureStatus  — archived / not archived
-- {{#invoke:ArchiveLink|captureStatus|url=...}}
-- ---------------------------------------------------------------------------
function ArchiveLink.captureStatus(frame)
	local args = (frame:getParent() and frame:getParent().args) or frame.args
	local url  = trim(args.url or args[1] or '')
	if url == '' then return '<span class="error">ArchiveLink: no URL</span>' end

	local cap = getCapture(url)
	if cap and cap.sources_page then
		local ci   = CICON[cap.capture_type] or '📦'
		local dt   = fmtDate(cap.captured_at or '')
		local link = srcLink(cap.sources_page,'view')
		local when = dt~='' and (' '..dt) or ''
		return frame:preprocess(
			'<span class="archivelink-capture-status archivelink-capture-status--yes">'
			..'✅&#160;Archived'..when..'&#160;'..ci..'&#160;('..link..')</span>'
		)
	else
		local row = getStatus(url)
		local wb  = row.wayback_available
			and '&#160;<small>(Wayback available)</small>' or ''
		return '<span class="archivelink-capture-status archivelink-capture-status--no">'
			..'❌&#160;Not archived'..wb..'</span>'
	end
end

-- ---------------------------------------------------------------------------
-- 5. urlHealth  — compact one-liner
-- {{#invoke:ArchiveLink|urlHealth|url=...}}
-- ---------------------------------------------------------------------------
function ArchiveLink.urlHealth(frame)
	local args   = (frame:getParent() and frame:getParent().args) or frame.args
	local url    = trim(args.url or args[1] or '')
	if url == '' then return '' end

	local row    = getStatus(url)
	local status = row.status or 'unknown'
	local parts  = { (ICON[status] or '⚪')..'&#160;'..(LABEL[status] or status) }

	local cap = getCapture(url)
	if cap and cap.sources_page then
		local ci = CICON[cap.capture_type] or '📦'
		table.insert(parts, ci..'&#160;'..srcLink(cap.sources_page,'archived'))
	elseif status == 'dead' or status == 'unknown' then
		if row.wayback_available then
			table.insert(parts, '📅&#160;<small>Wayback available</small>')
		else
			table.insert(parts, '<small>not archived</small>')
		end
	end

	return frame:preprocess(
		'<span class="archivelink-url-health">'
		..table.concat(parts,'&#160;·&#160;')..'</span>'
	)
end

-- ---------------------------------------------------------------------------
-- 6. linkTable  — full wikitable of all URLs on a page
-- {{#invoke:ArchiveLink|linkTable|page=...|compact=1|caption=...}}
-- ---------------------------------------------------------------------------
function ArchiveLink.linkTable(frame)
	local args      = (frame:getParent() and frame:getParent().args) or frame.args
	local pageTitle = param(args,'page') or mw.title.getCurrentTitle().prefixedText
	local compact   = param(args,'compact') ~= nil
	local caption   = param(args,'caption') or ''

	local b = bridge()
	if not b then
		return '<span class="error">ArchiveLink extension not loaded</span>'
	end

	local urls = b.getPageLinks(pageTitle)
	if not urls or #urls == 0 then
		return '<span class="archivelink-table-empty">No tracked external links on this page.</span>'
	end

	-- Header
	local hdrs = compact
		and {'URL','Status','Archived'}
		or  {'URL','Status','Last checked','Archived'}

	local lines = {
		'{| class="wikitable archivelink-link-table" style="width:100%"',
		caption~='' and ('|+ '..esc(caption)) or nil,
		'|-',
		'! '..table.concat(hdrs,' !! '),
	}
	-- remove nil
	local clean = {}
	for _,v in ipairs(lines) do if v then table.insert(clean,v) end end
	lines = clean

	for _, url in ipairs(urls) do
		local row    = getStatus(url)
		local cap    = getCapture(url)
		local status = (row and row.status) or 'unknown'
		local icon   = ICON[status]  or '⚪'
		local cls    = CLS[status]   or CLS.unknown

		-- URL cell
		local disp = url
		if #disp > 70 then disp = disp:sub(1,67)..'…' end
		local urlCell = '['..url..' <code>'..esc(disp)..'</code>]'

		-- Status cell
		local stCell = '<span class="archivelink-status-badge '..cls..'">'
			..icon..'&#160;'..(LABEL[status] or status)..'</span>'
		if row and row.http_code and row.http_code ~= false then
			stCell = stCell..' <small>('..tostring(row.http_code)..')</small>'
		end

		-- Archived cell
		local archCell
		if cap and cap.sources_page then
			local ci = CICON[cap.capture_type] or '📦'
			local dt = fmtDate(cap.captured_at or '')
			archCell = '✅&#160;'..ci..'&#160;'..srcLink(cap.sources_page)
				..(dt~='' and ('<br><small>'..dt..'</small>') or '')
		elseif row and row.wayback_available then
			archCell = '📅&#160;<small>Wayback only</small>'
		else
			archCell = '❌&#160;<small>—</small>'
		end

		-- Checked cell
		local chkCell = ''
		if not compact then
			if row and row.status_checked and row.status_checked ~= false then
				chkCell = '<small>'..fmtDate(row.status_checked)..'</small>'
			else
				chkCell = '<small>never</small>'
			end
		end

		local cells = compact
			and {urlCell, stCell, archCell}
			or  {urlCell, stCell, chkCell, archCell}

		table.insert(lines, '|-')
		table.insert(lines, '| '..table.concat(cells,' || '))
	end

	table.insert(lines, '|}')
	return frame:preprocess(table.concat(lines,'\n'))
end

-- ---------------------------------------------------------------------------
return ArchiveLink