diff --git a/api_test/main.c b/api_test/main.c index 4c490f50b..0b4d1c4db 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_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[] = + "
\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_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"); + 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_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"); + 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_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"); + 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_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"); + cmark_node_free(doc); + + static const char unindented[] = + "
\n" + "content\n" + "\n" + "more\n"; + + 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"); + cmark_node_free(doc); + + static const char custom_element[] = + "\n" + " first\n" + "\n" + " nested\n" + "\n"; + + 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"); + 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_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 c7457ed50..41fe53086 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 \-\-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 .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..b317c10a7 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_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 9278a9840..b481b3dec 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_HTML_BLOCK_BLANK_LINES)) { + 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..5a360d619 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_HTML_BLOCK_BLANK_LINES (1 << 20) + /** * ## Version information */ diff --git a/src/main.c b/src/main.c index 25885fdd9..bba1c496d 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(" --html-block-blank-lines 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], "--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) { 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;