From 2820aec7564ebc37c7362a4116ccc19b618a521f Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sun, 17 May 2026 08:35:46 +0300 Subject: [PATCH 1/3] Intermediate commit --- pom.xml | 2 +- .../ui/common/MarkdownRenderer.java | 105 +++++++++--- .../ui/common/MarkdownRendererTest.java | 157 ++++++++++++++++++ 3 files changed, 242 insertions(+), 22 deletions(-) diff --git a/pom.xml b/pom.xml index 1b493418..0411d9cf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.23.6-SNAPSHOT + 0.23.9-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 17e2906d..a60648ff 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java @@ -226,29 +226,46 @@ public static void appendInfo(JEditorPane pane, String line) { */ 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; + List savedTypeStack = null; + List savedIndentStack = null; + int savedOwnerIndent = -1; for (int i = 0; i < parts.length; i++) { if (i % 2 == 0) { - // regular text segment - // strip trailing ``` from previous code block end String seg = parts[i]; if (i > 0 && seg.startsWith("```")) { seg = seg.substring(3); } - String converted = convertSegment(seg); if (continueList) { - // strip the new
    ...
wrapper and continue inside the open list - converted = converted.replaceFirst("^
    ", "") - .replaceFirst("
$", ""); + String[] contLines = seg.split("\n", -1); + // Find the first non-blank list item line. + int firstIdx = 0; + while (firstIdx < contLines.length + && (contLines[firstIdx].isBlank() + || (!isOrdered(contLines[firstIdx]) && !isUnordered(contLines[firstIdx])))) { + firstIdx++; + } + int subListEnd = firstIdx; + if (firstIdx < contLines.length + && leadingSpaces(contLines[firstIdx]) > savedOwnerIndent) { + // Sub-items belong inside the currently open
  • — render them there. + subListEnd = renderNestedList(contLines, firstIdx, html); + } + // Close the owner item now that its content (code block + any sub-list) is done. + html.append("
  • "); + // Render remaining lines using the saved outer context so that items at + // different indent levels (siblings vs. outer) are placed correctly. + renderNestedList(contLines, subListEnd, html, + new ArrayList<>(savedIndentStack), new ArrayList<>(savedTypeStack)); continueList = false; - html.append(converted).append(""); + savedTypeStack = null; + savedIndentStack = null; + savedOwnerIndent = -1; } else { - html.append(converted); + html.append(convertSegment(seg)); } } else { - // code block content String code = parts[i]; if (code.endsWith("```")) { code = code.substring(0, code.length() - 3); @@ -256,18 +273,38 @@ public static String toHtml(String markdown) { 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
  • before re-closing it. - if (html.toString().endsWith("")) { + // Reconstruct the active list context by scanning all text segments up to here. + savedIndentStack = new ArrayList<>(); + savedTypeStack = new ArrayList<>(); + for (int k = 0; k < i; k += 2) { + for (String pl : parts[k].split("\n", -1)) { + if (!isOrdered(pl) && !isUnordered(pl)) continue; + int indent = leadingSpaces(pl); + boolean isOl = isOrdered(pl); + while (!savedIndentStack.isEmpty() + && savedIndentStack.get(savedIndentStack.size() - 1) >= indent) { + savedIndentStack.remove(savedIndentStack.size() - 1); + savedTypeStack.remove(savedTypeStack.size() - 1); + } + savedIndentStack.add(indent); + savedTypeStack.add(isOl); + } + } + savedOwnerIndent = savedIndentStack.isEmpty() ? -1 + : savedIndentStack.get(savedIndentStack.size() - 1); + // Remove ALL trailing list-close tags and the owner item's
  • from html. + String buf = html.toString(); + while (buf.endsWith("") || buf.endsWith("")) { html.delete(html.length() - 5, html.length()); + buf = html.toString(); } - if (html.toString().endsWith("")) { + if (buf.endsWith("")) { html.delete(html.length() - 5, html.length()); } - html.append(preHtml).append(""); + // Append code block but leave the owner
  • open for sub-list content. + html.append(preHtml); continueList = true; } else { html.append(preHtml); @@ -275,7 +312,11 @@ public static String toHtml(String markdown) { } } if (continueList) { - html.append(""); + // Code block was the last content — close owner item then close all open lists. + html.append("
  • "); + for (int k = savedTypeStack.size() - 1; k >= 0; k--) { + html.append(savedTypeStack.get(k) ? "" : ""); + } } return html.toString(); } @@ -538,8 +579,12 @@ private static void appendHtml(JEditorPane pane, String html) { } private static int renderNestedList(String[] lines, int start, StringBuilder html) { - List indentStack = new ArrayList<>(); - List typeStack = new ArrayList<>(); + return renderNestedList(lines, start, html, new ArrayList<>(), new ArrayList<>()); + } + + private static int renderNestedList(String[] lines, int start, StringBuilder html, + List indentStack, List typeStack) { + int startIndent = indentStack.isEmpty() ? -1 : indentStack.get(0); int i = start; while (i < lines.length) { @@ -561,6 +606,7 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm boolean ordered = isOrdered(line); if (indentStack.isEmpty()) { + startIndent = indent; html.append(ordered ? "
      " : "
        "); indentStack.add(indent); typeStack.add(ordered); @@ -571,11 +617,15 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm indentStack.add(indent); typeStack.add(ordered); } else if (indent < topIndent) { - while (indentStack.size() > 1 && indentStack.get(indentStack.size() - 1) > indent) { + while (!indentStack.isEmpty() && indentStack.get(indentStack.size() - 1) > indent) { html.append(typeStack.get(typeStack.size() - 1) ? "
    " : ""); indentStack.remove(indentStack.size() - 1); typeStack.remove(typeStack.size() - 1); } + // Item is less indented than our starting level — belongs to outer context. + if (indentStack.isEmpty() || indent < startIndent) { + break; + } } if (!typeStack.isEmpty() && typeStack.get(typeStack.size() - 1) != ordered) { html.append(typeStack.get(typeStack.size() - 1) ? "" : ""); @@ -585,8 +635,21 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm } String content = line.replaceFirst("^\\s*(?:\\d+\\.|[-*])\\s+", ""); - html.append("
  • ").append(inlineToHtml(content)).append("
  • "); + html.append("
  • ").append(inlineToHtml(content)); i++; + // Lookahead: include non-list lines more indented than topIndent as continuation content. + int curTopIndent = indentStack.get(indentStack.size() - 1); + int j = i; + while (j < lines.length && lines[j].isBlank()) j++; + while (j < lines.length && !lines[j].isBlank() + && !isOrdered(lines[j]) && !isUnordered(lines[j]) + && leadingSpaces(lines[j]) > curTopIndent) { + html.append(inlineToHtml(lines[j].trim())); + j++; + while (j < lines.length && lines[j].isBlank()) j++; + } + i = j; + html.append("
  • "); } for (int k = typeStack.size() - 1; k >= 0; k--) { 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 84d8772b..29c90345 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java @@ -423,4 +423,161 @@ void testIndentedFencedCodeBlockNoLeadingSpaces() { assertTrue(preContent.contains("mkdir foo"), "Code block must contain the command: " + preContent); } + + // ------------------------------------------------------------------------- + // Nested list + multiple code blocks + // ------------------------------------------------------------------------- + + @Test + void testNestedListMultipleCodeBlocks() { + // Depth-2 list: items 1 and 2 each have an indented code block. + String md = + "1. First:\n\n" + + " ```bash\n" + + " cmd1\n" + + " ```\n\n" + + "2. Second:\n\n" + + " ```bash\n" + + " cmd2\n" + + " ```\n\n" + + "3. Third\n"; + String html = MarkdownRenderer.toHtml(md); + // All items must be in ONE
      . + int firstOl = html.indexOf("
        , got: " + html); + assertTrue(html.contains("cmd1"), "cmd1 present: " + html); + assertTrue(html.contains("cmd2"), "cmd2 present: " + html); + assertTrue(html.contains("Third"), "Third present: " + html); + // Both
         blocks must appear before the list closes.
        +        int pre1 = html.indexOf("
        ");
        +        int pre2 = html.indexOf("
        ", pre1 + 1);
        +        assertTrue(pre2 > pre1, "Two 
         blocks expected: " + html);
        +        assertTrue(html.indexOf("
      ") > pre2, "
    must come after both
    : " + html);
    +    }
    +
    +    @Test
    +    void testListReturnsToOuterLevelAfterSubList() {
    +        // item 2 has a sub-list; item 3 must return to the top level.
    +        String md =
    +                "1. Item one\n" +
    +                "    1. Sub-item\n" +
    +                "2. Item two\n";
    +        String html = MarkdownRenderer.toHtml(md);
    +        // "Item two" must be in the outer 
      , not stuck inside the inner one. + // Verify:
    count is exactly 2 (inner + outer). + int count = 0; + int idx = 0; + while ((idx = html.indexOf("", idx)) != -1) { count++; idx++; } + assertEquals(2, count, "Expected 2 tags, got: " + html); + assertTrue(html.contains("Item two"), "Item two present: " + html); + } + + @Test + void testUnorderedListIndentedCodeBlockNotSplit() { + String md = + "- Item one\n\n" + + " ```bash\n" + + " echo hi\n" + + " ```\n\n" + + "- Item two\n"; + String html = MarkdownRenderer.toHtml(md); + int firstUl = html.indexOf("