From 0a971f057f3d29d62c7edafc5aa89becb684e080 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Tue, 12 May 2026 11:54:15 +0300 Subject: [PATCH] Fixed markdown preview rendering indented code blocks outside their list item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fenced code blocks indented inside an ordered list item were rendered as standalone
 elements between separate 
    elements, causing: - Wrong visual nesting (code block at left margin, not indented under its item) - Ordered list numbering reset to 1 after each code block - 4 extra leading spaces copied from the code block content Fix: detect indented fences (preceding segment ends with whitespace) and embed the
     inside the parent 
  1. , continuing the same
      for subsequent list items. Top-level code blocks (no indent) still break the list as expected. Added orderedListNumber() to emit
        so any list — indented or top-level — renders from its written start number. Added dedent() to strip common leading indentation from code block content. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + pom.xml | 2 +- .../ui/common/MarkdownRenderer.java | 70 +++++++++++++++-- .../ui/common/MarkdownRendererTest.java | 77 ++++++++++++++++++- 4 files changed, 137 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f489f73..5982a31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed markdown preview rendering fenced code blocks indented inside list items outside the list instead of nested within their list item - Fixed .md files in the Files panel losing all standard context menu items (Open, Cut, Copy, Delete, etc.) after installing the plugin — only "Preview Markdown" was shown - Fixed Bypass Permissions edit mode not suppressing the file diff dialog: the edit mode was lost from the registry because it was set before the working directory was assigned at session start diff --git a/pom.xml b/pom.xml index 436368b1..816fba30 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.51-SNAPSHOT + 0.22.53-SNAPSHOT nbm Netbeans Plugin Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java index ecfc8957..17e2906d 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java @@ -228,6 +228,7 @@ public static String toHtml(String markdown) { StringBuilder html = new StringBuilder(); // Split on fenced code blocks; odd-indexed segments are code content. String[] parts = markdown.split("```[^\\n]*\\n", -1); + boolean continueList = false; for (int i = 0; i < parts.length; i++) { if (i % 2 == 0) { // regular text segment @@ -236,19 +237,46 @@ public static String toHtml(String markdown) { if (i > 0 && seg.startsWith("```")) { seg = seg.substring(3); } - html.append(convertSegment(seg)); + String converted = convertSegment(seg); + if (continueList) { + // strip the new
          ...
        wrapper and continue inside the open list + converted = converted.replaceFirst("^
          ", "") + .replaceFirst("
        $", ""); + continueList = false; + html.append(converted).append("
      "); + } else { + html.append(converted); + } } else { - // code block content (strip trailing ```) + // code block content String code = parts[i]; if (code.endsWith("```")) { code = code.substring(0, code.length() - 3); } - if (code.endsWith("\n")) { - code = code.substring(0, code.length() - 1); + code = code.stripTrailing(); + code = dedent(code); + String preHtml = "
      " + esc(code) + "
      "; + // An indented fence (spaces before ```) means the code block belongs to a list item + boolean indented = i > 0 && parts[i - 1].matches("(?s).*[ \t]+$"); + if (indented) { + // Remove the list closure emitted by the previous segment, then + // embed the code block inside the last
    1. before re-closing it. + if (html.toString().endsWith("
    ")) { + html.delete(html.length() - 5, html.length()); + } + if (html.toString().endsWith("
  2. ")) { + html.delete(html.length() - 5, html.length()); + } + html.append(preHtml).append(""); + continueList = true; + } else { + html.append(preHtml); } - html.append("
    ").append(esc(code)).append("
    "); } } + if (continueList) { + html.append("
"); + } return html.toString(); } @@ -533,13 +561,13 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm boolean ordered = isOrdered(line); if (indentStack.isEmpty()) { - html.append(ordered ? "
    " : "
      "); + html.append(ordered ? "
        " : "
          "); indentStack.add(indent); typeStack.add(ordered); } else { int topIndent = indentStack.get(indentStack.size() - 1); if (indent > topIndent) { - html.append(ordered ? "
            " : "
              "); + html.append(ordered ? "
                " : "
                  "); indentStack.add(indent); typeStack.add(ordered); } else if (indent < topIndent) { @@ -552,7 +580,7 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm if (!typeStack.isEmpty() && typeStack.get(typeStack.size() - 1) != ordered) { html.append(typeStack.get(typeStack.size() - 1) ? "
              " : "
            "); typeStack.set(typeStack.size() - 1, ordered); - html.append(ordered ? "
              " : "
                "); + html.append(ordered ? "
                  " : "
                    "); } } @@ -573,5 +601,31 @@ private static int leadingSpaces(String line) { return n; } + private static int orderedListNumber(String line) { + java.util.regex.Matcher m = + java.util.regex.Pattern.compile("^\\s*(\\d+)\\.\\s+").matcher(line); + return m.find() ? Integer.parseInt(m.group(1)) : 1; + } + + private static String dedent(String code) { + String[] lines = code.split("\n", -1); + int min = Integer.MAX_VALUE; + for (String l : lines) { + if (!l.isBlank()) { + int sp = 0; + while (sp < l.length() && l.charAt(sp) == ' ') sp++; + min = Math.min(min, sp); + } + } + if (min == 0 || min == Integer.MAX_VALUE) return code; + StringBuilder sb = new StringBuilder(); + for (int k = 0; k < lines.length; k++) { + String l = lines[k]; + sb.append(l.length() >= min ? l.substring(min) : l); + if (k < lines.length - 1) sb.append('\n'); + } + return sb.toString(); + } + private MarkdownRenderer() {} } diff --git a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java index 0ea16901..84d8772b 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java @@ -89,7 +89,7 @@ void testUnorderedList() { @Test void testOrderedList() { String html = MarkdownRenderer.toHtml("1. first\n2. second"); - assertTrue(html.contains("
                      "), "ol open"); + assertTrue(html.contains("
                        "), "ol open"); assertTrue(html.contains("
                      1. first
                      2. "), "item first"); assertTrue(html.contains("
                      3. second
                      4. "), "item second"); } @@ -275,8 +275,8 @@ void testNestedOrderedList() { String md = "1. first\n 1. sub-first\n 2. sub-second\n2. second"; String html = MarkdownRenderer.toHtml(md); // outer ol must contain an inner ol - int outerOl = html.indexOf("
                          "); - int innerOl = html.indexOf("
                            ", outerOl + 1); + int outerOl = html.indexOf("
                              must exist inside outer
                                "); assertTrue(html.contains("
                              1. first
                              2. ") || html.contains("
                              3. first"), "outer first item"); @@ -304,7 +304,7 @@ void testNestedUnorderedList() { void testMixedNestedList() { String md = "1. item\n - nested-ul\n2. item2"; String html = MarkdownRenderer.toHtml(md); - assertTrue(html.contains("
                                  "), "outer ol"); + assertTrue(html.contains("
                                    "), "outer ol"); assertTrue(html.contains("
                                      "), "inner ul"); assertTrue(html.contains("nested-ul"), "nested item text"); } @@ -354,4 +354,73 @@ void testResolveImagePathsWindowsBackslashesNormalized() { String result = MarkdownRenderer.resolveImagePaths(html, "C:\\docs\\project"); assertEquals("\"\"", result); } + + // ------------------------------------------------------------------------- + // Ordered list numbering after code blocks + // ------------------------------------------------------------------------- + + @Test + void testOrderedListExplicitStartAttribute() { + // A list starting at 3 must emit
                                        + String html = MarkdownRenderer.toHtml("3. Third\n4. Fourth\n"); + assertTrue(html.contains("
                                          "), + "Expected
                                            but got: " + html); + } + + @Test + void testIndentedCodeBlockDoesNotResetNumbering() { + // Code block indented inside a list item — both items must be in ONE
                                              + String md = "1. Item one\n\n ```bash\n code\n ```\n\n2. Item two\n"; + String html = MarkdownRenderer.toHtml(md); + // Only one
                                                should exist; items continue in the same list + int firstOl = html.indexOf("
                                                  ; got: " + html); + assertTrue(html.contains("Item one") && html.contains("Item two"), + "Both items must be present: " + html); + } + + @Test + void testIndentedCodeBlockInsideLi() { + //
                                                   must be inside 
                                                1. , not between two separate
                                                    elements + String md = "1. Item one\n\n ```bash\n code here\n ```\n\n2. Item two\n"; + String html = MarkdownRenderer.toHtml(md); + // must not appear before
                                                    +        int liClose = html.indexOf("");
                                                    +        int preOpen = html.indexOf("
                                                    ");
                                                    +        assertTrue(preOpen > 0, "Expected 
                                                     in: " + html);
                                                    +        assertTrue(preOpen < liClose,
                                                    +                "
                                                     must appear before the first ; got: " + html);
                                                    +    }
                                                    +
                                                    +    @Test
                                                    +    void testTopLevelCodeBlockResetsNumbering() {
                                                    +        // Top-level code block between two lists — second list starts at 1
                                                    +        String md = "1. Item A\n\n```\ntop level\n```\n\n1. New list\n";
                                                    +        String html = MarkdownRenderer.toHtml(md);
                                                    +        long count = html.chars().filter(c -> c == '1')
                                                    +                .count(); // rough: just assert no 
                                                      for the second list + assertFalse(html.contains("
                                                        "), + "Top-level code block should reset numbering; must not have
                                                          but got: " + html); + } + + // ------------------------------------------------------------------------- + // Indented code block — no leading spaces in
                                                          +    // -------------------------------------------------------------------------
                                                          +
                                                          +    @Test
                                                          +    void testIndentedFencedCodeBlockNoLeadingSpaces() {
                                                          +        String md = "1. Item\n\n    ```bash\n    mkdir foo\n    ```\n\n2. Done\n";
                                                          +        String html = MarkdownRenderer.toHtml(md);
                                                          +        // Extract 
                                                           content and verify no leading spaces
                                                          +        int preStart = html.indexOf("
                                                          ") + "
                                                          ".length();
                                                          +        int preEnd = html.indexOf("
                                                          "); + assertTrue(preStart > 0 && preEnd > preStart, "Expected
                                                           block in: " + html);
                                                          +        String preContent = html.substring(preStart, preEnd);
                                                          +        assertFalse(preContent.startsWith(" "),
                                                          +                "Code block content must not start with spaces but got: [" + preContent + "]");
                                                          +        assertTrue(preContent.contains("mkdir foo"),
                                                          +                "Code block must contain the command: " + preContent);
                                                          +    }
                                                           }