Skip to content

Commit 1990f14

Browse files
committed
feat: finalize multi-section support
1 parent ac3ebd7 commit 1990f14

File tree

11 files changed

+702
-139
lines changed

11 files changed

+702
-139
lines changed

builder.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,133 @@ type DocumentBuilder struct {
5454
errors []error
5555
}
5656

57+
// SectionBuilder provides a fluent API for configuring document sections.
58+
type SectionBuilder struct {
59+
section domain.Section
60+
parent *DocumentBuilder
61+
err error
62+
}
63+
64+
func (sb *SectionBuilder) recordError(err error) {
65+
if err == nil {
66+
return
67+
}
68+
if sb.err == nil {
69+
sb.err = err
70+
}
71+
if sb.parent != nil {
72+
sb.parent.errors = append(sb.parent.errors, err)
73+
}
74+
}
75+
76+
func (sb *SectionBuilder) ensureSection(op string) bool {
77+
if sb == nil {
78+
return false
79+
}
80+
if sb.err != nil {
81+
return false
82+
}
83+
if sb.section == nil {
84+
sb.recordError(errors.InvalidState(op, "section is nil"))
85+
return false
86+
}
87+
return true
88+
}
89+
90+
// PageSize sets the page size for the section.
91+
92+
func (sb *SectionBuilder) PageSize(size domain.PageSize) *SectionBuilder {
93+
if !sb.ensureSection("SectionBuilder.PageSize") {
94+
return sb
95+
}
96+
97+
if err := sb.section.SetPageSize(size); err != nil {
98+
sb.recordError(err)
99+
}
100+
return sb
101+
}
102+
103+
// Orientation sets the page orientation for the section.
104+
func (sb *SectionBuilder) Orientation(orient domain.Orientation) *SectionBuilder {
105+
if !sb.ensureSection("SectionBuilder.Orientation") {
106+
return sb
107+
}
108+
if err := sb.section.SetOrientation(orient); err != nil {
109+
sb.recordError(err)
110+
}
111+
return sb
112+
}
113+
114+
// Margins sets the page margins for the section.
115+
func (sb *SectionBuilder) Margins(margins domain.Margins) *SectionBuilder {
116+
if !sb.ensureSection("SectionBuilder.Margins") {
117+
return sb
118+
}
119+
if err := sb.section.SetMargins(margins); err != nil {
120+
sb.recordError(err)
121+
}
122+
return sb
123+
}
124+
125+
// Columns sets the column layout for the section.
126+
func (sb *SectionBuilder) Columns(count int) *SectionBuilder {
127+
if !sb.ensureSection("SectionBuilder.Columns") {
128+
return sb
129+
}
130+
if err := sb.section.SetColumns(count); err != nil {
131+
sb.recordError(err)
132+
}
133+
return sb
134+
}
135+
136+
// Header returns the requested header for direct manipulation.
137+
func (sb *SectionBuilder) Header(headerType domain.HeaderType) (domain.Header, error) {
138+
if sb == nil {
139+
return nil, errors.InvalidState("SectionBuilder.Header", "section is nil")
140+
}
141+
if !sb.ensureSection("SectionBuilder.Header") {
142+
return nil, sb.err
143+
}
144+
head, err := sb.section.Header(headerType)
145+
if err != nil {
146+
sb.recordError(err)
147+
return nil, err
148+
}
149+
return head, nil
150+
}
151+
152+
// Footer returns the requested footer for direct manipulation.
153+
func (sb *SectionBuilder) Footer(footerType domain.FooterType) (domain.Footer, error) {
154+
if sb == nil {
155+
return nil, errors.InvalidState("SectionBuilder.Footer", "section is nil")
156+
}
157+
if !sb.ensureSection("SectionBuilder.Footer") {
158+
return nil, sb.err
159+
}
160+
foot, err := sb.section.Footer(footerType)
161+
if err != nil {
162+
sb.recordError(err)
163+
return nil, err
164+
}
165+
return foot, nil
166+
}
167+
168+
// Section exposes the underlying domain.Section for advanced scenarios.
169+
func (sb *SectionBuilder) Section() domain.Section {
170+
if sb == nil {
171+
return nil
172+
}
173+
return sb.section
174+
}
175+
176+
// End returns control to the DocumentBuilder.
177+
func (sb *SectionBuilder) End() *DocumentBuilder {
178+
if sb == nil {
179+
return nil
180+
}
181+
return sb.parent
182+
}
183+
57184
// NewDocumentBuilder creates a new document builder with optional configuration.
58185
//
59186
// Example:
@@ -131,6 +258,39 @@ func (b *DocumentBuilder) AddTable(rows, cols int) *TableBuilder {
131258
}
132259
}
133260

261+
// DefaultSection returns a SectionBuilder for configuring the document's default section.
262+
// Errors are accumulated and surfaced during Build().
263+
func (b *DocumentBuilder) DefaultSection() *SectionBuilder {
264+
section, err := b.doc.DefaultSection()
265+
sb := &SectionBuilder{
266+
section: section,
267+
parent: b,
268+
}
269+
if err != nil {
270+
sb.recordError(err)
271+
}
272+
return sb
273+
}
274+
275+
// AddSection inserts a new section with the specified break type (default NextPage).
276+
// Returns a SectionBuilder for configuring the new section.
277+
func (b *DocumentBuilder) AddSection(breakType ...domain.SectionBreakType) *SectionBuilder {
278+
bt := domain.SectionBreakTypeNextPage
279+
if len(breakType) > 0 {
280+
bt = breakType[0]
281+
}
282+
283+
section, err := b.doc.AddSectionWithBreak(bt)
284+
sb := &SectionBuilder{
285+
section: section,
286+
parent: b,
287+
}
288+
if err != nil {
289+
sb.recordError(err)
290+
}
291+
return sb
292+
}
293+
134294
// SetMetadata sets the document metadata.
135295
func (b *DocumentBuilder) SetMetadata(meta *domain.Metadata) *DocumentBuilder {
136296
if err := b.doc.SetMetadata(meta); err != nil {

builder_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,129 @@ func TestDocumentBuilder_Build(t *testing.T) {
5151
})
5252
}
5353

54+
func TestDocumentBuilder_Sections(t *testing.T) {
55+
t.Run("configures default section", func(t *testing.T) {
56+
builder := NewDocumentBuilder()
57+
builder.DefaultSection().
58+
PageSize(domain.PageSizeA4).
59+
Orientation(domain.OrientationLandscape).
60+
Margins(domain.Margins{Top: 720, Right: 1440, Bottom: 720, Left: 1440, Header: 360, Footer: 360}).
61+
Columns(2).
62+
End()
63+
builder.AddParagraph().Text("section content").End()
64+
65+
doc, err := builder.Build()
66+
if err != nil {
67+
t.Fatalf("expected no error, got %v", err)
68+
}
69+
70+
sections := doc.Sections()
71+
if len(sections) != 1 {
72+
t.Fatalf("expected 1 section, got %d", len(sections))
73+
}
74+
75+
sec := sections[0]
76+
if sec.PageSize() != domain.PageSizeA4 {
77+
t.Errorf("expected A4 page size, got %+v", sec.PageSize())
78+
}
79+
if sec.Orientation() != domain.OrientationLandscape {
80+
t.Errorf("expected landscape orientation, got %v", sec.Orientation())
81+
}
82+
if sec.Columns() != 2 {
83+
t.Errorf("expected 2 columns, got %d", sec.Columns())
84+
}
85+
m := sec.Margins()
86+
if m.Top != 720 || m.Bottom != 720 || m.Header != 360 || m.Footer != 360 {
87+
t.Errorf("unexpected margins %+v", m)
88+
}
89+
})
90+
91+
t.Run("adds additional section with break", func(t *testing.T) {
92+
builder := NewDocumentBuilder()
93+
builder.AddParagraph().Text("Before section break").End()
94+
95+
builder.AddSection(domain.SectionBreakTypeEvenPage).
96+
Columns(3).
97+
Orientation(domain.OrientationPortrait).
98+
End()
99+
100+
builder.AddParagraph().Text("After section break").End()
101+
102+
doc, err := builder.Build()
103+
if err != nil {
104+
t.Fatalf("expected no error, got %v", err)
105+
}
106+
107+
sections := doc.Sections()
108+
if len(sections) != 2 {
109+
t.Fatalf("expected 2 sections, got %d", len(sections))
110+
}
111+
if sections[1].Columns() != 3 {
112+
t.Errorf("expected 3 columns on second section, got %d", sections[1].Columns())
113+
}
114+
115+
blocks := doc.Blocks()
116+
foundBreak := false
117+
for _, block := range blocks {
118+
if block.SectionBreak != nil {
119+
foundBreak = true
120+
if block.SectionBreak.Type != domain.SectionBreakTypeEvenPage {
121+
t.Errorf("expected even page break, got %v", block.SectionBreak.Type)
122+
}
123+
}
124+
}
125+
if !foundBreak {
126+
t.Fatalf("expected section break block in document")
127+
}
128+
})
129+
130+
t.Run("records errors when section configuration fails", func(t *testing.T) {
131+
builder := NewDocumentBuilder()
132+
builder.DefaultSection().Columns(0).End()
133+
134+
if _, err := builder.Build(); err == nil {
135+
t.Fatal("expected error due to invalid column count, got nil")
136+
}
137+
})
138+
139+
t.Run("configures header via section builder", func(t *testing.T) {
140+
builder := NewDocumentBuilder()
141+
secBuilder := builder.DefaultSection()
142+
header, err := secBuilder.Header(domain.HeaderDefault)
143+
if err != nil {
144+
t.Fatalf("expected header, got error %v", err)
145+
}
146+
147+
para, err := header.AddParagraph()
148+
if err != nil {
149+
t.Fatalf("expected paragraph in header, got %v", err)
150+
}
151+
run, err := para.AddRun()
152+
if err != nil {
153+
t.Fatalf("expected run in header paragraph, got %v", err)
154+
}
155+
if err := run.SetText("Header text"); err != nil {
156+
t.Fatalf("expected to set header text, got %v", err)
157+
}
158+
159+
secBuilder.End()
160+
builder.AddParagraph().Text("body content").End()
161+
doc, err := builder.Build()
162+
if err != nil {
163+
t.Fatalf("expected no error, got %v", err)
164+
}
165+
166+
section := doc.Sections()[0]
167+
storedHeader, err := section.Header(domain.HeaderDefault)
168+
if err != nil {
169+
t.Fatalf("expected header from section, got %v", err)
170+
}
171+
if len(storedHeader.Paragraphs()) == 0 {
172+
t.Fatal("expected header to contain paragraphs")
173+
}
174+
})
175+
}
176+
54177
func TestParagraphBuilder_Text(t *testing.T) {
55178
t.Run("adds single text run", func(t *testing.T) {
56179
builder := NewDocumentBuilder()

0 commit comments

Comments
 (0)