diff --git a/api_test/main.c b/api_test/main.c index 1a41e694a..478ee5da4 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -730,6 +730,30 @@ static void render_plaintext(test_batch_runner *runner) { "render document without wrapping"); free(plaintext); + static const char multibyte_markdown[] = + "- D\xC3\xA9j\xC3\xA0 vu \xC3\xA0 Z\xC3\xBCrich, \xE6\x97\xA5\xE6\x9C\xAC " + "\xF0\x9F\x98\x80 encore\n" + "\n" + " ```\n" + " code one\n" + " code two\n" + " ```\n"; + cmark_node *multibyte_doc = cmark_parse_document( + multibyte_markdown, sizeof(multibyte_markdown) - 1, CMARK_OPT_DEFAULT); + plaintext = cmark_render_plaintext(multibyte_doc, CMARK_OPT_DEFAULT, 12); + STR_EQ(runner, plaintext, + " - D\xC3\xA9j\xC3\xA0 vu\n" + " \xC3\xA0\n" + " Z\xC3\xBCrich,\n" + " \xE6\x97\xA5\xE6\x9C\xAC \xF0\x9F\x98\x80\n" + " encore\n" + " \n" + " code one\n" + " code two\n", + "render multibyte text and code block with wrapping"); + free(plaintext); + cmark_node_free(multibyte_doc); + cmark_node *text = cmark_node_new(CMARK_NODE_TEXT); cmark_node_set_literal(text, "Hi"); plaintext = cmark_render_plaintext(text, CMARK_OPT_DEFAULT, 0); diff --git a/src/plaintext.c b/src/plaintext.c index 0c7d257b3..6db5b92e2 100644 --- a/src/plaintext.c +++ b/src/plaintext.c @@ -10,12 +10,6 @@ // Functions to convert cmark_nodes to plain text strings. -static CMARK_INLINE void outc(cmark_renderer *renderer, cmark_node *node, - cmark_escaping escape, - int32_t c, unsigned char nextc) { - cmark_render_code_point(renderer, c); -} - static int S_render_node(cmark_renderer *renderer, cmark_node *node, cmark_event_type ev_type, int options) { int list_number; @@ -214,5 +208,5 @@ char *cmark_render_plaintext_with_mem(cmark_node *root, int options, int width, // a different meaning with OPT_HARDBREAKS width = 0; } - return cmark_render(mem, root, options, width, outc, S_render_node); + return cmark_render(mem, root, options, width, NULL, S_render_node); } diff --git a/src/render.c b/src/render.c index 1a0d2ae8d..ebfb08e23 100644 --- a/src/render.c +++ b/src/render.c @@ -19,6 +19,12 @@ static CMARK_INLINE void S_blankline(cmark_renderer *renderer) { } } +static void S_outc_verbatim(cmark_renderer *renderer, cmark_node *node, + cmark_escaping escape, int32_t c, + unsigned char nextc) { + cmark_render_code_point(renderer, c); +} + static void S_out(cmark_renderer *renderer, cmark_node *node, const char *source, bool wrap, cmark_escaping escape) { @@ -57,6 +63,13 @@ static void S_out(cmark_renderer *renderer, cmark_node *node, renderer->need_cr -= 1; } + // Characters the renderer writes back unchanged are copied in runs rather + // than decoded and re-encoded one by one. Spaces (when wrapping), newlines + // (in LITERAL), U+FFFE, U+FFFF and invalid UTF-8 still go through the loop + // below. + bool verbatim = + !ext && (escape == LITERAL || renderer->outc == S_outc_verbatim); + while (i < length) { if (renderer->begin_line) { cmark_strbuf_put(renderer->buffer, renderer->prefix->ptr, @@ -65,6 +78,41 @@ static void S_out(cmark_renderer *renderer, cmark_node *node, renderer->column = renderer->prefix->size; } + // With no pending break point, the width check below cannot fire. + if (verbatim && (renderer->width == 0 || renderer->last_breakable == 0)) { + int run_end = i; + int code_points = 0; + bool all_digits = true; + while (run_end < length) { + unsigned char byte = (unsigned char)source[run_end]; + if (byte < 0x80) { + c = byte; + len = 1; + } else { + len = cmark_utf8proc_iterate((const uint8_t *)source + run_end, + length - run_end, &c); + // cmark_utf8proc_encode_char writes U+FFFE and U+FFFF as one byte. + if (len == -1 || c == 0xFFFE || c == 0xFFFF) + break; + } + if ((c == ' ' && wrap) || (c == '\n' && escape == LITERAL)) + break; + // (char)c truncates, as the per-character path below does. + all_digits = all_digits && cmark_isdigit((char)c); + run_end += len; + code_points += 1; + } + if (run_end > i) { + cmark_strbuf_put(renderer->buffer, (const unsigned char *)source + i, + run_end - i); + renderer->column += code_points; + renderer->begin_line = false; + renderer->begin_content = renderer->begin_content && all_digits; + i = run_end; + continue; + } + } + len = cmark_utf8proc_iterate((const uint8_t *)source + i, length - i, &c); if (len == -1) { // error condition return; // return without rendering rest of string @@ -169,6 +217,9 @@ char *cmark_render(cmark_mem *mem, cmark_node *root, int options, int width, char *result; cmark_iter *iter = cmark_iter_new(root); + if (outc == NULL) + outc = S_outc_verbatim; + cmark_renderer renderer = {mem, &buf, &pref, 0, width, 0, 0, true, true, false, false, outc, S_cr, S_blankline, S_out, diff --git a/src/render.h b/src/render.h index 4a68d1e07..0c6c079e1 100644 --- a/src/render.h +++ b/src/render.h @@ -47,6 +47,7 @@ void cmark_render_ascii(cmark_renderer *renderer, const char *s); void cmark_render_code_point(cmark_renderer *renderer, uint32_t c); +// A NULL outc writes every code point unchanged, which lets S_out copy runs. char *cmark_render(cmark_mem *mem, cmark_node *root, int options, int width, void (*outc)(cmark_renderer *, cmark_node *, cmark_escaping, int32_t,