Module:ProjectCounts
Module:ProjectCounts computes the figures shown in the statistics block on the Main Page (projects listed, screensavers archived, etc.) directly from the wikitext of the relevant pages, so the numbers stay current as tables and lists are edited. It is also used for section row counts on the BOINC projects page itself.
The module reads the source wikitext of target pages at parse time. Each page it reads is registered as a parser dependency, so any page using these invokes re-renders automatically when the target page changes. No manual purging or updating of figures is needed.
Live test
The following invokes run against live data. Expected values as of October 2026 — 38, 6, 293, 337, 71, 101 — will drift upward as the wiki grows; a sudden 0 or a red "Script error" means something broke (check section heading names first).
Active: 37
Private: 6
Completed: 294
Listed: 337
Recordings: 71
Documented: 101
Functions
count
Counts the data rows in the wikitable(s) of one section of a page.
Usage:
{{#invoke:ProjectCounts|count|Section name|page=Page title}}
1(required) — the section heading, exactly as written on the page (without the equals signs; any heading level works).page(optional) — page to read; defaults toBOINC projects.
Examples:
| Call | Returns |
|---|---|
| {{#invoke:ProjectCounts|count|Active BOINC Projects}} | 38 |
| {{#invoke:ProjectCounts|count|Completed BOINC Projects}} | 293 |
| {{#invoke:ProjectCounts|count|Screensaver Gallery|page=BOINC project screensavers}} | 35 (project rows; see recordings below)
|
Return values recorded October 2026; they change as the tables grow.
total
Sums the data rows of several sections of one page. This produces the "Projects listed" figure on the Main Page.
Usage:
{{#invoke:ProjectCounts|total|Active BOINC Projects|Private BOINC Projects|Completed BOINC Projects}}
Returns 337 (38 + 6 + 293 as of October 2026). page again defaults to BOINC projects.
recordings
Counts unique YouTube recordings in a section, by deduplicating video IDs found in watch?v= links, youtu.be/ short links, and img.youtube.com/vi/ thumbnail references. Sections where one table row holds several videos are counted correctly — this measures recordings, not rows.
Usage:
{{#invoke:ProjectCounts|recordings|Screensaver Gallery|page=BOINC project screensavers}}
{{#invoke:ProjectCounts|recordings}}
Returns 71 as of October 2026.
documented
Counts distinct existing mainspace articles linked from a section — link targets must exist (redlinks are skipped) and namespaced links (File:, Category:, etc.) are ignored. Defaults to the curated project list on the Main Page.
Usage:
{{#invoke:ProjectCounts|documented|Local BOINC project wiki pages|page=Main Page}}
{{#invoke:ProjectCounts|documented}}
Returns 102 as of October 2026 (101 project articles plus the link to the BOINC projects master table itself).
- Note: the Main Page "Projects documented" stat currently uses
{{PAGESINCATEGORY:BOINC projects|pages}}rather than this function. Infobox software automatically adds Category:BOINC projects to every article containing it, so the category count reflects documented project pages without relying on the curated list.documentedremains available for counting any curated link list per se.
How the counting works
- Section slicing
- The named heading is located at any heading level; the section runs until the next heading of the same or higher level (or end of page). Sub-sections are therefore included, but siblings are not. The heading name is matched exactly — a
== Screensaver Gallery ==invoke will no longer match if the heading is later renamed, e.g. to "Gallery". If a function suddenly returns 0 after an edit, check the heading text first. - Row counting (
count,total) - Data rows = number of
|-row separators at line start, minus one per table for its header separator. Assumes each table has exactly one header row and that every data row begins with|-on its own line — the format used by all project tables on this wiki.
Assumptions and limitations
- Table-format dependent in the manner described above. Free-form or multi-header tables will mis-count.
- Correct results depend on section headings staying constant; consider them part of the interface.
- Lua cannot enumerate category members or template transclusions. For "pages in a category" use
{{PAGESINCATEGORY:Category name|pages}}; for "pages using a template" see Special:WhatLinksHere/Template:Infobox software.
Where the module is used
| Statistic | Location | Function used |
|---|---|---|
| Projects listed | Main Page | total over the three sections of BOINC projects
|
| Screensavers archived | Main Page | recordings on BOINC project screensavers
|
| Section row counts | BOINC projects section headers | count per section
|
| Projects documented | Main Page | (category-based: {{PAGESINCATEGORY:BOINC projects|pages}}, powered by Template:Infobox software)
|
Maintenance
An off-wiki audit script (wiki_reconcile.py, pure Python, no dependencies) cross-checks the curated Main Page list against pages transcluding the infobox; it reports documented-but-unlisted pages, listed pages lacking an infobox, and redlinks. Run it after large re-organizations of the project listings.
-- Module:ProjectCounts
-- Counts BOINC project table rows and related figures for the wiki.
--
-- {{#invoke:ProjectCounts|count|Section name|page=Page title}}
-- Data rows in the wikitable(s) of one section.
-- {{#invoke:ProjectCounts|total|Section A|Section B|...|page=Page title}}
-- Sum of data rows across several sections of one page.
-- {{#invoke:ProjectCounts|documented|Local BOINC project wiki pages|page=Main Page}}
-- Distinct existing mainspace articles linked in a section.
-- {{#invoke:ProjectCounts|recordings|Screensaver Gallery|page=BOINC project screensavers}}
-- Unique YouTube recordings linked in a section.
local p = {}
local function esc(s) return (s:gsub('(%W)', '%%%1')) end
local function getContent(page)
local t = mw.title.new(page)
if not t then return nil end
return t:getContent()
end
-- Extract the wikitext of one section: from its heading (any level)
-- to the next heading of the same or higher level.
local function getSection(content, section)
local hStart, hEnd, marks = content:find('\n(=+)%s*' .. esc(section) .. '%s*=*')
if not hStart then return nil end
local depth = #marks
local secEnd = #content + 1
local pos = hEnd
while true do
local nStart, nEnd, nMarks = content:find('\n(=+)', pos)
if not nStart then break end
if #nMarks <= depth then secEnd = nStart break end
pos = nEnd
end
return '\n' .. content:sub(hEnd + 1, secEnd - 1)
end
-- Data rows = row separators minus one header separator per table.
local function countRows(body)
local _, tables = body:gsub('\n{|', '')
local _, seps = body:gsub('\n|%-', '')
return math.max(seps - tables, 0)
end
local function countSection(page, section)
local content = getContent(page)
if not content then return 0 end
local body = getSection(content, mw.text.trim(section))
if not body then return 0 end
return countRows(body)
end
function p.count(frame)
return countSection(frame.args.page or 'BOINC projects', frame.args[1] or '')
end
function p.total(frame)
local page = frame.args.page or 'BOINC projects'
local content = getContent(page)
if not content then return 0 end
local sum = 0
for _, s in ipairs(frame.args) do
local body = getSection(content, mw.text.trim(s))
if body then sum = sum + countRows(body) end
end
return sum
end
function p.documented(frame)
local page = frame.args.page or 'Main Page'
local section = mw.text.trim(frame.args[1] or 'Local BOINC project wiki pages')
local content = getContent(page)
if not content then return 0 end
local body = getSection(content, section)
if not body then return 0 end
local seen, n = {}, 0
for link in body:gmatch('%[%[([^|%]%[#:]+)') do
link = mw.text.trim(link)
if link ~= '' and not seen[link] then
seen[link] = true
local t = mw.title.new(link)
if t and t.namespace == 0 and t.exists then
n = n + 1
end
end
end
return n
end
function p.recordings(frame)
local content = getContent(frame.args.page or 'BOINC project screensavers')
if not content then return 0 end
local body = getSection(content, mw.text.trim(frame.args[1] or 'Screensaver Gallery'))
if not body then return 0 end
local seen, n = {}, 0
local function mark(id)
if id and id ~= '' and not seen[id] then
seen[id] = true
n = n + 1
end
end
for id in body:gmatch('youtube%.com/watch%?v=([%w_-]+)') do mark(id) end
for id in body:gmatch('youtu%.be/([%w_-]+)') do mark(id) end
for id in body:gmatch('img%.youtube%.com/vi/([%w_-]+)') do mark(id) end
return n
end
return p