Module:PhysicsQC: Difference between revisions
Jump to navigation
Jump to search
Move Quantum full contents images into right column |
Prevent trailing data wrappers from breaking Quantum image column layout |
||
| (2 intermediate revisions by the same user not shown) | |||
| Line 10: | Line 10: | ||
-- * Index main groups: 2 columns. | -- * Index main groups: 2 columns. | ||
-- * Book II / Book III / Book IV: one horizontal row below Index. | -- * Book II / Book III / Book IV: one horizontal row below Index. | ||
-- * Full contents: | -- * Full contents: 2 text columns plus a right image column. | ||
-- * Full contents preserves raw body; section images render beside the text. | -- * Full contents preserves raw body; section images render beside the text. | ||
-- * Counts exclude File/Image/Book/etc. | -- * Counts exclude File/Image/Book/etc. | ||
| Line 147: | Line 147: | ||
return count | return count | ||
end | |||
local function normalizeSectionImage(imageWikitext) | |||
imageWikitext = imageWikitext or '' | |||
local file = imageWikitext:match('%[%[%s*[Ff]ile%s*:%s*([^%]|%]]+)') | |||
if not file then | |||
file = imageWikitext:match('%[%[%s*[Ii]mage%s*:%s*([^%]|%]]+)') | |||
end | |||
if not file then | |||
return imageWikitext | |||
end | |||
file = trim(file) | |||
file = file:gsub('|.*$', '') | |||
file = trim(file) | |||
return '[[File:' .. file .. '|250px]]' | |||
end | end | ||
| Line 397: | Line 416: | ||
local rawBody = table.concat(section.lines, '\n') | local rawBody = table.concat(section.lines, '\n') | ||
local sectionImage = '' | local sectionImage = '' | ||
-- The source data page wraps Full contents in <ol><div>...</div></ol>. | |||
-- The final parsed section can inherit those closing tags; remove them so | |||
-- they cannot close the rendered section layout before the image column. | |||
rawBody = rawBody:gsub('%s*<%s*/%s*ol%s*>%s*<%s*/%s*div%s*>%s*$', '') | |||
rawBody = rawBody:gsub('%s*<%s*/%s*div%s*>%s*<%s*/%s*includeonly%s*>%s*$', '') | |||
local divImage = rawBody:match('^%s*<div[^>]-float%s*:%s*right.-%[%[File:[^%]]+%]%].-</div>') | local divImage = rawBody:match('^%s*<div[^>]-float%s*:%s*right.-%[%[File:[^%]]+%]%].-</div>') | ||
| Line 425: | Line 450: | ||
local imageHtml = '' | local imageHtml = '' | ||
if sectionImage ~= '' then | if sectionImage ~= '' then | ||
imageHtml = '<div style="flex:0 0 270px; max-width:270px; margin-left:1em;">' .. sectionImage .. '</div>' | imageHtml = '<div style="flex:0 0 270px; max-width:270px; margin-left:1em; text-align:center;">' .. normalizeSectionImage(sectionImage) .. '</div>' | ||
end | end | ||
| Line 433: | Line 458: | ||
table.insert(out, '<div style="font-weight:bold; margin-bottom:0.4em;">' .. escapeHtml(sectionLabel) .. ' (' .. count .. ') <span style="font-size:85%; font-weight:normal;">[[#Index|Back to index]]</span></div>') | table.insert(out, '<div style="font-weight:bold; margin-bottom:0.4em;">' .. escapeHtml(sectionLabel) .. ' (' .. count .. ') <span style="font-size:85%; font-weight:normal;">[[#Index|Back to index]]</span></div>') | ||
-- Full contents text is split into columns with the section image in a right column. | -- Full contents text is split into two columns with the section image in a right column. | ||
table.insert(out, '<div style="display:flex; align-items:flex-start; gap:1em;">') | table.insert(out, '<div style="display:flex; align-items:flex-start; gap:1em;">') | ||
table.insert(out, '<div style="flex:1 1 auto; min-width:0; column-count: | table.insert(out, '<div style="flex:1 1 auto; min-width:0; column-count:2; column-gap:2em; column-rule:1px solid #eadf9a;">') | ||
table.insert(out, body) | table.insert(out, body) | ||
table.insert(out, '</div>') | table.insert(out, '</div>') | ||
Latest revision as of 18:37, 21 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 (199 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 (21) 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
163. Physics:Quantum well
168. 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: 2 text columns plus a right image column.
-- * Full contents preserves raw body; section images render beside the text.
-- * 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
local function normalizeSectionImage(imageWikitext)
imageWikitext = imageWikitext or ''
local file = imageWikitext:match('%[%[%s*[Ff]ile%s*:%s*([^%]|%]]+)')
if not file then
file = imageWikitext:match('%[%[%s*[Ii]mage%s*:%s*([^%]|%]]+)')
end
if not file then
return imageWikitext
end
file = trim(file)
file = file:gsub('|.*$', '')
file = trim(file)
return '[[File:' .. file .. '|250px]]'
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 = ''
-- The source data page wraps Full contents in <ol><div>...</div></ol>.
-- The final parsed section can inherit those closing tags; remove them so
-- they cannot close the rendered section layout before the image column.
rawBody = rawBody:gsub('%s*<%s*/%s*ol%s*>%s*<%s*/%s*div%s*>%s*$', '')
rawBody = rawBody:gsub('%s*<%s*/%s*div%s*>%s*<%s*/%s*includeonly%s*>%s*$', '')
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="flex:0 0 270px; max-width:270px; margin-left:1em; text-align:center;">' .. normalizeSectionImage(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="font-weight:bold; margin-bottom:0.4em;">' .. escapeHtml(sectionLabel) .. ' (' .. count .. ') <span style="font-size:85%; font-weight:normal;">[[#Index|Back to index]]</span></div>')
-- Full contents text is split into two columns with the section image in a right column.
table.insert(out, '<div style="display:flex; align-items:flex-start; gap:1em;">')
table.insert(out, '<div style="flex:1 1 auto; min-width:0; column-count:2; column-gap:2em; column-rule:1px solid #eadf9a;">')
table.insert(out, body)
table.insert(out, '</div>')
table.insert(out, imageHtml)
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