From db63847d9e40115e57aae31de1aa1cf5102ab24c Mon Sep 17 00:00:00 2001 From: Ben Dolman Date: Tue, 30 Oct 2012 17:45:51 -0600 Subject: [PATCH 1/7] Add MultiMarkdown footnotes extension --- html/html.c | 54 ++++++++++ src/markdown.c | 279 ++++++++++++++++++++++++++++++++++++++++++++++++- src/markdown.h | 5 +- 3 files changed, 335 insertions(+), 3 deletions(-) diff --git a/html/html.c b/html/html.c index 7f08ee8e..25af8e78 100755 --- a/html/html.c +++ b/html/html.c @@ -486,6 +486,54 @@ rndr_normal_text(struct buf *ob, const struct buf *text, void *opaque) escape_html(ob, text->data, text->size); } +static void +rndr_footnotes(struct buf *ob, const struct buf *text, void *opaque) +{ + BUFPUTSL(ob, "
\n
\n
    \n"); + + if (text) + bufput(ob, text->data, text->size); + + BUFPUTSL(ob, "\n
\n
"); +} + +static void +rndr_footnote_def(struct buf *ob, const struct buf *text, unsigned int num, void *opaque) +{ + size_t i = 0; + int pfound = 0; + + /* insert anchor at the end of first paragraph block */ + if (text) { + while ((i+3) < text->size) { + if (text->data[i++] != '<') continue; + if (text->data[i++] != '/') continue; + if (text->data[i++] != 'p' && text->data[i] != 'P') continue; + if (text->data[i] != '>') continue; + i -= 3; + pfound = 1; + break; + } + } + + bufprintf(ob, "\n
  • \n", num); + if (pfound) { + bufput(ob, text->data, i); + bufprintf(ob, " ", num); + bufput(ob, text->data + i, text->size - i); + } else if (text) { + bufput(ob, text->data, text->size); + } + BUFPUTSL(ob, "
  • \n"); +} + +static int +rndr_footnote_ref(struct buf *ob, unsigned int num, void *opaque) +{ + bufprintf(ob, "%d", num, num, num); + return 1; +} + static void toc_header(struct buf *ob, const struct buf *text, int level, void *opaque) { @@ -554,6 +602,8 @@ sdhtml_toc_renderer(struct sd_callbacks *callbacks, struct html_renderopt *optio NULL, NULL, NULL, + NULL, + NULL, NULL, rndr_codespan, @@ -566,6 +616,7 @@ sdhtml_toc_renderer(struct sd_callbacks *callbacks, struct html_renderopt *optio rndr_triple_emphasis, rndr_strikethrough, rndr_superscript, + NULL, NULL, NULL, @@ -595,6 +646,8 @@ sdhtml_renderer(struct sd_callbacks *callbacks, struct html_renderopt *options, rndr_table, rndr_tablerow, rndr_tablecell, + rndr_footnotes, + rndr_footnote_def, rndr_autolink, rndr_codespan, @@ -607,6 +660,7 @@ sdhtml_renderer(struct sd_callbacks *callbacks, struct html_renderopt *options, rndr_triple_emphasis, rndr_strikethrough, rndr_superscript, + rndr_footnote_ref, NULL, rndr_normal_text, diff --git a/src/markdown.c b/src/markdown.c index ea3cf232..4ed25f60 100644 --- a/src/markdown.c +++ b/src/markdown.c @@ -55,6 +55,29 @@ struct link_ref { struct link_ref *next; }; +/* footnote_ref: reference to a footnote */ +struct footnote_ref { + unsigned int id; + + int is_used; + unsigned int num; + + struct buf *contents; +}; + +/* footnote_item: an item in a footnote_list */ +struct footnote_item { + struct footnote_ref *ref; + struct footnote_item *next; +}; + +/* footnote_list: linked list of footnote_item */ +struct footnote_list { + unsigned int count; + struct footnote_item *head; + struct footnote_item *tail; +}; + /* char_trigger: function pointer to render active chars */ /* returns the number of chars taken care of */ /* data is the pointer of the beginning of the span */ @@ -111,6 +134,8 @@ struct sd_markdown { void *opaque; struct link_ref *refs[REF_TABLE_SIZE]; + struct footnote_list footnotes_found; + struct footnote_list footnotes_used; uint8_t active_char[256]; struct stack work_bufs[2]; unsigned int ext_flags; @@ -233,6 +258,72 @@ free_link_refs(struct link_ref **references) } } +static struct footnote_ref * +create_footnote_ref(struct footnote_list *list, const uint8_t *name, size_t name_size) +{ + struct footnote_ref *ref = calloc(1, sizeof(struct footnote_ref)); + if (!ref) + return NULL; + + ref->id = hash_link_ref(name, name_size); + + return ref; +} + +static int +add_footnote_ref(struct footnote_list *list, struct footnote_ref *ref) +{ + struct footnote_item *item = calloc(1, sizeof(struct footnote_item)); + if (!item) + return 0; + item->ref = ref; + + if (list->head == NULL) { + list->head = list->tail = item; + } else { + list->tail->next = item; + list->tail = item; + } + list->count++; + + return 1; +} + +static struct footnote_ref * +find_footnote_ref(struct footnote_list *list, uint8_t *name, size_t length) +{ + unsigned int hash = hash_link_ref(name, length); + struct footnote_item *item = NULL; + + item = list->head; + + while (item != NULL) { + if (item->ref->id == hash) + return item->ref; + item = item->next; + } + + return NULL; +} + +static void +free_footnote_list(struct footnote_list *list, int free_refs) +{ + struct footnote_item *item = list->head; + struct footnote_item *next; + + while (item) { + next = item->next; + if (free_refs) { + bufrelease(item->ref->contents); + free(item->ref); + } + free(item); + item = next; + } +} + + /* * Check whether a char is a Markdown space. @@ -878,6 +969,34 @@ char_link(struct buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t offset txt_e = i; i++; + + /* footnote link */ + if (rndr->ext_flags & MKDEXT_FOOTNOTES && data[1] == '^') { + if (txt_e < 3) + goto cleanup; + + struct buf id = { 0, 0, 0, 0 }; + struct footnote_ref *fr; + + id.data = data + 2; + id.size = txt_e - 2; + + fr = find_footnote_ref(&rndr->footnotes_found, id.data, id.size); + + /* mark footnote used */ + if (fr && !fr->is_used) { + if(!add_footnote_ref(&rndr->footnotes_used, fr)) + goto cleanup; + fr->is_used = 1; + fr->num = rndr->footnotes_used.count; + } + + /* render */ + if (fr && rndr->cb.footnote_ref) + ret = rndr->cb.footnote_ref(ob, fr->num, rndr->opaque); + + goto cleanup; + } /* skip any amount of whitespace or newline */ /* (this is much more laxist than original markdown syntax) */ @@ -1117,7 +1236,7 @@ char_superscript(struct buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t /* is_empty • returns the line length when it is empty, 0 otherwise */ static size_t -is_empty(uint8_t *data, size_t size) +is_empty(const uint8_t *data, size_t size) { size_t i; @@ -1819,6 +1938,44 @@ parse_atxheader(struct buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t return skip; } +/* parse_footnote_def • parse a single footnote definition */ +static void +parse_footnote_def(struct buf *ob, struct sd_markdown *rndr, unsigned int num, uint8_t *data, size_t size) +{ + struct buf *work = 0; + work = rndr_newbuf(rndr, BUFFER_SPAN); + + parse_block(work, rndr, data, size); + + if (rndr->cb.footnote_def) + rndr->cb.footnote_def(ob, work, num, rndr->opaque); + rndr_popbuf(rndr, BUFFER_SPAN); +} + +/* parse_footnote_list • render the contents of the footnotes */ +static void +parse_footnote_list(struct buf *ob, struct sd_markdown *rndr, struct footnote_list *footnotes) +{ + struct buf *work = 0; + struct footnote_item *item; + struct footnote_ref *ref; + + if (footnotes->count == 0) + return; + + work = rndr_newbuf(rndr, BUFFER_BLOCK); + + item = footnotes->head; + while (item) { + ref = item->ref; + parse_footnote_def(work, rndr, ref->num, ref->contents->data, ref->contents->size); + item = item->next; + } + + if (rndr->cb.footnotes) + rndr->cb.footnotes(ob, work, rndr->opaque); + rndr_popbuf(rndr, BUFFER_BLOCK); +} /* htmlblock_end • checking end of HTML block : [ \t]*\n[ \t*]\n */ /* returns the length on match, 0 otherwise */ @@ -2250,6 +2407,106 @@ parse_block(struct buf *ob, struct sd_markdown *rndr, uint8_t *data, size_t size * REFERENCE PARSING * *********************/ +/* is_footnote • returns whether a line is a footnote definition or not */ +static int +is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct footnote_list *list) +{ + size_t i = 0; + struct buf *contents = 0; + size_t ind = 0; + int in_empty = 0; + size_t start = 0; + + size_t id_offset, id_end; + + /* up to 3 optional leading spaces */ + if (beg + 3 >= end) return 0; + if (data[beg] == ' ') { i = 1; + if (data[beg + 1] == ' ') { i = 2; + if (data[beg + 2] == ' ') { i = 3; + if (data[beg + 3] == ' ') return 0; } } } + i += beg; + + /* id part: caret followed by anything between brackets */ + if (data[i] != '[') return 0; + i++; + if (i >= end || data[i] != '^') return 0; + i++; + id_offset = i; + while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') + i++; + if (i >= end || data[i] != ']') return 0; + id_end = i; + + /* spacer: colon (space | tab)* newline? (space | tab)* */ + i++; + if (i >= end || data[i] != ':') return 0; + i++; + while (i < end && data[i] == ' ') i++; + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; + } + while (i < end && data[i] == ' ') i++; + if (i >= end || data[i] == '\n' || data[i] == '\r') return 0; + + /* getting content buffer */ + contents = bufnew(64); + + start = i; + + /* process lines similiar to a list item */ + while (i < end) { + i++; + + while (i < end && data[i - 1] != '\n') + i++; + + /* process an empty line */ + if (is_empty(data + start, i - start)) { + in_empty = 1; + start = i; + continue; + } + + /* calculating the indentation */ + ind = 0; + while (ind < 4 && start + ind < end && data[start + ind] == ' ') + ind++; + + /* joining only indented stuff after empty lines; + * note that now we only require 1 space of indentation + * to continue, just like lists */ + if (in_empty && ind == 0) { + break; + } + else if (in_empty) { + bufputc(contents, '\n'); + } + + in_empty = 0; + + /* adding the line into the content buffer */ + bufput(contents, data + start + ind, i - start - ind); + start = i; + } + + if (last) + *last = start; + + if (list) { + struct footnote_ref *ref; + ref = create_footnote_ref(list, data + id_offset, id_end - id_offset); + if (!ref) + return 0; + if (!add_footnote_ref(list, ref)) + return 0; + ref->contents = contents; + } + + return 1; +} + /* is_ref • returns whether a line is a reference or not */ static int is_ref(const uint8_t *data, size_t beg, size_t end, size_t *last, struct link_ref **refs) @@ -2471,6 +2728,14 @@ sd_markdown_render(struct buf *ob, const uint8_t *document, size_t doc_size, str /* reset the references table */ memset(&md->refs, 0x0, REF_TABLE_SIZE * sizeof(void *)); + + int footnotes_enabled = md->ext_flags & MKDEXT_FOOTNOTES; + + /* reset the footnotes lists */ + if (footnotes_enabled) { + memset(&md->footnotes_found, 0x0, sizeof(md->footnotes_found)); + memset(&md->footnotes_used, 0x0, sizeof(md->footnotes_used)); + } /* first pass: looking for references, copying everything else */ beg = 0; @@ -2481,7 +2746,9 @@ sd_markdown_render(struct buf *ob, const uint8_t *document, size_t doc_size, str beg += 3; while (beg < doc_size) /* iterating over lines */ - if (is_ref(document, beg, doc_size, &end, md->refs)) + if (footnotes_enabled && is_footnote(document, beg, doc_size, &end, &md->footnotes_found)) + beg = end; + else if (is_ref(document, beg, doc_size, &end, md->refs)) beg = end; else { /* skipping to the next line */ end = beg; @@ -2516,6 +2783,10 @@ sd_markdown_render(struct buf *ob, const uint8_t *document, size_t doc_size, str parse_block(ob, md, text->data, text->size); } + + /* footnotes */ + if (footnotes_enabled) + parse_footnote_list(ob, md, &md->footnotes_used); if (md->cb.doc_footer) md->cb.doc_footer(ob, md->opaque); @@ -2523,6 +2794,10 @@ sd_markdown_render(struct buf *ob, const uint8_t *document, size_t doc_size, str /* clean-up */ bufrelease(text); free_link_refs(md->refs); + if (footnotes_enabled) { + free_footnote_list(&md->footnotes_found, 1); + free_footnote_list(&md->footnotes_used, 0); + } assert(md->work_bufs[BUFFER_SPAN].size == 0); assert(md->work_bufs[BUFFER_BLOCK].size == 0); diff --git a/src/markdown.h b/src/markdown.h index 6f6553ec..e3ce760e 100644 --- a/src/markdown.h +++ b/src/markdown.h @@ -59,6 +59,7 @@ enum mkd_extensions { MKDEXT_SPACE_HEADERS = (1 << 6), MKDEXT_SUPERSCRIPT = (1 << 7), MKDEXT_LAX_SPACING = (1 << 8), + MKDEXT_FOOTNOTES = (1 << 9), }; /* sd_callbacks - functions for rendering parsed data */ @@ -75,7 +76,8 @@ struct sd_callbacks { void (*table)(struct buf *ob, const struct buf *header, const struct buf *body, void *opaque); void (*table_row)(struct buf *ob, const struct buf *text, void *opaque); void (*table_cell)(struct buf *ob, const struct buf *text, int flags, void *opaque); - + void (*footnotes)(struct buf *ob, const struct buf *text, void *opaque); + void (*footnote_def)(struct buf *ob, const struct buf *text, unsigned int num, void *opaque); /* span level callbacks - NULL or return 0 prints the span verbatim */ int (*autolink)(struct buf *ob, const struct buf *link, enum mkd_autolink type, void *opaque); @@ -89,6 +91,7 @@ struct sd_callbacks { int (*triple_emphasis)(struct buf *ob, const struct buf *text, void *opaque); int (*strikethrough)(struct buf *ob, const struct buf *text, void *opaque); int (*superscript)(struct buf *ob, const struct buf *text, void *opaque); + int (*footnote_ref)(struct buf *ob, unsigned int num, void *opaque); /* low level callbacks - NULL copies input directly into the output */ void (*entity)(struct buf *ob, const struct buf *entity, void *opaque); From 511d6e973eefafb98f3a78cf831e53111ac81667 Mon Sep 17 00:00:00 2001 From: Ben Dolman Date: Fri, 9 Nov 2012 00:05:29 -0700 Subject: [PATCH 2/7] Support CRLF (aka \r\n) in footnotes --- src/markdown.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/markdown.c b/src/markdown.c index 4ed25f60..22c6a5bd 100644 --- a/src/markdown.c +++ b/src/markdown.c @@ -2445,7 +2445,7 @@ is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct fo while (i < end && data[i] == ' ') i++; if (i < end && (data[i] == '\n' || data[i] == '\r')) { i++; - if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; + if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; } while (i < end && data[i] == ' ') i++; if (i >= end || data[i] == '\n' || data[i] == '\r') return 0; @@ -2457,14 +2457,15 @@ is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct fo /* process lines similiar to a list item */ while (i < end) { - i++; - - while (i < end && data[i - 1] != '\n') - i++; - + while (i < end && data[i] != '\n' && data[i] != '\r') i++; + /* process an empty line */ if (is_empty(data + start, i - start)) { in_empty = 1; + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; + } start = i; continue; } @@ -2488,6 +2489,14 @@ is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct fo /* adding the line into the content buffer */ bufput(contents, data + start + ind, i - start - ind); + /* add carriage return */ + if (i < end) { + bufput(contents, "\n", 1); + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; + } + } start = i; } From 9bf0176339b4a0823efb159b4a01fc57d7afb1a9 Mon Sep 17 00:00:00 2001 From: Ben Dolman Date: Fri, 9 Nov 2012 00:11:27 -0700 Subject: [PATCH 3/7] Fix a potential memory leak --- src/markdown.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/markdown.c b/src/markdown.c index 22c6a5bd..7cf5c7ee 100644 --- a/src/markdown.c +++ b/src/markdown.c @@ -306,6 +306,13 @@ find_footnote_ref(struct footnote_list *list, uint8_t *name, size_t length) return NULL; } +static void +free_footnote_ref(struct footnote_ref *ref) +{ + bufrelease(ref->contents); + free(ref); +} + static void free_footnote_list(struct footnote_list *list, int free_refs) { @@ -314,10 +321,8 @@ free_footnote_list(struct footnote_list *list, int free_refs) while (item) { next = item->next; - if (free_refs) { - bufrelease(item->ref->contents); - free(item->ref); - } + if (free_refs) + free_footnote_ref(item->ref); free(item); item = next; } @@ -2508,8 +2513,10 @@ is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct fo ref = create_footnote_ref(list, data + id_offset, id_end - id_offset); if (!ref) return 0; - if (!add_footnote_ref(list, ref)) + if (!add_footnote_ref(list, ref)) { + free_footnote_ref(ref); return 0; + } ref->contents = contents; } From 42aae21e0e4f985229298282b1df47ad514c082a Mon Sep 17 00:00:00 2001 From: Ben Dolman Date: Fri, 9 Nov 2012 00:14:37 -0700 Subject: [PATCH 4/7] Fix a tab spacing problem --- src/markdown.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/markdown.c b/src/markdown.c index 7cf5c7ee..b78656e8 100644 --- a/src/markdown.c +++ b/src/markdown.c @@ -2516,7 +2516,7 @@ is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct fo if (!add_footnote_ref(list, ref)) { free_footnote_ref(ref); return 0; - } + } ref->contents = contents; } From aedf499601bb184d8802301bfdec04868d08972c Mon Sep 17 00:00:00 2001 From: Ben Dolman Date: Fri, 9 Nov 2012 00:22:35 -0700 Subject: [PATCH 5/7] No more colons in footnote html ids. Apparently causes issues with jQuery. --- html/html.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/html/html.c b/html/html.c index 25af8e78..bd3c1def 100755 --- a/html/html.c +++ b/html/html.c @@ -516,10 +516,10 @@ rndr_footnote_def(struct buf *ob, const struct buf *text, unsigned int num, void } } - bufprintf(ob, "\n
  • \n", num); + bufprintf(ob, "\n
  • \n", num); if (pfound) { bufput(ob, text->data, i); - bufprintf(ob, " ", num); + bufprintf(ob, " ", num); bufput(ob, text->data + i, text->size - i); } else if (text) { bufput(ob, text->data, text->size); @@ -530,7 +530,7 @@ rndr_footnote_def(struct buf *ob, const struct buf *text, unsigned int num, void static int rndr_footnote_ref(struct buf *ob, unsigned int num, void *opaque) { - bufprintf(ob, "%d", num, num, num); + bufprintf(ob, "%d", num, num, num); return 1; } From e9461f71fdf34b67dde792c1cabfdb2024ff81ac Mon Sep 17 00:00:00 2001 From: "Sami A." Date: Fri, 9 Nov 2012 06:55:07 -0800 Subject: [PATCH 6/7] Prevent line break of footnote backlink --- html/html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/html.c b/html/html.c index bd3c1def..2faf0615 100755 --- a/html/html.c +++ b/html/html.c @@ -519,7 +519,7 @@ rndr_footnote_def(struct buf *ob, const struct buf *text, unsigned int num, void bufprintf(ob, "\n
  • \n", num); if (pfound) { bufput(ob, text->data, i); - bufprintf(ob, " ", num); + bufprintf(ob, " ", num); bufput(ob, text->data + i, text->size - i); } else if (text) { bufput(ob, text->data, text->size); From 0bf8cfd48f6b5cae74a4ebef75d741a35eb1000d Mon Sep 17 00:00:00 2001 From: "Sami A." Date: Fri, 9 Nov 2012 06:55:30 -0800 Subject: [PATCH 7/7] Add line break after footnote div --- html/html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/html.c b/html/html.c index 2faf0615..ba626e71 100755 --- a/html/html.c +++ b/html/html.c @@ -494,7 +494,7 @@ rndr_footnotes(struct buf *ob, const struct buf *text, void *opaque) if (text) bufput(ob, text->data, text->size); - BUFPUTSL(ob, "\n\n"); + BUFPUTSL(ob, "\n\n\n"); } static void