diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..4c0ad9406
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,83 @@
+name: CI tests
+
+on: [push, workflow_dispatch]
+
+jobs:
+ linux:
+
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ cmake_opts:
+ - '-DCMARK_SHARED=ON'
+ - ''
+ compiler:
+ - c: 'clang'
+ cpp: 'clang++'
+ - c: 'gcc'
+ cpp: 'g++'
+ env:
+ CMAKE_OPTIONS: ${{ matrix.cmake_opts }}
+ CC: ${{ matrix.compiler.c }}
+ CXX: ${{ matrix.compiler.cpp }}
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Install valgrind
+ run: |
+ sudo apt install -y valgrind
+ - name: Build and test
+ run: |
+ make
+ make test
+ make leakcheck
+
+ macos:
+
+ runs-on: macOS-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ cmake_opts:
+ - '-DCMARK_SHARED=ON'
+ - ''
+ compiler:
+ - c: 'clang'
+ cpp: 'clang++'
+ - c: 'gcc'
+ cpp: 'g++'
+ env:
+ CMAKE_OPTIONS: ${{ matrix.cmake_opts }}
+ CC: ${{ matrix.compiler.c }}
+ CXX: ${{ matrix.compiler.cpp }}
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Build and test
+ env:
+ CMAKE_OPTIONS: -DCMARK_SHARED=OFF
+ run: |
+ make
+ make test
+
+ windows:
+
+ runs-on: windows-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ cmake_opts:
+ - '-DCMARK_SHARED=ON'
+ - ''
+ env:
+ CMAKE_OPTIONS: ${{ matrix.cmake_opts }}
+
+ steps:
+ - uses: actions/checkout@v1
+ - uses: ilammy/msvc-dev-cmd@v1
+ - name: Build and test
+ run: |
+ chcp 65001
+ nmake.exe /nologo /f Makefile.nmake test
+ shell: cmd
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e30278cb2..bb4976a27 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 0)
+set(PROJECT_VERSION_GFM 2)
set(PROJECT_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}.gfm.${PROJECT_VERSION_GFM})
include("FindAsan.cmake")
diff --git a/Makefile b/Makefile
index a05ba97a6..b84e9e741 100644
--- a/Makefile
+++ b/Makefile
@@ -101,7 +101,7 @@ mingw:
cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain-mingw32.cmake -DCMAKE_INSTALL_PREFIX=$(MINGW_INSTALLDIR) ;\
$(MAKE) && $(MAKE) install
-man/man3/cmark-gfm.3: src/cmark-gfm.h | $(CMARK)
+man/man3/cmark-gfm.3: src/include/cmark-gfm.h | $(CMARK)
python man/make_man_page.py $< > $@ \
archive:
diff --git a/api_test/main.c b/api_test/main.c
index 1051c2904..bc7d39c6b 100644
--- a/api_test/main.c
+++ b/api_test/main.c
@@ -1417,6 +1417,114 @@ static void parser_interrupt(test_batch_runner *runner) {
cmark_syntax_extension_free(cmark_get_default_mem_allocator(), my_ext);
}
+static void render_spoiler(test_batch_runner *runner) {
+ cmark_gfm_core_extensions_ensure_registered();
+
+ cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT);
+ cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("spoiler"));
+
+ {
+ static const char markdown[] = "we have some ||spicy text|| here";
+ cmark_parser_feed(parser, markdown, sizeof(markdown) - 1);
+
+ cmark_node *doc = cmark_parser_finish(parser);
+ char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT);
+ STR_EQ(runner, xml, "\n"
+ "\n"
+ "\n"
+ " \n"
+ " we have some \n"
+ " \n"
+ " spicy text\n"
+ " \n"
+ " here\n"
+ " \n"
+ "\n", "rendering spoilers should appear correctly");
+ }
+ {
+ static const char markdown[] = "we have some |non-spicy text| here";
+ cmark_parser_feed(parser, markdown, sizeof(markdown) - 1);
+
+ cmark_node *doc = cmark_parser_finish(parser);
+ char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT);
+ STR_EQ(runner, xml, "\n"
+ "\n"
+ "\n"
+ " \n"
+ " we have some |non-spicy text| here\n"
+ " \n"
+ "\n", "rendering spoilers without proper delimiters should appear correctly");
+ }
+ {
+ static const char markdown[] = "we have some >!incorrectly spicy text!< here";
+ cmark_parser_feed(parser, markdown, sizeof(markdown) - 1);
+
+ cmark_node *doc = cmark_parser_finish(parser);
+ char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT);
+ STR_EQ(runner, xml, "\n"
+ "\n"
+ "\n"
+ " \n"
+ " we have some >!incorrectly spicy text!< here\n"
+ " \n"
+ "\n", "rendering spoilers without proper delimiters should appear correctly");
+ }
+}
+
+static void render_spoiler_reddit(test_batch_runner *runner) {
+ cmark_gfm_core_extensions_ensure_registered();
+
+ cmark_parser *parser = cmark_parser_new(CMARK_OPT_SPOILER_REDDIT_STYLE);
+ cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("spoiler"));
+
+ {
+ static const char markdown[] = "we have some >!spicy text!< here";
+ cmark_parser_feed(parser, markdown, sizeof(markdown) - 1);
+
+ cmark_node *doc = cmark_parser_finish(parser);
+ char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT);
+ STR_EQ(runner, xml, "\n"
+ "\n"
+ "\n"
+ " \n"
+ " we have some \n"
+ " \n"
+ " spicy text\n"
+ " \n"
+ " here\n"
+ " \n"
+ "\n", "rendering spoilers should appear correctly");
+ }
+ {
+ static const char markdown[] = "we have some !non-spicy text! here";
+ cmark_parser_feed(parser, markdown, sizeof(markdown) - 1);
+
+ cmark_node *doc = cmark_parser_finish(parser);
+ char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT);
+ STR_EQ(runner, xml, "\n"
+ "\n"
+ "\n"
+ " \n"
+ " we have some !non-spicy text! here\n"
+ " \n"
+ "\n", "rendering spoilers without proper delimiters should appear correctly");
+ }
+ {
+ static const char markdown[] = "we have some ||incorrectly spicy text|| here";
+ cmark_parser_feed(parser, markdown, sizeof(markdown) - 1);
+
+ cmark_node *doc = cmark_parser_finish(parser);
+ char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT);
+ STR_EQ(runner, xml, "\n"
+ "\n"
+ "\n"
+ " \n"
+ " we have some ||incorrectly spicy text|| here\n"
+ " \n"
+ "\n", "rendering spoilers without proper delimiters should appear correctly");
+ }
+}
+
int main() {
int retval;
test_batch_runner *runner = test_batch_runner_new();
@@ -1452,6 +1560,7 @@ int main() {
verify_custom_attributes_node(runner);
verify_custom_attributes_node_with_footnote(runner);
parser_interrupt(runner);
+ render_spoiler(runner);
test_print_summary(runner);
retval = test_ok(runner) ? 0 : 1;
diff --git a/bin/main.c b/bin/main.c
index 2bab06284..87aaf619b 100644
--- a/bin/main.c
+++ b/bin/main.c
@@ -64,6 +64,9 @@ 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(" --spoiler-reddit-style Use Reddit-style spoiler delimiters,\n"
+ " i.e. >!spoilertext!< instead of\n"
+ " ||spoilertext||\n");
printf(" --help, -h Print usage information\n");
printf(" --version Print version\n");
}
@@ -185,6 +188,8 @@ int main(int argc, char *argv[]) {
options |= CMARK_OPT_VALIDATE_UTF8;
} else if (strcmp(argv[i], "--liberal-html-tag") == 0) {
options |= CMARK_OPT_LIBERAL_HTML_TAG;
+ } else if (strcmp(argv[i], "--spoiler-reddit-style") == 0) {
+ options |= CMARK_OPT_SPOILER_REDDIT_STYLE;
} else if ((strcmp(argv[i], "--help") == 0) ||
(strcmp(argv[i], "-h") == 0)) {
print_usage();
diff --git a/changelog.txt b/changelog.txt
index b86a41a22..a8f7bb1b1 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,18 @@
+[0.29.0.gfm.2]
+ * Fixed issues with footnote rendering when used with the autolinker (#121),
+ and when footnotes are adjacent (#139).
+ * We now allow footnotes to be referenced from inside a footnote definition,
+ we use the footnote label for the fnref href text when rendering html, and
+ we insert multiple backrefs when a footnote has been referenced multiple
+ times (#229, #230)
+ * We added new data- attributes to footnote html rendering to make them
+ easier to style (#234)
+
+[0.29.0.gfm.1]
+
+ * Fixed denial of service bug in GFM's table extension
+ per https://github.com/github/cmark-gfm/security/advisories/GHSA-7gc6-9qr5-hc85
+
[0.29.0]
* Update spec to 0.29.
diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt
index 4977c98c3..62984af5c 100644
--- a/extensions/CMakeLists.txt
+++ b/extensions/CMakeLists.txt
@@ -11,6 +11,7 @@ set(LIBRARY_SOURCES
ext_scanners.re
ext_scanners.h
tasklist.c
+ spoiler.c
)
include_directories(
diff --git a/extensions/core-extensions.c b/extensions/core-extensions.c
index 131cdf402..c9bff9471 100644
--- a/extensions/core-extensions.c
+++ b/extensions/core-extensions.c
@@ -2,6 +2,7 @@
#include "autolink.h"
#include "mutex.h"
#include "strikethrough.h"
+#include "spoiler.h"
#include "table.h"
#include "tagfilter.h"
#include "tasklist.h"
@@ -15,6 +16,7 @@ static int core_extensions_registration(cmark_plugin *plugin) {
cmark_plugin_register_syntax_extension(plugin, create_autolink_extension());
cmark_plugin_register_syntax_extension(plugin, create_tagfilter_extension());
cmark_plugin_register_syntax_extension(plugin, create_tasklist_extension());
+ cmark_plugin_register_syntax_extension(plugin, create_spoiler_extension());
return 1;
}
diff --git a/extensions/spoiler.c b/extensions/spoiler.c
new file mode 100644
index 000000000..532ea7df8
--- /dev/null
+++ b/extensions/spoiler.c
@@ -0,0 +1,194 @@
+#include "spoiler.h"
+#include
+#include
+
+cmark_node_type CMARK_NODE_SPOILER;
+
+static cmark_node *match(cmark_syntax_extension *self, cmark_parser *parser,
+ cmark_node *parent, unsigned char character,
+ cmark_inline_parser *inline_parser) {
+ cmark_node *res = NULL;
+ int left_flanking, right_flanking, punct_before, punct_after, delims;
+ char buffer[101];
+
+ if ((parser->options & CMARK_OPT_SPOILER_REDDIT_STYLE)) {
+ // Reddit-style spoilers - flanked by angle brackets and exclamation marks,
+ // e.g. >!this is a spoiler!<
+ int pos = cmark_inline_parser_get_offset(inline_parser);
+ char *txt = NULL;
+ bool opener = false;
+ bool closer = false;
+ if (cmark_inline_parser_peek_at(inline_parser, pos) == '>' &&
+ cmark_inline_parser_peek_at(inline_parser, pos + 1) == '!') {
+ txt = ">!";
+ opener = true;
+ } else if (cmark_inline_parser_peek_at(inline_parser, pos) == '!' &&
+ cmark_inline_parser_peek_at(inline_parser, pos + 1) == '<') {
+ txt = "!<";
+ closer = true;
+ }
+
+ if (opener && pos > 0 && !cmark_isspace(cmark_inline_parser_peek_at(inline_parser, pos - 1))) {
+ opener = false;
+ }
+
+ if (closer) {
+ cmark_chunk *chunk = cmark_inline_parser_get_chunk(inline_parser);
+ bufsize_t len = chunk->len;
+ if (pos + 2 < len && !cmark_isspace(cmark_inline_parser_peek_at(inline_parser, pos + 2))) {
+ closer = false;
+ }
+ }
+
+ if ((!opener && !closer) || !txt)
+ return NULL;
+
+ res = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
+ cmark_node_set_literal(res, txt);
+ res->start_line = cmark_inline_parser_get_line(inline_parser);
+ res->start_column = cmark_inline_parser_get_column(inline_parser);
+
+ cmark_inline_parser_set_offset(inline_parser, pos + 2);
+
+ res->end_line = cmark_inline_parser_get_line(inline_parser);
+ res->end_column = cmark_inline_parser_get_column(inline_parser);
+
+ // Set the character for this delimiter to `!`, since it's a heterogenous
+ // delimiter and the delimiter API assumes single repeated characters.
+ cmark_inline_parser_push_delimiter(inline_parser, '!', opener, closer, res);
+ } else {
+ // Discord-style spoilers - flanked on both sides by two pipes,
+ // e.g. ||this is a spoiler||
+ if (character != '|')
+ return NULL;
+
+ delims = cmark_inline_parser_scan_delimiters(
+ inline_parser, sizeof(buffer) - 1, '|',
+ &left_flanking,
+ &right_flanking, &punct_before, &punct_after);
+
+ memset(buffer, '|', delims);
+ buffer[delims] = 0;
+
+ res = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem);
+ cmark_node_set_literal(res, buffer);
+ res->start_line = res->end_line = cmark_inline_parser_get_line(inline_parser);
+ res->start_column = cmark_inline_parser_get_column(inline_parser) - delims;
+
+ if ((left_flanking || right_flanking) && (delims == 2)) {
+ cmark_inline_parser_push_delimiter(inline_parser, character, left_flanking,
+ right_flanking, res);
+ }
+ }
+
+ return res;
+}
+
+static delimiter *insert(cmark_syntax_extension *self, cmark_parser *parser,
+ cmark_inline_parser *inline_parser, delimiter *opener,
+ delimiter *closer) {
+ cmark_node *spoiler;
+ cmark_node *tmp, *next;
+ delimiter *delim, *tmp_delim;
+ delimiter *res = closer->next;
+
+ spoiler = opener->inl_text;
+
+ if (opener->inl_text->as.literal.len != closer->inl_text->as.literal.len)
+ goto done;
+
+ if (!cmark_node_set_type(spoiler, CMARK_NODE_SPOILER))
+ goto done;
+
+ cmark_node_set_syntax_extension(spoiler, self);
+
+ tmp = cmark_node_next(opener->inl_text);
+
+ while (tmp) {
+ if (tmp == closer->inl_text)
+ break;
+ next = cmark_node_next(tmp);
+ cmark_node_append_child(spoiler, tmp);
+ tmp = next;
+ }
+
+ spoiler->end_column = closer->inl_text->start_column + closer->inl_text->as.literal.len - 1;
+ cmark_node_free(closer->inl_text);
+
+ delim = closer;
+ while (delim != NULL && delim != opener) {
+ tmp_delim = delim->previous;
+ cmark_inline_parser_remove_delimiter(inline_parser, delim);
+ delim = tmp_delim;
+ }
+
+ cmark_inline_parser_remove_delimiter(inline_parser, opener);
+
+done:
+ return res;
+}
+
+static const char *get_type_string(cmark_syntax_extension *extension,
+ cmark_node *node) {
+ return node->type == CMARK_NODE_SPOILER ? "spoiler" : "";
+}
+
+static int can_contain(cmark_syntax_extension *extension, cmark_node *node,
+ cmark_node_type child_type) {
+ if (node->type != CMARK_NODE_SPOILER)
+ return false;
+
+ return CMARK_NODE_TYPE_INLINE_P(child_type);
+}
+
+static void commonmark_render(cmark_syntax_extension *extension,
+ cmark_renderer *renderer, cmark_node *node,
+ cmark_event_type ev_type, int options) {
+ if (options & CMARK_OPT_SPOILER_REDDIT_STYLE) {
+ bool entering = (ev_type == CMARK_EVENT_ENTER);
+ if (entering) {
+ renderer->out(renderer, node, ">!", false, LITERAL);
+ } else {
+ renderer->out(renderer, node, "!<", false, LITERAL);
+ }
+ } else {
+ renderer->out(renderer, node, "||", false, LITERAL);
+ }
+}
+
+static void html_render(cmark_syntax_extension *extension,
+ cmark_html_renderer *renderer, cmark_node *node,
+ cmark_event_type ev_type, int options) {
+ bool entering = (ev_type == CMARK_EVENT_ENTER);
+ if (entering) {
+ cmark_strbuf_puts(renderer->html, "");
+ } else {
+ cmark_strbuf_puts(renderer->html, "");
+ }
+}
+
+cmark_syntax_extension *create_spoiler_extension(void) {
+ cmark_syntax_extension *ext = cmark_syntax_extension_new("spoiler");
+ cmark_llist *special_chars = NULL;
+
+ cmark_syntax_extension_set_get_type_string_func(ext, get_type_string);
+ cmark_syntax_extension_set_can_contain_func(ext, can_contain);
+ cmark_syntax_extension_set_commonmark_render_func(ext, commonmark_render);
+ cmark_syntax_extension_set_html_render_func(ext, html_render);
+ cmark_syntax_extension_set_plaintext_render_func(ext, commonmark_render);
+ CMARK_NODE_SPOILER = cmark_syntax_extension_add_node(1);
+
+ cmark_syntax_extension_set_match_inline_func(ext, match);
+ cmark_syntax_extension_set_inline_from_delim_func(ext, insert);
+
+ cmark_mem *mem = cmark_get_default_mem_allocator();
+ special_chars = cmark_llist_append(mem, special_chars, (void *)'|');
+ special_chars = cmark_llist_append(mem, special_chars, (void *)'>');
+ special_chars = cmark_llist_append(mem, special_chars, (void *)'<');
+ special_chars = cmark_llist_append(mem, special_chars, (void *)'!');
+ cmark_syntax_extension_set_special_inline_chars(ext, special_chars);
+
+ cmark_syntax_extension_set_emphasis(ext, 1);
+
+ return ext;
+}
diff --git a/extensions/spoiler.h b/extensions/spoiler.h
new file mode 100644
index 000000000..a86bbbf39
--- /dev/null
+++ b/extensions/spoiler.h
@@ -0,0 +1,9 @@
+#ifndef CMARK_GFM_SPOILER_H
+#define CMARK_GFM_SPOILER_H
+
+#include "cmark-gfm-core-extensions.h"
+
+extern cmark_node_type CMARK_NODE_SPOILER;
+cmark_syntax_extension *create_spoiler_extension(void);
+
+#endif /* CMARK_GFM_SPOILER_H */
diff --git a/src/blocks.c b/src/blocks.c
index 6e87f19cb..99da25eb3 100644
--- a/src/blocks.c
+++ b/src/blocks.c
@@ -494,7 +494,6 @@ static void process_footnotes(cmark_parser *parser) {
while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
cur = cmark_iter_get_node(iter);
if (ev_type == CMARK_EVENT_EXIT && cur->type == CMARK_NODE_FOOTNOTE_DEFINITION) {
- cmark_node_unlink(cur);
cmark_footnote_create(map, cur);
}
}
@@ -511,6 +510,15 @@ static void process_footnotes(cmark_parser *parser) {
if (!footnote->ix)
footnote->ix = ++ix;
+ // store a reference to this footnote reference's footnote definition
+ // this is used by renderers when generating label ids
+ cur->parent_footnote_def = footnote->node;
+
+ // keep track of a) count of how many times this footnote def has been
+ // referenced, and b) which reference index this footnote ref is at.
+ // this is used by renderers when generating links and backreferences.
+ cur->footnote.ref_ix = ++footnote->node->footnote.def_count;
+
char n[32];
snprintf(n, sizeof(n), "%d", footnote->ix);
cmark_chunk_free(parser->mem, &cur->as.literal);
@@ -541,13 +549,16 @@ static void process_footnotes(cmark_parser *parser) {
qsort(map->sorted, map->size, sizeof(cmark_map_entry *), sort_footnote_by_ix);
for (unsigned int i = 0; i < map->size; ++i) {
cmark_footnote *footnote = (cmark_footnote *)map->sorted[i];
- if (!footnote->ix)
+ if (!footnote->ix) {
+ cmark_node_unlink(footnote->node);
continue;
+ }
cmark_node_append_child(parser->root, footnote->node);
footnote->node = NULL;
}
}
+ cmark_unlink_footnotes_map(map);
cmark_map_free(map);
}
diff --git a/src/commonmark.c b/src/commonmark.c
index 94fd4388f..328da12a3 100644
--- a/src/commonmark.c
+++ b/src/commonmark.c
@@ -488,7 +488,13 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node,
case CMARK_NODE_FOOTNOTE_REFERENCE:
if (entering) {
LIT("[^");
- OUT(cmark_chunk_to_cstr(renderer->mem, &node->as.literal), false, LITERAL);
+
+ char *footnote_label = renderer->mem->calloc(node->parent_footnote_def->as.literal.len + 1, sizeof(char));
+ memmove(footnote_label, node->parent_footnote_def->as.literal.data, node->parent_footnote_def->as.literal.len);
+
+ OUT(footnote_label, false, LITERAL);
+ renderer->mem->free(footnote_label);
+
LIT("]");
}
break;
@@ -497,9 +503,13 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node,
if (entering) {
renderer->footnote_ix += 1;
LIT("[^");
- char n[32];
- snprintf(n, sizeof(n), "%d", renderer->footnote_ix);
- OUT(n, false, LITERAL);
+
+ char *footnote_label = renderer->mem->calloc(node->as.literal.len + 1, sizeof(char));
+ memmove(footnote_label, node->as.literal.data, node->as.literal.len);
+
+ OUT(footnote_label, false, LITERAL);
+ renderer->mem->free(footnote_label);
+
LIT("]:\n");
cmark_strbuf_puts(renderer->prefix, " ");
diff --git a/src/footnotes.c b/src/footnotes.c
index f2d2765f4..c2b745f79 100644
--- a/src/footnotes.c
+++ b/src/footnotes.c
@@ -38,3 +38,26 @@ void cmark_footnote_create(cmark_map *map, cmark_node *node) {
cmark_map *cmark_footnote_map_new(cmark_mem *mem) {
return cmark_map_new(mem, footnote_free);
}
+
+// Before calling `cmark_map_free` on a map with `cmark_footnotes`, first
+// unlink all of the footnote nodes before freeing their memory.
+//
+// Sometimes, two (unused) footnote nodes can end up referencing each other,
+// which as they get freed up by calling `cmark_map_free` -> `footnote_free` ->
+// etc, can lead to a use-after-free error.
+//
+// Better to `unlink` every footnote node first, setting their next, prev, and
+// parent pointers to NULL, and only then walk thru & free them up.
+void cmark_unlink_footnotes_map(cmark_map *map) {
+ cmark_map_entry *ref;
+ cmark_map_entry *next;
+
+ ref = map->refs;
+ while(ref) {
+ next = ref->next;
+ if (((cmark_footnote *)ref)->node) {
+ cmark_node_unlink(((cmark_footnote *)ref)->node);
+ }
+ ref = next;
+ }
+}
diff --git a/src/html.c b/src/html.c
index 5959d7a0b..96daa18e2 100644
--- a/src/html.c
+++ b/src/html.c
@@ -59,16 +59,30 @@ static void filter_html_block(cmark_html_renderer *renderer, uint8_t *data, size
cmark_strbuf_put(html, data, (bufsize_t)len);
}
-static bool S_put_footnote_backref(cmark_html_renderer *renderer, cmark_strbuf *html) {
+static bool S_put_footnote_backref(cmark_html_renderer *renderer, cmark_strbuf *html, cmark_node *node) {
if (renderer->written_footnote_ix >= renderer->footnote_ix)
return false;
renderer->written_footnote_ix = renderer->footnote_ix;
- cmark_strbuf_puts(html, "footnote_ix);
- cmark_strbuf_puts(html, n);
- cmark_strbuf_puts(html, "\" class=\"footnote-backref\">↩");
+ 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\">↩");
+
+ if (node->footnote.def_count > 1)
+ {
+ for(int i = 2; i <= node->footnote.def_count; i++) {
+ char n[32];
+ snprintf(n, sizeof(n), "%d", i);
+
+ cmark_strbuf_puts(html, " 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\">↩");
+ }
+ }
return true;
}
@@ -273,7 +287,7 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node,
} else {
if (parent->type == CMARK_NODE_FOOTNOTE_DEFINITION && node->next == NULL) {
cmark_strbuf_putc(html, ' ');
- S_put_footnote_backref(renderer, html);
+ S_put_footnote_backref(renderer, html, parent);
}
cmark_strbuf_puts(html, "
\n");
}
@@ -405,16 +419,15 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node,
case CMARK_NODE_FOOTNOTE_DEFINITION:
if (entering) {
if (renderer->footnote_ix == 0) {
- cmark_strbuf_puts(html, "