@@ -132,11 +132,28 @@ internal override void ProcessSection((List<OpenXmlElement> content, SectionProp
132132 l = margins . Left . Value ;
133133 if ( margins . Right != null )
134134 r = margins . Right . Value ;
135- }
135+ }
136136
137137 // Convert twips to points
138138 var pageSet = new QuestPdfPageSet ( w / 20f , h / 20f , l / 20f , t / 20f , r / 20f , b / 20f ,
139139 QuestPDF . Infrastructure . Unit . Point ) ;
140+
141+ var columns = sectionProperties . GetFirstChild < Columns > ( ) ;
142+ if ( columns != null && columns . ColumnCount != null && columns . ColumnCount > 1 )
143+ {
144+ pageSet . NumberOfColumns = columns . ColumnCount . Value ;
145+
146+ if ( columns . Space . ToFloat ( ) is float columnGap && columnGap > 0 )
147+ {
148+ pageSet . SpaceBetweenColumns = columnGap / 20f ; // Convert twips to points
149+ }
150+
151+ if ( columns . EqualWidth != null && columns . EqualWidth . Value == false )
152+ {
153+ // TODO
154+ }
155+ }
156+
140157 if ( pageColor . HasValue )
141158 pageSet . BackgroundColor = pageColor . Value ;
142159
@@ -145,8 +162,7 @@ internal override void ProcessSection((List<OpenXmlElement> content, SectionProp
145162
146163 // Process headers for this section
147164 var headerRefs = sectionProperties . Elements < HeaderReference > ( ) ;
148- // QuestPDF can't produce different header and footer on odd/even pages.
149- // For now, handle the default header and footer only.
165+ // TODO: QuestPDF can produce different header and footer on odd/even pages using the ShowIf() function.
150166 var headerRef = headerRefs . FirstOrDefault ( h => h . Type == null || ! h . Type . HasValue || h . Type . Value == HeaderFooterValues . Default ) ;
151167 if ( headerRef ? . Id ? . Value is string headerId && mainPart . GetPartById ( headerId ) is HeaderPart headerPart )
152168 {
@@ -158,8 +174,7 @@ internal override void ProcessSection((List<OpenXmlElement> content, SectionProp
158174
159175 // Process footers for this section
160176 var footerRefs = sectionProperties . Elements < FooterReference > ( ) ;
161- // QuestPDF can't produce different header and footer on odd/even pages.
162- // For now, handle the default header and footer only.
177+ // TODO: QuestPDF can produce different header and footer on odd/even pages using the ShowIf() function.
163178 var footerRef = footerRefs . FirstOrDefault ( h => h . Type == null || ! h . Type . HasValue || h . Type . Value == HeaderFooterValues . Default ) ;
164179 if ( footerRef ? . Id ? . Value is string footerId && mainPart . GetPartById ( footerId ) is FooterPart footerPart )
165180 {
@@ -214,6 +229,9 @@ internal override void ProcessParagraph(Paragraph paragraph, QuestPdfModel outpu
214229 p . EndIndent = indent . EndIndent ;
215230 p . FirstLineIndent = indent . FirstLineIndent ;
216231
232+ p . KeepTogether = paragraph . GetEffectiveProperty < KeepLines > ( ) . ToBool ( ) ;
233+ // TODO: KeepNext (cannot be set at paragraph level in QuestPDF and requires a different approach)
234+
217235 // Add paragraph to the current container (body, header, footer, table cell, ...)
218236 if ( currentContainer . Count > 0 )
219237 currentContainer . Peek ( ) . Content . Add ( p ) ;
@@ -396,7 +414,8 @@ internal override void ProcessBreak(Break @break, QuestPdfModel output)
396414 // Add span to the paragraph/hyperlink
397415 newRunContainer . AddSpan ( newSpan ) ;
398416
399- // If the run container is an hyperlink, enclose it into a new paragraph
417+ // If the run container is an hyperlink, enclose it into a new paragraph,
418+ // otherwise the run container is the container itself.
400419 QuestPdfParagraph newParagraph ;
401420 if ( newRunContainer is QuestPdfParagraph paragraph )
402421 {
@@ -428,7 +447,47 @@ internal override void ProcessBreak(Break @break, QuestPdfModel output)
428447 }
429448 else if ( @break . Type . HasValue && @break . Type . Value == BreakValues . Page )
430449 {
450+ // Close and retrieve the current span, run container (paragraph/hyperlink) and paragraph
451+ var oldSpan = currentSpan . Pop ( ) ;
452+ var oldRunContainer = currentRunContainer . Pop ( ) ;
453+ var oldParagraph = currentParagraph . Pop ( ) ;
454+
455+ // Add a new QuestPdfPageBreak object
456+ currentContainer . Peek ( ) . Content . Add ( new QuestPdfPageBreak ( ) ) ;
457+
458+ // The old span and paragraph were closed ahead of time to process the Break element.
459+ // Create a new paragraph and span with the same properties to contain further elements.
460+
461+ // Create a new run container and span
462+ var newRunContainer = oldRunContainer . CloneEmpty ( ) ;
463+ var newSpan = oldSpan . CloneEmpty ( ) ;
464+
465+ // Add span to the paragraph/hyperlink
466+ newRunContainer . AddSpan ( newSpan ) ;
467+
468+ // If the run container is an hyperlink, enclose it into a new paragraph,
469+ // otherwise the run container is the container itself.
470+ QuestPdfParagraph newParagraph ;
471+ if ( newRunContainer is QuestPdfParagraph paragraph )
472+ {
473+ newParagraph = paragraph ;
474+ }
475+ else
476+ {
477+ newParagraph = ( QuestPdfParagraph ) ( oldParagraph . CloneEmpty ( ) ) ;
478+ if ( newRunContainer is QuestPdfHyperlink hyperlink )
479+ {
480+ newParagraph . Elements . Add ( hyperlink ) ;
481+ }
482+ }
431483
484+ // Set current span, run container and paragraph
485+ currentParagraph . Push ( newParagraph ) ;
486+ currentRunContainer . Push ( newRunContainer ) ;
487+ currentSpan . Push ( newSpan ) ;
488+
489+ // Add paragraph to the current container (body, header, footer, table cell, ...)
490+ currentContainer . Peek ( ) . Content . Add ( newParagraph ) ;
432491 }
433492 else if ( @break . Type . HasValue && @break . Type . Value == BreakValues . Column )
434493 {
@@ -479,6 +538,27 @@ internal override void ProcessSymbolChar(SymbolChar symbolChar, QuestPdfModel ou
479538 }
480539 }
481540
541+ internal override void ProcessPageNumber ( PageNumber pageNumber , QuestPdfModel output )
542+ {
543+ if ( currentRunContainer . Count > 0 &&
544+ currentSpan . Count > 0 ) // PageNumber can only be present inside a Run, just like regular Text elements.
545+ {
546+ // Close and retrieve the current span
547+ var oldSpan = currentSpan . Pop ( ) ;
548+
549+ // Add a new QuestPdfPageNumber object to the current run container.
550+ currentRunContainer . Peek ( ) . AddPageNumber ( ) ;
551+
552+ // The old span was closed ahead of time to process the PageNumber element.
553+ // Create a new span with the same properties to contain further text elements.
554+ // The new span will be closed by the ProcessRun method.
555+ // If there are no remaining elements, the new span will be empty
556+ // and will be ignored during rendering.
557+ var newSpan = oldSpan . CloneEmpty ( ) ;
558+ currentSpan . Push ( newSpan ) ;
559+ }
560+ }
561+
482562 internal override void ProcessTable ( Table table , QuestPdfModel output )
483563 {
484564 // Process table properties and create a new QuestPdfTable object
@@ -553,10 +633,6 @@ internal override void ProcessBookmarkEnd(BookmarkEnd bookmarkEnd, QuestPdfModel
553633 {
554634 }
555635
556- internal override void ProcessPageNumber ( PageNumber pageNumber , QuestPdfModel output )
557- {
558- }
559-
560636 internal override void ProcessAnnotationReference ( AnnotationReferenceMark annotationRef , QuestPdfModel output )
561637 {
562638 }
0 commit comments