跳转到内容

模組:VGNL

维基百科,自由的百科全书

require("strict")

local getArgs = require("Module:Arguments").getArgs

-- Assume that the data has been sorted by issue id.
local issue_item_data = mw.loadData("Module:VGNL/items")

local __all__ = {
    "date",
    "target",
    "link",
    "index",
    "toc",
}

local ROOT_PATH = "維基專題:電子遊戲/簡訊"
-- The following indicators can be loaded from [[Module:VGNL/items]] instead of 
-- being hard-coded here.  
-- The data for `issue_item_data` should be fetched using `require` instead of 
-- `mw.loadData`. ([[mw:Extension:Scribunto/Lua reference_manual#mw.loadData]])
local MERGE_TO_FEATURE = "<MERGED>"
local UPCOMING = "<UPCOMING>"
local CANCELLED = "<CANCELLED>"

--- @alias IssueId [number, number]
--- @alias Date [number, number, number]
--- @alias Released Date | `UPCOMING` | `CANCELLED`
--- @alias ColumnName "feature" | "interview" | "news" | "quality_content"
--- @class IssueItem
--- @field id IssueId
--- @field issue string
--- @field date Released
--- @field feature string | nil
--- @field interview string | `MERGE_TO_FEATURE` | nil
--- @alias IssueItemList IssueItem[]


-- ===== 跪求Lua 5.4,Lua 5.1要什麼功能什麼沒有🤣 ===== --

--- Check if an item exists in a list-like table.
--- @param __item any
--- @param __list any[]
--- @return boolean
local function in_list(__item, __list)
    for _, v in ipairs(__list) do
        if __item == v then
            return true
        end
    end
    return false
end


--- Filter an item in a list-like table based on a condition.
--- @param __function function
--- @param __list any[]
--- @return any
local function filter_one(__function, __list)
    for _, v in ipairs(__list) do
        if __function(v) then
            return v
        end
    end
end

--- Filter all items in a list-like table met the condition.
--- @param __function function
--- @param __list any[]
--- @return any[]
local function filter_all(__function, __list)
    local result = {}
    for _, v in ipairs(__list) do
        if __function(v) then
            table.insert(result, v)
        end
    end
    return result
end

--- Filter item(s) in a list-like table met the condition.
--- @param __function function
--- @param __list any[]
--- @param __mode "all" | "one" | nil @Default to "all".
--- @return any | any[]
local function filter(__function, __list, __mode)
    if __mode == "one" then
        return filter_one(__function, __list)
    end
    return filter_all(__function, __list)
end


-- ===== Some tools. ===== --

--- Format an issue ID tuple into a string.
--- For example: `{2024, 07}` becomes "2024-07".
--- @param val IssueId
--- @return string
local function format_issue_id(val)
    local vol, iss = val[1], val[2]
    return vol .. "-" .. string.format("%02d", iss)
end

--- Parse an issue ID string into an ID tuple.
--- For example: "2024-07" becomes `{2024, 07}`.
--- @param val string
--- @return IssueId
local function parse_issue_id(val)
    local _, _, _y, _m = mw.ustring.find(val, "(%d%d%d%d)%-(%d%d)")
    local y, m = tonumber(_y), tonumber(_m)
    return {y, m}
end

--- Format a date tuple into a Chinese date string.
--- For example: `{2025, 3, 25}` becomes "2025年3月25日".
--- @param val Date
--- @return string
local function format_date(val)
    local y, m, d = val[1], val[2], val[3]
    return string.format("%s年%s月%s日", y, m, d)
end

--- Convert an issue's release date or status code into a string.
--- @param val Released
--- @return string
local function get_index_date_str(val)
    if type(val) == "table" then
        return format_date(val)
    end
    if val == UPCOMING then
        return "(編輯中)"
    end
    if val == CANCELLED then
        return "(未出刊)"
    end
    error('unexpected relased date or status code')
end

--- Retrieve the first issue by its ID or status.
--- @param items IssueItemList
--- @param id
--- | string  # e.g. "2024-01"
--- | "released"  # the most recently released issue
--- | "upcoming"  # the next upcoming issue
--- | nil  # defaults to "released"
--- @return IssueItem
local function get_issue_item(items, id)
    local cond
    -- Match recently released issue.
    if (id == nil) or (id == "released") then
        cond = function(item)
            return type(item.date) == "table"
        end
        local filtered_items = filter(cond, items)
        return filtered_items[#filtered_items]
    end
    -- Match the next upcoming issue
    if id == "upcoming" then
        cond = function(item)
            return item.date == UPCOMING
        end
        local filtered_items = filter(cond, items)
        return filtered_items[1]
    end
    -- Match issue id text (e.g. "2020-01")
    local parsed_id = parse_issue_id(id)
    cond = function(item)
        return (item.id[1] == parsed_id[1]) and (item.id[2] == parsed_id[2])
    end
    return filter(cond, items, "one")
end

--- Get the link target (i.e. page title) of an issue's data by column.
--- @param item IssueItem
--- @param column ColumnName?
--- @return string
local function get_link_target(item, column)
    local id_path = format_issue_id(item.id)
    local issue_base_path = ROOT_PATH .. "/" .. id_path
    if column == nil then
        return issue_base_path
    end
    local code_to_path_mapping = {
        ["feature"] = "專題",
        ["interview"] = "訪談",
        ["news"] = "宣告",
        ["quality_content"] = "特優內容",
    }
    return issue_base_path .. "/" .. code_to_path_mapping[column]
end

--- Get the column title for use in the index page.
--- @param item IssueItem
--- @param column ColumnName?
--- @return string?
local function get_index_text(item, column)
    if column == nil then
        return format_issue_id(item.id)
    end
    if in_list(column, {"news", "quality_content"}) then
        return "連結"
    end
    if in_list(column, {"feature", "interview"}) then
        return item[column]
    end
end

--- Get the column title for use in a content environment.
--- @param item IssueItem
--- @param column ColumnName?
--- @return string?
local function get_toc_text(item, column)
    if column == nil then
        return item.issue
    end
    if column == "feature" then
        local title = item[column]
        if title == nil then
            return
        end
        return "專題報導:" .. title
    end
    if (column == "interview") then
        local title = item[column]
        if (title == nil) or (title == MERGE_TO_FEATURE) then
            return
        end
        return "訪談:" .. title
    end
    if column == "news" then
        return "宣告"
    end
    if column == "quality_content" then
        return "高品質內容變動"
    end
end

--- I bet five cents that you wouldn't understand my docstrings if I
--- wrote it.
--- @param target string?
--- @param display string?
--- @return string?
local function build_linked_text(target, display)
    if target == nil then
        return display
    end
    if display == nil then
        return "[[" .. target .. "]]"
    end
    return "[[" .. target .. "|" .. display .. "]]"
end


-- ===== Index table builders ===== --

--- Build the header row for the index table.
--- @return table<string, any>
local function _build_index_table_header_row()
    local row, cell = mw.html.create("tr"), nil
    -- Append the no. header cell.
    cell = mw.html.create("th")
    cell
        :attr("scope", "col")
        :attr("data-sort-type", "number")
        :wikitext("序號")
    row:node(cell)
    --Append the other header cells.
    local headers = {"期數", "出刊日期", "專題", "訪談", "宣告", "特優內容"}
    for _, header in ipairs(headers) do
        cell = mw.html.create("th")
        cell
            :attr("scope", "col")
            :addClass("unsortable")
            :wikitext(header)
        row:node(cell)
    end
    return row
end

--- Build the table cell for an issue row in the index table.
--- @param item IssueItem
--- @param column ColumnName?
--- @return table<string, any>
local function _build_index_table_issue_cell(item, column)
    local text
    local display = get_index_text(item, column)
    if display == nil then
        text = "—"
    else
        local target = get_link_target(item, column)
        text = build_linked_text(target, display)
    end
    return mw.html.create("td"):wikitext(text)
end

--- Build the content row for an issue in the index table.
--- @param index number
--- @param item IssueItem
--- @return table<string, any>
local function _build_index_table_issue_row(index, item)
    local build_cell = _build_index_table_issue_cell
    local row, cell = mw.html.create("tr"), nil
    -- Append the row header cell.
    cell = mw.html.create("th")
    cell:attr("scope", "row"):wikitext(index)
    row:node(cell)
    -- Append the issue cell.
    cell = build_cell(item)
    row:node(cell)
    -- Append the published date cell.
    cell = mw.html.create("td")
    cell:wikitext(get_index_date_str(item.date))
    row:node(cell)
    -- Append feature and interview cell.
    if get_index_text(item, "interview") == MERGE_TO_FEATURE then
        -- Append the spanning feature cell.
        cell = build_cell(item, "feature"):attr("colspan", 2)
        row:node(cell)
    else
        -- Append the feature cell.
        cell = build_cell(item, "feature")
        row:node(cell)
        -- Append the interview cell.
        cell = build_cell(item, "interview")
        row:node(cell)
    end
    -- Append the news cell.
    cell = build_cell(item, "news")
    row:node(cell)
    -- Append the quaility content cell.
    cell = build_cell(item, "quality_content")
    row:node(cell)
    -- Return the row
    return row
end

--- Build and return the index table in wikitext format.
--- @param items IssueItemList
--- @return table<string, any>
local function build_table(items)
    local table_title = "《電子遊戲專題簡訊》目錄"
    local result, child = mw.html.create("table"), nil
    result
        :addClass("wikitable")
        :addClass("sortable")
        :css("text-align", "center")
    -- Append caption.
    child = mw.html.create("caption")
    child:wikitext(table_title)
    result:node(child)
    -- Append the header row.
    child = _build_index_table_header_row()
    result:node(child)
    -- Append the issue rows.
    for id, item in ipairs(items) do
        child = _build_index_table_issue_row(id, item)
        result:node(child)
    end
    -- We done.
    return result
end


-- ===== Function to be called. ===== --

local p = {}

--- Create a function that invokes a module function with the given
--- arguments.
--- @param func_name string
--- @return function
local function create_invoke_wrapper(func_name)
    return function(frame)
        local args = getArgs(frame)
        return p[func_name](args)
    end
end

--- Retrieve the link target from the most recent issue with the given
--- status.
--- @param args table
--- @return string?
function p._target(args)
    local id, column = args[1], args.column
    local item = get_issue_item(issue_item_data, id)
    return get_link_target(item, column)
end

--- Retrieve the linked text from the most recent issue with the given
--- status.
--- @param args table
--- @return string?
function p._link(args)
    local id, column = args[1], args.column
    local item = get_issue_item(issue_item_data, id)
    local target = get_link_target(item, column)
    local text = get_toc_text(item, column)
    return build_linked_text(target, text)
end

--- Retrieve the released date (or status) from the most recent issue.
--- @param args table
--- @return string?
function p._date(args)
    local id = args[1]
    local item = get_issue_item(issue_item_data, id)
    return get_index_date_str(item.date)
end

--- Retrieve the content from the most recent issue with the given
--- status.
--- @param args table
--- @return string?
function p._toc(args)
    local id, style = args[1], args.style
    local item = get_issue_item(issue_item_data, id)
    local linked_texts = {}
    local columns = {"feature", "interview", "news", "quality_content"}
    for _, column in ipairs(columns) do
        local text = get_toc_text(item, column)
        if (text ~= nil) and (text ~= MERGE_TO_FEATURE) then
            local target = get_link_target(item, column)
            local link = build_linked_text(target, text)
            table.insert(linked_texts, link)
        end
    end
    -- Apply style.
    if style == "inline" then
        return table.concat(linked_texts, "、")
    end
    for k, v in ipairs(linked_texts) do
        linked_texts[k] = "* " .. v
    end
    return table.concat(linked_texts, "\n")
end

--- Build and return the index table in wikitext format.
--- @param args table
--- @return string
function p._index(args) -- noqa
    return tostring(build_table(issue_item_data))
end

-- ===== Return results ===== --

for _, v in ipairs(__all__) do
    p[v] = create_invoke_wrapper("_" .. v)
end

return p