Module:SscCitedWorks: Difference between revisions

From Bodhicitta
(Vajra: 64px, positioned from a class)
(Page-type QuoteSource: group by the cited page instead of scraping RKTS ids)
Line 6: Line 6:
-- The join is the RKTS catalogue number. A quote segment records its source as
-- The join is the RKTS catalogue number. A quote segment records its source as
-- "Ratnameghasūtra (RKTSK 231)", and the text page holds the same id in
-- "Ratnameghasūtra (RKTSK 231)", and the text page holds the same id in
-- DrlPageName, so no title matching is involved.
-- the page itself, since QuoteSource is now a page property.
--
--
-- COUNTS ARE QUOTATIONS, NOT SEGMENTS. One quotation is often split across
-- COUNTS ARE QUOTATIONS, NOT SEGMENTS. One quotation is often split across
Line 27: Line 27:
end
end


-- Quotation counts keyed by RKTS id.
-- Quotation counts keyed by the cited page.
local function quotationsById()
--
-- QuoteSource is a Page-type property, so a value IS the work's page (or a
-- redirect to it, which SMW resolves). That replaces the old route of scraping
-- an RKTS catalogue number out of the value and hopping through DrlPageName —
-- which could never reach the works that have no catalogue number at all.
local function quotationsByPage()
local query = mw.smw.ask{
local query = mw.smw.ask{
'[[' .. SSC_PREFIX .. ']][[QuoteSource::+]]',
'[[' .. SSC_PREFIX .. ']][[QuoteSource::+]]',
'?QuoteSource#=QuoteSource',
'?QuoteSource=QuoteSource',
'?SegmentOrder#=SegmentOrder',
'?SegmentOrder#=SegmentOrder',
'?TranslationChapter#=TranslationChapter',
'?TranslationChapter#=TranslationChapter',
Line 38: Line 43:
if not query then return {} end
if not query then return {} end


local segments = {}
local bySource = {}
for _, row in ipairs(query) do
for _, row in ipairs(query) do
local order = tonumber(firstValue(row.SegmentOrder))
local order = tonumber(firstValue(row.SegmentOrder))
local source = firstValue(row.QuoteSource)
if order then
if order and type(source) == 'string' then
local chapter = tostring(firstValue(row.TranslationChapter) or '')
table.insert(segments, {
-- A segment can name SEVERAL works when a passage is shared between
order  = order,
-- them, so every value counts towards its own work. Taking only the
source  = source,
-- first credited the whole passage to one and left the other empty.
chapter = tostring(firstValue(row.TranslationChapter) or ''),
local sources = row.QuoteSource
})
if type(sources) ~= 'table' then sources = { sources } end
for _, source in ipairs(sources) do
-- The printout carries the page the value resolved to, redirects
-- followed, so counts key on the work itself rather than on
-- whichever alias a segment happened to use. The Samādhirājasūtra
-- is cited under five names and must count as one work.
local page = source
if type(page) == 'table' then page = page.fulltext end
if type(page) == 'string' and page ~= '' then
bySource[page] = bySource[page] or {}
table.insert(bySource[page], { order = order, chapter = chapter })
end
end
end
end
end
end
table.sort(segments, function(a, b) return a.order < b.order end)


-- Runs are found PER SOURCE, not by walking the whole sequence. Two different
-- Runs are found PER SOURCE, not by walking the whole sequence. Two different
-- works quoted in adjacent segments are two quotations, and walking the
-- works quoted in adjacent segments are two quotations, and walking the
-- combined list makes each one look like it interrupts the other. Grouping
-- combined list makes each look like it interrupts the other. Grouping within
-- within a source also matches Module:SscCitations, whose per-page counts are
-- a source also matches Module:SscCitations, whose per-page counts are what a
-- what a reader sees on the text page itself — the two must agree.
-- reader sees on the text page itself — the two must agree.
local bySource = {}
for _, seg in ipairs(segments) do
-- A segment can name SEVERAL sources, semicolon-separated, when a passage
-- is shared between works:
--
--  Bodhisattvapratimokṣa… (RKTSK 248);Vinayaviniścayopāli… (RKTSK 68)
--
-- Every id in the value is collected, so the segment counts towards each
-- work — matching Module:SscCitations, whose wildcard finds the segment
-- from either page. Taking only the first id credited the whole passage
-- to one work and left the other showing nothing.
--
-- Anchored on the closing parenthesis: without it "RKTSK 9" also matches
-- "RKTSK 96" and "RKTSK 99".
for canon, number in seg.source:gmatch('(RKTS[KT])%s*([%d%-]+)%s*%)') do
local id = canon:upper() .. ' ' .. number
bySource[id] = bySource[id] or {}
table.insert(bySource[id], seg)
end
end
 
local counts = {}
local counts = {}
for id, list in pairs(bySource) do
for page, list in pairs(bySource) do
table.sort(list, function(a, b) return a.order < b.order end)
table.sort(list, function(a, b) return a.order < b.order end)
local n, previous = 0, nil
local n, previous = 0, nil
Line 89: Line 84:
previous = seg
previous = seg
end
end
counts[id] = n
counts[page] = n
end
end
return counts
return counts
Line 95: Line 90:


function p.main(frame)
function p.main(frame)
local counts = quotationsById()
local counts = quotationsByPage()


local ids = {}
local names = {}
for id in pairs(counts) do table.insert(ids, id) end
for page in pairs(counts) do table.insert(names, page) end
if #ids == 0 then return '' end
if #names == 0 then return '' end
table.sort(ids)
table.sort(names)


-- One query for every quoted id, rather than one per work.
-- One query for every cited page, rather than one per work. Values repeat
-- the page name only, per SMW's OR syntax: [[P::a||b||c]]. Repeating the
-- property ([[P::a||P::b]]) silently returns almost nothing.
local pages = mw.smw.ask{
local pages = mw.smw.ask{
'[[DrlPageName::' .. table.concat(ids, '||') .. ']]',
'[[' .. table.concat(names, '||') .. ']]',
'?#-=Page',
'?#-=Page',
'?DrlPageName#=DrlPageName',
'?Display title of=Title',
'?Display title of=Title',
'?FullTitleTib=FullTitleTib',
'?FullTitleTib=FullTitleTib',
Line 118: Line 114:
for _, row in ipairs(pages) do
for _, row in ipairs(pages) do
local page = firstValue(row.Page)
local page = firstValue(row.Page)
local id = firstValue(row.DrlPageName)
-- counts is keyed by the resolved page (see quotationsByPage), which is
if page and page ~= SSC_PAGE and id and counts[id] then
-- what this query returns, so the two line up directly.
local n = page and counts[page]
if page and page ~= SSC_PAGE and n then
local label = firstValue(row.Title)
local label = firstValue(row.Title)
if not label or label == '' then label = page:gsub('^Texts/', '') end
if not label or label == '' then label = page:gsub('^Texts/', '') end
Line 128: Line 126:
trans = firstValue(row.FullTitleTrans),
trans = firstValue(row.FullTitleTrans),
class = firstValue(row.Classification),
class = firstValue(row.Classification),
count = counts[id],
count = n,
})
})
end
end

Revision as of 16:48, 17 September 2026

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

local p = {}

-- The list of works quoted in the Śikṣāsamuccaya, generated from the aligned
-- translation memory.
--
-- The join is the RKTS catalogue number. A quote segment records its source as
-- "Ratnameghasūtra (RKTSK 231)", and the text page holds the same id in
-- the page itself, since QuoteSource is now a page property.
--
-- COUNTS ARE QUOTATIONS, NOT SEGMENTS. One quotation is often split across
-- consecutive segments, so counting segments overstates badly: the
-- Pitāputrasamāgamanasūtra has 62 quote segments but is quoted 14 times, and one
-- of its runs is 36 segments long. Across the SSC, 633 segments are 345
-- quotations. SMW cannot express "consecutive", so the grouping happens here.
--
-- A chapter change breaks a run: two adjacent quotes from one work in different
-- chapters are two quotations. No run in the current data crosses a chapter
-- boundary, but the rule is correct and costs nothing.

local SSC_PREFIX = '~Translation Memory:016-Tsadra-SSC-Root/*'
local SSC_PAGE = 'Texts/Śikṣāsamuccaya'
local SEGMENT_LIMIT = 5000

local function firstValue(v)
	if type(v) == 'table' then return v[1] end
	return v
end

-- Quotation counts keyed by the cited page.
--
-- QuoteSource is a Page-type property, so a value IS the work's page (or a
-- redirect to it, which SMW resolves). That replaces the old route of scraping
-- an RKTS catalogue number out of the value and hopping through DrlPageName —
-- which could never reach the works that have no catalogue number at all.
local function quotationsByPage()
	local query = mw.smw.ask{
		'[[' .. SSC_PREFIX .. ']][[QuoteSource::+]]',
		'?QuoteSource=QuoteSource',
		'?SegmentOrder#=SegmentOrder',
		'?TranslationChapter#=TranslationChapter',
		limit = SEGMENT_LIMIT
	}
	if not query then return {} end

	local bySource = {}
	for _, row in ipairs(query) do
		local order = tonumber(firstValue(row.SegmentOrder))
		if order then
			local chapter = tostring(firstValue(row.TranslationChapter) or '')
			-- A segment can name SEVERAL works when a passage is shared between
			-- them, so every value counts towards its own work. Taking only the
			-- first credited the whole passage to one and left the other empty.
			local sources = row.QuoteSource
			if type(sources) ~= 'table' then sources = { sources } end
			for _, source in ipairs(sources) do
				-- The printout carries the page the value resolved to, redirects
				-- followed, so counts key on the work itself rather than on
				-- whichever alias a segment happened to use. The Samādhirājasūtra
				-- is cited under five names and must count as one work.
				local page = source
				if type(page) == 'table' then page = page.fulltext end
				if type(page) == 'string' and page ~= '' then
					bySource[page] = bySource[page] or {}
					table.insert(bySource[page], { order = order, chapter = chapter })
				end
			end
		end
	end

	-- Runs are found PER SOURCE, not by walking the whole sequence. Two different
	-- works quoted in adjacent segments are two quotations, and walking the
	-- combined list makes each look like it interrupts the other. Grouping within
	-- a source also matches Module:SscCitations, whose per-page counts are what a
	-- reader sees on the text page itself — the two must agree.
	local counts = {}
	for page, list in pairs(bySource) do
		table.sort(list, function(a, b) return a.order < b.order end)
		local n, previous = 0, nil
		for _, seg in ipairs(list) do
			local continues = previous
				and seg.chapter == previous.chapter
				and seg.order == previous.order + 1
			if not continues then n = n + 1 end
			previous = seg
		end
		counts[page] = n
	end
	return counts
end

function p.main(frame)
	local counts = quotationsByPage()

	local names = {}
	for page in pairs(counts) do table.insert(names, page) end
	if #names == 0 then return '' end
	table.sort(names)

	-- One query for every cited page, rather than one per work. Values repeat
	-- the page name only, per SMW's OR syntax: [[P::a||b||c]]. Repeating the
	-- property ([[P::a||P::b]]) silently returns almost nothing.
	local pages = mw.smw.ask{
		'[[' .. table.concat(names, '||') .. ']]',
		'?#-=Page',
		'?Display title of=Title',
		'?FullTitleTib=FullTitleTib',
		'?Fulltitletrans=FullTitleTrans',
		'?Classification=Classification',
		limit = 500
	}
	if not pages then return '' end

	local works = {}
	for _, row in ipairs(pages) do
		local page = firstValue(row.Page)
		-- counts is keyed by the resolved page (see quotationsByPage), which is
		-- what this query returns, so the two line up directly.
		local n = page and counts[page]
		if page and page ~= SSC_PAGE and n then
			local label = firstValue(row.Title)
			if not label or label == '' then label = page:gsub('^Texts/', '') end
			table.insert(works, {
				page  = page,
				label = label,
				tib   = firstValue(row.FullTitleTib),
				trans = firstValue(row.FullTitleTrans),
				class = firstValue(row.Classification),
				count = n,
			})
		end
	end
	if #works == 0 then return '' end

	-- Alphabetical by title, matching the hand-written page it replaces. Sorting
	-- by count would bury the sūtras quoted once among the busiest works.
	table.sort(works, function(a, b) return a.label < b.label end)

	local out = {}
	for _, w in ipairs(works) do
		local author = {}
		if w.tib and w.tib ~= '' then table.insert(author, 'Tib. ' .. w.tib) end
		if w.trans and w.trans ~= '' then table.insert(author, 'Eng. ' .. w.trans) end

		table.insert(out, '<div class="row filterable">')
		-- The vajra sits ON the border between the two columns (right: -26px on a
		-- position-relative parent), tying them together. Desktop only.
		table.insert(out, '<div class="col-md-3 col-lg-2 pr-md-5 pt-md-1 pb-1 text-center ' ..
			'text-md-right border-right timeline-border font-serif position-relative">' ..
			'<div class="ssc-cited-vajra d-none d-md-block position-absolute">' ..
			'[[File:Red Horizontal Vajra.png|50px|link=|]]</div>' ..
			'<span class="ssc-cited-count">' .. w.count .. '</span>' ..
			'<span class="ssc-cited-label">' ..
			(w.count == 1 and 'quotation' or 'quotations') .. '</span></div>')
		table.insert(out, frame:expandTemplate{
			title = 'TimelineTileHorizontalTranslations',
			args = {
				wrapperclasses = 'col-md-9 px-2 px-md-5 py-0 font-serif',
				texts = '[[' .. w.page .. '|' .. w.label .. ']]',
				author = table.concat(author, ' &nbsp;·&nbsp; '),
				description = w.class or '',
			}
		})
		table.insert(out, '</div>')
	end

	return frame:preprocess(table.concat(out, '\n'))
end

return p