diff --git a/api_test/CMakeLists.txt b/api_test/CMakeLists.txt index 533c2c6d8..541e939b7 100644 --- a/api_test/CMakeLists.txt +++ b/api_test/CMakeLists.txt @@ -1,5 +1,7 @@ add_executable(api_test cplusplus.cpp + delim_test.c + delim_test.h harness.c harness.h main.c diff --git a/api_test/delim_test.c b/api_test/delim_test.c new file mode 100644 index 000000000..7968bb3e6 --- /dev/null +++ b/api_test/delim_test.c @@ -0,0 +1,316 @@ +#include +#include +#include + +#include "cmark.h" +#include "cmark_delim.h" +#include "harness.h" +#include "delim_test.h" + +static void test_emph_asterisk(test_batch_runner *runner) { + const char *md = "*italic*"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *emph = cmark_node_first_child(para); + + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(emph, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + + OK(runner, ok == 1, "emph asterisk: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '*', "emph asterisk: delim_char is *"); + INT_EQ(runner, delim_len, 1, "emph asterisk: delim_len is 1"); + + cmark_node_free(doc); +} + +static void test_emph_underscore(test_batch_runner *runner) { + const char *md = "_italic_"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *emph = cmark_node_first_child(para); + + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(emph, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + + OK(runner, ok == 1, "emph underscore: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '_', "emph underscore: delim_char is _"); + INT_EQ(runner, delim_len, 1, "emph underscore: delim_len is 1"); + + cmark_node_free(doc); +} + +static void test_strong_asterisk(test_batch_runner *runner) { + const char *md = "**bold**"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *strong = cmark_node_first_child(para); + + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(strong, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + + OK(runner, ok == 1, "strong asterisk: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '*', "strong asterisk: delim_char is *"); + INT_EQ(runner, delim_len, 2, "strong asterisk: delim_len is 2"); + + cmark_node_free(doc); +} + +static void test_strong_underscore(test_batch_runner *runner) { + const char *md = "__bold__"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *strong = cmark_node_first_child(para); + + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(strong, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + + OK(runner, ok == 1, "strong underscore: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '_', "strong underscore: delim_char is _"); + INT_EQ(runner, delim_len, 2, "strong underscore: delim_len is 2"); + + cmark_node_free(doc); +} + +static void test_code_single_backtick(test_batch_runner *runner) { + const char *md = "`code`"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *code = cmark_node_first_child(para); + + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(code, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + + OK(runner, ok == 1, "code single: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '`', "code single: delim_char is `"); + INT_EQ(runner, delim_len, 1, "code single: delim_len is 1"); + + cmark_node_free(doc); +} + +static void test_code_double_backtick(test_batch_runner *runner) { + const char *md = "``code with ` inside``"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *code = cmark_node_first_child(para); + + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(code, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + + OK(runner, ok == 1, "code double: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '`', "code double: delim_char is `"); + INT_EQ(runner, delim_len, 2, "code double: delim_len is 2"); + + cmark_node_free(doc); +} + +static void test_delim_length_convenience(test_batch_runner *runner) { + // Test cmark_node_get_delim_length convenience function + cmark_node *emph = cmark_node_new(CMARK_NODE_EMPH); + cmark_node *strong = cmark_node_new(CMARK_NODE_STRONG); + cmark_node *code = cmark_node_new(CMARK_NODE_CODE); + cmark_node *text = cmark_node_new(CMARK_NODE_TEXT); + + INT_EQ(runner, cmark_node_get_delim_length(emph), 1, "delim_length emph is 1"); + INT_EQ(runner, cmark_node_get_delim_length(strong), 2, "delim_length strong is 2"); + INT_EQ(runner, cmark_node_get_delim_length(code), 0, "delim_length code is 0 (needs source)"); + INT_EQ(runner, cmark_node_get_delim_length(text), 0, "delim_length text is 0"); + INT_EQ(runner, cmark_node_get_delim_length(NULL), 0, "delim_length NULL is 0"); + + cmark_node_free(emph); + cmark_node_free(strong); + cmark_node_free(code); + cmark_node_free(text); +} + +static void test_delim_char_convenience(test_batch_runner *runner) { + // Test cmark_node_get_delim_char convenience function + const char *md = "*italic* and `code`"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *emph = cmark_node_first_child(para); + cmark_node *code = cmark_node_last_child(para); + + INT_EQ(runner, cmark_node_get_delim_char(emph, md, strlen(md), CMARK_OPT_SOURCEPOS), '*', + "delim_char convenience for emph"); + INT_EQ(runner, cmark_node_get_delim_char(code, md, strlen(md), CMARK_OPT_SOURCEPOS), '`', + "delim_char convenience for code"); + + cmark_node_free(doc); +} + +static void test_null_handling(test_batch_runner *runner) { + // Test NULL node handling + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(NULL, "test", 4, CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 0, "get_delimiter_info returns 0 for NULL node"); + + // Test NULL source handling + cmark_node *emph = cmark_node_new(CMARK_NODE_EMPH); + ok = cmark_node_get_delimiter_info(emph, NULL, 0, CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 0, "get_delimiter_info returns 0 for NULL source"); + cmark_node_free(emph); + + // Test non-delimiter node type + cmark_node *text = cmark_node_new(CMARK_NODE_TEXT); + ok = cmark_node_get_delimiter_info(text, "test", 4, CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 0, "get_delimiter_info returns 0 for TEXT node"); + cmark_node_free(text); + + // Test missing CMARK_OPT_SOURCEPOS + const char *md = "*italic*"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + cmark_node *emph_node = cmark_node_first_child(para); + ok = cmark_node_get_delimiter_info(emph_node, md, strlen(md), CMARK_OPT_DEFAULT, &delim_char, &delim_len); + OK(runner, ok == 0, "get_delimiter_info returns 0 without CMARK_OPT_SOURCEPOS"); + cmark_node_free(doc); +} + +static void test_nested_emphasis(test_batch_runner *runner) { + // Test nested emphasis: ***bold italic*** + const char *md = "***bold italic***"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + // Structure is: para -> emph -> strong -> text + cmark_node *emph = cmark_node_first_child(para); + cmark_node *strong = cmark_node_first_child(emph); + + int delim_char = 0, delim_len = 0; + + // Check outer emph + OK(runner, cmark_node_get_type(emph) == CMARK_NODE_EMPH, "nested: outer is emph"); + cmark_node_get_delimiter_info(emph, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + INT_EQ(runner, delim_char, '*', "nested: emph delim_char is *"); + INT_EQ(runner, delim_len, 1, "nested: emph delim_len is 1"); + + // Check inner strong + OK(runner, cmark_node_get_type(strong) == CMARK_NODE_STRONG, "nested: inner is strong"); + cmark_node_get_delimiter_info(strong, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + INT_EQ(runner, delim_char, '*', "nested: strong delim_char is *"); + INT_EQ(runner, delim_len, 2, "nested: strong delim_len is 2"); + + cmark_node_free(doc); +} + +static void test_multiline(test_batch_runner *runner) { + // Test emphasis spanning multiple lines + const char *md = "line 1\n*italic\ntext*\nline 3"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + + // Find the emph node + cmark_node *node = cmark_node_first_child(para); + while (node && cmark_node_get_type(node) != CMARK_NODE_EMPH) { + node = cmark_node_next(node); + } + + OK(runner, node != NULL, "multiline: found emph node"); + if (node) { + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(node, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 1, "multiline: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '*', "multiline: delim_char is *"); + INT_EQ(runner, delim_len, 1, "multiline: delim_len is 1"); + } + + cmark_node_free(doc); +} + +static void test_multiline_code_then_emph(test_batch_runner *runner) { + // Test the edge case: multi-line code span followed by emphasis + // This requires CMARK_OPT_SOURCEPOS to track positions correctly + const char *md = "`multi\nline` *emph*"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + + // Find the code node (first child) + cmark_node *code = cmark_node_first_child(para); + OK(runner, cmark_node_get_type(code) == CMARK_NODE_CODE, "multiline_code_emph: first is code"); + + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(code, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 1, "multiline_code_emph: code get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '`', "multiline_code_emph: code delim_char is `"); + INT_EQ(runner, delim_len, 1, "multiline_code_emph: code delim_len is 1"); + + // Find the emph node (after text node with space) + cmark_node *node = cmark_node_next(code); + while (node && cmark_node_get_type(node) != CMARK_NODE_EMPH) { + node = cmark_node_next(node); + } + + OK(runner, node != NULL, "multiline_code_emph: found emph node"); + if (node) { + ok = cmark_node_get_delimiter_info(node, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 1, "multiline_code_emph: emph get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '*', "multiline_code_emph: emph delim_char is *"); + INT_EQ(runner, delim_len, 1, "multiline_code_emph: emph delim_len is 1"); + } + + cmark_node_free(doc); +} + +static void test_cr_line_endings(test_batch_runner *runner) { + // Test CR-only line endings (old Mac style) + const char *md = "line1\r*emph*"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + + // Find emph node + cmark_node *node = cmark_node_first_child(para); + while (node && cmark_node_get_type(node) != CMARK_NODE_EMPH) { + node = cmark_node_next(node); + } + + OK(runner, node != NULL, "cr_endings: found emph node"); + if (node) { + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(node, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 1, "cr_endings: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '*', "cr_endings: delim_char is *"); + INT_EQ(runner, delim_len, 1, "cr_endings: delim_len is 1"); + } + + cmark_node_free(doc); +} + +static void test_crlf_line_endings(test_batch_runner *runner) { + // Test CRLF line endings (Windows style) + const char *md = "line1\r\n*emph*"; + cmark_node *doc = cmark_parse_document_for_delimiters(md, strlen(md), CMARK_OPT_DEFAULT); + cmark_node *para = cmark_node_first_child(doc); + + // Find emph node + cmark_node *node = cmark_node_first_child(para); + while (node && cmark_node_get_type(node) != CMARK_NODE_EMPH) { + node = cmark_node_next(node); + } + + OK(runner, node != NULL, "crlf_endings: found emph node"); + if (node) { + int delim_char = 0, delim_len = 0; + int ok = cmark_node_get_delimiter_info(node, md, strlen(md), CMARK_OPT_SOURCEPOS, &delim_char, &delim_len); + OK(runner, ok == 1, "crlf_endings: get_delimiter_info returns 1"); + INT_EQ(runner, delim_char, '*', "crlf_endings: delim_char is *"); + INT_EQ(runner, delim_len, 1, "crlf_endings: delim_len is 1"); + } + + cmark_node_free(doc); +} + +void test_delimiters(test_batch_runner *runner) { + test_emph_asterisk(runner); + test_emph_underscore(runner); + test_strong_asterisk(runner); + test_strong_underscore(runner); + test_code_single_backtick(runner); + test_code_double_backtick(runner); + test_delim_length_convenience(runner); + test_delim_char_convenience(runner); + test_null_handling(runner); + test_nested_emphasis(runner); + test_multiline(runner); + test_multiline_code_then_emph(runner); + test_cr_line_endings(runner); + test_crlf_line_endings(runner); +} diff --git a/api_test/delim_test.h b/api_test/delim_test.h new file mode 100644 index 000000000..f90827b14 --- /dev/null +++ b/api_test/delim_test.h @@ -0,0 +1,16 @@ +#ifndef CMARK_DELIM_TEST_H +#define CMARK_DELIM_TEST_H + +#include "harness.h" + +#ifdef __cplusplus +extern "C" { +#endif + +void test_delimiters(test_batch_runner *runner); + +#ifdef __cplusplus +} +#endif + +#endif /* CMARK_DELIM_TEST_H */ diff --git a/api_test/main.c b/api_test/main.c index f3fc9ffdf..3ac1de6b9 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -8,6 +8,7 @@ #include "harness.h" #include "cplusplus.h" +#include "delim_test.h" #define UTF8_REPL "\xEF\xBF\xBD" @@ -1185,6 +1186,7 @@ int main(void) { source_pos(runner); source_pos_inlines(runner); ref_source_pos(runner); + test_delimiters(runner); test_print_summary(runner); retval = test_ok(runner) ? 0 : 1; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 523b2cb82..6c2fec04d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(cmark buffer.c cmark.c cmark_ctype.c + cmark_delim.c commonmark.c houdini_href_e.c houdini_html_e.c @@ -72,6 +73,7 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libcmark.pc install(FILES cmark.h + cmark_delim.h ${CMAKE_CURRENT_BINARY_DIR}/cmark_export.h ${CMAKE_CURRENT_BINARY_DIR}/cmark_version.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} diff --git a/src/cmark_delim.c b/src/cmark_delim.c new file mode 100644 index 000000000..c803fe506 --- /dev/null +++ b/src/cmark_delim.c @@ -0,0 +1,167 @@ +#include "cmark_delim.h" +#include "cmark.h" +#include + +cmark_node *cmark_parse_document_for_delimiters( + const char *buffer, + size_t len, + int options) +{ + return cmark_parse_document(buffer, len, options | CMARK_OPT_SOURCEPOS); +} + +/** + * Helper: Convert 1-indexed line and column to 0-indexed position in source. + * + * Handles all line ending styles: LF (\n), CR (\r), and CRLF (\r\n). + * + * @param line 1-indexed line number + * @param col 1-indexed column number + * @param source The source text + * @param len Length of source text + * @return 0-indexed position in source, or len if out of bounds + */ +static size_t col_to_idx(int line, int col, const char *source, size_t len) { + size_t idx = 0; + int current_line = 1; + + // Skip to the correct line, handling \n, \r, and \r\n + while (idx < len && current_line < line) { + char c = source[idx]; + if (c == '\n') { + current_line++; + idx++; + } else if (c == '\r') { + current_line++; + idx++; + // Skip \n if this is \r\n (CRLF) + if (idx < len && source[idx] == '\n') { + idx++; + } + } else { + idx++; + } + } + + // Add column offset (col is 1-indexed, so subtract 1) + if (col > 0) { + idx += (size_t)(col - 1); + } + + return (idx < len) ? idx : len; +} + +int cmark_node_get_delimiter_info( + cmark_node *node, + const char *source, + size_t source_len, + int options, + int *delim_char, + int *delim_len) +{ + if (node == NULL || source == NULL) { + return 0; + } + + // Require CMARK_OPT_SOURCEPOS for accurate position tracking + if (!(options & CMARK_OPT_SOURCEPOS)) { + return 0; + } + + cmark_node_type type = cmark_node_get_type(node); + int start_col = cmark_node_get_start_column(node); + int start_line = cmark_node_get_start_line(node); + + switch (type) { + case CMARK_NODE_EMPH: + // Emph: start_column points TO the opening delimiter + if (delim_len) { + *delim_len = 1; + } + if (delim_char) { + if (start_col >= 1) { + size_t idx = col_to_idx(start_line, start_col, source, source_len); + *delim_char = (idx < source_len) ? (unsigned char)source[idx] : 0; + } else { + *delim_char = 0; + } + } + return 1; + + case CMARK_NODE_STRONG: + // Strong: start_column points TO the first opening delimiter + if (delim_len) { + *delim_len = 2; + } + if (delim_char) { + if (start_col >= 1) { + size_t idx = col_to_idx(start_line, start_col, source, source_len); + *delim_char = (idx < source_len) ? (unsigned char)source[idx] : 0; + } else { + *delim_char = 0; + } + } + return 1; + + case CMARK_NODE_CODE: + // Code uses backticks + if (delim_char) { + *delim_char = '`'; + } + if (delim_len) { + if (start_col > 1) { + // Count backticks backwards from content start + size_t idx = col_to_idx(start_line, start_col - 1, source, source_len); + int count = 0; + + // Count consecutive backticks going backwards + while (idx < source_len && source[idx] == '`') { + count++; + if (idx == 0) { + break; + } + idx--; + } + + *delim_len = (count > 0) ? count : 1; + } else { + *delim_len = 1; + } + } + return 1; + + default: + return 0; + } +} + +int cmark_node_get_delim_char( + cmark_node *node, + const char *source, + size_t source_len, + int options) +{ + int delim_char = 0; + cmark_node_get_delimiter_info(node, source, source_len, options, &delim_char, NULL); + return delim_char; +} + +int cmark_node_get_delim_length(cmark_node *node) +{ + if (node == NULL) { + return 0; + } + + switch (cmark_node_get_type(node)) { + case CMARK_NODE_EMPH: + return 1; + case CMARK_NODE_STRONG: + return 2; + case CMARK_NODE_CODE: + // For code, we need the source to count backticks + // Return 0 to indicate caller should use the full function + return 0; + default: + return 0; + } +} diff --git a/src/cmark_delim.h b/src/cmark_delim.h new file mode 100644 index 000000000..b4673accf --- /dev/null +++ b/src/cmark_delim.h @@ -0,0 +1,103 @@ +#ifndef CMARK_DELIM_H +#define CMARK_DELIM_H + +#include "cmark.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file cmark_delim.h + * @brief Functions for retrieving delimiter information from cmark nodes. + * + * These functions allow downstream code to determine the original markdown + * delimiter characters and lengths for inline nodes (EMPH, STRONG, CODE). + * This enables use cases like rendering markdown with delimiters included + * inside the styled elements, e.g., `**example**` -> `**example**`. + * + * The functions compute delimiter info on-demand from the original source text + * and node position information, requiring no modifications to cmark's parsing. + * + * USAGE: + * 1. Parse with cmark_parse_document_for_delimiters() - recommended + * 2. Or parse with CMARK_OPT_SOURCEPOS and pass those options to the query functions + * + * The delimiter query functions require CMARK_OPT_SOURCEPOS and will return 0 if + * this option is not provided. + */ + +/** + * Parse a CommonMark document with source position tracking enabled. + * + * This is a convenience wrapper around cmark_parse_document() that ensures + * accurate source positions are available for delimiter info queries. + * Use this function when you plan to call cmark_node_get_delimiter_info() + * or related functions on the resulting document. + * + * @param buffer The markdown source text + * @param len Length of buffer + * @param options Parsing options (CMARK_OPT_SOURCEPOS is added automatically) + * @return Root node of parsed document + */ +CMARK_EXPORT cmark_node *cmark_parse_document_for_delimiters( + const char *buffer, + size_t len, + int options); + +/** + * Get delimiter info for an inline node by examining the source text. + * + * For EMPH nodes, returns the delimiter character ('*' or '_') and length 1. + * For STRONG nodes, returns the delimiter character ('*' or '_') and length 2. + * For CODE nodes, returns '`' and the number of backticks used. + * + * @param node The cmark node (EMPH, STRONG, or CODE) + * @param source The original markdown source text passed to the parser + * @param source_len Length of source text + * @param options The options used when parsing (must include CMARK_OPT_SOURCEPOS) + * @param delim_char Output: the delimiter character ('*', '_', or '`'), may be NULL + * @param delim_len Output: the delimiter length (1 for emph, 2 for strong, N for code), may be NULL + * @return 1 on success, 0 if node is NULL, source is NULL, options missing CMARK_OPT_SOURCEPOS, + * or node type doesn't have delimiters + */ +CMARK_EXPORT int cmark_node_get_delimiter_info( + cmark_node *node, + const char *source, + size_t source_len, + int options, + int *delim_char, + int *delim_len); + +/** + * Convenience function to get just the delimiter character. + * + * @param node The cmark node (EMPH, STRONG, or CODE) + * @param source The original markdown source text passed to the parser + * @param source_len Length of source text + * @param options The options used when parsing (must include CMARK_OPT_SOURCEPOS) + * @return The delimiter char ('*', '_', or '`'), or 0 if not applicable or invalid options + */ +CMARK_EXPORT int cmark_node_get_delim_char( + cmark_node *node, + const char *source, + size_t source_len, + int options); + +/** + * Get the delimiter length for a node without needing the source text. + * + * For EMPH, always returns 1. + * For STRONG, always returns 2. + * For CODE, returns 0 (use cmark_node_get_delimiter_info to get actual backtick count). + * + * @param node The cmark node + * @return The delimiter length, or 0 if not applicable or if source is needed + */ +CMARK_EXPORT int cmark_node_get_delim_length(cmark_node *node); + +#ifdef __cplusplus +} +#endif + +#endif /* CMARK_DELIM_H */