Browse Source

Implement improved support for semantic tables

Marcin Ziąbek 3 months ago
parent
commit
eac4b416de

+ 99 - 0
Source/QuestPDF.DocumentationExamples/SemanticExamples.cs

@@ -8,6 +8,105 @@ namespace QuestPDF.DocumentationExamples;
 
 public class SemanticExamples
 {
+    [Test]
+    public void HeaderAndFooter()
+    {
+        Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.MinSize(new PageSize(0, 0));
+                    page.MaxSize(new PageSize(600, 250));
+                    page.DefaultTextStyle(x => x.FontSize(16));
+                    page.Margin(25);
+
+                    page.Content()
+                        .Border(1)
+                        .BorderColor(Colors.Grey.Lighten1)
+                        .Table(table =>
+                        {
+                            table.ApplySemanticTags();
+                            
+                            var pageSizes = new List<(string name, double width, double height)>()
+                            {
+                                ("Letter (ANSI A)", 8.5f, 11),
+                                ("Legal", 8.5f, 14),
+                                ("Ledger (ANSI B)", 11, 17),
+                                ("Tabloid (ANSI B)", 17, 11),
+                                ("ANSI C", 22, 17),
+                                ("ANSI D", 34, 22),
+                                ("ANSI E", 44, 34)
+                            };
+
+                            const int inchesToPoints = 72;
+
+                            IContainer DefaultCellStyle(IContainer container, string backgroundColor)
+                            {
+                                return container
+                                    .Border(1)
+                                    .BorderColor(Colors.Grey.Lighten1)
+                                    .Background(backgroundColor)
+                                    .PaddingVertical(5)
+                                    .PaddingHorizontal(10)
+                                    .AlignCenter()
+                                    .AlignMiddle();
+                            }
+
+                            table.ColumnsDefinition(columns =>
+                            {
+                                columns.RelativeColumn();
+
+                                columns.ConstantColumn(80);
+                                columns.ConstantColumn(80);
+
+                                columns.ConstantColumn(80);
+                                columns.ConstantColumn(80);
+                            });
+
+                            table.Header(header =>
+                            {
+                                // please be sure to call the 'header' handler!
+
+                                header.Cell().RowSpan(2).Element(CellStyle).ExtendHorizontal().AlignLeft()
+                                    .SemanticParagraph().Text("Document type").Bold();
+
+                                header.Cell().ColumnSpan(2).Element(CellStyle).SemanticParagraph().Text("Inches").Bold();
+                                header.Cell().ColumnSpan(2).Element(CellStyle).SemanticParagraph().Text("Points").Bold();
+
+                                header.Cell().Element(CellStyle).SemanticParagraph().Text("Width");
+                                header.Cell().Element(CellStyle).SemanticParagraph().Text("Height");
+
+                                header.Cell().Element(CellStyle).SemanticParagraph().Text("Width");
+                                header.Cell().Element(CellStyle).SemanticParagraph().Text("Height");
+
+                                // you can extend existing styles by creating additional methods
+                                IContainer CellStyle(IContainer container) =>
+                                    DefaultCellStyle(container, Colors.Grey.Lighten3);
+                            });
+
+                            foreach (var page in pageSizes)
+                            {
+                                table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().SemanticParagraph().Text(page.name);
+
+                                // inches
+                                table.Cell().Element(CellStyle).SemanticParagraph().Text(page.width);
+                                table.Cell().Element(CellStyle).SemanticParagraph().Text(page.height);
+
+                                // points
+                                table.Cell().Element(CellStyle).SemanticParagraph().Text(page.width * inchesToPoints);
+                                table.Cell().Element(CellStyle).SemanticParagraph().Text(page.height * inchesToPoints);
+
+                                IContainer CellStyle(IContainer container) =>
+                                    DefaultCellStyle(container, Colors.White).ShowOnce();
+                            }
+                        });
+                });
+            })
+            .GeneratePdfAndShow();
+    }
+    
+    
     public class BookTermModel
     {
         public string Term { get; set; }

+ 4 - 0
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -6,6 +6,7 @@ using QuestPDF.Drawing.DocumentCanvases;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Drawing.Proxy;
 using QuestPDF.Elements;
+using QuestPDF.Elements.Table;
 using QuestPDF.Elements.Text;
 using QuestPDF.Elements.Text.Items;
 using QuestPDF.Helpers;
@@ -347,6 +348,9 @@ namespace QuestPDF.Drawing
                 
                 else if (x is DynamicHost dynamicHost)
                     dynamicHost.SemanticTreeManager = semanticTreeManager;
+                
+                else if (x is Table table)
+                    table.SemanticTreeManager = semanticTreeManager;
             });
         }
         

+ 5 - 0
Source/QuestPDF/Drawing/SemanticTreeManager.cs

@@ -59,6 +59,11 @@ class SemanticTreeManager
         Stack.Pop();
     }
     
+    public SemanticTreeNode PeekStack()
+    {
+        return Stack.Peek();
+    }
+    
     public void Reset()
     {
         CurrentNodeId = 0;

+ 22 - 17
Source/QuestPDF/Elements/SemanticTag.cs

@@ -17,23 +17,7 @@ internal class SemanticTag : ContainerElement
 
     internal override void Draw(Size availableSpace)
     {
-        if (TagType is "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6")
-            UpdateHeaderText();
-        
-        if (SemanticTreeNode == null)
-        {
-            var id = SemanticTreeManager.GetNextNodeId();
-            
-            SemanticTreeNode = new SemanticTreeNode
-            {
-                NodeId = id,
-                Type = TagType,
-                Alt = Alt,
-                Lang = Lang
-            };
-            
-            SemanticTreeManager.AddNode(SemanticTreeNode);
-        }
+        RegisterCurrentSemanticNode();
         
         SemanticTreeManager.PushOnStack(SemanticTreeNode);
         Canvas.SetSemanticNodeId(SemanticTreeNode.NodeId);
@@ -42,6 +26,27 @@ internal class SemanticTag : ContainerElement
         SemanticTreeManager.PopStack();
     }
 
+    internal void RegisterCurrentSemanticNode()
+    {
+        if (SemanticTreeNode != null)
+            return;
+        
+        if (TagType is "H" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6")
+            UpdateHeaderText();
+        
+        var id = SemanticTreeManager.GetNextNodeId();
+            
+        SemanticTreeNode = new SemanticTreeNode
+        {
+            NodeId = id,
+            Type = TagType,
+            Alt = Alt,
+            Lang = Lang
+        };
+            
+        SemanticTreeManager.AddNode(SemanticTreeNode);
+    }
+
     private void UpdateHeaderText()
     {
         if (!string.IsNullOrWhiteSpace(Alt))

+ 34 - 0
Source/QuestPDF/Elements/Table/Table.cs

@@ -110,6 +110,7 @@ namespace QuestPDF.Elements.Table
         internal override void Draw(Size availableSpace)
         {
             Initialize();
+            RegisterSemanticTree();
             
             if (IsRendered)
                 return;
@@ -353,5 +354,38 @@ namespace QuestPDF.Elements.Table
         }
     
         #endregion
+        
+        #region Semantic
+        
+        internal SemanticTreeManager SemanticTreeManager { get; set; } = new();
+
+        private void RegisterSemanticTree()
+        {
+            foreach (var tableRow in Cells.GroupBy(x => x.Row))
+            {
+                var rowSemanticTreeNode = new SemanticTreeNode()
+                {
+                    NodeId = SemanticTreeManager.GetNextNodeId(), 
+                    Type = "TR"
+                };
+                
+                SemanticTreeManager.AddNode(rowSemanticTreeNode);
+                SemanticTreeManager.PushOnStack(rowSemanticTreeNode);
+                
+                foreach (var tableCell in tableRow.OrderBy(x => x.Column))
+                {
+                    var semanticTag = tableCell.Child as SemanticTag;
+                    
+                    if (semanticTag == null)
+                        continue;
+                    
+                    semanticTag.RegisterCurrentSemanticNode();
+                }
+                
+                SemanticTreeManager.PopStack();
+            }
+        }
+        
+        #endregion
     }
 }

+ 1 - 1
Source/QuestPDF/Fluent/SemanticExtensions.cs

@@ -305,7 +305,7 @@ public static class SemanticExtensions
     /// </summary>
     public static IContainer SemanticTableFooter(this IContainer container)
     {
-        return container.SemanticTag("TFood");
+        return container.SemanticTag("TFoot");
     }
     
     #endregion

+ 14 - 2
Source/QuestPDF/Fluent/TableExtensions.cs

@@ -162,7 +162,7 @@ namespace QuestPDF.Fluent
             var hasHeader = HeaderTable.Cells.Any();
             var hasFooter = FooterTable.Cells.Any();
             
-            ConfigureTable(HeaderTable);
+            ConfigureTable(HeaderTable, isHeader: true);
             ConfigureTable(ContentTable);
             ConfigureTable(FooterTable);
             
@@ -191,13 +191,25 @@ namespace QuestPDF.Fluent
 
             return container;
             
-            static void ConfigureTable(Table table)
+            void ConfigureTable(Table table, bool isHeader = false)
             {
                 if (!table.Columns.Any())
                     throw new DocumentComposeException($"Table should have at least one column. Please call the '{nameof(ColumnsDefinition)}' method to define columns.");
             
                 table.PlanCellPositions();
                 table.ValidateCellPositions();
+
+                if (EnableAutomatedSemanticTagging)
+                {
+                    foreach (var tableCell in table.Cells)
+                    {
+                        tableCell.CreateProxy(x => new SemanticTag()
+                        {
+                            Child = x,
+                            TagType = isHeader ? "TH" : "TD"
+                        });
+                    }
+                }
             }
         }
     }