Module:GetRels: Difference between revisions

From Bodhicitta
(VSCode edit)
(VSCode edit)
Line 54: Line 54:
}
}


-- Helper function: Get relationship direction data from research wiki
-- Helper function: Check if a relationship is horizontal (symmetric)
local function getRelationshipDirections()
local function isHorizontalRelationship(rel)
local query = '[[-Has subobject::Property:Relationship]]'
return HORIZONTAL_RELS[rel] == true
local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
end
'&po=RelPropName%0ARelPropDir&p%5Blimit%5D=108&p%5Bformat%5D=csv'
 
-- Helper function: Fetch outgoing relationships for a page (page has property pointing to other pages)
local data = mw.ext.externalData.getExternalData{
local function fetchOutgoingRelationships(pageA)
url = url,
local results = {}
data = {
 
RelPropName = 'RelPropName',
for relName, _ in pairs(REL_NAMES) do
RelPropDir = 'RelPropDir'
local query = mw.smw.ask{
},
'[[' .. pageA .. ']][[' .. relName .. '::+]]',
format = 'CSV with header'
'?' .. relName,
}
limit = RELATIONSHIP_LIMIT
}
if not data then
 
return {}
if query then
end
for _, row in ipairs(query) do
local targets = row[relName]
local directions = {}
if targets then
for _, item in pairs(data) do
-- Handle both single value and table of values
if item.RelPropName then
if type(targets) == 'table' then
directions[item.RelPropName] = item.RelPropDir
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
end
end
end
end
 
return directions
return #results > 0 and results or nil
end
end


-- Helper function: Fetch relationships from external wiki
-- Helper function: Fetch incoming relationships (other pages have property pointing to this page)
local function fetchRelationships(pageA, direction)
local function fetchIncomingRelationships(pageA)
local query
local results = {}
if direction == 'outgoing' then
 
query = '[[-Has subobject::' .. pageA .. ']][[RelatedTo::<q>[[Category:BCA Project]]</q>]][[Relationship::+]][[RelationshipType::!ResourceToPerson]]'
for relName, _ in pairs(REL_NAMES) do
else
local query = mw.smw.ask{
query = '[[-Has subobject::<q>[[Category:BCA Project]]</q>]][[RelatedTo::' .. pageA .. ']][[Relationship::+]][[RelationshipType::!ResourceToPerson]]'
'[[' .. relName .. '::' .. pageA .. ']]',
'?#-=Page',
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})
end
end
end
end
end
 
local url = RESEARCH_WIKI_BASE .. '&q=' .. mw.uri.encode(query) ..
return #results > 0 and results or nil
'&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
end


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


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

Revision as of 18:42, 28 January 2026

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

-- 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'}
}

-- 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
}

-- 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)
local function fetchOutgoingRelationships(pageA)
	local results = {}

	for relName, _ in pairs(REL_NAMES) do
		local query = mw.smw.ask{
			'[[' .. pageA .. ']][[' .. relName .. '::+]]',
			'?' .. relName,
			limit = RELATIONSHIP_LIMIT
		}

		if query then
			for _, row in ipairs(query) 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
		end
	end

	return #results > 0 and results or nil
end

-- Helper function: Fetch incoming relationships (other pages have property pointing to this page)
local function fetchIncomingRelationships(pageA)
	local results = {}

	for relName, _ in pairs(REL_NAMES) do
		local query = mw.smw.ask{
			'[[' .. relName .. '::' .. pageA .. ']]',
			'?#-=Page',
			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})
				end
			end
		end
	end

	return #results > 0 and results or nil
end

-- Helper function: Check if page is a multi-volume series
local function isMultiVolumeSeries(page)
	local query = mw.smw.ask{
		'[[' .. page .. ']][[MediaSeries::+]][[Volumenumber::>>1]]',
		limit = 1
	}

	return query ~= nil
end

-- Helper function: Get volume number for a page
local function getVolumeNumber(page)
	local query = mw.smw.ask{
		'[[' .. page .. ']][[Volumenumber::+]]',
		'?Volumenumber',
		limit = 1
	}

	if query and query[1] then
		return query[1].Volumenumber
	end

	return nil
end

-- Helper function: Get page classification and display title
local function getPageMetadata(page)
	local query = mw.smw.ask{
		'[[' .. page .. ']][[Classification::+]]',
		'?Classification',
		'?Display title of=Title',
		limit = 1
	}

	if query and query[1] then
		return {
			classification = query[1].Classification,
			title = query[1].Title
		}
	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 '<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
local function generateTile(frame, page)
	return frame:expandTemplate{
		title = 'TileQueryNoDef',
		args = {query = '[[Category:Library Items]][[' .. page .. ']]OR[[Category:Library Items]][[Texts/' .. 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 = {'<div class="filterableContent clearfix">'}
	
	-- Determine display name
	local relName = getRelationshipName(rel, dir == 'left' and 'inverse' or 'forward')
	
	-- Create header
	table.insert(output, '<h4>' .. relName .. '</h4><div class="row mx-0">')
	
	-- 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>')
	
	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