From 698a6d1ddfdea79c5c6faffc01a26bceaa5e94b9 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 9 Sep 2026 19:32:34 +1200 Subject: [PATCH 1/2] Add support for indented HTML blocks Assisted-By: devx/7ef83274-928f-499d-b668-48449b1ff60e --- api_test/main.c | 99 ++++++++++++++++++++++++++++++++++++++++++++ man/man1/cmark-gfm.1 | 4 ++ readme.md | 2 + src/blocks.c | 37 ++++++++++++++--- src/cmark-gfm.h | 6 +++ src/main.c | 3 ++ src/node.h | 7 +++- 7 files changed, 151 insertions(+), 7 deletions(-) diff --git a/api_test/main.c b/api_test/main.c index 4c490f50b..9e4c42867 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -1255,6 +1255,104 @@ static void test_front_matter(test_batch_runner *runner) { #undef PARSE } +static void test_indented_html_blocks(test_batch_runner *runner) { +#define PARSE(str, opts) cmark_parse_document(str, sizeof(str) - 1, opts) + + static const char nested_html[] = + "
\n" + "\t

one

\n" + "\n" + "

two

\n" + "
\n"; + + cmark_node *doc = PARSE(nested_html, CMARK_OPT_DEFAULT); + cmark_node *html = doc->first_child; + INT_EQ(runner, cmark_node_get_type(html), CMARK_NODE_HTML_BLOCK, + "indented HTML: default first node is HTML"); + OK(runner, html->next != NULL, + "indented HTML: default parsing ends at the blank line"); + cmark_node_free(doc); + + doc = PARSE(nested_html, CMARK_OPT_INDENTED_HTML_BLOCKS); + html = doc->first_child; + INT_EQ(runner, cmark_node_get_type(html), CMARK_NODE_HTML_BLOCK, + "indented HTML: flagged node is HTML"); + STR_EQ(runner, cmark_node_get_literal(html), nested_html, + "indented HTML: consistent indentation retains blank lines"); + OK(runner, html->next == NULL, + "indented HTML: consistent content remains one block"); + cmark_node_free(doc); + + static const char initially_blank[] = + "
\n" + "\n" + "\t

content

\n" + "
\n"; + + doc = PARSE(initially_blank, CMARK_OPT_INDENTED_HTML_BLOCKS); + html = doc->first_child; + STR_EQ(runner, cmark_node_get_literal(html), initially_blank, + "indented HTML: indentation may be established after a blank line"); + OK(runner, html->next == NULL, + "indented HTML: initially blank content remains one block"); + cmark_node_free(doc); + + static const char dedented[] = + "
\n" + "\tcontent\n" + "\n" + "outside\n"; + + doc = PARSE(dedented, CMARK_OPT_INDENTED_HTML_BLOCKS); + html = doc->first_child; + STR_EQ(runner, cmark_node_get_literal(html), "
\n\tcontent\n\n", + "indented HTML: retained blank line belongs to preceding HTML"); + INT_EQ(runner, cmark_node_get_type(html->next), CMARK_NODE_PARAGRAPH, + "indented HTML: dedented content terminates the block"); + cmark_node_free(doc); + + static const char inconsistent[] = + "
\n" + "\tcontent\n" + "\n" + " less indented\n"; + + doc = PARSE(inconsistent, CMARK_OPT_INDENTED_HTML_BLOCKS); + html = doc->first_child; + INT_EQ(runner, cmark_node_get_type(html->next), CMARK_NODE_PARAGRAPH, + "indented HTML: shallower indentation terminates the block"); + cmark_node_free(doc); + + static const char unindented[] = + "
\n" + "content\n" + "\n" + "more\n"; + + doc = PARSE(unindented, CMARK_OPT_INDENTED_HTML_BLOCKS); + html = doc->first_child; + OK(runner, html->next != NULL, + "indented HTML: unindented content still ends at a blank line"); + cmark_node_free(doc); + + static const char custom_element[] = + "\n" + " first\n" + "\n" + " nested\n" + "\n"; + + doc = PARSE(custom_element, CMARK_OPT_INDENTED_HTML_BLOCKS); + html = doc->first_child; + STR_EQ(runner, cmark_node_get_literal(html), custom_element, + "indented HTML: type 7 blocks and deeper indentation are supported"); + OK(runner, html->next == NULL, + "indented HTML: custom element remains one block"); + cmark_node_free(doc); + +#undef PARSE +} + static void test_feed_across_line_ending(test_batch_runner *runner) { // See #117 cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT); @@ -1487,6 +1585,7 @@ int main() { test_cplusplus(runner); test_safe(runner); test_front_matter(runner); + test_indented_html_blocks(runner); test_feed_across_line_ending(runner); test_pathological_regressions(runner); source_pos(runner); diff --git a/man/man1/cmark-gfm.1 b/man/man1/cmark-gfm.1 index c7457ed50..d3a4d9f53 100644 --- a/man/man1/cmark-gfm.1 +++ b/man/man1/cmark-gfm.1 @@ -62,6 +62,10 @@ be rendered as curly quotes, depending on their position. Parse language prefixes on inline code spans, such as \f[C]ruby:`Object.new`\f[], and expose the language identifier as code info. .TP 12n +.B \-\-indented-html-blocks +Allow indented content in type 6 and 7 HTML blocks to continue across blank +lines while its established indentation is preserved. +.TP 12n .B \-\-unsafe Render raw HTML and potentially dangerous URLs. (Raw HTML is not replaced by a placeholder comment; potentially diff --git a/readme.md b/readme.md index 7bf8fb9cb..e8b249df1 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,8 @@ Compared with GitHub's `cmark-gfm`, CMarkly: as `CMARK_NODE_FRONT_MATTER` nodes. - Supports opt-in language prefixes for inline code, such as `` ruby:`Object.new` ``, through `CMARK_OPT_INLINE_CODE_INFO` and the code-info accessors. +- Supports opt-in blank lines within consistently indented HTML content through + `CMARK_OPT_INDENTED_HTML_BLOCKS`. - Provides `cmark_node_clone` for independently cloning complete node trees, including supported extension metadata and footnote relationships. User data is intentionally not copied. diff --git a/src/blocks.c b/src/blocks.c index 9278a9840..c9fb5dea9 100644 --- a/src/blocks.c +++ b/src/blocks.c @@ -91,8 +91,8 @@ static CMARK_INLINE bool S_ends_on_current_line(cmark_parser *parser, cmark_node // similar to fenced code blocks. // Types 6-7 end at a blank line, so their last content line is // the previous line and they should NOT match here. - (S_type(b) == CMARK_NODE_HTML_BLOCK && b->as.html_block_type >= 1 && - b->as.html_block_type <= 5) || + (S_type(b) == CMARK_NODE_HTML_BLOCK && b->as.html_block.type >= 1 && + b->as.html_block.type <= 5) || // Single-line blocks: finalized on same line they started b->start_line == parser->line_number; } @@ -1037,7 +1037,7 @@ static bool parse_code_block_prefix(cmark_parser *parser, cmark_chunk *input, static bool parse_html_block_prefix(cmark_parser *parser, cmark_node *container) { bool res = false; - int html_block_type = container->as.html_block_type; + int html_block_type = container->as.html_block.type; assert(html_block_type >= 1 && html_block_type <= 7); switch (html_block_type) { @@ -1051,7 +1051,32 @@ static bool parse_html_block_prefix(cmark_parser *parser, break; case 6: case 7: - res = !parser->blank; + if (!(parser->options & CMARK_OPT_INDENTED_HTML_BLOCKS)) { + res = !parser->blank; + } else if (parser->blank) { + // Tentatively retain blank lines. The next nonblank line determines + // whether the HTML block continues: + res = true; + } else if (S_last_line_blank(container)) { + // Establish the content indentation lazily so a blank line may follow + // the opening tag. A non-indented line still terminates the block: + if (container->as.html_block.indent == 0 && parser->indent > 0) { + container->as.html_block.indent = parser->indent; + } + + res = container->as.html_block.indent > 0 && + parser->indent >= container->as.html_block.indent; + } else { + // Record the shallowest positive content indentation before a blank + // line. Deeper nested HTML may then continue without changing it: + if (parser->indent > 0 && + (container->as.html_block.indent == 0 || + parser->indent < container->as.html_block.indent)) { + container->as.html_block.indent = parser->indent; + } + + res = true; + } break; } @@ -1224,7 +1249,7 @@ static void open_new_blocks(cmark_parser *parser, cmark_node **container, input, parser->first_nonspace))))) { *container = add_child(parser, *container, CMARK_NODE_HTML_BLOCK, parser->first_nonspace + 1); - (*container)->as.html_block_type = matched; + (*container)->as.html_block.type = matched; // note, we don't adjust parser->offset because the tag is part of the // text } else if (!indented && cont_type == CMARK_NODE_PARAGRAPH && @@ -1421,7 +1446,7 @@ static void add_text_to_container(cmark_parser *parser, cmark_node *container, add_line(container, input, parser); int matches_end_condition; - switch (container->as.html_block_type) { + switch (container->as.html_block.type) { case 1: // , , matches_end_condition = diff --git a/src/cmark-gfm.h b/src/cmark-gfm.h index 054e981c7..a301a7ee3 100644 --- a/src/cmark-gfm.h +++ b/src/cmark-gfm.h @@ -798,6 +798,12 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar */ #define CMARK_OPT_INLINE_CODE_INFO (1 << 19) +/** Allow indented content in type 6 and 7 HTML blocks to continue across + * blank lines. The indentation established by the HTML content must be + * preserved after each blank line. + */ +#define CMARK_OPT_INDENTED_HTML_BLOCKS (1 << 20) + /** * ## Version information */ diff --git a/src/main.c b/src/main.c index 25885fdd9..ba44a8f09 100644 --- a/src/main.c +++ b/src/main.c @@ -65,6 +65,7 @@ void print_usage() { printf(" --full-info-string Include remainder of code block info\n" " string in a separate attribute.\n"); printf(" --inline-code-info Parse inline code language prefixes\n"); + printf(" --indented-html-blocks Continue indented HTML across blank lines\n"); printf(" --help, -h Print usage information\n"); printf(" --version Print version\n"); } @@ -168,6 +169,8 @@ int main(int argc, char *argv[]) { options |= CMARK_OPT_FULL_INFO_STRING; } else if (strcmp(argv[i], "--inline-code-info") == 0) { options |= CMARK_OPT_INLINE_CODE_INFO; + } else if (strcmp(argv[i], "--indented-html-blocks") == 0) { + options |= CMARK_OPT_INDENTED_HTML_BLOCKS; } else if (strcmp(argv[i], "--table-prefer-style-attributes") == 0) { options |= CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES; } else if (strcmp(argv[i], "--strikethrough-double-tilde") == 0) { diff --git a/src/node.h b/src/node.h index 73ca76053..7d800e888 100644 --- a/src/node.h +++ b/src/node.h @@ -33,6 +33,11 @@ typedef struct { int8_t fenced; } cmark_code; +typedef struct { + int type; + int indent; +} cmark_html_block; + typedef struct { int level; bool setext; @@ -104,7 +109,7 @@ struct cmark_node { cmark_heading heading; cmark_link link; cmark_custom custom; - int html_block_type; + cmark_html_block html_block; int cell_index; // For keeping track of TABLE_CELL table alignments void *opaque; } as; From fb37b70a381c8e047eee381c7aff2a68c29f9542 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 9 Sep 2026 19:44:14 +1200 Subject: [PATCH 2/2] Rename HTML block blank line option Assisted-By: devx/7ef83274-928f-499d-b668-48449b1ff60e --- api_test/main.c | 16 ++++++++-------- man/man1/cmark-gfm.1 | 2 +- readme.md | 2 +- src/blocks.c | 2 +- src/cmark-gfm.h | 2 +- src/main.c | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api_test/main.c b/api_test/main.c index 9e4c42867..0b4d1c4db 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -1255,7 +1255,7 @@ static void test_front_matter(test_batch_runner *runner) { #undef PARSE } -static void test_indented_html_blocks(test_batch_runner *runner) { +static void test_html_block_blank_lines(test_batch_runner *runner) { #define PARSE(str, opts) cmark_parse_document(str, sizeof(str) - 1, opts) static const char nested_html[] = @@ -1273,7 +1273,7 @@ static void test_indented_html_blocks(test_batch_runner *runner) { "indented HTML: default parsing ends at the blank line"); cmark_node_free(doc); - doc = PARSE(nested_html, CMARK_OPT_INDENTED_HTML_BLOCKS); + doc = PARSE(nested_html, CMARK_OPT_HTML_BLOCK_BLANK_LINES); html = doc->first_child; INT_EQ(runner, cmark_node_get_type(html), CMARK_NODE_HTML_BLOCK, "indented HTML: flagged node is HTML"); @@ -1289,7 +1289,7 @@ static void test_indented_html_blocks(test_batch_runner *runner) { "\t

content

\n" "
\n"; - doc = PARSE(initially_blank, CMARK_OPT_INDENTED_HTML_BLOCKS); + doc = PARSE(initially_blank, CMARK_OPT_HTML_BLOCK_BLANK_LINES); html = doc->first_child; STR_EQ(runner, cmark_node_get_literal(html), initially_blank, "indented HTML: indentation may be established after a blank line"); @@ -1303,7 +1303,7 @@ static void test_indented_html_blocks(test_batch_runner *runner) { "\n" "outside\n"; - doc = PARSE(dedented, CMARK_OPT_INDENTED_HTML_BLOCKS); + doc = PARSE(dedented, CMARK_OPT_HTML_BLOCK_BLANK_LINES); html = doc->first_child; STR_EQ(runner, cmark_node_get_literal(html), "
\n\tcontent\n\n", "indented HTML: retained blank line belongs to preceding HTML"); @@ -1317,7 +1317,7 @@ static void test_indented_html_blocks(test_batch_runner *runner) { "\n" " less indented\n"; - doc = PARSE(inconsistent, CMARK_OPT_INDENTED_HTML_BLOCKS); + doc = PARSE(inconsistent, CMARK_OPT_HTML_BLOCK_BLANK_LINES); html = doc->first_child; INT_EQ(runner, cmark_node_get_type(html->next), CMARK_NODE_PARAGRAPH, "indented HTML: shallower indentation terminates the block"); @@ -1329,7 +1329,7 @@ static void test_indented_html_blocks(test_batch_runner *runner) { "\n" "more\n"; - doc = PARSE(unindented, CMARK_OPT_INDENTED_HTML_BLOCKS); + doc = PARSE(unindented, CMARK_OPT_HTML_BLOCK_BLANK_LINES); html = doc->first_child; OK(runner, html->next != NULL, "indented HTML: unindented content still ends at a blank line"); @@ -1342,7 +1342,7 @@ static void test_indented_html_blocks(test_batch_runner *runner) { " nested\n" "\n"; - doc = PARSE(custom_element, CMARK_OPT_INDENTED_HTML_BLOCKS); + doc = PARSE(custom_element, CMARK_OPT_HTML_BLOCK_BLANK_LINES); html = doc->first_child; STR_EQ(runner, cmark_node_get_literal(html), custom_element, "indented HTML: type 7 blocks and deeper indentation are supported"); @@ -1585,7 +1585,7 @@ int main() { test_cplusplus(runner); test_safe(runner); test_front_matter(runner); - test_indented_html_blocks(runner); + test_html_block_blank_lines(runner); test_feed_across_line_ending(runner); test_pathological_regressions(runner); source_pos(runner); diff --git a/man/man1/cmark-gfm.1 b/man/man1/cmark-gfm.1 index d3a4d9f53..41fe53086 100644 --- a/man/man1/cmark-gfm.1 +++ b/man/man1/cmark-gfm.1 @@ -62,7 +62,7 @@ be rendered as curly quotes, depending on their position. Parse language prefixes on inline code spans, such as \f[C]ruby:`Object.new`\f[], and expose the language identifier as code info. .TP 12n -.B \-\-indented-html-blocks +.B \-\-html-block-blank-lines Allow indented content in type 6 and 7 HTML blocks to continue across blank lines while its established indentation is preserved. .TP 12n diff --git a/readme.md b/readme.md index e8b249df1..b317c10a7 100644 --- a/readme.md +++ b/readme.md @@ -26,7 +26,7 @@ Compared with GitHub's `cmark-gfm`, CMarkly: - Supports opt-in language prefixes for inline code, such as `` ruby:`Object.new` ``, through `CMARK_OPT_INLINE_CODE_INFO` and the code-info accessors. - Supports opt-in blank lines within consistently indented HTML content through - `CMARK_OPT_INDENTED_HTML_BLOCKS`. + `CMARK_OPT_HTML_BLOCK_BLANK_LINES`. - Provides `cmark_node_clone` for independently cloning complete node trees, including supported extension metadata and footnote relationships. User data is intentionally not copied. diff --git a/src/blocks.c b/src/blocks.c index c9fb5dea9..b481b3dec 100644 --- a/src/blocks.c +++ b/src/blocks.c @@ -1051,7 +1051,7 @@ static bool parse_html_block_prefix(cmark_parser *parser, break; case 6: case 7: - if (!(parser->options & CMARK_OPT_INDENTED_HTML_BLOCKS)) { + if (!(parser->options & CMARK_OPT_HTML_BLOCK_BLANK_LINES)) { res = !parser->blank; } else if (parser->blank) { // Tentatively retain blank lines. The next nonblank line determines diff --git a/src/cmark-gfm.h b/src/cmark-gfm.h index a301a7ee3..5a360d619 100644 --- a/src/cmark-gfm.h +++ b/src/cmark-gfm.h @@ -802,7 +802,7 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar * blank lines. The indentation established by the HTML content must be * preserved after each blank line. */ -#define CMARK_OPT_INDENTED_HTML_BLOCKS (1 << 20) +#define CMARK_OPT_HTML_BLOCK_BLANK_LINES (1 << 20) /** * ## Version information diff --git a/src/main.c b/src/main.c index ba44a8f09..bba1c496d 100644 --- a/src/main.c +++ b/src/main.c @@ -65,7 +65,7 @@ void print_usage() { printf(" --full-info-string Include remainder of code block info\n" " string in a separate attribute.\n"); printf(" --inline-code-info Parse inline code language prefixes\n"); - printf(" --indented-html-blocks Continue indented HTML across blank lines\n"); + printf(" --html-block-blank-lines Continue indented HTML across blank lines\n"); printf(" --help, -h Print usage information\n"); printf(" --version Print version\n"); } @@ -169,8 +169,8 @@ int main(int argc, char *argv[]) { options |= CMARK_OPT_FULL_INFO_STRING; } else if (strcmp(argv[i], "--inline-code-info") == 0) { options |= CMARK_OPT_INLINE_CODE_INFO; - } else if (strcmp(argv[i], "--indented-html-blocks") == 0) { - options |= CMARK_OPT_INDENTED_HTML_BLOCKS; + } else if (strcmp(argv[i], "--html-block-blank-lines") == 0) { + options |= CMARK_OPT_HTML_BLOCK_BLANK_LINES; } else if (strcmp(argv[i], "--table-prefer-style-attributes") == 0) { options |= CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES; } else if (strcmp(argv[i], "--strikethrough-double-tilde") == 0) {