🇮🇷 Iran Proxy | https://www.wikipedia.org/wiki/Module:User:Gommeh/CategoryView
Jump to content

Module:User:Gommeh/CategoryView

From Wikipedia, the free encyclopedia
-- Define module table
local p = {}

--------------------------------------------------------
-- Utility functions
--------------------------------------------------------

local function trim(s)
    return mw.text.trim(s or "")
end

local function split(s, sep)
    s = s or ""
    local out = {}
    if s == "" then
        return out
    end
    for part in mw.text.gsplit(s, sep, true) do
        table.insert(out, part)
    end
    return out
end

local function startsWith(str, prefix)
    return str:sub(1, #prefix) == prefix
end

-- Remove ALL templates ({{...}}) – used only for sort cleaning
local function stripTemplates(s)
    s = s or ""
    local n
    repeat
        s, n =
            mw.ustring.gsub(
            s,
            "%b{}",
            function(m)
                -- keep only non-template braces; %b{} matches {}, we need {{...}} specifically
                if mw.ustring.sub(m, 1, 2) == "{{" and mw.ustring.sub(m, -2) == "}}" then
                    return ""
                end
                return m
            end
        )
    until n == 0
    return mw.text.trim(s)
end

-- Yank [[A|B]] -> B ; [[A]] -> A
local function linksToText(s)
    s = s or ""
    s = mw.ustring.gsub(s, "%[%[([^%]|%]]-)|([^%]]-)%]%]", "%2")
    s = mw.ustring.gsub(s, "%[%[([^%]]-)%]%]", "%1")
    return mw.text.trim(s)
end

-- Remove any namespace prefix (everything before the first colon)
local function removeNamespacePrefix(s)
    s = s or ""
    -- This removes up to and including the first colon, if present
    local colonPos = mw.ustring.find(s, ":", 1, true)
    if colonPos then
        s = mw.ustring.sub(s, colonPos + 1)
    end
    return mw.text.trim(s)
end

-- Clean a string for sorting: strip templates/refs/html, convert links, drop '' markup
local function cleanForSort(s)
    s = s or ""
    -- strip specific markup first
    s = mw.ustring.gsub(s, "<ref.-</ref>", "")
    s = mw.ustring.gsub(s, "<.->", "")
    s = stripTemplates(s)
    s = linksToText(s)
    s = mw.ustring.gsub(s, "''+", "")
    s = mw.ustring.gsub(s, "%s+", " ")
    return mw.text.trim(s)
end

-- Extract first link target's BASEPAGENAME (for |sort=base)
local function targetBaseName(w)
    if not w or w == "" then
        return nil
    end
    local inner = mw.ustring.match(w, "%[%[([^%]]+)%]%]")
    if not inner then
        return nil
    end
    inner = mw.text.trim(inner)
    local cut = #inner + 1
    local p1 = inner:find("|", 1, true)
    if p1 and p1 < cut then
        cut = p1
    end
    local p2 = inner:find("#", 1, true)
    if p2 and p2 < cut then
        cut = p2
    end
    local target = mw.text.trim(inner:sub(1, cut - 1))
    if target == "" then
        return nil
    end
    local t = mw.title.new(target)
    if t and t.baseText and t.baseText ~= "" then
        return t.baseText
    end
    local last = mw.ustring.match(target, "([^/]+)$")
    return last or target
end

-- Detect and remove {{icon|...}} (case/spacing tolerant), returning cleaned text + array of icons
local function extractIcons(w)
    w = w or ""
    local icons = {}
    -- Match {{ icon | ... }} in a case-insensitive way; capture the whole template to restore later
    local cleaned =
        mw.ustring.gsub(
        w,
        "%{%{%s*[Ii][Cc][Oo][Nn]%s*|.-%}%}",
        function(m)
            table.insert(icons, trim(m))
            return ""
        end
    )
    cleaned = mw.ustring.gsub(cleaned, "%s%s+", " ")
    return trim(cleaned), icons
end

--------------------------------------------------------
-- Strip leading articles (e.g., "The", "A", "An") from sort key
--------------------------------------------------------
local function stripArticles(s, ignoreList)
    s = s or ""
    local low = mw.ustring.lower(s)
    for _, art in ipairs(ignoreList or {}) do
        local a = mw.ustring.lower(trim(art))
        if a ~= "" then
            if mw.ustring.sub(low, 1, #a + 1) == a .. " " then
                return trim(mw.ustring.sub(s, #a + 2))
            end
        end
    end
    return s
end

--------------------------------------------------------
-- Determine the first character heading for grouping
--------------------------------------------------------
local function firstHeadingChar(s)
    local c = mw.ustring.sub(s or "", 1, 1)
    if not c or c == "" then
        return "#"
    end
    local upper = mw.ustring.upper(c)
    if upper:match("%a") then
        return upper
    end
    return "#"
end

--------------------------------------------------------
-- Parse a single list item into components
--------------------------------------------------------
local function parseItem(raw, useBase)
    raw = trim(raw)
    if raw == "" then
        return nil
    end

    local left, explicitSort = raw, nil

    -- Allow explicit sort key using "||"
    if raw:find("%|%|", 1, true) then
        local parts = mw.text.split(raw, "||", true)
        left = trim(parts[1] or "")
        explicitSort = trim(parts[2] or "")
    end

    -- Pull icons out FIRST so they can't contaminate sort
    local leftNoIcons, icons = extractIcons(left)

    -- Helpers: detect wikilinks/templates anywhere in the string
    local function hasWikilink(s)
        return mw.ustring.find(s or "", "%[%[[^%]]+%]%]") ~= nil
    end
    local function hasTemplate(s)
        return mw.ustring.find(s or "", "%{%{") ~= nil
    end

    -- Preserve exact wikitext for display (minus icons).
    -- Autolink ONLY if there's no link already AND no templates present.
    local displayWikitext
    if hasWikilink(leftNoIcons) or hasTemplate(leftNoIcons) then
        -- already linked or contains templates; do NOT wrap
        displayWikitext = trim(leftNoIcons)
    else
        if leftNoIcons == "" then
            return nil
        end
        displayWikitext = "[[" .. leftNoIcons .. "]]"
    end

    -- *** KEY FIX: derive the primary visible text from the link ***
    local linkVisible = linksToText(leftNoIcons)
    if linkVisible == "" then
        -- fallback: if no link present, use cleaned plain text
        linkVisible = cleanForSort(leftNoIcons)
    end

    -- strip namespace prefixes like Category:, File:, Template:, etc.
    linkVisible = removeNamespacePrefix(linkVisible)

    -- Build sort basis
    local sortBasis
    if explicitSort and explicitSort ~= "" then
        sortBasis = explicitSort
    elseif useBase then
        sortBasis = targetBaseName(leftNoIcons) or linkVisible
    else
        sortBasis = linkVisible
    end

    -- Final clean + tie-break text
    local sortClean = cleanForSort(sortBasis)
    local tieText = cleanForSort(linkVisible)

    return {
        wikitextBase = displayWikitext, -- WITHOUT icons
        icons = icons, -- array of {{icon|...}} to reattach
        displayText = tieText,
        sort = sortClean
    }
end

--------------------------------------------------------
-- Main render
--------------------------------------------------------
function p.render(frame)
    local args = frame:getParent() and frame:getParent().args or frame.args

    local pagesParam = args.pages or args.list or ""
    local cols = tonumber(args.cols) or 3
    if cols < 1 then
        cols = 1
    end
    if cols > 6 then
        cols = 6
    end

    local ignoreParam = args.ignore_prefix or args.ignore or ""
    local ignoreList = {}
    for _, x in ipairs(split(ignoreParam, ",")) do
        local t = trim(x)
        if t ~= "" then
            table.insert(ignoreList, t)
        end
    end

    local sortMode = (args.sort or args.sortby or ""):lower()
    local useBase = (args.base == "1" or args.base == "yes" or sortMode == "base" or sortMode == "basepagename")

    -- Parse input (newline, comma, semicolon)
    local rawItems = {}
    for _, sep in ipairs({"%c", ",", ";"}) do
        if #rawItems == 0 then
            if sep == "%c" then
                for line in mw.text.gsplit(pagesParam, "\n", false) do
                    if trim(line) ~= "" then
                        table.insert(rawItems, line)
                    end
                end
            else
                if #rawItems <= 1 then
                    rawItems = {}
                    for part in mw.text.gsplit(pagesParam, sep, false) do
                        if trim(part) ~= "" then
                            table.insert(rawItems, part)
                        end
                    end
                end
            end
        end
    end
    if #rawItems == 0 and trim(pagesParam) ~= "" then
        table.insert(rawItems, pagesParam)
    end

    -- Build items
    local items = {}
    for _, r in ipairs(rawItems) do
        local it = parseItem(r, useBase)
        if it then
            it.sort = stripArticles(it.sort, ignoreList)
            table.insert(items, it)
        end
    end

    -- Group
    local groups = {["#"] = {}}
    for c = string.byte("A"), string.byte("Z") do
        groups[string.char(c)] = {}
    end
    for _, it in ipairs(items) do
        local first = mw.ustring.sub(it.sort or "", 1, 1)
        local key = firstHeadingChar(mw.ustring.upper(first or ""))
        if not groups[key] then
            key = "#"
        end
        table.insert(groups[key], it)
    end

    -- Sort comparator
    local function cmp(a, b)
        local as = mw.ustring.upper(a.sort or "")
        local bs = mw.ustring.upper(b.sort or "")
        if as == bs then
            local ad = mw.ustring.upper(a.displayText or "")
            local bd = mw.ustring.upper(b.displayText or "")
            return ad < bd
        end
        return as < bs
    end
    for _, tbl in pairs(groups) do
        table.sort(tbl, cmp)
    end

    -- Render
    local root = mw.html.create("div"):addClass("mw-category")
    local colsDiv = root:tag("div"):addClass("mw-category-columns"):css("column-count", tostring(cols))

    local order = {"#"}
    for c = string.byte("A"), string.byte("Z") do
        table.insert(order, string.char(c))
    end

    local hasAny = false
    for _, key in ipairs(order) do
        local tbl = groups[key]
        if tbl and #tbl > 0 then
            hasAny = true
            -- Create the group container (one per letter/#)
            local groupDiv = colsDiv:tag("div"):addClass("mw-category-group")

            -- ALWAYS lock the heading + first item together (singleton or not)
            local lock =
                groupDiv:tag("div"):css("break-inside", "avoid"):css("-webkit-column-break-inside", "avoid"):css(
                "-moz-column-break-inside",
                "avoid"
            ):css("break-after", "avoid")

            -- Heading inside the lock
            local headingText = (key == "#") and "0–9" or key
            lock:tag("div"):css("font-weight", "bold"):css("font-size", "120%"):css("margin-top", "0.6em"):css(
                "margin-bottom",
                "0.2em"
            ):wikitext(mw.text.nowiki(headingText))

            -- First item inside the lock (only if list non-empty)
            if #tbl >= 1 then
                local firstUl = lock:tag("ul")
                local it1 = tbl[1]
                local wt1 = it1.wikitextBase or ""
                if it1.icons and #it1.icons > 0 then
                    wt1 = wt1 .. " " .. table.concat(it1.icons, " ")
                end
                firstUl:tag("li"):wikitext(mw.text.trim(wt1))
            end

            -- Remaining items (if any) outside the lock so they can flow across columns
            if #tbl >= 2 then
                local restUl = groupDiv:tag("ul")
                for i = 2, #tbl do
                    local it = tbl[i]
                    local wt = it.wikitextBase or ""
                    if it.icons and #it.icons > 0 then
                        wt = wt .. " " .. table.concat(it.icons, " ")
                    end
                    restUl:tag("li"):wikitext(mw.text.trim(wt))
                end
            end
        end
    end

    if not hasAny then
        return tostring(mw.html.create("div"):addClass("plainlinks"):wikitext("''No pages to display.''"))
    end

    return tostring(root)
end

return p