#include <stdlib.h> #include <stdio.h> #include <string.h> #include <assert.h> #include "config.h" #include "cmark.h" #include "node.h" #include "buffer.h" #include "html/houdini.h" // Functions to convert cmark_nodes to HTML strings. static bool finish_node(strbuf *html, cmark_node *node, bool tight); static void escape_html(strbuf *dest, const unsigned char *source, int length) { if (length < 0) length = strlen((char *)source); houdini_escape_html0(dest, source, (size_t)length, 0); } static void escape_href(strbuf *dest, const unsigned char *source, int length) { if (length < 0) length = strlen((char *)source); houdini_escape_href(dest, source, (size_t)length); } static inline void cr(strbuf *html) { if (html->size && html->ptr[html->size - 1] != '\n') strbuf_putc(html, '\n'); } // Convert the inline children of a node to a plain string. static void inlines_to_plain_html(strbuf *html, cmark_node* node) { cmark_node* cur = node->first_child; if (cur == NULL) { return; } while (true) { switch(cur->type) { case NODE_TEXT: case NODE_INLINE_CODE: case NODE_INLINE_HTML: escape_html(html, cur->as.literal.data, cur->as.literal.len); break; case NODE_LINEBREAK: case NODE_SOFTBREAK: strbuf_putc(html, ' '); break; default: break; } if (cur->first_child) { cur = cur->first_child; continue; } next_sibling: if (cur->next) { cur = cur->next; continue; } cur = cur->parent; if (cur == node) { break; } goto next_sibling; } } // Convert a cmark_node to HTML. static void node_to_html(strbuf *html, cmark_node *node) { cmark_node *cur; char start_header[] = "<h0>"; bool tight = false; bool visit_children; strbuf *info; if (node == NULL) { return; } cur = node; while (true) { // Only NODE_IMAGE wants to skip its children. visit_children = true; switch(cur->type) { case NODE_DOCUMENT: break; case NODE_PARAGRAPH: if (!tight) { cr(html); strbuf_puts(html, "<p>"); } break; case NODE_BLOCK_QUOTE: cr(html); strbuf_puts(html, "<blockquote>\n"); // BLOCK_QUOTE doesn't use any of the 'as' structs, // so the 'list' member can be used to store the // current value of 'tight'. cur->as.list.tight = tight; tight = false; break; case NODE_LIST_ITEM: cr(html); strbuf_puts(html, "<li>"); break; case NODE_LIST: { cmark_list *list = &cur->as.list; bool tmp; // make sure a list starts at the beginning of the line: cr(html); if (list->list_type == CMARK_BULLET_LIST) { strbuf_puts(html, "<ul>\n"); } else if (list->start == 1) { strbuf_puts(html, "<ol>\n"); } else { strbuf_printf(html, "<ol start=\"%d\">\n", list->start); } // Store the current value of 'tight' by swapping. tmp = list->tight; list->tight = tight; tight = tmp; break; } case NODE_HEADER: cr(html); start_header[2] = '0' + cur->as.header.level; strbuf_puts(html, start_header); break; case NODE_CODE_BLOCK: info = &cur->as.code.info; cr(html); if (&cur->as.code.fence_length == 0 || strbuf_len(info) == 0) { strbuf_puts(html, "<pre><code>"); } else { int first_tag = strbuf_strchr(info, ' ', 0); if (first_tag < 0) first_tag = strbuf_len(info); strbuf_puts(html, "<pre><code class=\"language-"); escape_html(html, info->ptr, first_tag); strbuf_puts(html, "\">"); } escape_html(html, cur->string_content.ptr, cur->string_content.size); break; case NODE_HTML: cr(html); strbuf_put(html, cur->string_content.ptr, cur->string_content.size); break; case NODE_HRULE: cr(html); strbuf_puts(html, "<hr />\n"); break; case NODE_REFERENCE_DEF: break; case NODE_TEXT: escape_html(html, cur->as.literal.data, cur->as.literal.len); break; case NODE_LINEBREAK: strbuf_puts(html, "<br />\n"); break; case NODE_SOFTBREAK: strbuf_putc(html, '\n'); break; case NODE_INLINE_CODE: strbuf_puts(html, "<code>"); escape_html(html, cur->as.literal.data, cur->as.literal.len); break; case NODE_INLINE_HTML: strbuf_put(html, cur->as.literal.data, cur->as.literal.len); break; case NODE_LINK: strbuf_puts(html, "<a href=\""); if (cur->as.link.url) escape_href(html, cur->as.link.url, -1); if (cur->as.link.title) { strbuf_puts(html, "\" title=\""); escape_html(html, cur->as.link.title, -1); } strbuf_puts(html, "\">"); break; case NODE_IMAGE: strbuf_puts(html, "<img src=\""); if (cur->as.link.url) escape_href(html, cur->as.link.url, -1); strbuf_puts(html, "\" alt=\""); inlines_to_plain_html(html, cur); if (cur->as.link.title) { strbuf_puts(html, "\" title=\""); escape_html(html, cur->as.link.title, -1); } strbuf_puts(html, "\" />"); visit_children = false; break; case NODE_STRONG: strbuf_puts(html, "<strong>"); break; case NODE_EMPH: strbuf_puts(html, "<em>"); break; default: assert(false); } if (visit_children && cur->first_child) { cur = cur->first_child; continue; } next_sibling: tight = finish_node(html, cur, tight); if (cur == node) { break; } if (cur->next) { cur = cur->next; continue; } cur = cur->parent; goto next_sibling; } } // Returns the restored value of 'tight'. static bool finish_node(strbuf *html, cmark_node *node, bool tight) { char end_header[] = "</h0>\n"; switch (node->type) { case NODE_PARAGRAPH: if (!tight) { strbuf_puts(html, "</p>\n"); } break; case NODE_BLOCK_QUOTE: { cmark_list *list = &node->as.list; strbuf_puts(html, "</blockquote>\n"); // Restore old 'tight' value. tight = list->tight; list->tight = false; break; } case NODE_LIST_ITEM: strbuf_puts(html, "</li>\n"); break; case NODE_LIST: { cmark_list *list = &node->as.list; bool tmp; strbuf_puts(html, list->list_type == CMARK_BULLET_LIST ? "</ul>\n" : "</ol>\n"); // Restore old 'tight' value. tmp = tight; tight = list->tight; list->tight = tmp; break; } case NODE_HEADER: end_header[3] = '0' + node->as.header.level; strbuf_puts(html, end_header); break; case NODE_CODE_BLOCK: strbuf_puts(html, "</code></pre>\n"); break; case NODE_INLINE_CODE: strbuf_puts(html, "</code>"); break; case NODE_LINK: strbuf_puts(html, "</a>"); break; case NODE_STRONG: strbuf_puts(html, "</strong>"); break; case NODE_EMPH: strbuf_puts(html, "</em>"); break; default: break; } return tight; } char *cmark_render_html(cmark_node *root) { char *result; strbuf html = GH_BUF_INIT; node_to_html(&html, root); result = (char *)strbuf_detach(&html); strbuf_free(&html); return result; }