diff --git a/CHANGELOG.md b/CHANGELOG.md index d2709898..ace28740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed markdown preview not rendering nested list structure (multi-level indented lists now display with proper nesting) - Fixed "Continue last" and "Resume specific" staying enabled (and selected) when a directory or profile with no sessions is chosen in the session selector - Fixed model list showing garbled names (e.g. "Mtoket 4.6 with 1M context") when the /model menu includes 1M-context variants and cost annotations diff --git a/pom.xml b/pom.xml index 3ce8db4b..3bec908f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.22.48-SNAPSHOT + 0.22.49-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 bb4d829e..ecfc8957 100644 --- a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java +++ b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java @@ -299,31 +299,9 @@ && isSeparatorRow(lines[i + 1])) { continue; } - // --- Unordered list --- - if (isUnordered(line)) { - html.append(""); - continue; - } - - // --- Ordered list --- - if (isOrdered(line)) { - html.append("
    "); - while (i < lines.length && isOrdered(lines[i])) { - html.append("
  1. ") - .append(inlineToHtml(lines[i] - .replaceFirst("^\\s*\\d+\\.\\s+", ""))) - .append("
  2. "); - i++; - } - html.append("
"); + // --- List (nested ordered/unordered) --- + if (isUnordered(line) || isOrdered(line)) { + i = renderNestedList(lines, i, html); continue; } @@ -531,5 +509,69 @@ 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<>(); + + int i = start; + while (i < lines.length) { + String line = lines[i]; + + if (line.isBlank()) { + int j = i + 1; + while (j < lines.length && lines[j].isBlank()) j++; + if (j < lines.length && (isOrdered(lines[j]) || isUnordered(lines[j]))) { + i = j; + continue; + } + break; + } + + if (!isOrdered(line) && !isUnordered(line)) break; + + int indent = leadingSpaces(line); + boolean ordered = isOrdered(line); + + if (indentStack.isEmpty()) { + html.append(ordered ? "
    " : "
      "); + indentStack.add(indent); + typeStack.add(ordered); + } else { + int topIndent = indentStack.get(indentStack.size() - 1); + if (indent > topIndent) { + html.append(ordered ? "
        " : "
          "); + indentStack.add(indent); + typeStack.add(ordered); + } else if (indent < topIndent) { + while (indentStack.size() > 1 && indentStack.get(indentStack.size() - 1) > indent) { + html.append(typeStack.get(typeStack.size() - 1) ? "
      " : "
    "); + indentStack.remove(indentStack.size() - 1); + typeStack.remove(typeStack.size() - 1); + } + } + 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 ? "
    " : "
      "); + } + } + + String content = line.replaceFirst("^\\s*(?:\\d+\\.|[-*])\\s+", ""); + html.append("
    • ").append(inlineToHtml(content)).append("
    • "); + i++; + } + + for (int k = typeStack.size() - 1; k >= 0; k--) { + html.append(typeStack.get(k) ? "
" : ""); + } + return i; + } + + private static int leadingSpaces(String line) { + int n = 0; + while (n < line.length() && line.charAt(n) == ' ') n++; + return n; + } + 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 443ad51c..0ea16901 100644 --- a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java +++ b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java @@ -266,6 +266,49 @@ void testHeadingSlugStripNonAlphanumeric() { assertTrue(html.contains(""), "slug strips punctuation"); } + // ------------------------------------------------------------------------- + // toHtml() — nested lists + // ------------------------------------------------------------------------- + + @Test + 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); + assertTrue(innerOl > outerOl, "inner
        must exist inside outer
          "); + assertTrue(html.contains("
        1. first
        2. ") || html.contains("
        3. first"), + "outer first item"); + assertTrue(html.contains("sub-first"), "sub-first item"); + assertTrue(html.contains("sub-second"), "sub-second item"); + assertTrue(html.contains("second"), "outer second item"); + // must have at least 2
        closing tags + long closeCount = html.chars().filter(c -> c == '<').count(); + assertTrue(html.indexOf("
      ", html.indexOf("
    ") + 1) > 0, + "at least 2
tags expected"); + } + + @Test + void testNestedUnorderedList() { + String md = "- alpha\n - sub-alpha\n - sub-beta\n- beta"; + String html = MarkdownRenderer.toHtml(md); + int outerUl = html.indexOf("
    "); + int innerUl = html.indexOf("
      ", outerUl + 1); + assertTrue(innerUl > outerUl, "inner
        must exist inside outer
          "); + assertTrue(html.contains("alpha"), "outer alpha"); + assertTrue(html.contains("sub-alpha"), "sub-alpha"); + } + + @Test + void testMixedNestedList() { + String md = "1. item\n - nested-ul\n2. item2"; + String html = MarkdownRenderer.toHtml(md); + assertTrue(html.contains("
            "), "outer ol"); + assertTrue(html.contains("
              "), "inner ul"); + assertTrue(html.contains("nested-ul"), "nested item text"); + } + // ------------------------------------------------------------------------- // resolveImagePaths() // -------------------------------------------------------------------------