tibia

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

local p = {}

-- Utility functions
local function trim(s)
    return (s or ""):gsub("^%s*(.-)%s*$", "%1")
end

local function escapePattern(text)
    return (text or ""):gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1")
end

local function normalizeTarget(target)
    if not target then return nil end
    target = trim(target)
    target = target:gsub("%s+", " ")
    if target == "" then return nil end
    local first = target:sub(1,1):upper()
    local rest = target:sub(2)
    return first .. (rest or "")
end

-- Count links in wikitext
local function countLinksInWikitext(wikitext, target)
    if not wikitext or not target then return 0 end
    local targetNorm = normalizeTarget(target)
    local count = 0
    for inner in wikitext:gmatch("%[%[(.-)%]%]") do
        local before = inner:match("^(.-)[|#]") or inner
        before = trim(before)
        if before ~= "" then
            local beforeNorm = normalizeTarget(before)
            if beforeNorm == targetNorm then
                count = count + 1
            else
                if before:find(":") then
                    local afterColon = before:match("([^:]+)$")
                    if afterColon and normalizeTarget(afterColon) == targetNorm then
                        count = count + 1
                    end
                elseif beforeNorm:lower() == targetNorm:lower() then
                    count = count + 1
                end
            end
        end
    end
    return count
end

-- Count plain text occurrences
local function normWhitespace(s)
    s = trim(s)
    s = s:gsub("%s+", " ")
    return s
end

local function countTextInWikitext(wikitext, text)
    if not wikitext or not text then return 0 end
    local hay = normWhitespace(wikitext):lower()
    local needle = normWhitespace(text):lower()
    if needle == "" then return 0 end
    local pat = escapePattern(needle)
    local count = 0
    local start = 1
    while true do
        local s,e = hay:find(pat, start, true)
        if not s then break end
        count = count + 1
        start = e + 1
    end
    return count
end

-- Extract between headings
local function extractBetweenHeadings(wikitext, startHeading, endHeading)
    if not wikitext then return "" end
    if not startHeading then return wikitext end

    local lines = {}
    local inSection = false
    local startHeadingNorm = startHeading:lower()
    local endHeadingNorm = endHeading and endHeading:lower() or nil

    for line in wikitext:gmatch("([^\n]*)\n?") do
        local heading = line:match("^%s*(=+)%s*(.-)%s*=%s*$")
        if heading then
            local title = trim(line:match("=+%s*(.-)%s*=+"))
            local titleNorm = title:lower()
            if titleNorm == startHeadingNorm then
                inSection = true
            elseif endHeadingNorm and titleNorm == endHeadingNorm and inSection then
                table.insert(lines, line)
                break
            end
        end
        if inSection then
            table.insert(lines, line)
        end
    end

    return table.concat(lines, "\n")
end

-- Fetch content recursively for {{:Page}}
local function fetchContentRecursive(titleName, depth)
    depth = depth or 0
    if depth > 5 then return false, "Too many recursive transclusions" end
    if not titleName or titleName == "" then return false, "invalid title" end

    local ok, titleObj = pcall(function() return mw.title.new(titleName) end)
    if not ok or not titleObj then return false, "invalid title object" end

    local ok2, wikitext = pcall(function() return titleObj:getContent() end)
    if not ok2 or not wikitext then return false, "getContent error" end
    wikitext = trim(wikitext)

    -- Handle simple {{:Page}} transclusion
    local single_colon = wikitext:match("^%{%s*:%s*([^%}]+)%s*%}$") or wikitext:match("^:%s*(%S+)%s*$")
    if single_colon then
        return fetchContentRecursive(single_colon, depth + 1)
    end

    return true, wikitext
end

-- Public functions
function p.LinkSearch(frame)
    local args = frame.args or {}

    -- Public-facing parameters
    local pageTitle = args.pageTitle or args[1] or args["1"]
    local linkTo = args.link or args.Link or args[2] or args["2"]
    local startHeading = args.start_heading or args[3] or args["3"]
    local endHeading = args.end_heading or args[4] or args["4"]

    if not pageTitle or pageTitle == "" then return "Error: missing PageTitle" end
    if not linkTo or linkTo == "" then return "Error: missing LinkToSearch" end

    local ok, content = fetchContentRecursive(pageTitle)
    if not ok then return "Error: " .. content end

    if startHeading then
        content = extractBetweenHeadings(content, startHeading, endHeading)
    end

    local count = countLinksInWikitext(content, linkTo)
    return tostring(count)
end

function p.TextSearch(frame)
    local args = frame.args or {}

    -- Public-facing parameters
    local pageTitle = args.pageTitle or args[1] or args["1"]
    local textTo = args.text or args.Text or args[2] or args["2"]
    local startHeading = args.start_heading or args[3] or args["3"]
    local endHeading = args.end_heading or args[4] or args["4"]

    if not pageTitle or pageTitle == "" then return "Error: missing PageTitle" end
    if not textTo or textTo == "" then return "Error: missing TextToSearch" end

    local ok, content = fetchContentRecursive(pageTitle)
    if not ok then return "Error: " .. content end

    if startHeading then
        content = extractBetweenHeadings(content, startHeading, endHeading)
    end

    local count = countTextInWikitext(content, textTo)
    return tostring(count)
end

return p