Exemple #1
0
void cmark_parser_free(cmark_parser *parser) {
  cmark_strbuf_free(parser->curline);
  free(parser->curline);
  cmark_strbuf_free(parser->linebuf);
  free(parser->linebuf);
  cmark_reference_map_free(parser->refmap);
  free(parser);
}
Exemple #2
0
char*
cmark_render(cmark_node *root,
             int options,
             int width,
             void (*outc)(cmark_renderer*,
                          cmark_escaping,
                          int32_t,
                          unsigned char),
             int (*render_node)(cmark_renderer *renderer,
                                cmark_node *node,
                                cmark_event_type ev_type,
                                int options))
{
	cmark_strbuf pref = GH_BUF_INIT;
	cmark_strbuf buf = GH_BUF_INIT;
	cmark_node *cur;
	cmark_event_type ev_type;
	char *result;
	cmark_iter *iter = cmark_iter_new(root);

	cmark_renderer renderer = { &buf, &pref, 0, width,
	                            0, 0, true, false, false,
	                            outc, S_cr, S_blankline, S_out
	                          };

	while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
		cur = cmark_iter_get_node(iter);
		if (!render_node(&renderer, cur, ev_type, options)) {
			// a false value causes us to skip processing
			// the node's contents.  this is used for
			// autolinks.
			cmark_iter_reset(iter, cur, CMARK_EVENT_EXIT);
		}
	}

	// ensure final newline
	if (renderer.buffer->ptr[renderer.buffer->size - 1] != '\n') {
		cmark_strbuf_putc(renderer.buffer, '\n');
	}

	result = (char *)cmark_strbuf_detach(renderer.buffer);

	cmark_iter_free(iter);
	cmark_strbuf_free(renderer.prefix);
	cmark_strbuf_free(renderer.buffer);

	return result;
}
Exemple #3
0
void cmark_consolidate_text_nodes(cmark_node *root) {
  cmark_iter *iter = cmark_iter_new(root);
  cmark_strbuf buf = GH_BUF_INIT;
  cmark_event_type ev_type;
  cmark_node *cur, *tmp, *next;

  while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
    cur = cmark_iter_get_node(iter);
    if (ev_type == CMARK_EVENT_ENTER && cur->type == CMARK_NODE_TEXT &&
        cur->next && cur->next->type == CMARK_NODE_TEXT) {
      cmark_strbuf_clear(&buf);
      cmark_strbuf_put(&buf, cur->as.literal.data, cur->as.literal.len);
      tmp = cur->next;
      while (tmp && tmp->type == CMARK_NODE_TEXT) {
        cmark_iter_next(iter); // advance pointer
        cmark_strbuf_put(&buf, tmp->as.literal.data, tmp->as.literal.len);
        next = tmp->next;
        cmark_node_free(tmp);
        tmp = next;
      }
      cmark_chunk_free(&cur->as.literal);
      cur->as.literal = cmark_chunk_buf_detach(&buf);
    }
  }

  cmark_strbuf_free(&buf);
  cmark_iter_free(iter);
}
Exemple #4
0
char *cmark_render_xml(cmark_node *root, long options)
{
	char *result;
	cmark_strbuf xml = GH_BUF_INIT;
	cmark_event_type ev_type;
	cmark_node *cur;
	struct render_state state = { &xml, 0 };

	if (options & CMARK_OPT_NORMALIZE) {
		cmark_consolidate_text_nodes(root);
	}

	cmark_iter *iter = cmark_iter_new(root);

	cmark_strbuf_puts(state.xml,
	                  "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
	cmark_strbuf_puts(state.xml,
	                  "<!DOCTYPE CommonMark SYSTEM \"CommonMark.dtd\">\n");
	while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
		cur = cmark_iter_get_node(iter);
		S_render_node(cur, ev_type, &state, options);
	}
	result = (char *)cmark_strbuf_detach(&xml);

	cmark_iter_free(iter);
	cmark_strbuf_free(&xml);
	return result;
}
Exemple #5
0
static void md_convert_hashes(cmark_iter *const iter) {
	for(;;) {
		cmark_event_type const event = cmark_iter_next(iter);
		if(CMARK_EVENT_DONE == event) break;
		if(CMARK_EVENT_EXIT != event) continue;
		cmark_node *const node = cmark_iter_get_node(iter);
		cmark_node_type const type = cmark_node_get_type(node);
		if(CMARK_NODE_LINK != type && CMARK_NODE_IMAGE != type) continue;

		char const *const URI = cmark_node_get_url(node);
		if(!URI) continue;
		if(0 != strncasecmp(URI, STR_LEN("hash:"))) continue;

		cmark_node *sup = superscript("#", HASH_INFO_MSG, URI);
		cmark_node_insert_after(node, sup);
		cmark_iter_reset(iter, sup, CMARK_EVENT_EXIT);

		char *escaped = QSEscape(URI, strlen(URI), true);
		size_t const elen = strlen(escaped);
		cmark_strbuf rel[1];
		char const qpfx[] = "/sources/";
		cmark_strbuf_init(&DEFAULT_MEM_ALLOCATOR, rel, sizeof(qpfx)-1+elen);
		cmark_strbuf_put(rel, (unsigned char const *)qpfx, sizeof(qpfx)-1);
		cmark_strbuf_put(rel, (unsigned char const *)escaped, elen);
		free(escaped); escaped = NULL;
		cmark_node_set_url(node, cmark_strbuf_cstr(rel));
		cmark_strbuf_free(rel);
	}
}
Exemple #6
0
void cmark_free_doc_parser(cmark_doc_parser *parser)
{
	cmark_strbuf_free(parser->curline);
	free(parser->curline);
	cmark_reference_map_free(parser->refmap);
	free(parser);
}
static cmark_node *try_opening_code_block(cmark_syntax_extension *self,
        bool indented,
        cmark_parser *parser,
        cmark_node *parent,
        const char *input)
{
    cmark_node *ret = NULL;
    cmark_bufsize_t matched = scan_open_gtkdoc_code_block(input,
                              cmark_parser_get_first_nonspace(parser));

    if (!indented && matched) {
        ret = cmark_parser_add_child(parser, parent,
                                     CMARK_NODE_CODE_BLOCK, cmark_parser_get_offset(parser));
        cmark_node_set_syntax_extension(ret, self);
        cmark_node_set_fenced(ret, true, 2,
                              cmark_parser_get_first_nonspace(parser) - cmark_parser_get_offset(parser),
                              0);
        cmark_parser_advance_offset(parser, input, matched, false);

        matched = scan_language_comment(input, matched);
        if (matched) {
            cmark_strbuf *lang = cmark_strbuf_new(32);
            cmark_strbuf_put(lang, (unsigned char *) input + 17, matched - 20);
            /* Will be transformed to fence info */
            cmark_node_set_string_content(ret, cmark_strbuf_get(lang));
            cmark_strbuf_free(lang);
            cmark_parser_advance_offset(parser, input, matched, false);
        }

    }

    return ret;
}
Exemple #8
0
// Free a cmark_node list and any children.
static
void S_free_nodes(cmark_node *e)
{
	cmark_node *next;
	while (e != NULL) {
		cmark_strbuf_free(&e->string_content);
		switch (e->type) {
		case NODE_CODE_BLOCK:
			cmark_chunk_free(&e->as.code.info);
			cmark_chunk_free(&e->as.code.literal);
			break;
		case NODE_TEXT:
		case NODE_INLINE_HTML:
		case NODE_CODE:
		case NODE_HTML:
			cmark_chunk_free(&e->as.literal);
			break;
		case NODE_LINK:
		case NODE_IMAGE:
			free(e->as.link.url);
			free(e->as.link.title);
			break;
		default:
			break;
		}
		if (e->last_child) {
			// Splice children into list
			e->last_child->next = e->next;
			e->next = e->first_child;
		}
		next = e->next;
		free(e);
		e = next;
	}
}
static cmark_node *fixup_nodes(cmark_inline_parser *inline_parser,
                                  cmark_node *parent,
                                  int size)
{
  int node_text_len;
  cmark_node *prev = NULL;
  cmark_node *tmp;
  int name_size = size;
  cmark_strbuf *name;

  for (prev = cmark_node_last_child(parent); prev; prev = cmark_node_previous(prev)) {
    if (cmark_node_get_type(prev) == CMARK_NODE_TEXT) {
      const char *text = cmark_node_get_literal(prev);
      node_text_len = strlen(text);
      size -= node_text_len;

      if (size <= 0) {
        if (size < 0) {
          char *split_text = my_strndup(text, size * -1);
          cmark_node *split = cmark_node_new(CMARK_NODE_TEXT);

          cmark_node_set_literal(split, split_text);
          free(split_text);

          split_text = my_strndup(text + (size * - 1), node_text_len - size);
          cmark_node_set_literal(prev, split_text);
          free(split_text);

          cmark_node_insert_before(prev, split);
        }
        break;
      }
    } else {
      return NULL;
    }
  }

  name = cmark_strbuf_new(name_size + 1);

  tmp = prev;

  while (tmp) {
    cmark_node *next = cmark_node_next(tmp);

    cmark_strbuf_puts(name, cmark_node_get_literal(tmp));
    if (tmp != prev)
      cmark_node_free(tmp);
    tmp = next;
  }

  cmark_node_set_type(prev, CMARK_NODE_LINK);
  cmark_node_set_url(prev, cmark_strbuf_get(name));

  cmark_strbuf_free(name);

  return prev;
}
static cmark_node *symbol_link_match(cmark_syntax_extension *self,
                                     cmark_parser *parser,
                                     cmark_node *parent,
                                     cmark_inline_parser *inline_parser) {
    cmark_node *link = NULL;
    char *symbol_name = NULL;
    NamedLink *named_link = NULL;
    int start_offset = cmark_inline_parser_get_offset(inline_parser);
    ParsingContext context;

    context.parser = inline_parser;
    context.allow_dashes = 0;

    if (start_offset > 0) {
        char prev_char = cmark_inline_parser_peek_at(
                             inline_parser,
                             start_offset - 1);

        if (prev_char && prev_char != ' ' && prev_char != '\t' && prev_char != '\n')
            return NULL;
    }

    cmark_inline_parser_advance_offset(inline_parser);

    symbol_name = cmark_inline_parser_take_while(inline_parser,
                  (CMarkInlinePredicate) is_valid_symbol_name, &context);

    if (!symbol_name)
        goto done;

    named_link = PRIV(self)->link_resolve_func(symbol_name);
    if (!named_link || !named_link->ref) {
        int actual_line, actual_col;

        translate_sourcepos(get_first_parent_block(parent),
                            start_offset, &actual_line, &actual_col);
        cmark_strbuf *message = cmark_strbuf_new(0);
        cmark_strbuf_puts(message, "Trying to link to non-existing symbol ‘");
        cmark_strbuf_puts(message, symbol_name);
        cmark_strbuf_puts(message, "’");
        diagnose("gtk-doc-bad-link", cmark_strbuf_get(message), actual_line - 1, actual_col - 1);
        cmark_strbuf_free(message);
        link = cmark_node_new (CMARK_NODE_TEXT);
        cmark_node_set_literal (link, symbol_name);
    } else {
        link = cmark_node_new(CMARK_NODE_LINK);
    }

    cmark_node_set_url(link, symbol_name);

done:
    free(symbol_name);
    free_named_link(named_link);

    return link;
}
Exemple #11
0
cmark_node *cmark_parser_finish(cmark_parser *parser)
{
	if (parser->linebuf->size) {
		S_process_line(parser, parser->linebuf->ptr,
		               parser->linebuf->size);
		cmark_strbuf_clear(parser->linebuf);
	}

	finalize_document(parser);
	cmark_strbuf_free(parser->curline);
#if CMARK_DEBUG_NODES
	if (cmark_node_check(parser->root, stderr)) {
		abort();
	}
#endif
	return parser->root;
}
Exemple #12
0
char *cmark_render_man(cmark_node *root, long options)
{
	char *result;
	cmark_strbuf man = GH_BUF_INIT;
	struct render_state state = { &man, NULL };
	cmark_node *cur;
	cmark_event_type ev_type;
	cmark_iter *iter = cmark_iter_new(root);

	if (options == 0) options = 0; // avoid warning about unused parameters

	while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
		cur = cmark_iter_get_node(iter);
		S_render_node(cur, ev_type, &state);
	}
	result = (char *)cmark_strbuf_detach(&man);

	cmark_iter_free(iter);
	cmark_strbuf_free(&man);
	return result;
}
Exemple #13
0
cmark_node *cmark_parser_finish(cmark_parser *parser) {
  if (parser->linebuf->size) {
    S_process_line(parser, parser->linebuf->ptr, parser->linebuf->size);
    cmark_strbuf_clear(parser->linebuf);
  }

  finalize_document(parser);

  if (parser->options & CMARK_OPT_NORMALIZE) {
    cmark_consolidate_text_nodes(parser->root);
  }

  cmark_strbuf_free(parser->curline);

#if CMARK_DEBUG_NODES
  if (cmark_node_check(parser->root, stderr)) {
    abort();
  }
#endif
  return parser->root;
}
Exemple #14
0
// Free a cmark_node list and any children.
static void S_free_nodes(cmark_node *e) {
  cmark_node *next;
  while (e != NULL) {
    cmark_strbuf_free(&e->content);
    switch (e->type) {
    case CMARK_NODE_CODE_BLOCK:
      cmark_chunk_free(NODE_MEM(e), &e->as.code.info);
      cmark_chunk_free(NODE_MEM(e), &e->as.code.literal);
      break;
    case CMARK_NODE_TEXT:
    case CMARK_NODE_HTML_INLINE:
    case CMARK_NODE_CODE:
    case CMARK_NODE_HTML_BLOCK:
      cmark_chunk_free(NODE_MEM(e), &e->as.literal);
      break;
    case CMARK_NODE_LINK:
    case CMARK_NODE_IMAGE:
      cmark_chunk_free(NODE_MEM(e), &e->as.link.url);
      cmark_chunk_free(NODE_MEM(e), &e->as.link.title);
      break;
    case CMARK_NODE_CUSTOM_BLOCK:
    case CMARK_NODE_CUSTOM_INLINE:
      cmark_chunk_free(NODE_MEM(e), &e->as.custom.on_enter);
      cmark_chunk_free(NODE_MEM(e), &e->as.custom.on_exit);
      break;
    default:
      break;
    }
    if (e->last_child) {
      // Splice children into list
      e->last_child->next = e->next;
      e->next = e->first_child;
    }
    next = e->next;
    NODE_MEM(e)->free(e);
    e = next;
  }
}
static cmark_node *fixup_nodes(cmark_syntax_extension *self,
                               cmark_parser *parser,
                               cmark_inline_parser *inline_parser,
                               cmark_node *parent,
                               int start_offset,
                               int size)
{
    int node_text_len;
    cmark_node *prev = NULL;
    cmark_node *tmp;
    int name_size = size;
    cmark_strbuf *name;
    NamedLink *named_link;

    for (prev = cmark_node_last_child(parent); prev; prev = cmark_node_previous(prev)) {
        if (cmark_node_get_type(prev) == CMARK_NODE_TEXT) {
            const char *text = cmark_node_get_literal(prev);
            node_text_len = strlen(text);
            size -= node_text_len;

            if (size <= 0) {
                if (size < 0) {
                    char *split_text = my_strndup(text, size * -1);
                    cmark_node *split = cmark_node_new(CMARK_NODE_TEXT);

                    cmark_node_set_literal(split, split_text);
                    free(split_text);

                    split_text = my_strndup(text + (size * - 1), node_text_len - size);
                    cmark_node_set_literal(prev, split_text);
                    free(split_text);

                    cmark_node_insert_before(prev, split);
                }
                break;
            }
        } else {
            return NULL;
        }
    }

    name = cmark_strbuf_new(name_size + 1);

    tmp = prev;

    while (tmp) {
        cmark_node *next = cmark_node_next(tmp);

        cmark_strbuf_puts(name, cmark_node_get_literal(tmp));
        if (tmp != prev)
            cmark_node_free(tmp);
        tmp = next;
    }

    named_link = PRIV(self)->link_resolve_func(cmark_strbuf_get(name));

    if (!named_link || !named_link->ref) {
        int actual_line, actual_col;

        translate_sourcepos(get_first_parent_block(parent),
                            start_offset, &actual_line, &actual_col);

        cmark_strbuf *message = cmark_strbuf_new(0);
        cmark_strbuf_puts(message, "Trying to link to non-existing symbol ‘");
        cmark_strbuf_puts(message, cmark_strbuf_get(name));
        cmark_strbuf_puts(message, "’");
        diagnose("gtk-doc-bad-link", cmark_strbuf_get(message), actual_line - 1,
                 actual_col - 1);
        cmark_strbuf_free(message);
        cmark_node_set_literal(prev, cmark_strbuf_get(name));
        cmark_strbuf_free(name);
        return prev;
    }

    free_named_link(named_link);

    cmark_node_set_type(prev, CMARK_NODE_LINK);
    cmark_node_set_url(prev, cmark_strbuf_get(name));

    cmark_strbuf_free(name);

    return prev;
}
Exemple #16
0
static int
S_render_node(cmark_renderer *renderer,
              cmark_node *node,
              cmark_event_type ev_type,
              int options)
{
	cmark_node *tmp;
	int list_number;
	cmark_delim_type list_delim;
	int numticks;
	int i;
	bool entering = (ev_type == CMARK_EVENT_ENTER);
	const char *info, *code, *title;
	size_t info_len, code_len;
	cmark_strbuf listmarker = GH_BUF_INIT;
	char *emph_delim;
	bufsize_t marker_width;

	// Don't adjust tight list status til we've started the list.
	// Otherwise we loose the blank line between a paragraph and
	// a following list.
	if (!(node->type == CMARK_NODE_ITEM && node->prev == NULL &&
	      entering)) {
		tmp = get_containing_block(node);
		renderer->in_tight_list_item =
		    (tmp->type == CMARK_NODE_ITEM &&
		     cmark_node_get_list_tight(tmp->parent)) ||
		    (tmp &&
		     tmp->parent &&
		     tmp->parent->type == CMARK_NODE_ITEM &&
		     cmark_node_get_list_tight(tmp->parent->parent));
	}

	switch (node->type) {
	case CMARK_NODE_DOCUMENT:
		break;

	case CMARK_NODE_BLOCK_QUOTE:
		if (entering) {
			LIT("> ");
			cmark_strbuf_puts(renderer->prefix, "> ");
		} else {
			cmark_strbuf_truncate(renderer->prefix,
			                      renderer->prefix->size - 2);
			BLANKLINE();
		}
		break;

	case CMARK_NODE_LIST:
		if (!entering && node->next &&
		    (node->next->type == CMARK_NODE_CODE_BLOCK ||
		     node->next->type == CMARK_NODE_LIST)) {
			// this ensures 2 blank lines after list,
			// if before code block or list:
			LIT("\n");
		}
		break;

	case CMARK_NODE_ITEM:
		if (cmark_node_get_list_type(node->parent) ==
		    CMARK_BULLET_LIST) {
			marker_width = 2;
		} else {
			list_number = cmark_node_get_list_start(node->parent);
			list_delim = cmark_node_get_list_delim(node->parent);
			tmp = node;
			while (tmp->prev) {
				tmp = tmp->prev;
				list_number += 1;
			}
			// we ensure a width of at least 4 so
			// we get nice transition from single digits
			// to double
			cmark_strbuf_printf(&listmarker,
			                    "%d%s%s", list_number,
			                    list_delim == CMARK_PAREN_DELIM ?
			                    ")" : ".",
			                    list_number < 10 ? "  " : " ");
			marker_width = listmarker.size;
		}
		if (entering) {
			if (cmark_node_get_list_type(node->parent) ==
			    CMARK_BULLET_LIST) {
				LIT("* ");
				cmark_strbuf_puts(renderer->prefix, "  ");
			} else {
				LIT((char *)listmarker.ptr);
				for (i = marker_width; i--;) {
					cmark_strbuf_putc(renderer->prefix, ' ');
				}
			}
		} else {
			cmark_strbuf_truncate(renderer->prefix,
			                      renderer->prefix->size -
			                      marker_width);
			CR();
		}
		cmark_strbuf_free(&listmarker);
		break;

	case CMARK_NODE_HEADER:
		if (entering) {
			for (int i = cmark_node_get_header_level(node); i > 0; i--) {
				LIT("#");
			}
			LIT(" ");
			renderer->no_wrap = true;
		} else {
			renderer->no_wrap = false;
			BLANKLINE();
		}
		break;

	case CMARK_NODE_CODE_BLOCK:
		BLANKLINE();
		info = cmark_node_get_fence_info(node);
		info_len = safe_strlen(info);
		code = cmark_node_get_literal(node);
		code_len = safe_strlen(code);
		// use indented form if no info, and code doesn't
		// begin or end with a blank line, and code isn't
		// first thing in a list item
		if (info_len == 0 &&
		    (code_len > 2 &&
		     !isspace(code[0]) &&
		     !(isspace(code[code_len - 1]) &&
		       isspace(code[code_len - 2]))) &&
		    !(node->prev == NULL && node->parent &&
		      node->parent->type == CMARK_NODE_ITEM)) {
			LIT("    ");
			cmark_strbuf_puts(renderer->prefix, "    ");
			OUT(cmark_node_get_literal(node), false, LITERAL);
			cmark_strbuf_truncate(renderer->prefix,
			                      renderer->prefix->size - 4);
		} else {
			numticks = longest_backtick_sequence(code) + 1;
			if (numticks < 3) {
				numticks = 3;
			}
			for (i = 0; i < numticks; i++) {
				LIT("`");
			}
			LIT(" ");
			OUT(info, false, LITERAL);
			CR();
			OUT(cmark_node_get_literal(node), false, LITERAL);
			CR();
			for (i = 0; i < numticks; i++) {
				LIT("`");
			}
		}
		BLANKLINE();
		break;

	case CMARK_NODE_HTML:
		BLANKLINE();
		OUT(cmark_node_get_literal(node), false, LITERAL);
		BLANKLINE();
		break;

	case CMARK_NODE_HRULE:
		BLANKLINE();
		LIT("-----");
		BLANKLINE();
		break;

	case CMARK_NODE_PARAGRAPH:
		if (!entering) {
			BLANKLINE();
		}
		break;

	case CMARK_NODE_TEXT:
		OUT(cmark_node_get_literal(node), true, NORMAL);
		break;

	case CMARK_NODE_LINEBREAK:
		if (!(CMARK_OPT_HARDBREAKS & options)) {
			LIT("\\");
		}
		CR();
		break;

	case CMARK_NODE_SOFTBREAK:
		if (renderer->width == 0 &&
		    !(CMARK_OPT_HARDBREAKS & options)) {
			CR();
		} else {
			OUT(" ", true, LITERAL);
		}
		break;

	case CMARK_NODE_CODE:
		code = cmark_node_get_literal(node);
		code_len = safe_strlen(code);
		numticks = shortest_unused_backtick_sequence(code);
		for (i = 0; i < numticks; i++) {
			LIT("`");
		}
		if (code_len == 0 || code[0] == '`') {
			LIT(" ");
		}
		OUT(cmark_node_get_literal(node), true, LITERAL);
		if (code_len == 0 || code[code_len - 1] == '`') {
			LIT(" ");
		}
		for (i = 0; i < numticks; i++) {
			LIT("`");
		}
		break;

	case CMARK_NODE_INLINE_HTML:
		OUT(cmark_node_get_literal(node), false, LITERAL);
		break;

	case CMARK_NODE_STRONG:
		if (entering) {
			LIT("**");
		} else {
			LIT("**");
		}
		break;

	case CMARK_NODE_EMPH:
		// If we have EMPH(EMPH(x)), we need to use *_x_*
		// because **x** is STRONG(x):
		if (node->parent && node->parent->type == CMARK_NODE_EMPH &&
		    node->next == NULL && node->prev == NULL) {
			emph_delim = "_";
		} else {
			emph_delim = "*";
		}
		if (entering) {
			LIT(emph_delim);
		} else {
			LIT(emph_delim);
		}
		break;

	case CMARK_NODE_LINK:
		if (is_autolink(node)) {
			if (entering) {
				LIT("<");
				if (strncmp(cmark_node_get_url(node),
				            "mailto:", 7) == 0) {
					LIT((char *)cmark_node_get_url(node) + 7);
				} else {
					LIT((char *)cmark_node_get_url(node));
				}
				LIT(">");
				// return signal to skip contents of node...
				return 0;
			}
		} else {
			if (entering) {
				LIT("[");
			} else {
				LIT("](");
				OUT(cmark_node_get_url(node), false, URL);
				title = cmark_node_get_title(node);
				if (safe_strlen(title) > 0) {
					LIT(" \"");
					OUT(title, false, TITLE);
					LIT("\"");
				}
				LIT(")");
			}
		}
		break;

	case CMARK_NODE_IMAGE:
		if (entering) {
			LIT("![");
		} else {
			LIT("](");
			OUT(cmark_node_get_url(node), false, URL);
			title = cmark_node_get_title(node);
			if (safe_strlen(title) > 0) {
				OUT(" \"", true, LITERAL);
				OUT(title, false, TITLE);
				LIT("\"");
			}
			LIT(")");
		}
		break;

	default:
		assert(false);
		break;
	}

	return 1;
}