diff --git a/LIMITATIONS.adoc b/LIMITATIONS.adoc index 5961da9f..fd65c48c 100644 --- a/LIMITATIONS.adoc +++ b/LIMITATIONS.adoc @@ -3,36 +3,12 @@ This document reports the known limitations and differences with Asciidoc/Asciidoctor. In any case, feel free to https://github.com/bytesparadise/libasciidoc/issues[open an issue] if you want to discuss an actual limitation of Libasciidoc or if you want to report a new one. +We also accept pull requests. -== Quoted Text - -Quoted text rendering can differ in the following cases: - -- when the punctuation is unbalanced. Eg: -.... -some **bold content*. -.... -will be rendered as the raw input: -.... -some **bold content*. -.... -instead of : -.... -

some *bold content

-.... - -- when quoted text uses the same punctuation. Eg: -.... -*some *nested bold* content*. -.... -Libasciidoc will detect the nested bold quote and renderer accordingly: -.... -some nested bold content. -.... -whereas Asciidoc/Asciidoctor will produce : -.... -

some *nested bold content*.

-.... +This list is not necessarily complete, but it should reflect the differences +likely to impact real documents. +The entire list of known issues is tracked in https://github.com/bytesparadise/libasciidoc/issues[GitHub]. +We plan to address the majority of these issues. == Two-line Section Titles @@ -61,6 +37,7 @@ will produce no HTML element at all, whereas Asciidoc/Asciidoctor will produce : Constrained or imbalanced monospace text may not act fully constrained, or may be confused in the presence of imbalanced quoted strings or apostrophes. Typically such markup is erroneous, but the results from such errors may differ between implementations. +See https://github.com/bytesparadise/libasciidoc/issues/630[Issue #630]. == Attribute Lists @@ -74,19 +51,32 @@ Escaping of quotes within quoted strings used as attribute value does not work. == Tables -Only simple tables are supported. No per-cell table styling, custom delimiters, or unusually formatted tables. -There is no support for the cols table attribute - the number of columns is determined by the number of cells -in the first line of the table. +Custom table delimiters, and custom formats (TSV, CSV, DSV) are not supported. +See https://github.com/bytesparadise/libasciidoc/issues/696[Issue #696]. -No support for nested tables. +Tables will not parse if the content starts with a blank line. +See https://github.com/bytesparadise/libasciidoc/issues/692[Issue #692]. + +Individual cell styles, including spans and repeats, are not supported. +See https://github.com/bytesparadise/libasciidoc/issues/695[Issue #695]. + +The style attribute for cell contents (specified in the cols attribute) is ignored (the "d" style for inline content is assumed). +See https://github.com/bytesparadise/libasciidoc/issues/694[Issue #694]. + +The parser will likely be confused by ragged tables, or tables that do not use one line per row. +See https://github.com/bytesparadise/libasciidoc/issues/637[Issue #637]. + +No support for nested tables. See https://github.com/bytesparadise/libasciidoc/issues/697[Issue #697]. == Lists Interactive checklists are not supported. +See https://github.com/bytesparadise/libasciidoc/issues/675[Issue #675]. == Images Interactive SVG support is missing, as is support for inline SVG. +See https://github.com/bytesparadise/libasciidoc/issues/674[Issue #674]. The global figure-caption attribute is not honored. Use per-image caption attributes for more control if needed. @@ -94,6 +84,7 @@ Use per-image caption attributes for more control if needed. == Multimedia Video and audio elements are not supported. +See https://github.com/bytesparadise/libasciidoc/issues/677[Issue #677]. == File Inclusions @@ -104,22 +95,27 @@ the "full" parsing. == Symbols and Characters Markup for the mdash and arrow symbols is not recognized. +See https://github.com/bytesparadise/libasciidoc/issues/678[Issue #678]. Symbols for quotes (both single and double) will be inlined as numeric HTML entities, even in cases where this is not strictly necessary. Symbolic entity names (such as `◊` for ◊) are not recognized -- leaving recognition and rendering dependent on the user-agent. +See https://github.com/bytesparadise/libasciidoc/issues/680[Issue #680]. == User Interface Macros The experimental macros for user interfaces (`kbd`, `menu`, and `btn`) are not supported. +See https://github.com/bytesparadise/libasciidoc/issues/607[Issue #607]. == Admonitions Use of unicode symbols or other replacement using the per-type caption attribute is not supported. +See https://github.com/bytesparadise/libasciidoc/issues/679[Issue #679]. == Favicon The `favicon` document attribute is not recognized. +See https://github.com/bytesparadise/libasciidoc/issues/681[Issue #681]. == Syntax Highlighters @@ -128,24 +124,30 @@ Only the `pygments` highlighter is recognized. == Math MathML and equations (`[stem]` blocks) are not supported yet. +See https://github.com/bytesparadise/libasciidoc/issues/608[Issue #608]. == Bibliographies Bibliographies using bibtex are not supported yet. +See https://github.com/bytesparadise/libasciidoc/issues/609[Issue #609]. == Links When using the `*` and `_` characters at the end of URLs of external links in a quoted text, the attributes markers need to be explicitly set. Eg: `+++a link to *https://foo.com/_[]*+++`. Using the caret short-hand to indicate link targets should use the blank window is not support. +See https://github.com/bytesparadise/libasciidoc/issues/682[Issue #682]. == Document Types The inline and book document types are not supported. Article and manpage documents work fine. +See https://github.com/bytesparadise/libasciidoc/issues/628[Issue #628] and +https://github.com/bytesparadise/libasciidoc/issues/629[Issue #629]. == CSS At present no CSS is provided, but the output generated should be compatible with asciidoctor CSS. +See https://github.com/bytesparadise/libasciidoc/issues/63[Issue #63]. == Output Formats (Back-ends) @@ -154,3 +156,5 @@ Only HTML and XHTML backends are supported. == CLI Support for -d to set the document type is missing. +See https://github.com/bytesparadise/libasciidoc/issues/616[Issue #616]. + diff --git a/pkg/parser/table_test.go b/pkg/parser/table_test.go index ea00bc3b..c82676f5 100644 --- a/pkg/parser/table_test.go +++ b/pkg/parser/table_test.go @@ -18,6 +18,10 @@ var _ = Describe("tables", func() { expected := types.DraftDocument{ Blocks: []interface{}{ types.Table{ + Columns: []types.TableColumn{ + {Width: "50", VAlign: "top", HAlign: "left"}, + {Width: "50", VAlign: "top", HAlign: "left"}, + }, Lines: []types.TableLine{ { Cells: [][]interface{}{ @@ -63,6 +67,11 @@ var _ = Describe("tables", func() { expected := types.DraftDocument{ Blocks: []interface{}{ types.Table{ + Columns: []types.TableColumn{ + {Width: "33.3333", VAlign: "top", HAlign: "left"}, + {Width: "33.3333", VAlign: "top", HAlign: "left"}, + {Width: "33.3334", VAlign: "top", HAlign: "left"}, + }, Lines: []types.TableLine{ { Cells: [][]interface{}{ @@ -123,6 +132,11 @@ var _ = Describe("tables", func() { Attributes: types.Attributes{ types.AttrTitle: "table title", }, + Columns: []types.TableColumn{ + {Width: "50", HAlign: "left", VAlign: "top"}, + {Width: "50", HAlign: "left", VAlign: "top"}, + }, + Header: types.TableLine{ Cells: [][]interface{}{ { @@ -208,7 +222,11 @@ var _ = Describe("tables", func() { }, }, }, - + Columns: []types.TableColumn{ + // autowidth clears width + {Width: "", HAlign: "left", VAlign: "top"}, + {Width: "", HAlign: "left", VAlign: "top"}, + }, Lines: []types.TableLine{ { Cells: [][]interface{}{ @@ -249,6 +267,132 @@ var _ = Describe("tables", func() { expected := types.DraftDocument{ Blocks: []interface{}{ types.Table{ + Columns: []types.TableColumn{}, + Lines: []types.TableLine{}, + }, + }, + } + Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected)) + }) + + It("empty table with cols attr", func() { + source := "[cols=\"3,2,5\"]\n|===\n|===" + expected := types.DraftDocument{ + Blocks: []interface{}{ + types.Table{ + Attributes: types.Attributes{ + types.AttrCols: "3,2,5", + }, + Columns: []types.TableColumn{ + {Width: "30", HAlign: "left", VAlign: "top"}, + {Width: "20", HAlign: "left", VAlign: "top"}, + {Width: "50", HAlign: "left", VAlign: "top"}, + }, + Lines: []types.TableLine{}, + }, + }, + } + Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected)) + }) + + It("autowidth overrides column widths", func() { + source := "[%autowidth,cols=\"3,2,5\"]\n|===\n|===" + expected := types.DraftDocument{ + Blocks: []interface{}{ + types.Table{ + Attributes: types.Attributes{ + types.AttrOptions: map[string]bool{"autowidth": true}, + types.AttrCols: "3,2,5", + }, + Columns: []types.TableColumn{ + {Width: "", HAlign: "left", VAlign: "top"}, + {Width: "", HAlign: "left", VAlign: "top"}, + {Width: "", HAlign: "left", VAlign: "top"}, + }, + Lines: []types.TableLine{}, + }, + }, + } + Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected)) + }) + + It("column autowidth", func() { + source := "[cols=\"30,~,~\"]\n|===\n|===" + expected := types.DraftDocument{ + Blocks: []interface{}{ + types.Table{ + Attributes: types.Attributes{ + types.AttrCols: "30,~,~", + }, + Columns: []types.TableColumn{ + {Width: "30", HAlign: "left", VAlign: "top"}, + {Width: "", HAlign: "left", VAlign: "top"}, + {Width: "", HAlign: "left", VAlign: "top"}, + }, + Lines: []types.TableLine{}, + }, + }, + } + Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected)) + }) + + It("columns with repeat", func() { + source := "[cols=\"3*10,2*~\"]\n|===\n|===" + expected := types.DraftDocument{ + Blocks: []interface{}{ + types.Table{ + Attributes: types.Attributes{ + types.AttrCols: "3*10,2*~", + }, + Columns: []types.TableColumn{ + {Width: "10", HAlign: "left", VAlign: "top"}, + {Width: "10", HAlign: "left", VAlign: "top"}, + {Width: "10", HAlign: "left", VAlign: "top"}, + {Width: "", HAlign: "left", VAlign: "top"}, + {Width: "", HAlign: "left", VAlign: "top"}, + }, + Lines: []types.TableLine{}, + }, + }, + } + Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected)) + }) + It("columns with alignment changes", func() { + source := "[cols=\"2*^.^,<,.>\"]\n|===\n|===" + expected := types.DraftDocument{Blocks: []interface{}{ + types.Table{ + Attributes: types.Attributes{ + types.AttrCols: "2*^.^,<,.>", + }, + Columns: []types.TableColumn{ + {Width: "25", HAlign: "center", VAlign: "middle"}, + {Width: "25", HAlign: "center", VAlign: "middle"}, + {Width: "25", HAlign: "left", VAlign: "top"}, + {Width: "25", HAlign: "left", VAlign: "bottom"}, + }, + Lines: []types.TableLine{}, + }, + }, + } + Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected)) + }) + + // TODO: This checks that we parse the styles -- we don't actually do anything with them further yet. + It("columns with alignment changes and styles", func() { + source := "[cols=\"2*^.^d,s\"]\n|===\n|===" + expected := types.DraftDocument{ + FrontMatter: types.FrontMatter{Content: nil}, + Blocks: []interface{}{ + types.Table{ + Attributes: types.Attributes{ + types.AttrCols: "2*^.^d,s", + }, + Columns: []types.TableColumn{ + {Width: "25", HAlign: "center", VAlign: "middle"}, // "d" is aliased to "" + {Width: "25", HAlign: "center", VAlign: "middle"}, + {Width: "25", HAlign: "left", VAlign: "top", Style: "e"}, + {Width: "25", HAlign: "left", VAlign: "bottom", Style: "s"}, + }, Lines: []types.TableLine{}, }, }, diff --git a/pkg/renderer/sgml/html5/table.go b/pkg/renderer/sgml/html5/table.go index f5844dd1..1acd76f7 100644 --- a/pkg/renderer/sgml/html5/table.go +++ b/pkg/renderer/sgml/html5/table.go @@ -12,7 +12,9 @@ const ( "{{ if .Title }}{{ .Caption }}{{ .Title }}\n{{ end }}" + "{{ if .Body }}" + "\n" + - "{{ range $i, $w := .CellWidths }}\n{{ end}}" + + "{{ range $i, $w := .Columns }}\n{{ end}}" + "\n" + "{{ .Header }}" + "{{ .Body }}" + @@ -27,9 +29,7 @@ const ( tableCaptionTmpl = "Table {{ .TableNumber }}. " - // TODO: cell styling via attributes + tableHeaderCellTmpl = "{{ .Content }}\n" - tableHeaderCellTmpl = "{{ .Content }}\n" - - tableCellTmpl = "

{{ .Content }}

\n" + tableCellTmpl = "

{{ .Content }}

\n" ) diff --git a/pkg/renderer/sgml/html5/table_test.go b/pkg/renderer/sgml/html5/table_test.go index 03fb67e7..1bc363c9 100644 --- a/pkg/renderer/sgml/html5/table_test.go +++ b/pkg/renderer/sgml/html5/table_test.go @@ -264,4 +264,168 @@ var _ = Describe("tables", func() { Expect(RenderHTML(source)).To(MatchHTML(expected)) }) + It("table with cols relative widths", func() { + source := "[cols=\"3,2,5\"]\n|===\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + +

one

two

three

` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("table with cols relative widths and header", func() { + source := "[cols=\"3,2,5\"]\n|===\n|h1|h2|h3\n\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + + + + + + + + +
h1h2h3

one

two

three

` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("autowidth overrides column widths", func() { + source := "[%autowidth,cols=\"3,2,5\"]\n|===\n|h1|h2|h3\n\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + + + + + + + + +
h1h2h3

one

two

three

` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("column auto-width", func() { + source := "[cols=\"30,~,~\"]\n|===\n|h1|h2|h3\n\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + + + + + + + + +
h1h2h3

one

two

three

` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("columns with repeat", func() { + source := "[cols=\"3*10,2*~\"]\n|===\n|h1|h2|h3|h4|h5\n\n|one|two|three|four|five\n|===" + expected := ` +++++++ + + + + + + + + + + + + + + + + + + +
h1h2h3h4h5

one

two

three

four

five

` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + It("columns with alignment changes", func() { + // source := "[cols=\"2*^.^,<,.>,>\"]\n|===\n|===" + + source := "[cols=\"2*^.^,<,.>,>\"]\n|===\n|h1|h2|h3|h4|h5\n\n|one|two|three|four|five\n|===" + expected := ` +++++++ + + + + + + + + + + + + + + + + + + +
h1h2h3h4h5

one

two

three

four

five

` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + + // TODO: Verify styles -- it's verified in the parser for now, but we still need to implement styles. }) diff --git a/pkg/renderer/sgml/table.go b/pkg/renderer/sgml/table.go index 0e6d4e81..57433b04 100644 --- a/pkg/renderer/sgml/table.go +++ b/pkg/renderer/sgml/table.go @@ -1,39 +1,18 @@ package sgml import ( - "fmt" - "math" "strconv" "strings" "github.com/bytesparadise/libasciidoc/pkg/renderer" "github.com/bytesparadise/libasciidoc/pkg/types" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) func (r *sgmlRenderer) renderTable(ctx *renderer.Context, t types.Table) (string, error) { result := &strings.Builder{} caption := &strings.Builder{} - // inspect first line to obtain cell width ratio - widths := []string{} - if len(t.Lines) > 0 { - line := t.Lines[0] - n := len(line.Cells) - widths = make([]string, n) - total := 0.0 - for i := 0; i < n-1; i++ { - w := 100.0 / float64(n) - widths[i] = formatColumnWidth(w) - total += w - } - // last width - // int values don't need 4 decimals precision - - widths[n-1] = formatColumnWidth(100-total, lastColumn()) // make sure the last width as the upper rounded value - log.Debugf("current total width: %v -> %v", total, widths[n-1]) - } number := 0 title := r.renderElementTitle(t.Attributes) fit := "stretch" @@ -80,7 +59,7 @@ func (r *sgmlRenderer) renderTable(ctx *renderer.Context, t types.Table) (string } } - header, err := r.renderTableHeader(ctx, t.Header) + header, err := r.renderTableHeader(ctx, t.Header, t.Columns) if err != nil { return "", errors.Wrap(err, "failed to render table") } @@ -93,7 +72,7 @@ func (r *sgmlRenderer) renderTable(ctx *renderer.Context, t types.Table) (string err = r.table.Execute(result, struct { Context *renderer.Context Title sanitized - CellWidths []string + Columns []types.TableColumn TableNumber int Caption string Frame string @@ -108,7 +87,7 @@ func (r *sgmlRenderer) renderTable(ctx *renderer.Context, t types.Table) (string }{ Context: ctx, Title: r.renderElementTitle(t.Attributes), - CellWidths: widths, + Columns: t.Columns, TableNumber: number, Caption: caption.String(), Roles: r.renderElementRoles(t.Attributes), @@ -127,11 +106,13 @@ func (r *sgmlRenderer) renderTable(ctx *renderer.Context, t types.Table) (string return result.String(), nil } -func (r *sgmlRenderer) renderTableHeader(ctx *renderer.Context, l types.TableLine) (string, error) { +func (r *sgmlRenderer) renderTableHeader(ctx *renderer.Context, l types.TableLine, cols []types.TableColumn) (string, error) { result := &strings.Builder{} content := &strings.Builder{} + col := 0 for _, cell := range l.Cells { - c, err := r.renderTableHeaderCell(ctx, cell) + c, err := r.renderTableHeaderCell(ctx, cell, cols[col%len(cols)]) + col++ if err != nil { return "", errors.Wrap(err, "unable to render header") } @@ -149,7 +130,7 @@ func (r *sgmlRenderer) renderTableHeader(ctx *renderer.Context, l types.TableLin return result.String(), err } -func (r *sgmlRenderer) renderTableHeaderCell(ctx *renderer.Context, cell []interface{}) (string, error) { +func (r *sgmlRenderer) renderTableHeaderCell(ctx *renderer.Context, cell []interface{}, col types.TableColumn) (string, error) { result := &strings.Builder{} content, err := r.renderInlineElements(ctx, cell) if err != nil { @@ -159,10 +140,14 @@ func (r *sgmlRenderer) renderTableHeaderCell(ctx *renderer.Context, cell []inter Context *renderer.Context Content string Cell []interface{} + VAlign string + HAlign string }{ Context: ctx, Content: content, Cell: cell, + HAlign: col.HAlign, + VAlign: col.VAlign, }) return result.String(), err } @@ -171,7 +156,7 @@ func (r *sgmlRenderer) renderTableBody(ctx *renderer.Context, t types.Table) (st result := &strings.Builder{} content := &strings.Builder{} for _, row := range t.Lines { - c, err := r.renderTableRow(ctx, row) + c, err := r.renderTableRow(ctx, row, t.Columns) if err != nil { return "", errors.Wrap(err, "unable to render header") } @@ -181,19 +166,23 @@ func (r *sgmlRenderer) renderTableBody(ctx *renderer.Context, t types.Table) (st Context *renderer.Context Content string Rows []types.TableLine + Columns []types.TableColumn }{ Context: ctx, Content: content.String(), Rows: t.Lines, + Columns: t.Columns, }) return result.String(), err } -func (r *sgmlRenderer) renderTableRow(ctx *renderer.Context, l types.TableLine) (string, error) { +func (r *sgmlRenderer) renderTableRow(ctx *renderer.Context, l types.TableLine, cols []types.TableColumn) (string, error) { result := &strings.Builder{} content := &strings.Builder{} + col := 0 for _, cell := range l.Cells { - c, err := r.renderTableCell(ctx, cell) + c, err := r.renderTableCell(ctx, cell, cols[col%len(cols)]) + col++ if err != nil { return "", errors.Wrap(err, "unable to render header") } @@ -211,7 +200,7 @@ func (r *sgmlRenderer) renderTableRow(ctx *renderer.Context, l types.TableLine) return result.String(), err } -func (r *sgmlRenderer) renderTableCell(ctx *renderer.Context, cell []interface{}) (string, error) { +func (r *sgmlRenderer) renderTableCell(ctx *renderer.Context, cell []interface{}, col types.TableColumn) (string, error) { result := &strings.Builder{} content, err := r.renderInlineElements(ctx, cell) if err != nil { @@ -221,29 +210,14 @@ func (r *sgmlRenderer) renderTableCell(ctx *renderer.Context, cell []interface{} Context *renderer.Context Content string Cell []interface{} + HAlign string + VAlign string }{ Context: ctx, Content: content, Cell: cell, + HAlign: col.HAlign, + VAlign: col.VAlign, }) return result.String(), err } - -type formatColumnWidthOption func(float64) float64 - -func lastColumn() formatColumnWidthOption { - return func(v float64) float64 { - return v + 0.00005 - } -} - -func formatColumnWidth(v float64, options ...formatColumnWidthOption) string { - if v == math.Trunc(v) { - // whole numbers don't need 4 decimals - return strconv.Itoa(int(v)) - } - for _, opt := range options { - v = opt(v) - } - return fmt.Sprintf("%.4f", v) -} diff --git a/pkg/renderer/sgml/xhtml5/table.go b/pkg/renderer/sgml/xhtml5/table.go index 234f0259..4824cc13 100644 --- a/pkg/renderer/sgml/xhtml5/table.go +++ b/pkg/renderer/sgml/xhtml5/table.go @@ -12,7 +12,9 @@ const ( "{{ if .Title }}{{ .Caption }}{{ .Title }}\n{{ end }}" + "{{ if .Body }}" + "\n" + - "{{ range $i, $w := .CellWidths }}\n{{ end}}" + + "{{ range $i, $w := .Columns }}\n{{ end}}" + "\n" + "{{ .Header }}" + "{{ .Body }}" + diff --git a/pkg/renderer/sgml/xhtml5/table_test.go b/pkg/renderer/sgml/xhtml5/table_test.go index cf391d2c..eeec2df1 100644 --- a/pkg/renderer/sgml/xhtml5/table_test.go +++ b/pkg/renderer/sgml/xhtml5/table_test.go @@ -263,4 +263,169 @@ var _ = Describe("tables", func() { ` Expect(RenderXHTML(source)).To(MatchHTML(expected)) }) + + It("table with cols relative widths", func() { + source := "[cols=\"3,2,5\"]\n|===\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + +

one

two

three

` + Expect(RenderXHTML(source)).To(MatchHTML(expected)) + }) + + It("table with cols relative widths and header", func() { + source := "[cols=\"3,2,5\"]\n|===\n|h1|h2|h3\n\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + + + + + + + + +
h1h2h3

one

two

three

` + Expect(RenderXHTML(source)).To(MatchHTML(expected)) + }) + + It("autowidth overrides column widths", func() { + source := "[%autowidth,cols=\"3,2,5\"]\n|===\n|h1|h2|h3\n\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + + + + + + + + +
h1h2h3

one

two

three

` + Expect(RenderXHTML(source)).To(MatchHTML(expected)) + }) + + It("column auto-width", func() { + source := "[cols=\"30,~,~\"]\n|===\n|h1|h2|h3\n\n|one|two|three\n|===" + expected := ` +++++ + + + + + + + + + + + + + + +
h1h2h3

one

two

three

` + Expect(RenderXHTML(source)).To(MatchHTML(expected)) + }) + + It("columns with repeat", func() { + source := "[cols=\"3*10,2*~\"]\n|===\n|h1|h2|h3|h4|h5\n\n|one|two|three|four|five\n|===" + expected := ` +++++++ + + + + + + + + + + + + + + + + + + +
h1h2h3h4h5

one

two

three

four

five

` + Expect(RenderXHTML(source)).To(MatchHTML(expected)) + }) + + It("columns with alignment changes", func() { + // source := "[cols=\"2*^.^,<,.>,>\"]\n|===\n|===" + + source := "[cols=\"2*^.^,<,.>,>\"]\n|===\n|h1|h2|h3|h4|h5\n\n|one|two|three|four|five\n|===" + expected := ` +++++++ + + + + + + + + + + + + + + + + + + +
h1h2h3h4h5

one

two

three

four

five

` + Expect(RenderXHTML(source)).To(MatchHTML(expected)) + }) + + // TODO: Verify styles -- it's verified in the parser for now, but we still need to implement styles. }) diff --git a/pkg/types/attributes.go b/pkg/types/attributes.go index c82beec7..9cad684a 100644 --- a/pkg/types/attributes.go +++ b/pkg/types/attributes.go @@ -103,6 +103,8 @@ const ( AttrStripes = "stripes" // AttrFloat is for image or table float (text flows around) AttrFloat = "float" + // AttrCols the table columns attribute + AttrCols = "cols" // AttrPositional2 positional parameter 2 AttrPositional2 = "@2" // AttrPositional3 positional parameter 3 diff --git a/pkg/types/table.go b/pkg/types/table.go new file mode 100644 index 00000000..20d50092 --- /dev/null +++ b/pkg/types/table.go @@ -0,0 +1,277 @@ +package types + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// ------------------------------------------ +// Tables +// ------------------------------------------ + +type TableColumn struct { + widthVal float64 // internally used number, will be 0 for automatic, cleared post processing + Width string // percentage or relative (0 for automatic) + HAlign string // left, right, or center + VAlign string // top, bottom, or middle + Style string // Single character +} + +var defaultColumn = TableColumn{ + widthVal: 1, + HAlign: "left", + VAlign: "top", +} + +// Table the structure for the tables +type Table struct { + Attributes Attributes + Header TableLine + Columns []TableColumn + Lines []TableLine +} + +// parseNum like atoi, but stops on non-digit character (and only unsigned) +func (t Table) parseNum(c string) (int, string) { + n := 0 + for c != "" && c[0] >= '0' && c[0] <= '9' { + n *= 10 + n += int(c[0] - '0') + c = c[1:] + } + return n, c +} + +func (t Table) parseColumnsAttr() ([]TableColumn, error) { + attr, ok := t.Attributes.GetAsString(AttrCols) + if !ok { + return nil, nil + } + var cols []TableColumn + + for _, c := range strings.Split(attr, ",") { + c = strings.TrimSpace(c) + n := 0 + repeat := 1 + n, c = t.parseNum(c) + col := defaultColumn + if c == "" { + // If this was a number by itself, consider it a Width + col.widthVal = float64(n) + cols = append(cols, col) + continue + } + + if c[0] == '*' { + repeat = n + c = c[1:] + } else if n != 0 { + return nil, fmt.Errorf("bad column repeat (expected '*', got %s)", c) + } + + // Horizontal alignment + if c != "" { + switch c[0] { + case '<': + col.HAlign = "left" + c = c[1:] + case '>': + col.HAlign = "right" + c = c[1:] + case '^': + col.HAlign = "center" + c = c[1:] + } + } + + // Vertical alignment + if len(c) >= 2 && c[0] == '.' { + switch c[1] { + case '<': + col.VAlign = "top" + c = c[2:] + case '>': + col.VAlign = "bottom" + c = c[2:] + case '^': + col.VAlign = "middle" + c = c[2:] + default: + return nil, errors.New("bad column vertical alignment") + } + } + + // Width + if c != "" && c[0] == '~' { + col.widthVal = 0 // Auto-width + c = c[1:] + } else if c != "" && c[0] >= '0' && c[0] <= '9' { + n, c = t.parseNum(c) + col.widthVal = float64(n) + } + + // Style - must be the last item + switch c { + case "": // nothing left (no explicit style) + case "a", "e", "h", "l", "m", "s", "v": + col.Style = c + case "d": + col.Style = "" // leave default unset + default: + return nil, fmt.Errorf("bad column specification (%s unparsed)", c) + } + + for repeat > 0 { + cols = append(cols, col) + repeat-- + } + } + + return cols, nil +} + +func (t Table) processColumnWidths() ([]TableColumn, error) { + + widths := make([]float64, len(t.Columns)) + cols := make([]TableColumn, 0, len(t.Columns)) + + // Autowidth table uses full autowidth on all columns. + if !t.Attributes.HasOption("autowidth") { + + percent := false + total := 0.0 + for i, c := range t.Columns { + if c.widthVal == 0 { + percent = true + } + widths[i] = c.widthVal + total += c.widthVal + } + + if percent { + // Zero or more fixed percentages. + // At least one column automatically expanding to fill remainder. + if total > 100 { + return nil, fmt.Errorf("total widths (%f) cannot exceed 100%%", total) + } + } else { + // Relative widths, we have to calculate as percentages. + used := 0.0 + for i, v := range widths { + if i == len(widths)-1 { + // Last column uses remainder -- addresses rounding errors. + // (Also, faster, simpler math.) + v = 100 - used + } else { + // This rounds to nearest .001 percent. This allows us to + // use %.6g format in templates without precision loss. + v = v * 1000000 / total + v = math.Round(v) + v /= 10000 + } + used += v + widths[i] = v + } + } + } + + // This is a little more complex because we use pass by value. + for i, c := range t.Columns { + if widths[i] != 0 { + c.Width = strconv.FormatFloat(widths[i], 'g', 6, 64) + } + c.widthVal = 0 + cols = append(cols, c) + } + return cols, nil +} + +// NewTable initializes a new table with the given lines and attributes +func NewTable(header interface{}, lines []interface{}, attributes interface{}) (Table, error) { + attrs, err := NewAttributes(attributes) + if err != nil { + return Table{}, errors.Wrap(err, "failed to initialize a Table element") + } + t := Table{ + Attributes: attrs, + } + + if t.Columns, err = t.parseColumnsAttr(); err != nil { + return Table{}, errors.Wrap(err, "failed to initialize a Table element") + } + + if header, ok := header.(TableLine); ok { + t.Header = header + if t.Columns == nil { + // columns determined by our cell count here + for i := 0; i < len(header.Cells); i++ { + t.Columns = append(t.Columns, defaultColumn) + } + } + } + // need to regroup columns of all lines, they dispatch on lines + cells := make([][]interface{}, 0) + for _, l := range lines { + if l, ok := l.(TableLine); ok { + // if no header line was set, inspect the first line to determine the number of columns per line + if t.Columns == nil { + for i := 0; i < len(l.Cells); i++ { + t.Columns = append(t.Columns, defaultColumn) + } + } + cells = append(cells, l.Cells...) + } + } + + // Calculate the actual widths now + if t.Columns, err = t.processColumnWidths(); err != nil { + return Table{}, errors.Wrap(err, "failed to initialize a Table element") + } + + t.Lines = make([]TableLine, 0, len(cells)) + if len(lines) > 0 { + log.Debugf("buffered %d columns for the table", len(cells)) + l := TableLine{ + Cells: make([][]interface{}, len(t.Columns)), + } + for i, c := range cells { + log.Debugf("adding cell with content '%v' in table line at offset %d", c, i%len(t.Columns)) + l.Cells[i%len(t.Columns)] = c + if (i+1)%len(t.Columns) == 0 { // switch to next line + log.Debugf("adding line with content '%v' in table", l) + t.Lines = append(t.Lines, l) + l = TableLine{ + Cells: make([][]interface{}, len(t.Columns)), + } + } + } + } + // log.Debugf("initialized a new table with %d line(s)", len(lines)) + return t, nil +} + +// TableLine a table line is made of columns, each column being a group of []interface{} (to support quoted text, etc.) +type TableLine struct { + Cells [][]interface{} +} + +// NewTableLine initializes a new TableLine with the given columns +func NewTableLine(columns []interface{}) (TableLine, error) { + c := make([][]interface{}, 0) + for _, column := range columns { + if e, ok := column.([]interface{}); ok { + c = append(c, e) + } else { + return TableLine{}, errors.Errorf("unsupported element of type %T", column) + } + } + // log.Debugf("initialized a new table line with %d columns", len(c)) + return TableLine{ + Cells: c, + }, nil +} diff --git a/pkg/types/types.go b/pkg/types/types.go index 0c809977..ff9636c4 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -1603,85 +1603,6 @@ func (l *CalloutList) LastItem() ListItem { return &(l.Items[len(l.Items)-1]) } -// ------------------------------------------ -// Tables -// ------------------------------------------ - -// Table the structure for the tables -type Table struct { - Attributes Attributes - Header TableLine - Lines []TableLine -} - -// NewTable initializes a new table with the given lines and attributes -func NewTable(header interface{}, lines []interface{}, attributes interface{}) (Table, error) { - attrs, err := NewAttributes(attributes) - if err != nil { - return Table{}, errors.Wrapf(err, "failed to initialize a Table element") - } - t := Table{ - Attributes: attrs, - } - columnsPerLine := -1 // unknown until first "line" is processed - if header, ok := header.(TableLine); ok { - t.Header = header - columnsPerLine = len(header.Cells) - } - // need to regroup columns of all lines, they dispatch on lines - cells := make([][]interface{}, 0) - for _, l := range lines { - if l, ok := l.(TableLine); ok { - // if no header line was set, inspect the first line to determine the number of columns per line - if columnsPerLine == -1 { - columnsPerLine = len(l.Cells) - } - cells = append(cells, l.Cells...) - } - } - t.Lines = make([]TableLine, 0, len(cells)) - if len(lines) > 0 { - log.Debugf("buffered %d columns for the table", len(cells)) - l := TableLine{ - Cells: make([][]interface{}, columnsPerLine), - } - for i, c := range cells { - log.Debugf("adding cell with content '%v' in table line at offset %d", c, (i % columnsPerLine)) - l.Cells[i%columnsPerLine] = c - if (i+1)%columnsPerLine == 0 { // switch to next line - log.Debugf("adding line with content '%v' in table", l) - t.Lines = append(t.Lines, l) - l = TableLine{ - Cells: make([][]interface{}, columnsPerLine), - } - } - } - } - // log.Debugf("initialized a new table with %d line(s)", len(lines)) - return t, nil -} - -// TableLine a table line is made of columns, each column being a group of []interface{} (to support quoted text, etc.) -type TableLine struct { - Cells [][]interface{} -} - -// NewTableLine initializes a new TableLine with the given columns -func NewTableLine(columns []interface{}) (TableLine, error) { - c := make([][]interface{}, 0) - for _, column := range columns { - if e, ok := column.([]interface{}); ok { - c = append(c, e) - } else { - return TableLine{}, errors.Errorf("unsupported element of type %T", column) - } - } - // log.Debugf("initialized a new table line with %d columns", len(c)) - return TableLine{ - Cells: c, - }, nil -} - // ------------------------------------------ // Literal blocks // ------------------------------------------