Module:SegmentQuery: Difference between revisions

From Bodhicitta
(VSCode edit)
(VSCode edit)
Line 27: Line 27:
local ALL_PRINTOUTS = {
local ALL_PRINTOUTS = {
     'SegmentSource', 'SourceTranslit', 'SegmentTranslation',
     'SegmentSource', 'SourceTranslit', 'SegmentTranslation',
     'SegmentOrder', 'RootTextChapVerseNum', 'SegmentFormat',
     'SegmentOrder', 'RootTextChapVerseNum', 'RootTextVerseNum', 'SegmentFormat',
     'SanskritSource', 'SanskritTranslit', 'TransMemNum',
     'SanskritSource', 'SanskritTranslit', 'TransMemNum',
     'SegmentSourceTsadra', 'SegmentTranslitTsadra',
     'SegmentSourceTsadra', 'SegmentTranslitTsadra',

Revision as of 17:34, 23 April 2026

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

-- Module:SegmentQuery
-- Unified module for bilingual segment queries and formatting.
-- Handles chunked deferred queries, row formatting, and language rendering.
--
-- Entry points:
--   {{#invoke:SegmentQuery|renderChunks|chapter}} — from DisplayTmSection
--   {{#invoke:SegmentQuery|formatRow}}             — from Template:DisplaySegmentRow (SMW result template)

local p = {}

-------------------------------------------------------------------------------
-- Configuration
-------------------------------------------------------------------------------

local CHUNK_SIZE_NORMAL = 20
local CHUNK_SIZE_LARGE  = 3   -- smaller chunks for chapters with "Large content" segments
local MAX_SEGMENTS      = 500 -- absolute safety cap

-- TransMemID prefixes that indicate a non-English SegmentTranslation
local TRANSMEM_LANG = {
    ['004-Tsadra-BCA-Root-Padmakara-French'] = 'french',
    ['021-Tsadra-BCA-Padmakara-Spanish']     = 'spanish',
}

-- All printouts the #ask query can request.
-- LeaveOutArgs suppresses individual printouts by name.
local ALL_PRINTOUTS = {
    'SegmentSource', 'SourceTranslit', 'SegmentTranslation',
    'SegmentOrder', 'RootTextChapVerseNum', 'RootTextVerseNum', 'SegmentFormat',
    'SanskritSource', 'SanskritTranslit', 'TransMemNum',
    'SegmentSourceTsadra', 'SegmentTranslitTsadra',
    'SourceSabcheNumber', 'QuoteSource', 'Flag', 'TransMemID',
}

-- Printouts that are always included regardless of LeaveOutArgs
local ALWAYS_INCLUDE = {
    SegmentOrder = true, RootTextChapVerseNum = true, SegmentFormat = true,
    TransMemNum = true, SourceSabcheNumber = true, QuoteSource = true,
    Flag = true, TransMemID = true,
}

-- Priority order for default visible language per side
local leftPriority  = { 'tibetan', 'tibetan-tsadra', 'devanagari', 'wylie', 'wylie-tsadra', 'sanskrit' }
local rightPriority = { 'english', 'french', 'spanish', 'wylie', 'wylie-tsadra' }

-------------------------------------------------------------------------------
-- Language helpers (shared by formatRow)
-------------------------------------------------------------------------------

-- Build language table, routing SegmentTranslation to the correct language
-- based on TransMemID (defaults to english)
local function buildLanguages(transMemLang)
    local englishParams  = (transMemLang == 'english') and {'SegmentTranslation'} or {}
    local frenchParams   = (transMemLang == 'french')  and {'SegmentTranslation', 'FrenchTranslation', 'French'} or {'FrenchTranslation', 'French'}
    local spanishParams  = (transMemLang == 'spanish') and {'SegmentTranslation', 'Spanish'} or {'Spanish'}
    return {
        { id = 'tibetan',        params = {'SegmentSource'},         classPrefix = 'tibetan-version',        textClass = 'text-160',     langType = 'source' },
        { id = 'tibetan-tsadra', params = {'SegmentSourceTsadra'},   classPrefix = 'tibetan-tsadra-version', textClass = 'text-160',     langType = 'source' },
        { id = 'english',        params = englishParams,             classPrefix = 'english-version',        textClass = 'text-article', langType = 'translation' },
        { id = 'wylie',          params = {'SourceTranslit'},        classPrefix = 'wylie-version',          textClass = 'text-article', langType = 'translation' },
        { id = 'wylie-tsadra',   params = {'SegmentTranslitTsadra'}, classPrefix = 'wylie-tsadra-version',  textClass = 'text-article', langType = 'translation' },
        { id = 'french',         params = frenchParams,              classPrefix = 'french-version',         textClass = 'text-article', langType = 'translation' },
        { id = 'spanish',        params = spanishParams,             classPrefix = 'spanish-version',        textClass = 'text-article', langType = 'translation' },
        { id = 'devanagari',     params = {'SanskritSource'},        classPrefix = 'devanagari-version',     textClass = 'text-160',     langType = 'source' },
        { id = 'sanskrit',       params = {'SanskritTranslit'},      classPrefix = 'sanskrit-version',       textClass = 'text-article', langType = 'translation' },
    }
end

-- Get first non-empty value from a list of param names
local function getParamValue(args, paramNames)
    for _, name in ipairs(paramNames) do
        local val = args[name]
        if val and mw.text.trim(val) ~= '' then
            return mw.text.trim(val)
        end
    end
    return nil
end

-- Resolve default language for a side: first priority-list entry with content in args
local function resolveDefault(args, priorityList, languages)
    local paramsByLang = {}
    for _, lang in ipairs(languages) do
        paramsByLang[lang.id] = lang.params
    end
    for _, id in ipairs(priorityList) do
        local params = paramsByLang[id]
        if params and #params > 0 and getParamValue(args, params) then
            return id
        end
    end
    return priorityList[1]
end

-- Filter languages to only the requested one (single-language mode)
local function filterLanguages(langs, targetId)
    local filtered = {}
    for _, lang in ipairs(langs) do
        if lang.id == targetId then
            table.insert(filtered, lang)
        end
    end
    return filtered
end

-- Check if a language id belongs to a priority list
local function inPriorityList(priorityList, langId)
    for _, id in ipairs(priorityList) do
        if id == langId then return true end
    end
    return false
end

-------------------------------------------------------------------------------
-- Formatting helpers (absorbed from FormatSegmentRow)
-------------------------------------------------------------------------------

-- Convert semicolon-separated page names into wiki links
local function wikiLinkList(str)
    local links = {}
    for page in mw.text.gsplit(str, '%s*;%s*') do
        if page ~= '' then
            table.insert(links, '[[' .. page .. ']]')
        end
    end
    return table.concat(links, ', ')
end

-- Wrap content with poem extension tag (for translation-side verse formats)
local function wrapPoem(frame, content)
    return frame:extensionTag('poem', '<p class="m-0">' .. content .. '</p>')
end

-- Apply SegmentFormat styling to content
-- isSource: true for source-type languages, false for translation-type
local function formatContent(content, format, isSource, frame, sabcheNum, quoteSource)
    if not content then return nil end

    if format == 'Root' then
        if isSource then
            return '<div class="pl-4" style="color: #8c1409;"><p>' .. content .. '</p></div>'
        else
            return '<div class="p-bl-fix pl-4 my-2" style="color: #8c1409;"><p>' .. content .. '</p></div>'
        end

    elseif format == 'Title' then
        if isSource then
            return '<span class="text-130" style="color: #467083;">' .. content .. '</span>'
        else
            return '<div class="h3 font-serif-alt" style="color: #467083; white-space: break-spaces;">' .. content .. '</div>'
        end

    elseif format == 'Commentary' or format == 'Commentary2' or format == 'Colophon' then
        return content

    elseif format == 'Quote' then
        local noteSuffix = ''
        if quoteSource and quoteSource ~= '' then
            noteSuffix = frame:expandTemplate{ title = 'note', args = { 'source', wikiLinkList(quoteSource) } }
        end
        if isSource then
            return '<div class="pl-4" style="color: #897435;">' .. content .. noteSuffix .. '</div>'
        else
            return '<div class="pl-4" style="color: #897435;">' .. wrapPoem(frame, content .. noteSuffix) .. '</div>'
        end

    elseif format == 'DoubleQuote' or format == 'Verse inside a quote' then
        local noteSuffix = ''
        if quoteSource and quoteSource ~= '' then
            noteSuffix = frame:expandTemplate{ title = 'note', args = { 'source', wikiLinkList(quoteSource) } }
        end
        if isSource then
            return '<div class="pl-5" style="color: #897435;">' .. content .. noteSuffix .. '</div>'
        else
            return '<div class="pl-5" style="color: #897435;">' .. wrapPoem(frame, content .. noteSuffix) .. '</div>'
        end

    elseif format == 'Sabche' then
        if isSource then
            return '<span style="color: #467083;">' .. content .. '</span>'
        else
            local sabche = '<div class="p-bl-fix"><span style="color: #467083;">' .. content .. '</span>'
            if sabcheNum then
                sabche = sabche .. ' <span class="float-right">' .. sabcheNum .. '</span>'
            end
            return sabche .. '</div>'
        end

    elseif format == 'SabcheHeading' then
        if isSource then
            return '<div class="mt-3" style="color: #467083;">' .. content .. '</div>'
        else
            local heading = '<div class="h5 mt-3 font-serif-alt" style="color: #467083;">' .. content
            if sabcheNum then
                heading = heading .. ' <span class="float-right">' .. sabcheNum .. '</span>'
            end
            return heading .. '</div>'
        end

    elseif format == 'SabcheSubHeading' then
        if isSource then
            return '<div class="mt-2" style="color: #467083;">' .. content .. '</div>'
        else
            local subheading = '<div class="h6 mt-2 font-serif-alt" style="color: #467083;">' .. content
            if sabcheNum then
                subheading = subheading .. ' <span class="float-right">' .. sabcheNum .. '</span>'
            end
            return subheading .. '</div>'
        end

    elseif format == 'Section Title1' then
        if isSource then
            return content
        else
            return '<div class="h3 font-serif-alt" style="color: #467083; white-space: break-spaces;">' .. content .. '</div>'
        end

    elseif format == 'Section Title2' then
        if isSource then
            return '<span class="text-120" style="color: #467083;">' .. content .. '</span>'
        else
            return '<div class="h4 font-serif-alt" style="color: #467083; white-space: break-spaces;">' .. content .. '</div>'
        end

    elseif format == 'Indented Verse' then
        if isSource then
            return '<div class="pl-4">' .. content .. '</div>'
        else
            return '<div class="pl-4">' .. wrapPoem(frame, content) .. '</div>'
        end

    elseif format == 'Root non-verse' then
        return '<span style="color: #8c1409;">' .. content .. '</span>'

    else
        -- Default: no special formatting
        return content
    end
end

-- Render all language divs for one side (left or right)
local function renderSide(args, side, format, frame, defaultLang, languages)
    local sabcheNum = getParamValue(args, {'SourceSabcheNumber'})
    local quoteSource = getParamValue(args, {'QuoteSource'})
    local parts = {}

    for _, lang in ipairs(languages) do
        if #lang.params > 0 then
            local content = getParamValue(args, lang.params)
            if content then
                local isSource = (lang.langType == 'source')
                local formatted = formatContent(content, format, isSource, frame, sabcheNum, quoteSource)

                if formatted then
                    local hidden = lang.id ~= defaultLang and ' hidden' or ''
                    local classes = 'toggleable ' .. lang.classPrefix .. '-' .. side .. ' ' .. lang.textClass .. hidden
                    table.insert(parts, '<div class="' .. classes .. '">' .. formatted .. '</div>')
                end
            end
        end
    end

    return table.concat(parts)
end

-------------------------------------------------------------------------------
-- Staff group check (cached per page parse)
-------------------------------------------------------------------------------

local isStaffCache = nil
local function isStaff(frame)
    if isStaffCache == nil then
        isStaffCache = mw.text.trim(frame:preprocess('{{#ifingroup:staff|1|0}}')) == '1'
    end
    return isStaffCache
end

-------------------------------------------------------------------------------
-- SingleLanguage / SingleColumn check (cached per page parse)
-------------------------------------------------------------------------------

local singleLangCache   -- nil = not checked yet, false = not set
local singleColumnCache

local function getSingleLang(frame)
    if singleLangCache == nil then
        local pageTitle = mw.title.getCurrentTitle().prefixedText
        local raw = mw.text.trim(frame:callParserFunction('#show', pageTitle, '?SingleLanguage'))
        singleLangCache = (raw ~= '') and raw:lower() or false
    end
    return singleLangCache
end

local function getSingleColumn(frame)
    if singleColumnCache == nil then
        local pageTitle = mw.title.getCurrentTitle().prefixedText
        local raw = mw.text.trim(frame:callParserFunction('#show', pageTitle, '?SingleColumn')):lower()
        singleColumnCache = (raw == 'true' or raw == '1' or raw == 't')
    end
    return singleColumnCache
end

-------------------------------------------------------------------------------
-- formatRow: called by Template:DisplaySegmentRow for each SMW result
-- (absorbs the old DisplaySegmentRow template wrapper + FormatSegmentRow module)
-------------------------------------------------------------------------------

function p.formatRow(frame)
    local args = frame:getParent().args
    local format = getParamValue(args, {'SegmentFormat'}) or 'Commentary'

    -- Determine which language SegmentTranslation holds based on TransMemID
    local transMemId = getParamValue(args, {'TransMemID'}) or ''
    local transMemLang = TRANSMEM_LANG[transMemId] or 'english'

    local languages = buildLanguages(transMemLang)

    local defaultLeft  = resolveDefault(args, leftPriority, languages)
    local defaultRight = resolveDefault(args, rightPriority, languages)

    local singleLang   = getSingleLang(frame)
    local singleColumn = getSingleColumn(frame)

    local singleIsLeft  = singleLang and inPriorityList(leftPriority, singleLang)
    local singleIsRight = singleLang and inPriorityList(rightPriority, singleLang)

    -- Build content columns
    local content
    if singleColumn then
        local rendered = renderSide(args, 'left', format, frame, defaultLeft, languages)
        content = '<div class="col-12 font-serif line14 p-bl-fix mb-3">' .. rendered .. '</div>'
    elseif singleLang then
        if singleIsLeft then
            local rendered = renderSide(args, 'left', format, frame, singleLang, filterLanguages(languages, singleLang))
            content = '<div class="col-12 font-serif line14 p-bl-fix mb-3">' .. rendered .. '</div>'
        elseif singleIsRight then
            local rendered = renderSide(args, 'right', format, frame, singleLang, filterLanguages(languages, singleLang))
            content = '<div class="col-12 line14 p-bl-fix mb-3">' .. rendered .. '</div>'
        else
            local left  = renderSide(args, 'left', format, frame, defaultLeft, languages)
            local right = renderSide(args, 'right', format, frame, defaultRight, languages)
            content = '<div class="col-md-6 font-serif line14 p-bl-fix mb-3">' .. left .. '</div>'
                   .. '<div class="col-md-6 line14 p-bl-fix mb-3">' .. right .. '</div>'
        end
    else
        local left  = renderSide(args, 'left', format, frame, defaultLeft, languages)
        local right = renderSide(args, 'right', format, frame, defaultRight, languages)
        content = '<div class="col-md-6 font-serif line14 p-bl-fix mb-3">' .. left .. '</div>'
               .. '<div class="col-md-6 line14 p-bl-fix mb-3">' .. right .. '</div>'
    end

    -- Wrap in row div (previously done by Template:DisplaySegmentRow)
    local parts = { '<div class="row bilingual-row py-1 filterable">' }

    -- Staff debug info
    if isStaff(frame) then
        local resultNum = args['#'] or ''
        local pageTitle = args[1] or ''
        local segOrder  = getParamValue(args, {'SegmentOrder'}) or ''
        local titleObj  = pageTitle ~= '' and mw.title.new(pageTitle)
        local url       = titleObj and tostring(titleObj:fullUrl()) or ''
        table.insert(parts,
            '<div class="col-12 text-center font-sans-wiki text-90 py-1 mb-2">'
            .. 'Query result #' .. resultNum
            .. ' &nbsp;·&nbsp; '
            .. '[' .. url .. ' '
            .. '<i class="fa-sharp-duotone fa-memo fa-sm"></i> TM segment #' .. segOrder
            .. ']</div>'
        )
    end

    table.insert(parts, content)
    table.insert(parts, '</div>')

    return table.concat(parts)
end

-------------------------------------------------------------------------------
-- renderChunks: emits deferred #ask queries, called from DisplayTmSection
-- Now also builds the #ask wikitext directly (absorbs Template:SegmentQuery)
-------------------------------------------------------------------------------

function p.renderChunks(frame)
    local currentTitle = mw.title.getCurrentTitle()
    local pargs = frame:getParent().args
    local leaveOutArgs = pargs.LeaveOutArgs or ''
    local transMemID = mw.text.trim(pargs.TransMemID or '')

    -- Mirror SegmentQuery's fallback: if TranslationChapter not passed, use SUBPAGENAME
    local chapterRaw = mw.text.trim(frame.args[1] or '')
    local hasChapter = chapterRaw ~= ''

    -- Build chapter condition
    local chapterCondition = ''
    if hasChapter then
        local values = {}
        for v in chapterRaw:gmatch('[^;]+') do
            table.insert(values, mw.text.trim(v))
        end
        chapterCondition = '[[TranslationChapter::' .. table.concat(values, '||') .. ']]'
    end

    local baseConditions =
        '[[TransMemID::' .. transMemID .. ']]' ..
        chapterCondition ..
        '[[SegmentFormat::!Deprecated]]'

    -- 1. Count total segments
    local countResult = mw.smw.ask(
        baseConditions ..
        '|?SegmentOrder' ..
        '|limit=' .. MAX_SEGMENTS ..
        '|link=none'
    )
    local totalSegments = countResult and #countResult or 0
    if totalSegments == 0 then
        return '<!-- SegmentQuery: 0 segments found for TransMemID=' .. transMemID .. ' -->'
    end

    -- 2. Check for any "Large content" flag in this chapter
    local largeCheck = mw.smw.ask(
        baseConditions ..
        '[[Flag::Large content]]' ..
        '|?SegmentOrder' ..
        '|limit=1' ..
        '|link=none'
    )
    local chunkSize = (largeCheck and #largeCheck > 0) and CHUNK_SIZE_LARGE or CHUNK_SIZE_NORMAL

    -- 3. Build printout list, respecting LeaveOutArgs
    local printouts = {}
    for _, name in ipairs(ALL_PRINTOUTS) do
        if ALWAYS_INCLUDE[name] or not leaveOutArgs:find(name, 1, true) then
            table.insert(printouts, '|?' .. name)
        end
    end
    local printoutStr = table.concat(printouts)

    -- 4. Emit one deferred #ask per chunk
    local parts = {}
    local offset = 0
    while offset < totalSegments do
        -- Build the full #ask wikitext
        local ask = '{{#ask:'
            .. baseConditions
            .. printoutStr
            .. '|named args=true'
            .. '|sort=SegmentOrder'
            .. '|order=asc'
            .. '|format=template'
            .. '|template=DisplaySegmentRow'
            .. '|limit=' .. chunkSize
            .. '|offset=' .. offset
            .. '|link=none'
            .. '|intro=<div class="filterableContent mb-4">'
            .. '|outro=</div>'
            .. '|searchlabel='
            .. '|default='
            .. '|@deferred'
            .. '}}'

        table.insert(parts, frame:preprocess(ask))
        offset = offset + chunkSize
    end

    return table.concat(parts)
end

return p