From 54a0051900632975680a8ff5afe24741ca02e5e9 Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Tue, 2 Aug 2022 14:54:55 -0600 Subject: [PATCH 1/3] add support for colspan/rowspan in tables rdar://97739626 --- bin/main.c | 4 + .../include/cmark-gfm-core-extensions.h | 28 ++ extensions/table.c | 266 ++++++++++++++++-- src/include/cmark-gfm.h | 9 + src/node.c | 13 + test/extensions.txt | 2 +- test/spec.txt | 79 ++++++ 7 files changed, 372 insertions(+), 29 deletions(-) diff --git a/bin/main.c b/bin/main.c index 2bab06284..a37468422 100644 --- a/bin/main.c +++ b/bin/main.c @@ -62,6 +62,8 @@ void print_usage() { printf(" with two tildes\n"); printf(" --table-prefer-style-attributes Use style attributes to align table cells\n" " instead of align attributes.\n"); + printf(" --table-rowspan-ditto Use a double-quote 'ditto mark' to indicate\n" + " row span in tables instead of a caret.\n"); printf(" --full-info-string Include remainder of code block info\n" " string in a separate attribute.\n"); printf(" --help, -h Print usage information\n"); @@ -167,6 +169,8 @@ int main(int argc, char *argv[]) { options |= CMARK_OPT_FULL_INFO_STRING; } else if (strcmp(argv[i], "--table-prefer-style-attributes") == 0) { options |= CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES; + } else if (strcmp(argv[i], "--table-rowspan-ditto") == 0) { + options |= CMARK_OPT_TABLE_ROWSPAN_DITTO; } else if (strcmp(argv[i], "--strikethrough-double-tilde") == 0) { options |= CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE; } else if (strcmp(argv[i], "--sourcepos") == 0) { diff --git a/extensions/include/cmark-gfm-core-extensions.h b/extensions/include/cmark-gfm-core-extensions.h index 20da8d6e3..54ba232bf 100644 --- a/extensions/include/cmark-gfm-core-extensions.h +++ b/extensions/include/cmark-gfm-core-extensions.h @@ -32,6 +32,34 @@ int cmark_gfm_extensions_set_table_alignments(cmark_node *node, uint16_t ncols, CMARK_GFM_EXTENSIONS_EXPORT int cmark_gfm_extensions_get_table_row_is_header(cmark_node *node); +/** Sets the column span for the table cell, returning 1 on success and 0 on error. + */ +CMARK_GFM_EXTENSIONS_EXPORT +int cmark_gfm_extensions_set_table_cell_colspan(cmark_node *node, unsigned colspan); + +/** Sets the row span for the table cell, returning 1 on success and 0 on error. + */ +CMARK_GFM_EXTENSIONS_EXPORT +int cmark_gfm_extensions_set_table_cell_rowspan(cmark_node *node, unsigned rowspan); + +/** + Gets the column span for the table cell, returning \c UINT_MAX on error. + + A value of 0 indicates that the cell is a "filler" cell, intended to be overlapped with a previous + cell with a span > 1. + */ +CMARK_GFM_EXTENSIONS_EXPORT +unsigned cmark_gfm_extensions_get_table_cell_colspan(cmark_node *node); + +/** + Gets the row span for the table cell, returning \c UINT_MAX on error. + + A value of 0 indicates that the cell is a "filler" cell, intended to be overlapped with a previous + cell with a span > 1. + */ +CMARK_GFM_EXTENSIONS_EXPORT +unsigned cmark_gfm_extensions_get_table_cell_rowspan(cmark_node *node); + /** Sets whether the node is a table header row, returning 1 on success and 0 on error. */ CMARK_GFM_EXTENSIONS_EXPORT diff --git a/extensions/table.c b/extensions/table.c index b8f989efb..04a72995d 100644 --- a/extensions/table.c +++ b/extensions/table.c @@ -29,15 +29,22 @@ typedef struct { bool is_header; } node_table_row; +typedef struct { + unsigned colspan, rowspan; +} node_cell_data; + typedef struct { cmark_strbuf *buf; int start_offset, end_offset, internal_offset; + node_cell_data *cell_data; } node_cell; static void free_table_cell(cmark_mem *mem, void *data) { node_cell *cell = (node_cell *)data; cmark_strbuf_free((cmark_strbuf *)cell->buf); mem->free(cell->buf); + if (cell->cell_data) + mem->free(cell->cell_data); mem->free(cell); } @@ -60,6 +67,10 @@ static void free_node_table_row(cmark_mem *mem, void *ptr) { mem->free(ptr); } +static void free_node_table_cell_data(cmark_mem *mem, void *data) { + mem->free(data); +} + static int get_n_table_columns(cmark_node *node) { if (!node || node->type != CMARK_NODE_TABLE) return -1; @@ -90,6 +101,59 @@ static int set_table_alignments(cmark_node *node, uint8_t *alignments) { return 1; } +static unsigned get_cell_colspan(cmark_node *node) { + if (!node || node->type != CMARK_NODE_TABLE_CELL) + return UINT_MAX; + + node_cell_data *data = (node_cell_data *)node->as.opaque; + if (!data) + return 1; // default to 1 in case the cell was created as filler on an incomplete row + return data->colspan; +} + +static unsigned get_cell_rowspan(cmark_node *node) { + if (!node || node->type != CMARK_NODE_TABLE_CELL) + return UINT_MAX; + + node_cell_data *data = (node_cell_data *)node->as.opaque; + if (!data) + return 1; // default to 1 in case the cell was created as filler on an incomplete row + return data->rowspan; +} + +static int set_cell_colspan(cmark_node *node, unsigned colspan) { + if (!node || node->type != CMARK_NODE_TABLE_CELL) + return 0; + + node_cell_data *data = (node_cell_data *)node->as.opaque; + if (!data) + return 0; + data->colspan = colspan; + return 1; +} + +static int set_cell_rowspan(cmark_node *node, unsigned rowspan) { + if (!node || node->type != CMARK_NODE_TABLE_CELL) + return 0; + + node_cell_data *data = (node_cell_data *)node->as.opaque; + if (!data) + return 0; + data->rowspan = rowspan; + return 1; +} + +static int increment_cell_rowspan(cmark_node *node) { + if (!node || node->type != CMARK_NODE_TABLE_CELL) + return 0; + + node_cell_data *data = (node_cell_data *)node->as.opaque; + if (!data) + return 0; + ++data->rowspan; + return 1; +} + static cmark_strbuf *unescape_pipes(cmark_mem *mem, unsigned char *string, bufsize_t len) { cmark_strbuf *res = (cmark_strbuf *)mem->calloc(1, sizeof(cmark_strbuf)); @@ -155,13 +219,49 @@ static table_row *row_from_string(cmark_syntax_extension *self, node_cell *cell = (node_cell *)parser->mem->calloc(1, sizeof(*cell)); cell->buf = cell_buf; cell->start_offset = offset; - cell->end_offset = offset + cell_matched - 1; + if (cell_matched > 0) + cell->end_offset = offset + cell_matched - 1; + else + cell->end_offset = offset; while (cell->start_offset > 0 && string[cell->start_offset - 1] != '|') { --cell->start_offset; ++cell->internal_offset; } + cell->cell_data = (node_cell_data *)parser->mem->calloc(1, sizeof(node_cell_data)); + + // Check for a column-spanning cell + if (row->n_columns > 0 && cmark_strbuf_len(cell->buf) == 0 && cell->start_offset == cell->end_offset) { + cell->cell_data->colspan = 0; + + // find the last cell that isn't part of a colspan, and increment that colspan + cmark_llist *tmp = row->cells; + node_cell *colspan_cell = NULL; + while (tmp) { + node_cell *this_cell = (node_cell *)tmp->data; + if (this_cell->cell_data->colspan > 0) + colspan_cell = this_cell; + tmp = tmp->next; + } + if (colspan_cell) + ++colspan_cell->cell_data->colspan; + } else { + cell->cell_data->colspan = 1; + } + + // Check for a row-span marker. Actually incrementing the spanning cell's rowspan will happen later + cell->cell_data->rowspan = 1; + if (parser->options & CMARK_OPT_TABLE_ROWSPAN_DITTO) { + if (strcmp(cmark_strbuf_cstr(cell->buf), "\"") == 0) { + cell->cell_data->rowspan = 0; + } + } else { + if (strcmp(cmark_strbuf_cstr(cell->buf), "^") == 0) { + cell->cell_data->rowspan = 0; + } + } + // make sure we never wrap row->n_columns // offset will != len and our exit will clean up as intended if (row->n_columns == UINT16_MAX) { @@ -337,6 +437,8 @@ static cmark_node *try_opening_table_header(cmark_syntax_extension *self, header_cell->start_line = header_cell->end_line = parent_container->start_line; header_cell->internal_offset = cell->internal_offset; header_cell->end_column = parent_container->start_column + cell->end_offset; + header_cell->as.opaque = cell->cell_data; + cell->cell_data = NULL; cmark_node_set_string_content(header_cell, (char *) cell->buf->ptr); cmark_node_set_syntax_extension(header_cell, self); } @@ -377,9 +479,39 @@ static cmark_node *try_opening_table_row(cmark_syntax_extension *self, return NULL; } + // Check the new row for rowspan markers and increment the rowspan of the cell it's merging with + int table_columns = get_n_table_columns(parent_container); + { + cmark_llist *tmp; + int i; + + for (tmp = row->cells, i = 0; tmp && i < table_columns; tmp = tmp->next, ++i) { + node_cell *this_cell = (node_cell *)tmp->data; + if (this_cell->cell_data->rowspan == 0) { + // Rowspan marker. Scan up through previous rows and increment the spanning cell's rowspan + cmark_node *check_row = table_row_block->prev; + cmark_node *spanning_cell = NULL; + while (check_row && !spanning_cell) { + cmark_node *check_cell = cmark_node_nth_child(check_row, i); + unsigned check_rowspan = get_cell_rowspan(check_cell); + if (check_rowspan == 0) { + check_row = check_row->prev; + } else { + spanning_cell = check_cell; + } + } + if (spanning_cell) { + increment_cell_rowspan(spanning_cell); + // The rowspan marker cell still has the ^/" marker, clear it out so it won't display + cmark_strbuf_truncate(this_cell->buf, 0); + } + } + } + } + { cmark_llist *tmp; - int i, table_columns = get_n_table_columns(parent_container); + int i; for (tmp = row->cells, i = 0; tmp && i < table_columns; tmp = tmp->next, ++i) { node_cell *cell = (node_cell *) tmp->data; @@ -387,6 +519,8 @@ static cmark_node *try_opening_table_row(cmark_syntax_extension *self, CMARK_NODE_TABLE_CELL, parent_container->start_column + cell->start_offset); node->internal_offset = cell->internal_offset; node->end_column = parent_container->start_column + cell->end_offset; + node->as.opaque = cell->cell_data; + cell->cell_data = NULL; cmark_node_set_string_content(node, (char *) cell->buf->ptr); cmark_node_set_syntax_extension(node, self); } @@ -491,10 +625,24 @@ static void commonmark_render(cmark_syntax_extension *extension, renderer->out(renderer, node, "|", false, LITERAL); } } else if (node->type == CMARK_NODE_TABLE_CELL) { + unsigned colspan = get_cell_colspan(node); + unsigned rowspan = get_cell_rowspan(node); if (entering) { - renderer->out(renderer, node, " ", false, LITERAL); + if (colspan > 0) { + renderer->out(renderer, node, " ", false, LITERAL); + if (rowspan == 0) { + if (options & CMARK_OPT_TABLE_ROWSPAN_DITTO) { + renderer->out(renderer, node, "\"", false, LITERAL); + } else { + renderer->out(renderer, node, "^", false, LITERAL); + } + } + } } else { - renderer->out(renderer, node, " |", false, LITERAL); + if (colspan > 0) { + renderer->out(renderer, node, " ", false, LITERAL); + } + renderer->out(renderer, node, "|", false, LITERAL); if (((node_table_row *)node->parent->as.opaque)->is_header && !node->next) { int i; @@ -590,6 +738,22 @@ static const char *xml_attr(cmark_syntax_extension *extension, case 'c': return " align=\"center\""; case 'r': return " align=\"right\""; } + } else { + unsigned colspan = get_cell_colspan(node); + unsigned rowspan = get_cell_rowspan(node); + // XXX: The extension API doesn't allow you to return a dynamic string without leaking it, so + // specific column- and row-span information isn't printed + if (colspan == 0) { + return " colspan_filler"; + } else if (rowspan == 0) { + return " rowspan_filler"; + } else if (colspan > 1 && rowspan > 1) { + return " colspan rowspan"; + } else if (colspan > 1) { + return " colspan"; + } else if (rowspan > 1) { + return " rowspan"; + } } } @@ -663,6 +827,23 @@ static void html_table_add_align(cmark_strbuf* html, const char* align, int opti } } +static void html_table_add_spans(cmark_strbuf *html, unsigned colspan, unsigned rowspan) { + if (colspan > 1) { + char n[32]; + snprintf(n, sizeof(n), "%d", colspan); + cmark_strbuf_puts(html, " colspan=\""); + cmark_strbuf_puts(html, n); + cmark_strbuf_puts(html, "\""); + } + if (rowspan > 1) { + char n[32]; + snprintf(n, sizeof(n), "%d", rowspan); + cmark_strbuf_puts(html, " rowspan=\""); + cmark_strbuf_puts(html, n); + cmark_strbuf_puts(html, "\""); + } +} + struct html_table_state { unsigned need_closing_table_body : 1; unsigned in_table_header : 1; @@ -722,33 +903,40 @@ static void html_render(cmark_syntax_extension *extension, } } } else if (node->type == CMARK_NODE_TABLE_CELL) { - uint8_t *alignments = get_table_alignments(node->parent->parent); - if (entering) { - cmark_html_render_cr(html); - if (table_state->in_table_header) { - cmark_strbuf_puts(html, " 0 && rowspan > 0) { + uint8_t *alignments = get_table_alignments(node->parent->parent); + if (entering) { + cmark_html_render_cr(html); + if (table_state->in_table_header) { + cmark_strbuf_puts(html, "parent->first_child; n; n = n->next, ++i) - if (n == node) - break; + int i = 0; + for (n = node->parent->first_child; n; n = n->next, ++i) + if (n == node) + break; - switch (alignments[i]) { - case 'l': html_table_add_align(html, "left", options); break; - case 'c': html_table_add_align(html, "center", options); break; - case 'r': html_table_add_align(html, "right", options); break; - } + switch (alignments[i]) { + case 'l': html_table_add_align(html, "left", options); break; + case 'c': html_table_add_align(html, "center", options); break; + case 'r': html_table_add_align(html, "right", options); break; + } - cmark_html_render_sourcepos(node, html, options); - cmark_strbuf_putc(html, '>'); - } else { - if (table_state->in_table_header) { - cmark_strbuf_puts(html, ""); + html_table_add_spans(html, colspan, rowspan); + + cmark_html_render_sourcepos(node, html, options); + cmark_strbuf_putc(html, '>'); } else { - cmark_strbuf_puts(html, ""); + if (table_state->in_table_header) { + cmark_strbuf_puts(html, ""); + } else { + cmark_strbuf_puts(html, ""); + } } } } else { @@ -762,7 +950,7 @@ static void opaque_alloc(cmark_syntax_extension *self, cmark_mem *mem, cmark_nod } else if (node->type == CMARK_NODE_TABLE_ROW) { node->as.opaque = mem->calloc(1, sizeof(node_table_row)); } else if (node->type == CMARK_NODE_TABLE_CELL) { - node->as.opaque = mem->calloc(1, sizeof(node_cell)); + node->as.opaque = mem->calloc(1, sizeof(node_cell_data)); } } @@ -771,6 +959,8 @@ static void opaque_free(cmark_syntax_extension *self, cmark_mem *mem, cmark_node free_node_table(mem, node->as.opaque); } else if (node->type == CMARK_NODE_TABLE_ROW) { free_node_table_row(mem, node->as.opaque); + } else if (node->type == CMARK_NODE_TABLE_CELL) { + free_node_table_cell_data(mem, node->as.opaque); } } @@ -846,3 +1036,23 @@ int cmark_gfm_extensions_set_table_row_is_header(cmark_node *node, int is_header ((node_table_row *)node->as.opaque)->is_header = (is_header != 0); return 1; } + +unsigned cmark_gfm_extensions_get_table_cell_colspan(cmark_node *node) +{ + return get_cell_colspan(node); +} + +unsigned cmark_gfm_extensions_get_table_cell_rowspan(cmark_node *node) +{ + return get_cell_rowspan(node); +} + +int cmark_gfm_extensions_set_table_cell_colspan(cmark_node *node, unsigned colspan) +{ + return set_cell_colspan(node, colspan); +} + +int cmark_gfm_extensions_set_table_cell_rowspan(cmark_node *node, unsigned rowspan) +{ + return set_cell_rowspan(node, rowspan); +} diff --git a/src/include/cmark-gfm.h b/src/include/cmark-gfm.h index aee012955..d5503efc5 100644 --- a/src/include/cmark-gfm.h +++ b/src/include/cmark-gfm.h @@ -226,6 +226,10 @@ CMARK_GFM_EXPORT cmark_node *cmark_node_first_child(cmark_node *node); */ CMARK_GFM_EXPORT cmark_node *cmark_node_last_child(cmark_node *node); +/** Returns the N'th child of 'node', or NULL if 'node' does not have at least N children. + */ +CMARK_GFM_EXPORT cmark_node *cmark_node_nth_child(cmark_node *node, int n); + /** * ## Iterator * @@ -780,6 +784,11 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar */ #define CMARK_OPT_PRESERVE_WHITESPACE ((1 << 19) | CMARK_OPT_INLINE_ONLY) +/** Parse table cells defining row span using a double-quote symbol (`"`, or "ditto mark") + * instead of the default caret symbol (`^`). + */ +#define CMARK_OPT_TABLE_ROWSPAN_DITTO (1 << 20) + /** * ## Version information */ diff --git a/src/node.c b/src/node.c index 1d9b468cc..39074f27e 100644 --- a/src/node.c +++ b/src/node.c @@ -307,6 +307,19 @@ cmark_node *cmark_node_last_child(cmark_node *node) { } } +cmark_node *cmark_node_nth_child(cmark_node *node, int n) { + if (node == NULL) { + return NULL; + } + int i = 0; + cmark_node *ret = node->first_child; + while (ret && i < n) { + ret = ret->next; + ++i; + } + return ret; +} + void *cmark_node_get_user_data(cmark_node *node) { if (node == NULL) { return NULL; diff --git a/test/extensions.txt b/test/extensions.txt index 37033b1ca..7268e6b08 100644 --- a/test/extensions.txt +++ b/test/extensions.txt @@ -429,7 +429,7 @@ Here's a link to [Freedom Planet 2][]. ```````````````````````````````` example | a | b | c | | --- | --- | --- | -| d || e | +| d | | e | . diff --git a/test/spec.txt b/test/spec.txt index 582131d70..cc0161317 100644 --- a/test/spec.txt +++ b/test/spec.txt @@ -3513,6 +3513,85 @@ If there are no rows in the body, no `` is generated in HTML output:
```````````````````````````````` +Tables also support spanning rows and/or columns. To make a cell span multiple +columns in the same row, add "empty" cells to the end of the spanning cell: + +```````````````````````````````` example table +| one | two | +| --- | --- | +| hello || +. + + + + + + + + + + + + +
onetwo
hello
+```````````````````````````````` + +To make a cell span multiple rows, add a caret (`^`) to the cell(s) in following +rows that should be spanned over: + +```````````````````````````````` example table +| one | two | +| --- | ----- | +| big | small | +| ^ | small | +. + + + + + + + + + + + + + + + + +
onetwo
bigsmall
small
+```````````````````````````````` + +Column spans and row spans can also be combined: + +```````````````````````````````` example table +| one | two | three | +| --- | --- | ----- | +| big || small | +| ^ || small | +. + + + + + + + + + + + + + + + + + +
onetwothree
bigsmall
small
+```````````````````````````````` + # Container blocks From 99fe27ac64174f3669d8d0eaade4b312651a3656 Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Thu, 4 Aug 2022 09:16:55 -0600 Subject: [PATCH 2/3] review: reword rowspan comment for clarity --- extensions/table.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/table.c b/extensions/table.c index 04a72995d..51e661bbd 100644 --- a/extensions/table.c +++ b/extensions/table.c @@ -250,7 +250,7 @@ static table_row *row_from_string(cmark_syntax_extension *self, cell->cell_data->colspan = 1; } - // Check for a row-span marker. Actually incrementing the spanning cell's rowspan will happen later + // Check this cell for a row-span marker, so that the spanning cell's rowspan can be incremented later. cell->cell_data->rowspan = 1; if (parser->options & CMARK_OPT_TABLE_ROWSPAN_DITTO) { if (strcmp(cmark_strbuf_cstr(cell->buf), "\"") == 0) { From 6fef6496a9d3a45547636e1354c2c1c9728cedbf Mon Sep 17 00:00:00 2001 From: Victoria Mitchell Date: Tue, 13 Sep 2022 16:07:21 -0600 Subject: [PATCH 3/3] make colspan/rowspan optional --- api_test/main.c | 155 ++++++++++++++++++ bin/main.c | 4 + .../include/cmark-gfm-core-extensions.h | 4 + extensions/table.c | 63 +++---- src/include/cmark-gfm.h | 8 +- test/extensions.txt | 2 +- test/spec.txt | 79 --------- 7 files changed, 205 insertions(+), 110 deletions(-) diff --git a/api_test/main.c b/api_test/main.c index 2e497ce95..efed0fb60 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -1417,6 +1417,160 @@ static void parser_interrupt(test_batch_runner *runner) { cmark_syntax_extension_free(cmark_get_default_mem_allocator(), my_ext); } +static void compare_table_spans_html(test_batch_runner *runner, const char *markdown, bool use_ditto, + const char *expected_html, const char *msg) { + int options = CMARK_OPT_TABLE_SPANS; + if (use_ditto) + options |= CMARK_OPT_TABLE_ROWSPAN_DITTO; + cmark_parser *parser = cmark_parser_new(options); + cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("table")); + + cmark_parser_feed(parser, markdown, strlen(markdown)); + + cmark_node *doc = cmark_parser_finish(parser); + char *html = cmark_render_html(doc, options, NULL); + STR_EQ(runner, html, expected_html, msg); + + free(html); + cmark_node_free(doc); + cmark_parser_free(parser); +} + +static void table_spans(test_batch_runner *runner) { + { + static const char markdown[] = + "| one | two |\n" + "| --- | --- |\n" + "| hello ||\n"; + static const char html[] = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
onetwo
hello
\n"; + compare_table_spans_html(runner, markdown, false, html, + "table colspans should work when enabled"); + } + { + static const char markdown[] = + "| one | two |\n" + "| --- | ----- |\n" + "| big | small |\n" + "| ^ | small |\n"; + static const char html[] = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
onetwo
bigsmall
small
\n"; + compare_table_spans_html(runner, markdown, false, html, + "table rowspans should work when enabled"); + } + { + static const char markdown[] = + "| one | two |\n" + "| --- | ----- |\n" + "| big | small |\n" + "| \" | small |\n"; + static const char html[] = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
onetwo
bigsmall
small
\n"; + compare_table_spans_html(runner, markdown, true, html, + "rowspan ditto marks should work when enabled"); + } + { + static const char markdown[] = + "| one | two | three |\n" + "| --- | --- | ----- |\n" + "| big || small |\n" + "| ^ || small |\n"; + static const char html[] = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
onetwothree
bigsmall
small
\n"; + compare_table_spans_html(runner, markdown, false, html, + "colspan and rowspan should combine sensibly"); + } + { + static const char markdown[] = + "| one | two | three |\n" + "| --- | --- | ----- |\n" + "| big || small |\n" + "| \" || small |\n"; + static const char html[] = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
onetwothree
bigsmall
small
\n"; + compare_table_spans_html(runner, markdown, true, html, + "colspan and rowspan should combine when ditto marks are enabled"); + } +} + int main() { int retval; test_batch_runner *runner = test_batch_runner_new(); @@ -1452,6 +1606,7 @@ int main() { verify_custom_attributes_node(runner); verify_custom_attributes_node_with_footnote(runner); parser_interrupt(runner); + table_spans(runner); test_print_summary(runner); retval = test_ok(runner) ? 0 : 1; diff --git a/bin/main.c b/bin/main.c index a37468422..1c5aae847 100644 --- a/bin/main.c +++ b/bin/main.c @@ -62,6 +62,8 @@ void print_usage() { printf(" with two tildes\n"); printf(" --table-prefer-style-attributes Use style attributes to align table cells\n" " instead of align attributes.\n"); + printf(" --table-spans Enable parsing row- and column-span\n" + " in tables\n"); printf(" --table-rowspan-ditto Use a double-quote 'ditto mark' to indicate\n" " row span in tables instead of a caret.\n"); printf(" --full-info-string Include remainder of code block info\n" @@ -169,6 +171,8 @@ int main(int argc, char *argv[]) { options |= CMARK_OPT_FULL_INFO_STRING; } else if (strcmp(argv[i], "--table-prefer-style-attributes") == 0) { options |= CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES; + } else if (strcmp(argv[i], "--table-spans") == 0) { + options |= CMARK_OPT_TABLE_SPANS; } else if (strcmp(argv[i], "--table-rowspan-ditto") == 0) { options |= CMARK_OPT_TABLE_ROWSPAN_DITTO; } else if (strcmp(argv[i], "--strikethrough-double-tilde") == 0) { diff --git a/extensions/include/cmark-gfm-core-extensions.h b/extensions/include/cmark-gfm-core-extensions.h index 54ba232bf..d85a89237 100644 --- a/extensions/include/cmark-gfm-core-extensions.h +++ b/extensions/include/cmark-gfm-core-extensions.h @@ -47,6 +47,8 @@ int cmark_gfm_extensions_set_table_cell_rowspan(cmark_node *node, unsigned rowsp A value of 0 indicates that the cell is a "filler" cell, intended to be overlapped with a previous cell with a span > 1. + + Column span is only parsed when \c CMARK_OPT_TABLE_SPANS is set. */ CMARK_GFM_EXTENSIONS_EXPORT unsigned cmark_gfm_extensions_get_table_cell_colspan(cmark_node *node); @@ -56,6 +58,8 @@ unsigned cmark_gfm_extensions_get_table_cell_colspan(cmark_node *node); A value of 0 indicates that the cell is a "filler" cell, intended to be overlapped with a previous cell with a span > 1. + + Row span is only parsed when \c CMARK_OPT_TABLE_SPANS is set. */ CMARK_GFM_EXTENSIONS_EXPORT unsigned cmark_gfm_extensions_get_table_cell_rowspan(cmark_node *node); diff --git a/extensions/table.c b/extensions/table.c index 51e661bbd..1eeb6b7b8 100644 --- a/extensions/table.c +++ b/extensions/table.c @@ -229,37 +229,41 @@ static table_row *row_from_string(cmark_syntax_extension *self, ++cell->internal_offset; } - cell->cell_data = (node_cell_data *)parser->mem->calloc(1, sizeof(node_cell_data)); - - // Check for a column-spanning cell - if (row->n_columns > 0 && cmark_strbuf_len(cell->buf) == 0 && cell->start_offset == cell->end_offset) { - cell->cell_data->colspan = 0; - - // find the last cell that isn't part of a colspan, and increment that colspan - cmark_llist *tmp = row->cells; - node_cell *colspan_cell = NULL; - while (tmp) { - node_cell *this_cell = (node_cell *)tmp->data; - if (this_cell->cell_data->colspan > 0) - colspan_cell = this_cell; - tmp = tmp->next; + if (parser->options & CMARK_OPT_TABLE_SPANS) { + cell->cell_data = (node_cell_data *)parser->mem->calloc(1, sizeof(node_cell_data)); + + // Check for a column-spanning cell + if (row->n_columns > 0 && cmark_strbuf_len(cell->buf) == 0 && cell->start_offset == cell->end_offset) { + cell->cell_data->colspan = 0; + + // find the last cell that isn't part of a colspan, and increment that colspan + cmark_llist *tmp = row->cells; + node_cell *colspan_cell = NULL; + while (tmp) { + node_cell *this_cell = (node_cell *)tmp->data; + if (this_cell->cell_data->colspan > 0) + colspan_cell = this_cell; + tmp = tmp->next; + } + if (colspan_cell) + ++colspan_cell->cell_data->colspan; + } else { + cell->cell_data->colspan = 1; } - if (colspan_cell) - ++colspan_cell->cell_data->colspan; - } else { - cell->cell_data->colspan = 1; - } - // Check this cell for a row-span marker, so that the spanning cell's rowspan can be incremented later. - cell->cell_data->rowspan = 1; - if (parser->options & CMARK_OPT_TABLE_ROWSPAN_DITTO) { - if (strcmp(cmark_strbuf_cstr(cell->buf), "\"") == 0) { - cell->cell_data->rowspan = 0; + // Check this cell for a row-span marker, so that the spanning cell's rowspan can be incremented later. + cell->cell_data->rowspan = 1; + if (parser->options & CMARK_OPT_TABLE_ROWSPAN_DITTO) { + if (strcmp(cmark_strbuf_cstr(cell->buf), "\"") == 0) { + cell->cell_data->rowspan = 0; + } + } else { + if (strcmp(cmark_strbuf_cstr(cell->buf), "^") == 0) { + cell->cell_data->rowspan = 0; + } } } else { - if (strcmp(cmark_strbuf_cstr(cell->buf), "^") == 0) { - cell->cell_data->rowspan = 0; - } + cell->cell_data = NULL; } // make sure we never wrap row->n_columns @@ -479,9 +483,10 @@ static cmark_node *try_opening_table_row(cmark_syntax_extension *self, return NULL; } - // Check the new row for rowspan markers and increment the rowspan of the cell it's merging with int table_columns = get_n_table_columns(parent_container); - { + + if (parser->options & CMARK_OPT_TABLE_SPANS) { + // Check the new row for rowspan markers and increment the rowspan of the cell it's merging with cmark_llist *tmp; int i; diff --git a/src/include/cmark-gfm.h b/src/include/cmark-gfm.h index d5503efc5..4513a19a1 100644 --- a/src/include/cmark-gfm.h +++ b/src/include/cmark-gfm.h @@ -784,10 +784,16 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar */ #define CMARK_OPT_PRESERVE_WHITESPACE ((1 << 19) | CMARK_OPT_INLINE_ONLY) +/** Parse row- and column-span in tables. + */ +#define CMARK_OPT_TABLE_SPANS (1 << 20) + /** Parse table cells defining row span using a double-quote symbol (`"`, or "ditto mark") * instead of the default caret symbol (`^`). + * + * Does nothing unless \c CMARK_OPT_TABLE_SPANS is also set. */ -#define CMARK_OPT_TABLE_ROWSPAN_DITTO (1 << 20) +#define CMARK_OPT_TABLE_ROWSPAN_DITTO (1 << 21) /** * ## Version information diff --git a/test/extensions.txt b/test/extensions.txt index 7268e6b08..37033b1ca 100644 --- a/test/extensions.txt +++ b/test/extensions.txt @@ -429,7 +429,7 @@ Here's a link to [Freedom Planet 2][]. ```````````````````````````````` example | a | b | c | | --- | --- | --- | -| d | | e | +| d || e | . diff --git a/test/spec.txt b/test/spec.txt index cc0161317..582131d70 100644 --- a/test/spec.txt +++ b/test/spec.txt @@ -3513,85 +3513,6 @@ If there are no rows in the body, no `` is generated in HTML output:
```````````````````````````````` -Tables also support spanning rows and/or columns. To make a cell span multiple -columns in the same row, add "empty" cells to the end of the spanning cell: - -```````````````````````````````` example table -| one | two | -| --- | --- | -| hello || -. - - - - - - - - - - - - -
onetwo
hello
-```````````````````````````````` - -To make a cell span multiple rows, add a caret (`^`) to the cell(s) in following -rows that should be spanned over: - -```````````````````````````````` example table -| one | two | -| --- | ----- | -| big | small | -| ^ | small | -. - - - - - - - - - - - - - - - - -
onetwo
bigsmall
small
-```````````````````````````````` - -Column spans and row spans can also be combined: - -```````````````````````````````` example table -| one | two | three | -| --- | --- | ----- | -| big || small | -| ^ || small | -. - - - - - - - - - - - - - - - - - -
onetwothree
bigsmall
small
-```````````````````````````````` - # Container blocks