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