diff --git a/CMakeLists.txt b/CMakeLists.txt index 06dcd3da3..ec620b7e2 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(cmark-gfm) set(PROJECT_VERSION_MAJOR 0) set(PROJECT_VERSION_MINOR 29) set(PROJECT_VERSION_PATCH 0) -set(PROJECT_VERSION_GFM 6) +set(PROJECT_VERSION_GFM 11) set(PROJECT_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}.gfm.${PROJECT_VERSION_GFM}) include("FindAsan.cmake") diff --git a/api_test/main.c b/api_test/main.c index efed0fb60..e950451ba 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -1575,6 +1575,7 @@ int main() { int retval; test_batch_runner *runner = test_batch_runner_new(); + cmark_enable_safety_checks(true); version(runner); constructor(runner); accessors(runner); diff --git a/changelog.txt b/changelog.txt index 4cc5c58ec..848e71029 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,44 @@ +[0.29.0.gfm.11] + + * Improved fixes for polynomial time complexity issues per + https://github.com/github/cmark-gfm/security/advisories/GHSA-66g8-4hjf-77xh + (#323, #324) + * Added fuzzing target for bracketed patterns (#318) + * Fixed bug in list numbering introduced in + 763587e8775350b8cb4a2aa0f4cec3685aa96e8b (#322) which caused list numbers + to increment by 2 + * Fixed strict prototype clang warning (#310) + * Fixed regression test (#312) + * Added additional output formats to quadratic fuzzer (#327) + * Fixed buffer overflow in fuzzing harness (#326) + + Note: these changes may lead to minor changes in expected output on plaintext + rendering of list items. Notably, blank lines may no longer delineate the start + of a list when rendering to plaintext due to changes in how the tight list status + is calculated. + +[0.29.0.gfm.10] + + * Fixed polynomial time complexity issue per + https://github.com/github/cmark-gfm/security/advisories/GHSA-r8vr-c48j-fcc5 + * Fixed polynomial time complexity issues per + https://github.com/github/cmark-gfm/security/advisories/GHSA-66g8-4hjf-77xh + + Note: these changes remove redundant bold tag nesting which may result + in existing rendering tests failing, e.g. rendering "____bold____" to html + will no longer yield "

bold

". + +[0.29.0.gfm.9] + + * Cleanup: Use of a private header was cleaned up (#248) + * Cleanup: Man page was updated (#255) + * Cleanup: Warnings for -Wstrict-prototypes were cleaned up (#285) + * Cleanup: We avoid header duplication (#289) + + * We now store positioning info for url_match (#201) + * We now expose cmark_parent_footnote_def for non-C renderers (#254) + * Footnote aria-label text now reference the specific footnote backref, and we include a data-footnote-backref-idx attribute so the label can be internationalized in a downstream filter (#307) + [0.29.0.gfm.8] * We restored backwards compatibility by deprecating the `cmark_init_standard_node_flags()` requirement, which is now a noop (#305) diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt index d5b819802..576ad0ba5 100644 --- a/extensions/CMakeLists.txt +++ b/extensions/CMakeLists.txt @@ -17,8 +17,6 @@ include_directories( ${PROJECT_BINARY_DIR}/src ) -include (GenerateExportHeader) - include_directories(include ${CMAKE_CURRENT_BINARY_DIR}) set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE} -pg") @@ -29,6 +27,7 @@ if (CMARK_SHARED) set_target_properties(${LIBRARY} PROPERTIES OUTPUT_NAME "cmark-gfm-extensions" + DEFINE_SYMBOL "cmark-gfm" SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}.gfm.${PROJECT_VERSION_GFM} VERSION ${PROJECT_VERSION}) @@ -38,9 +37,6 @@ if (CMARK_SHARED) # Avoid name clash between PROGRAM and LIBRARY pdb files. set_target_properties(${LIBRARY} PROPERTIES PDB_NAME cmark-gfm-extensions_dll) - generate_export_header(${LIBRARY} - BASE_NAME cmark-gfm-extensions) - list(APPEND CMARK_INSTALL ${LIBRARY}) target_link_libraries(${LIBRARY} libcmark-gfm) @@ -51,6 +47,7 @@ if (CMARK_STATIC) set_target_properties(${STATICLIBRARY} PROPERTIES COMPILE_FLAGS "-DCMARK_GFM_STATIC_DEFINE -DCMARK_GFM_EXTENSIONS_STATIC_DEFINE" + DEFINE_SYMBOL "cmark-gfm" POSITION_INDEPENDENT_CODE ON) if (MSVC) @@ -63,11 +60,6 @@ if (CMARK_STATIC) VERSION ${PROJECT_VERSION}) endif(MSVC) - if (NOT CMARK_SHARED) - generate_export_header(${STATICLIBRARY} - BASE_NAME cmark-gfm-extensions) - endif() - list(APPEND CMARK_INSTALL ${STATICLIBRARY}) endif() @@ -84,7 +76,6 @@ install(TARGETS ${CMARK_INSTALL} if (CMARK_SHARED OR CMARK_STATIC) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/cmark-gfm-core-extensions.h - ${CMAKE_CURRENT_SOURCE_DIR}/include/extensions-export.h DESTINATION include ) diff --git a/extensions/autolink.c b/extensions/autolink.c index f3b75a7e9..491d96c32 100644 --- a/extensions/autolink.c +++ b/extensions/autolink.c @@ -267,6 +267,11 @@ static cmark_node *url_match(cmark_parser *parser, cmark_node *parent, cmark_node *text = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem); text->as.literal = url; cmark_node_append_child(node, text); + + node->start_line = text->start_line = node->end_line = text->end_line = cmark_inline_parser_get_line(inline_parser); + + node->start_column = text->start_column = max_rewind - rewind; + node->end_column = text->end_column = cmark_inline_parser_get_column(inline_parser) - 1; return node; } diff --git a/extensions/include/cmark-gfm-core-extensions.h b/extensions/include/cmark-gfm-core-extensions.h index d85a89237..6f7aada65 100644 --- a/extensions/include/cmark-gfm-core-extensions.h +++ b/extensions/include/cmark-gfm-core-extensions.h @@ -6,40 +6,40 @@ extern "C" { #endif #include "cmark-gfm-extension_api.h" -#include "extensions-export.h" -#include "cmark-gfm_config.h" // for bool +#include "export.h" +#include #include -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT void cmark_gfm_core_extensions_ensure_registered(void); -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT uint16_t cmark_gfm_extensions_get_table_columns(cmark_node *node); /** Sets the number of columns for the table, returning 1 on success and 0 on error. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT int cmark_gfm_extensions_set_table_columns(cmark_node *node, uint16_t n_columns); -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT uint8_t *cmark_gfm_extensions_get_table_alignments(cmark_node *node); /** Sets the alignments for the table, returning 1 on success and 0 on error. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT int cmark_gfm_extensions_set_table_alignments(cmark_node *node, uint16_t ncols, uint8_t *alignments); -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT int cmark_gfm_extensions_get_table_row_is_header(cmark_node *node); /** Sets the column span for the table cell, returning 1 on success and 0 on error. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT int cmark_gfm_extensions_set_table_cell_colspan(cmark_node *node, unsigned colspan); /** Sets the row span for the table cell, returning 1 on success and 0 on error. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT int cmark_gfm_extensions_set_table_cell_rowspan(cmark_node *node, unsigned rowspan); /** @@ -50,7 +50,7 @@ int cmark_gfm_extensions_set_table_cell_rowspan(cmark_node *node, unsigned rowsp Column span is only parsed when \c CMARK_OPT_TABLE_SPANS is set. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT unsigned cmark_gfm_extensions_get_table_cell_colspan(cmark_node *node); /** @@ -61,22 +61,22 @@ unsigned cmark_gfm_extensions_get_table_cell_colspan(cmark_node *node); Row span is only parsed when \c CMARK_OPT_TABLE_SPANS is set. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT unsigned cmark_gfm_extensions_get_table_cell_rowspan(cmark_node *node); /** Sets whether the node is a table header row, returning 1 on success and 0 on error. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT int cmark_gfm_extensions_set_table_row_is_header(cmark_node *node, int is_header); -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT bool cmark_gfm_extensions_get_tasklist_item_checked(cmark_node *node); /* For backwards compatibility */ #define cmark_gfm_extensions_tasklist_is_checked cmark_gfm_extensions_get_tasklist_item_checked /** Sets whether a tasklist item is "checked" (completed), returning 1 on success and 0 on error. */ -CMARK_GFM_EXTENSIONS_EXPORT +CMARK_GFM_EXPORT int cmark_gfm_extensions_set_tasklist_item_checked(cmark_node *node, bool is_checked); #ifdef __cplusplus diff --git a/extensions/include/extensions-export.h b/extensions/include/extensions-export.h deleted file mode 100644 index f91b8564c..000000000 --- a/extensions/include/extensions-export.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef CMARK_GFM_EXTENSIONS_EXPORT_H -#define CMARK_GFM_EXTENSIONS_EXPORT_H - -#ifdef CMARK_USE_CMAKE_HEADERS -// if the CMake config header exists, use that instead of this Swift package prebuilt one -// we need to undefine the header guard, since export.h uses the same one -#undef CMARK_GFM_EXTENSIONS_EXPORT_H -#include "cmark-gfm-extensions_export.h" -#else - -#ifdef CMARK_GFM_EXTENSIONS_STATIC_DEFINE -# define CMARK_GFM_EXTENSIONS_EXPORT -# define CMARK_GFM_EXTENSIONS_NO_EXPORT -#else -# if defined(_WIN32) -# ifndef CMARK_GFM_EXTENSIONS_EXPORT -# ifdef libcmark_gfm_extensions_EXPORTS -# define CMARK_GFM_EXTENSIONS_EXPORT __declspec(dllexport) -# else -# define CMARK_GFM_EXTENSIONS_EXPORT __declspec(dllimport) -# endif -# endif - -# ifndef CMARK_GFM_EXTENSIONS_NO_EXPORT -# define CMARK_GFM_EXTENSIONS_NO_EXPORT -# endif -# else -# ifndef CMARK_GFM_EXTENSIONS_EXPORT -# ifdef libcmark_gfm_extensions_EXPORTS -# define CMARK_GFM_EXTENSIONS_EXPORT __attribute__((__visibility__("default"))) -# else -# define CMARK_GFM_EXTENSIONS_EXPORT __attribute__((__visibility__("default"))) -# endif -# endif - -# ifndef CMARK_GFM_EXTENSIONS_NO_EXPORT -# define CMARK_GFM_EXTENSIONS_NO_EXPORT __attribute__((__visibility__("hidden"))) -# endif -# endif -#endif - -#ifndef CMARK_GFM_EXTENSIONS_DEPRECATED -# if defined(_WIN32) -# define CMARK_GFM_EXTENSIONS_DEPRECATED __declspec(deprecated) -# else -# define CMARK_GFM_EXTENSIONS_DEPRECATED __attribute__ ((__deprecated__)) -# endif -#endif - -#ifndef CMARK_GFM_EXTENSIONS_DEPRECATED_EXPORT -# define CMARK_GFM_EXTENSIONS_DEPRECATED_EXPORT CMARK_GFM_EXTENSIONS_EXPORT CMARK_GFM_EXTENSIONS_DEPRECATED -#endif - -#ifndef CMARK_GFM_EXTENSIONS_DEPRECATED_NO_EXPORT -# define CMARK_GFM_EXTENSIONS_DEPRECATED_NO_EXPORT CMARK_GFM_EXTENSIONS_NO_EXPORT CMARK_GFM_EXTENSIONS_DEPRECATED -#endif - -#endif /* CMARK_GFM_EXTENSIONS_EXPORT_H */ - -#endif /* "cmark-gfm-extensions_export.h" */ diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt index 3cae79421..a9ed57aa5 100644 --- a/fuzz/CMakeLists.txt +++ b/fuzz/CMakeLists.txt @@ -19,3 +19,4 @@ macro(fuzzer name) endmacro() fuzzer(fuzz_quadratic) +fuzzer(fuzz_quadratic_brackets) diff --git a/fuzz/fuzz_quadratic.c b/fuzz/fuzz_quadratic.c index cab1b6330..523c0f444 100644 --- a/fuzz/fuzz_quadratic.c +++ b/fuzz/fuzz_quadratic.c @@ -75,8 +75,13 @@ int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { cmark_parser_feed(parser, markdown, markdown_size); cmark_node *doc = cmark_parser_finish(parser); - + free(cmark_render_html(doc, fuzz_config.options, NULL)); + free(cmark_render_xml(doc, fuzz_config.options)); + free(cmark_render_man(doc, fuzz_config.options, 80)); + free(cmark_render_commonmark(doc, fuzz_config.options, 80)); + free(cmark_render_plaintext(doc, fuzz_config.options, 80)); + free(cmark_render_latex(doc, fuzz_config.options, 80)); cmark_node_free(doc); cmark_parser_free(parser); diff --git a/fuzz/fuzz_quadratic_brackets.c b/fuzz/fuzz_quadratic_brackets.c new file mode 100644 index 000000000..5c1646464 --- /dev/null +++ b/fuzz/fuzz_quadratic_brackets.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include "cmark-gfm.h" +#include "cmark-gfm-core-extensions.h" +#include +#include +#include +#include + +const char *extension_names[] = { + "autolink", + "strikethrough", + "table", + "tagfilter", + NULL, +}; + +int LLVMFuzzerInitialize(int *argc, char ***argv) { + cmark_gfm_core_extensions_ensure_registered(); + return 0; +} + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + struct __attribute__((packed)) { + int options; + int width; + uint8_t startlen; + uint8_t openlen; + uint8_t middlelen; + uint8_t closelen; + } fuzz_config; + + if (size >= sizeof(fuzz_config)) { + /* The beginning of `data` is treated as fuzzer configuration */ + memcpy(&fuzz_config, data, sizeof(fuzz_config)); + + /* Test options that are used by GitHub. */ + fuzz_config.options = CMARK_OPT_UNSAFE | CMARK_OPT_FOOTNOTES | CMARK_OPT_GITHUB_PRE_LANG | CMARK_OPT_HARDBREAKS; + fuzz_config.openlen = fuzz_config.openlen & 0x7; + fuzz_config.middlelen = fuzz_config.middlelen & 0x7; + fuzz_config.closelen = fuzz_config.closelen & 0x7; + + /* Remainder of input is the markdown */ + const char *markdown0 = (const char *)(data + sizeof(fuzz_config)); + const size_t markdown_size0 = size - sizeof(fuzz_config); + char markdown[0x80000]; + if (markdown_size0 <= sizeof(markdown)) { + size_t markdown_size = 0; + const size_t componentslen = fuzz_config.startlen + fuzz_config.openlen + fuzz_config.middlelen + fuzz_config.closelen; + if (componentslen <= markdown_size0) { + size_t offset = 0; + const size_t endlen = markdown_size0 - componentslen; + memcpy(&markdown[markdown_size], &markdown0[offset], fuzz_config.startlen); + markdown_size += fuzz_config.startlen; + offset += fuzz_config.startlen; + + if (0 < fuzz_config.openlen) { + while (markdown_size + fuzz_config.openlen <= sizeof(markdown)/2) { + memcpy(&markdown[markdown_size], &markdown0[offset], + fuzz_config.openlen); + markdown_size += fuzz_config.openlen; + } + offset += fuzz_config.openlen; + } + memcpy(&markdown[markdown_size], &markdown0[offset], + fuzz_config.middlelen); + markdown_size += fuzz_config.middlelen; + offset += fuzz_config.middlelen; + if (0 < fuzz_config.closelen) { + while (markdown_size + fuzz_config.closelen + endlen <= sizeof(markdown)) { + memcpy(&markdown[markdown_size], &markdown0[offset], + fuzz_config.closelen); + markdown_size += fuzz_config.closelen; + } + offset += fuzz_config.closelen; + } + if (markdown_size + endlen <= sizeof(markdown)) { + memcpy(&markdown[markdown_size], &markdown0[offset], + endlen); + markdown_size += endlen; + } + } else { + markdown_size = markdown_size0; + memcpy(markdown, markdown0, markdown_size); + } + + cmark_parser *parser = cmark_parser_new(fuzz_config.options); + + for (const char **it = extension_names; *it; ++it) { + const char *extension_name = *it; + cmark_syntax_extension *syntax_extension = cmark_find_syntax_extension(extension_name); + if (!syntax_extension) { + fprintf(stderr, "%s is not a valid syntax extension\n", extension_name); + abort(); + } + cmark_parser_attach_syntax_extension(parser, syntax_extension); + } + + cmark_parser_feed(parser, markdown, markdown_size); + cmark_node *doc = cmark_parser_finish(parser); + + free(cmark_render_html(doc, fuzz_config.options, NULL)); + + cmark_node_free(doc); + cmark_parser_free(parser); + } + } + return 0; +} diff --git a/man/man1/cmark-gfm.1 b/man/man1/cmark-gfm.1 index 5c68d79c3..4fca62732 100644 --- a/man/man1/cmark-gfm.1 +++ b/man/man1/cmark-gfm.1 @@ -58,10 +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 \-\-safe -Do not render raw HTML or potentially dangerous URLs. -(Raw HTML is replaced by a placeholder comment; potentially -dangerous URLs are replaced by empty strings.) Dangerous +.B \-\-unsafe +Render raw HTML and potentially dangerous URLs. +(Raw HTML is not replaced by a placeholder comment; potentially +dangerous URLs are not replaced by empty strings.) Dangerous URLs are those that begin with `javascript:`, `vbscript:`, `file:`, or `data:` (except for `image/png`, `image/gif`, `image/jpeg`, or `image/webp` mime types). diff --git a/src/arena.c b/src/arena.c index c1cea10eb..86bf5b749 100644 --- a/src/arena.c +++ b/src/arena.c @@ -118,6 +118,6 @@ static void arena_free(void *ptr) { cmark_mem CMARK_ARENA_MEM_ALLOCATOR = {arena_calloc, arena_realloc, arena_free}; -cmark_mem *cmark_get_arena_mem_allocator() { +cmark_mem *cmark_get_arena_mem_allocator(void) { return &CMARK_ARENA_MEM_ALLOCATOR; } diff --git a/src/blocks.c b/src/blocks.c index 49bcbce39..a7290652c 100644 --- a/src/blocks.c +++ b/src/blocks.c @@ -27,6 +27,14 @@ #define CODE_INDENT 4 #define TAB_STOP 4 +/** + * Very deeply nested lists can cause quadratic performance issues. + * This constant is used in open_new_blocks() to limit the nesting + * depth. It is unlikely that a non-contrived markdown document will + * be nested this deeply. + */ +#define MAX_LIST_DEPTH 100 + #ifndef MIN #define MIN(x, y) ((x < y) ? x : y) #endif @@ -1148,10 +1156,11 @@ static void open_new_blocks(cmark_parser *parser, cmark_node **container, bool has_content; int save_offset; int save_column; + size_t depth = 0; while (cont_type != CMARK_NODE_CODE_BLOCK && cont_type != CMARK_NODE_HTML_BLOCK) { - + depth++; S_find_first_nonspace(parser, input); indented = parser->indent >= CODE_INDENT; @@ -1253,6 +1262,7 @@ static void open_new_blocks(cmark_parser *parser, cmark_node **container, (*container)->internal_offset = matched; } else if ((!indented || cont_type == CMARK_NODE_LIST) && parser->indent < 4 && + depth < MAX_LIST_DEPTH && (matched = parse_list_marker( parser->mem, input, parser->first_nonspace, (*container)->type == CMARK_NODE_PARAGRAPH, &data))) { diff --git a/src/cmark.c b/src/cmark.c index 08477fa57..f2619f429 100644 --- a/src/cmark.c +++ b/src/cmark.c @@ -10,9 +10,9 @@ cmark_node_type CMARK_NODE_LAST_BLOCK = CMARK_NODE_FOOTNOTE_DEFINITION; cmark_node_type CMARK_NODE_LAST_INLINE = CMARK_NODE_ATTRIBUTE; -int cmark_version() { return CMARK_GFM_VERSION; } +int cmark_version(void) { return CMARK_GFM_VERSION; } -const char *cmark_version_string() { return CMARK_GFM_VERSION_STRING; } +const char *cmark_version_string(void) { return CMARK_GFM_VERSION_STRING; } static void *xcalloc(size_t nmem, size_t size) { void *ptr = calloc(nmem, size); @@ -38,7 +38,7 @@ static void xfree(void *ptr) { cmark_mem CMARK_DEFAULT_MEM_ALLOCATOR = {xcalloc, xrealloc, xfree}; -cmark_mem *cmark_get_default_mem_allocator() { +cmark_mem *cmark_get_default_mem_allocator(void) { return &CMARK_DEFAULT_MEM_ALLOCATOR; } diff --git a/src/commonmark.c b/src/commonmark.c index 328da12a3..d296567e0 100644 --- a/src/commonmark.c +++ b/src/commonmark.c @@ -154,23 +154,8 @@ static bool is_autolink(cmark_node *node) { link_text->as.literal.len) == 0); } -// if node is a block node, returns node. -// otherwise returns first block-level node that is an ancestor of node. -// if there is no block-level ancestor, returns NULL. -static cmark_node *get_containing_block(cmark_node *node) { - while (node) { - if (CMARK_NODE_BLOCK_P(node)) { - return node; - } else { - node = node->parent; - } - } - return NULL; -} - static int S_render_node(cmark_renderer *renderer, cmark_node *node, cmark_event_type ev_type, int options) { - cmark_node *tmp; int list_number; cmark_delim_type list_delim; int numticks; @@ -190,14 +175,17 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, // Don't adjust tight list status til we've started the list. // Otherwise we loose the blank line between a paragraph and // a following list. - if (!(node->type == CMARK_NODE_ITEM && node->prev == NULL && entering)) { - tmp = get_containing_block(node); - renderer->in_tight_list_item = - tmp && // tmp might be NULL if there is no containing block - ((tmp->type == CMARK_NODE_ITEM && - cmark_node_get_list_tight(tmp->parent)) || - (tmp && tmp->parent && tmp->parent->type == CMARK_NODE_ITEM && - cmark_node_get_list_tight(tmp->parent->parent))); + if (entering) { + if (node->parent && node->parent->type == CMARK_NODE_ITEM) { + renderer->in_tight_list_item = node->parent->parent->as.list.tight; + } + } else { + if (node->type == CMARK_NODE_LIST) { + renderer->in_tight_list_item = + node->parent && + node->parent->type == CMARK_NODE_ITEM && + node->parent->parent->as.list.tight; + } } if (node->extension && node->extension->commonmark_render_func) { @@ -235,13 +223,8 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, if (cmark_node_get_list_type(node->parent) == CMARK_BULLET_LIST) { marker_width = 4; } else { - list_number = cmark_node_get_list_start(node->parent); + list_number = cmark_node_get_item_index(node); list_delim = cmark_node_get_list_delim(node->parent); - tmp = node; - while (tmp->prev) { - tmp = tmp->prev; - list_number += 1; - } // we ensure a width of at least 4 so // we get nice transition from single digits // to double @@ -406,10 +389,12 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, break; case CMARK_NODE_STRONG: - if (entering) { - LIT("**"); - } else { - LIT("**"); + if (node->parent == NULL || node->parent->type != CMARK_NODE_STRONG) { + if (entering) { + LIT("**"); + } else { + LIT("**"); + } } break; diff --git a/src/html.c b/src/html.c index 96daa18e2..f1f2ff774 100644 --- a/src/html.c +++ b/src/html.c @@ -63,10 +63,16 @@ static bool S_put_footnote_backref(cmark_html_renderer *renderer, cmark_strbuf * if (renderer->written_footnote_ix >= renderer->footnote_ix) return false; renderer->written_footnote_ix = renderer->footnote_ix; + char m[32]; + snprintf(m, sizeof(m), "%d", renderer->written_footnote_ix); cmark_strbuf_puts(html, "as.literal.data, node->as.literal.len); - cmark_strbuf_puts(html, "\" class=\"footnote-backref\" data-footnote-backref aria-label=\"Back to content\">↩"); + cmark_strbuf_puts(html, "\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\""); + cmark_strbuf_puts(html, m); + cmark_strbuf_puts(html, "\" aria-label=\"Back to reference "); + cmark_strbuf_puts(html, m); + cmark_strbuf_puts(html, "\">↩"); if (node->footnote.def_count > 1) { @@ -78,7 +84,15 @@ static bool S_put_footnote_backref(cmark_html_renderer *renderer, cmark_strbuf * houdini_escape_href(html, node->as.literal.data, node->as.literal.len); cmark_strbuf_puts(html, "-"); cmark_strbuf_puts(html, n); - cmark_strbuf_puts(html, "\" class=\"footnote-backref\" data-footnote-backref aria-label=\"Back to content\">↩"); + cmark_strbuf_puts(html, "\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\""); + cmark_strbuf_puts(html, m); + cmark_strbuf_puts(html, "-"); + cmark_strbuf_puts(html, n); + cmark_strbuf_puts(html, "\" aria-label=\"Back to reference "); + cmark_strbuf_puts(html, m); + cmark_strbuf_puts(html, "-"); + cmark_strbuf_puts(html, n); + cmark_strbuf_puts(html, "\">↩"); cmark_strbuf_puts(html, n); cmark_strbuf_puts(html, ""); } @@ -350,10 +364,12 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node, break; case CMARK_NODE_STRONG: - if (entering) { - cmark_strbuf_puts(html, ""); - } else { - cmark_strbuf_puts(html, ""); + if (node->parent == NULL || node->parent->type != CMARK_NODE_STRONG) { + if (entering) { + cmark_strbuf_puts(html, ""); + } else { + cmark_strbuf_puts(html, ""); + } } break; diff --git a/src/include/cmark-gfm.h b/src/include/cmark-gfm.h index 4513a19a1..43b8893c8 100644 --- a/src/include/cmark-gfm.h +++ b/src/include/cmark-gfm.h @@ -230,6 +230,11 @@ CMARK_GFM_EXPORT cmark_node *cmark_node_last_child(cmark_node *node); */ CMARK_GFM_EXPORT cmark_node *cmark_node_nth_child(cmark_node *node, int n); +/** Returns the footnote reference of 'node', or NULL if 'node' doesn't have a + * footnote reference. + */ +CMARK_GFM_EXPORT cmark_node *cmark_node_parent_footnote_def(cmark_node *node); + /** * ## Iterator * @@ -418,6 +423,17 @@ CMARK_GFM_EXPORT int cmark_node_get_list_tight(cmark_node *node); */ CMARK_GFM_EXPORT int cmark_node_set_list_tight(cmark_node *node, int tight); +/** + * Returns item index of 'node'. This is only used when rendering output + * formats such as commonmark, which need to output the index. It is not + * required for formats such as html or latex. + */ +CMARK_GFM_EXPORT int cmark_node_get_item_index(cmark_node *node); + +/** Sets item index of 'node'. Returns 1 on success, 0 on failure. + */ +CMARK_GFM_EXPORT int cmark_node_set_item_index(cmark_node *node, int idx); + /** 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/include/node.h b/src/include/node.h index e4a849965..76c5b7992 100644 --- a/src/include/node.h +++ b/src/include/node.h @@ -87,6 +87,14 @@ struct cmark_node { cmark_syntax_extension *extension; + /** + * Used during cmark_render() to cache the most recent non-NULL + * extension, if you go up the parent chain like this: + * + * node->parent->...parent->extension + */ + cmark_syntax_extension *ancestor_extension; + union { int ref_ix; int def_count; @@ -125,7 +133,7 @@ void cmark_register_node_flag(cmark_node_internal_flags *flags); * library. It is now a no-op. */ CMARK_GFM_EXPORT -void cmark_init_standard_node_flags(); +void cmark_init_standard_node_flags(void); static CMARK_INLINE cmark_mem *cmark_node_mem(cmark_node *node) { return node->content.mem; @@ -150,6 +158,13 @@ static CMARK_INLINE bool CMARK_NODE_INLINE_P(cmark_node *node) { CMARK_GFM_EXPORT bool cmark_node_can_contain_type(cmark_node *node, cmark_node_type child_type); +/** + * Enable (or disable) extra safety checks. These extra checks cause + * extra performance overhead (in some cases quadratic), so they are only + * intended to be used during testing. + */ +CMARK_GFM_EXPORT void cmark_enable_safety_checks(bool enable); + #ifdef __cplusplus } #endif diff --git a/src/latex.c b/src/latex.c index c204f6003..87e7dd32b 100644 --- a/src/latex.c +++ b/src/latex.c @@ -385,10 +385,12 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, break; case CMARK_NODE_STRONG: - if (entering) { - LIT("\\textbf{"); - } else { - LIT("}"); + if (node->parent == NULL || node->parent->type != CMARK_NODE_STRONG) { + if (entering) { + LIT("\\textbf{"); + } else { + LIT("}"); + } } break; diff --git a/src/man.c b/src/man.c index c60e1bfe7..b92cbd803 100644 --- a/src/man.c +++ b/src/man.c @@ -74,7 +74,6 @@ static void S_outc(cmark_renderer *renderer, cmark_node *node, static int S_render_node(cmark_renderer *renderer, cmark_node *node, cmark_event_type ev_type, int options) { - cmark_node *tmp; int list_number; bool entering = (ev_type == CMARK_EVENT_ENTER); bool allow_wrap = renderer->width > 0 && !(CMARK_OPT_NOBREAKS & options); @@ -123,12 +122,7 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, if (cmark_node_get_list_type(node->parent) == CMARK_BULLET_LIST) { LIT("\\[bu] 2"); } else { - list_number = cmark_node_get_list_start(node->parent); - tmp = node; - while (tmp->prev) { - tmp = tmp->prev; - list_number += 1; - } + list_number = cmark_node_get_item_index(node); char list_number_s[LIST_NUMBER_SIZE]; snprintf(list_number_s, LIST_NUMBER_SIZE, "\"%d.\" 4", list_number); LIT(list_number_s); @@ -225,10 +219,12 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, break; case CMARK_NODE_STRONG: - if (entering) { - LIT("\\f[B]"); - } else { - LIT("\\f[]"); + if (node->parent == NULL || node->parent->type != CMARK_NODE_STRONG) { + if (entering) { + LIT("\\f[B]"); + } else { + LIT("\\f[]"); + } } break; diff --git a/src/node.c b/src/node.c index a4b67121e..d93ed307d 100644 --- a/src/node.c +++ b/src/node.c @@ -7,6 +7,30 @@ #include "syntax_extension.h" CMARK_DEFINE_LOCK(nextflag) +CMARK_DEFINE_LOCK(safety) + +/** + * Expensive safety checks are off by default, but can be enabled + * by calling cmark_enable_safety_checks(). + */ +static bool enable_safety_checks = false; + +void cmark_enable_safety_checks(bool enable) { + CMARK_INITIALIZE_AND_LOCK(safety); + enable_safety_checks = enable; + CMARK_UNLOCK(safety); +} + +/** + * Check whether safety checks have been enabled in a function to guard access + * behind a lock. + */ +static bool S_safety_checks_enabled() { + CMARK_INITIALIZE_AND_LOCK(safety); + bool safety_enabled = enable_safety_checks; + CMARK_UNLOCK(safety); + return safety_enabled; +} static void S_node_unlink(cmark_node *node); @@ -36,7 +60,7 @@ void cmark_register_node_flag(cmark_node_internal_flags *flags) { CMARK_UNLOCK(nextflag); } -void cmark_init_standard_node_flags() {} +void cmark_init_standard_node_flags(void) {} bool cmark_node_can_contain_type(cmark_node *node, cmark_node_type child_type) { if (child_type == CMARK_NODE_DOCUMENT) { @@ -78,8 +102,6 @@ bool cmark_node_can_contain_type(cmark_node *node, cmark_node_type child_type) { } static bool S_can_contain(cmark_node *node, cmark_node *child) { - cmark_node *cur; - if (node == NULL || child == NULL) { return false; } @@ -87,14 +109,16 @@ static bool S_can_contain(cmark_node *node, cmark_node *child) { return 0; } - // Verify that child is not an ancestor of node or equal to node. - cur = node; - do { - if (cur == child) { - return false; - } - cur = cur->parent; - } while (cur != NULL); + if (S_safety_checks_enabled()) { + // Verify that child is not an ancestor of node or equal to node. + cmark_node *cur = node; + do { + if (cur == child) { + return false; + } + cur = cur->parent; + } while (cur != NULL); + } return cmark_node_can_contain_type(node, (cmark_node_type) child->type); } @@ -349,6 +373,14 @@ cmark_node *cmark_node_nth_child(cmark_node *node, int n) { return ret; } +cmark_node *cmark_node_parent_footnote_def(cmark_node *node) { + if (node == NULL) { + return NULL; + } else { + return node->parent_footnote_def; + } +} + void *cmark_node_get_user_data(cmark_node *node) { if (node == NULL) { return NULL; @@ -576,6 +608,31 @@ int cmark_node_set_list_tight(cmark_node *node, int tight) { } } +int cmark_node_get_item_index(cmark_node *node) { + if (node == NULL) { + return 0; + } + + if (node->type == CMARK_NODE_ITEM) { + return node->as.list.start; + } else { + return 0; + } +} + +int cmark_node_set_item_index(cmark_node *node, int idx) { + if (node == NULL || idx < 0) { + return 0; + } + + if (node->type == CMARK_NODE_ITEM) { + node->as.list.start = idx; + return 1; + } else { + return 0; + } +} + const char *cmark_node_get_fence_info(cmark_node *node) { if (node == NULL) { return NULL; diff --git a/src/plaintext.c b/src/plaintext.c index 0edbd1406..c9aa0fd6e 100644 --- a/src/plaintext.c +++ b/src/plaintext.c @@ -16,23 +16,8 @@ static CMARK_INLINE void outc(cmark_renderer *renderer, cmark_node *node, cmark_render_code_point(renderer, c); } -// if node is a block node, returns node. -// otherwise returns first block-level node that is an ancestor of node. -// if there is no block-level ancestor, returns NULL. -static cmark_node *get_containing_block(cmark_node *node) { - while (node) { - if (CMARK_NODE_BLOCK_P(node)) { - return node; - } else { - node = node->parent; - } - } - return NULL; -} - static int S_render_node(cmark_renderer *renderer, cmark_node *node, cmark_event_type ev_type, int options) { - cmark_node *tmp; int list_number; cmark_delim_type list_delim; int i; @@ -46,14 +31,17 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, // Don't adjust tight list status til we've started the list. // Otherwise we loose the blank line between a paragraph and // a following list. - if (!(node->type == CMARK_NODE_ITEM && node->prev == NULL && entering)) { - tmp = get_containing_block(node); - renderer->in_tight_list_item = - tmp && // tmp might be NULL if there is no containing block - ((tmp->type == CMARK_NODE_ITEM && - cmark_node_get_list_tight(tmp->parent)) || - (tmp && tmp->parent && tmp->parent->type == CMARK_NODE_ITEM && - cmark_node_get_list_tight(tmp->parent->parent))); + if (entering) { + if (node->parent && node->parent->type == CMARK_NODE_ITEM) { + renderer->in_tight_list_item = node->parent->parent->as.list.tight; + } + } else { + if (node->type == CMARK_NODE_LIST) { + renderer->in_tight_list_item = + node->parent && + node->parent->type == CMARK_NODE_ITEM && + node->parent->parent->as.list.tight; + } } if (node->extension && node->extension->plaintext_render_func) { @@ -79,13 +67,8 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, if (cmark_node_get_list_type(node->parent) == CMARK_BULLET_LIST) { marker_width = 4; } else { - list_number = cmark_node_get_list_start(node->parent); + list_number = cmark_node_get_item_index(node); list_delim = cmark_node_get_list_delim(node->parent); - tmp = node; - while (tmp->prev) { - tmp = tmp->prev; - list_number += 1; - } // we ensure a width of at least 4 so // we get nice transition from single digits // to double diff --git a/src/render.c b/src/render.c index 02e9e838b..1a0d2ae8d 100644 --- a/src/render.c +++ b/src/render.c @@ -31,13 +31,7 @@ static void S_out(cmark_renderer *renderer, cmark_node *node, cmark_chunk remainder = cmark_chunk_literal(""); int k = renderer->buffer->size - 1; - cmark_syntax_extension *ext = NULL; - cmark_node *n = node; - while (n && !ext) { - ext = n->extension; - if (!ext) - n = n->parent; - } + cmark_syntax_extension *ext = node->ancestor_extension; if (ext && !ext->commonmark_escape_func) ext = NULL; @@ -182,6 +176,20 @@ char *cmark_render(cmark_mem *mem, cmark_node *root, int options, int width, while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) { cur = cmark_iter_get_node(iter); + if (cur->extension) { + cur->ancestor_extension = cur->extension; + } else if (cur->parent) { + cur->ancestor_extension = cur->parent->ancestor_extension; + } + if (cur->type == CMARK_NODE_ITEM) { + // Calculate the list item's index, for the benefit of output formats + // like commonmark and plaintext. + if (cur->prev) { + cmark_node_set_item_index(cur, 1 + cmark_node_get_item_index(cur->prev)); + } else { + cmark_node_set_item_index(cur, cmark_node_get_list_start(cur->parent)); + } + } if (!render_node(&renderer, cur, ev_type, options)) { // a false value causes us to skip processing // the node's contents. this is used for diff --git a/src/xml.c b/src/xml.c index d572290ae..184bff486 100644 --- a/src/xml.c +++ b/src/xml.c @@ -11,6 +11,7 @@ #include "syntax_extension.h" #define BUFFER_SIZE 100 +#define MAX_INDENT 40 // Functions to convert cmark_nodes to XML strings. @@ -26,7 +27,7 @@ struct render_state { static CMARK_INLINE void indent(struct render_state *state) { int i; - for (i = 0; i < state->indent; i++) { + for (i = 0; i < state->indent && i < MAX_INDENT; i++) { cmark_strbuf_putc(state->xml, ' '); } } diff --git a/test/extensions.txt b/test/extensions.txt index a1a150faa..fe98fe548 100644 --- a/test/extensions.txt +++ b/test/extensions.txt @@ -737,7 +737,7 @@ Hi!
  1. -

    Some bolded footnote definition.

    +

    Some bolded footnote definition.

  2. @@ -745,15 +745,15 @@ Hi!
    as well as code blocks
     
    -

    or, naturally, simple paragraphs.

    +

    or, naturally, simple paragraphs.

  3. -

    no code block here (spaces are stripped away)

    +

    no code block here (spaces are stripped away)

  4. this is now a code block (8 spaces indentation)
     
    - +
@@ -773,7 +773,7 @@ This footnote is referenced[^a-footnote] multiple times, in lots of different pl
  1. -

    This footnote definition should have three backrefs. 2 3

    +

    This footnote definition should have three backrefs. 2 3

@@ -790,7 +790,7 @@ Hello[^">]
  1. -

    pwned

    +

    pwned

diff --git a/test/regression.txt b/test/regression.txt index 844da60a2..0ec493d3d 100644 --- a/test/regression.txt +++ b/test/regression.txt @@ -4,8 +4,7 @@ Issue #113: EOL character weirdness on Windows (Important: first line ends with CR + CR + LF) ```````````````````````````````` example -line1 - +line1 line2 .

line1

@@ -194,7 +193,7 @@ A footnote in a paragraph[^1]
  1. -

    a footnote 2

    +

    a footnote 2

@@ -284,10 +283,10 @@ This is some text. It has a citation.[^citation]
  1. -

    This is a long winded parapgraph that also has another citation.2

    +

    This is a long winded parapgraph that also has another citation.2

  2. -

    My second citation.

    +

    My second citation.

@@ -306,10 +305,10 @@ This is some text. It has two footnotes references, side-by-side without any spa
  1. -

    Hello.

    +

    Hello.

  2. -

    Goodbye.

    +

    Goodbye.

@@ -331,10 +330,10 @@ It has another footnote that contains many different characters (the autolinker
  1. -

    this renders properly.

    +

    this renders properly.

  2. -

    so does this.

    +

    so does this.

diff --git a/test/spec.txt b/test/spec.txt index eaab9032d..d42f3369e 100644 --- a/test/spec.txt +++ b/test/spec.txt @@ -6926,7 +6926,7 @@ foo__bar__ ```````````````````````````````` example __foo, __bar__, baz__ . -

foo, bar, baz

+

foo, bar, baz

```````````````````````````````` @@ -7197,7 +7197,7 @@ foo***bar***baz ```````````````````````````````` example foo******bar*********baz . -

foobar***baz

+

foobar***baz

```````````````````````````````` @@ -7268,21 +7268,21 @@ __foo _bar_ baz__ ```````````````````````````````` example __foo __bar__ baz__ . -

foo bar baz

+

foo bar baz

```````````````````````````````` ```````````````````````````````` example ____foo__ bar__ . -

foo bar

+

foo bar

```````````````````````````````` ```````````````````````````````` example **foo **bar**** . -

foo bar

+

foo bar

```````````````````````````````` @@ -7567,14 +7567,14 @@ switching delimiters: ```````````````````````````````` example ****foo**** . -

foo

+

foo

```````````````````````````````` ```````````````````````````````` example ____foo____ . -

foo

+

foo

```````````````````````````````` @@ -7585,7 +7585,7 @@ delimiters: ```````````````````````````````` example ******foo****** . -

foo

+

foo

```````````````````````````````` @@ -7601,7 +7601,7 @@ Rule 14: ```````````````````````````````` example _____foo_____ . -

foo

+

foo

````````````````````````````````