diff --git a/api_test/main.c b/api_test/main.c index 66ee0110f..5ad9a66bf 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -306,6 +306,117 @@ static void inline_code_info(test_batch_runner *runner) { cmark_node_free(doc); } +static void node_clone(test_batch_runner *runner) { + static const char markdown[] = + "## Heading\n\nruby:`Object.new` and [link](url 'title')\n"; + cmark_node *doc = cmark_parse_document(markdown, sizeof(markdown) - 1, + CMARK_OPT_INLINE_CODE_INFO); + cmark_node *clone = cmark_node_clone(doc); + + OK(runner, clone != NULL, "clone a document"); + OK(runner, clone != doc, "clone is independent"); + + cmark_node *paragraph = cmark_node_next(cmark_node_first_child(clone)); + cmark_node *code = cmark_node_first_child(paragraph); + STR_EQ(runner, cmark_node_get_code_info(code), "ruby", + "clone inline code info"); + STR_EQ(runner, cmark_node_get_literal(code), "Object.new", + "clone inline code literal"); + + cmark_node_set_literal(code, "Class.new"); + cmark_node *original_paragraph = cmark_node_next(cmark_node_first_child(doc)); + cmark_node *original_code = cmark_node_first_child(original_paragraph); + STR_EQ(runner, cmark_node_get_literal(original_code), "Object.new", + "mutating clone does not change source"); + + cmark_node *paragraph_clone = cmark_node_clone(original_paragraph); + OK(runner, paragraph_clone != NULL, "clone child node"); + + cmark_node_free(doc); + char *html = cmark_render_html(clone, CMARK_OPT_DEFAULT, NULL); + STR_EQ(runner, html, + "

Heading

\n" + "

Class.new and " + "link

\n", + "clone remains valid after source is freed"); + free(html); + cmark_node_free(clone); + + html = cmark_render_html(paragraph_clone, CMARK_OPT_DEFAULT, NULL); + STR_EQ(runner, html, + "

Object.new and " + "link

\n", + "render cloned child node"); + free(html); + cmark_node_free(paragraph_clone); + + OK(runner, cmark_node_clone(NULL) == NULL, "cloning null returns null"); +} + +static void node_clone_extensions(test_batch_runner *runner) { + static const char markdown[] = + "| Left | Right |\n| :--- | ---: |\n| A | B |\n"; + + cmark_gfm_core_extensions_ensure_registered(); + cmark_syntax_extension *table_extension = + cmark_find_syntax_extension("table"); + cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT); + cmark_parser_attach_syntax_extension(parser, table_extension); + cmark_parser_feed(parser, markdown, sizeof(markdown) - 1); + cmark_node *doc = cmark_parser_finish(parser); + cmark_parser_free(parser); + + cmark_node *clone = cmark_node_clone(doc); + OK(runner, clone != NULL, "clone extension nodes"); + + cmark_node *table = cmark_node_first_child(doc); + cmark_node *cloned_table = cmark_node_first_child(clone); + INT_EQ(runner, cmark_gfm_extensions_get_table_columns(cloned_table), 2, + "clone table column count"); + + uint8_t *alignments = cmark_gfm_extensions_get_table_alignments(table); + uint8_t *cloned_alignments = + cmark_gfm_extensions_get_table_alignments(cloned_table); + OK(runner, cloned_alignments != alignments, + "clone table alignments independently"); + INT_EQ(runner, cloned_alignments[0], alignments[0], + "clone left table alignment"); + INT_EQ(runner, cloned_alignments[1], alignments[1], + "clone right table alignment"); + + cmark_node *header = cmark_node_first_child(cloned_table); + OK(runner, cmark_gfm_extensions_get_table_row_is_header(header), + "clone table header metadata"); + + cmark_node_free(doc); + INT_EQ(runner, cmark_gfm_extensions_get_table_columns(cloned_table), 2, + "cloned table remains valid after source is freed"); + + cmark_mem *mem = cmark_get_default_mem_allocator(); + cmark_llist *extensions = NULL; + extensions = cmark_llist_append(mem, extensions, table_extension); + char *html = cmark_render_html(clone, CMARK_OPT_DEFAULT, extensions); + STR_EQ(runner, html, + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
LeftRight
AB
\n", + "render cloned table"); + free(html); + cmark_llist_free(mem, extensions); + cmark_node_free(clone); +} + static void node_check(test_batch_runner *runner) { // Construct an incomplete tree. cmark_node *doc = cmark_node_new(CMARK_NODE_DOCUMENT); @@ -1264,6 +1375,8 @@ int main() { constructor(runner); accessors(runner); inline_code_info(runner); + node_clone(runner); + node_clone_extensions(runner); node_check(runner); iterator(runner); iterator_delete(runner); diff --git a/extensions/table.c b/extensions/table.c index e8359f259..1df8018b7 100644 --- a/extensions/table.c +++ b/extensions/table.c @@ -842,6 +842,29 @@ static void opaque_free(cmark_syntax_extension *self, cmark_mem *mem, cmark_node } } +static void opaque_copy(cmark_syntax_extension *self, cmark_mem *mem, + cmark_node *node, cmark_node *source) { + if (node->type == CMARK_NODE_TABLE) { + node_table *table = (node_table *)node->as.opaque; + node_table *source_table = (node_table *)source->as.opaque; + + table->n_columns = source_table->n_columns; + table->n_rows = source_table->n_rows; + table->n_nonempty_cells = source_table->n_nonempty_cells; + + if (source_table->alignments) { + table->alignments = mem->calloc(source_table->n_columns, sizeof(uint8_t)); + memcpy(table->alignments, source_table->alignments, + source_table->n_columns * sizeof(uint8_t)); + } + } else if (node->type == CMARK_NODE_TABLE_ROW) { + *(node_table_row *)node->as.opaque = *(node_table_row *)source->as.opaque; + } else if (node->type == CMARK_NODE_TABLE_CELL) { + mem->free(node->as.opaque); + node->as.cell_index = source->as.cell_index; + } +} + static int escape(cmark_syntax_extension *self, cmark_node *node, int c) { return node->type != CMARK_NODE_TABLE && @@ -867,6 +890,7 @@ cmark_syntax_extension *create_table_extension(void) { cmark_syntax_extension_set_html_render_func(self, html_render); cmark_syntax_extension_set_opaque_alloc_func(self, opaque_alloc); cmark_syntax_extension_set_opaque_free_func(self, opaque_free); + cmark_syntax_extension_set_opaque_copy_func(self, opaque_copy); cmark_syntax_extension_set_commonmark_escape_func(self, escape); CMARK_NODE_TABLE = cmark_syntax_extension_add_node(0); CMARK_NODE_TABLE_ROW = cmark_syntax_extension_add_node(0); diff --git a/src/cmark-gfm-extension_api.h b/src/cmark-gfm-extension_api.h index bb92a59bf..3aeded75d 100644 --- a/src/cmark-gfm-extension_api.h +++ b/src/cmark-gfm-extension_api.h @@ -264,6 +264,10 @@ typedef void (*cmark_opaque_free_func) (cmark_syntax_extension *extension, cmark_mem *mem, cmark_node *node); +typedef void (*cmark_opaque_copy_func)(cmark_syntax_extension *extension, + cmark_mem *mem, cmark_node *node, + cmark_node *source); + /** Free a cmark_syntax_extension. */ CMARK_GFM_EXPORT @@ -406,6 +410,12 @@ CMARK_GFM_EXPORT void cmark_syntax_extension_set_opaque_free_func(cmark_syntax_extension *extension, cmark_opaque_free_func func); +/** See the documentation for 'cmark_syntax_extension' + */ +CMARK_GFM_EXPORT +void cmark_syntax_extension_set_opaque_copy_func( + cmark_syntax_extension *extension, cmark_opaque_copy_func func); + /** See the documentation for 'cmark_syntax_extension' */ CMARK_GFM_EXPORT diff --git a/src/cmark-gfm.h b/src/cmark-gfm.h index c0fe68678..054e981c7 100644 --- a/src/cmark-gfm.h +++ b/src/cmark-gfm.h @@ -196,6 +196,12 @@ CMARK_GFM_EXPORT cmark_node *cmark_node_new_with_mem_and_ext(cmark_node_type typ cmark_mem *mem, cmark_syntax_extension *extension); +/** Creates an independent deep copy of 'node' and all its children. + * User data is not copied. Returns NULL when extension-specific node data + * cannot be copied. + */ +CMARK_GFM_EXPORT cmark_node *cmark_node_clone(cmark_node *node); + /** Frees the memory allocated for a node and any children. */ CMARK_GFM_EXPORT void cmark_node_free(cmark_node *node); diff --git a/src/html.c b/src/html.c index 104a61835..6257b0a81 100644 --- a/src/html.c +++ b/src/html.c @@ -303,7 +303,7 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node, case CMARK_NODE_PARAGRAPH: parent = cmark_node_parent(node); - grandparent = cmark_node_parent(parent); + grandparent = parent ? cmark_node_parent(parent) : NULL; if (grandparent != NULL && grandparent->type == CMARK_NODE_LIST) { tight = grandparent->as.list.tight; } else { @@ -316,7 +316,8 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node, cmark_html_render_sourcepos(node, html, options); cmark_strbuf_putc(html, '>'); } else { - if (parent->type == CMARK_NODE_FOOTNOTE_DEFINITION && node->next == NULL) { + if (parent && parent->type == CMARK_NODE_FOOTNOTE_DEFINITION && + node->next == NULL) { cmark_strbuf_putc(html, ' '); S_put_footnote_backref(renderer, html, parent); } diff --git a/src/node.c b/src/node.c index 4147aa594..f7f0892e3 100644 --- a/src/node.c +++ b/src/node.c @@ -145,6 +145,144 @@ cmark_node *cmark_node_new(cmark_node_type type) { return cmark_node_new_with_ext(type, NULL); } +static cmark_chunk S_clone_chunk(cmark_mem *mem, const cmark_chunk *source) { + cmark_chunk clone = {NULL, source->len, 1}; + clone.data = (unsigned char *)mem->calloc(source->len + 1, 1); + if (source->len > 0) { + memcpy(clone.data, source->data, source->len); + } + return clone; +} + +static cmark_node *S_clone_node(cmark_node *node) { + cmark_mem *mem = NODE_MEM(node); + cmark_node *clone = cmark_node_new_with_mem_and_ext( + (cmark_node_type)node->type, mem, node->extension); + + if (!clone) { + return NULL; + } + + cmark_strbuf_set(&clone->content, node->content.ptr, node->content.size); + clone->start_line = node->start_line; + clone->start_column = node->start_column; + clone->end_line = node->end_line; + clone->end_column = node->end_column; + clone->internal_offset = node->internal_offset; + clone->flags = node->flags; + clone->footnote = node->footnote; + + switch (node->type) { + case CMARK_NODE_HEADING: + clone->as.heading = node->as.heading; + break; + case CMARK_NODE_LIST: + case CMARK_NODE_ITEM: + clone->as.list = node->as.list; + break; + case CMARK_NODE_CODE_BLOCK: + case CMARK_NODE_FRONT_MATTER: + case CMARK_NODE_CODE: + clone->as.code = node->as.code; + clone->as.code.info = S_clone_chunk(mem, &node->as.code.info); + clone->as.code.literal = S_clone_chunk(mem, &node->as.code.literal); + break; + case CMARK_NODE_TEXT: + case CMARK_NODE_HTML_INLINE: + case CMARK_NODE_HTML_BLOCK: + case CMARK_NODE_FOOTNOTE_REFERENCE: + case CMARK_NODE_FOOTNOTE_DEFINITION: + clone->as.literal = S_clone_chunk(mem, &node->as.literal); + break; + case CMARK_NODE_LINK: + case CMARK_NODE_IMAGE: + clone->as.link.url = S_clone_chunk(mem, &node->as.link.url); + clone->as.link.title = S_clone_chunk(mem, &node->as.link.title); + break; + case CMARK_NODE_CUSTOM_BLOCK: + case CMARK_NODE_CUSTOM_INLINE: + clone->as.custom.on_enter = S_clone_chunk(mem, &node->as.custom.on_enter); + clone->as.custom.on_exit = S_clone_chunk(mem, &node->as.custom.on_exit); + break; + default: + break; + } + + if (node->extension && node->extension->opaque_alloc_func) { + if (!node->extension->opaque_copy_func) { + cmark_node_free(clone); + return NULL; + } + + node->extension->opaque_copy_func(node->extension, mem, clone, node); + } + + for (cmark_node *child = node->first_child; child; child = child->next) { + cmark_node *child_clone = S_clone_node(child); + if (!child_clone) { + cmark_node_free(clone); + return NULL; + } + + if (!cmark_node_append_child(clone, child_clone)) { + cmark_node_free(child_clone); + cmark_node_free(clone); + return NULL; + } + } + + return clone; +} + +static cmark_node *S_find_clone(cmark_node *source, cmark_node *clone, + cmark_node *target) { + if (source == target) { + return clone; + } + + cmark_node *source_child = source->first_child; + cmark_node *clone_child = clone->first_child; + while (source_child && clone_child) { + cmark_node *result = S_find_clone(source_child, clone_child, target); + if (result) { + return result; + } + source_child = source_child->next; + clone_child = clone_child->next; + } + + return NULL; +} + +static void S_clone_footnote_links(cmark_node *source, cmark_node *clone, + cmark_node *source_root, + cmark_node *clone_root) { + if (source->parent_footnote_def) { + clone->parent_footnote_def = + S_find_clone(source_root, clone_root, source->parent_footnote_def); + } + + cmark_node *source_child = source->first_child; + cmark_node *clone_child = clone->first_child; + while (source_child && clone_child) { + S_clone_footnote_links(source_child, clone_child, source_root, clone_root); + source_child = source_child->next; + clone_child = clone_child->next; + } +} + +cmark_node *cmark_node_clone(cmark_node *node) { + if (!node) { + return NULL; + } + + cmark_node *clone = S_clone_node(node); + if (clone) { + S_clone_footnote_links(node, clone, node, clone); + } + return clone; +} + static void free_node_as(cmark_node *node) { switch (node->type) { case CMARK_NODE_CODE_BLOCK: diff --git a/src/syntax_extension.c b/src/syntax_extension.c index d24fe43e6..d9bfc944a 100644 --- a/src/syntax_extension.c +++ b/src/syntax_extension.c @@ -143,6 +143,11 @@ void cmark_syntax_extension_set_opaque_free_func(cmark_syntax_extension *extensi extension->opaque_free_func = func; } +void cmark_syntax_extension_set_opaque_copy_func( + cmark_syntax_extension *extension, cmark_opaque_copy_func func) { + extension->opaque_copy_func = func; +} + void cmark_syntax_extension_set_commonmark_escape_func(cmark_syntax_extension *extension, cmark_commonmark_escape_func func) { extension->commonmark_escape_func = func; diff --git a/src/syntax_extension.h b/src/syntax_extension.h index a5fe11e57..5d46a7b05 100644 --- a/src/syntax_extension.h +++ b/src/syntax_extension.h @@ -28,6 +28,7 @@ struct cmark_syntax_extension { cmark_postprocess_func postprocess_func; cmark_opaque_alloc_func opaque_alloc_func; cmark_opaque_free_func opaque_free_func; + cmark_opaque_copy_func opaque_copy_func; cmark_commonmark_escape_func commonmark_escape_func; };