From 35282137839550fe021e5b2a0e5575ea3c4605e1 Mon Sep 17 00:00:00 2001 From: Josh B <421772+HT154@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:02:10 -0700 Subject: [PATCH] [pkl.table] Add HTML style (#67) Adds an HTML style to the Table renderer. --- packages/pkl.table/PklProject | 4 +- packages/pkl.table/table.pkl | 117 ++++++++++++++---- packages/pkl.table/tests/table.pkl | 24 ++++ .../pkl.table/tests/table.pkl-expected.pcf | 87 +++++++++++++ 4 files changed, 204 insertions(+), 28 deletions(-) diff --git a/packages/pkl.table/PklProject b/packages/pkl.table/PklProject index 22b1b05..f2bcd30 100644 --- a/packages/pkl.table/PklProject +++ b/packages/pkl.table/PklProject @@ -17,6 +17,6 @@ amends "../basePklProject.pkl" package { - version = "1.0.1" - description = "Generates pretty human-readable and markdown-compatible tables." + version = "1.1.0" + description = "Generates pretty human-readable, markdown-compatible, and HTML tables." } diff --git a/packages/pkl.table/table.pkl b/packages/pkl.table/table.pkl index d301c80..7647028 100644 --- a/packages/pkl.table/table.pkl +++ b/packages/pkl.table/table.pkl @@ -39,9 +39,17 @@ class Column { align: Alignment = "left" } +/// Used to provide fully custom content for an entire table row +/// One possible usage is to express colspan in HTML tables +open class RowDirective { + content: String + + function render(style: TableStyle): String = content +} + /// Style options used to draw the table /// Default values produce a table consisting of simple ASCII characters -class TableStyle { +open class TableStyle { /// Placeholder to use in place of null property values nullPlaceholder: String = "null" @@ -49,30 +57,37 @@ class TableStyle { includeHeader: Boolean = true /// Default character to use for horizontal rules - defaultHorizontal: Char = "-" + defaultHorizontal: String(!fixedWidth || length == 1) = "-" + + /// Indicates whether or not the table is expected to produce fixed-width output + /// Disabling this enables styles to produce markup languages like HTML + fixedWidth = true /// Characters to use for specific horizontal rules - horizontals: Mapping = new { + horizontals: Mapping = new { ["top"] = defaultHorizontal ["inner"] = defaultHorizontal ["bottom"] = defaultHorizontal } /// Default character to use for vertical rules - defaultVertical: Char = "|" + defaultVertical: String(!fixedWidth || length == 1) = "|" /// Characters to use for specific horizontal rules - verticals: Mapping = new { + verticals: Mapping = new { ["left"] = defaultVertical ["inner"] = defaultVertical ["right"] = defaultVertical } + /// Characters to use for specific vertical rules in the header + headerVerticals: Mapping = verticals + /// Default character to use for corners - defaultCorner: Char = "+" + defaultCorner: String(!fixedWidth || length == 1) = "+" /// Characters to use for specific corners - corners: Mapping> = new { + corners: Mapping> = new { ["left"] { ["top"] = defaultCorner ["inner"] = defaultCorner @@ -91,6 +106,36 @@ class TableStyle { } } +/// Style options specific to HTML tables +/// Default values produce a table with two-space indentation +open class TableStyleHTML extends TableStyle { + /// Controls which character(s) are used for one indentation level + indent: String = " " + + /// Indent controls the baseline indent level applied to every line + baseIndent: String = "" + + local _style = this + + fixedWidth = false + defaultCorner = "" + horizontals { + ["top"] = "\(baseIndent)\n\(baseIndent)\(indent)<\(if (_style.includeHeader) "thead" else "tbody")>" + ["inner"] = "\(baseIndent)\(indent)\n\(baseIndent)\(indent)" + ["bottom"] = "\(baseIndent)\(indent)\n\(baseIndent)
" + } + verticals { + ["left"] = "\(baseIndent)\(indent.repeat(2))\n\(baseIndent)\(indent.repeat(3))" + ["inner"] = "\n\(baseIndent)\(indent.repeat(3))" + ["right"] = "\n\(baseIndent)\(indent.repeat(2))" + } + headerVerticals { + ["left"] = "\(baseIndent)\(indent.repeat(2))\n\(baseIndent)\(indent.repeat(3))" + ["inner"] = "\n\(baseIndent)\(indent.repeat(3))" + ["right"] = "\n\(baseIndent)\(indent.repeat(2))" + } +} + local class InterimTable { style: TableStyle @@ -110,11 +155,15 @@ local class InterimTable { } } - renderedCells: Listing> = new { + renderedCells: Listing<*Mapping | RowDirective> = new { for (row in rows) { - new { - for (column in columns) { - [column.key] = renderCell(column.key, row.getPropertyOrNull(column.key)) + when (row is RowDirective) { + row + } else { + new { + for (column in columns) { + [column.key] = renderCell(column.key, row.getPropertyOrNull(column.key)) + } } } } @@ -125,13 +174,17 @@ local class InterimTable { renderHorizontalRule("top") } when (style.includeHeader) { - renderRow(columns.toList().toMap((col) -> col.key, (col) -> col.title)) + renderRow(columns.toList().toMap((col) -> col.key, (col) -> col.title), true) when (style.horizontals["inner"] != null) { renderHorizontalRule("inner") } } for (row in renderedCells) { - renderRow(row.toMap()) + when (row is RowDirective) { + row.render(style) + } else { + renderRow(row.toMap(), false) + } } when (style.horizontals["bottom"] != null) { renderHorizontalRule("bottom") @@ -144,25 +197,34 @@ local class InterimTable { value?.toString() ?? style.nullPlaceholder - function renderRow(renderedCells: Map) = new Listing { - style.verticals["left"] ?? "" + function renderRow(renderedCells: Map, inHeader: Boolean) = new Listing { + local verticals = if (inHeader) style.headerVerticals else style.verticals + verticals["left"] ?? "" for (i, col in columns) { - " " - if (col.align == "left") - renderedCells[col.key].padEnd(columnWidths[col.key], " ") - else if (col.align == "right") - renderedCells[col.key].padStart(columnWidths[col.key], " ") - else "" - " " - style.verticals[if (i == columns.length - 1) "right" else "inner"] ?? "" + when (style.fixedWidth) { + " " + if (col.align == "left") + renderedCells[col.key].padEnd(columnWidths[col.key], " ") + else if (col.align == "right") + renderedCells[col.key].padStart(columnWidths[col.key], " ") + else "" + " " + } else { + renderedCells[col.key] + } + verticals[if (i == columns.length - 1) "right" else "inner"] ?? "" } }.join("") function renderHorizontalRule(verticalPosition: VerticalPosition): String = new Listing { style.corners["left"][verticalPosition] - for (i, col in columns) { - style.horizontals[verticalPosition].repeat(columnWidths[col.key] + 2) - style.corners[if (i == columns.length - 1) "right" else "inner"][verticalPosition] + when (style.fixedWidth) { + for (i, col in columns) { + style.horizontals[verticalPosition].repeat(columnWidths[col.key] + 2) + style.corners[if (i == columns.length - 1) "right" else "inner"][verticalPosition] + } + } else { + style.horizontals[verticalPosition] } }.join("") } @@ -249,3 +311,6 @@ const markdownStyle: TableStyle = new { ["bottom"] = null } } + +/// [TableStyle] that renders HTML tables +const htmlStyle: TableStyleHTML = new {} diff --git a/packages/pkl.table/tests/table.pkl b/packages/pkl.table/tests/table.pkl index 5aa00d2..84bb880 100644 --- a/packages/pkl.table/tests/table.pkl +++ b/packages/pkl.table/tests/table.pkl @@ -51,6 +51,14 @@ examples { } }.renderDocument(data) } + ["row directive"] { + new table.TableRenderer { + style = table.htmlStyle + columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } + }.renderDocument((data) { + new table.RowDirective { content = "\(table.htmlStyle.indent.repeat(2))this is a colspan" } + }) + } ["coverter"] { new table.TableRenderer { columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma"); table.Column("delta") } @@ -78,4 +86,20 @@ examples { columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } }.renderDocument(data) } + ["html"] { + new table.TableRenderer { + style = table.htmlStyle + columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } + }.renderDocument(data) + } + ["html with custom indent and no header"] { + new table.TableRenderer { + style = (table.htmlStyle) { + baseIndent = "indent:" + indent = "\t" + includeHeader = false + } + columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } + }.renderDocument(data) + } } diff --git a/packages/pkl.table/tests/table.pkl-expected.pcf b/packages/pkl.table/tests/table.pkl-expected.pcf index 32ed18e..6856c7f 100644 --- a/packages/pkl.table/tests/table.pkl-expected.pcf +++ b/packages/pkl.table/tests/table.pkl-expected.pcf @@ -23,6 +23,38 @@ examples { """ } + ["row directive"] { + """ + + + + + + + + + + + + + + + + + + + + + + + + + + +
alphabetagamma
123123123.h
456456456.h
789loooooong789789.h
this is a colspan
+ + """ + } ["coverter"] { """ +--------------+------+-----------+-------+ @@ -67,6 +99,61 @@ examples { | 456 | 456 | 456.h | | 789loooooong | 789 | 789.h | + """ + } + ["html"] { + """ + + + + + + + + + + + + + + + + + + + + + + + + + +
alphabetagamma
123123123.h
456456456.h
789loooooong789789.h
+ + """ + } + ["html with custom indent and no header"] { + """ + indent: + indent:\t + indent:\t\t + indent:\t\t\t + indent:\t\t\t + indent:\t\t\t + indent:\t\t + indent:\t\t + indent:\t\t\t + indent:\t\t\t + indent:\t\t\t + indent:\t\t + indent:\t\t + indent:\t\t\t + indent:\t\t\t + indent:\t\t\t + indent:\t\t + indent:\t + indent:
123123123.h
456456456.h
789loooooong789789.h
+ """ } }