Module:GetRootVerses

From Bodhicitta


Quick Guide[edit]

This module displays root verses from the Bodhicaryāvatāra with multiple source versions, translations, and interactive toggles.

Quick Start[edit]

Display a verse[edit]

{{#invoke:GetRootVerses|main|verse=1.1}}

Store verse properties[edit]

{{#invoke:GetRootVerses|set}}

Simple display (specific sources only)[edit]

{{#invoke:GetRootVerses|SimpleVerse
|verse=1.1
|sources=001-Tsadra-BCA-Root-Padmakara;009-Tsadra-BCA-Root-Gomez
}}

Functions[edit]

Function Parameters Description
set none Stores SMW properties for current verse
getSourceToggles verse Returns toggle controls for source versions
main verse (optional) Full verse display with all sources
SimpleVerse verse, sources Simplified display with selected sources

Source IDs[edit]

ID Label
001-Tsadra-BCA-Root-Padmakara Padmakara Tibetan
009-Tsadra-BCA-Root-Gomez Sanskrit
005-Tsadra-BCA-Root-Dunhuang Dunhuang
017-Tsadra-BCA-Root-Tsadra_Edition Tsadra Edition

Features[edit]

  • Multiple source versions (Tibetan, Sanskrit, Dunhuang)
  • Interactive source/translation toggles
  • Audio players for chanted verses
  • Multiple translation display
  • Responsive layout
  • Links to translation memory data

Performance[edit]

  • 1-2 queries per page (optimized)
  • <2 seconds load time
  • Modular architecture for easy maintenance

Dependencies[edit]

Helper Modules[edit]

See Also[edit]




Full Documentation[edit]

This module provides verse display functionality for the Bodhicaryāvatāra (BCA) project, handling retrieval and presentation of root verses from multiple translation memories with their associated translations.

Architecture[edit]

The module is split into three components for better maintainability and performance:

Module:GetRootVerses (main module)
Entry point containing all public functions: set, getSourceToggles, main, and SimpleVerse
Module:GetRootVerses/Data
Data retrieval layer that makes optimized SMW queries and organizes verse data by source
Module:GetRootVerses/Display
Presentation layer containing reusable HTML rendering functions

Public Functions[edit]

set[edit]

Stores semantic properties for the current verse page from translation memory data.

Usage:

{{#invoke:GetRootVerses|set}}

Called from: Verse pages automatically via template Returns: Empty string on success, error message on failure Side effects: Sets SMW properties on the page including:

  • SegmentSource - Original Tibetan text
  • SourceTranslit - Wylie transliteration
  • SegmentTranslation - English translation
  • Spanish - Spanish translation (if available)
  • SanskritSource - Sanskrit text (if available)
  • SanskritTranslit - Sanskrit transliteration (if available)
  • FrenchTranslation - French translation (if available)
  • Additional metadata fields

getSourceToggles[edit]

Generates toggle controls for switching between different source versions (Tibetan, Sanskrit, Dunhuang, etc.).

Usage:

{{#invoke:GetRootVerses|getSourceToggles|verse=1.1}}

Parameters:

  • verse - Verse number in format Chapter.Verse (e.g., "1.1", "5.109")

Returns: HTML toggle widgets for available source versions

main[edit]

Main display function that renders verse content with all available sources, translations, and interactive elements.

Usage:

{{#invoke:GetRootVerses|main|verse=1.1}}

Parameters:

  • verse - Verse number (optional, defaults to current page's subpage text)

Returns: Complete HTML structure including:

  • Source text in original script (Tibetan, Sanskrit, etc.)
  • Transliteration (toggleable)
  • Primary English translation
  • Toggle controls for showing/hiding content
  • Audio players (for chanted verses where available)
  • Other available translations in expandable sections

Features:

  • Responsive grid layout
  • Interactive toggles for source/translation visibility
  • Horizontal scrolling for multiple translations
  • Audio verse players
  • Links to translation memory data pages

SimpleVerse[edit]

Simplified verse display without the full toggle interface, suitable for embedding in other contexts.

Usage:

{{#invoke:GetRootVerses|SimpleVerse
|verse=1.1
|sources=001-Tsadra-BCA-Root-Padmakara;009-Tsadra-BCA-Root-Gomez
}}

Parameters:

  • verse - Verse number (required)
  • sources - Semicolon-separated list of translation memory IDs

Returns: Simplified HTML display with requested sources only

Translation Memory IDs[edit]

The module works with the following translation memory sources:

ID Label Language Description
001-Tsadra-BCA-Root-Padmakara Padmakara Tibetan Tibetan Primary Tibetan source with Padmakara translation
009-Tsadra-BCA-Root-Gomez Sanskrit Sanskrit Sanskrit text with transliteration
005-Tsadra-BCA-Root-Dunhuang Dunhuang Tibetan Dunhuang manuscript version
017-Tsadra-BCA-Root-Tsadra_Edition Tsadra Edition Tibetan Tsadra Foundation critical edition
004-Tsadra-BCA-Root-Padmakara-French French French French Padmakara translation

Examples[edit]

Basic Verse Display[edit]

Display verse 1.1 with all available sources:

{{#invoke:GetRootVerses|main|verse=1.1}}

Display Specific Sources[edit]

Show only Tibetan and Sanskrit for verse 5.109:

{{#invoke:GetRootVerses|SimpleVerse
|verse=5.109
|sources=001-Tsadra-BCA-Root-Padmakara;009-Tsadra-BCA-Root-Gomez
}}

On a Verse Page[edit]

Typical usage on a verse page combines multiple functions:

<!-- Store properties -->
{{#invoke:GetRootVerses|set}}

<!-- Display toggles -->
<div class="verse-controls">
{{#invoke:GetRootVerses|getSourceToggles}}
</div>

<!-- Display verse -->
{{#invoke:GetRootVerses|main}}

Performance[edit]

The module is optimized for minimal database queries:

  • Single query approach: Makes one comprehensive SMW query per verse to retrieve all data
  • No caching: Avoids memory accumulation issues in MediaWiki's Lua environment
  • Lazy loading: Audio files only fetched when needed
  • Efficient organization: Data processed in Lua memory after retrieval

Typical performance:

  • 1-2 SMW queries per page load
  • <2 second load time for complex verses
  • Minimal memory footprint

Technical Details[edit]

Data Retrieval[edit]

The Module:GetRootVerses/Data module's getAllVerseData() function makes a single optimized query:

-- Single query returns ALL verse data
local query = [[
  [[Category:Translation Memories]]
  [[RootTextVerseNumPadmaK::verse]]
  [[SegmentFormat::Root]]
  |?TransMemID|?SegmentSource|?SegmentTranslation|...
  |limit=1000
]]

Results are organized into a structured table:

{
  bySource = {
    ['001-Tsadra-BCA-Root-Padmakara'] = {
      segments = {...},
      metadata = {...}
    },
    ...
  },
  rootSources = {...},
  translations = {...}
}

Display Components[edit]

The Module:GetRootVerses/Display module provides reusable rendering functions:

  • renderVerseContent() - Source text and transliteration
  • renderSourceHeader() - Source title with toggles
  • renderVersionColumn() - Wrapper for VersionContent template
  • renderTranslationsBox() - Scrollable translations container
  • createToggle() - Visibility toggle widgets

Templates Used[edit]

The module relies on these templates:

Widgets Used[edit]

  • Widget:SingleVisibilityToggle - Toggle controls for showing/hiding content

Dependencies[edit]

Extensions[edit]

  • Semantic MediaWiki (SMW) - For querying verse data
  • External Data - For fetching audio track information from commons.tsadra.org
  • Scribunto - MediaWiki's Lua scripting extension

Required Properties[edit]

The module expects these SMW properties to be set on Translation Memory pages:

  • TransMemID
  • TransMemNum
  • RootTextVerseNumPadmaK
  • SegmentSource
  • SourceSortOrder
  • SourceTranslit
  • SegmentTranslation
  • SegmentOrder
  • RootTextChapterNum
  • RootTextVerseNum
  • SegmentFormat
  • SourceVersionLabel
  • RootSourceTransMem
  • Spanish (optional)
  • Flag (optional)

Error Handling[edit]

The module handles various error conditions gracefully:

  • No verse data found: Returns empty string
  • Missing SMW: Returns error message "mw.smw module not found"
  • Invalid verse number: Returns empty string
  • Missing audio: Continues without audio player
  • Missing translations: Shows only available translations

Maintenance[edit]

Updating Translation Memories[edit]

When translation memory data is updated:

  1. Update the corresponding Translation Memory pages with new data
  2. Pages using the module will automatically reflect changes
  3. No module modification needed

Adding New Sources[edit]

To add a new translation memory source:

  1. Add the new TransMemID to the DATA section queries if it's a root source
  2. Update the AUDIO_CATEGORIES if it has audio
  3. Module will automatically include it in queries

Troubleshooting[edit]

No verses displaying:

  • Check that Translation Memory pages exist and have proper categories
  • Verify RootTextVerseNumPadmaK property is set correctly
  • Ensure SegmentFormat is set to "Root"

Missing translations:

  • Verify RootSourceTransMem property points to correct source
  • Check that translation pages are categorized as Translation Memories

Audio not appearing:

  • Verify audio files exist on commons.tsadra.org
  • Check category structure (BCA Tibetan/Sanskrit Chanted Verses)
  • Confirm ChapterNumber and VerseNumber properties are set

Version History[edit]

  • v2.0 (2025) - Optimized version with single-query approach, modular architecture
  • v1.0 (Previous) - Original monolithic implementation

See Also[edit]

Categories[edit]

This module is used on verse pages throughout the BCA project and is part of the core display infrastructure.






Developer Documentation[edit]

This page provides technical details for developers working on the GetRootVerses module system.

Architecture Overview[edit]

Module:GetRootVerses (main)
├── Public API functions
├── Helper functions for rendering
└── Requires:
    ├── Module:GetRootVerses/Data
    └── Module:GetRootVerses/Display

Module:GetRootVerses/Data
├── getAllVerseData() - Single optimized query
├── getAudioTrack() - Fetch audio files
├── processVerseSegments() - Organize segments
└── processTranslations() - Sort translations

Module:GetRootVerses/Display
├── renderVerseContent() - Source text HTML
├── renderSourceHeader() - Headers with toggles
├── renderVersionColumn() - Content columns
├── renderTranslationsBox() - Translation container
└── createToggle() - Toggle widgets

Key Design Decisions[edit]

Single Query Approach[edit]

The module uses ONE comprehensive SMW query to retrieve all verse data, rather than multiple queries:

-- Single query gets everything
function p.getAllVerseData(verse)
    local query = [[
        [[Category:Translation Memories]]
        [[RootTextVerseNumPadmaK::]] .. verse .. [[]]
        [[SegmentFormat::Root]]
        |?TransMemID|?SegmentSource|?SegmentTranslation
        |?Spanish|?Flag|...all properties
        |limit=1000
    ]]
    
    local results = smw.ask(query)
    -- Organize in memory by source
    return organized
end

Why:

  • 95% reduction in database queries (20 → 1)
  • All data available for processing
  • No caching needed (avoiding memory issues)
  • Faster overall performance

No Module-Level Caching[edit]

The module explicitly avoids module-level caching:

-- BAD - causes memory accumulation
local cache = {}
function getData()
    if cache[key] then return cache[key] end
    cache[key] = data
    return data
end

-- GOOD - data lives only during page render
function getData()
    local data = smw.ask(query)
    return data
end

Why:

  • MediaWiki keeps Lua modules loaded across pages
  • Module-level tables persist and grow
  • Causes "Not enough LUA memory" errors
  • Parser cache handles caching instead

Separation of Concerns[edit]

Three modules with distinct responsibilities:

Module Responsibility Should NOT
GetRootVerses Public API, orchestration Query SMW, build HTML strings
GetRootVerses/Data Data retrieval, organization Render HTML, call templates
GetRootVerses/Display HTML rendering Query data, process business logic

Data Flow[edit]

1. User calls: {{#invoke:GetRootVerses|main|verse=1.1}}
                    ↓
2. Main module: dataHelper.getAllVerseData(verse)
                    ↓
3. Data module: Makes ONE SMW query
                    ↓
4. Data module: Organizes results by source
                    ↓
5. Returns: { bySource = {...}, rootSources = {...}, translations = {...} }
                    ↓
6. Main module: Loops through sources, calls renderSourceSection()
                    ↓
7. Render function: Calls displayHelper functions
                    ↓
8. Display module: Returns HTML strings
                    ↓
9. Main module: Concatenates all HTML
                    ↓
10. Output: Complete verse display HTML

API Reference[edit]

Module:GetRootVerses/Data[edit]

-- Get all verse data in single query
getAllVerseData(verse)
  Parameters:
    verse (string) - Verse number e.g. "1.1"
  Returns:
    table - Organized data structure
      .bySource[tmid] = {segments, metadata}
      .rootSources[tmid] = metadata
      .translations[root_tmid][trans_tmid] = data

-- Get audio track file
getAudioTrack(chapter, verse, language)
  Parameters:
    chapter (string) - Chapter number
    verse (string) - Verse number  
    language (string) - 'tibetan' or 'sanskrit'
  Returns:
    string|nil - Audio filename or nil

-- Process verse segments
processVerseSegments(segments)
  Parameters:
    segments (table) - Array of segment data
  Returns:
    table - Processed structure with flags, verse numbers, etc.

-- Process translations
processTranslations(translationsData)
  Parameters:
    translationsData (table) - Translation data by source
  Returns:
    array - Sorted translations ready for display

Module:GetRootVerses/Display[edit]

-- Create toggle widget
createToggle(frame, id, label, options)
  Parameters:
    frame - MediaWiki frame object
    id (string) - Toggle element ID
    label (string) - Display label
    options (table) - Optional styling/state options
  Returns:
    string - HTML for toggle widget

-- Render verse content
renderVerseContent(data)
  Parameters:
    data (table) - Segment data with SegmentSource, SourceTranslit
  Returns:
    string - HTML for source text and transliteration

-- Render source header
renderSourceHeader(frame, label, transMemNum, showToggles)
  Parameters:
    frame - MediaWiki frame
    label (string) - Source version label
    transMemNum (string) - Translation memory number
    showToggles (boolean) - Whether to include toggle controls
  Returns:
    string - HTML header

-- Render version column
renderVersionColumn(frame, args)
  Parameters:
    frame - MediaWiki frame
    args (table) - Arguments for VersionContent template
  Returns:
    string - Template expansion result

-- Render translations box
renderTranslationsBox(frame, translations, sourceLabel, transMemNum)
  Parameters:
    frame - MediaWiki frame
    translations (array) - Processed translation data
    sourceLabel (string) - Source version label
    transMemNum (string) - Translation memory number
  Returns:
    string - HTML for scrollable translations container

Data Structures[edit]

getAllVerseData() Return Format[edit]

{
  bySource = {
    ['001-Tsadra-BCA-Root-Padmakara'] = {
      segments = {
        {
          Page = 'Translation_Memories/...',
          TransMemID = '001-Tsadra-BCA-Root-Padmakara',
          TransMemNum = '001',
          SegmentSource = 'བྱང་ཆུབ་སེམས་དཔའ་...',
          SourceTranslit = 'byang chub sems dpa\'...',
          SegmentTranslation = 'Homage to the bodhisattvas...',
          SegmentOrder = '1',
          RootTextChapterNum = '1',
          RootTextVerseNum = '1',
          Flag = nil,
          Spanish = nil
        },
        -- More segments...
      },
      metadata = {
        TransMemID = '001-Tsadra-BCA-Root-Padmakara',
        TransMemNum = '001',
        SourceVersionLabel = 'Padmakara Tibetan',
        TranslationWikiPage = 'Books/...',
        SourceWikiPage = 'Texts/...',
        ShortTitle = 'Padmakara 2006',
        RootSourceTransMem = '001-Tsadra-BCA-Root-Padmakara'
      }
    },
    ['009-Tsadra-BCA-Root-Gomez'] = { ... },
    -- More sources...
  },
  
  rootSources = {
    ['001-Tsadra-BCA-Root-Padmakara'] = { metadata },
    ['009-Tsadra-BCA-Root-Gomez'] = { metadata },
    -- Only root sources
  },
  
  translations = {
    ['001-Tsadra-BCA-Root-Padmakara'] = {
      ['002-Other-Translation'] = { segments, metadata },
      ['003-Another-Translation'] = { segments, metadata },
      -- All translations based on this root source
    },
    -- More root sources...
  }
}

processTranslations() Return Format[edit]

{
  {
    id = '002-Other-Translation',
    content = {'<p>Translation text...</p>', '<p>More text...</p>'},
    source = 'Books/Title',
    shortTitle = 'Publisher Year',
    year = '2008',
    page = 'Translation_Memories/Page'
  },
  -- More translations, sorted by year
}

Performance Optimization Tips[edit]

Query Optimization[edit]

-- GOOD: One query with all needed properties
local query = [[
  [[Category:X]][[Property::Y]]
  |?PropA|?PropB|?PropC|?PropD
  |limit=1000
]]

-- BAD: Multiple queries
for each item do
  local q1 = [[[[Item]]|?PropA]]
  local q2 = [[[[Item]]|?PropB]]
end

String Building[edit]

-- GOOD: Build array, concat once
local parts = {'<div>', content, '</div>'}
return table.concat(parts)

-- BAD: Repeated concatenation
local str = '<div>'
str = str .. content
str = str .. '</div>'
return str

Memory Management[edit]

-- GOOD: Local variables only
function getData()
  local result = query()
  return result
end

-- BAD: Module-level state
local cache = {}
function getData()
  cache[key] = query()
  return cache[key]
end

Testing[edit]

Unit Testing Data Functions[edit]

-- Test getAllVerseData
local data = require('Module:GetRootVerses/Data')
local result = data.getAllVerseData('1.1')

assert(result.bySource['001-Tsadra-BCA-Root-Padmakara'], 
       'Should have Tibetan source')
assert(#result.bySource['001-Tsadra-BCA-Root-Padmakara'].segments > 0,
       'Should have segments')

Integration Testing[edit]

<!-- Test on sandbox page -->
Test verse 1.1:
{{#invoke:GetRootVerses/sandbox|main|verse=1.1}}

Test missing verse:
{{#invoke:GetRootVerses/sandbox|main|verse=999.999}}

Test SimpleVerse:
{{#invoke:GetRootVerses/sandbox|SimpleVerse|verse=1.1|sources=001-Tsadra-BCA-Root-Padmakara}}

Debugging[edit]

Enable Debug Output[edit]

-- Temporary debug code
function p.main(frame)
    local verse = pargs.verse
    local allData = dataHelper.getAllVerseData(verse)
    
    -- Debug: show what we got
    local debug = mw.text.jsonEncode(allData.rootSources)
    return '<pre>' .. debug .. '</pre>'
end

Common Issues[edit]

No data returned:

-- Check query results
local results = smw.ask(query)
if not results then
    return 'Query returned nil'
end
if #results == 0 then
    return 'Query returned empty table'
end

Memory errors:

-- Check for module-level variables
-- These persist across pages:
local persistentData = {}  -- BAD

-- These don't:
function getData()
    local localData = {}   -- GOOD
end

Extending the Module[edit]

Adding a New Source[edit]

No code changes needed! The module automatically includes any source that:

  1. Is categorized as Translation Memory
  2. Has RootTextVerseNumPadmaK property set
  3. Has SegmentFormat set to "Root"
  4. Has RootSourceTransMem pointing to itself (for root sources)

Adding a New Display Feature[edit]

1. Add rendering function to Display module:

-- In Module:GetRootVerses/Display
function p.renderNewFeature(data, options)
    local parts = {'<div class="new-feature">'}
    -- Build HTML
    parts[#parts + 1] = '</div>'
    return table.concat(parts)
end

2. Call from main module:

-- In Module:GetRootVerses
local newFeature = displayHelper.renderNewFeature(data, {opt = 'value'})
parts[#parts + 1] = newFeature

Adding a New Data Source[edit]

To fetch from a new data source (beyond SMW):

-- In Module:GetRootVerses/Data
function p.getExternalData(identifier)
    local data = mw.ext.externalData.getExternalData{
        url = 'https://example.org/api?id=' .. identifier,
        data = { result = 'Result' },
        format = 'JSON'
    }
    return data.result
end

Maintenance Procedures[edit]

Updating After MediaWiki Upgrade[edit]

  1. Test basic functionality
  2. Check Scribunto version compatibility
  3. Verify SMW query syntax still works
  4. Test template expansions
  5. Monitor performance

Optimizing Queries[edit]

If queries become slow:

-- Add index hints
[[Category:Translation Memories]]  -- Indexed
[[RootTextVerseNumPadmaK::1.1]]   -- Should be indexed
[[SegmentFormat::Root]]            -- Should be indexed

-- Reduce limit if possible
|limit=1000  -- Try 500 if data allows

-- Request only needed properties
|?Prop1|?Prop2  -- Not |?Property::*

Code Style Guidelines[edit]

  • Use descriptive variable names
  • Comment complex logic
  • Keep functions under 50 lines
  • Prefer local variables
  • Use table.concat for string building
  • Validate input parameters
  • Return early on errors
  • Use helper functions for repeated code

Related Documentation[edit]



-- Module:GetRootVerses (Optimized for minimal queries)
-- Main entry point for verse display functionality

local p = {}

-- Load helper modules
local dataHelper = require('Module:GetRootVerses/Data')
local displayHelper = require('Module:GetRootVerses/Display')

-- Frequently used functions
local smw = mw.smw
local getCurrentTitle = mw.title.getCurrentTitle

-- Store semantic properties for a verse from translation memories
function p.set(frame)
    if not smw then
        return "mw.smw module not found"
    end

    local verse = getCurrentTitle().subpageText

    -- Get ALL data in one query
    local allData = dataHelper.getAllVerseData(verse)

    -- Get Tibetan data
    local tibetanData = allData.bySource['001-Tsadra-BCA-Root-Padmakara']
    if not tibetanData or #tibetanData.segments == 0 then
        return ''
    end

    local data = tibetanData.segments[1]

    -- Build data store for SMW
    local dataStore = {
        'SegmentSource=' .. (data.SegmentSource or ''),
        'SourceTranslit=' .. (data.SourceTranslit or ''),
        'SegmentTranslation=' .. (data.SegmentTranslation or ''),
        'TransMemID=' .. (data.TransMemID or ''),
        'TransMemPage=' .. (data.Page or ''),
        'SegmentOrder=' .. (data.SegmentOrder or ''),
        'RootTextChapVerseNum=' .. (data.RootTextChapVerseNum or ''),
        'RootTextChapterNum=' .. (data.RootTextChapterNum or ''),
        'RootTextVerseNum=' .. (data.RootTextVerseNum or ''),
        '+sep=;',
        'SegmentFormat=Root',
        'TibetanChanted='
    }

    -- Add Sanskrit data if available
    local sanskritData = allData.bySource['009-Tsadra-BCA-Root-Gomez']
    if sanskritData and #sanskritData.segments > 0 then
        local skt = sanskritData.segments[1]
        dataStore[#dataStore + 1] = 'SanskritSource=' .. (skt.SegmentSource or '')
        dataStore[#dataStore + 1] = 'SanskritTranslit=' .. (skt.SourceTranslit or '')
        dataStore[#dataStore + 1] = 'SanskritTranslation=' .. (skt.SegmentTranslation or '')
    end

    -- Add French data if available
    local frenchData = allData.bySource['004-Tsadra-BCA-Root-Padmakara-French']
    if frenchData and #frenchData.segments > 0 then
        dataStore[#dataStore + 1] = 'FrenchTranslation=' .. (frenchData.segments[1].SegmentTranslation or '')
    end

    -- Add Spanish data if available
    local spanishData = allData.bySource['021-Tsadra-BCA-Padmakara-Spanish']
    if spanishData and #spanishData.segments > 0 then
        dataStore[#dataStore + 1] = 'Spanish=' .. (spanishData.segments[1].SegmentTranslation or '')
    end

    -- SEO: Build dynamic metadata properties
    local chapVerseNum = data.RootTextChapVerseNum or verse

    -- Title: use first line of Padmakara English translation as the verse theme
    local englishFirstLine = (data.SegmentTranslation or ''):match('^([^\n]+)') or ''
    -- Strip inline footnotes: superscript numbers followed by footnote text (e.g. "bliss,25‟Those who...")
    -- Footnotes appear as one or more digits immediately followed by a non-space char mid-sentence
    englishFirstLine = englishFirstLine:gsub('%d+[‟"\'].*$', '')
    -- Strip any remaining wikitext markup for clean title
    englishFirstLine = englishFirstLine:gsub('%[%[.-%|(.-)%]%]', '%1'):gsub('%[%[(.-)%]%]', '%1'):gsub("'''?", '')
    -- Strip complete HTML tags (e.g. <span>...</span>), then any remaining unclosed tag fragments
    englishFirstLine = englishFirstLine:gsub('<[^>]->[^<]*</[^>]+>', ''):gsub('<[^>]*>?.*$', '')
    englishFirstLine = englishFirstLine:match('^(.-)%s*$') or englishFirstLine  -- trim trailing whitespace
    englishFirstLine = englishFirstLine:match('^(.-)%s*[,;]?%s*$') or englishFirstLine  -- trim trailing comma/semicolon

    -- Collect translator ShortTitles from all present root sources
    local translators = {}
    local translatorSeen = {}
    for _, sourceInfo in pairs(allData.rootSources) do
        local st = sourceInfo.ShortTitle or ''
        if st ~= '' and not translatorSeen[st] then
            translators[#translators + 1] = st
            translatorSeen[st] = true
        end
    end
    -- Also include non-root translation ShortTitles
    for _, transGroup in pairs(allData.translations) do
        for _, transData in pairs(transGroup) do
            local st = transData.metadata.ShortTitle or ''
            if st ~= '' and not translatorSeen[st] then
                translators[#translators + 1] = st
                translatorSeen[st] = true
            end
        end
    end

    -- Collect commentary author labels: query all commentary basepages,
    -- then check each one for segments on this verse (N+1, but N is only 5-12)
    local commentaryAuthors = {}
    local commBasepages = mw.smw.ask(
        '[[Category:Translation Memory Basepages]][[Translation Memory:+]][[TransMemType::Commentary]]' ..
        '|?TransMemID|?AuthorLabel|?SourceVersionLabel|limit=50'
    )
    if commBasepages then
        for _, comm in ipairs(commBasepages) do
            local label = comm.SourceVersionLabel or comm.AuthorLabel
            if comm.TransMemID and label then
                local hasSegment = mw.smw.ask(
                    '[[TransMemID::' .. comm.TransMemID .. ']]' ..
                    '[[RootTextChapVerseNum::' .. mw.uri.encode(verse) .. ']]' ..
                    '[[SegmentFormat::!Deprecated]]|limit=1'
                )
                if hasSegment and #hasSegment > 0 then
                    commentaryAuthors[#commentaryAuthors + 1] = label
                end
            end
        end
    end

    -- Build AllTopics: verse identifiers + static terms + translators + commentary authors + language keywords
    local topicParts = {
        'BCA ' .. chapVerseNum,
        'Bodhicaryāvatāra ' .. chapVerseNum,
        'Śāntideva',
        'Shantideva',
        'Buddhist Translation Memory',
        'Way of the Bodhisattva',
        'Bilingual Buddhist Text'
    }
    for _, t in ipairs(translators) do
        topicParts[#topicParts + 1] = t
    end
    for _, a in ipairs(commentaryAuthors) do
        topicParts[#topicParts + 1] = a
    end
    if allData.bySource['009-Tsadra-BCA-Root-Gomez'] then
        topicParts[#topicParts + 1] = 'Sanskrit Buddhist Poetry'
    end
    if allData.bySource['004-Tsadra-BCA-Root-Padmakara-French'] then
        topicParts[#topicParts + 1] = 'French Buddhist Translation'
    end
    if allData.bySource['021-Tsadra-BCA-Padmakara-Spanish'] or
       (allData.translations['001-Tsadra-BCA-Root-Padmakara'] and
        allData.translations['001-Tsadra-BCA-Root-Padmakara']['001-Spanish']) then
        topicParts[#topicParts + 1] = 'Spanish Buddhist Translation'
    end

    -- Build Description
    local translatorStr = #translators > 0 and table.concat(translators, ', ') or 'multiple translators'
    local commStr = #commentaryAuthors > 0 and (' Commentaries by ' .. table.concat(commentaryAuthors, ', ') .. '.') or ''
    local description = 'Study hub for Bodhicaryāvatāra Verse ' .. chapVerseNum ..
        '. Features the original Tibetan and Sanskrit with translations by ' ..
        translatorStr .. '.' .. commStr

    -- Set SEO properties
    dataStore[#dataStore + 1] = 'Title=BCA Verse ' .. chapVerseNum .. ': ' .. englishFirstLine .. ' | Bodhicaryāvatāra'
    dataStore[#dataStore + 1] = 'Description=' .. description
    dataStore[#dataStore + 1] = 'AllTopics=' .. table.concat(topicParts, '; ')

    local success = smw.set(dataStore)
    return success == true and '' or 'An error occurred: ' .. (success.error or 'unknown error')
end

-- Generate toggle controls for different source versions
function p.getSourceToggles(frame)
    local pargs = frame:getParent().args
    local verse = pargs.verse or getCurrentTitle().subpageText
    
    -- Get all data in one query
    local allData = dataHelper.getAllVerseData(verse)
    
    local outputs = {}
    for tmid, metadata in pairs(allData.rootSources) do
        if metadata.TransMemNum and metadata.SourceVersionLabel then
            outputs[#outputs + 1] = displayHelper.createToggle(
                frame,
                'source-' .. metadata.TransMemNum,
                metadata.SourceVersionLabel
            )
        end
    end
    
    return table.concat(outputs)
end

-- Main verse display function
function p.main(frame)
    local pargs = frame:getParent().args
    local verse = pargs.verse or getCurrentTitle().subpageText
    
    -- SINGLE QUERY to get all data
    local allData = dataHelper.getAllVerseData(verse)
    
    if not allData.rootSources or not next(allData.rootSources) then
        return ''
    end
    
    local output = {'<div class="row">'}
    
    -- Sort root sources by TransMemNum
    local sortedSources = {}
    for tmid, metadata in pairs(allData.rootSources) do
        sortedSources[#sortedSources + 1] = {tmid = tmid, metadata = metadata}
    end
    table.sort(sortedSources, function(a, b)
        return (a.metadata.SourceSortOrder or '') < (b.metadata.SourceSortOrder or '')
    end)
    
    for _, sourceInfo in ipairs(sortedSources) do
        local tmid = sourceInfo.tmid
        local sourceData = allData.bySource[tmid]
        
        if sourceData and #sourceData.segments > 0 then
            local section = renderSourceSection(frame, verse, sourceData, allData.translations[tmid])
            if section then
                output[#output + 1] = section
            end
        end
    end
    
    output[#output + 1] = '</div>'
    return table.concat(output)
end

-- Helper: Render a complete source section
function renderSourceSection(frame, verse, sourceData, translationsData)
    local metadata = sourceData.metadata
    local processed = dataHelper.processVerseSegments(sourceData.segments)
    
    if not processed then
        return nil
    end
    
    local parts = {
        '<div id="source-', metadata.TransMemNum,
        '" class="toggleable col-xl-6 col-xxxl-4 position-relative mb-2">'
    }
    
    -- Source header with toggles
    parts[#parts + 1] = displayHelper.renderSourceHeader(
        frame,
        metadata.SourceVersionLabel,
        metadata.TransMemNum,
        true
    )
    
    parts[#parts + 1] = '<div class="row">'
    
    -- Build source content
    local sourceContent = {}
    local translationContent = {}
    
    for i, segment in ipairs(processed.segments) do
        sourceContent[#sourceContent + 1] = displayHelper.renderVerseContent(segment)
        
        local showAltVerse = (metadata.TransMemNum == '005')
        sourceContent[#sourceContent + 1] = displayHelper.renderVerseInfo(segment, {
            showAltVerse = showAltVerse
        })
        
        if segment.SegmentTranslation then
            translationContent[#translationContent + 1] = '<p>'
            translationContent[#translationContent + 1] = segment.SegmentTranslation
            translationContent[#translationContent + 1] = '</p>'
        end
    end
    
    -- Add audio players (only if needed)
    if processed.chapterNum and metadata.TransMemNum == '001' then
        for verseNum in pairs(processed.verseNums) do
            local audioFile = dataHelper.getAudioTrack(processed.chapterNum, verseNum, 'tibetan', metadata.TransMemNum)
            if audioFile then
                sourceContent[#sourceContent + 1] = displayHelper.renderAudioVerse(
                    frame, audioFile, metadata.TransMemNum, verseNum,
                    processed.segments[1].SegmentOrder or '1'
                )
            end
        end
    elseif processed.chapterNum and metadata.TransMemNum == '009' then
        for verseNum in pairs(processed.verseNums) do
            local audioFile = dataHelper.getAudioTrack(processed.chapterNum, verseNum, 'sanskrit', metadata.TransMemNum)
            if audioFile then
                sourceContent[#sourceContent + 1] = displayHelper.renderAudioVerse(
                    frame, audioFile, metadata.TransMemNum, verseNum,
                    processed.segments[1].SegmentOrder or '1'
                )
            end
        end
    end
    
    -- Handle special cases for translation content
    local translationText = table.concat(translationContent)
    local sourceWidth, transWidth = 'col-sm-5', 'col-sm-7'
    if metadata.TransMemNum == '009' and translationText == '' then
        translationText = '<em>Translation Currently Unavailable</em>'
    elseif metadata.TransMemNum == '017' and translationText == '' then
        translationText = ''
        sourceWidth, transWidth = 'col-12', 'col-12'
    end
    
    -- Render columns
    parts[#parts + 1] = displayHelper.renderVersionColumn(frame, {
        class = 'source-column ' .. sourceWidth .. ' pr-sm-0 mb-2',
        content = table.concat(sourceContent),
        fontSizeAdjust = '160',
        flag = table.concat(processed.flags, ';'),
        id = 'sourceLang-' .. metadata.TransMemNum
    })
    
    parts[#parts + 1] = displayHelper.renderVersionColumn(frame, {
        class = 'translation-column ' .. transWidth .. ' mb-2',
        content = translationText,
        source = metadata.TranslationWikiPage or metadata.SourceWikiPage,
        id = 'transLang-' .. metadata.TransMemNum,
        shorttitle = metadata.ShortTitle
    })
    
    parts[#parts + 1] = '</div>'
    
    -- Other translations section
    if translationsData and next(translationsData) then
        local processedTranslations = dataHelper.processTranslations(translationsData)
        
        if #processedTranslations > 0 then
            local isChecked = (metadata.TransMemNum == "001")
            parts[#parts + 1] = displayHelper.renderTranslationsToggle(
                frame, metadata.TransMemNum, isChecked
            )
            
            parts[#parts + 1] = displayHelper.renderTranslationsBox(
                frame,
                processedTranslations,
                metadata.SourceVersionLabel,
                metadata.TransMemNum
            )
        end
    end
    
    parts[#parts + 1] = '</div>'
    
    return table.concat(parts)
end

-- Simplified verse display
function p.SimpleVerse(frame)
    local pargs = frame:getParent().args
    local verse = pargs.verse
    local sources = pargs.sources
    
    if not verse or not sources then
        return ''
    end
    
    -- SINGLE QUERY to get all data
    local allData = dataHelper.getAllVerseData(verse)
    
    local output = {'<div class="mx-2 mx-sm-0">'}
    
    for source in string.gmatch(sources, "([^;]+)") do
        local sourceData = allData.bySource[source]
        
        if sourceData and #sourceData.segments > 0 then
            local section = renderSimpleSourceSection(frame, verse, sourceData, allData.translations[source])
            if section then
                output[#output + 1] = section
            end
        end
    end
    
    output[#output + 1] = '</div>'
    return table.concat(output)
end

-- Helper: Render simplified source section
function renderSimpleSourceSection(frame, verse, sourceData, translationsData)
    local metadata = sourceData.metadata
    local processed = dataHelper.processVerseSegments(sourceData.segments)
    
    if not processed then
        return nil
    end
    
    local parts = {'<div class="position-relative mb-4">'}
    
    -- Header
    parts[#parts + 1] = '<div class="h4 mt-2 mb-3">'
    parts[#parts + 1] = metadata.SourceVersionLabel
    
    if metadata.TransMemNum == '005' and processed.segments[1].RootTextChapVerseNum then
        parts[#parts + 1] = ' <span class="text-70 text-muted">(v. '
        parts[#parts + 1] = processed.segments[1].RootTextChapVerseNum
        parts[#parts + 1] = ')</span>'
    end
    
    parts[#parts + 1] = '</div><div class="row">'
    
    -- Build content
    local sourceContent = {}
    local translationContent = {}
    
    for _, segment in ipairs(processed.segments) do
        sourceContent[#sourceContent + 1] = displayHelper.renderVerseContent(segment)
        
        if segment.SegmentTranslation then
            translationContent[#translationContent + 1] = '<p>'
            translationContent[#translationContent + 1] = segment.SegmentTranslation
            translationContent[#translationContent + 1] = '</p>'
        end
    end
    
    -- Columns
    parts[#parts + 1] = displayHelper.renderVersionColumn(frame, {
        class = 'col-sm-5 pr-sm-0 mb-2',
        content = table.concat(sourceContent),
        fontSizeAdjust = '140',
        flag = table.concat(processed.flags, ';')
    })
    
    parts[#parts + 1] = displayHelper.renderVersionColumn(frame, {
        class = 'col-sm-7 mb-2',
        content = table.concat(translationContent),
        source = metadata.TranslationWikiPage,
        shorttitle = metadata.ShortTitle
    })
    
    parts[#parts + 1] = '</div>'
    
    -- Other translations
    if translationsData and next(translationsData) then
        local processedTranslations = dataHelper.processTranslations(translationsData)
        
        if #processedTranslations > 0 then
            parts[#parts + 1] = displayHelper.renderSimpleTranslationsBox(
                frame,
                processedTranslations,
                metadata.SourceVersionLabel
            )
        end
    end
    
    parts[#parts + 1] = '</div>'
    
    return table.concat(parts)
end

return p