From 9dd45733de009f77627b7cf4a2122f93a6e70979 Mon Sep 17 00:00:00 2001 From: zayeem shaikh Date: Sun, 18 Oct 2026 23:19:04 +0530 Subject: [PATCH] Escape \end{verbatim} inside latex code blocks The latex renderer writes code block contents unescaped between \begin{verbatim} and \end{verbatim}. LaTeX ends the environment at the first literal \end{verbatim}, so a code block containing that line closes it early and the rest of the block is read as TeX. Close the environment before each occurrence, print it escaped in \texttt, and reopen it. --- api_test/main.c | 20 ++++++++++++++++++++ src/latex.c | 32 +++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/api_test/main.c b/api_test/main.c index 0df1a7d8d..7f2bacb74 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -717,6 +717,26 @@ static void render_latex(test_batch_runner *runner) { "enumerate counter ignores bullet list nesting"); free(latex); cmark_node_free(nested_doc); + + // A code block containing \end{verbatim} must not close the verbatim + // environment and let the following lines be read as LaTeX. + static const char code[] = " a\n" + " \\end{verbatim}\n" + " \\input{secret}\n"; + cmark_node *code_doc = + cmark_parse_document(code, sizeof(code) - 1, CMARK_OPT_DEFAULT); + latex = cmark_render_latex(code_doc, CMARK_OPT_DEFAULT, 0); + STR_EQ(runner, latex, + "\\begin{verbatim}\n" + "a\n" + "\\end{verbatim}\n" + "\\texttt{\\textbackslash{}end\\{verbatim\\}}\n" + "\\begin{verbatim}\n" + "\\input{secret}\n" + "\\end{verbatim}\n", + "escape \\end{verbatim} inside code block"); + free(latex); + cmark_node_free(code_doc); } static void render_commonmark(test_batch_runner *runner) { diff --git a/src/latex.c b/src/latex.c index e3a25653e..2e970d93d 100644 --- a/src/latex.c +++ b/src/latex.c @@ -217,6 +217,36 @@ static int S_get_enumlevel(cmark_node *node) { return enumlevel; } +// A literal \end{verbatim} would close the environment early and the +// rest of the code block would be read as LaTeX, so close the environment +// before each occurrence, print it escaped, and reopen. +static void S_render_verbatim(cmark_renderer *renderer, const char *code) { + static const char end_verbatim[] = "\\end{verbatim}"; + const char *match; + cmark_strbuf buf = CMARK_BUF_INIT(renderer->mem); + + while ((match = strstr(code, end_verbatim)) != NULL) { + cmark_strbuf_set(&buf, (const unsigned char *)code, + (bufsize_t)(match - code)); + OUT((const char *)buf.ptr, false, LITERAL); + CR(); + LIT("\\end{verbatim}"); + CR(); + LIT("\\texttt{"); + OUT(end_verbatim, false, NORMAL); + LIT("}"); + CR(); + LIT("\\begin{verbatim}"); + CR(); + code = match + sizeof(end_verbatim) - 1; + if (*code == '\n') { + code++; + } + } + OUT(code, false, LITERAL); + cmark_strbuf_free(&buf); +} + static int S_render_node(cmark_renderer *renderer, cmark_node *node, cmark_event_type ev_type, int options) { int list_number; @@ -318,7 +348,7 @@ static int S_render_node(cmark_renderer *renderer, cmark_node *node, CR(); LIT("\\begin{verbatim}"); CR(); - OUT(cmark_node_get_literal(node), false, LITERAL); + S_render_verbatim(renderer, cmark_node_get_literal(node)); CR(); LIT("\\end{verbatim}"); BLANKLINE();