<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://en.battlestarwiki.org/Module:Portal/history?feed=atom</id>
	<title>Module:Portal - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://en.battlestarwiki.org/Module:Portal/history?feed=atom"/>
	<link rel="alternate" type="text/html" href="https://en.battlestarwiki.org/Module:Portal/history"/>
	<updated>2026-04-13T16:47:47Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.45.1</generator>
	<entry>
		<id>https://en.battlestarwiki.org/w/index.php?title=Module:Portal&amp;diff=262259&amp;oldid=prev</id>
		<title>Joe Beaudoin Jr.: Created page with &quot;-- Module:Portal -- Provides dynamic portal widgets for BattlestarWiki: --   stats bar, newest page, random image, colonial calendar -- -- Usage (from Template:Portal/Layout): --   {{#invoke:Portal|stats|category=Characters (RDM)|episodes=73|characters=62|seasons=4|year=2003}} --   {{#invoke:Portal|newestPage|category=Characters (RDM)}} --   {{#invoke:Portal|randomImage|category=Images (RDM)}} --   {{#invoke:Portal|colonialCalendar|category=RDM}}  local p = {} local mwTi...&quot;</title>
		<link rel="alternate" type="text/html" href="https://en.battlestarwiki.org/w/index.php?title=Module:Portal&amp;diff=262259&amp;oldid=prev"/>
		<updated>2026-04-13T13:57:43Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;-- Module:Portal -- Provides dynamic portal widgets for BattlestarWiki: --   stats bar, newest page, random image, colonial calendar -- -- Usage (from Template:Portal/Layout): --   {{#invoke:Portal|stats|category=Characters (RDM)|episodes=73|characters=62|seasons=4|year=2003}} --   {{#invoke:Portal|newestPage|category=Characters (RDM)}} --   {{#invoke:Portal|randomImage|category=Images (RDM)}} --   {{#invoke:Portal|colonialCalendar|category=RDM}}  local p = {} local mwTi...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- Module:Portal&lt;br /&gt;
-- Provides dynamic portal widgets for BattlestarWiki:&lt;br /&gt;
--   stats bar, newest page, random image, colonial calendar&lt;br /&gt;
--&lt;br /&gt;
-- Usage (from Template:Portal/Layout):&lt;br /&gt;
--   {{#invoke:Portal|stats|category=Characters (RDM)|episodes=73|characters=62|seasons=4|year=2003}}&lt;br /&gt;
--   {{#invoke:Portal|newestPage|category=Characters (RDM)}}&lt;br /&gt;
--   {{#invoke:Portal|randomImage|category=Images (RDM)}}&lt;br /&gt;
--   {{#invoke:Portal|colonialCalendar|category=RDM}}&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
local mwTitle  = mw.title&lt;br /&gt;
local mwHtml   = mw.html&lt;br /&gt;
local mwSite   = mw.site&lt;br /&gt;
local mwUstring = mw.ustring&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
-- Helpers&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
--- Trim whitespace from both ends of a string.&lt;br /&gt;
local function trim(s)&lt;br /&gt;
    return s and mwUstring.match(s, &amp;#039;^%s*(.-)%s*$&amp;#039;) or &amp;#039;&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Return the value of an arg, trimmed, or a default.&lt;br /&gt;
local function arg(args, key, default)&lt;br /&gt;
    local v = args[key]&lt;br /&gt;
    return (v and trim(v) ~= &amp;#039;&amp;#039;) and trim(v) or default&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Build a CSS class string, filtering nils.&lt;br /&gt;
local function classes(...)&lt;br /&gt;
    local t = {}&lt;br /&gt;
    for _, c in ipairs({...}) do&lt;br /&gt;
        if c then t[#t+1] = c end&lt;br /&gt;
    end&lt;br /&gt;
    return table.concat(t, &amp;#039; &amp;#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Get the count of members in a MediaWiki category via mw.title magic.&lt;br /&gt;
--- Returns nil if category doesn&amp;#039;t exist.&lt;br /&gt;
local function getCategoryCount(catName)&lt;br /&gt;
    local t = mwTitle.new(&amp;#039;Category:&amp;#039; .. catName)&lt;br /&gt;
    if not t then return nil end&lt;br /&gt;
    -- pagesInCategory returns a table {all=N, files=N, subcats=N, pages=N}&lt;br /&gt;
    -- Needs to be called through the expensive-function path; we use the&lt;br /&gt;
    -- parser function as a safer fallback.&lt;br /&gt;
    local ok, count = pcall(function()&lt;br /&gt;
        return t.pagesInCategory&lt;br /&gt;
    end)&lt;br /&gt;
    if ok and count then return count end&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Fetch the most recently created page in a category.&lt;br /&gt;
--- Returns {title, excerpt, timestamp} or nil.&lt;br /&gt;
--- NOTE: mw.title API doesn&amp;#039;t support category member queries directly;&lt;br /&gt;
--- we use mw.ext.cargo if available, otherwise return nil so the template&lt;br /&gt;
--- can show a static fallback. In production this should be backed by a&lt;br /&gt;
--- Cargo table or a Lua-side API call cached in frame:callParserFunction.&lt;br /&gt;
local function getNewestInCategory(catName)&lt;br /&gt;
    -- Cargo query (requires Cargo extension + populated tables)&lt;br /&gt;
    if mw.ext and mw.ext.cargo then&lt;br /&gt;
        local ok, rows = pcall(function()&lt;br /&gt;
            return mw.ext.cargo.query(&lt;br /&gt;
                &amp;#039;Pages&amp;#039;,&lt;br /&gt;
                &amp;#039;_pageName,_pageTitle,_creationDate&amp;#039;,&lt;br /&gt;
                {&lt;br /&gt;
                    where  = &amp;#039;_category = &amp;quot;&amp;#039; .. catName .. &amp;#039;&amp;quot;&amp;#039;,&lt;br /&gt;
                    orderBy = &amp;#039;_creationDate DESC&amp;#039;,&lt;br /&gt;
                    limit  = 1&lt;br /&gt;
                }&lt;br /&gt;
            )&lt;br /&gt;
        end)&lt;br /&gt;
        if ok and rows and rows[1] then&lt;br /&gt;
            local r = rows[1]&lt;br /&gt;
            return {&lt;br /&gt;
                title     = r._pageTitle or r._pageName,&lt;br /&gt;
                pageName  = r._pageName,&lt;br /&gt;
                timestamp = r._creationDate&lt;br /&gt;
            }&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    -- Fallback: nil — template renders a static placeholder message&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
--- Return a random file from a category.&lt;br /&gt;
--- In a live wiki this uses mw.title combined with randomness seeded by&lt;br /&gt;
--- the current date so the image changes daily rather than on every purge.&lt;br /&gt;
--- Returns a file title string or nil.&lt;br /&gt;
local function getDailyRandomImage(catName)&lt;br /&gt;
    -- Seed by date so the same image shows all day (stable across purges)&lt;br /&gt;
    local dateStr = mw.getCurrentFrame():callParserFunction(&amp;#039;#time&amp;#039;, &amp;#039;Ymd&amp;#039;)&lt;br /&gt;
    local seed    = tonumber(dateStr) or 20240101&lt;br /&gt;
    math.randomseed(seed)&lt;br /&gt;
&lt;br /&gt;
    -- We can&amp;#039;t enumerate category members purely in Lua/Scribunto, so we&lt;br /&gt;
    -- depend on either:&lt;br /&gt;
    --   (a) A companion sub-page Portal:XXX/ImageList  (one filename per line)&lt;br /&gt;
    --   (b) The RandomInCategory extension&lt;br /&gt;
    --   (c) A Cargo table of files tagged with the category&lt;br /&gt;
    --&lt;br /&gt;
    -- Strategy: try sub-page first, then RandomInCategory magic word, else nil.&lt;br /&gt;
&lt;br /&gt;
    -- (a) Sub-page list  e.g. &amp;quot;Portal:Battlestar Galactica (RDM)/ImageList&amp;quot;&lt;br /&gt;
    --     Each line: File:Foo.jpg | optional caption&lt;br /&gt;
    local listPage = mwTitle.getCurrentTitle().prefixedText .. &amp;#039;/ImageList&amp;#039;&lt;br /&gt;
    local listTitle = mwTitle.new(listPage)&lt;br /&gt;
    if listTitle and listTitle.exists then&lt;br /&gt;
        local content = listTitle:getContent()&lt;br /&gt;
        if content then&lt;br /&gt;
            local files = {}&lt;br /&gt;
            for line in content:gmatch(&amp;#039;[^\n]+&amp;#039;) do&lt;br /&gt;
                local fname = trim(line:match(&amp;#039;^([^|]+)&amp;#039;))&lt;br /&gt;
                if fname ~= &amp;#039;&amp;#039; then&lt;br /&gt;
                    files[#files+1] = fname&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            if #files &amp;gt; 0 then&lt;br /&gt;
                return files[math.random(#files)]&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- (b) Nothing found — return nil; template uses a placeholder&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
-- Colonial Calendar data&lt;br /&gt;
-- Keyed as &amp;quot;M-D&amp;quot; (no zero-padding) → list of {year, text, link}&lt;br /&gt;
-- Covers real-world broadcast dates and in-universe events.&lt;br /&gt;
-- Editors can extend this table or replace it with a data module.&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local CALENDAR = {&lt;br /&gt;
    [&amp;#039;1-14&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2005&amp;#039;, text=&amp;#039;[[33 (episode)|33]] premieres on Sci Fi Channel — the first regular episode of the Re-imagined Series&amp;#039;, link=&amp;#039;33 (episode)&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;1-21&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2005&amp;#039;, text=&amp;#039;[[Water (episode)|Water]] airs on Sci Fi Channel&amp;#039;, link=&amp;#039;Water (episode)&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;3-4&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2005&amp;#039;, text=&amp;#039;[[Tigh Me Up, Tigh Me Down]] airs on Sky One (UK)&amp;#039;, link=&amp;#039;Tigh Me Up, Tigh Me Down&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;3-18&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2005&amp;#039;, text=&amp;#039;[[Colonial Day]] airs on Sky One&amp;#039;, link=&amp;#039;Colonial Day&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;3-20&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2009&amp;#039;, text=&amp;#039;[[Daybreak, Part II|Daybreak]] finale airs — end of the Re-imagined Series&amp;#039;, link=&amp;#039;Daybreak, Part II&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;4-1&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2005&amp;#039;, text=&amp;#039;[[Kobol\&amp;#039;s Last Gleaming, Part II]] airs — Season 1 finale&amp;#039;, link=&amp;quot;Kobol&amp;#039;s Last Gleaming, Part II&amp;quot; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;4-12&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2007&amp;#039;, text=&amp;#039;[[The Woman King]] airs on Sci Fi Channel&amp;#039;, link=&amp;#039;The Woman King&amp;#039; },&lt;br /&gt;
        { year=&amp;#039;2009&amp;#039;, text=&amp;#039;[[Islanded in a Stream of Stars]] airs — three episodes remain&amp;#039;, link=&amp;#039;Islanded in a Stream of Stars&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;6-3&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2005&amp;#039;, text=&amp;#039;[[Scattered]] airs — Season 2 premiere&amp;#039;, link=&amp;#039;Scattered&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;9-22&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2006&amp;#039;, text=&amp;#039;[[Occupation]]/[[Precipice]] two-hour Season 3 premiere airs&amp;#039;, link=&amp;#039;Occupation&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;10-6&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2006&amp;#039;, text=&amp;#039;[[Exodus, Part II]] airs — fan-favourite battle sequence&amp;#039;, link=&amp;#039;Exodus, Part II&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;#039;12-8&amp;#039;] = {&lt;br /&gt;
        { year=&amp;#039;2003&amp;#039;, text=&amp;#039;[[Miniseries, Night 1|BSG Miniseries]] premieres on Sci Fi Channel&amp;#039;, link=&amp;#039;Miniseries, Night 1&amp;#039; },&lt;br /&gt;
    },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- Build lookup normalised to &amp;quot;M-D&amp;quot; (strips leading zeros)&lt;br /&gt;
local function todayKey()&lt;br /&gt;
    local m = tonumber(mw.getCurrentFrame():callParserFunction(&amp;#039;#time&amp;#039;, &amp;#039;n&amp;#039;))&lt;br /&gt;
    local d = tonumber(mw.getCurrentFrame():callParserFunction(&amp;#039;#time&amp;#039;, &amp;#039;j&amp;#039;))&lt;br /&gt;
    return m .. &amp;#039;-&amp;#039; .. d&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
-- Public: stats&lt;br /&gt;
-- Args: category, episodes, characters, seasons, year&lt;br /&gt;
-- Any missing numeric arg is omitted from the bar.&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
function p.stats(frame)&lt;br /&gt;
    local args = frame:getParent().args&lt;br /&gt;
    local cat  = arg(args, &amp;#039;category&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    -- Collect stat items: {num, label}&lt;br /&gt;
    local items = {}&lt;br /&gt;
&lt;br /&gt;
    -- Article count from category&lt;br /&gt;
    if cat ~= &amp;#039;&amp;#039; then&lt;br /&gt;
        local n = getCategoryCount(cat)&lt;br /&gt;
        if n then&lt;br /&gt;
            items[#items+1] = { num = tostring(n), label = &amp;#039;articles&amp;#039; }&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Manual counts passed as template args&lt;br /&gt;
    local function addStat(key, label)&lt;br /&gt;
        local v = arg(args, key, nil)&lt;br /&gt;
        if v then items[#items+1] = { num = v, label = label } end&lt;br /&gt;
    end&lt;br /&gt;
    addStat(&amp;#039;episodes&amp;#039;,   &amp;#039;episodes&amp;#039;)&lt;br /&gt;
    addStat(&amp;#039;characters&amp;#039;, &amp;#039;characters&amp;#039;)&lt;br /&gt;
    addStat(&amp;#039;seasons&amp;#039;,    &amp;#039;seasons&amp;#039;)&lt;br /&gt;
    addStat(&amp;#039;year&amp;#039;,       &amp;#039;first aired&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    if #items == 0 then return &amp;#039;&amp;#039; end&lt;br /&gt;
&lt;br /&gt;
    local root = mwHtml.create(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-stats-bar&amp;#039;)&lt;br /&gt;
    for _, item in ipairs(items) do&lt;br /&gt;
        root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-stat&amp;#039;)&lt;br /&gt;
            :tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-stat-num&amp;#039;):wikitext(item.num):done()&lt;br /&gt;
            :tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-stat-label&amp;#039;):wikitext(item.label):done()&lt;br /&gt;
    end&lt;br /&gt;
    return tostring(root)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
-- Public: newestPage&lt;br /&gt;
-- Args: category&lt;br /&gt;
-- Returns an HTML block for the newest article widget body.&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
function p.newestPage(frame)&lt;br /&gt;
    local args    = frame:getParent().args&lt;br /&gt;
    local cat     = arg(args, &amp;#039;category&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
    local fallback = arg(args, &amp;#039;fallback&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    local newest = (cat ~= &amp;#039;&amp;#039;) and getNewestInCategory(cat) or nil&lt;br /&gt;
&lt;br /&gt;
    local root = mwHtml.create(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-newest&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    if newest then&lt;br /&gt;
        root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-newest-label&amp;#039;)&lt;br /&gt;
            :wikitext(&amp;#039;Newest · Category:&amp;#039; .. cat)&lt;br /&gt;
        root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-newest-title&amp;#039;)&lt;br /&gt;
            :wikitext(&amp;#039;[[&amp;#039; .. newest.pageName .. &amp;#039;|&amp;#039; .. newest.title .. &amp;#039;]]&amp;#039;)&lt;br /&gt;
        if newest.timestamp then&lt;br /&gt;
            root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-newest-meta&amp;#039;)&lt;br /&gt;
                :wikitext(&amp;#039;Created &amp;#039; .. newest.timestamp)&lt;br /&gt;
        end&lt;br /&gt;
    elseif fallback ~= &amp;#039;&amp;#039; then&lt;br /&gt;
        -- Static fallback provided by the template editor&lt;br /&gt;
        root:wikitext(fallback)&lt;br /&gt;
    else&lt;br /&gt;
        root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-newest-placeholder&amp;#039;)&lt;br /&gt;
            :wikitext(&amp;#039;&amp;lt;small&amp;gt;[[Special:NewPages|Browse new articles]]&amp;#039;&lt;br /&gt;
                .. (cat ~= &amp;#039;&amp;#039; and &amp;#039; · filtered to [[:Category:&amp;#039; .. cat .. &amp;#039;]]&amp;#039; or &amp;#039;&amp;#039;)&lt;br /&gt;
                .. &amp;#039;&amp;lt;/small&amp;gt;&amp;#039;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return frame:preprocess(tostring(root))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
-- Public: randomImage&lt;br /&gt;
-- Args: category  (used to find the ImageList sub-page)&lt;br /&gt;
-- Returns wikitext for a [[File:...]] transclusion, or placeholder.&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
function p.randomImage(frame)&lt;br /&gt;
    local args = frame:getParent().args&lt;br /&gt;
    local cat  = arg(args, &amp;#039;category&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
    local size = arg(args, &amp;#039;size&amp;#039;, &amp;#039;400px&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    local file = getDailyRandomImage(cat)&lt;br /&gt;
&lt;br /&gt;
    if file then&lt;br /&gt;
        -- Normalise: strip leading &amp;quot;File:&amp;quot; if present&lt;br /&gt;
        file = file:gsub(&amp;#039;^[Ff]ile:&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
        return &amp;#039;[[File:&amp;#039; .. file .. &amp;#039;|&amp;#039; .. size .. &amp;#039;|portal random image]]&amp;#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Placeholder div so the layout doesn&amp;#039;t collapse&lt;br /&gt;
    local ph = mwHtml.create(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-image-placeholder&amp;#039;)&lt;br /&gt;
        :wikitext(&amp;#039;No image list found. Create [[&amp;#039; ..&lt;br /&gt;
            mwTitle.getCurrentTitle().prefixedText .. &amp;#039;/ImageList]] to enable the random image slider.&amp;#039;)&lt;br /&gt;
    return tostring(ph)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
-- Public: colonialCalendar&lt;br /&gt;
-- Returns an HTML fragment listing today&amp;#039;s BSG events.&lt;br /&gt;
-- Args: category (used to filter if needed; currently shows all entries)&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
function p.colonialCalendar(frame)&lt;br /&gt;
    local key    = todayKey()&lt;br /&gt;
    local events = CALENDAR[key]&lt;br /&gt;
&lt;br /&gt;
    local root = mwHtml.create(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-calendar&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    -- Date display&lt;br /&gt;
    local dateDisplay = mw.getCurrentFrame():callParserFunction(&amp;#039;#time&amp;#039;, &amp;#039;F j&amp;#039;)&lt;br /&gt;
    root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-calendar-date&amp;#039;):wikitext(dateDisplay)&lt;br /&gt;
&lt;br /&gt;
    local sub = root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-calendar-sub&amp;#039;)&lt;br /&gt;
        :wikitext(&amp;#039;Real-world BSG broadcast history&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    if events and #events &amp;gt; 0 then&lt;br /&gt;
        local list = root:tag(&amp;#039;ul&amp;#039;):addClass(&amp;#039;portal-calendar-events&amp;#039;)&lt;br /&gt;
        for _, ev in ipairs(events) do&lt;br /&gt;
            list:tag(&amp;#039;li&amp;#039;):addClass(&amp;#039;portal-calendar-event&amp;#039;)&lt;br /&gt;
                :tag(&amp;#039;span&amp;#039;):addClass(&amp;#039;portal-calendar-year&amp;#039;):wikitext(ev.year):done()&lt;br /&gt;
                :wikitext(&amp;#039; — &amp;#039;)&lt;br /&gt;
                :tag(&amp;#039;span&amp;#039;):wikitext(ev.text)&lt;br /&gt;
        end&lt;br /&gt;
    else&lt;br /&gt;
        root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-calendar-none&amp;#039;)&lt;br /&gt;
            :wikitext(&amp;#039;&amp;lt;small&amp;gt;No recorded BSG events on this date.&amp;lt;/small&amp;gt;&amp;#039;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return frame:preprocess(tostring(root))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
-- Public: relatedPortals&lt;br /&gt;
-- Args: type = &amp;#039;series&amp;#039; | &amp;#039;topic&amp;#039;&lt;br /&gt;
-- Renders the related-portals tile grid appropriate for the portal type.&lt;br /&gt;
------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local SERIES_PORTALS = {&lt;br /&gt;
    { title = &amp;#039;Portal:Battlestar Galactica (TOS)&amp;#039;, label = &amp;#039;Original Series&amp;#039;,    sub = &amp;#039;TOS · 1978&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Galactica 1980&amp;#039;,             label = &amp;#039;Galactica 1980&amp;#039;,     sub = &amp;#039;1980&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Battlestar Galactica (RDM)&amp;#039;, label = &amp;#039;Re-imagined Series&amp;#039;, sub = &amp;#039;RDM · 2003–2009&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Caprica&amp;#039;,                    label = &amp;#039;Caprica&amp;#039;,            sub = &amp;#039;Prequel · 2010–2011&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Blood and Chrome&amp;#039;,           label = &amp;#039;Blood &amp;amp;amp; Chrome&amp;#039;, sub = &amp;#039;Prequel · 2012&amp;#039; },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
local TOPIC_PORTALS = {&lt;br /&gt;
    { title = &amp;#039;Portal:Characters&amp;#039;, label = &amp;#039;Characters&amp;#039;, sub = &amp;#039;All series&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Ships&amp;#039;,      label = &amp;#039;Ships&amp;#039;,      sub = &amp;#039;All series&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Cylons&amp;#039;,     label = &amp;#039;Cylons&amp;#039;,     sub = &amp;#039;All series&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Episodes&amp;#039;,   label = &amp;#039;Episodes&amp;#039;,   sub = &amp;#039;All series&amp;#039; },&lt;br /&gt;
    { title = &amp;#039;Portal:Merchandise&amp;#039;,label = &amp;#039;Merchandise&amp;#039;,sub = &amp;#039;All series&amp;#039; },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function p.relatedPortals(frame)&lt;br /&gt;
    local args    = frame:getParent().args&lt;br /&gt;
    local pType   = arg(args, &amp;#039;type&amp;#039;, &amp;#039;series&amp;#039;)  -- &amp;#039;series&amp;#039; or &amp;#039;topic&amp;#039;&lt;br /&gt;
    local exclude = arg(args, &amp;#039;exclude&amp;#039;, &amp;#039;&amp;#039;)      -- current portal title to skip&lt;br /&gt;
&lt;br /&gt;
    local list = (pType == &amp;#039;topic&amp;#039;) and TOPIC_PORTALS or (SERIES_PORTALS)&lt;br /&gt;
    -- Merge both lists for cross-linking&lt;br /&gt;
    local combined = {}&lt;br /&gt;
    for _, v in ipairs(SERIES_PORTALS) do combined[#combined+1] = v end&lt;br /&gt;
    for _, v in ipairs(TOPIC_PORTALS)  do combined[#combined+1] = v end&lt;br /&gt;
&lt;br /&gt;
    local root = mwHtml.create(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-related-grid&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
    for _, portal in ipairs(combined) do&lt;br /&gt;
        if portal.title ~= exclude then&lt;br /&gt;
            root:tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-related-tile&amp;#039;)&lt;br /&gt;
                :tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-related-title&amp;#039;)&lt;br /&gt;
                    :wikitext(&amp;#039;[[&amp;#039; .. portal.title .. &amp;#039;|&amp;#039; .. portal.label .. &amp;#039;]]&amp;#039;):done()&lt;br /&gt;
                :tag(&amp;#039;div&amp;#039;):addClass(&amp;#039;portal-related-sub&amp;#039;)&lt;br /&gt;
                    :wikitext(portal.sub)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return tostring(root)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>Joe Beaudoin Jr.</name></author>
	</entry>
</feed>