59fe69d35d
BREAKING CHANGE: Template names changed from page1_stats.html to page_1.html ## Phase 1: Unified Pagination Function - Implemented PaginateMultiList() to replace PaginateSkills(), PaginateSpells(), and PaginatePage2PlayLists() - Single metadata-driven function handles all list types (skills, weapons, spells, equipment) - Properly handles filters (learned/unlearned/language) via template metadata - Shares list trackers by ListType+Filter combination to avoid duplication - Added comprehensive tests for all edge cases ## Phase 2: Template Naming Convention - Renamed templates to be data-agnostic: - page1_stats.html -> page_1.html - page1.2_stats.html -> page_1.2.html - page2_play.html -> page_2.html - page2.2_play.html -> page_2.2.html - page3_spell.html -> page_3.html - page3.2_spell.html -> page_3.2.html - page4_equip.html -> page_4.html - Updated GenerateContinuationTemplateName() for new naming (page_1.html -> page_1.2.html) - Updated ExtractBaseTemplateName() to handle new format - Updated all test files and source files with new template names ## Phase 3: Simplified RenderPageWithContinuations - Removed hardcoded switch statements based on template names - Replaced with generic dataMap and unified pagination call - Extracted populatePageDataFromDistribution() to handle data mapping - Template type detection now driven by metadata, not hardcoded names ## Benefits - ✅ Extensibility: Add new templates without code changes - ✅ Maintainability: One pagination algorithm instead of three - ✅ Clarity: Template names reflect page numbers, not content types - ✅ Flexibility: Templates can mix any data types - ✅ All 40+ tests passing ## Technical Details - Added SkillsColumn3 and SkillsColumn4 fields to PageData for continuation pages - Template metadata loaded from HTML comments drives pagination behavior - Backward compatibility maintained for old template references in comments
202 lines
6.6 KiB
Go
202 lines
6.6 KiB
Go
package pdfrender
|
|
|
|
// GetBlockCapacity gets the MAX capacity for a block from template metadata
|
|
// Returns 0 if block not found
|
|
func GetBlockCapacity(templateSet *TemplateSet, templateName, blockName string) int {
|
|
for _, tmpl := range templateSet.Templates {
|
|
if tmpl.Metadata.Name == templateName {
|
|
block := GetBlockByName(tmpl.Metadata.Blocks, blockName)
|
|
if block != nil {
|
|
return block.MaxItems
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// PreparePaginatedPageData prepares data for rendering a template page with proper pagination
|
|
// It takes the full view model and returns PageData with lists split according to template capacity
|
|
func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName string, pageNumber int, date string) (*PageData, error) {
|
|
// Get template metadata to determine capacities
|
|
templateSet := DefaultA4QuerTemplateSet()
|
|
|
|
pageData := &PageData{
|
|
Character: viewModel.Character,
|
|
Attributes: viewModel.Attributes,
|
|
DerivedValues: viewModel.DerivedValues,
|
|
GameResults: viewModel.GameResults,
|
|
Meta: PageMeta{
|
|
Date: date,
|
|
PageNumber: pageNumber,
|
|
},
|
|
}
|
|
|
|
// For page1_stats.html - paginate skills across two columns
|
|
if templateName == "page_1.html" {
|
|
// Get the template metadata
|
|
var template *TemplateMetadata
|
|
for _, tmpl := range templateSet.Templates {
|
|
if tmpl.Metadata.Name == templateName {
|
|
template = &tmpl.Metadata
|
|
break
|
|
}
|
|
}
|
|
|
|
if template != nil {
|
|
// Get skill blocks (should be skills_column1 and skills_column2)
|
|
var skillBlocks []BlockMetadata
|
|
for _, block := range template.Blocks {
|
|
if block.ListType == "skills" {
|
|
skillBlocks = append(skillBlocks, block)
|
|
}
|
|
}
|
|
|
|
if len(skillBlocks) >= 2 {
|
|
// Calculate how to split skills across columns
|
|
col1Capacity := skillBlocks[0].MaxItems
|
|
col2Capacity := skillBlocks[1].MaxItems
|
|
|
|
// Debug logging
|
|
// fmt.Printf("DEBUG: col1Capacity=%d, col2Capacity=%d\n", col1Capacity, col2Capacity)
|
|
|
|
col1Skills, col2Skills := SplitSkillsForColumns(viewModel.Skills, col1Capacity, col2Capacity)
|
|
// Fill to capacity to ensure empty rows render in template
|
|
pageData.SkillsColumn1 = FillToCapacity(col1Skills, col1Capacity)
|
|
pageData.SkillsColumn2 = FillToCapacity(col2Skills, col2Capacity)
|
|
pageData.Skills = viewModel.Skills // Keep for backward compatibility
|
|
} else {
|
|
pageData.Skills = viewModel.Skills
|
|
}
|
|
} else {
|
|
pageData.Skills = viewModel.Skills
|
|
}
|
|
} else if templateName == "page_2.html" {
|
|
// Get capacities from template
|
|
weaponsCapacity := GetBlockCapacity(&templateSet, templateName, "weapons_main")
|
|
learnedCapacity := GetBlockCapacity(&templateSet, templateName, "skills_learned")
|
|
languageCapacity := GetBlockCapacity(&templateSet, templateName, "skills_languages")
|
|
|
|
// Limit and fill weapons to capacity
|
|
weapons := viewModel.Weapons
|
|
if weaponsCapacity > 0 && len(weapons) > weaponsCapacity {
|
|
weapons = weapons[:weaponsCapacity]
|
|
}
|
|
if weaponsCapacity > 0 {
|
|
pageData.Weapons = FillToCapacity(weapons, weaponsCapacity)
|
|
} else {
|
|
pageData.Weapons = weapons
|
|
}
|
|
|
|
// Filter skills by category for page2 blocks
|
|
var learnedSkills, languageSkills []SkillViewModel
|
|
for _, skill := range viewModel.Skills {
|
|
if skill.Category == "Sprache" {
|
|
languageSkills = append(languageSkills, skill)
|
|
} else if skill.IsLearned {
|
|
learnedSkills = append(learnedSkills, skill)
|
|
}
|
|
}
|
|
|
|
// Apply capacity limits
|
|
if learnedCapacity > 0 && len(learnedSkills) > learnedCapacity {
|
|
learnedSkills = learnedSkills[:learnedCapacity]
|
|
}
|
|
if languageCapacity > 0 && len(languageSkills) > languageCapacity {
|
|
languageSkills = languageSkills[:languageCapacity]
|
|
}
|
|
|
|
// Fill to capacity to ensure empty rows render
|
|
if learnedCapacity > 0 {
|
|
pageData.SkillsLearned = FillToCapacity(learnedSkills, learnedCapacity)
|
|
} else {
|
|
pageData.SkillsLearned = learnedSkills
|
|
}
|
|
if languageCapacity > 0 {
|
|
pageData.SkillsLanguage = FillToCapacity(languageSkills, languageCapacity)
|
|
} else {
|
|
pageData.SkillsLanguage = languageSkills
|
|
}
|
|
pageData.Skills = viewModel.Skills // Keep for backward compatibility
|
|
} else if templateName == "page_3.html" {
|
|
// Get capacities from template
|
|
spellsLeftCapacity := GetBlockCapacity(&templateSet, templateName, "spells_left")
|
|
spellsRightCapacity := GetBlockCapacity(&templateSet, templateName, "spells_right")
|
|
magicItemsCapacity := GetBlockCapacity(&templateSet, templateName, "magic_items")
|
|
|
|
// Split spells into left and right columns
|
|
leftSpells, rightSpells := SplitSkillsIntoColumns(viewModel.Spells, spellsLeftCapacity, spellsRightCapacity)
|
|
// Fill to capacity to ensure empty rows render
|
|
if spellsLeftCapacity > 0 {
|
|
pageData.SpellsLeft = FillToCapacity(leftSpells, spellsLeftCapacity)
|
|
} else {
|
|
pageData.SpellsLeft = leftSpells
|
|
}
|
|
if spellsRightCapacity > 0 {
|
|
pageData.SpellsRight = FillToCapacity(rightSpells, spellsRightCapacity)
|
|
} else {
|
|
pageData.SpellsRight = rightSpells
|
|
}
|
|
pageData.Spells = viewModel.Spells // Keep for backward compatibility
|
|
|
|
// Limit and fill magic items
|
|
magicItems := viewModel.MagicItems
|
|
if magicItemsCapacity > 0 && len(magicItems) > magicItemsCapacity {
|
|
magicItems = magicItems[:magicItemsCapacity]
|
|
}
|
|
if magicItemsCapacity > 0 {
|
|
pageData.MagicItems = FillToCapacity(magicItems, magicItemsCapacity)
|
|
} else {
|
|
pageData.MagicItems = magicItems
|
|
}
|
|
} else if templateName == "page_4.html" {
|
|
// Page 4 needs ALL equipment to properly render containers
|
|
// The template has complex logic showing containers on left, worn items and container sections on right
|
|
// Don't truncate based on capacity - let the template handle all items
|
|
pageData.Equipment = viewModel.Equipment
|
|
}
|
|
|
|
return pageData, nil
|
|
}
|
|
|
|
// SplitSkillsForColumns splits skills into two separate lists for two-column layout
|
|
// Returns (column1Skills, column2Skills)
|
|
func SplitSkillsForColumns(skills []SkillViewModel, col1Max, col2Max int) ([]SkillViewModel, []SkillViewModel) {
|
|
col1 := skills
|
|
if len(col1) > col1Max {
|
|
col1 = col1[:col1Max]
|
|
}
|
|
|
|
col2 := []SkillViewModel{}
|
|
if len(skills) > col1Max {
|
|
remaining := skills[col1Max:]
|
|
if len(remaining) > col2Max {
|
|
col2 = remaining[:col2Max]
|
|
} else {
|
|
col2 = remaining
|
|
}
|
|
}
|
|
|
|
return col1, col2
|
|
}
|
|
|
|
// SplitSkillsIntoColumns splits spells/items into two columns (generic for any slice type)
|
|
// Returns (column1, column2)
|
|
func SplitSkillsIntoColumns[T any](items []T, col1Max, col2Max int) ([]T, []T) {
|
|
col1 := items
|
|
if len(col1) > col1Max {
|
|
col1 = col1[:col1Max]
|
|
}
|
|
|
|
col2 := []T{}
|
|
if len(items) > col1Max {
|
|
remaining := items[col1Max:]
|
|
if len(remaining) > col2Max {
|
|
col2 = remaining[:col2Max]
|
|
} else {
|
|
col2 = remaining
|
|
}
|
|
}
|
|
|
|
return col1, col2
|
|
}
|