Module:GetRels
From Bodhicitta
Documentation for this module may be created at Module:GetRels/doc
local p = {}
-- Configuration
local RELATIONSHIP_LIMIT = 500
local FILTER_THRESHOLD_RELS = 5
local FILTER_THRESHOLD_PAGES = 10
-- Show the class pills only once there are more rows than this in total.
local FILTER_THRESHOLD_ROWS = 10
-- Relationship display name mappings
local REL_NAMES = {
-- Not a RelatedTo relationship. Generated from the local TextTopics property
-- by fetchTopicOfRelationships, and only ever in one direction, so both
-- labels are the same.
["Is a topic of"] = {forward = 'A topic of', inverse = 'A topic of'},
["Contains a full translation of"] = {forward = 'Full translation of', inverse = 'Full translations'},
["Contains a modern commentary of"] = {forward = 'Modern commentary of', inverse = 'Modern commentaries'},
["Contains a partial translation of"] = {forward = 'Partial translation of', inverse = 'Partial translations'},
["Contains a translated commentary of"] = {forward = 'Translated commentary of', inverse = 'Translated Commentaries'},
["Contains a recension of"] = {forward = 'Recension of', inverse = 'Recensions'},
["Contains a review of"] = {forward = 'Review of', inverse = 'Reviews'},
["Contains a teaching based on"] = {forward = 'Teaching based on', inverse = 'Works containing teachings'},
["Contains a translation of"] = {forward = 'Translation of', inverse = 'Translations'},
["Contains polemical content related to"] = {forward = 'Polemical content on', inverse = 'Polemical content'},
["Contains scholarship related to"] = {forward = 'Scholarship on', inverse = 'Scholarship'},
["Has a title identical or similar to"] = {forward = 'Similar title', inverse = 'Similar title'},
["Has other volumes"] = {forward = 'Other volumes', inverse = 'Other volumes'},
["Is a book based on this dissertation"] = {forward = 'Based on dissertation', inverse = 'Based on dissertation'},
["Is a chapter or a part of"] = {forward = 'Chapter or part of', inverse = 'Contains chapter or part'},
["Is a commentary of"] = {forward = 'Classical commentary of', inverse = 'Classical commentaries'},
["Is a member of"] = {forward = 'Member of', inverse = 'Contains members'},
["Is a reading of"] = {forward = 'Reading of', inverse = 'Readings'},
["Is a scholarly presentation about"] = {forward = 'Scholarly presentation about', inverse = 'Scholarly presentations'},
["Is a teaching based on"] = {forward = 'Teaching on', inverse = 'Teachings'},
["Is cited in"] = {forward = 'Cited in', inverse = 'Citations'},
["Is an interview about"] = {forward = 'Interview about', inverse = 'Interviews'},
["Is another edition of"] = {forward = 'Other editions', inverse = 'Other editions'},
["Is related to"] = {forward = 'Related', inverse = 'Related'},
["Shares intertextuality with"] = {forward = 'Shares intertextuality', inverse = 'Shares intertextuality'}
}
-- Relationship directions (horizontal relationships are symmetric/peer-level)
-- All others are vertical (hierarchical/directional)
local HORIZONTAL_RELS = {
["Has other volumes"] = true,
["Has a title identical or similar to"] = true,
["Shares intertextuality with"] = true,
["Is another edition of"] = true
}
-- Relationship display priority (lower number = higher priority)
local REL_PRIORITY = {
["Contains a recension of"] = 1,
["Contains a full translation of"] = 2,
["Contains a translation of"] = 3,
["Is a commentary of"] = 4,
["Contains a modern commentary of"] = 5,
["Contains a translated commentary of"] = 6,
["Contains a partial translation of"] = 7,
["Is another edition of"] = 8,
["Has other volumes"] = 9,
["Has a title identical or similar to"] = 10,
["Is a reading of"] = 11,
["Is a book based on this dissertation"] = 12,
["Is a chapter or a part of"] = 13,
["Is a member of"] = 14,
["Is a teaching based on"] = 15,
["Contains a teaching based on"] = 16,
["Contains a review of"] = 17,
["Is cited in"] = 18,
["Contains scholarship related to"] = 19,
["Is a scholarly presentation about"] = 20,
["Is an interview about"] = 21,
["Shares intertextuality with"] = 22,
["Is related to"] = 23,
["Contains polemical content related to"] = 24,
-- TextTopics is not one of the RelatedTo relationships; it is a separate
-- local property. It is listed last so it reads as broader context after the
-- specific bibliographic relationships above.
["Is a topic of"] = 25
}
-- Classification to namespace mapping
local CLASS_NAMESPACE = {
['Articles'] = 'Articles',
['Books'] = 'Books',
['Classical Works'] = 'Texts',
['Dissertations'] = 'Books',
['Multimedia'] = 'Media',
['Periodical Issues'] = 'Books',
['Sanskrit Publications'] = 'Books',
['Tibetan Publications'] = 'Books'
}
-- Helper function: Check if a relationship is horizontal (symmetric)
local function isHorizontalRelationship(rel)
return HORIZONTAL_RELS[rel] == true
end
-- Helper function: Fetch outgoing relationships for a page (page has property pointing to other pages)
-- Single query fetches all relationship properties at once instead of one query per property.
-- limit=1 restricts to 1 subject row (pageA itself); multi-valued properties are returned in full
-- regardless of the row limit, so RELATIONSHIP_LIMIT no longer applies here.
local function fetchOutgoingRelationships(pageA)
local queryArgs = { '[[' .. pageA .. ']]', limit = 1 }
for relName, _ in pairs(REL_NAMES) do
-- Use ?#- operator to get unlinked (plain text) values
table.insert(queryArgs, '?' .. relName .. '#-')
end
local query = mw.smw.ask(queryArgs)
if not query or not query[1] then return nil end
local results = {}
local row = query[1]
for relName, _ in pairs(REL_NAMES) do
local targets = row[relName]
if targets then
-- Handle both single value and table of values
if type(targets) == 'table' then
for _, target in ipairs(targets) do
table.insert(results, {Relationship = relName, RelatedTo = target})
end
else
table.insert(results, {Relationship = relName, RelatedTo = targets})
end
end
end
return #results > 0 and results or nil
end
-- Helper function: Fetch incoming relationships (other pages have property pointing to this page).
-- Also fetches Classification, Display title of, MediaSeries, Volumenumber, and tile fields for
-- each result row so that downstream checks (isLibraryItem, isMultiVolumeSeries, buildPageLink,
-- generateTile) can use the cached data instead of issuing additional per-page queries.
local function fetchIncomingRelationships(pageA, pageFull)
local results = {}
-- Match either the bare or the full title. Relationship properties store the
-- full title, but accepting both keeps this working for pages that are not
-- subpages, and for any entry recorded the other way round.
local target = pageA
if pageFull and pageFull ~= pageA then
target = pageA .. '||' .. pageFull
end
for relName, _ in pairs(REL_NAMES) do
local query = mw.smw.ask{
'[[' .. relName .. '::' .. target .. ']]',
'?#-=Page',
'?Classification=Classification',
'?Display title of=Title',
'?MediaSeries=MediaSeries',
'?Volumenumber=Volumenumber',
'?Class=Class',
'?TileDescription=TileDescription',
'?TileImageUrl=TileImageUrl',
'?Citation=Citation',
'?Pubdate=Pubdate',
'?FrontCoverImg#=FrontCoverImg',
'?BcaAuthor#=BcaAuthor',
'?Description=Description',
'?FullTitleTib=FullTitleTib',
'?DescriptionTib=DescriptionTib',
'?TileLabel=TileLabel',
limit = RELATIONSHIP_LIMIT
}
if query then
for _, row in ipairs(query) do
local sourcePage = row.Page
if sourcePage then
table.insert(results, {
Page = sourcePage,
Relationship = relName,
RelatedTo = pageA,
Classification = row.Classification,
Title = row.Title,
MediaSeries = row.MediaSeries,
Volumenumber = row.Volumenumber,
Class = row.Class,
TileDescription = row.TileDescription,
TileImageUrl = row.TileImageUrl,
Citation = row.Citation,
Pubdate = row.Pubdate,
FrontCoverImg = row.FrontCoverImg,
BcaAuthor = row.BcaAuthor,
Description = row.Description,
FullTitleTib = row.FullTitleTib,
DescriptionTib = row.DescriptionTib,
TileLabel = row.TileLabel,
})
end
end
end
end
return #results > 0 and results or nil
end
-- Helper function: Build a lookup table of page metadata from the outgoing targets.
-- Fetches all fields needed for tile rendering plus library/series checks,
-- for all outgoing target pages in a single query, keyed by plain page name.
local function fetchOutgoingPageData(targets)
if not targets or #targets == 0 then return {} end
-- Build OR condition: [[PageA]]OR[[PageB]]OR...
local conditions = {}
for _, page in ipairs(targets) do
table.insert(conditions, '[[' .. page .. ']]')
end
local query = mw.smw.ask{
table.concat(conditions, 'OR'),
'?#-=Page',
'?Classification=Classification',
'?Display title of=Title',
'?MediaSeries=MediaSeries',
'?Volumenumber=Volumenumber',
'?Class=Class',
'?TileDescription=TileDescription',
'?TileImageUrl=TileImageUrl',
'?Citation=Citation',
'?Pubdate=Pubdate',
'?FrontCoverImg#=FrontCoverImg',
'?BcaAuthor#=BcaAuthor',
'?Description=Description',
'?FullTitleTib=FullTitleTib',
'?DescriptionTib=DescriptionTib',
'?TileLabel=TileLabel',
limit = #targets
}
local cache = {}
if query then
for _, row in ipairs(query) do
if row.Page then
cache[row.Page] = {
Classification = row.Classification,
Title = row.Title,
MediaSeries = row.MediaSeries,
Volumenumber = row.Volumenumber,
Class = row.Class,
TileDescription = row.TileDescription,
TileImageUrl = row.TileImageUrl,
Citation = row.Citation,
Pubdate = row.Pubdate,
FrontCoverImg = row.FrontCoverImg,
BcaAuthor = row.BcaAuthor,
Description = row.Description,
FullTitleTib = row.FullTitleTib,
DescriptionTib = row.DescriptionTib,
TileLabel = row.TileLabel,
}
end
end
end
return cache
end
-- Helper function: Check if page is a Library Item using pre-fetched metadata.
-- A page with a Classification value is a Library Item on this wiki.
-- Falls back to a targeted SMW query only when Classification is absent, which handles
-- Classical Works stored under a Texts/ prefix or linked via the KeyTextPage property.
local function isLibraryItem(page, cachedData)
local classification = cachedData and cachedData.Classification
if type(classification) == 'table' then classification = classification[1] end
if classification and classification ~= '' then
return true
end
-- Fallback for pages not found in cache (Texts/ prefix or KeyTextPage variants)
local fallback = mw.smw.ask{
'[[Category:Library Items]][[Texts/' .. page .. ']]OR[[Category:Library Items]][[KeyTextPage::' .. page .. ']]',
limit = 1
}
return fallback ~= nil and #fallback > 0
end
-- Helper function: Check if page is a multi-volume series using pre-fetched data.
local function isMultiVolumeSeries(cachedData)
if not cachedData then return false end
local hasSeries = cachedData.MediaSeries and cachedData.MediaSeries ~= ''
local vol = cachedData.Volumenumber
local volNum = type(vol) == 'table' and tonumber(vol[1]) or tonumber(vol)
return hasSeries and volNum and volNum > 1
end
-- Helper function: Build page link with namespace using pre-fetched metadata.
local function buildPageLink(page, cachedData)
local classification = cachedData and cachedData.Classification
local title = cachedData and cachedData.Title
-- Classification may be a table if multi-valued (take first)
if type(classification) == 'table' then classification = classification[1] end
if type(title) == 'table' then title = title[1] end
local pagename = (title and title ~= '') and title or page
-- Remove RKTS identifiers
pagename = string.gsub(pagename, '%s?%(RKTS%w?%s?%d*%)', '')
local namespace = CLASS_NAMESPACE[classification] or classification or ''
if namespace ~= '' then
return namespace .. '/' .. pagename
end
return pagename
end
-- Helper function: Remove duplicates from indexed table
local function makeTableUnique(tbl)
local seen = {}
local result = {}
for _, value in ipairs(tbl) do
if not seen[value] then
seen[value] = true
table.insert(result, value)
end
end
return result
end
-- Helper function: Split string by delimiter
local function splitString(str, delimiter)
local result = {}
for match in string.gmatch(str, "([^" .. delimiter .. "]+)") do
table.insert(result, match)
end
return result
end
-- Helper function: Get display name for relationship
local function getRelationshipName(rel, direction)
if REL_NAMES[rel] then
return direction == 'forward' and REL_NAMES[rel].forward or REL_NAMES[rel].inverse
end
return rel
end
-- Helper function: Process outgoing relationships
local function processOutgoingRelationships(frame, data, allRels)
if not data then
return
end
for _, item in ipairs(data) do
local rel = item.Relationship
if not allRels[rel] then
allRels[rel] = {}
end
if allRels[rel].right then
allRels[rel].right = allRels[rel].right .. '~' .. item.RelatedTo
else
allRels[rel].right = item.RelatedTo
end
end
end
-- Helper function: Process incoming relationships.
-- pageCache receives pre-fetched metadata keyed by plain page name.
-- Incoming page metadata (fetched inline) is written into pageCache here so that
-- generateRelationshipSection can look it up by page name later.
local function processIncomingRelationships(frame, data, allRels, pageA, pageCache)
if not data then
return
end
for _, item in ipairs(data) do
local pageB = frame:preprocess('{{PAGENAME:' .. item.Page .. '}}')
local rel = item.Relationship
if not allRels[rel] then
allRels[rel] = {}
end
-- Build cachedData from inline fields; fall back to pageCache entry if sparse
local cachedData = {
Classification = item.Classification,
Title = item.Title,
MediaSeries = item.MediaSeries,
Volumenumber = item.Volumenumber,
Class = item.Class,
TileDescription = item.TileDescription,
TileImageUrl = item.TileImageUrl,
FullTitleTib = item.FullTitleTib,
DescriptionTib = item.DescriptionTib,
TileLabel = item.TileLabel,
-- Row fields. This table is rebuilt field by field rather than
-- copied, so anything added to the queries must be listed here too
-- or it is silently dropped before it reaches the row.
Citation = item.Citation,
Pubdate = item.Pubdate,
FrontCoverImg = item.FrontCoverImg,
BcaAuthor = item.BcaAuthor,
Description = item.Description,
}
if not cachedData.Classification and pageCache[pageB] then
cachedData = pageCache[pageB]
end
-- Store in pageCache so generateRelationshipSection can find it by name
if not pageCache[pageB] then
pageCache[pageB] = cachedData
end
-- Skip if this is part of a multi-volume series (uses cached data, no extra query)
if not isMultiVolumeSeries(cachedData) then
if not isHorizontalRelationship(rel) then
if allRels[rel].left then
allRels[rel].left = allRels[rel].left .. '~' .. pageB
else
allRels[rel].left = pageB
end
elseif not allRels[rel].right then
-- Horizontal relationships are symmetric. Only use the incoming pass as a fallback
-- when the outgoing pass found nothing (i.e. the property is only set on page B side).
allRels[rel].right = pageB
end
end
end
end
-- Helper function: Check if filters are needed
local function shouldShowFilters(allRels)
local relCount = 0
for _ in pairs(allRels) do
relCount = relCount + 1
end
if relCount > FILTER_THRESHOLD_RELS then
return true
end
for _, dirs in pairs(allRels) do
for _, pages in pairs(dirs) do
local pageCount = 0
for _ in string.gmatch(pages, "([^~]+)") do
pageCount = pageCount + 1
end
if pageCount > FILTER_THRESHOLD_PAGES then
return true
end
end
end
return false
end
-- Helper function: Generate filter widgets
local function generateFilters(frame)
local filterRels = frame:callParserFunction{
name = '#widget',
args = {
'FilterRelationships',
id = 'Relationships',
placeholder = 'Filter by relationships...'
}
}
local filterPages = frame:callParserFunction{
name = '#widget',
args = {
'FilterRelationships',
id = 'Pages',
placeholder = 'Filter by pages found...'
}
}
return '<div class="row"><div class="col-sm-6">' .. filterRels .. '</div><div class="col-sm-6">' .. filterPages .. '</div></div>'
end
-- Helper function: Generate tile for existing page using pre-fetched cached data.
-- Calls RelationshipTile directly, bypassing the RelTileQuery #ask entirely.
local function generateTile(frame, page, cachedData)
local function firstVal(v)
if type(v) == 'table' then return v[1] or '' end
return v or ''
end
-- Rendered with TopicBiblioRow, the same row used on topic pages and for the
-- Explore section above, so a work looks the same wherever it is met.
--
-- The row falls back Citation -> Title -> page name, so entries with no
-- citation still read correctly, and it does its own reverse
-- AssociatedArticle lookup for narration. Nothing here needs to know about
-- either.
return frame:expandTemplate{
title = 'TopicBiblioRow',
args = {
[1] = page,
Citation = firstVal(cachedData and cachedData.Citation),
BcaAuthor = firstVal(cachedData and cachedData.BcaAuthor),
Title = firstVal(cachedData and cachedData.Title),
Class = firstVal(cachedData and cachedData.Class),
Pubdate = firstVal(cachedData and cachedData.Pubdate),
FrontCoverImg = firstVal(cachedData and cachedData.FrontCoverImg),
TileDescription = firstVal(cachedData and cachedData.TileDescription),
Description = firstVal(cachedData and cachedData.Description),
FullTitleTib = firstVal(cachedData and cachedData.FullTitleTib),
DescriptionTib = firstVal(cachedData and cachedData.DescriptionTib),
}
}
end
-- Helper function: Generate tile for non-existent page
local function generateNoExistTile(frame, page, cachedData)
local pageLink = buildPageLink(page, cachedData)
local parts = {
'<div class="col-md-6 mb-3 mb-md-4 filterable">',
'<div class="tsdwiki-pop-off tsdwiki-tile tsdwiki-tile-library">',
'<div class="tsdwiki-tile-library-content" style="flex: 0 0 100%; max-width: 100%;">',
'<div class="h4 mb-2 p-0 clipped-2-lines border-0 text-140 line13 carnelian">',
pageLink,
'</div>',
'<div class="text-muted clipped-3-lines unclamp-here carnelian">',
'<b>PAGE MISSING!</b> Please click to create this library item or the redirect that is missing to direct it to the appropriate page.',
'</div>',
'<div class="mt-1 mb-0 clipped-1-line carnelian font-italic text-right">',
'Click to create <i class="fa-sharp-duotone fa-solid fa-rectangle-history-circle-plus"></i>',
'</div></div>',
'{{NoExistTileLink|pageLink=' .. pageLink .. '}}',
'</div></div>',
'[[Category:Pages with red relationship tiles]]'
}
return frame:preprocess(table.concat(parts))
end
-- Helper function: Generate output for a relationship section.
-- pageCache is a lookup table of pre-fetched metadata keyed by plain page name.
-- Works that name this page as a topic, via the local TextTopics property.
--
-- This is not a RelatedTo relationship, so it is fetched separately, but the
-- rows carry the same printouts as fetchIncomingRelationships so they can be
-- rendered by the same tile code and appear as one more section.
local function fetchTopicOfRelationships(pageA)
local query = mw.smw.ask{
'[[TextTopics::' .. pageA .. ']]',
'?#-=Page',
'?Classification=Classification',
'?Display title of=Title',
'?MediaSeries=MediaSeries',
'?Volumenumber=Volumenumber',
'?Class=Class',
'?TileDescription=TileDescription',
'?TileImageUrl=TileImageUrl',
'?Citation=Citation',
'?Pubdate=Pubdate',
'?FrontCoverImg#=FrontCoverImg',
'?BcaAuthor#=BcaAuthor',
'?Description=Description',
'?FullTitleTib=FullTitleTib',
'?DescriptionTib=DescriptionTib',
'?TileLabel=TileLabel',
limit = RELATIONSHIP_LIMIT
}
if not query then
return nil
end
local results = {}
for _, row in ipairs(query) do
if row.Page then
table.insert(results, {
Page = row.Page,
Relationship = 'Is a topic of',
RelatedTo = pageA,
Classification = row.Classification,
Title = row.Title,
MediaSeries = row.MediaSeries,
Volumenumber = row.Volumenumber,
Class = row.Class,
TileDescription = row.TileDescription,
TileImageUrl = row.TileImageUrl,
Citation = row.Citation,
Pubdate = row.Pubdate,
FrontCoverImg = row.FrontCoverImg,
BcaAuthor = row.BcaAuthor,
Description = row.Description,
FullTitleTib = row.FullTitleTib,
DescriptionTib = row.DescriptionTib,
TileLabel = row.TileLabel,
})
end
end
if #results == 0 then
return nil
end
return results
end
-- Helper function: Build the controls shown above the sections — a text
-- filter and, when there is more than one class, the class pills.
--
-- Same markup and same behaviour as the pills on topic pages: tws.js keys on
-- .topic-biblio-pill and matches data-filter-class against each row's
-- data-class, so nothing extra is needed on the JS side.
--
-- Only worth showing when there is enough to filter. Below the threshold the
-- reader can already see everything, and a row of pills would be more clutter
-- than help.
local function generateControls(frame, classTally)
local total = classTally.__total or 0
if total <= FILTER_THRESHOLD_ROWS then
return ''
end
-- Ordered, not alphabetical: Editorial Content first, matching topic pages,
-- then the rest in a stable reading order. Anything not listed follows,
-- sorted, so a new Class still appears without a code change.
local ORDER = {'Editorial Content', 'Books', 'Articles', 'Classical Works',
'Multimedia', 'Dissertations', 'Tibetan Publications', 'Sanskrit Publications',
'Periodical Issues'}
local seen, ordered = {}, {}
for _, cls in ipairs(ORDER) do
if classTally[cls] then
table.insert(ordered, cls)
seen[cls] = true
end
end
local rest = {}
for cls in pairs(classTally) do
if cls ~= '__total' and not seen[cls] then
table.insert(rest, cls)
end
end
table.sort(rest)
for _, cls in ipairs(rest) do
table.insert(ordered, cls)
end
-- Fewer than two classes means the pills would be pointless — "All" and the
-- one class filter to the same set — but the text field is still worth
-- having, so only the pill row is dropped, not the whole control bar.
local showPills = #ordered >= 2
-- No newline before the first pill. MediaWiki wraps a run that starts on its
-- own line in a <p>, which then becomes the only flex child and collapses
-- the pill spacing. Same trap as on the topic page template.
local out = {'<div class="topic-biblio-controls">'}
-- Text filter. The widget emits the same .filter-input the rest of the wiki
-- uses, and tws.js already handles it: it matches accent-insensitively over
-- each .filterable row's text, highlights the hits, and re-applies the class
-- filter afterwards so the two controls compose instead of fighting.
-- Plain wrapper, not a Bootstrap .row: the grid's negative x-margins have no
-- container here to absorb them and push out a horizontal scrollbar. Width
-- and centring are done in CSS instead.
table.insert(out, '<div class="topic-biblio-filter">')
table.insert(out, frame:callParserFunction{
name = '#widget',
args = {
'filterInput',
placeholder = 'Filter ' .. total .. ' related works...'
}
})
table.insert(out, '</div>')
if showPills then
table.insert(out, '<div class="topic-biblio-pills">')
table.insert(out, '<span class="topic-biblio-pill is-active" data-filter-class="">All <span class="rel-count">'
.. total .. '</span></span>')
for _, cls in ipairs(ordered) do
table.insert(out, '<span class="topic-biblio-pill" data-filter-class="' .. cls .. '">'
.. cls .. ' <span class="rel-count">' .. classTally[cls] .. '</span></span>')
end
table.insert(out, '</div>')
end
table.insert(out, '</div>')
return table.concat(out)
end
local function generateRelationshipSection(frame, rel, dir, pages, pageCache, classTally)
-- Determine display name
local relName = getRelationshipName(rel, dir == 'left' and 'inverse' or 'forward')
-- Process pages, collecting only existing ones
local pagesList = makeTableUnique(splitString(pages, '~'))
local tiles = {}
for _, page in ipairs(pagesList) do
local cachedData = pageCache[page]
if isLibraryItem(page, cachedData) then
-- Page exists on this wiki — render tile directly from cached data, no extra query
table.insert(tiles, generateTile(frame, page, cachedData))
-- Tally the class for the filter pills. Counted here, from what is
-- actually rendered, so a pill can never claim more than is shown.
-- A work can appear under several relationships, so this counts row
-- instances rather than distinct works — which is what the pills
-- filter over.
if classTally then
local cls = cachedData and cachedData.Class
if type(cls) == 'table' then cls = cls[1] end
if cls and cls ~= '' then
classTally[cls] = (classTally[cls] or 0) + 1
classTally.__total = (classTally.__total or 0) + 1
end
end
-- else
-- -- Page not found on this wiki — show red "PAGE MISSING" tile
-- table.insert(tiles, generateNoExistTile(frame, page, cachedData))
end
end
-- Hide entire section if no related pages exist on this wiki
if #tiles == 0 then
return ''
end
-- Count comes from #tiles rather than #pagesList, so it reflects what is
-- actually rendered. Pages that do not exist on this wiki are filtered out
-- above, and a heading claiming more items than are shown would be wrong.
-- topic-biblio-section / -list, not the old Bootstrap grid: these are the
-- full-width rows used on topic pages, not two-column tiles. The grid's
-- own "row" class would also collide with .topic-biblio-row.
-- No .filterableContent here: one wrapper spans every section (see main),
-- so the text filter reaches all rows rather than only the section it sits in.
local output = {'<div class="clearfix topic-biblio-section">'}
table.insert(output, '<div class="h4 mx-3"><span class="rel-count">' .. #tiles ..
'</span> ' .. relName .. '</div><div class="topic-biblio-list">')
for _, tile in ipairs(tiles) do
table.insert(output, tile)
end
table.insert(output, '</div></div>')
return table.concat(output)
end
-- Main function
function p.main(frame)
-- Get current page name.
--
-- Two forms are needed, because the two property families on this wiki store
-- their targets differently. The relationship properties store the FULL page
-- title (Texts/Bodhipathapradīpa) while TextTopics stores the BARE subpage
-- name (Bodhipathapradīpa). Querying only the bare form, as this module used
-- to, made every relationship section come back empty on a subpage while
-- "A topic of" still worked — which read as the topic section crowding the
-- others out, when in fact the others were never being found.
local pageA = mw.title.getCurrentTitle().subpageText
local pageFull = mw.title.getCurrentTitle().prefixedText
-- Fetch relationships using local SMW queries
local outgoingRels = fetchOutgoingRelationships(pageA)
local incomingRels = fetchIncomingRelationships(pageA, pageFull)
local topicOfRels = fetchTopicOfRelationships(pageA)
-- Return empty if nothing relates to this page at all
if not outgoingRels and not incomingRels and not topicOfRels then
return ''
end
-- Collect all unique outgoing target page names so we can batch-fetch their metadata.
-- Incoming results already carry inline metadata from fetchIncomingRelationships.
local outgoingTargets = {}
local seenTargets = {}
if outgoingRels then
for _, item in ipairs(outgoingRels) do
if not seenTargets[item.RelatedTo] then
seenTargets[item.RelatedTo] = true
table.insert(outgoingTargets, item.RelatedTo)
end
end
end
-- One query for all outgoing targets: Classification, Display title, MediaSeries, Volumenumber, tile fields
local pageCache = fetchOutgoingPageData(outgoingTargets)
-- Organize relationships (incoming metadata comes inline; outgoing uses pageCache)
local allRels = {}
processOutgoingRelationships(frame, outgoingRels, allRels)
processIncomingRelationships(frame, incomingRels, allRels, pageA, pageCache)
-- TextTopics rows carry the same fields as incoming rows, so they go through
-- the same processor and land in allRels as one more relationship.
processIncomingRelationships(frame, topicOfRels, allRels, pageA, pageCache)
-- Build output
local output = {}
-- -- Add filters if needed
-- if shouldShowFilters(allRels) then
-- table.insert(output, generateFilters(frame))
-- end
-- Sort relationships by priority
local sortedRels = {}
for rel, _ in pairs(allRels) do
table.insert(sortedRels, rel)
end
table.sort(sortedRels, function(a, b)
local prioA = REL_PRIORITY[a] or 999
local prioB = REL_PRIORITY[b] or 999
return prioA < prioB
end)
-- Sections are built first so the pills can be counted from what was
-- actually rendered, then prepended. Counting up front would mean counting
-- pages that get filtered out as non-existent further down.
local sections = {}
local classTally = {}
for _, rel in ipairs(sortedRels) do
local dirs = allRels[rel]
for dir, pages in pairs(dirs) do
table.insert(sections, generateRelationshipSection(frame, rel, dir, pages, pageCache, classTally))
end
end
table.insert(output, generateControls(frame, classTally))
-- One .filterableContent around every section, so the single text field
-- filters the whole relationships area rather than one section of it.
table.insert(output, '<div class="filterableContent">')
for _, section in ipairs(sections) do
table.insert(output, section)
end
table.insert(output, '</div>')
return table.concat(output)
end
return p