From 6cd7a9dbf91164158ad144b389a31909b4b39573 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 13 Jul 2026 15:00:56 +1200 Subject: [PATCH 1/7] Add inline code info strings --- src/blocks.c | 2 ++ src/cmark-gfm.h | 9 ++++++ src/commonmark.c | 23 +++++++++++++++- src/html.c | 67 +++++++++++++++++++++++++++++---------------- src/inlines.c | 49 +++++++++++++++++++++++++++++++-- src/iterator.c | 5 +++- src/node.c | 25 ++++++++++++----- src/node.h | 1 + src/xml.c | 13 ++++++++- test/regression.txt | 34 +++++++++++++++++++++++ 10 files changed, 193 insertions(+), 35 deletions(-) diff --git a/src/blocks.c b/src/blocks.c index 9278a9840..b4d8f572c 100644 --- a/src/blocks.c +++ b/src/blocks.c @@ -372,6 +372,7 @@ static cmark_node *finalize(cmark_parser *parser, cmark_node *b) { cmark_strbuf_trim(&tmp); cmark_strbuf_unescape(&tmp); b->as.code.info = cmark_chunk_buf_detach(&tmp); + b->as.code.has_info = 1; if (node_content->ptr[pos] == '\r') pos += 1; @@ -1213,6 +1214,7 @@ static void open_new_blocks(cmark_parser *parser, cmark_node **container, (*container)->as.code.fence_offset = (int8_t)(parser->first_nonspace - parser->offset); (*container)->as.code.info = cmark_chunk_literal(""); + (*container)->as.code.has_info = 1; S_advance_offset(parser, input, parser->first_nonspace + matched - parser->offset, false); diff --git a/src/cmark-gfm.h b/src/cmark-gfm.h index 8848bf405..2c6ca6fae 100644 --- a/src/cmark-gfm.h +++ b/src/cmark-gfm.h @@ -425,6 +425,15 @@ CMARK_GFM_EXPORT int cmark_node_get_item_index(cmark_node *node); */ CMARK_GFM_EXPORT int cmark_node_set_item_index(cmark_node *node, int idx); +/** Returns the info string from a code block or code span. + */ +CMARK_GFM_EXPORT const char *cmark_node_get_code_info(cmark_node *node); + +/** Sets the info string in a code block or code span, returning 1 on + * success and 0 on failure. + */ +CMARK_GFM_EXPORT int cmark_node_set_code_info(cmark_node *node, const char *info); + /** Returns the info string from a fenced code block. */ CMARK_GFM_EXPORT const char *cmark_node_get_fence_info(cmark_node *node); diff --git a/src/commonmark.c b/src/commonmark.c index 3897f9b92..5175a4dd4 100644 --- a/src/commonmark.c +++ b/src/commonmark.c @@ -115,6 +115,16 @@ static int shortest_unused_backtick_sequence(const char *code) { return (int)i; } +static bool code_info_needs_quotes(const char *info) { + while (*info) { + if (cmark_isspace((unsigned char)*info)) { + return true; + } + info++; + } + return false; +} + static bool is_autolink(cmark_node *node) { cmark_chunk *title; cmark_chunk *url; @@ -271,7 +281,7 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, if (!first_in_list_item) { BLANKLINE(); } - info = cmark_node_get_fence_info(node); + info = cmark_node_get_code_info(node); info_len = strlen(info); fencechar[0] = strchr(info, '`') == NULL ? '`' : '~'; code = cmark_node_get_literal(node); @@ -376,6 +386,17 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, for (i = 0; i < numticks; i++) { LIT("`"); } + if (node->as.code.has_info) { + info = cmark_node_get_code_info(node); + LIT(":"); + if (code_info_needs_quotes(info)) { + LIT("\""); + OUT(info, false, LITERAL); + LIT("\""); + } else { + OUT(info, false, LITERAL); + } + } break; case CMARK_NODE_HTML_INLINE: diff --git a/src/html.c b/src/html.c index a3ab5f59d..37bad6127 100644 --- a/src/html.c +++ b/src/html.c @@ -18,6 +18,34 @@ static void escape_html(cmark_strbuf *dest, const unsigned char *source, houdini_escape_html0(dest, source, length, 0); } +static bufsize_t first_code_info_tag(cmark_chunk *info) { + bufsize_t first_tag = 0; + while (first_tag < info->len && !cmark_isspace(info->data[first_tag])) { + first_tag += 1; + } + return first_tag; +} + +static void render_code_info_attrs(cmark_strbuf *html, cmark_chunk *info, + int options, bool pre_lang) { + bufsize_t first_tag = first_code_info_tag(info); + + if (pre_lang) { + cmark_strbuf_puts(html, " lang=\""); + } else { + cmark_strbuf_puts(html, " class=\"language-"); + } + + escape_html(html, info->data, first_tag); + + if (first_tag < info->len && (options & CMARK_OPT_FULL_INFO_STRING)) { + cmark_strbuf_puts(html, "\" data-meta=\""); + escape_html(html, info->data + first_tag + 1, info->len - first_tag - 1); + } + + cmark_strbuf_putc(html, '"'); +} + static void filter_html_block(cmark_html_renderer *renderer, uint8_t *data, size_t len) { cmark_strbuf *html = renderer->html; cmark_llist *it; @@ -125,7 +153,11 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node, case CMARK_NODE_TEXT: case CMARK_NODE_CODE: case CMARK_NODE_HTML_INLINE: - escape_html(html, node->as.literal.data, node->as.literal.len); + if (node->type == CMARK_NODE_CODE) { + escape_html(html, node->as.code.literal.data, node->as.code.literal.len); + } else { + escape_html(html, node->as.literal.data, node->as.literal.len); + } break; case CMARK_NODE_LINEBREAK: @@ -220,32 +252,17 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node, cmark_html_render_sourcepos(node, html, options); cmark_strbuf_puts(html, ">"); } else { - bufsize_t first_tag = 0; - while (first_tag < node->as.code.info.len && - !cmark_isspace(node->as.code.info.data[first_tag])) { - first_tag += 1; - } - if (options & CMARK_OPT_GITHUB_PRE_LANG) { cmark_strbuf_puts(html, "as.code.info.data, first_tag); - if (first_tag < node->as.code.info.len && (options & CMARK_OPT_FULL_INFO_STRING)) { - cmark_strbuf_puts(html, "\" data-meta=\""); - escape_html(html, node->as.code.info.data + first_tag + 1, node->as.code.info.len - first_tag - 1); - } - cmark_strbuf_puts(html, "\">"); + render_code_info_attrs(html, &node->as.code.info, options, true); + cmark_strbuf_puts(html, ">"); } else { cmark_strbuf_puts(html, "as.code.info.data, first_tag); - if (first_tag < node->as.code.info.len && (options & CMARK_OPT_FULL_INFO_STRING)) { - cmark_strbuf_puts(html, "\" data-meta=\""); - escape_html(html, node->as.code.info.data + first_tag + 1, node->as.code.info.len - first_tag - 1); - } - cmark_strbuf_puts(html, "\">"); + cmark_strbuf_puts(html, ">as.code.info, options, false); + cmark_strbuf_putc(html, '>'); } } @@ -327,8 +344,12 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node, break; case CMARK_NODE_CODE: - cmark_strbuf_puts(html, ""); - escape_html(html, node->as.literal.data, node->as.literal.len); + cmark_strbuf_puts(html, "as.code.has_info) { + render_code_info_attrs(html, &node->as.code.info, options, false); + } + cmark_strbuf_putc(html, '>'); + escape_html(html, node->as.code.literal.data, node->as.code.literal.len); cmark_strbuf_puts(html, ""); break; diff --git a/src/inlines.c b/src/inlines.c index 30f2c1700..251d9a1c5 100644 --- a/src/inlines.c +++ b/src/inlines.c @@ -24,7 +24,6 @@ static const char *RIGHTSINGLEQUOTE = "\xE2\x80\x99"; // Macros for creating various kinds of simple. #define make_str(subj, sc, ec, s) make_literal(subj, CMARK_NODE_TEXT, sc, ec, s) -#define make_code(subj, sc, ec, s) make_literal(subj, CMARK_NODE_CODE, sc, ec, s) #define make_raw_html(subj, sc, ec, s) make_literal(subj, CMARK_NODE_HTML_INLINE, sc, ec, s) #define make_linebreak(mem) make_simple(mem, CMARK_NODE_LINEBREAK) #define make_softbreak(mem) make_simple(mem, CMARK_NODE_SOFTBREAK) @@ -104,6 +103,18 @@ static CMARK_INLINE cmark_node *make_simple(cmark_mem *mem, cmark_node_type t) { return e; } +static CMARK_INLINE cmark_node *make_code(subject *subj, int start_column, + int end_column, cmark_chunk literal) { + cmark_node *e = (cmark_node *)subj->mem->calloc(1, sizeof(*e)); + cmark_strbuf_init(subj->mem, &e->content, 0); + e->type = CMARK_NODE_CODE; + e->as.code.literal = literal; + e->start_line = e->end_line = subj->line; + e->start_column = start_column + 1 + subj->column_offset + subj->block_offset; + e->end_column = end_column + 1 + subj->column_offset + subj->block_offset; + return e; +} + // Like make_str, but parses entities. static cmark_node *make_str_with_entities(subject *subj, int start_column, int end_column, @@ -385,6 +396,39 @@ static void S_normalize_code(cmark_strbuf *s) { } +static CMARK_INLINE int is_inline_code_info_char(int c) { + return cmark_isalnum(c) || c == '_' || c == '-' || c == '+' || c == '#' || + c == '.'; +} + +static void parse_inline_code_info(subject *subj, cmark_node *node) { + if (peek_char(subj) != ':') { + return; + } + + advance(subj); + node->as.code.has_info = 1; + + if (peek_char(subj) == '"') { + cmark_strbuf buf = CMARK_BUF_INIT(subj->mem); + advance(subj); + + while (!is_eof(subj) && peek_char(subj) != '"' && + !S_is_line_end_char(peek_char(subj))) { + cmark_strbuf_putc(&buf, peek_char(subj)); + advance(subj); + } + + if (peek_char(subj) == '"') { + advance(subj); + } + + node->as.code.info = cmark_chunk_buf_detach(&buf); + } else { + node->as.code.info = take_while(subj, is_inline_code_info_char); + } +} + // Parse backtick code section or raw backticks, return an inline. // Assumes that the subject has a backtick at the current position. @@ -404,7 +448,8 @@ static cmark_node *handle_backticks(subject *subj, int options) { S_normalize_code(&buf); cmark_node *node = make_code(subj, startpos, endpos - openticks.len - 1, cmark_chunk_buf_detach(&buf)); - adjust_subj_node_newlines(subj, node, endpos - startpos, openticks.len, options); + parse_inline_code_info(subj, node); + adjust_subj_node_newlines(subj, node, subj->pos - startpos, openticks.len, options); return node; } } diff --git a/src/iterator.c b/src/iterator.c index 13fdb7616..850ac89ab 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -139,10 +139,13 @@ void cmark_node_own(cmark_node *root) { switch (cur->type) { case CMARK_NODE_TEXT: case CMARK_NODE_HTML_INLINE: - case CMARK_NODE_CODE: case CMARK_NODE_HTML_BLOCK: cmark_chunk_to_cstr(iter->mem, &cur->as.literal); break; + case CMARK_NODE_CODE: + cmark_chunk_to_cstr(iter->mem, &cur->as.code.literal); + cmark_chunk_to_cstr(iter->mem, &cur->as.code.info); + break; case CMARK_NODE_LINK: cmark_chunk_to_cstr(iter->mem, &cur->as.link.url); cmark_chunk_to_cstr(iter->mem, &cur->as.link.title); diff --git a/src/node.c b/src/node.c index e8c503409..41c417c8a 100644 --- a/src/node.c +++ b/src/node.c @@ -149,12 +149,12 @@ static void free_node_as(cmark_node *node) { switch (node->type) { case CMARK_NODE_CODE_BLOCK: case CMARK_NODE_FRONT_MATTER: + case CMARK_NODE_CODE: cmark_chunk_free(NODE_MEM(node), &node->as.code.info); cmark_chunk_free(NODE_MEM(node), &node->as.code.literal); break; case CMARK_NODE_TEXT: case CMARK_NODE_HTML_INLINE: - case CMARK_NODE_CODE: case CMARK_NODE_HTML_BLOCK: case CMARK_NODE_FOOTNOTE_REFERENCE: case CMARK_NODE_FOOTNOTE_DEFINITION: @@ -378,11 +378,11 @@ const char *cmark_node_get_literal(cmark_node *node) { case CMARK_NODE_HTML_BLOCK: case CMARK_NODE_TEXT: case CMARK_NODE_HTML_INLINE: - case CMARK_NODE_CODE: case CMARK_NODE_FOOTNOTE_REFERENCE: case CMARK_NODE_FOOTNOTE_DEFINITION: return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.literal); + case CMARK_NODE_CODE: case CMARK_NODE_CODE_BLOCK: case CMARK_NODE_FRONT_MATTER: return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.literal); @@ -403,11 +403,11 @@ int cmark_node_set_literal(cmark_node *node, const char *content) { case CMARK_NODE_HTML_BLOCK: case CMARK_NODE_TEXT: case CMARK_NODE_HTML_INLINE: - case CMARK_NODE_CODE: case CMARK_NODE_FOOTNOTE_REFERENCE: cmark_chunk_set_cstr(NODE_MEM(node), &node->as.literal, content); return 1; + case CMARK_NODE_CODE: case CMARK_NODE_CODE_BLOCK: case CMARK_NODE_FRONT_MATTER: cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.literal, content); @@ -595,12 +595,13 @@ int cmark_node_set_item_index(cmark_node *node, int idx) { } } -const char *cmark_node_get_fence_info(cmark_node *node) { +const char *cmark_node_get_code_info(cmark_node *node) { if (node == NULL) { return NULL; } - if (node->type == CMARK_NODE_CODE_BLOCK || + if (node->type == CMARK_NODE_CODE || + node->type == CMARK_NODE_CODE_BLOCK || node->type == CMARK_NODE_FRONT_MATTER) { return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.info); } else { @@ -608,20 +609,30 @@ const char *cmark_node_get_fence_info(cmark_node *node) { } } -int cmark_node_set_fence_info(cmark_node *node, const char *info) { +int cmark_node_set_code_info(cmark_node *node, const char *info) { if (node == NULL) { return 0; } - if (node->type == CMARK_NODE_CODE_BLOCK || + if (node->type == CMARK_NODE_CODE || + node->type == CMARK_NODE_CODE_BLOCK || node->type == CMARK_NODE_FRONT_MATTER) { cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info); + node->as.code.has_info = info != NULL; return 1; } else { return 0; } } +const char *cmark_node_get_fence_info(cmark_node *node) { + return cmark_node_get_code_info(node); +} + +int cmark_node_set_fence_info(cmark_node *node, const char *info) { + return cmark_node_set_code_info(node, info); +} + int cmark_node_get_fenced(cmark_node *node, int *length, int *offset, char *character) { if (node == NULL) { return 0; diff --git a/src/node.h b/src/node.h index 73ca76053..22059eccc 100644 --- a/src/node.h +++ b/src/node.h @@ -30,6 +30,7 @@ typedef struct { uint8_t fence_length; uint8_t fence_offset; unsigned char fence_char; + uint8_t has_info; int8_t fenced; } cmark_code; diff --git a/src/xml.c b/src/xml.c index 6358e3dfb..698cfa0be 100644 --- a/src/xml.c +++ b/src/xml.c @@ -72,7 +72,6 @@ static int S_render_node(cmark_node *node, cmark_event_type ev_type, literal = true; break; case CMARK_NODE_TEXT: - case CMARK_NODE_CODE: case CMARK_NODE_HTML_BLOCK: case CMARK_NODE_HTML_INLINE: cmark_strbuf_puts(xml, " xml:space=\"preserve\">"); @@ -81,6 +80,18 @@ static int S_render_node(cmark_node *node, cmark_event_type ev_type, cmark_strbuf_puts(xml, cmark_node_get_type_string(node)); literal = true; break; + case CMARK_NODE_CODE: + if (node->as.code.has_info) { + cmark_strbuf_puts(xml, " info=\""); + escape_xml(xml, node->as.code.info.data, node->as.code.info.len); + cmark_strbuf_putc(xml, '"'); + } + cmark_strbuf_puts(xml, " xml:space=\"preserve\">"); + escape_xml(xml, node->as.code.literal.data, node->as.code.literal.len); + cmark_strbuf_puts(xml, "line2

```````````````````````````````` +Inline code spans may carry unquoted info strings. + +```````````````````````````````` example +Ruby code: `Object.new`:ruby +. +

Ruby code: Object.new

+```````````````````````````````` + +Inline code spans may carry quoted extended info strings. + +```````````````````````````````` example +Ruby code with extended fence info: `Object.new`:"ruby lineno=5" +. +

Ruby code with extended fence info: Object.new

+```````````````````````````````` + +Inline code spans may carry empty info strings. + +```````````````````````````````` example +Empty info: `x`: +. +

Empty info: x

+```````````````````````````````` + +Unterminated quoted inline code info strings stop at the line ending. + +```````````````````````````````` example +Before `x`:"ruby lineno=5 +after +. +

Before x +after

+```````````````````````````````` + Issue #114: cmark skipping first character in line (Important: the blank lines around "Repeatedly" contain a tab.) From ea35b475d1b5792ff2327a2b3bae1444c08e8353 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 13 Jul 2026 15:06:02 +1200 Subject: [PATCH 2/7] Ignore empty inline code info --- src/inlines.c | 3 ++- test/regression.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/inlines.c b/src/inlines.c index 251d9a1c5..1518b9d4d 100644 --- a/src/inlines.c +++ b/src/inlines.c @@ -407,7 +407,6 @@ static void parse_inline_code_info(subject *subj, cmark_node *node) { } advance(subj); - node->as.code.has_info = 1; if (peek_char(subj) == '"') { cmark_strbuf buf = CMARK_BUF_INIT(subj->mem); @@ -427,6 +426,8 @@ static void parse_inline_code_info(subject *subj, cmark_node *node) { } else { node->as.code.info = take_while(subj, is_inline_code_info_char); } + + node->as.code.has_info = node->as.code.info.len > 0; } diff --git a/test/regression.txt b/test/regression.txt index 53b8d74c4..de085c6c2 100644 --- a/test/regression.txt +++ b/test/regression.txt @@ -32,7 +32,7 @@ Inline code spans may carry empty info strings. ```````````````````````````````` example Empty info: `x`: . -

Empty info: x

+

Empty info: x

```````````````````````````````` Unterminated quoted inline code info strings stop at the line ending. From b1526ace7d3a61ff55bc875bafb071d428e35a72 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 13 Jul 2026 15:22:49 +1200 Subject: [PATCH 3/7] Gate inline code info behind option --- src/cmark-gfm.h | 5 +++++ src/inlines.c | 4 +++- src/main.c | 3 +++ test/CMakeLists.txt | 8 +++++++- test/inline-code-info.txt | 35 +++++++++++++++++++++++++++++++++++ test/regression.txt | 30 ++---------------------------- 6 files changed, 55 insertions(+), 30 deletions(-) create mode 100644 test/inline-code-info.txt diff --git a/src/cmark-gfm.h b/src/cmark-gfm.h index 2c6ca6fae..3ca9dc257 100644 --- a/src/cmark-gfm.h +++ b/src/cmark-gfm.h @@ -785,6 +785,11 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar */ #define CMARK_OPT_FRONT_MATTER (1 << 18) +/** Parse inline code info strings, e.g. `code`:ruby or + * `code`:"ruby lineno=5". + */ +#define CMARK_OPT_INLINE_CODE_INFO (1 << 19) + /** * ## Version information */ diff --git a/src/inlines.c b/src/inlines.c index 1518b9d4d..69a3f6a17 100644 --- a/src/inlines.c +++ b/src/inlines.c @@ -449,7 +449,9 @@ static cmark_node *handle_backticks(subject *subj, int options) { S_normalize_code(&buf); cmark_node *node = make_code(subj, startpos, endpos - openticks.len - 1, cmark_chunk_buf_detach(&buf)); - parse_inline_code_info(subj, node); + if (options & CMARK_OPT_INLINE_CODE_INFO) { + parse_inline_code_info(subj, node); + } adjust_subj_node_newlines(subj, node, subj->pos - startpos, openticks.len, options); return node; } diff --git a/src/main.c b/src/main.c index a62c4f2ca..4d008d3ec 100644 --- a/src/main.c +++ b/src/main.c @@ -64,6 +64,7 @@ void print_usage() { " instead of align attributes.\n"); printf(" --full-info-string Include remainder of code block info\n" " string in a separate attribute.\n"); + printf(" --inline-code-info Parse inline code info strings\n"); printf(" --help, -h Print usage information\n"); printf(" --version Print version\n"); } @@ -165,6 +166,8 @@ int main(int argc, char *argv[]) { goto success; } else if (strcmp(argv[i], "--full-info-string") == 0) { 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], "--table-prefer-style-attributes") == 0) { options |= CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES; } else if (strcmp(argv[i], "--strikethrough-double-tilde") == 0) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2b5c99bc5..0c79168bf 100755 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -96,6 +96,13 @@ IF (PYTHONINTERP_FOUND) "--program" "${CMAKE_CURRENT_BINARY_DIR}/../src/cmark-gfm --full-info-string" ) + add_test(option_inline_code_info + ${PYTHON_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/roundtrip_tests.py" + "--spec" "${CMAKE_CURRENT_SOURCE_DIR}/inline-code-info.txt" + "--program" "${CMAKE_CURRENT_BINARY_DIR}/../src/cmark-gfm --inline-code-info" + ) + add_test(regressiontest_executable ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/spec_tests.py" "--no-normalize" "--spec" @@ -111,4 +118,3 @@ ELSE(PYTHONINTERP_FOUND) echo "Skipping spec tests, because no python 3 interpreter is available.") ENDIF(PYTHONINTERP_FOUND) - diff --git a/test/inline-code-info.txt b/test/inline-code-info.txt new file mode 100644 index 000000000..12f468567 --- /dev/null +++ b/test/inline-code-info.txt @@ -0,0 +1,35 @@ +### Inline code info strings + +Inline code spans may carry unquoted info strings. + +```````````````````````````````` example +Ruby code: `Object.new`:ruby +. +

Ruby code: Object.new

+```````````````````````````````` + +Inline code spans may carry quoted extended info strings. + +```````````````````````````````` example +Ruby code with extended fence info: `Object.new`:"ruby lineno=5" +. +

Ruby code with extended fence info: Object.new

+```````````````````````````````` + +Inline code spans may carry empty info strings, which are normalized away. + +```````````````````````````````` example +Empty info: `x`: +. +

Empty info: x

+```````````````````````````````` + +Unterminated quoted inline code info strings stop at the line ending. + +```````````````````````````````` example +Before `x`:"ruby lineno=5 +after +. +

Before x +after

+```````````````````````````````` diff --git a/test/regression.txt b/test/regression.txt index de085c6c2..a87b26803 100644 --- a/test/regression.txt +++ b/test/regression.txt @@ -11,38 +11,12 @@ line2

line2

```````````````````````````````` -Inline code spans may carry unquoted info strings. +Inline code info strings are not parsed unless explicitly enabled. ```````````````````````````````` example Ruby code: `Object.new`:ruby . -

Ruby code: Object.new

-```````````````````````````````` - -Inline code spans may carry quoted extended info strings. - -```````````````````````````````` example -Ruby code with extended fence info: `Object.new`:"ruby lineno=5" -. -

Ruby code with extended fence info: Object.new

-```````````````````````````````` - -Inline code spans may carry empty info strings. - -```````````````````````````````` example -Empty info: `x`: -. -

Empty info: x

-```````````````````````````````` - -Unterminated quoted inline code info strings stop at the line ending. - -```````````````````````````````` example -Before `x`:"ruby lineno=5 -after -. -

Before x -after

+

Ruby code: Object.new:ruby

```````````````````````````````` Issue #114: cmark skipping first character in line From 7bde15ec139cffbd0d89621282c1db593b4fe3c7 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 20 Aug 2026 12:16:22 +1200 Subject: [PATCH 4/7] Prefer language prefixes for inline code Assisted-By: devx/c76f68ef-c5ab-4096-b170-fc6974ad760d --- api_test/main.c | 59 ++++++++++++++++++++++ changelog.txt | 4 ++ src/blocks.c | 2 - src/cmark-gfm.h | 12 +++-- src/commonmark.c | 25 ++-------- src/html.c | 2 +- src/inlines.c | 87 +++++++++++++++++++++++---------- src/iterator.c | 4 +- src/main.c | 2 +- src/node.c | 56 ++++++++++++++++++--- src/node.h | 1 - src/xml.c | 2 +- test/CMakeLists.txt | 7 +++ test/inline-code-info-smart.txt | 14 ++++++ test/inline-code-info.txt | 46 +++++++++++------ test/regression.txt | 6 +-- 16 files changed, 248 insertions(+), 81 deletions(-) create mode 100644 test/inline-code-info-smart.txt diff --git a/api_test/main.c b/api_test/main.c index 68412f335..f611e8c87 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -120,6 +120,7 @@ static void accessors(test_batch_runner *runner) { STR_EQ(runner, cmark_node_get_literal(fenced), "fenced\n", "get_literal fenced code"); STR_EQ(runner, cmark_node_get_fence_info(fenced), "lang", "get_fence_info"); + STR_EQ(runner, cmark_node_get_code_info(fenced), "lang", "get_code_info"); cmark_node *code = cmark_node_next(fenced); STR_EQ(runner, cmark_node_get_literal(code), "code\n", @@ -163,6 +164,8 @@ static void accessors(test_batch_runner *runner) { OK(runner, cmark_node_set_literal(fenced, "FENCED\n"), "set_literal fenced code"); OK(runner, cmark_node_set_fence_info(fenced, "LANG"), "set_fence_info"); + STR_EQ(runner, cmark_node_get_code_info(fenced), "LANG", + "get updated code info"); OK(runner, cmark_node_set_literal(html, "
HTML
\n"), "set_literal html"); @@ -243,6 +246,61 @@ static void accessors(test_batch_runner *runner) { cmark_node_free(doc); } +static void inline_code_info(test_batch_runner *runner) { + static const char markdown[] = "ruby:`Object.new`"; + cmark_node *doc = cmark_parse_document(markdown, sizeof(markdown) - 1, + CMARK_OPT_INLINE_CODE_INFO); + cmark_node *paragraph = cmark_node_first_child(doc); + cmark_node *code = cmark_node_first_child(paragraph); + + INT_EQ(runner, cmark_node_get_type(code), CMARK_NODE_CODE, + "inline code language node type"); + STR_EQ(runner, cmark_node_get_literal(code), "Object.new", + "inline code language literal"); + STR_EQ(runner, cmark_node_get_code_info(code), "ruby", + "get inline code info"); + + OK(runner, cmark_node_get_fence_info(code) == NULL, + "fence info rejects inline code"); + OK(runner, !cmark_node_set_fence_info(code, "c++"), + "set fence info rejects inline code"); + OK(runner, cmark_node_get_code_info(paragraph) == NULL, + "code info rejects paragraph"); + OK(runner, !cmark_node_set_code_info(paragraph, "ruby"), + "set code info rejects paragraph"); + + char *html = cmark_render_html(doc, CMARK_OPT_DEFAULT, NULL); + STR_EQ(runner, html, + "

Object.new

\n", + "render inline code language"); + free(html); + + char *commonmark = cmark_render_commonmark(doc, CMARK_OPT_DEFAULT, 0); + STR_EQ(runner, commonmark, "ruby:`Object.new`\n", + "render inline code language as commonmark"); + free(commonmark); + + OK(runner, !cmark_node_set_code_info(code, "ruby lineno=5"), + "reject invalid inline code info"); + STR_EQ(runner, cmark_node_get_code_info(code), "ruby", + "preserve inline code info after invalid set"); + + OK(runner, cmark_node_set_code_info(code, "c++"), "set inline code info"); + STR_EQ(runner, cmark_node_get_code_info(code), "c++", + "get updated inline code info"); + + OK(runner, cmark_node_set_code_info(code, ""), "clear inline code info"); + html = cmark_render_html(doc, CMARK_OPT_DEFAULT, NULL); + STR_EQ(runner, html, "

Object.new

\n", + "render inline code without empty language class"); + free(html); + + OK(runner, cmark_node_set_code_info(code, NULL), + "clear inline code info with null"); + + cmark_node_free(doc); +} + static void node_check(test_batch_runner *runner) { // Construct an incomplete tree. cmark_node *doc = cmark_node_new(CMARK_NODE_DOCUMENT); @@ -1200,6 +1258,7 @@ int main() { version(runner); constructor(runner); accessors(runner); + inline_code_info(runner); node_check(runner); iterator(runner); iterator_delete(runner); diff --git a/changelog.txt b/changelog.txt index 77837e85f..0b653c07e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,9 @@ [unreleased] + * Add `CMARK_OPT_INLINE_CODE_INFO`: parse language-prefixed inline code such + as ``ruby:`Object.new``` and render the language as a `language-ruby` + class. + * Add `CMARK_OPT_FRONT_MATTER`: parse a "---" delimited block at the start of a document and expose it as a `CMARK_NODE_FRONT_MATTER` node. An optional info string (e.g. "--- yaml") records the content format; diff --git a/src/blocks.c b/src/blocks.c index b4d8f572c..9278a9840 100644 --- a/src/blocks.c +++ b/src/blocks.c @@ -372,7 +372,6 @@ static cmark_node *finalize(cmark_parser *parser, cmark_node *b) { cmark_strbuf_trim(&tmp); cmark_strbuf_unescape(&tmp); b->as.code.info = cmark_chunk_buf_detach(&tmp); - b->as.code.has_info = 1; if (node_content->ptr[pos] == '\r') pos += 1; @@ -1214,7 +1213,6 @@ static void open_new_blocks(cmark_parser *parser, cmark_node **container, (*container)->as.code.fence_offset = (int8_t)(parser->first_nonspace - parser->offset); (*container)->as.code.info = cmark_chunk_literal(""); - (*container)->as.code.has_info = 1; S_advance_offset(parser, input, parser->first_nonspace + matched - parser->offset, false); diff --git a/src/cmark-gfm.h b/src/cmark-gfm.h index 3ca9dc257..c0fe68678 100644 --- a/src/cmark-gfm.h +++ b/src/cmark-gfm.h @@ -425,12 +425,15 @@ CMARK_GFM_EXPORT int cmark_node_get_item_index(cmark_node *node); */ CMARK_GFM_EXPORT int cmark_node_set_item_index(cmark_node *node, int idx); -/** Returns the info string from a code block or code span. +/** Returns the info string from a code block or the language identifier from + * an inline code span. */ CMARK_GFM_EXPORT const char *cmark_node_get_code_info(cmark_node *node); -/** Sets the info string in a code block or code span, returning 1 on - * success and 0 on failure. +/** Sets the info string in a code block or the language identifier on an + * inline code span, returning 1 on success and 0 on failure. Inline code + * language identifiers must begin with an alphanumeric character and may + * additionally contain '_', '-', '+', '#', and '.'. */ CMARK_GFM_EXPORT int cmark_node_set_code_info(cmark_node *node, const char *info); @@ -785,8 +788,7 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar */ #define CMARK_OPT_FRONT_MATTER (1 << 18) -/** Parse inline code info strings, e.g. `code`:ruby or - * `code`:"ruby lineno=5". +/** Parse inline code language prefixes, e.g. ruby:`code`. */ #define CMARK_OPT_INLINE_CODE_INFO (1 << 19) diff --git a/src/commonmark.c b/src/commonmark.c index 5175a4dd4..80095e296 100644 --- a/src/commonmark.c +++ b/src/commonmark.c @@ -115,16 +115,6 @@ static int shortest_unused_backtick_sequence(const char *code) { return (int)i; } -static bool code_info_needs_quotes(const char *info) { - while (*info) { - if (cmark_isspace((unsigned char)*info)) { - return true; - } - info++; - } - return false; -} - static bool is_autolink(cmark_node *node) { cmark_chunk *title; cmark_chunk *url; @@ -373,6 +363,10 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, extra_spaces = code_len == 0 || code[0] == '`' || code[code_len - 1] == '`' || code[0] == ' ' || code[code_len - 1] == ' '; + if (node->as.code.info.len > 0) { + OUT(cmark_node_get_code_info(node), false, LITERAL); + LIT(":"); + } for (i = 0; i < numticks; i++) { LIT("`"); } @@ -386,17 +380,6 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, for (i = 0; i < numticks; i++) { LIT("`"); } - if (node->as.code.has_info) { - info = cmark_node_get_code_info(node); - LIT(":"); - if (code_info_needs_quotes(info)) { - LIT("\""); - OUT(info, false, LITERAL); - LIT("\""); - } else { - OUT(info, false, LITERAL); - } - } break; case CMARK_NODE_HTML_INLINE: diff --git a/src/html.c b/src/html.c index 37bad6127..104a61835 100644 --- a/src/html.c +++ b/src/html.c @@ -345,7 +345,7 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node, case CMARK_NODE_CODE: cmark_strbuf_puts(html, "as.code.has_info) { + if (node->as.code.info.len > 0) { render_code_info_attrs(html, &node->as.code.info, options, false); } cmark_strbuf_putc(html, '>'); diff --git a/src/inlines.c b/src/inlines.c index 69a3f6a17..5acf816c5 100644 --- a/src/inlines.c +++ b/src/inlines.c @@ -401,36 +401,38 @@ static CMARK_INLINE int is_inline_code_info_char(int c) { c == '.'; } -static void parse_inline_code_info(subject *subj, cmark_node *node) { - if (peek_char(subj) != ':') { - return; +static bufsize_t scan_inline_code_info_prefix(subject *subj, bufsize_t start) { + if (start >= subj->input.len || !cmark_isalnum(subj->input.data[start]) || + (start > 0 && is_inline_code_info_char(subj->input.data[start - 1]))) { + return 0; } - advance(subj); + bufsize_t pos = start + 1; + while (pos < subj->input.len && + is_inline_code_info_char(subj->input.data[pos])) { + pos++; + } - if (peek_char(subj) == '"') { - cmark_strbuf buf = CMARK_BUF_INIT(subj->mem); - advance(subj); + if (pos + 1 < subj->input.len && subj->input.data[pos] == ':' && + subj->input.data[pos + 1] == '`') { + return pos; + } - while (!is_eof(subj) && peek_char(subj) != '"' && - !S_is_line_end_char(peek_char(subj))) { - cmark_strbuf_putc(&buf, peek_char(subj)); - advance(subj); - } + return 0; +} - if (peek_char(subj) == '"') { - advance(subj); +static bufsize_t find_inline_code_info_prefix(subject *subj, bufsize_t start, + bufsize_t limit) { + while (start < limit) { + if (scan_inline_code_info_prefix(subj, start)) { + return start; } - - node->as.code.info = cmark_chunk_buf_detach(&buf); - } else { - node->as.code.info = take_while(subj, is_inline_code_info_char); + start++; } - node->as.code.has_info = node->as.code.info.len > 0; + return 0; } - // Parse backtick code section or raw backticks, return an inline. // Assumes that the subject has a backtick at the current position. static cmark_node *handle_backticks(subject *subj, int options) { @@ -448,15 +450,35 @@ static cmark_node *handle_backticks(subject *subj, int options) { endpos - startpos - openticks.len); S_normalize_code(&buf); - cmark_node *node = make_code(subj, startpos, endpos - openticks.len - 1, cmark_chunk_buf_detach(&buf)); - if (options & CMARK_OPT_INLINE_CODE_INFO) { - parse_inline_code_info(subj, node); - } - adjust_subj_node_newlines(subj, node, subj->pos - startpos, openticks.len, options); + cmark_node *node = make_code(subj, startpos, endpos - openticks.len - 1, + cmark_chunk_buf_detach(&buf)); + adjust_subj_node_newlines(subj, node, endpos - startpos, openticks.len, + options); return node; } } +static cmark_node *handle_inline_code_info(subject *subj, int options) { + bufsize_t startpos = subj->pos; + bufsize_t colonpos = scan_inline_code_info_prefix(subj, startpos); + if (colonpos == 0) { + return NULL; + } + + cmark_chunk info = + cmark_chunk_dup(&subj->input, startpos, colonpos - startpos); + subj->pos = colonpos + 1; + + cmark_node *node = handle_backticks(subj, options); + if (node->type != CMARK_NODE_CODE) { + cmark_node_free(node); + subj->pos = startpos; + return NULL; + } + + node->as.code.info = chunk_clone(subj->mem, &info); + return node; +} // Scan ***, **, or * and return number scanned, or 0. // Advances position. @@ -1506,6 +1528,14 @@ static int parse_inline(cmark_parser *parser, subject *subj, cmark_node *parent, if (c == 0) { return 0; } + if (options & CMARK_OPT_INLINE_CODE_INFO) { + new_inl = handle_inline_code_info(subj, options); + } + if (new_inl != NULL) { + append_child(parent, new_inl); + return 1; + } + switch (c) { case '\r': case '\n': @@ -1559,6 +1589,13 @@ static int parse_inline(cmark_parser *parser, subject *subj, cmark_node *parent, break; endpos = subject_find_special_char(subj, options); + if (options & CMARK_OPT_INLINE_CODE_INFO) { + bufsize_t info_start = + find_inline_code_info_prefix(subj, subj->pos + 1, endpos); + if (info_start > 0 && info_start < endpos) { + endpos = info_start; + } + } contents = cmark_chunk_dup(&subj->input, subj->pos, endpos - subj->pos); startpos = subj->pos; subj->pos = endpos; diff --git a/src/iterator.c b/src/iterator.c index 850ac89ab..edd24901b 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -144,7 +144,9 @@ void cmark_node_own(cmark_node *root) { break; case CMARK_NODE_CODE: cmark_chunk_to_cstr(iter->mem, &cur->as.code.literal); - cmark_chunk_to_cstr(iter->mem, &cur->as.code.info); + if (cur->as.code.info.len > 0) { + cmark_chunk_to_cstr(iter->mem, &cur->as.code.info); + } break; case CMARK_NODE_LINK: cmark_chunk_to_cstr(iter->mem, &cur->as.link.url); diff --git a/src/main.c b/src/main.c index 4d008d3ec..25885fdd9 100644 --- a/src/main.c +++ b/src/main.c @@ -64,7 +64,7 @@ void print_usage() { " instead of align attributes.\n"); printf(" --full-info-string Include remainder of code block info\n" " string in a separate attribute.\n"); - printf(" --inline-code-info Parse inline code info strings\n"); + printf(" --inline-code-info Parse inline code language prefixes\n"); printf(" --help, -h Print usage information\n"); printf(" --version Print version\n"); } diff --git a/src/node.c b/src/node.c index 41c417c8a..b92df47a5 100644 --- a/src/node.c +++ b/src/node.c @@ -609,16 +609,41 @@ const char *cmark_node_get_code_info(cmark_node *node) { } } +static int valid_inline_code_info(const char *info) { + if (info == NULL || *info == '\0') { + return 1; + } + + if (!cmark_isalnum((unsigned char)*info)) { + return 0; + } + + while (*++info) { + unsigned char c = (unsigned char)*info; + if (!(cmark_isalnum(c) || c == '_' || c == '-' || c == '+' || c == '#' || + c == '.')) { + return 0; + } + } + + return 1; +} + int cmark_node_set_code_info(cmark_node *node, const char *info) { if (node == NULL) { return 0; } - if (node->type == CMARK_NODE_CODE || - node->type == CMARK_NODE_CODE_BLOCK || - node->type == CMARK_NODE_FRONT_MATTER) { + if (node->type == CMARK_NODE_CODE) { + if (!valid_inline_code_info(info)) { + return 0; + } + + cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info); + return 1; + } else if (node->type == CMARK_NODE_CODE_BLOCK || + node->type == CMARK_NODE_FRONT_MATTER) { cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info); - node->as.code.has_info = info != NULL; return 1; } else { return 0; @@ -626,11 +651,30 @@ int cmark_node_set_code_info(cmark_node *node, const char *info) { } const char *cmark_node_get_fence_info(cmark_node *node) { - return cmark_node_get_code_info(node); + if (node == NULL) { + return NULL; + } + + if (node->type == CMARK_NODE_CODE_BLOCK || + node->type == CMARK_NODE_FRONT_MATTER) { + return cmark_chunk_to_cstr(NODE_MEM(node), &node->as.code.info); + } else { + return NULL; + } } int cmark_node_set_fence_info(cmark_node *node, const char *info) { - return cmark_node_set_code_info(node, info); + if (node == NULL) { + return 0; + } + + if (node->type == CMARK_NODE_CODE_BLOCK || + node->type == CMARK_NODE_FRONT_MATTER) { + cmark_chunk_set_cstr(NODE_MEM(node), &node->as.code.info, info); + return 1; + } else { + return 0; + } } int cmark_node_get_fenced(cmark_node *node, int *length, int *offset, char *character) { diff --git a/src/node.h b/src/node.h index 22059eccc..73ca76053 100644 --- a/src/node.h +++ b/src/node.h @@ -30,7 +30,6 @@ typedef struct { uint8_t fence_length; uint8_t fence_offset; unsigned char fence_char; - uint8_t has_info; int8_t fenced; } cmark_code; diff --git a/src/xml.c b/src/xml.c index 698cfa0be..6fa979dcf 100644 --- a/src/xml.c +++ b/src/xml.c @@ -81,7 +81,7 @@ static int S_render_node(cmark_node *node, cmark_event_type ev_type, literal = true; break; case CMARK_NODE_CODE: - if (node->as.code.has_info) { + if (node->as.code.info.len > 0) { cmark_strbuf_puts(xml, " info=\""); escape_xml(xml, node->as.code.info.data, node->as.code.info.len); cmark_strbuf_putc(xml, '"'); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0c79168bf..1d53a8270 100755 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -103,6 +103,13 @@ IF (PYTHONINTERP_FOUND) "--program" "${CMAKE_CURRENT_BINARY_DIR}/../src/cmark-gfm --inline-code-info" ) + add_test(option_inline_code_info_smart + ${PYTHON_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/roundtrip_tests.py" + "--spec" "${CMAKE_CURRENT_SOURCE_DIR}/inline-code-info-smart.txt" + "--program" "${CMAKE_CURRENT_BINARY_DIR}/../src/cmark-gfm --inline-code-info --smart" + ) + add_test(regressiontest_executable ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/spec_tests.py" "--no-normalize" "--spec" diff --git a/test/inline-code-info-smart.txt b/test/inline-code-info-smart.txt new file mode 100644 index 000000000..9b99d4c15 --- /dev/null +++ b/test/inline-code-info-smart.txt @@ -0,0 +1,14 @@ +### Inline code language prefixes with smart punctuation + +Language prefixes containing punctuation are recognized before smart +punctuation processing. + +```````````````````````````````` example +Objective-C: objective-c:`NSObject` +Dotted: foo.bar:`value` +Underscored: foo_bar:`value` +. +

Objective-C: NSObject +Dotted: value +Underscored: value

+```````````````````````````````` diff --git a/test/inline-code-info.txt b/test/inline-code-info.txt index 12f468567..df36b9674 100644 --- a/test/inline-code-info.txt +++ b/test/inline-code-info.txt @@ -1,35 +1,53 @@ -### Inline code info strings +### Inline code language prefixes -Inline code spans may carry unquoted info strings. +Inline code spans may carry language prefixes. ```````````````````````````````` example -Ruby code: `Object.new`:ruby +Ruby code: ruby:`Object.new` .

Ruby code: Object.new

```````````````````````````````` -Inline code spans may carry quoted extended info strings. +Language prefixes work with longer code span delimiters. ```````````````````````````````` example -Ruby code with extended fence info: `Object.new`:"ruby lineno=5" +Ruby code: ruby:`` `Object.new` `` . -

Ruby code with extended fence info: Object.new

+

Ruby code: `Object.new`

```````````````````````````````` -Inline code spans may carry empty info strings, which are normalized away. +Language identifiers may contain characters used by common language names. ```````````````````````````````` example -Empty info: `x`: +C++ code: c++:`std::vector` +C# code: c#:`System.Object` +Objective-C code: objective-c:`NSObject` +Ruby template: ruby_template:`render` . -

Empty info: x

+

C++ code: std::vector +C# code: System.Object +Objective-C code: NSObject +Ruby template: render

```````````````````````````````` -Unterminated quoted inline code info strings stop at the line ending. +Ordinary punctuation following inline code is preserved. ```````````````````````````````` example -Before `x`:"ruby lineno=5 -after +Use `x`: then continue. . -

Before x -after

+

Use x: then continue.

+```````````````````````````````` + +Invalid and empty language prefixes remain literal text. + +```````````````````````````````` example +Empty: :`x` +Quoted: "ruby":`x` +Invalid: ruby/:`x` +Unclosed: ruby:`x +. +

Empty: :x +Quoted: "ruby":x +Invalid: ruby/:x +Unclosed: ruby:`x

```````````````````````````````` diff --git a/test/regression.txt b/test/regression.txt index a87b26803..4fb990334 100644 --- a/test/regression.txt +++ b/test/regression.txt @@ -11,12 +11,12 @@ line2

line2

```````````````````````````````` -Inline code info strings are not parsed unless explicitly enabled. +Inline code language prefixes are not parsed unless explicitly enabled. ```````````````````````````````` example -Ruby code: `Object.new`:ruby +Ruby code: ruby:`Object.new` . -

Ruby code: Object.new:ruby

+

Ruby code: ruby:Object.new

```````````````````````````````` Issue #114: cmark skipping first character in line From 8934fd531f06fd727d278aea9da893bb4629175d Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 20 Aug 2026 15:09:14 +1200 Subject: [PATCH 5/7] Fix MSVC inline code info warning Assisted-By: devx/c76f68ef-c5ab-4096-b170-fc6974ad760d --- src/inlines.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inlines.c b/src/inlines.c index 5acf816c5..270d7bfba 100644 --- a/src/inlines.c +++ b/src/inlines.c @@ -397,8 +397,8 @@ static void S_normalize_code(cmark_strbuf *s) { } static CMARK_INLINE int is_inline_code_info_char(int c) { - return cmark_isalnum(c) || c == '_' || c == '-' || c == '+' || c == '#' || - c == '.'; + return cmark_isalnum((char)c) || c == '_' || c == '-' || c == '+' || + c == '#' || c == '.'; } static bufsize_t scan_inline_code_info_prefix(subject *subj, bufsize_t start) { From 62a3c8855e78fe49f2f8ef82bc1f1fa187dc1857 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 20 Aug 2026 15:30:16 +1200 Subject: [PATCH 6/7] Share inline code info character rules Assisted-By: devx/c76f68ef-c5ab-4096-b170-fc6974ad760d --- api_test/main.c | 5 +++++ src/cmark_ctype.c | 9 +++++++++ src/cmark_ctype.h | 5 +++++ src/inlines.c | 15 ++++++--------- src/node.c | 6 ++---- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/api_test/main.c b/api_test/main.c index f611e8c87..66ee0110f 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -289,6 +289,11 @@ static void inline_code_info(test_batch_runner *runner) { STR_EQ(runner, cmark_node_get_code_info(code), "c++", "get updated inline code info"); + OK(runner, cmark_node_set_code_info(code, "a_b-c+#.d"), + "set inline code info with all supported punctuation"); + STR_EQ(runner, cmark_node_get_code_info(code), "a_b-c+#.d", + "get inline code info with all supported punctuation"); + OK(runner, cmark_node_set_code_info(code, ""), "clear inline code info"); html = cmark_render_html(doc, CMARK_OPT_DEFAULT, NULL); STR_EQ(runner, html, "

Object.new

\n", diff --git a/src/cmark_ctype.c b/src/cmark_ctype.c index c0c4d5b03..46e264396 100644 --- a/src/cmark_ctype.c +++ b/src/cmark_ctype.c @@ -42,3 +42,12 @@ int cmark_isalnum(char c) { int cmark_isdigit(char c) { return cmark_ctype_class[(uint8_t)c] == 3; } int cmark_isalpha(char c) { return cmark_ctype_class[(uint8_t)c] == 4; } + +int cmark_is_inline_code_info_start_char(char c) { + return cmark_isalnum(c); +} + +int cmark_is_inline_code_info_char(char c) { + return cmark_is_inline_code_info_start_char(c) || c == '_' || c == '-' || + c == '+' || c == '#' || c == '.'; +} diff --git a/src/cmark_ctype.h b/src/cmark_ctype.h index 67c1cb037..10b5f220f 100644 --- a/src/cmark_ctype.h +++ b/src/cmark_ctype.h @@ -26,6 +26,11 @@ int cmark_isdigit(char c); CMARK_GFM_EXPORT int cmark_isalpha(char c); +/* Character classes for inline code info identifiers. */ +int cmark_is_inline_code_info_start_char(char c); + +int cmark_is_inline_code_info_char(char c); + #ifdef __cplusplus } #endif diff --git a/src/inlines.c b/src/inlines.c index 270d7bfba..453c89b2c 100644 --- a/src/inlines.c +++ b/src/inlines.c @@ -396,20 +396,17 @@ static void S_normalize_code(cmark_strbuf *s) { } -static CMARK_INLINE int is_inline_code_info_char(int c) { - return cmark_isalnum((char)c) || c == '_' || c == '-' || c == '+' || - c == '#' || c == '.'; -} - static bufsize_t scan_inline_code_info_prefix(subject *subj, bufsize_t start) { - if (start >= subj->input.len || !cmark_isalnum(subj->input.data[start]) || - (start > 0 && is_inline_code_info_char(subj->input.data[start - 1]))) { + if (start >= subj->input.len || + !cmark_is_inline_code_info_start_char((char)subj->input.data[start]) || + (start > 0 && cmark_is_inline_code_info_char( + (char)subj->input.data[start - 1]))) { return 0; } bufsize_t pos = start + 1; - while (pos < subj->input.len && - is_inline_code_info_char(subj->input.data[pos])) { + while (pos < subj->input.len && cmark_is_inline_code_info_char( + (char)subj->input.data[pos])) { pos++; } diff --git a/src/node.c b/src/node.c index b92df47a5..4147aa594 100644 --- a/src/node.c +++ b/src/node.c @@ -614,14 +614,12 @@ static int valid_inline_code_info(const char *info) { return 1; } - if (!cmark_isalnum((unsigned char)*info)) { + if (!cmark_is_inline_code_info_start_char(*info)) { return 0; } while (*++info) { - unsigned char c = (unsigned char)*info; - if (!(cmark_isalnum(c) || c == '_' || c == '-' || c == '+' || c == '#' || - c == '.')) { + if (!cmark_is_inline_code_info_char(*info)) { return 0; } } From 51bb37b71c434c30cd287dbf04f1028c41e3aecd Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 20 Aug 2026 15:45:39 +1200 Subject: [PATCH 7/7] Document inline code info Assisted-By: devx/c76f68ef-c5ab-4096-b170-fc6974ad760d --- man/man1/cmark-gfm.1 | 4 ++++ man/man3/cmark-gfm.3 | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/man/man1/cmark-gfm.1 b/man/man1/cmark-gfm.1 index 4fca62732..c7457ed50 100644 --- a/man/man1/cmark-gfm.1 +++ b/man/man1/cmark-gfm.1 @@ -58,6 +58,10 @@ be rendered as curly quotes, depending on their position. \f[C]\-\-\-\f[] will be rendered as an em-dash. \f[C]...\f[] will be rendered as ellipses. .TP 12n +.B \-\-inline-code-info +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 \-\-unsafe Render raw HTML and potentially dangerous URLs. (Raw HTML is not replaced by a placeholder comment; potentially diff --git a/man/man3/cmark-gfm.3 b/man/man3/cmark-gfm.3 index 001b7c707..d431bb780 100644 --- a/man/man3/cmark-gfm.3 +++ b/man/man3/cmark-gfm.3 @@ -477,6 +477,23 @@ Returns 1 if \f[I]node\f[] is a tight list, 0 otherwise. .PP Sets the "tightness" of a list. Returns 1 on success, 0 on failure. +.PP +\fIconst char *\f[] \fBcmark_node_get_code_info\f[](\fIcmark_node *node\f[]) + +.PP +Returns the info string from a code block or the language identifier from +an inline code span. + +.PP +\fIint\f[] \fBcmark_node_set_code_info\f[](\fIcmark_node *node\f[], \fIconst char *info\f[]) + +.PP +Sets the info string in a code block or the language identifier on an +inline code span, returning 1 on success and 0 on failure. Inline code +language identifiers must begin with an alphanumeric character and may +additionally contain \f[C]_\f[], \f[C]-\f[], \f[C]+\f[], \f[C]#\f[], +and \f[C].\f[]. + .PP \fIconst char *\f[] \fBcmark_node_get_fence_info\f[](\fIcmark_node *node\f[]) @@ -1009,6 +1026,18 @@ Use style attributes to align table cells instead of align attributes. Include the remainder of the info string in code blocks in a separate attribute. +.PP +.nf +\fC +.RS 0n +#define CMARK_OPT_INLINE_CODE_INFO (1 << 19) +.RE +\f[] +.fi + +.PP +Parse inline code language prefixes, e.g. \f[C]ruby:`code`\f[]. + .SS Version information @@ -1038,4 +1067,3 @@ CMARK_VERSION_STRING for compile time checks. AUTHORS .PP John MacFarlane, Vicent Marti, Kārlis Gaņģis, Nick Wellnhofer. -