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
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.23.6-SNAPSHOT</version>
<version>0.23.10-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 @@ -226,56 +226,101 @@ 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<Boolean> savedTypeStack = null;
List<Integer> 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 <ol start="N">...</ol> wrapper and continue inside the open list
converted = converted.replaceFirst("^<ol start=\"\\d+\">", "")
.replaceFirst("</ol>$", "");
String[] contLines = seg.split("\n", -1);
// Skip only blank lines to find the first non-blank content.
int firstIdx = 0;
while (firstIdx < contLines.length && contLines[firstIdx].isBlank()) {
firstIdx++;
}
int subListEnd = firstIdx;
if (firstIdx < contLines.length
&& leadingSpaces(contLines[firstIdx]) > savedOwnerIndent) {
// Sub-items belong inside the currently open <li> — 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("</li>");
// Render remaining lines using the saved outer context so that items at
// different indent levels (siblings vs. outer) are placed correctly.
int listEnd = renderNestedList(contLines, subListEnd, html,
new ArrayList<>(savedIndentStack), new ArrayList<>(savedTypeStack));
continueList = false;
html.append(converted).append("</ol>");
savedTypeStack = null;
savedIndentStack = null;
savedOwnerIndent = -1;
// Process any content after the list (headings, paragraphs, next list prefix).
if (listEnd < contLines.length) {
String tail = String.join("\n",
Arrays.copyOfRange(contLines, listEnd, contLines.length));
html.append(convertSegment(tail));
}
} 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);
}
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>")) {
// 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 </li> from html.
String buf = html.toString();
while (buf.endsWith("</ol>") || buf.endsWith("</ul>")) {
html.delete(html.length() - 5, html.length());
buf = html.toString();
}
if (html.toString().endsWith("</li>")) {
if (buf.endsWith("</li>")) {
html.delete(html.length() - 5, html.length());
}
html.append(preHtml).append("</li>");
// Append code block but leave the owner <li> open for sub-list content.
html.append(preHtml);
continueList = true;
} else {
html.append(preHtml);
}
}
}
if (continueList) {
html.append("</ol>");
// Code block was the last content — close owner item then close all open lists.
html.append("</li>");
for (int k = savedTypeStack.size() - 1; k >= 0; k--) {
html.append(savedTypeStack.get(k) ? "</ol>" : "</ul>");
}
}
return html.toString();
}
Expand Down Expand Up @@ -538,8 +583,12 @@ private static void appendHtml(JEditorPane pane, String html) {
}

private static int renderNestedList(String[] lines, int start, StringBuilder html) {
List<Integer> indentStack = new ArrayList<>();
List<Boolean> typeStack = new ArrayList<>();
return renderNestedList(lines, start, html, new ArrayList<>(), new ArrayList<>());
}

private static int renderNestedList(String[] lines, int start, StringBuilder html,
List<Integer> indentStack, List<Boolean> typeStack) {
int startIndent = indentStack.isEmpty() ? -1 : indentStack.get(0);

int i = start;
while (i < lines.length) {
Expand All @@ -561,6 +610,7 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm
boolean ordered = isOrdered(line);

if (indentStack.isEmpty()) {
startIndent = indent;
html.append(ordered ? "<ol start=\"" + orderedListNumber(line) + "\">" : "<ul>");
indentStack.add(indent);
typeStack.add(ordered);
Expand All @@ -571,11 +621,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) ? "</ol>" : "</ul>");
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) ? "</ol>" : "</ul>");
Expand All @@ -585,8 +639,21 @@ private static int renderNestedList(String[] lines, int start, StringBuilder htm
}

String content = line.replaceFirst("^\\s*(?:\\d+\\.|[-*])\\s+", "");
html.append("<li>").append(inlineToHtml(content)).append("</li>");
html.append("<li>").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("</li>");
}

for (int k = typeStack.size() - 1; k >= 0; k--) {
Expand Down
Loading
Loading