Module:PhysicsQC: Difference between revisions
Jump to navigation
Jump to search
Refresh PhysicsQC module cache after Vacuum index expansion |
Keep Quantum index section images inside headers |
||
| (7 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
-- Cache bump: | -- Cache bump: Render section images inside Quantum index headers. | ||
-- Cache bump: Fields chapter expanded to 12 entries. | -- Cache bump: Fields chapter expanded to 12 entries. | ||
| Line 41: | Line 41: | ||
end | end | ||
local function getFramePageContent(frame, pageName) | |||
if frame and frame.preprocess and pageName and pageName ~= '' then | |||
local ok, text = pcall(function() | |||
return frame:preprocess('{{:' .. pageName .. '}}') | |||
end) | |||
if ok and text and text ~= '' and text:match('==%s*Full contents%s*==') then | |||
return text:gsub('<%s*/?%s*includeonly%s*>', '') | |||
end | |||
end | |||
return getPageContent(pageName) | |||
end | |||
local function getCurrentPageName() | local function getCurrentPageName() | ||
local t = mw.title.getCurrentTitle() | local t = mw.title.getCurrentTitle() | ||
| Line 382: | Line 395: | ||
local function renderSectionList(section, collapse, sectionNumber, articleStartNumber) | local function renderSectionList(section, collapse, sectionNumber, articleStartNumber) | ||
local | local rawBody = table.concat(section.lines, '\n') | ||
local sectionImage = '' | |||
local divImage = rawBody:match('^%s*<div[^>]-float%s*:%s*right.-%[%[File:[^%]]+%]%].-</div>') | |||
if divImage then | |||
sectionImage = divImage:match('%[%[File:[^%]]+%]%]') or '' | |||
rawBody = rawBody:gsub('^%s*<div[^>]-float%s*:%s*right.-%[%[File:[^%]]+%]%].-</div>%s*', '', 1) | |||
else | |||
sectionImage = rawBody:match('^%s*(%[%[File:[^%]]+%]%])') or '' | |||
if sectionImage ~= '' then | |||
rawBody = rawBody:gsub('^%s*%[%[File:[^%]]+%]%]%s*', '', 1) | |||
end | |||
end | |||
local body, nextArticleNumber = forceOrderedList(rawBody, articleStartNumber) | |||
local count = countArticleLinks(body) | local count = countArticleLinks(body) | ||
local anchor = makeAnchorId(section.name) | local anchor = makeAnchorId(section.name) | ||
| Line 394: | Line 421: | ||
if sectionNumber then | if sectionNumber then | ||
sectionLabel = tostring(sectionNumber) .. '. ' .. sectionLabel | sectionLabel = tostring(sectionNumber) .. '. ' .. sectionLabel | ||
end | |||
local imageHtml = '' | |||
if sectionImage ~= '' then | |||
imageHtml = '<div style="margin-left:auto; flex:0 0 auto;">' .. sectionImage .. '</div>' | |||
end | end | ||
| Line 399: | Line 431: | ||
table.insert(out, '<div id="' .. anchor .. '" class="mw-collapsible' .. collapsedClass .. '" style="border:1px solid #e0d890; background:#fffdf0; margin:0.8em 0; padding:0.6em;">') | table.insert(out, '<div id="' .. anchor .. '" class="mw-collapsible' .. collapsedClass .. '" style="border:1px solid #e0d890; background:#fffdf0; margin:0.8em 0; padding:0.6em;">') | ||
table.insert(out, '<div style="font-weight:bold; margin-bottom:0.4em;">' .. escapeHtml(sectionLabel) .. ' (' .. count .. ') <span style="font-size:85%; font-weight:normal;">[[#Index| | table.insert(out, '<div style="display:flex; align-items:flex-start; gap:1em; font-weight:bold; margin-bottom:0.4em;"><div style="min-width:0;">' .. escapeHtml(sectionLabel) .. ' (' .. count .. ') <span style="font-size:85%; font-weight:normal;">[[#Index|Back to index]]</span></div>' .. imageHtml .. '</div>') | ||
-- IMPORTANT: | -- IMPORTANT: | ||
| Line 412: | Line 444: | ||
return table.concat(out, '\n'), nextArticleNumber | return table.concat(out, '\n'), nextArticleNumber | ||
end | end | ||
local function renderFullContents(sections) | local function renderFullContents(sections) | ||
if #sections == 0 then | if #sections == 0 then | ||
Latest revision as of 23:47, 20 May 2026
Module:PhysicsQC
This module renders a structured, collapsible table of contents for the Quantum Collection project.
It separates **content (Q)** from **logic (M)** and is invoked from a book or overview page (B).
Usage
Table of contents (198 articles)
Index
Core theory
Applications and extensions
Full contents
1. Foundations (14) Back to index
2. Conceptual and interpretations (14) Back to index
3. Mathematical structure and systems (15) Back to index
4. Atomic and spectroscopy (14) Back to index
5. Wavefunctions and modes (9) Back to index
6. Quantum dynamics and evolution (20) Back to index
7. Measurement and information (9) Back to index
8. Quantum information and computing (10) Back to index
9. Quantum optics and experiments (5) Back to index
10. Open quantum systems (9) Back to index
11. Quantum field theory (23) Back to index
12. Statistical mechanics and kinetic theory (9) Back to index
13. Condensed matter and solid-state physics (15) Back to index
162. Physics:Quantum well
167. Physics:Quantum dot
14. Plasma and fusion physics (8) Back to index
15. Timeline (8) Back to index
16. Advanced and frontier topics (16) Back to index
Architecture
- Q (source page) – contains structured content
- M (this module) – processes, counts, and renders
- B (book/page) – invokes the module
Source page (Q) format
The source page must contain:
<includeonly> == Core pathway == # [[Physics:Quantum basics]] # [[Physics:Quantum mechanics]] == Full contents == === Foundations === <li>[[Physics:Quantum basics]]</li> <li>[[Physics:Quantum mechanics]]</li> === Conceptual and interpretations === <li>[[Physics:Quantum Interpretations of quantum mechanics]]</li> === Quantum optics and experiments === <li>[[Physics:Quantum optics beam splitter experiments]]</li> [[Template:Quantum optics operators]] </includeonly>
Features
- Automatic total article count
- Automatic per-section counts
- Continuous numbering across sections
- Independent collapsible sections
- Clean rendering (no manual layout required)
- No JavaScript required
Counting rules
Only items matching:
...
are counted.
The following namespaces are excluded:
- Template:
- File:
- Image:
- Category:
- Help:
- Special:
Rendering behavior
Each section:
- is converted into a collapsible block
- receives an automatic item count
- is assigned correct numbering via
<ol start="...">
The module handles:
- layout
- numbering
- counting
The source page should NOT contain:
- manual collapsible blocks
- manual
<ol>orstart= - layout styling
Important notes
- Templates should NOT be inside
<li>if you want numbering to match totals. - Images and descriptive text are preserved inside sections.
- Section headings must use:
=== Section name ===
Output structure
The module returns:
= Table of content (N articles)= == Core pathway == ... == Full contents == [collapsible sections]
Maintenance
To update content:
- Edit the source page (Q)
- Do NOT modify the module (M)
The module automatically updates:
- numbering
- totals
- layout
Summary
- Q = content and structure
- M = logic and rendering
- B = invocation
This separation ensures scalability, consistency, and minimal maintenance.
local p = {}
-- Cache bump: Render section images inside Quantum index headers.
-- Cache bump: Fields chapter expanded to 12 entries.
-- ============================================================
-- Module:PhysicsQC
-- Quantum Collection table-of-contents and gallery renderer.
--
-- Restored rules:
-- * Index main groups: 2 columns.
-- * Book II / Book III / Book IV: one horizontal row below Index.
-- * Full contents: ONE column.
-- * Full contents preserves raw body, including images.
-- * Counts exclude File/Image/Book/etc.
-- * Gallery uses compact cards.
-- ============================================================
local function trim(s)
if not s then
return ''
end
return mw.text.trim(s)
end
local function escapeHtml(s)
return mw.text.encode(s or '')
end
local function getPageContent(pageName)
local title = mw.title.new(pageName or '')
if not title then
return ''
end
local text = title:getContent() or ''
-- Prevent visible <includeonly> / </includeonly> leaks from data/template pages.
text = text:gsub('<%s*/?%s*includeonly%s*>', '')
return text
end
local function getFramePageContent(frame, pageName)
if frame and frame.preprocess and pageName and pageName ~= '' then
local ok, text = pcall(function()
return frame:preprocess('{{:' .. pageName .. '}}')
end)
if ok and text and text ~= '' and text:match('==%s*Full contents%s*==') then
return text:gsub('<%s*/?%s*includeonly%s*>', '')
end
end
return getPageContent(pageName)
end
local function getCurrentPageName()
local t = mw.title.getCurrentTitle()
if not t then
return ''
end
return t.prefixedText or ''
end
local function isCurrentBookPage()
return getCurrentPageName():match('^Book:Quantum Collection') ~= nil
end
local function stripNamespaceForLabel(page)
page = page or ''
page = page:gsub('^Physics:Quantum data analysis/', '')
page = page:gsub('^Physics:Quantum methods/', '')
page = page:gsub('^Physics:Quantum materials/', '')
page = page:gsub('^Physics:Quantum matter/', '')
page = page:gsub('^Physics:Quantum atoms/', '')
page = page:gsub('^Physics:Quantum basics/', '')
page = page:gsub('^Physics:Quantum ', '')
page = page:gsub('^Physics:', '')
page = page:gsub('^Book:Quantum Collection/', '')
page = page:gsub('_', ' ')
return page
end
local function makeAnchorId(s)
s = s or ''
s = mw.text.decode(s, true)
s = s:gsub('<[^>]+>', '')
s = s:gsub(' ', ' ')
s = trim(s)
s = s:gsub('%s+', '_')
return mw.uri.anchorEncode(s)
end
local function isSkippedLink(link)
if not link or link == '' then
return true
end
link = trim(link)
if link:match('^#') then return true end
if link:match('^File:') then return true end
if link:match('^Image:') then return true end
if link:match('^Category:') then return true end
if link:match('^Help:') then return true end
if link:match('^Special:') then return true end
if link:match('^Template:') then return true end
if link:match('^Module:') then return true end
return false
end
local function isSkippedArticleCountLink(link)
if isSkippedLink(link) then return true end
if link:match('^Book:') then return true end
return false
end
local function parseWikiLink(raw)
raw = raw or ''
local target, label = raw:match('^([^|]+)|(.+)$')
if not target then
target = raw
label = stripNamespaceForLabel(raw)
end
target = trim(target)
label = trim(label)
return target, label
end
local function countArticleLinks(text)
local count = 0
local seen = {}
for raw in (text or ''):gmatch('%[%[([^%]]+)%]%]') do
local target = raw:match('^([^|#]+)') or raw
target = trim(target)
if not isSkippedArticleCountLink(target) and not seen[target] then
seen[target] = true
count = count + 1
end
end
return count
end
-- ============================================================
-- Parse == Index ==
-- ============================================================
local function parseIndex(content)
local indexText = content:match('==%s*Index%s*==%s*(.-)%s*==%s*Full contents%s*==')
if not indexText then
indexText = content:match('==%s*Index%s*==%s*(.*)') or ''
end
local groups = {}
local currentGroup = nil
for line in indexText:gmatch('[^\r\n]+') do
line = trim(line)
if line ~= '' then
local boldContent = line:match("^'''%s*(.-)%s*'''%s*$")
if boldContent then
currentGroup = {
name = trim(boldContent),
links = {}
}
table.insert(groups, currentGroup)
else
for raw in line:gmatch('%[%[([^%]]+)%]%]') do
local target, label = parseWikiLink(raw)
target = target:match('^([^#]+)') or target
target = trim(target)
if currentGroup and not isSkippedLink(target) then
table.insert(currentGroup.links, {
target = target,
label = label
})
end
end
end
end
end
return groups
end
local function isBookGroup(group)
if not group or not group.name then
return false
end
local name = group.name
if name == 'Book II' then return true end
if name == 'Book III' then return true end
if name == 'Book IV' then return true end
if name == 'Quantum Book II' then return true end
if name == 'Quantum Book III' then return true end
if name == 'Quantum Book IV' then return true end
return false
end
local function displayBookGroupName(name)
name = name or ''
name = name:gsub('^Quantum%s+', '')
return name
end
local function renderOrdinaryIndexGroup(group, startNumber)
local out = {}
local n = startNumber or 1
table.insert(out, '<div style="break-inside:avoid; margin-bottom:0.8em;">')
table.insert(out, '<div style="font-weight:bold; margin-bottom:0.25em;">' .. escapeHtml(group.name) .. '</div>')
if #group.links > 0 then
table.insert(out, '<div style="margin-top:0; margin-left:1.2em;">')
for _, link in ipairs(group.links) do
local anchor = makeAnchorId(link.label)
if link.target:match('^Book:') then
table.insert(out, '<div>' .. n .. '. [[' .. link.target .. '|' .. escapeHtml(link.label) .. ']]</div>')
n = n + 1
else
table.insert(out, '<div>' .. n .. '. [[#' .. anchor .. '|' .. escapeHtml(link.label) .. ']]</div>')
n = n + 1
end
end
table.insert(out, '</div>')
end
table.insert(out, '</div>')
return table.concat(out, '\n'), n
end
local function renderBookRow(bookGroups)
local out = {}
table.insert(out, '<div class="noexcerpt" style="margin:1em 0 0.8em 0;">')
table.insert(out, '<div style="display:flex; gap:2em; align-items:flex-start;">')
for _, group in ipairs(bookGroups) do
table.insert(out, '<div style="flex:1; min-width:0;">')
table.insert(out, '<div style="font-weight:bold; margin-bottom:0.25em;">' .. escapeHtml(displayBookGroupName(group.name)) .. '</div>')
if #group.links > 0 then
table.insert(out, '<ul style="margin-top:0;">')
for _, link in ipairs(group.links) do
if link.target:match('^Book:') then
table.insert(out, '<li>[[' .. link.target .. '|' .. escapeHtml(link.label) .. ']]</li>')
else
local anchor = makeAnchorId(link.label)
table.insert(out, '<li>[[#' .. anchor .. '|' .. escapeHtml(link.label) .. ']]</li>')
end
end
table.insert(out, '</ul>')
end
table.insert(out, '</div>')
end
table.insert(out, '</div>')
table.insert(out, '</div>')
return table.concat(out, '\n')
end
local function renderIndex(groups)
if #groups == 0 then
return ''
end
local normalGroups = {}
local bookGroups = {}
for _, group in ipairs(groups) do
if isBookGroup(group) then
table.insert(bookGroups, group)
else
table.insert(normalGroups, group)
end
end
local out = {}
table.insert(out, '= Index =')
table.insert(out, '<div class="noexcerpt" style="column-count:2; column-gap:2em;">')
local indexNumber = 1
for _, group in ipairs(normalGroups) do
local rendered
rendered, indexNumber = renderOrdinaryIndexGroup(group, indexNumber)
table.insert(out, rendered)
end
table.insert(out, '</div>')
if #bookGroups > 0 then
table.insert(out, renderBookRow(bookGroups))
end
return table.concat(out, '\n')
end
-- ============================================================
-- Parse == Full contents ==
-- ============================================================
local function parseFullContents(content)
local full = content:match('==%s*Full contents%s*==%s*(.*)') or ''
local sections = {}
local current = nil
for line in full:gmatch('[^\r\n]+') do
local heading = line:match('^===%s*(.-)%s*===%s*$')
if heading then
current = {
name = trim(heading),
lines = {}
}
table.insert(sections, current)
elseif current then
table.insert(current.lines, line)
end
end
return sections
end
local function forceOrderedList(body, startNumber)
body = body or ''
startNumber = startNumber or 1
-- Extra safety in case includeonly tags survive from copied content.
body = body:gsub('<%s*/?%s*includeonly%s*>', '')
-- If the data page already contains an ordered list, preserve it.
if body:match('<%s*ol[%s>]') then
return body, startNumber
end
local out = {}
local inList = false
local n = startNumber
for line in body:gmatch('([^\r\n]*)\r?\n?') do
if line ~= '' then
local liContent = line:match('^%s*<%s*li%s*>(.-)<%s*/%s*li%s*>%s*$')
if liContent then
if not inList then
table.insert(out, '<div style="margin-top:0; margin-left:1.6em;">')
inList = true
end
table.insert(out, '<div>' .. n .. '. ' .. liContent .. '</div>')
n = n + 1
else
if inList then
table.insert(out, '</div>')
inList = false
end
table.insert(out, line)
end
end
end
if inList then
table.insert(out, '</div>')
end
return table.concat(out, '\n'), n
end
local function renderSectionList(section, collapse, sectionNumber, articleStartNumber)
local rawBody = table.concat(section.lines, '\n')
local sectionImage = ''
local divImage = rawBody:match('^%s*<div[^>]-float%s*:%s*right.-%[%[File:[^%]]+%]%].-</div>')
if divImage then
sectionImage = divImage:match('%[%[File:[^%]]+%]%]') or ''
rawBody = rawBody:gsub('^%s*<div[^>]-float%s*:%s*right.-%[%[File:[^%]]+%]%].-</div>%s*', '', 1)
else
sectionImage = rawBody:match('^%s*(%[%[File:[^%]]+%]%])') or ''
if sectionImage ~= '' then
rawBody = rawBody:gsub('^%s*%[%[File:[^%]]+%]%]%s*', '', 1)
end
end
local body, nextArticleNumber = forceOrderedList(rawBody, articleStartNumber)
local count = countArticleLinks(body)
local anchor = makeAnchorId(section.name)
local collapsedClass = ''
if collapse then
collapsedClass = ' mw-collapsed'
end
local sectionLabel = section.name
if sectionNumber then
sectionLabel = tostring(sectionNumber) .. '. ' .. sectionLabel
end
local imageHtml = ''
if sectionImage ~= '' then
imageHtml = '<div style="margin-left:auto; flex:0 0 auto;">' .. sectionImage .. '</div>'
end
local out = {}
table.insert(out, '<div id="' .. anchor .. '" class="mw-collapsible' .. collapsedClass .. '" style="border:1px solid #e0d890; background:#fffdf0; margin:0.8em 0; padding:0.6em;">')
table.insert(out, '<div style="display:flex; align-items:flex-start; gap:1em; font-weight:bold; margin-bottom:0.4em;"><div style="min-width:0;">' .. escapeHtml(sectionLabel) .. ' (' .. count .. ') <span style="font-size:85%; font-weight:normal;">[[#Index|Back to index]]</span></div>' .. imageHtml .. '</div>')
-- IMPORTANT:
-- Full contents is intentionally ONE column and raw.
-- This preserves original list formatting and any images.
table.insert(out, '<div>')
table.insert(out, body)
table.insert(out, '</div>')
table.insert(out, '</div>')
return table.concat(out, '\n'), nextArticleNumber
end
local function renderFullContents(sections)
if #sections == 0 then
return ''
end
local out = {}
local collapse = false
table.insert(out, '= Full contents =')
local articleNumber = 1
for i, section in ipairs(sections) do
local rendered
rendered, articleNumber = renderSectionList(section, collapse, i, articleNumber)
table.insert(out, rendered)
end
return table.concat(out, '\n')
end
local function totalArticleCount(sections)
local n = 0
for _, section in ipairs(sections) do
n = n + countArticleLinks(table.concat(section.lines, '\n'))
end
return n
end
-- ============================================================
-- Public TOC functions
-- ============================================================
function p.tocHeading(frame)
local args = frame.args or {}
local dataPage = args[1] or 'Physics:Quantum basics/See also'
local content = getPageContent(dataPage)
local sections = parseFullContents(content)
local n = totalArticleCount(sections)
return '= Table of contents (' .. n .. ' articles) ='
end
function p.tocHeadingAndList(frame)
local args = frame.args or {}
local dataPage = args[1] or 'Physics:Quantum basics/See also'
local content = getPageContent(dataPage)
local groups = parseIndex(content)
local sections = parseFullContents(content)
local n = totalArticleCount(sections)
local out = {}
table.insert(out, '= Table of contents (' .. n .. ' articles) =')
table.insert(out, renderIndex(groups))
table.insert(out, renderFullContents(sections))
return table.concat(out, '\n\n')
end
-- ============================================================
-- Child-book wrappers
-- ============================================================
function p.qm(frame)
frame = frame or {}
frame.args = frame.args or {}
frame.args[1] = frame.args[1] or 'Physics:Quantum basics/See also/Matter'
return p.tocHeadingAndList(frame)
end
function p.qt(frame)
frame = frame or {}
frame.args = frame.args or {}
frame.args[1] = frame.args[1] or 'Physics:Quantum basics/See also/Methods'
return p.tocHeadingAndList(frame)
end
function p.qd(frame)
frame = frame or {}
frame.args = frame.args or {}
frame.args[1] = frame.args[1] or 'Physics:Quantum basics/See also/Data Analysis'
return p.tocHeadingAndList(frame)
end
-- ============================================================
-- Gallery helpers
-- ============================================================
local function extractPagesForGallery(content)
local pages = {}
local seen = {}
local full = content:match('==%s*Full contents%s*==%s*(.*)') or content
for raw in full:gmatch('%[%[([^%]]+)%]%]') do
local target = raw:match('^([^|#]+)') or raw
target = trim(target)
if not isSkippedLink(target) and not target:match('^Book:') and not seen[target] then
seen[target] = true
table.insert(pages, target)
end
end
return pages
end
local function firstFileInArticle(page)
local text = getPageContent(page)
if text == '' then
return nil
end
local file = text:match('%[%[%s*[Ff]ile%s*:%s*([^%]|%]]+)')
if not file then
file = text:match('%[%[%s*[Ii]mage%s*:%s*([^%]|%]]+)')
end
if file then
file = trim(file)
file = file:gsub('|.*$', '')
file = trim(file)
end
return file
end
local function normalizeGalleryWidth(value)
local width = tonumber(value) or 150
if width < 40 then
width = 40
elseif width > 400 then
width = 400
end
return math.floor(width)
end
local function renderGalleryCard(page, file, width)
local label = stripNamespaceForLabel(page)
local out = {}
table.insert(out, '<div style="display:inline-block; vertical-align:top; width:180px; margin:6px; padding:5px; border:1px solid #e0d890; background:#fff8cc; text-align:center;">')
if file then
table.insert(out, '[[File:' .. file .. '|' .. width .. 'px]]')
else
table.insert(out, '<div style="height:120px; display:flex; align-items:center; justify-content:center; border:1px dashed #c0a850; background:#fffdf0; color:#900; font-size:90%;">No image found</div>')
end
table.insert(out, '<div style="font-size:90%; line-height:1.25; margin-top:4px;">[[' .. page .. '|' .. escapeHtml(label) .. ']]</div>')
table.insert(out, '</div>')
return table.concat(out, '\n')
end
-- ============================================================
-- Public gallery function
-- ============================================================
function p.gallery(frame)
local args = frame.args or {}
local dataPage = args[1] or 'Physics:Quantum basics/See also'
local width = normalizeGalleryWidth(args.width or args[2])
local content = getPageContent(dataPage)
local pages = extractPagesForGallery(content)
local out = {}
local imageCount = 0
local missingCount = 0
local pageCount = #pages
table.insert(out, '<div class="noexcerpt" style="max-width:100%; clear:both;">')
for _, page in ipairs(pages) do
local file = firstFileInArticle(page)
if file then
imageCount = imageCount + 1
else
missingCount = missingCount + 1
end
table.insert(out, renderGalleryCard(page, file, width))
end
table.insert(out, '</div>')
local heading = '== Gallery (' .. imageCount .. ' images, ' .. missingCount .. ' missing, ' .. pageCount .. ' pages) =='
return heading .. '\n' .. table.concat(out, '\n')
end
return p















