Jump to content

Module:News

From WhiskerWiki

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

-- ================================================================================
-- Module dependencies
-- ================================================================================
-- Mirrors HopperWiki's Module:News; the classes use the ww- prefix so they pick
-- up WhiskerWiki's --ww-* theme tokens.
local p = {}
local json_decode = require("mw.text").jsonDecode

-- ================================================================================
-- Utility function to sort news items
-- ================================================================================
-- Same-date items keep their order on the data page (table.sort isn't stable),
-- so whichever is listed first there gets the "latest" treatment.
local function sort_news_items_by_date(news_items)
    for i, news_item in ipairs(news_items) do news_item._order = i end
    table.sort(news_items, function(a, b)
        local date_a, date_b = a.date_posted or "", b.date_posted or ""
        if date_a ~= date_b then return date_a > date_b end  -- descending order
        return a._order < b._order
    end)
end

-- ================================================================================
-- Fetch and parse news data from a given page
-- ================================================================================
local function fetch_news_data(data_page)
    local raw_content = mw.title.new(data_page):getContent()
    if not raw_content then return {} end
    -- Remove <pre> tags
    raw_content = raw_content:gsub("^<pre>%s*", ""):gsub("%s*</pre>$", "")
    -- Decode JSON
    local success, news_data = pcall(json_decode, raw_content)
    return success and news_data or {}
end

-- ================================================================================
-- Post types: the optional "type" field on a news item picks its icon. Icons are
-- .ww-icon-* CSS masks (mediawiki/badge_icons/), so they follow the theme tokens.
-- Items with no type, or an unknown one, get the generic megaphone.
-- ================================================================================
local news_types = {
    feature        = { icon = "sparkles",    label = "New feature" },
    data           = { icon = "database",    label = "Data & integrations" },
    community      = { icon = "users",       label = "Community" },
    sounds         = { icon = "audio-lines", label = "Sounds" },
    geography      = { icon = "map",         label = "Geography" },
    taxonomy       = { icon = "network",     label = "Taxonomy" },
    upcoming       = { icon = "hourglass",   label = "Coming soon" },
    infrastructure = { icon = "wrench",      label = "Under the hood" },
}
local default_type = { icon = "megaphone", label = "News" }

-- ================================================================================
-- Date helpers: "2026-06-30" -> "Jun 30, 2026", and whether it is recent enough
-- for the NEW badge. Anything unparseable is shown as-is and never marked new.
-- ================================================================================
local function parse_date(date_string)
    local y, m, d = tostring(date_string or ""):match("^(%d%d%d%d)-(%d%d)-(%d%d)")
    if not y then return nil end
    return os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = 12 })
end

local function format_date(date_string)
    if not parse_date(date_string) then return date_string end
    return mw.getContentLanguage():formatDate("M j, Y", date_string)
end

local function is_recent(date_string, new_days)
    local posted = parse_date(date_string)
    return posted ~= nil and (os.time() - posted) <= new_days * 86400
end

-- ================================================================================
-- Render a news feed given a data page
-- ================================================================================
local function render_news_feed(data_page, limit, archive_link, archive_text, new_days)
    limit = tonumber(limit) or 5  -- default to 5 if not provided/invalid
    new_days = tonumber(new_days) or 30

    local news_items = fetch_news_data(data_page)
    sort_news_items_by_date(news_items)  -- Sort by date

    local output = {}
    table.insert(output, '<div class="news-feed">')
    table.insert(output, '<ul class="ww-news-list">')
    for i, news_item in ipairs(news_items) do
        if i > limit then break end  -- Limit to `limit` items
        local title_display
        if news_item.link and news_item.link ~= "" then
            if news_item.link:match("^https?://") then
                -- External link: [url title]
                title_display = string.format('<b>[%s %s]</b>', news_item.link, news_item.title)
            else
                -- Internal wiki page: [[page|title]]
                title_display = string.format('<b>[[%s|%s]]</b>', news_item.link, news_item.title)
            end
        else
            title_display = string.format('<b>%s</b>', news_item.title)
        end

        local news_type = news_types[news_item.type] or default_type
        local icon = string.format(
            '<span class="ww-news-icon" title="%s"><span class="ww-icon ww-icon-%s"></span></span>',
            news_type.label, news_type.icon
        )
        local new_badge = is_recent(news_item.date_posted, new_days)
            and ' <span class="ww-news-new">New</span>' or ""
        -- The date trails the summary on the same line instead of taking its own.
        -- Styling lives in MediaWiki:Common.css (.ww-news-*), themed via --ww-* tokens.
        local posted_date = news_item.date_posted and string.format(
            ' <span class="ww-news-meta">%s</span>',
            format_date(news_item.date_posted)
        ) or ""
        local item_class = (i == 1) and "ww-news-item ww-news-item--latest" or "ww-news-item"
        table.insert(output, string.format(
            '<li class="%s">%s<div class="ww-news-body">%s%s - %s%s</div></li>',
            item_class, icon, title_display, new_badge, news_item.summary, posted_date
        ))
    end
    table.insert(output, '</ul>')

    if archive_link and archive_link ~= "" then
        local link_text = (archive_text and archive_text ~= "") and archive_text or "See previous news items"
        table.insert(output, string.format(
            '<div class="news-feed-archive-link">[[%s|%s]]</div>',
            archive_link, link_text
        ))
    end

    table.insert(output, '</div>')
    return table.concat(output, "\n")
end

-- ================================================================================
-- Exposed render functions
-- ================================================================================
function p.render_whiskerwiki_news(frame)
    local args = frame.args
    return frame:preprocess(render_news_feed(
        "Data:WhiskerWiki news",
        args.limit,
        args.archive_link,
        args.archive_text,
        args.new_days
    ))
end

return p
Cookies help us deliver our services. By using our services, you agree to our use of cookies.