Module:GetRels: Difference between revisions

From Bodhicitta
((by SublimeText.Mediawiker))
((by SublimeText.Mediawiker))
Line 1: Line 1:
local p = {}
local p = {}


function p.main(frame)
-- Configuration
local RESEARCH_WIKI_BASE = 'https://research.tsadra.org/index.php?title=Special%3AAsk'
local RELATIONSHIP_LIMIT = 10000
local FILTER_THRESHOLD_RELS = 5
local FILTER_THRESHOLD_PAGES = 10


local debugStr = ''
-- Relationship display name mappings
local output = ''
local REL_NAMES = {
["Contains a full translation of"] = {forward = 'Translation of', inverse = 'Full translations'},
["Contains a modern commentary of"] = {forward = 'Commentary of', inverse = '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 = '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 = 'Commentary of', inverse = '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 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'}
}


-- get local pagename
-- Classification to namespace mapping
local pageA = mw.title.getCurrentTitle().subpageText
local CLASS_NAMESPACE = {
-- if pageA == "Bodhicaryāvatāra" then
['Articles'] = 'Articles',
-- return ''
['Books'] = 'Books',
-- else
['Classical Works'] = 'Texts',
-- local DrlPagenameCheck = mw.ext.externalData.getExternalData{
['Dissertations'] = 'Books',
-- url = 'https://research.tsadra.org/index.php?title=Special%3AAsk&q=' .. mw.uri.encode( '[[' .. pageA .. ']]' ) .. '&p%5Bmainlabel%5D=Page&p%5Blimit%5D=1&p%5Bformat%5D=csv',
['Multimedia'] = 'Media',
-- data = {
['Periodical Issues'] = 'Books',
-- pagename = 'Page'
['Sanskrit Publications'] = 'Books',
-- },
['Tibetan Publications'] = 'Books'
-- format = 'CSV with header'
}
-- }
-- for _, data in pairs( DrlPagenameCheck ) do
-- if data.pagename then
-- pageA = data.pagename
-- end
-- end
-- end


-- store relationships and their directions in a table
-- Helper function: Get relationship direction data from research wiki
local relsDirection = {}
local function getRelationshipDirections()
local relsDirectionQueryData = mw.ext.externalData.getExternalData{
local query = '[[-Has subobject::Property:Relationship]]'
url = 'https://research.tsadra.org/index.php?title=Special%3AAsk&q=' .. mw.uri.encode( '[[-Has subobject::Property:Relationship]]' ) .. '&po=RelPropName%0ARelPropDir&p%5Blimit%5D=108&p%5Bformat%5D=csv',
local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..  
data = {  
'&po=RelPropName%0ARelPropDir&p%5Blimit%5D=108&p%5Bformat%5D=csv'
local data = mw.ext.externalData.getExternalData{
url = url,
data = {
RelPropName = 'RelPropName',
RelPropName = 'RelPropName',
RelPropDir = 'RelPropDir'
RelPropDir = 'RelPropDir'
Line 35: Line 60:
format = 'CSV with header'
format = 'CSV with header'
}
}
for _, data in pairs( relsDirectionQueryData ) do relsDirection[data.RelPropName] = data.RelPropDir end
if not data then
return {}
end
local directions = {}
for _, item in pairs(data) do
if item.RelPropName then
directions[item.RelPropName] = item.RelPropDir
end
end
return directions
end


local outgoingRelsQuery = mw.ext.externalData.getExternalData{
-- Helper function: Fetch relationships from external wiki
url = 'https://research.tsadra.org/index.php?title=Special%3AAsk&q=' .. mw.uri.encode( '[[-Has subobject::' .. pageA .. ']][[RelatedTo::<q>[[Category:BCA Project]]</q>]][[Relationship::+]][[RelationshipType::!ResourceToPerson]]' ) .. '&po=RelatedTo%0ARelationship&p%5Blimit%5D=10000&p%5Blink%5D=none&p%5Bmainlabel%5D=Page&p%5Bformat%5D=csv&p%5Bvaluesep%5D=~',
local function fetchRelationships(pageA, direction)
local query
if direction == 'outgoing' then
query = '[[-Has subobject::' .. pageA .. ']][[RelatedTo::<q>[[Category:BCA Project]]</q>]][[Relationship::+]][[RelationshipType::!ResourceToPerson]]'
else
query = '[[-Has subobject::<q>[[Category:BCA Project]]</q>]][[RelatedTo::' .. pageA .. ']][[Relationship::+]][[RelationshipType::!ResourceToPerson]]'
end
local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
'&po=RelatedTo%0ARelationship&p%5Blimit%5D=' .. RELATIONSHIP_LIMIT ..
'&p%5Blink%5D=none&p%5Bmainlabel%5D=Page&p%5Bformat%5D=csv&p%5Bvaluesep%5D=~'
return mw.ext.externalData.getExternalData{
url = url,
data = {
data = {
Page = 'Page',
Page = 'Page',
Line 46: Line 97:
format = 'CSV with header'
format = 'CSV with header'
}
}
end


local incomingRelsQuery = mw.ext.externalData.getExternalData{
-- Helper function: Check if page is a multi-volume series
url = 'https://research.tsadra.org/index.php?title=Special%3AAsk&q=' .. mw.uri.encode( '[[-Has subobject::<q>[[Category:BCA Project]]</q>]][[RelatedTo::' .. pageA .. ']][[Relationship::+]][[RelationshipType::!ResourceToPerson]]' ) .. '&po=RelatedTo%0ARelationship&p%5Blimit%5D=10000&p%5Blink%5D=none&p%5Bmainlabel%5D=Page&p%5Bformat%5D=csv&p%5Bvaluesep%5D=~',
local function isMultiVolumeSeries(page)
local query = '[[' .. page .. ']][[MediaSeries::+]][[Volumenumber::>>1]]'
local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
'&p%5Bmainlabel%5D=wikipage&p%5Blimit%5D=1&p%5Bformat%5D=csv'
local data = mw.ext.externalData.getExternalData{
url = url,
data = {wikipage = 'wikipage'},
format = 'CSV with header'
}
return data and next(data) ~= nil
end
 
-- Helper function: Get volume number for a page
local function getVolumeNumber(page)
local query = '[[' .. page .. ']][[Volumenumber::+]]'
local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
'&po=Volumenumber&p%5Bmainlabel%5D=wikipage&p%5Blimit%5D=1&p%5Bformat%5D=csv'
local data = mw.ext.externalData.getExternalData{
url = url,
data = {
data = {
Page = 'Page',
wikipage = 'wikipage',
RelatedTo = 'RelatedTo',
Volumenumber = 'Volumenumber'
Relationship = 'Relationship'
},
},
format = 'CSV with header'
format = 'CSV with header'
}
}
if data then
for _, item in ipairs(data) do
return item.Volumenumber
end
end
return nil
end


if outgoingRelsQuery == nil and incomingRelsQuery == nil then
-- Helper function: Get page classification and display title
return ''
local function getPageMetadata(page)
local query = '[[' .. page .. ']][[Classification::+]]'
local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
'&po=Classification%0ADisplay%20title%20of&p%5Bmainlabel%5D=wikipage' ..
'&p%5Blink%5D=none&p%5Blimit%5D=1&p%5Bformat%5D=csv'
local data = mw.ext.externalData.getExternalData{
url = url,
data = {
wikipage = 'wikipage',
Classification = 'Classification',
Title = 'Display title of'
},
format = 'CSV with header'
}
if data then
for _, item in ipairs(data) do
return {
classification = item.Classification,
title = item.Title
}
end
end
end
return nil
end


local rel = ''
-- Helper function: Build page link with namespace
local allRels = {}
local function buildPageLink(page)
local metadata = getPageMetadata(page)
if not metadata then
return page
end
local pagename = metadata.title
if not pagename or pagename == '' then
pagename = page
else
-- Remove RKTS identifiers
pagename = string.gsub(pagename, '%s?%(RKTS%w?%s?%d*%)', '')
end
local namespace = CLASS_NAMESPACE[metadata.classification] or metadata.classification
return namespace .. '/' .. pagename
end


if outgoingRelsQuery == nil then else
-- 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


for _, data in ipairs( outgoingRelsQuery ) do
-- 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


local pageB = frame:preprocess( '{{PAGENAME:' .. data.Page .. '}}' )
-- Helper function: Get display name for relationship
local rel = data.Relationship
local function getRelationshipName(rel, direction)
if type(allRels[rel]) == 'table' then else allRels[rel] = {} end
if REL_NAMES[rel] then
if allRels[rel]['right'] then
return direction == 'forward' and REL_NAMES[rel].forward or REL_NAMES[rel].inverse
allRels[rel]['right'] = allRels[rel]['right'] .. '~' .. data.RelatedTo
end
else
return rel
allRels[rel]['right'] = data.RelatedTo
end
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
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
local function processIncomingRelationships(frame, data, allRels, relsDirection, pageA)
if not data then
return
end
end
 
if incomingRelsQuery == nil then else
for _, item in ipairs(data) do
 
local pageB = frame:preprocess('{{PAGENAME:' .. item.Page .. '}}')
for _, data in ipairs( incomingRelsQuery ) do
local rel = item.Relationship
 
local pageB = frame:preprocess( '{{PAGENAME:' .. data.Page .. '}}' )
if not allRels[rel] then
local rel = data.Relationship
allRels[rel] = {}
if type(allRels[rel]) == 'table' then else allRels[rel] = {} end
end
local mediaCheckQuery = mw.ext.externalData.getExternalData{
url = 'https://research.tsadra.org/index.php?title=Special%3AAsk&q=' .. mw.uri.encode( '[[' .. pageB .. ']][[MediaSeries::+]][[Volumenumber::>>1]]' ) .. '&p%5Bmainlabel%5D=wikipage&p%5Blimit%5D=108&p%5Bformat%5D=csv&p%5Bvaluesep%5D=~&p%5Bdefault%5D=',
-- Skip if this is part of a multi-volume series
data = { wikipage = 'wikipage'},
local skipItem = isMultiVolumeSeries(pageB)
format = 'CSV with header'
}
if not skipItem then
 
-- Handle vertical relationships differently
local next = next
if relsDirection[rel] == 'vertical' then
if next(mediaCheckQuery) == nil or not mediaCheckQuery then
if allRels[rel].left then
 
allRels[rel].left = allRels[rel].left .. '~' .. pageB
if relsDirection[rel] == 'vertical' then
else
if allRels[rel]['left'] then
allRels[rel].left = pageB
allRels[rel]['left'] = allRels[rel]['left'] .. '~' .. pageB
end
else
else
allRels[rel]['left'] = pageB
-- Process each related page
for page in string.gmatch(item.RelatedTo, "([^~]+)") do
local skipPage = false
-- Special handling for "Is another edition of" to check volume numbers
if rel == "Is another edition of" then
local volA = getVolumeNumber(pageA)
local volPage = getVolumeNumber(page)
if volA and volPage and volA ~= volPage then
skipPage = true
end
end
end
else
for page in string.gmatch(data.RelatedTo, "([^~]+)") do
-- Add to appropriate side if not skipped
 
if not skipPage then
if (rel == "Is another edition of" and getVolNum(pageA) ~= 'no value' and getVolNum(page) ~= 'no value' and getVolNum(page) ~= getVolNum(pageA)) then
local targetPage = (page == pageA) and pageB or page
elseif page == pageA then
if allRels[rel]['right'] then
if allRels[rel].right then
allRels[rel]['right'] = allRels[rel]['right'] .. '~' .. pageB
allRels[rel].right = allRels[rel].right .. '~' .. targetPage
else
allRels[rel]['right'] = pageB
end
else
else
if allRels[rel]['right'] then
allRels[rel].right = targetPage
allRels[rel]['right'] = allRels[rel]['right'] .. '~' .. page
else
allRels[rel]['right'] = page
end
end
end
end
end
end
end
end
end
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
end
 
checkIfNeedFilter(allRels)
for _, dirs in pairs(allRels) do
 
for _, pages in pairs(dirs) do
for rel, dirs in pairs( allRels ) do
local pageCount = 0
for dir, pages in pairs( dirs ) do
for _ in string.gmatch(pages, "([^~]+)") do
local pagesList = {}
pageCount = pageCount + 1
for page in string.gmatch(pages, "([^~]+)") do
table.insert(pagesList, page)
end
pagesList = makeTblUnique(pagesList)
local relName = ''
if dir == 'left' then
relName = getInverseRelName(rel)
elseif dir == 'right' then
relName = getRelName(rel)
end
end
 
if pageCount > FILTER_THRESHOLD_PAGES then
output = output .. '<h4>' .. relName .. '</h4><div class="row">'
return true
for _, page in ipairs(pagesList) do
local tileCheck = mw.smw.ask( '[[' .. page .. ']]' )
if tileCheck then
output = output .. frame:expandTemplate{ title = 'TileQueryNoDef', args = {
query = '[[' .. page .. ']]'
} }
else
local pageLink = getPageLink(page)
local NoExistTile = frame:preprocess( '<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]]' )
output = output .. NoExistTile
end
end
end
output = output .. '</div><div class="clearfix"></div>'
end
end
end
end
return false
end


-- output = debugStr .. '<br>' .. output
-- Helper function: Generate filter widgets
 
local function generateFilters(frame)
return output
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 '<tr><td>' .. filterRels .. '</td><td>' .. filterPages .. '</td></tr>'
end
end


function getRelName(rel)
-- Helper function: Generate tile for existing page
    local names = {
local function generateTile(frame, page)
["Contains a full translation of"] = 'Translation of',
return frame:expandTemplate{
["Contains a modern commentary of"] = 'Commentary of',
title = 'TileQueryNoDef',
["Contains a partial translation of"] = 'Partial translation of',
args = {query = '[[' .. page .. ']]'}
["Contains a translated commentary of"] = 'Translated commentary of',
}
["Contains a recension of"] = 'Recension of',
["Contains a review of"] = 'Review of',
["Contains a teaching based on"] = 'Teaching based on',
["Contains a translation of"] = 'Translation of',
["Contains polemical content related to"] = 'Polemical content on',
["Contains scholarship related to"] = 'Scholarship on',
["Has a title identical or similar to"] = 'Similar title',
["Has other volumes"] = 'Other volumes',
["Is a book based on this dissertation"] = 'Based on dissertation',
["Is a chapter or a part of"] = 'Chapter or part of',
["Is a commentary of"] = 'Commentary of',
["Is a member of"] = 'Member of',
["Is a reading of"] = 'Reading of',
["Is a scholarly presentation about"] = 'Scholarly presentation about',
["Is a teaching based on"] = 'Teaching on',
["Is an interview about"] = 'Interview about',
["Is another edition of"] = 'Other editions',
["Is related to"] = 'Related',
["Shares intertextuality with"] = 'Shares intertextuality'
    }
    if names[rel] then
  return names[rel]
else
return rel
end
end
end


function getInverseRelName(rel)
-- Helper function: Generate tile for non-existent page
    local names = {
local function generateNoExistTile(frame, page)
["Contains a full translation of"] = 'Full translations',
local pageLink = buildPageLink(page)
["Contains a modern commentary of"] = 'Commentaries',
["Contains a partial translation of"] = 'Partial translations',
local parts = {
["Contains a translated commentary of"] = 'Translated Commentaries',
'<div class="col-md-6 mb-3 mb-md-4 filterable">',
["Contains a recension of"] = 'Recensions',
'<div class="tsdwiki-pop-off tsdwiki-tile tsdwiki-tile-library">',
["Contains a review of"] = 'Reviews',
'<div class="tsdwiki-tile-library-content" style="flex: 0 0 100%; max-width: 100%;">',
["Contains a teaching based on"] = 'Teachings',
'<div class="h4 mb-2 p-0 clipped-2-lines border-0 text-140 line13 carnelian">',
["Contains a translation of"] = 'Translations',
pageLink,
["Contains polemical content related to"] = 'Polemical content',
'</div>',
["Contains scholarship related to"] = 'Scholarship',
'<div class="text-muted clipped-3-lines unclamp-here carnelian">',
["Has a title identical or similar to"] = 'Similar title',
'<b>PAGE MISSING!</b> Please click to create this library item or the redirect that is missing to direct it to the appropriate page.',
["Has other volumes"] = 'Other volumes',
'</div>',
["Is a book based on this dissertation"] = 'Based on dissertation',
'<div class="mt-1 mb-0 clipped-1-line carnelian font-italic text-right">',
["Is a chapter or a part of"] = 'Contains chapter or part',
'Click to create <i class="fa-sharp-duotone fa-solid fa-rectangle-history-circle-plus"></i>',
["Is a commentary of"] = 'Commentaries',
'</div></div>',
["Is a member of"] = 'Contains members',
'{{NoExistTileLink|pageLink=' .. pageLink .. '}}',
["Is a reading of"] = 'Readings',
'</div></div>',
["Is a scholarly presentation about"] = 'Scholarly presentations',
'[[Category:Pages with red relationship tiles]]'
["Is a teaching based on"] = 'Teachings',
}
["Is an interview about"] = 'Interviews',
["Is another edition of"] = 'Other editions',
return frame:preprocess(table.concat(parts))
["Is related to"] = 'Related',
["Shares intertextuality with"] = 'Shares intertextuality'
    }
    if names[rel] then
  return names[rel]
else
return rel
end
end
end


function checkIfNeedFilter(tableOfValues)
-- Helper function: Generate output for a relationship section
local relCheck
local function generateRelationshipSection(frame, rel, dir, pages)
for i, v in pairs(tableOfValues) do
local output = {}
if #v > 10 then
relCheck = true
-- Determine display name
local relName = getRelationshipName(rel, dir == 'left' and 'inverse' or 'forward')
-- Create header
table.insert(output, '<h4>' .. relName .. '</h4><div class="row">')
-- Process pages
local pagesList = makeTableUnique(splitString(pages, '~'))
for _, page in ipairs(pagesList) do
local tileCheck = mw.smw.ask('[[' .. page .. ']]')
if tileCheck then
table.insert(output, generateTile(frame, page))
else
table.insert(output, generateNoExistTile(frame, page))
end
end
end
end
if #tableOfValues > 5 or relCheck then
local filterRels = frame:callParserFunction{
table.insert(output, '</div><div class="clearfix"></div>')
name = '#widget', args = {
'FilterRelationships',
return table.concat(output)
id = 'Relationships',
placeholder = 'Filter by relationships...'
}
}
local filterPages = frame:callParserFunction{
name = '#widget', args = {
'FilterRelationships',
id = 'Pages',
placeholder = 'Filter by pages found...'
}
}
output = output .. '<tr><td>' .. filterRels .. '</td><td>' .. filterPages .. '</td></tr>'  
end
end
end


function getVolNum(page)
-- Main function
local volNumQr = mw.ext.externalData.getExternalData{
function p.main(frame)
url = 'https://research.tsadra.org/index.php?title=Special%3AAsk&q=' .. mw.uri.encode( '[[' .. page .. ']][[Volumenumber::+]]' ) .. '&po=Volumenumber&p%5Bmainlabel%5D=wikipage&p%5Blimit%5D=108&p%5Bformat%5D=csv&p%5Bvaluesep%5D=;&p%5Bdefault%5D=',
-- Get current page name
data = {
local pageA = mw.title.getCurrentTitle().subpageText
wikipage = 'wikipage',
Volumenumber = 'Volumenumber'
-- Fetch relationship directions
},
local relsDirection = getRelationshipDirections()
format = 'CSV with header'
}
-- Fetch relationships
 
local outgoingRels = fetchRelationships(pageA, 'outgoing')
if volNumQr then
local incomingRels = fetchRelationships(pageA, 'incoming')
for _, data in ipairs( volNumQr ) do
return data.Volumenumber
-- Return empty if no relationships found
end
if not outgoingRels and not incomingRels then
else
return ''
return 'no value'
end
-- Organize relationships
local allRels = {}
processOutgoingRelationships(frame, outgoingRels, allRels)
processIncomingRelationships(frame, incomingRels, allRels, relsDirection, pageA)
-- Build output
local output = {}
-- Add filters if needed
if shouldShowFilters(allRels) then
table.insert(output, generateFilters(frame))
end
end
end
 
-- Generate relationship sections
function makeTblUnique(indexedTable)
for rel, dirs in pairs(allRels) do
    local uniqueTbl = {}
for dir, pages in pairs(dirs) do
    for _, value in ipairs(indexedTable) do
table.insert(output, generateRelationshipSection(frame, rel, dir, pages))
        uniqueTbl[value] = true
    end
    indexedTable = {}
    for value,_ in pairs(uniqueTbl) do
        table.insert(indexedTable, value)
    end
    return indexedTable
end
 
function getPageLink(page)
local classQr = mw.ext.externalData.getExternalData{
url = 'https://research.tsadra.org/index.php?title=Special%3AAsk&q=' .. mw.uri.encode( '[[' .. page .. ']][[Classification::+]]' ) .. '&po=Classification%0ADisplay%20title%20of&p%5Bmainlabel%5D=wikipage&p%5Blink%5D=none&p%5Blimit%5D=108&p%5Bformat%5D=csv&p%5Bvaluesep%5D=;',
data = {
wikipage = 'wikipage',
Classification = 'Classification',
Title = 'Display title of'
},
format = 'CSV with header'
}
 
local classEquivalence = {
['Articles'] = 'Articles' ,
['Books'] = 'Books' ,
['Classical Works'] = 'Texts' ,
['Dissertations'] = 'Books' ,
['Multimedia'] = 'Media' ,
['Periodical Issues'] = 'Books' ,
['Sanskrit Publications'] = 'Books' ,
['Tibetan Publications'] = 'Books'
}
 
if classQr then
local plOutput = ''
for _, data in ipairs( classQr ) do
local pagename = ''
if data.Title == '' or data.Title == nil then
pagename = data.wikipage
else
pagename = string.gsub( data.Title, '%s?%(RKTS%w?%s?%d*%)', '' )
end
if classEquivalence[data.Classification] then
plOutput = classEquivalence[data.Classification] .. '/' .. pagename
else
plOutput = data.Classification .. '/' .. pagename
end
end
end
return plOutput
else
return 'no value'
end
end
return table.concat(output)
end
end


return p
return p

Revision as of 16:47, 3 October 2025

Documentation for this module may be created at Module:GetRels/doc

local p = {}

-- Configuration
local RESEARCH_WIKI_BASE = 'https://research.tsadra.org/index.php?title=Special%3AAsk'
local RELATIONSHIP_LIMIT = 10000
local FILTER_THRESHOLD_RELS = 5
local FILTER_THRESHOLD_PAGES = 10

-- Relationship display name mappings
local REL_NAMES = {
	["Contains a full translation of"] = {forward = 'Translation of', inverse = 'Full translations'},
	["Contains a modern commentary of"] = {forward = 'Commentary of', inverse = '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 = '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 = 'Commentary of', inverse = '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 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'}
}

-- 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: Get relationship direction data from research wiki
local function getRelationshipDirections()
	local query = '[[-Has subobject::Property:Relationship]]'
	local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) .. 
		'&po=RelPropName%0ARelPropDir&p%5Blimit%5D=108&p%5Bformat%5D=csv'
	
	local data = mw.ext.externalData.getExternalData{
		url = url,
		data = {
			RelPropName = 'RelPropName',
			RelPropDir = 'RelPropDir'
		},
		format = 'CSV with header'
	}
	
	if not data then
		return {}
	end
	
	local directions = {}
	for _, item in pairs(data) do
		if item.RelPropName then
			directions[item.RelPropName] = item.RelPropDir
		end
	end
	
	return directions
end

-- Helper function: Fetch relationships from external wiki
local function fetchRelationships(pageA, direction)
	local query
	if direction == 'outgoing' then
		query = '[[-Has subobject::' .. pageA .. ']][[RelatedTo::<q>[[Category:BCA Project]]</q>]][[Relationship::+]][[RelationshipType::!ResourceToPerson]]'
	else
		query = '[[-Has subobject::<q>[[Category:BCA Project]]</q>]][[RelatedTo::' .. pageA .. ']][[Relationship::+]][[RelationshipType::!ResourceToPerson]]'
	end
	
	local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
		'&po=RelatedTo%0ARelationship&p%5Blimit%5D=' .. RELATIONSHIP_LIMIT ..
		'&p%5Blink%5D=none&p%5Bmainlabel%5D=Page&p%5Bformat%5D=csv&p%5Bvaluesep%5D=~'
	
	return mw.ext.externalData.getExternalData{
		url = url,
		data = {
			Page = 'Page',
			RelatedTo = 'RelatedTo',
			Relationship = 'Relationship'
		},
		format = 'CSV with header'
	}
end

-- Helper function: Check if page is a multi-volume series
local function isMultiVolumeSeries(page)
	local query = '[[' .. page .. ']][[MediaSeries::+]][[Volumenumber::>>1]]'
	local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
		'&p%5Bmainlabel%5D=wikipage&p%5Blimit%5D=1&p%5Bformat%5D=csv'
	
	local data = mw.ext.externalData.getExternalData{
		url = url,
		data = {wikipage = 'wikipage'},
		format = 'CSV with header'
	}
	
	return data and next(data) ~= nil
end

-- Helper function: Get volume number for a page
local function getVolumeNumber(page)
	local query = '[[' .. page .. ']][[Volumenumber::+]]'
	local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
		'&po=Volumenumber&p%5Bmainlabel%5D=wikipage&p%5Blimit%5D=1&p%5Bformat%5D=csv'
	
	local data = mw.ext.externalData.getExternalData{
		url = url,
		data = {
			wikipage = 'wikipage',
			Volumenumber = 'Volumenumber'
		},
		format = 'CSV with header'
	}
	
	if data then
		for _, item in ipairs(data) do
			return item.Volumenumber
		end
	end
	
	return nil
end

-- Helper function: Get page classification and display title
local function getPageMetadata(page)
	local query = '[[' .. page .. ']][[Classification::+]]'
	local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
		'&po=Classification%0ADisplay%20title%20of&p%5Bmainlabel%5D=wikipage' ..
		'&p%5Blink%5D=none&p%5Blimit%5D=1&p%5Bformat%5D=csv'
	
	local data = mw.ext.externalData.getExternalData{
		url = url,
		data = {
			wikipage = 'wikipage',
			Classification = 'Classification',
			Title = 'Display title of'
		},
		format = 'CSV with header'
	}
	
	if data then
		for _, item in ipairs(data) do
			return {
				classification = item.Classification,
				title = item.Title
			}
		end
	end
	
	return nil
end

-- Helper function: Build page link with namespace
local function buildPageLink(page)
	local metadata = getPageMetadata(page)
	
	if not metadata then
		return page
	end
	
	local pagename = metadata.title
	if not pagename or pagename == '' then
		pagename = page
	else
		-- Remove RKTS identifiers
		pagename = string.gsub(pagename, '%s?%(RKTS%w?%s?%d*%)', '')
	end
	
	local namespace = CLASS_NAMESPACE[metadata.classification] or metadata.classification
	return namespace .. '/' .. 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
local function processIncomingRelationships(frame, data, allRels, relsDirection, pageA)
	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
		
		-- Skip if this is part of a multi-volume series
		local skipItem = isMultiVolumeSeries(pageB)
		
		if not skipItem then
			-- Handle vertical relationships differently
			if relsDirection[rel] == 'vertical' then
				if allRels[rel].left then
					allRels[rel].left = allRels[rel].left .. '~' .. pageB
				else
					allRels[rel].left = pageB
				end
			else
				-- Process each related page
				for page in string.gmatch(item.RelatedTo, "([^~]+)") do
					local skipPage = false
					
					-- Special handling for "Is another edition of" to check volume numbers
					if rel == "Is another edition of" then
						local volA = getVolumeNumber(pageA)
						local volPage = getVolumeNumber(page)
						
						if volA and volPage and volA ~= volPage then
							skipPage = true
						end
					end
					
					-- Add to appropriate side if not skipped
					if not skipPage then
						local targetPage = (page == pageA) and pageB or page
						
						if allRels[rel].right then
							allRels[rel].right = allRels[rel].right .. '~' .. targetPage
						else
							allRels[rel].right = targetPage
						end
					end
				end
			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 '<tr><td>' .. filterRels .. '</td><td>' .. filterPages .. '</td></tr>'
end

-- Helper function: Generate tile for existing page
local function generateTile(frame, page)
	return frame:expandTemplate{
		title = 'TileQueryNoDef',
		args = {query = '[[' .. page .. ']]'}
	}
end

-- Helper function: Generate tile for non-existent page
local function generateNoExistTile(frame, page)
	local pageLink = buildPageLink(page)
	
	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
local function generateRelationshipSection(frame, rel, dir, pages)
	local output = {}
	
	-- Determine display name
	local relName = getRelationshipName(rel, dir == 'left' and 'inverse' or 'forward')
	
	-- Create header
	table.insert(output, '<h4>' .. relName .. '</h4><div class="row">')
	
	-- Process pages
	local pagesList = makeTableUnique(splitString(pages, '~'))
	
	for _, page in ipairs(pagesList) do
		local tileCheck = mw.smw.ask('[[' .. page .. ']]')
		
		if tileCheck then
			table.insert(output, generateTile(frame, page))
		else
			table.insert(output, generateNoExistTile(frame, page))
		end
	end
	
	table.insert(output, '</div><div class="clearfix"></div>')
	
	return table.concat(output)
end

-- Main function
function p.main(frame)
	-- Get current page name
	local pageA = mw.title.getCurrentTitle().subpageText
	
	-- Fetch relationship directions
	local relsDirection = getRelationshipDirections()
	
	-- Fetch relationships
	local outgoingRels = fetchRelationships(pageA, 'outgoing')
	local incomingRels = fetchRelationships(pageA, 'incoming')
	
	-- Return empty if no relationships found
	if not outgoingRels and not incomingRels then
		return ''
	end
	
	-- Organize relationships
	local allRels = {}
	processOutgoingRelationships(frame, outgoingRels, allRels)
	processIncomingRelationships(frame, incomingRels, allRels, relsDirection, pageA)
	
	-- Build output
	local output = {}
	
	-- Add filters if needed
	if shouldShowFilters(allRels) then
		table.insert(output, generateFilters(frame))
	end
	
	-- Generate relationship sections
	for rel, dirs in pairs(allRels) do
		for dir, pages in pairs(dirs) do
			table.insert(output, generateRelationshipSection(frame, rel, dir, pages))
		end
	end
	
	return table.concat(output)
end

return p