Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.22.51-SNAPSHOT</version>
<version>0.22.53-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Plugin Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <ol start="N">...</ol> wrapper and continue inside the open list
converted = converted.replaceFirst("^<ol start=\"\\d+\">", "")
.replaceFirst("</ol>$", "");
continueList = false;
html.append(converted).append("</ol>");
} 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 = "<pre>" + esc(code) + "</pre>";
// 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 <li> before re-closing it.
if (html.toString().endsWith("</ol>")) {
html.delete(html.length() - 5, html.length());
}
if (html.toString().endsWith("</li>")) {
html.delete(html.length() - 5, html.length());
}
html.append(preHtml).append("</li>");
continueList = true;
} else {
html.append(preHtml);
}
html.append("<pre>").append(esc(code)).append("</pre>");
}
}
if (continueList) {
html.append("</ol>");
}
return html.toString();
}

Expand Down Expand Up @@ -533,13 +561,13 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm
boolean ordered = isOrdered(line);

if (indentStack.isEmpty()) {
html.append(ordered ? "<ol>" : "<ul>");
html.append(ordered ? "<ol start=\"" + orderedListNumber(line) + "\">" : "<ul>");
indentStack.add(indent);
typeStack.add(ordered);
} else {
int topIndent = indentStack.get(indentStack.size() - 1);
if (indent > topIndent) {
html.append(ordered ? "<ol>" : "<ul>");
html.append(ordered ? "<ol start=\"" + orderedListNumber(line) + "\">" : "<ul>");
indentStack.add(indent);
typeStack.add(ordered);
} else if (indent < topIndent) {
Expand All @@ -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) ? "</ol>" : "</ul>");
typeStack.set(typeStack.size() - 1, ordered);
html.append(ordered ? "<ol>" : "<ul>");
html.append(ordered ? "<ol start=\"" + orderedListNumber(line) + "\">" : "<ul>");
}
}

Expand All @@ -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() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ void testUnorderedList() {
@Test
void testOrderedList() {
String html = MarkdownRenderer.toHtml("1. first\n2. second");
assertTrue(html.contains("<ol>"), "ol open");
assertTrue(html.contains("<ol start=\"1\">"), "ol open");
assertTrue(html.contains("<li>first</li>"), "item first");
assertTrue(html.contains("<li>second</li>"), "item second");
}
Expand Down Expand Up @@ -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("<ol>");
int innerOl = html.indexOf("<ol>", outerOl + 1);
int outerOl = html.indexOf("<ol start=");
int innerOl = html.indexOf("<ol start=", outerOl + 1);
assertTrue(innerOl > outerOl, "inner <ol> must exist inside outer <ol>");
assertTrue(html.contains("<li>first</li>") || html.contains("<li>first"),
"outer first item");
Expand Down Expand Up @@ -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("<ol>"), "outer ol");
assertTrue(html.contains("<ol start=\"1\">"), "outer ol");
assertTrue(html.contains("<ul>"), "inner ul");
assertTrue(html.contains("nested-ul"), "nested item text");
}
Expand Down Expand Up @@ -354,4 +354,73 @@ void testResolveImagePathsWindowsBackslashesNormalized() {
String result = MarkdownRenderer.resolveImagePaths(html, "C:\\docs\\project");
assertEquals("<img src=\"file://C:/docs/project/img.png\" alt=\"\">", result);
}

// -------------------------------------------------------------------------
// Ordered list numbering after code blocks
// -------------------------------------------------------------------------

@Test
void testOrderedListExplicitStartAttribute() {
// A list starting at 3 must emit <ol start="3">
String html = MarkdownRenderer.toHtml("3. Third\n4. Fourth\n");
assertTrue(html.contains("<ol start=\"3\">"),
"Expected <ol start=\"3\"> but got: " + html);
}

@Test
void testIndentedCodeBlockDoesNotResetNumbering() {
// Code block indented inside a list item — both items must be in ONE <ol start="1">
String md = "1. Item one\n\n ```bash\n code\n ```\n\n2. Item two\n";
String html = MarkdownRenderer.toHtml(md);
// Only one <ol> should exist; items continue in the same list
int firstOl = html.indexOf("<ol ");
int secondOl = html.indexOf("<ol ", firstOl + 1);
assertEquals(-1, secondOl,
"Indented code block must not start a new <ol>; got: " + html);
assertTrue(html.contains("Item one") && html.contains("Item two"),
"Both items must be present: " + html);
}

@Test
void testIndentedCodeBlockInsideLi() {
// <pre> must be inside <li>, not between two separate <ol> elements
String md = "1. Item one\n\n ```bash\n code here\n ```\n\n2. Item two\n";
String html = MarkdownRenderer.toHtml(md);
// </li> must not appear before <pre>
int liClose = html.indexOf("</li>");
int preOpen = html.indexOf("<pre>");
assertTrue(preOpen > 0, "Expected <pre> in: " + html);
assertTrue(preOpen < liClose,
"<pre> must appear before the first </li>; 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 <ol start="2"> for the second list
assertFalse(html.contains("<ol start=\"2\">"),
"Top-level code block should reset numbering; must not have <ol start=\"2\"> but got: " + html);
}

// -------------------------------------------------------------------------
// Indented code block — no leading spaces in <pre>
// -------------------------------------------------------------------------

@Test
void testIndentedFencedCodeBlockNoLeadingSpaces() {
String md = "1. Item\n\n ```bash\n mkdir foo\n ```\n\n2. Done\n";
String html = MarkdownRenderer.toHtml(md);
// Extract <pre> content and verify no leading spaces
int preStart = html.indexOf("<pre>") + "<pre>".length();
int preEnd = html.indexOf("</pre>");
assertTrue(preStart > 0 && preEnd > preStart, "Expected <pre> 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);
}
}
Loading