diff --git a/api_test/main.c b/api_test/main.c index 68412f335..66ee0110f 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,66 @@ 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, "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", + "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 +1263,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/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. - diff --git a/src/cmark-gfm.h b/src/cmark-gfm.h index 8848bf405..c0fe68678 100644 --- a/src/cmark-gfm.h +++ b/src/cmark-gfm.h @@ -425,6 +425,18 @@ 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 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 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); + /** Returns the info string from a fenced code block. */ CMARK_GFM_EXPORT const char *cmark_node_get_fence_info(cmark_node *node); @@ -776,6 +788,10 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar */ #define CMARK_OPT_FRONT_MATTER (1 << 18) +/** Parse inline code language prefixes, e.g. ruby:`code`. + */ +#define CMARK_OPT_INLINE_CODE_INFO (1 << 19) + /** * ## Version information */ 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/commonmark.c b/src/commonmark.c index 3897f9b92..80095e296 100644 --- a/src/commonmark.c +++ b/src/commonmark.c @@ -271,7 +271,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); @@ -363,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("`"); } diff --git a/src/html.c b/src/html.c index a3ab5f59d..104a61835 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.info.len > 0) { + 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..453c89b2c 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 bufsize_t scan_inline_code_info_prefix(subject *subj, bufsize_t start) { + 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 && cmark_is_inline_code_info_char( + (char)subj->input.data[pos])) { + pos++; + } + + if (pos + 1 < subj->input.len && subj->input.data[pos] == ':' && + subj->input.data[pos + 1] == '`') { + return pos; + } + + return 0; +} + +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; + } + start++; + } + + return 0; +} // Parse backtick code section or raw backticks, return an inline. // Assumes that the subject has a backtick at the current position. @@ -403,12 +447,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)); - adjust_subj_node_newlines(subj, node, endpos - 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. @@ -1458,6 +1525,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': @@ -1511,6 +1586,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 13fdb7616..edd24901b 100644 --- a/src/iterator.c +++ b/src/iterator.c @@ -139,10 +139,15 @@ 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); + 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); cmark_chunk_to_cstr(iter->mem, &cur->as.link.title); diff --git a/src/main.c b/src/main.c index a62c4f2ca..25885fdd9 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 language prefixes\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/src/node.c b/src/node.c index e8c503409..4147aa594 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,6 +595,59 @@ int cmark_node_set_item_index(cmark_node *node, int idx) { } } +const char *cmark_node_get_code_info(cmark_node *node) { + if (node == NULL) { + return NULL; + } + + 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 { + return NULL; + } +} + +static int valid_inline_code_info(const char *info) { + if (info == NULL || *info == '\0') { + return 1; + } + + if (!cmark_is_inline_code_info_start_char(*info)) { + return 0; + } + + while (*++info) { + if (!cmark_is_inline_code_info_char(*info)) { + 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) { + 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); + return 1; + } else { + return 0; + } +} + const char *cmark_node_get_fence_info(cmark_node *node) { if (node == NULL) { return NULL; diff --git a/src/xml.c b/src/xml.c index 6358e3dfb..6fa979dcf 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.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, '"'); + } + cmark_strbuf_puts(xml, " xml:space=\"preserve\">"); + escape_xml(xml, node->as.code.literal.data, node->as.code.literal.len); + cmark_strbuf_puts(xml, "Objective-C: NSObject +Dotted: value +Underscored: value

+```````````````````````````````` diff --git a/test/inline-code-info.txt b/test/inline-code-info.txt new file mode 100644 index 000000000..df36b9674 --- /dev/null +++ b/test/inline-code-info.txt @@ -0,0 +1,53 @@ +### Inline code language prefixes + +Inline code spans may carry language prefixes. + +```````````````````````````````` example +Ruby code: ruby:`Object.new` +. +

Ruby code: Object.new

+```````````````````````````````` + +Language prefixes work with longer code span delimiters. + +```````````````````````````````` example +Ruby code: ruby:`` `Object.new` `` +. +

Ruby code: `Object.new`

+```````````````````````````````` + +Language identifiers may contain characters used by common language names. + +```````````````````````````````` example +C++ code: c++:`std::vector` +C# code: c#:`System.Object` +Objective-C code: objective-c:`NSObject` +Ruby template: ruby_template:`render` +. +

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

+```````````````````````````````` + +Ordinary punctuation following inline code is preserved. + +```````````````````````````````` example +Use `x`: then continue. +. +

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 0ec493d3d..4fb990334 100644 --- a/test/regression.txt +++ b/test/regression.txt @@ -11,6 +11,14 @@ line2

line2

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

Ruby code: ruby:Object.new

+```````````````````````````````` + Issue #114: cmark skipping first character in line (Important: the blank lines around "Repeatedly" contain a tab.)