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"
+ "| Left | \n"
+ "Right | \n"
+ "
\n"
+ "\n"
+ "\n"
+ "\n"
+ "| A | \n"
+ "B | \n"
+ "
\n"
+ "\n"
+ "
\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;
};