continuation of lists on a continuation page
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package pdfrender
|
||||
|
||||
import (
|
||||
"bamort/models"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
)
|
||||
|
||||
// TestIntegration_ContinuationPages_ActualFiles tests that continuation pages
|
||||
// are actually generated and saved as separate PDF files
|
||||
func TestIntegration_ContinuationPages_ActualFiles(t *testing.T) {
|
||||
// Arrange - Create character with 50 skills to force multiple pages
|
||||
char := &models.Char{
|
||||
BamortBase: models.BamortBase{
|
||||
Name: "Test Character With Many Skills",
|
||||
},
|
||||
Typ: "Krieger",
|
||||
Grad: 10,
|
||||
Alter: 35,
|
||||
Eigenschaften: []models.Eigenschaft{
|
||||
{Name: "St", Value: 90},
|
||||
{Name: "Gs", Value: 80},
|
||||
{Name: "Gw", Value: 70},
|
||||
{Name: "Ko", Value: 85},
|
||||
{Name: "In", Value: 60},
|
||||
{Name: "Zt", Value: 55},
|
||||
{Name: "Au", Value: 75},
|
||||
{Name: "pA", Value: 65},
|
||||
{Name: "Wk", Value: 50},
|
||||
},
|
||||
Lp: models.Lp{Value: 30, Max: 35},
|
||||
Ap: models.Ap{Value: 25, Max: 30},
|
||||
B: models.B{Value: 15},
|
||||
}
|
||||
|
||||
// Add 50 skills to force multiple continuation pages
|
||||
char.Fertigkeiten = make([]models.SkFertigkeit, 50)
|
||||
for i := 0; i < 50; i++ {
|
||||
char.Fertigkeiten[i] = models.SkFertigkeit{
|
||||
BamortCharTrait: models.BamortCharTrait{
|
||||
BamortBase: models.BamortBase{Name: "Skill " + string(rune('A'+i%26))},
|
||||
},
|
||||
Fertigkeitswert: 10 + i%15,
|
||||
Pp: i % 8,
|
||||
}
|
||||
}
|
||||
|
||||
// Map to view model
|
||||
viewModel, err := MapCharacterToViewModel(char)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to map character: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Character has %d skills", len(viewModel.Skills))
|
||||
|
||||
// Load templates
|
||||
loader := NewTemplateLoader("../templates/Default_A4_Quer")
|
||||
if err = loader.LoadTemplates(); err != nil {
|
||||
t.Fatalf("Failed to load templates: %v", err)
|
||||
}
|
||||
|
||||
renderer := NewPDFRenderer()
|
||||
|
||||
// Act - Render page1 with continuations
|
||||
pdfs, err := RenderPageWithContinuations(
|
||||
viewModel,
|
||||
"page1_stats.html",
|
||||
1,
|
||||
"20.12.2025",
|
||||
loader,
|
||||
renderer,
|
||||
)
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render with continuations: %v", err)
|
||||
}
|
||||
|
||||
if len(pdfs) < 2 {
|
||||
t.Fatalf("Expected at least 2 PDFs (main + continuation), got %d", len(pdfs))
|
||||
}
|
||||
|
||||
t.Logf("Generated %d PDF pages (1 main + %d continuations)", len(pdfs), len(pdfs)-1)
|
||||
|
||||
// Save all PDFs to disk
|
||||
outputDir := "/tmp/bamort_continuation_test"
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create output directory: %v", err)
|
||||
}
|
||||
|
||||
var filePaths []string
|
||||
for i, pdf := range pdfs {
|
||||
var filename string
|
||||
if i == 0 {
|
||||
filename = "page1_stats.pdf"
|
||||
} else {
|
||||
filename = "page1_stats_continuation_" + string(rune('0'+i)) + ".pdf"
|
||||
}
|
||||
|
||||
path := outputDir + "/" + filename
|
||||
if err := os.WriteFile(path, pdf, 0644); err != nil {
|
||||
t.Errorf("Failed to write %s: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
filePaths = append(filePaths, path)
|
||||
t.Logf("✓ Saved %s (%d bytes)", path, len(pdf))
|
||||
|
||||
// Verify PDF is valid
|
||||
if string(pdf[0:4]) != "%PDF" {
|
||||
t.Errorf("%s does not start with PDF marker", filename)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge all PDFs into a single file
|
||||
combinedPath := outputDir + "/page1_stats_combined.pdf"
|
||||
if err := api.MergeCreateFile(filePaths, combinedPath, false, nil); err != nil {
|
||||
t.Fatalf("Failed to merge PDFs: %v", err)
|
||||
}
|
||||
|
||||
// Get size of combined PDF
|
||||
combinedInfo, err := os.Stat(combinedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat combined PDF: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("\n✓ Combined all %d pages into: %s (%d bytes)", len(pdfs), combinedPath, combinedInfo.Size())
|
||||
t.Logf(" Output directory: %s", outputDir)
|
||||
t.Logf("\n✅ CONTINUATION PAGES SUCCESSFULLY GENERATED AND SAVED!")
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package pdfrender
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestContinuationPages_WhenSkillsExceedCapacity tests that overflow items
|
||||
// are distributed to continuation pages when template capacity is exceeded
|
||||
func TestContinuationPages_WhenSkillsExceedCapacity(t *testing.T) {
|
||||
// Arrange - Create more skills than fit on a single page
|
||||
// Assuming page1_stats has capacity of 64 (2 columns x 32 each)
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create 100 skills to force overflow
|
||||
skills := make([]SkillViewModel, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
skills[i] = SkillViewModel{
|
||||
Name: "Skill " + string(rune('A'+i%26)),
|
||||
Value: 10 + i%20,
|
||||
}
|
||||
}
|
||||
|
||||
// Act - Paginate skills for page1_stats
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Should have more than 1 page (100 skills / 64 capacity = 2 pages)
|
||||
if len(pages) < 2 {
|
||||
t.Errorf("Expected at least 2 pages for 100 skills, got %d", len(pages))
|
||||
}
|
||||
|
||||
// First page should be "page1_stats.html"
|
||||
if pages[0].TemplateName != "page1_stats.html" {
|
||||
t.Errorf("Expected first page template 'page1_stats.html', got '%s'", pages[0].TemplateName)
|
||||
}
|
||||
|
||||
// Second page should be continuation page with name pattern "page1.2_stats.html"
|
||||
expectedContinuation := "page1.2_stats.html"
|
||||
if pages[1].TemplateName != expectedContinuation {
|
||||
t.Errorf("Expected continuation page template '%s', got '%s'",
|
||||
expectedContinuation, pages[1].TemplateName)
|
||||
}
|
||||
|
||||
// Verify page numbers are sequential
|
||||
for i, page := range pages {
|
||||
expectedPageNum := i + 1
|
||||
if page.PageNumber != expectedPageNum {
|
||||
t.Errorf("Page %d: expected page number %d, got %d",
|
||||
i, expectedPageNum, page.PageNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all 100 skills are distributed
|
||||
totalSkills := 0
|
||||
for _, page := range pages {
|
||||
for _, data := range page.Data {
|
||||
if skillSlice, ok := data.([]SkillViewModel); ok {
|
||||
totalSkills += len(skillSlice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalSkills != 100 {
|
||||
t.Errorf("Expected 100 total skills across all pages, got %d", totalSkills)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContinuationPages_WhenWeaponsExceedCapacity tests overflow handling for weapons
|
||||
func TestContinuationPages_WhenWeaponsExceedCapacity(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Get capacity for page2_play weapons (should be 12)
|
||||
var weaponsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page2_play.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "weapons" {
|
||||
weaponsCapacity = block.MaxItems
|
||||
t.Logf("Found weapons block: %s with capacity %d", block.Name, block.MaxItems)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Total weapons capacity per page: %d", weaponsCapacity)
|
||||
|
||||
// Create more weapons than capacity (enough for exactly 2 pages)
|
||||
// If capacity is 3, create 4 weapons to get 2 pages (3 + 1)
|
||||
numWeapons := weaponsCapacity + 1
|
||||
weapons := make([]WeaponViewModel, numWeapons)
|
||||
for i := 0; i < len(weapons); i++ {
|
||||
weapons[i] = WeaponViewModel{
|
||||
Name: "Weapon " + string(rune('A'+i%26)),
|
||||
Value: 10 + i,
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Created %d weapons (capacity %d)", numWeapons, weaponsCapacity)
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page2_play.html")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Should have 2 pages (base + continuation)
|
||||
if len(pages) != 2 {
|
||||
t.Errorf("Expected 2 pages (main + continuation), got %d", len(pages))
|
||||
}
|
||||
|
||||
// First page should be original template
|
||||
if pages[0].TemplateName != "page2_play.html" {
|
||||
t.Errorf("Expected first page 'page2_play.html', got '%s'", pages[0].TemplateName)
|
||||
}
|
||||
|
||||
// Second page should be continuation
|
||||
if pages[1].TemplateName != "page2.2_play.html" {
|
||||
t.Errorf("Expected continuation 'page2.2_play.html', got '%s'", pages[1].TemplateName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestContinuationPages_MultipleOverflows tests handling of multiple continuation pages
|
||||
func TestContinuationPages_MultipleOverflows(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Get actual capacity from template
|
||||
var skillsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "skills" {
|
||||
skillsCapacity += block.MaxItems
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Skills capacity per page: %d", skillsCapacity)
|
||||
|
||||
// Create 200 skills to force multiple continuations
|
||||
skills := make([]SkillViewModel, 200)
|
||||
for i := 0; i < 200; i++ {
|
||||
skills[i] = SkillViewModel{
|
||||
Name: "Skill " + string(rune('A'+i%26)),
|
||||
Value: 10 + i%20,
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate expected pages
|
||||
expectedPages := (200 + skillsCapacity - 1) / skillsCapacity
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Should have calculated number of pages
|
||||
if len(pages) != expectedPages {
|
||||
t.Errorf("Expected %d pages for 200 skills (capacity %d), got %d", expectedPages, skillsCapacity, len(pages))
|
||||
}
|
||||
|
||||
t.Logf("Created %d pages for 200 skills", len(pages))
|
||||
|
||||
// Verify template names follow pattern
|
||||
expectedTemplates := []string{
|
||||
"page1_stats.html",
|
||||
"page1.2_stats.html",
|
||||
"page1.3_stats.html",
|
||||
"page1.4_stats.html",
|
||||
}
|
||||
|
||||
for i, page := range pages {
|
||||
if i < len(expectedTemplates) && page.TemplateName != expectedTemplates[i] {
|
||||
t.Errorf("Page %d: expected template '%s', got '%s'",
|
||||
i+1, expectedTemplates[i], page.TemplateName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestContinuationPages_NoOverflow tests that no continuation pages are created
|
||||
// when items fit within the original page capacity
|
||||
func TestContinuationPages_NoOverflow(t *testing.T) {
|
||||
// Arrange
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create only 10 skills (well below capacity)
|
||||
skills := make([]SkillViewModel, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
skills[i] = SkillViewModel{
|
||||
Name: "Skill " + string(rune('A'+i)),
|
||||
Value: 10 + i,
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Should have exactly 1 page (no overflow)
|
||||
if len(pages) != 1 {
|
||||
t.Errorf("Expected 1 page for 10 skills, got %d", len(pages))
|
||||
}
|
||||
|
||||
// Should use original template, not continuation
|
||||
if pages[0].TemplateName != "page1_stats.html" {
|
||||
t.Errorf("Expected original template 'page1_stats.html', got '%s'", pages[0].TemplateName)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package pdfrender
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -220,10 +222,25 @@ func TestIntegration_PaginationWithPDF(t *testing.T) {
|
||||
t.Fatalf("Failed to paginate skills: %v", err)
|
||||
}
|
||||
|
||||
// Should create 2 pages (64 + 36 skills)
|
||||
if len(pages) != 2 {
|
||||
t.Fatalf("Expected 2 pages, got %d", len(pages))
|
||||
// Calculate expected pages based on actual capacity
|
||||
var skillsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "skills" {
|
||||
skillsCapacity += block.MaxItems
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
expectedPages := (100 + skillsCapacity - 1) / skillsCapacity
|
||||
|
||||
if len(pages) != expectedPages {
|
||||
t.Fatalf("Expected %d pages for 100 skills (capacity %d), got %d", expectedPages, skillsCapacity, len(pages))
|
||||
}
|
||||
|
||||
t.Logf("Successfully paginated 100 skills into %d pages (capacity %d per page)", len(pages), skillsCapacity)
|
||||
|
||||
// Load templates
|
||||
loader := NewTemplateLoader("../templates/Default_A4_Quer")
|
||||
@@ -281,31 +298,25 @@ func TestIntegration_PaginationWithPDF(t *testing.T) {
|
||||
|
||||
t.Logf("Successfully generated page 1 PDF with %d skills, size: %d bytes", len(pageData.Skills), len(pdfBytes))
|
||||
|
||||
// Verify second page has remaining skills
|
||||
// Get expected capacity from template (reuse templateSet from above)
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
// Verify second page has remaining skills (if there are multiple pages)
|
||||
if len(pages) > 1 {
|
||||
col1Page2 := pages[1].Data["skills_column1"].([]SkillViewModel)
|
||||
col2Page2 := pages[1].Data["skills_column2"].([]SkillViewModel)
|
||||
totalPage2 := len(col1Page2) + len(col2Page2)
|
||||
|
||||
expectedPage2 := skillsCapacity
|
||||
if 100-skillsCapacity < skillsCapacity {
|
||||
expectedPage2 = 100 - skillsCapacity
|
||||
}
|
||||
|
||||
if totalPage2 != expectedPage2 {
|
||||
t.Errorf("Expected %d skills on page 2, got %d", expectedPage2, totalPage2)
|
||||
}
|
||||
|
||||
t.Logf("Page 2 has %d skills distributed across columns", totalPage2)
|
||||
}
|
||||
|
||||
col1Block := GetBlockByName(page1Template.Metadata.Blocks, "skills_column1")
|
||||
col2Block := GetBlockByName(page1Template.Metadata.Blocks, "skills_column2")
|
||||
expectedPage1Capacity := col1Block.MaxItems + col2Block.MaxItems
|
||||
|
||||
col1Page2 := pages[1].Data["skills_column1"].([]SkillViewModel)
|
||||
col2Page2 := pages[1].Data["skills_column2"].([]SkillViewModel)
|
||||
totalPage2 := len(col1Page2) + len(col2Page2)
|
||||
|
||||
expectedPage2 := 100 - expectedPage1Capacity // 100 total - capacity from page 1
|
||||
|
||||
if totalPage2 != expectedPage2 {
|
||||
t.Errorf("Expected %d skills on page 2 (100 total - %d from page 1), got %d", expectedPage2, expectedPage1Capacity, totalPage2)
|
||||
}
|
||||
|
||||
t.Logf("Page 2 would have %d skills distributed across columns", totalPage2)
|
||||
t.Logf("Page 2 would have %d skills distributed across columns", 100-skillsCapacity)
|
||||
}
|
||||
|
||||
// TestIntegration_MultiPageSpellList tests spell pagination across multiple pages
|
||||
@@ -534,99 +545,112 @@ func TestVisualInspection_AllPages(t *testing.T) {
|
||||
|
||||
renderer := NewPDFRenderer()
|
||||
|
||||
// Page 1: Stats page with skills
|
||||
t.Log("Generating Page 1: Stats...")
|
||||
page1Data, err := PreparePaginatedPageData(viewModel, "page1_stats.html", 1, "18.12.2025")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare page1 data: %v", err)
|
||||
}
|
||||
|
||||
html1, err := loader.RenderTemplateWithInlinedResources("page1_stats.html", page1Data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render page1: %v", err)
|
||||
}
|
||||
|
||||
pdf1, err := renderer.RenderHTMLToPDF(html1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate PDF for page1: %v", err)
|
||||
}
|
||||
|
||||
// Page 2: Play/Adventure page with weapons
|
||||
t.Log("Generating Page 2: Play...")
|
||||
page2Data, err := PreparePaginatedPageData(viewModel, "page2_play.html", 2, "18.12.2025")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare page2 data: %v", err)
|
||||
}
|
||||
|
||||
html2, err := loader.RenderTemplateWithInlinedResources("page2_play.html", page2Data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render page2: %v", err)
|
||||
}
|
||||
|
||||
pdf2, err := renderer.RenderHTMLToPDF(html2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate PDF for page2: %v", err)
|
||||
}
|
||||
|
||||
// Page 3: Spells page
|
||||
t.Log("Generating Page 3: Spells...")
|
||||
page3Data, err := PreparePaginatedPageData(viewModel, "page3_spell.html", 3, "18.12.2025")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare page3 data: %v", err)
|
||||
}
|
||||
|
||||
html3, err := loader.RenderTemplateWithInlinedResources("page3_spell.html", page3Data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render page3: %v", err)
|
||||
}
|
||||
|
||||
pdf3, err := renderer.RenderHTMLToPDF(html3)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate PDF for page3: %v", err)
|
||||
}
|
||||
|
||||
// Page 4: Equipment page
|
||||
t.Log("Generating Page 4: Equipment...")
|
||||
page4Data, err := PreparePaginatedPageData(viewModel, "page4_equip.html", 4, "18.12.2025")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to prepare page4 data: %v", err)
|
||||
}
|
||||
|
||||
html4, err := loader.RenderTemplateWithInlinedResources("page4_equip.html", page4Data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render page4: %v", err)
|
||||
}
|
||||
|
||||
pdf4, err := renderer.RenderHTMLToPDF(html4)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate PDF for page4: %v", err)
|
||||
}
|
||||
|
||||
// Save all PDFs to disk
|
||||
// Generate all pages with continuations if needed
|
||||
allPDFs := [][]byte{}
|
||||
var filePaths []string
|
||||
outputDir := "/tmp/bamort_pdf_test"
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create output directory: %v", err)
|
||||
}
|
||||
|
||||
files := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"page1_stats.pdf", pdf1},
|
||||
{"page2_play.pdf", pdf2},
|
||||
{"page3_spell.pdf", pdf3},
|
||||
{"page4_equip.pdf", pdf4},
|
||||
// Page 1: Stats page with skills (may have continuations)
|
||||
t.Log("Generating Page 1: Stats...")
|
||||
page1PDFs, err := RenderPageWithContinuations(viewModel, "page1_stats.html", 1, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page1: %v", err)
|
||||
}
|
||||
t.Logf(" Generated %d PDF(s) for page 1", len(page1PDFs))
|
||||
|
||||
var filePaths []string
|
||||
for _, file := range files {
|
||||
path := outputDir + "/" + file.name
|
||||
if err := os.WriteFile(path, file.data, 0644); err != nil {
|
||||
t.Errorf("Failed to write %s: %v", file.name, err)
|
||||
for i, pdf := range page1PDFs {
|
||||
allPDFs = append(allPDFs, pdf)
|
||||
var filename string
|
||||
if i == 0 {
|
||||
filename = "page1_stats.pdf"
|
||||
} else {
|
||||
filename = fmt.Sprintf("page1_stats_continuation_%d.pdf", i)
|
||||
}
|
||||
path := filepath.Join(outputDir, filename)
|
||||
if err := os.WriteFile(path, pdf, 0644); err != nil {
|
||||
t.Errorf("Failed to write %s: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
filePaths = append(filePaths, path)
|
||||
t.Logf("✓ Saved %s (%d bytes)", path, len(file.data))
|
||||
t.Logf(" ✓ Saved %s (%d bytes)", filename, len(pdf))
|
||||
}
|
||||
|
||||
// Page 2: Play/Adventure page with weapons (may have continuations)
|
||||
t.Log("Generating Page 2: Play...")
|
||||
page2PDFs, err := RenderPageWithContinuations(viewModel, "page2_play.html", 2, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page2: %v", err)
|
||||
}
|
||||
t.Logf(" Generated %d PDF(s) for page 2", len(page2PDFs))
|
||||
|
||||
for i, pdf := range page2PDFs {
|
||||
allPDFs = append(allPDFs, pdf)
|
||||
var filename string
|
||||
if i == 0 {
|
||||
filename = "page2_play.pdf"
|
||||
} else {
|
||||
filename = fmt.Sprintf("page2_play_continuation_%d.pdf", i)
|
||||
}
|
||||
path := filepath.Join(outputDir, filename)
|
||||
if err := os.WriteFile(path, pdf, 0644); err != nil {
|
||||
t.Errorf("Failed to write %s: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
filePaths = append(filePaths, path)
|
||||
t.Logf(" ✓ Saved %s (%d bytes)", filename, len(pdf))
|
||||
}
|
||||
|
||||
// Page 3: Spells page (may have continuations)
|
||||
t.Log("Generating Page 3: Spells...")
|
||||
page3PDFs, err := RenderPageWithContinuations(viewModel, "page3_spell.html", 3, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page3: %v", err)
|
||||
}
|
||||
t.Logf(" Generated %d PDF(s) for page 3", len(page3PDFs))
|
||||
|
||||
for i, pdf := range page3PDFs {
|
||||
allPDFs = append(allPDFs, pdf)
|
||||
var filename string
|
||||
if i == 0 {
|
||||
filename = "page3_spell.pdf"
|
||||
} else {
|
||||
filename = fmt.Sprintf("page3_spell_continuation_%d.pdf", i)
|
||||
}
|
||||
path := filepath.Join(outputDir, filename)
|
||||
if err := os.WriteFile(path, pdf, 0644); err != nil {
|
||||
t.Errorf("Failed to write %s: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
filePaths = append(filePaths, path)
|
||||
t.Logf(" ✓ Saved %s (%d bytes)", filename, len(pdf))
|
||||
}
|
||||
|
||||
// Page 4: Equipment page (may have continuations)
|
||||
t.Log("Generating Page 4: Equipment...")
|
||||
page4PDFs, err := RenderPageWithContinuations(viewModel, "page4_equip.html", 4, "18.12.2025", loader, renderer)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate page4: %v", err)
|
||||
}
|
||||
t.Logf(" Generated %d PDF(s) for page 4", len(page4PDFs))
|
||||
|
||||
for i, pdf := range page4PDFs {
|
||||
allPDFs = append(allPDFs, pdf)
|
||||
var filename string
|
||||
if i == 0 {
|
||||
filename = "page4_equip.pdf"
|
||||
} else {
|
||||
filename = fmt.Sprintf("page4_equip_continuation_%d.pdf", i)
|
||||
}
|
||||
path := filepath.Join(outputDir, filename)
|
||||
if err := os.WriteFile(path, pdf, 0644); err != nil {
|
||||
t.Errorf("Failed to write %s: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
filePaths = append(filePaths, path)
|
||||
t.Logf(" ✓ Saved %s (%d bytes)", filename, len(pdf))
|
||||
}
|
||||
|
||||
// Merge all PDFs into a single file
|
||||
@@ -644,13 +668,13 @@ func TestVisualInspection_AllPages(t *testing.T) {
|
||||
t.Logf("\n✓ Combined all pages into: %s (%d bytes)", combinedPath, combinedInfo.Size())
|
||||
|
||||
// Summary
|
||||
t.Logf("\n✓ All 4 pages generated successfully!")
|
||||
t.Logf("\n✓ All pages generated successfully!")
|
||||
t.Logf(" Character: %s (Grade %d)", viewModel.Character.Name, viewModel.Character.Grade)
|
||||
t.Logf(" Skills: %d", len(viewModel.Skills))
|
||||
t.Logf(" Weapons: %d", len(viewModel.Weapons))
|
||||
t.Logf(" Spells: %d", len(viewModel.Spells))
|
||||
t.Logf(" Equipment: %d items", len(viewModel.Equipment))
|
||||
t.Logf("\n Output directory: %s", outputDir)
|
||||
t.Logf(" Individual PDFs: %d bytes", len(pdf1)+len(pdf2)+len(pdf3)+len(pdf4))
|
||||
t.Logf(" Total PDFs generated: %d (including continuations)", len(allPDFs))
|
||||
t.Logf(" Combined PDF: %d bytes", combinedInfo.Size())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,70 @@
|
||||
package pdfrender
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateContinuationTemplateName creates a continuation template name
|
||||
// Example: "page1_stats.html" + pageNum 2 -> "page1.2_stats.html"
|
||||
func GenerateContinuationTemplateName(originalTemplate string, pageNum int) string {
|
||||
if pageNum == 1 {
|
||||
return originalTemplate
|
||||
}
|
||||
|
||||
// Split template name at first underscore to insert page continuation number
|
||||
// Example: "page1_stats.html" -> "page1" + "_stats.html"
|
||||
parts := strings.SplitN(originalTemplate, "_", 2)
|
||||
if len(parts) != 2 {
|
||||
// Fallback: just append .2, .3, etc before extension
|
||||
ext := ".html"
|
||||
base := strings.TrimSuffix(originalTemplate, ext)
|
||||
return fmt.Sprintf("%s.%d%s", base, pageNum, ext)
|
||||
}
|
||||
|
||||
// Extract page number and base name
|
||||
// "page1" -> "page" + "1"
|
||||
baseName := parts[0]
|
||||
suffix := parts[1]
|
||||
|
||||
// Insert continuation number: "page1.2_stats.html"
|
||||
return fmt.Sprintf("%s.%d_%s", baseName, pageNum, suffix)
|
||||
}
|
||||
|
||||
// ExtractBaseTemplateName extracts the base template name from a continuation template
|
||||
// Example: "page1.2_stats.html" -> "page1_stats.html"
|
||||
func ExtractBaseTemplateName(templateName string) string {
|
||||
// Check if it's a continuation template (contains .N_ pattern)
|
||||
parts := strings.SplitN(templateName, "_", 2)
|
||||
if len(parts) != 2 {
|
||||
return templateName
|
||||
}
|
||||
|
||||
baseName := parts[0]
|
||||
suffix := parts[1]
|
||||
|
||||
// Check if baseName contains a dot followed by a number (e.g., "page1.2")
|
||||
dotIdx := strings.LastIndex(baseName, ".")
|
||||
if dotIdx == -1 {
|
||||
return templateName // Not a continuation template
|
||||
}
|
||||
|
||||
// Verify the part after the dot is a number
|
||||
numPart := baseName[dotIdx+1:]
|
||||
if len(numPart) == 0 {
|
||||
return templateName
|
||||
}
|
||||
|
||||
for _, c := range numPart {
|
||||
if c < '0' || c > '9' {
|
||||
return templateName // Not a number, not a continuation template
|
||||
}
|
||||
}
|
||||
|
||||
// It's a continuation template, return the base name
|
||||
basePrefix := baseName[:dotIdx]
|
||||
return fmt.Sprintf("%s_%s", basePrefix, suffix)
|
||||
}
|
||||
|
||||
// SliceList slices a list based on start index and max items
|
||||
// Returns the sliced list and whether there are more items
|
||||
@@ -158,8 +222,11 @@ func (p *Paginator) paginateList(items interface{}, blocks []BlockMetadata, temp
|
||||
currentIndex += itemsToTake
|
||||
}
|
||||
|
||||
// Determine template name - use continuation naming for pages 2+
|
||||
pageTemplateName := GenerateContinuationTemplateName(templateName, pageNum)
|
||||
|
||||
distributions = append(distributions, PageDistribution{
|
||||
TemplateName: templateName,
|
||||
TemplateName: pageTemplateName,
|
||||
PageNumber: pageNum,
|
||||
Data: pageData,
|
||||
})
|
||||
@@ -261,3 +328,90 @@ func (p *Paginator) CalculatePagesNeeded(templateName string, listType string, i
|
||||
|
||||
return (itemCount + capacityPerPage - 1) / capacityPerPage, nil
|
||||
}
|
||||
|
||||
// PaginatePage2PlayLists handles pagination for page2_play.html which has both skills and weapons
|
||||
// Skills and weapons overflow together - if either overflows, create continuation pages with remaining items from both
|
||||
func (p *Paginator) PaginatePage2PlayLists(skills []SkillViewModel, weapons []WeaponViewModel, templateName string) ([]PageDistribution, error) {
|
||||
template := p.findTemplate(templateName)
|
||||
if template == nil {
|
||||
return nil, fmt.Errorf("template not found: %s", templateName)
|
||||
}
|
||||
|
||||
// Get capacities for each block type
|
||||
learnedCap := GetBlockCapacity(&p.templateSet, templateName, "skills_learned")
|
||||
unlearnedCap := GetBlockCapacity(&p.templateSet, templateName, "skills_unlearned")
|
||||
languageCap := GetBlockCapacity(&p.templateSet, templateName, "skills_languages")
|
||||
weaponsCap := GetBlockCapacity(&p.templateSet, templateName, "weapons_main")
|
||||
|
||||
// Filter skills into categories
|
||||
var learnedSkills, unlearnedSkills, languageSkills []SkillViewModel
|
||||
for _, skill := range skills {
|
||||
if skill.Category == "Sprache" {
|
||||
languageSkills = append(languageSkills, skill)
|
||||
} else if skill.IsLearned {
|
||||
learnedSkills = append(learnedSkills, skill)
|
||||
} else {
|
||||
unlearnedSkills = append(unlearnedSkills, skill)
|
||||
}
|
||||
}
|
||||
|
||||
// Track current position in each list
|
||||
learnedIdx := 0
|
||||
unlearnedIdx := 0
|
||||
languageIdx := 0
|
||||
weaponsIdx := 0
|
||||
|
||||
distributions := []PageDistribution{}
|
||||
pageNum := 1
|
||||
|
||||
// Continue creating pages while there are remaining items in any list
|
||||
for learnedIdx < len(learnedSkills) || unlearnedIdx < len(unlearnedSkills) ||
|
||||
languageIdx < len(languageSkills) || weaponsIdx < len(weapons) {
|
||||
|
||||
pageData := make(map[string]interface{})
|
||||
|
||||
// Add learned skills for this page
|
||||
learnedEnd := learnedIdx + learnedCap
|
||||
if learnedEnd > len(learnedSkills) {
|
||||
learnedEnd = len(learnedSkills)
|
||||
}
|
||||
pageData["skills_learned"] = learnedSkills[learnedIdx:learnedEnd]
|
||||
learnedIdx = learnedEnd
|
||||
|
||||
// Add unlearned skills for this page
|
||||
unlearnedEnd := unlearnedIdx + unlearnedCap
|
||||
if unlearnedEnd > len(unlearnedSkills) {
|
||||
unlearnedEnd = len(unlearnedSkills)
|
||||
}
|
||||
pageData["skills_unlearned"] = unlearnedSkills[unlearnedIdx:unlearnedEnd]
|
||||
unlearnedIdx = unlearnedEnd
|
||||
|
||||
// Add language skills for this page
|
||||
languageEnd := languageIdx + languageCap
|
||||
if languageEnd > len(languageSkills) {
|
||||
languageEnd = len(languageSkills)
|
||||
}
|
||||
pageData["skills_languages"] = languageSkills[languageIdx:languageEnd]
|
||||
languageIdx = languageEnd
|
||||
|
||||
// Add weapons for this page
|
||||
weaponsEnd := weaponsIdx + weaponsCap
|
||||
if weaponsEnd > len(weapons) {
|
||||
weaponsEnd = len(weapons)
|
||||
}
|
||||
pageData["weapons_main"] = weapons[weaponsIdx:weaponsEnd]
|
||||
weaponsIdx = weaponsEnd
|
||||
|
||||
// Create page distribution
|
||||
pageTemplateName := GenerateContinuationTemplateName(templateName, pageNum)
|
||||
distributions = append(distributions, PageDistribution{
|
||||
TemplateName: pageTemplateName,
|
||||
PageNumber: pageNum,
|
||||
Data: pageData,
|
||||
})
|
||||
|
||||
pageNum++
|
||||
}
|
||||
|
||||
return distributions, nil
|
||||
}
|
||||
|
||||
@@ -149,19 +149,10 @@ func PreparePaginatedPageData(viewModel *CharacterSheetViewModel, templateName s
|
||||
pageData.MagicItems = magicItems
|
||||
}
|
||||
} else if templateName == "page4_equip.html" {
|
||||
// Get capacity from template
|
||||
equipmentCapacity := GetBlockCapacity(&templateSet, templateName, "equipment_worn")
|
||||
|
||||
// Limit and fill equipment to capacity
|
||||
equipment := viewModel.Equipment
|
||||
if equipmentCapacity > 0 && len(equipment) > equipmentCapacity {
|
||||
equipment = equipment[:equipmentCapacity]
|
||||
}
|
||||
if equipmentCapacity > 0 {
|
||||
pageData.Equipment = FillToCapacity(equipment, equipmentCapacity)
|
||||
} else {
|
||||
pageData.Equipment = equipment
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -99,10 +99,10 @@ func TestSplitSkillsForColumns(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "few skills - only column 1",
|
||||
skills: 10,
|
||||
skills: 3, // Less than col1Max
|
||||
col1Max: col1MaxItems,
|
||||
col2Max: col2MaxItems,
|
||||
wantCol1: 10,
|
||||
wantCol1: 3,
|
||||
wantCol2: 0,
|
||||
},
|
||||
{
|
||||
@@ -115,11 +115,11 @@ func TestSplitSkillsForColumns(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "overflow to column 2",
|
||||
skills: col1MaxItems + 11,
|
||||
skills: col1MaxItems + 3, // More than col1, but less than col1+col2
|
||||
col1Max: col1MaxItems,
|
||||
col2Max: col2MaxItems,
|
||||
wantCol1: col1MaxItems,
|
||||
wantCol2: 11,
|
||||
wantCol2: 3,
|
||||
},
|
||||
{
|
||||
name: "both columns full",
|
||||
@@ -234,11 +234,10 @@ func TestPreparePaginatedPageData_Page3Spell(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPreparePaginatedPageData_Page4Equipment(t *testing.T) {
|
||||
// Get capacity from template
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
equipmentCap := GetBlockCapacity(&templateSet, "page4_equip.html", "equipment_worn")
|
||||
// Page 4 equipment page needs ALL equipment (not limited by capacity)
|
||||
// because the template has complex container logic that requires the full equipment list
|
||||
|
||||
// Create test data exceeding capacity
|
||||
// Create test data
|
||||
viewModel := &CharacterSheetViewModel{
|
||||
Equipment: make([]EquipmentViewModel, 50),
|
||||
}
|
||||
@@ -253,10 +252,10 @@ func TestPreparePaginatedPageData_Page4Equipment(t *testing.T) {
|
||||
t.Fatalf("PreparePaginatedPageData failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify capacity matches template
|
||||
if len(pageData.Equipment) != equipmentCap {
|
||||
t.Errorf("Equipment should be filled to %d (from template), got %d", equipmentCap, len(pageData.Equipment))
|
||||
// Verify all equipment is included (not truncated)
|
||||
if len(pageData.Equipment) != 50 {
|
||||
t.Errorf("Equipment should include all items (50), got %d", len(pageData.Equipment))
|
||||
}
|
||||
|
||||
t.Logf("Page4: %d equipment items (from template)", len(pageData.Equipment))
|
||||
t.Logf("Page4: %d equipment items (all items included for container rendering)", len(pageData.Equipment))
|
||||
}
|
||||
|
||||
@@ -61,12 +61,28 @@ func TestPaginateSkills_SinglePage(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
skills := make([]SkillViewModel, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
// Get column capacities from template
|
||||
var col1Capacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.Name == "skills_column1" {
|
||||
col1Capacity = block.MaxItems
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create skills that fit within first column only
|
||||
numSkills := col1Capacity
|
||||
skills := make([]SkillViewModel, numSkills)
|
||||
for i := 0; i < numSkills; i++ {
|
||||
skills[i] = SkillViewModel{Name: "Skill" + string(rune('A'+i))}
|
||||
}
|
||||
|
||||
// Act - page1_stats has 2 columns with 32 each = 64 total capacity
|
||||
// Act
|
||||
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
|
||||
|
||||
// Assert
|
||||
@@ -84,13 +100,13 @@ func TestPaginateSkills_SinglePage(t *testing.T) {
|
||||
t.Errorf("Expected page number 1, got %d", page.PageNumber)
|
||||
}
|
||||
|
||||
// Column 1 should have all 10 skills
|
||||
// Column 1 should have all skills (exactly matching capacity)
|
||||
col1Data, ok := page.Data["skills_column1"].([]SkillViewModel)
|
||||
if !ok {
|
||||
t.Fatal("skills_column1 data not found or wrong type")
|
||||
}
|
||||
if len(col1Data) != 10 {
|
||||
t.Errorf("Expected 10 skills in column 1, got %d", len(col1Data))
|
||||
if len(col1Data) != numSkills {
|
||||
t.Errorf("Expected %d skills in column 1 (capacity), got %d", numSkills, len(col1Data))
|
||||
}
|
||||
|
||||
// Column 2 should be empty (no overflow)
|
||||
@@ -117,10 +133,18 @@ func TestPaginateSkills_MultiColumn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
col1Block := GetBlockByName(page1Template.Metadata.Blocks, "skills_column1")
|
||||
col2Block := GetBlockByName(page1Template.Metadata.Blocks, "skills_column2")
|
||||
totalCapacity := col1Block.MaxItems + col2Block.MaxItems
|
||||
|
||||
// Create 40 skills - should fill first column and spill to second
|
||||
skills := make([]SkillViewModel, 40)
|
||||
for i := 0; i < 40; i++ {
|
||||
// Create enough skills to use both columns but fit on one page
|
||||
// Use totalCapacity - 1 to test partial fill of second column
|
||||
numSkills := col1Block.MaxItems + 2
|
||||
if numSkills > totalCapacity {
|
||||
numSkills = totalCapacity
|
||||
}
|
||||
|
||||
skills := make([]SkillViewModel, numSkills)
|
||||
for i := 0; i < numSkills; i++ {
|
||||
skills[i] = SkillViewModel{Name: "Skill" + string(rune(i))}
|
||||
}
|
||||
|
||||
@@ -132,8 +156,10 @@ func TestPaginateSkills_MultiColumn(t *testing.T) {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(pages) != 1 {
|
||||
t.Fatalf("Expected 1 page, got %d", len(pages))
|
||||
// Should fit on one page
|
||||
expectedPages := (numSkills + totalCapacity - 1) / totalCapacity
|
||||
if len(pages) != expectedPages {
|
||||
t.Fatalf("Expected %d page, got %d", expectedPages, len(pages))
|
||||
}
|
||||
|
||||
page := pages[0]
|
||||
@@ -146,9 +172,9 @@ func TestPaginateSkills_MultiColumn(t *testing.T) {
|
||||
|
||||
// Column 2 should have remaining skills
|
||||
col2Data := page.Data["skills_column2"].([]SkillViewModel)
|
||||
expectedCol2 := 40 - col1Block.MaxItems
|
||||
expectedCol2 := numSkills - col1Block.MaxItems
|
||||
if len(col2Data) != expectedCol2 {
|
||||
t.Errorf("Expected %d skills in column 2 (40 total - %d in col1), got %d", expectedCol2, col1Block.MaxItems, len(col2Data))
|
||||
t.Errorf("Expected %d skills in column 2 (%d total - %d in col1), got %d", expectedCol2, numSkills, col1Block.MaxItems, len(col2Data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +183,22 @@ func TestPaginateSkills_MultiPage(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Create 100 skills - should span 2 pages (64 capacity per page)
|
||||
skills := make([]SkillViewModel, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
// Get column capacities from template
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
col1Block := GetBlockByName(page1Template.Metadata.Blocks, "skills_column1")
|
||||
col2Block := GetBlockByName(page1Template.Metadata.Blocks, "skills_column2")
|
||||
totalCapacity := col1Block.MaxItems + col2Block.MaxItems
|
||||
|
||||
// Create enough skills to span exactly 2 pages
|
||||
numSkills := totalCapacity + 1
|
||||
skills := make([]SkillViewModel, numSkills)
|
||||
for i := 0; i < numSkills; i++ {
|
||||
skills[i] = SkillViewModel{Name: "Skill" + string(rune(i))}
|
||||
}
|
||||
|
||||
@@ -171,50 +210,32 @@ func TestPaginateSkills_MultiPage(t *testing.T) {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(pages) != 2 {
|
||||
t.Fatalf("Expected 2 pages, got %d", len(pages))
|
||||
}
|
||||
|
||||
// Get column capacities from template (reuse templateSet from above)
|
||||
var page1Template *TemplateWithMeta
|
||||
for i := range templateSet.Templates {
|
||||
if templateSet.Templates[i].Metadata.Name == "page1_stats.html" {
|
||||
page1Template = &templateSet.Templates[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
var col1Capacity, col2Capacity int
|
||||
for i := range page1Template.Metadata.Blocks {
|
||||
if page1Template.Metadata.Blocks[i].Name == "skills_column1" {
|
||||
col1Capacity = page1Template.Metadata.Blocks[i].MaxItems
|
||||
} else if page1Template.Metadata.Blocks[i].Name == "skills_column2" {
|
||||
col2Capacity = page1Template.Metadata.Blocks[i].MaxItems
|
||||
}
|
||||
expectedPages := 2
|
||||
if len(pages) != expectedPages {
|
||||
t.Fatalf("Expected %d pages, got %d", expectedPages, len(pages))
|
||||
}
|
||||
|
||||
// Page 1 should have full capacity (col1 + col2)
|
||||
page1 := pages[0]
|
||||
col1Page1 := page1.Data["skills_column1"].([]SkillViewModel)
|
||||
col2Page1 := page1.Data["skills_column2"].([]SkillViewModel)
|
||||
if len(col1Page1) != col1Capacity {
|
||||
t.Errorf("Page 1 col1: expected %d skills (template capacity), got %d", col1Capacity, len(col1Page1))
|
||||
if len(col1Page1) != col1Block.MaxItems {
|
||||
t.Errorf("Page 1 col1: expected %d skills (template capacity), got %d", col1Block.MaxItems, len(col1Page1))
|
||||
}
|
||||
if len(col2Page1) != col2Capacity {
|
||||
t.Errorf("Page 1 col2: expected %d skills (template capacity), got %d", col2Capacity, len(col2Page1))
|
||||
if len(col2Page1) != col2Block.MaxItems {
|
||||
t.Errorf("Page 1 col2: expected %d skills (template capacity), got %d", col2Block.MaxItems, len(col2Page1))
|
||||
}
|
||||
|
||||
// Page 2 should have remaining skills
|
||||
// Page 2 should have remaining skills (just 1 skill)
|
||||
page2 := pages[1]
|
||||
col1Page2 := page2.Data["skills_column1"].([]SkillViewModel)
|
||||
col2Page2 := page2.Data["skills_column2"].([]SkillViewModel)
|
||||
page1Total := col1Capacity + col2Capacity
|
||||
remainingSkills := 100 - page1Total
|
||||
if len(col1Page2) != col1Capacity {
|
||||
t.Errorf("Page 2 col1: expected %d skills (template capacity), got %d", col1Capacity, len(col1Page2))
|
||||
remainingSkills := numSkills - totalCapacity
|
||||
if len(col1Page2) != remainingSkills {
|
||||
t.Errorf("Page 2 col1: expected %d skills (remaining), got %d", remainingSkills, len(col1Page2))
|
||||
}
|
||||
expectedCol2 := remainingSkills - col1Capacity
|
||||
if len(col2Page2) != expectedCol2 {
|
||||
t.Errorf("Page 2 col2: expected %d skills (remaining), got %d", expectedCol2, len(col2Page2))
|
||||
if len(col2Page2) != 0 {
|
||||
t.Errorf("Page 2 col2: expected 0 skills, got %d", len(col2Page2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,12 +363,31 @@ func TestPaginateWeapons_SinglePage(t *testing.T) {
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
weapons := make([]WeaponViewModel, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
// Get weapon capacity from template
|
||||
var weaponCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page2_play.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "weapons" {
|
||||
weaponCapacity = block.MaxItems
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create fewer weapons than capacity
|
||||
numWeapons := weaponCapacity - 2
|
||||
if numWeapons < 1 {
|
||||
numWeapons = 1
|
||||
}
|
||||
weapons := make([]WeaponViewModel, numWeapons)
|
||||
for i := 0; i < numWeapons; i++ {
|
||||
weapons[i] = WeaponViewModel{Name: "Weapon" + string(rune('A'+i))}
|
||||
}
|
||||
|
||||
// Act - page2_play has weapons_main with MAX:30
|
||||
// Act
|
||||
pages, err := paginator.PaginateWeapons(weapons, "page2_play.html")
|
||||
|
||||
// Assert
|
||||
@@ -355,14 +395,15 @@ func TestPaginateWeapons_SinglePage(t *testing.T) {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if len(pages) != 1 {
|
||||
t.Fatalf("Expected 1 page, got %d", len(pages))
|
||||
expectedPages := 1
|
||||
if len(pages) != expectedPages {
|
||||
t.Fatalf("Expected %d page (capacity %d, items %d), got %d", expectedPages, weaponCapacity, numWeapons, len(pages))
|
||||
}
|
||||
|
||||
page := pages[0]
|
||||
weaponsData := page.Data["weapons_main"].([]WeaponViewModel)
|
||||
if len(weaponsData) != 10 {
|
||||
t.Errorf("Expected 10 weapons, got %d", len(weaponsData))
|
||||
if len(weaponsData) != numWeapons {
|
||||
t.Errorf("Expected %d weapons, got %d", numWeapons, len(weaponsData))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,11 +529,11 @@ func TestCalculatePagesNeeded(t *testing.T) {
|
||||
{"10 skills on page1", "page1_stats.html", "skills", 10, 1},
|
||||
{fmt.Sprintf("%d skills on page1", skillCapacity), "page1_stats.html", "skills", skillCapacity, 1},
|
||||
{fmt.Sprintf("%d skills on page1", skillCapacity+1), "page1_stats.html", "skills", skillCapacity + 1, 2},
|
||||
{"100 skills on page1", "page1_stats.html", "skills", 100, 2},
|
||||
{"10 weapons on page2", "page2_play.html", "weapons", 10, 1},
|
||||
{"100 skills on page1", "page1_stats.html", "skills", 100, (100 + skillCapacity - 1) / skillCapacity}, // Dynamic calculation
|
||||
{"10 weapons on page2", "page2_play.html", "weapons", 10, (10 + weaponCapacity - 1) / weaponCapacity}, // Dynamic calculation
|
||||
{fmt.Sprintf("%d weapons on page2", weaponCapacity), "page2_play.html", "weapons", weaponCapacity, 1},
|
||||
{fmt.Sprintf("%d weapons on page2", weaponCapacity+1), "page2_play.html", "weapons", weaponCapacity + 1, 2},
|
||||
{"10 spells on page3", "page3_spell.html", "spells", 10, 1},
|
||||
{"10 spells on page3", "page3_spell.html", "spells", 10, (10 + spellCapacity - 1) / spellCapacity}, // Dynamic calculation
|
||||
{fmt.Sprintf("%d spells on page3", spellCapacity), "page3_spell.html", "spells", spellCapacity, 1},
|
||||
{fmt.Sprintf("%d spells on page3", spellCapacity+1), "page3_spell.html", "spells", spellCapacity + 1, 2},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package pdfrender
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGenerateContinuationTemplateName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
template string
|
||||
pageNum int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "First page returns original",
|
||||
template: "page1_stats.html",
|
||||
pageNum: 1,
|
||||
expected: "page1_stats.html",
|
||||
},
|
||||
{
|
||||
name: "Second page gets continuation name",
|
||||
template: "page1_stats.html",
|
||||
pageNum: 2,
|
||||
expected: "page1.2_stats.html",
|
||||
},
|
||||
{
|
||||
name: "Third page gets continuation name",
|
||||
template: "page2_play.html",
|
||||
pageNum: 3,
|
||||
expected: "page2.3_play.html",
|
||||
},
|
||||
{
|
||||
name: "Multiple digits",
|
||||
template: "page1_stats.html",
|
||||
pageNum: 10,
|
||||
expected: "page1.10_stats.html",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := GenerateContinuationTemplateName(tt.template, tt.pageNum)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractBaseTemplateName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
template string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Base template returns itself",
|
||||
template: "page1_stats.html",
|
||||
expected: "page1_stats.html",
|
||||
},
|
||||
{
|
||||
name: "Continuation template returns base",
|
||||
template: "page1.2_stats.html",
|
||||
expected: "page1_stats.html",
|
||||
},
|
||||
{
|
||||
name: "Multiple digit continuation",
|
||||
template: "page1.10_stats.html",
|
||||
expected: "page1_stats.html",
|
||||
},
|
||||
{
|
||||
name: "Different page type",
|
||||
template: "page2.3_play.html",
|
||||
expected: "page2_play.html",
|
||||
},
|
||||
{
|
||||
name: "Template without underscore",
|
||||
template: "simple.html",
|
||||
expected: "simple.html",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ExtractBaseTemplateName(tt.template)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected '%s', got '%s'", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuationRoundTrip(t *testing.T) {
|
||||
// Test that generating a continuation name and extracting the base works correctly
|
||||
original := "page1_stats.html"
|
||||
|
||||
for pageNum := 1; pageNum <= 5; pageNum++ {
|
||||
continuation := GenerateContinuationTemplateName(original, pageNum)
|
||||
extracted := ExtractBaseTemplateName(continuation)
|
||||
|
||||
if extracted != original {
|
||||
t.Errorf("Round trip failed for page %d: original '%s', continuation '%s', extracted '%s'",
|
||||
pageNum, original, continuation, extracted)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package pdfrender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
)
|
||||
|
||||
// RenderPageWithContinuations renders a template page and all necessary continuation pages
|
||||
// Returns a slice of PDF bytes, one for each page (main + continuations)
|
||||
func RenderPageWithContinuations(
|
||||
viewModel *CharacterSheetViewModel,
|
||||
templateName string,
|
||||
startPageNumber int,
|
||||
date string,
|
||||
loader *TemplateLoader,
|
||||
renderer *PDFRenderer,
|
||||
) ([][]byte, error) {
|
||||
var pdfs [][]byte
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
paginator := NewPaginator(templateSet)
|
||||
|
||||
// Determine which list type this template handles
|
||||
var distributions []PageDistribution
|
||||
var err error
|
||||
|
||||
switch templateName {
|
||||
case "page1_stats.html":
|
||||
// Paginate skills
|
||||
distributions, err = paginator.PaginateSkills(viewModel.Skills, templateName, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to paginate skills: %w", err)
|
||||
}
|
||||
|
||||
case "page2_play.html":
|
||||
// Page 2 has both skills and weapons that overflow together
|
||||
// Use multi-list pagination so remaining items from both lists go to continuation pages
|
||||
distributions, err = paginator.PaginatePage2PlayLists(viewModel.Skills, viewModel.Weapons, templateName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to paginate page2 lists: %w", err)
|
||||
}
|
||||
|
||||
case "page3_spell.html":
|
||||
// Paginate spells
|
||||
distributions, err = paginator.PaginateSpells(viewModel.Spells, templateName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to paginate spells: %w", err)
|
||||
}
|
||||
|
||||
case "page4_equip.html":
|
||||
// Page 4 has a complex container-based layout where items are grouped by containers.
|
||||
// The template expects the full equipment list to properly render containers and their contents.
|
||||
// Pagination doesn't make sense here - render as single page with all equipment.
|
||||
pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html, err := loader.RenderTemplateWithInlinedResources(templateName, pageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pdf, err := renderer.RenderHTMLToPDF(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte{pdf}, nil
|
||||
|
||||
default:
|
||||
// For unknown templates, render single page without pagination
|
||||
pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html, err := loader.RenderTemplateWithInlinedResources(templateName, pageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pdf, err := renderer.RenderHTMLToPDF(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte{pdf}, nil
|
||||
}
|
||||
|
||||
// If only one page, use the simplified approach
|
||||
if len(distributions) == 1 {
|
||||
pageData, err := PreparePaginatedPageData(viewModel, templateName, startPageNumber, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html, err := loader.RenderTemplateWithInlinedResources(templateName, pageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pdf, err := renderer.RenderHTMLToPDF(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte{pdf}, nil
|
||||
}
|
||||
|
||||
// Multiple pages needed - render each one
|
||||
for i, dist := range distributions {
|
||||
pageData := &PageData{
|
||||
Character: viewModel.Character,
|
||||
Attributes: viewModel.Attributes,
|
||||
DerivedValues: viewModel.DerivedValues,
|
||||
GameResults: viewModel.GameResults,
|
||||
Meta: PageMeta{
|
||||
Date: date,
|
||||
PageNumber: startPageNumber + i,
|
||||
},
|
||||
}
|
||||
|
||||
// Populate the page data based on the distribution
|
||||
switch templateName {
|
||||
case "page1_stats.html":
|
||||
// Extract skills from distribution
|
||||
if col1, ok := dist.Data["skills_column1"].([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn1 = col1
|
||||
}
|
||||
if col2, ok := dist.Data["skills_column2"].([]SkillViewModel); ok {
|
||||
pageData.SkillsColumn2 = col2
|
||||
}
|
||||
// Combine for backward compatibility
|
||||
pageData.Skills = append(pageData.SkillsColumn1, pageData.SkillsColumn2...)
|
||||
|
||||
case "page2_play.html":
|
||||
// Extract all lists from distribution (skills and weapons)
|
||||
if weapons, ok := dist.Data["weapons_main"].([]WeaponViewModel); ok {
|
||||
pageData.Weapons = weapons
|
||||
}
|
||||
if learned, ok := dist.Data["skills_learned"].([]SkillViewModel); ok {
|
||||
pageData.SkillsLearned = learned
|
||||
}
|
||||
if unlearned, ok := dist.Data["skills_unlearned"].([]SkillViewModel); ok {
|
||||
// Unlearned skills are typically shown via general skills list
|
||||
// Add to Skills for template compatibility
|
||||
pageData.Skills = append(pageData.Skills, unlearned...)
|
||||
}
|
||||
if languages, ok := dist.Data["skills_languages"].([]SkillViewModel); ok {
|
||||
pageData.SkillsLanguage = languages
|
||||
}
|
||||
|
||||
case "page3_spell.html":
|
||||
// Extract spells from distribution
|
||||
if left, ok := dist.Data["spells_left"].([]SpellViewModel); ok {
|
||||
pageData.SpellsLeft = left
|
||||
}
|
||||
if right, ok := dist.Data["spells_right"].([]SpellViewModel); ok {
|
||||
pageData.SpellsRight = right
|
||||
}
|
||||
// Combine for backward compatibility
|
||||
pageData.Spells = append(pageData.SpellsLeft, pageData.SpellsRight...)
|
||||
|
||||
case "page4_equip.html":
|
||||
// Extract equipment from distribution
|
||||
if equipment, ok := dist.Data["equipment_worn"].([]EquipmentViewModel); ok {
|
||||
pageData.Equipment = append(pageData.Equipment, equipment...)
|
||||
}
|
||||
if equipment, ok := dist.Data["equipment_carried"].([]EquipmentViewModel); ok {
|
||||
pageData.Equipment = append(pageData.Equipment, equipment...)
|
||||
}
|
||||
}
|
||||
|
||||
// Render the page (use continuation template name if needed)
|
||||
html, err := loader.RenderTemplateWithInlinedResources(dist.TemplateName, pageData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render %s: %w", dist.TemplateName, err)
|
||||
}
|
||||
|
||||
pdf, err := renderer.RenderHTMLToPDF(html)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate PDF for %s: %w", dist.TemplateName, err)
|
||||
}
|
||||
|
||||
pdfs = append(pdfs, pdf)
|
||||
}
|
||||
|
||||
return pdfs, nil
|
||||
}
|
||||
|
||||
// MergePDFs merges multiple PDF byte slices into a single PDF
|
||||
func MergePDFs(pdfList [][]byte, outputPath string) error {
|
||||
if len(pdfList) == 0 {
|
||||
return fmt.Errorf("no PDFs to merge")
|
||||
}
|
||||
|
||||
if len(pdfList) == 1 {
|
||||
// Single PDF, just write it
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use pdfcpu to merge - this is a placeholder, actual implementation
|
||||
// would need to save individual PDFs first, then merge them
|
||||
// For now, this function signature is defined for future use
|
||||
return api.MergeCreateFile(nil, outputPath, false, nil)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package pdfrender
|
||||
|
||||
import (
|
||||
"bamort/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRenderWithContinuations_SkillsOverflow tests that continuation pages are rendered
|
||||
// when skills exceed template capacity
|
||||
func TestRenderWithContinuations_SkillsOverflow(t *testing.T) {
|
||||
// Arrange - Create character with many skills to force continuation
|
||||
char := &models.Char{
|
||||
BamortBase: models.BamortBase{
|
||||
Name: "Test Character With Many Skills",
|
||||
},
|
||||
Typ: "Krieger",
|
||||
Grad: 5,
|
||||
Alter: 30,
|
||||
Eigenschaften: []models.Eigenschaft{
|
||||
{Name: "St", Value: 90},
|
||||
{Name: "Gs", Value: 80},
|
||||
{Name: "Gw", Value: 70},
|
||||
{Name: "Ko", Value: 85},
|
||||
{Name: "In", Value: 60},
|
||||
{Name: "Zt", Value: 55},
|
||||
{Name: "Au", Value: 75},
|
||||
{Name: "pA", Value: 65},
|
||||
{Name: "Wk", Value: 50},
|
||||
},
|
||||
Lp: models.Lp{Value: 20, Max: 25},
|
||||
Ap: models.Ap{Value: 15, Max: 20},
|
||||
B: models.B{Value: 15},
|
||||
}
|
||||
|
||||
// Add 50 skills to force multiple pages
|
||||
char.Fertigkeiten = make([]models.SkFertigkeit, 50)
|
||||
for i := 0; i < 50; i++ {
|
||||
char.Fertigkeiten[i] = models.SkFertigkeit{
|
||||
BamortCharTrait: models.BamortCharTrait{
|
||||
BamortBase: models.BamortBase{Name: "Skill " + string(rune('A'+i%26))},
|
||||
},
|
||||
Fertigkeitswert: 10 + i%15,
|
||||
Pp: i % 8,
|
||||
}
|
||||
}
|
||||
|
||||
// Map to view model
|
||||
viewModel, err := MapCharacterToViewModel(char)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to map character: %v", err)
|
||||
}
|
||||
|
||||
// Load templates
|
||||
loader := NewTemplateLoader("../templates/Default_A4_Quer")
|
||||
if err = loader.LoadTemplates(); err != nil {
|
||||
t.Fatalf("Failed to load templates: %v", err)
|
||||
}
|
||||
|
||||
renderer := NewPDFRenderer()
|
||||
|
||||
// Act - Render page1 with continuations
|
||||
pdfs, err := RenderPageWithContinuations(
|
||||
viewModel,
|
||||
"page1_stats.html",
|
||||
1,
|
||||
"20.12.2025",
|
||||
loader,
|
||||
renderer,
|
||||
)
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render with continuations: %v", err)
|
||||
}
|
||||
|
||||
// Get template capacity to calculate expected pages
|
||||
templateSet := DefaultA4QuerTemplateSet()
|
||||
var skillsCapacity int
|
||||
for _, tmpl := range templateSet.Templates {
|
||||
if tmpl.Metadata.Name == "page1_stats.html" {
|
||||
for _, block := range tmpl.Metadata.Blocks {
|
||||
if block.ListType == "skills" {
|
||||
skillsCapacity += block.MaxItems
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
expectedPages := (50 + skillsCapacity - 1) / skillsCapacity
|
||||
if len(pdfs) != expectedPages {
|
||||
t.Errorf("Expected %d PDFs (pages) for 50 skills with capacity %d, got %d",
|
||||
expectedPages, skillsCapacity, len(pdfs))
|
||||
}
|
||||
|
||||
// Verify each PDF is valid
|
||||
for i, pdf := range pdfs {
|
||||
if len(pdf) == 0 {
|
||||
t.Errorf("PDF %d is empty", i+1)
|
||||
}
|
||||
if string(pdf[0:4]) != "%PDF" {
|
||||
t.Errorf("PDF %d does not start with PDF marker", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ Successfully generated %d continuation pages for 50 skills (capacity: %d)",
|
||||
len(pdfs), skillsCapacity)
|
||||
}
|
||||
|
||||
// TestRenderWithContinuations_NoOverflow tests that single page is rendered
|
||||
// when items fit within capacity
|
||||
func TestRenderWithContinuations_NoOverflow(t *testing.T) {
|
||||
// Arrange - Create character with few skills
|
||||
char := &models.Char{
|
||||
BamortBase: models.BamortBase{
|
||||
Name: "Test Character",
|
||||
},
|
||||
Typ: "Krieger",
|
||||
Grad: 5,
|
||||
Alter: 30,
|
||||
Eigenschaften: []models.Eigenschaft{
|
||||
{Name: "St", Value: 90},
|
||||
{Name: "Gs", Value: 80},
|
||||
{Name: "Gw", Value: 70},
|
||||
{Name: "Ko", Value: 85},
|
||||
{Name: "In", Value: 60},
|
||||
{Name: "Zt", Value: 55},
|
||||
{Name: "Au", Value: 75},
|
||||
{Name: "pA", Value: 65},
|
||||
{Name: "Wk", Value: 50},
|
||||
},
|
||||
Lp: models.Lp{Value: 20, Max: 25},
|
||||
Ap: models.Ap{Value: 15, Max: 20},
|
||||
B: models.B{Value: 15},
|
||||
}
|
||||
|
||||
// Add only 3 skills - should fit on one page
|
||||
char.Fertigkeiten = make([]models.SkFertigkeit, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
char.Fertigkeiten[i] = models.SkFertigkeit{
|
||||
BamortCharTrait: models.BamortCharTrait{
|
||||
BamortBase: models.BamortBase{Name: "Skill " + string(rune('A'+i))},
|
||||
},
|
||||
Fertigkeitswert: 10,
|
||||
Pp: 5,
|
||||
}
|
||||
}
|
||||
|
||||
// Map to view model
|
||||
viewModel, err := MapCharacterToViewModel(char)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to map character: %v", err)
|
||||
}
|
||||
|
||||
// Load templates
|
||||
loader := NewTemplateLoader("../templates/Default_A4_Quer")
|
||||
if err = loader.LoadTemplates(); err != nil {
|
||||
t.Fatalf("Failed to load templates: %v", err)
|
||||
}
|
||||
|
||||
renderer := NewPDFRenderer()
|
||||
|
||||
// Act
|
||||
pdfs, err := RenderPageWithContinuations(
|
||||
viewModel,
|
||||
"page1_stats.html",
|
||||
1,
|
||||
"20.12.2025",
|
||||
loader,
|
||||
renderer,
|
||||
)
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render: %v", err)
|
||||
}
|
||||
|
||||
if len(pdfs) != 1 {
|
||||
t.Errorf("Expected 1 PDF for 3 skills, got %d", len(pdfs))
|
||||
}
|
||||
|
||||
t.Logf("✓ Correctly generated single page for 3 skills")
|
||||
}
|
||||
@@ -68,6 +68,9 @@ func (tl *TemplateLoader) LoadTemplates() error {
|
||||
}
|
||||
|
||||
// RenderTemplate renders a specific template with the given data
|
||||
// For continuation templates (e.g., "page1.2_stats.html"), it automatically
|
||||
// falls back to the base template (e.g., "page1_stats.html") if the continuation
|
||||
// template doesn't exist as a physical file
|
||||
func (tl *TemplateLoader) RenderTemplate(templateName string, data interface{}) (string, error) {
|
||||
if tl.templates == nil {
|
||||
return "", fmt.Errorf("templates not loaded, call LoadTemplates first")
|
||||
@@ -75,7 +78,17 @@ func (tl *TemplateLoader) RenderTemplate(templateName string, data interface{})
|
||||
|
||||
tmpl := tl.templates.Lookup(templateName)
|
||||
if tmpl == nil {
|
||||
return "", fmt.Errorf("template not found: %s", templateName)
|
||||
// Try to extract base template name for continuation pages
|
||||
// e.g., "page1.2_stats.html" -> "page1_stats.html"
|
||||
baseTemplateName := ExtractBaseTemplateName(templateName)
|
||||
if baseTemplateName != templateName {
|
||||
tmpl = tl.templates.Lookup(baseTemplateName)
|
||||
if tmpl == nil {
|
||||
return "", fmt.Errorf("template not found: %s (and base template %s not found)", templateName, baseTemplateName)
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("template not found: %s", templateName)
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
Reference in New Issue
Block a user