\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;