From 36d36e47cc37c6046e60aeda3868dd5a0c837d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 13 May 2026 21:09:49 +0800 Subject: [PATCH 001/136] feat(autocomplete): add core interfaces, selection strategies, and word boundary resolver --- .../autocomplete/AutocompleteContext.java | 7 ++ .../autocomplete/SelectionStrategy.java | 6 ++ .../autocomplete/SyntaxContextResolver.java | 8 +++ .../autocomplete/SyntaxElementType.java | 9 +++ .../autocomplete/TextSyntaxContext.java | 26 +++++++ .../resolver/SelectionStrategies.java | 72 +++++++++++++++++++ .../resolver/WordBoundaryResolver.java | 38 ++++++++++ 7 files changed, 166 insertions(+) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java new file mode 100644 index 00000000..0aff6c5f --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java @@ -0,0 +1,7 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +public interface AutocompleteContext { + int replaceStart(); + int replaceEnd(); + String getPartialText(); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java new file mode 100644 index 00000000..417fac5e --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java @@ -0,0 +1,6 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +public interface SelectionStrategy { + int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex); + int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java new file mode 100644 index 00000000..e4e538f0 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java @@ -0,0 +1,8 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +import org.jetbrains.annotations.Nullable; + +public interface SyntaxContextResolver { + @Nullable + TextSyntaxContext resolve(String text, int cursorIndex); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java new file mode 100644 index 00000000..bff43cc7 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java @@ -0,0 +1,9 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +public enum SyntaxElementType { + WORD, + TAG_NAME, + ATTRIBUTE_NAME, + ATTRIBUTE_VALUE, + OTHER +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java new file mode 100644 index 00000000..ceca85d2 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java @@ -0,0 +1,26 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +import org.jetbrains.annotations.Nullable; + +public final class TextSyntaxContext { + private final SyntaxElementType elementType; + private final int elementStart; + private final int elementEnd; + @Nullable + private final AutocompleteContext autocomplete; + + public TextSyntaxContext(SyntaxElementType elementType, int elementStart, int elementEnd, + @Nullable AutocompleteContext autocomplete) { + this.elementType = elementType; + this.elementStart = elementStart; + this.elementEnd = elementEnd; + this.autocomplete = autocomplete; + } + + public SyntaxElementType getElementType() { return elementType; } + public int getElementStart() { return elementStart; } + public int getElementEnd() { return elementEnd; } + @Nullable + public AutocompleteContext getAutocomplete() { return autocomplete; } + public boolean shouldAutocomplete() { return autocomplete != null; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java new file mode 100644 index 00000000..10685c78 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java @@ -0,0 +1,72 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import java.util.EnumMap; +import java.util.Map; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SelectionStrategy; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; + +public final class SelectionStrategies { + + private SelectionStrategies() {} + + public static Map defaults() { + EnumMap map = new EnumMap<>(SyntaxElementType.class); + map.put(SyntaxElementType.WORD, new WordSelection()); + map.put(SyntaxElementType.TAG_NAME, new ElementBoundarySelection()); + map.put(SyntaxElementType.ATTRIBUTE_NAME, new ElementBoundarySelection()); + map.put(SyntaxElementType.ATTRIBUTE_VALUE, new ElementBoundarySelection()); + map.put(SyntaxElementType.OTHER, new NoOpSelection()); + return map; + } + + public static final class WordSelection implements SelectionStrategy { + @Override + public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex) { + int pos = cursorIndex; + while (pos > 0 && isWordChar(text.charAt(pos - 1))) { + pos--; + } + return pos; + } + + @Override + public int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex) { + int pos = cursorIndex; + int len = text.length(); + while (pos < len && isWordChar(text.charAt(pos))) { + pos++; + } + return pos; + } + + private static boolean isWordChar(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; + } + } + + public static final class ElementBoundarySelection implements SelectionStrategy { + @Override + public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex) { + return ctx.getElementStart(); + } + + @Override + public int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex) { + return ctx.getElementEnd(); + } + } + + public static final class NoOpSelection implements SelectionStrategy { + @Override + public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex) { + return cursorIndex; + } + + @Override + public int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex) { + return cursorIndex; + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java new file mode 100644 index 00000000..925bc2e8 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java @@ -0,0 +1,38 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; + +public final class WordBoundaryResolver implements SyntaxContextResolver { + + @Override + @Nullable + public TextSyntaxContext resolve(String text, int cursorIndex) { + if (text == null || text.isEmpty() || cursorIndex < 0 || cursorIndex > text.length()) { + return null; + } + + int start = cursorIndex; + while (start > 0 && isWordChar(text.charAt(start - 1))) { + start--; + } + + int end = cursorIndex; + while (end < text.length() && isWordChar(text.charAt(end))) { + end++; + } + + if (start == end && start == cursorIndex) { + return new TextSyntaxContext(SyntaxElementType.OTHER, cursorIndex, cursorIndex, null); + } + + return new TextSyntaxContext(SyntaxElementType.WORD, start, end, null); + } + + private static boolean isWordChar(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; + } +} From a9b4c76ab8b39fba6ed3c41dd37e3603bee5295b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 13 May 2026 21:18:45 +0800 Subject: [PATCH 002/136] feat(autocomplete): add AutocompletePopup widget Implements the dropdown popup UI for autocomplete suggestions with scrollable candidate list, selection highlighting, scrollbar, and viewport clamping. --- .../provider/AutocompleteCandidate.java | 10 + .../autocomplete/ui/AutocompletePopup.java | 235 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java new file mode 100644 index 00000000..dd999af1 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java @@ -0,0 +1,10 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import net.minecraft.client.gui.FontRenderer; + +public interface AutocompleteCandidate { + String displayText(); + String replacementText(); + default int renderHeight() { return 14; } + void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java new file mode 100644 index 00000000..3f5add91 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java @@ -0,0 +1,235 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui; + +import java.util.Collections; +import java.util.List; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; + +import com.hfstudio.guidenh.guide.document.LytRect; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; +import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorPopupLayout; +import com.hfstudio.guidenh.guide.internal.util.DisplayScale; + +public final class AutocompletePopup { + + public static final int MAX_VISIBLE_ITEMS = 5; + public static final int PADDING_X = 6; + public static final int PADDING_Y = 4; + public static final int SCROLLBAR_W = 5; + public static final int BACKGROUND_COLOR = 0xF0181C22; + public static final int BORDER_COLOR = 0xFF4D5661; + public static final int HOVER_COLOR = 0xCC2A3A46; + public static final int SCROLLBAR_TRACK_COLOR = 0x35101010; + public static final int SCROLLBAR_THUMB_COLOR = 0xA0D8D8D8; + + private boolean open; + private List candidates = Collections.emptyList(); + private int selectedIndex; + private int scrollY; + private int x, y, width, height; + private int viewportWidth, viewportHeight; + + public boolean isOpen() { return open; } + + public void show(List candidates, int anchorX, int anchorY, + int viewportWidth, int viewportHeight, FontRenderer fontRenderer) { + this.candidates = candidates != null ? candidates : Collections.emptyList(); + this.selectedIndex = this.candidates.isEmpty() ? -1 : 0; + this.scrollY = 0; + this.viewportWidth = viewportWidth; + this.viewportHeight = viewportHeight; + + if (this.candidates.isEmpty()) { + open = false; + return; + } + + computeSize(fontRenderer); + LytRect rect = SceneEditorPopupLayout.clampToViewport( + anchorX, anchorY, width, height, viewportWidth, viewportHeight, 2); + this.x = rect.x(); + this.y = rect.y(); + this.open = true; + } + + public void close() { + open = false; + candidates = Collections.emptyList(); + selectedIndex = -1; + } + + public void moveSelection(int delta) { + if (!open || candidates.isEmpty()) return; + int next = selectedIndex + delta; + if (next < 0) next = 0; + if (next >= candidates.size()) next = candidates.size() - 1; + selectedIndex = next; + ensureSelectionVisible(); + } + + @Nullable + public AutocompleteCandidate getSelected() { + if (!open || selectedIndex < 0 || selectedIndex >= candidates.size()) return null; + return candidates.get(selectedIndex); + } + + public void scrollWheel(int delta) { + if (!open) return; + int maxH = computeMaxItemHeight(); + scrollY = clampScroll(scrollY - delta * maxH * 2); + } + + public boolean mouseClicked(int mouseX, int mouseY) { + if (!open || !contains(mouseX, mouseY)) { + return false; + } + if (isInsideScrollbar(mouseX, mouseY)) { + int contentHeight = computeContentHeight(); + if (contentHeight > height) { + int trackH = Math.max(1, height - 2); + int maxScroll = Math.max(0, contentHeight - height); + scrollY = (mouseY - y) * maxScroll / trackH; + scrollY = clampScroll(scrollY); + } + return true; + } + int index = findItemIndex(mouseX, mouseY); + if (index >= 0 && index < candidates.size()) { + selectedIndex = index; + } + return true; + } + + public boolean contains(int mouseX, int mouseY) { + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + } + + public void draw(FontRenderer fontRenderer, int mouseX, int mouseY) { + if (!open) return; + + Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, BORDER_COLOR); + Gui.drawRect(x, y, x + width, y + height, BACKGROUND_COLOR); + + pushScissor(x + 1, y + 1, width - 2, height - 2); + + int drawY = y + PADDING_Y - scrollY; + int itemIndex = 0; + int itemHeight = computeMaxItemHeight(); + for (AutocompleteCandidate candidate : candidates) { + if (drawY + itemHeight >= y + 1 && drawY <= y + height - 1) { + if (itemIndex == selectedIndex) { + Gui.drawRect(x + 1, drawY - 1, x + width - SCROLLBAR_W - 1, drawY + itemHeight - 1, HOVER_COLOR); + } + candidate.render(fontRenderer, x + PADDING_X, drawY, + width - PADDING_X * 2 - (hasScrollbar() ? SCROLLBAR_W : 0), + itemIndex == selectedIndex); + } + itemIndex++; + drawY += itemHeight; + } + + popScissor(); + drawScrollbar(); + } + + private void computeSize(FontRenderer fontRenderer) { + int maxW = 72; + int maxItemH = 14; + for (AutocompleteCandidate c : candidates) { + int w = fontRenderer.getStringWidth(c.displayText()) + PADDING_X * 2 + 16; + if (w > maxW) maxW = w; + if (c.renderHeight() > maxItemH) maxItemH = c.renderHeight(); + } + this.width = Math.max(72, maxW + SCROLLBAR_W); + int visibleItems = Math.min(candidates.size(), MAX_VISIBLE_ITEMS); + this.height = visibleItems * maxItemH + PADDING_Y * 2; + } + + private int computeMaxItemHeight() { + int max = 14; + for (AutocompleteCandidate c : candidates) { + if (c.renderHeight() > max) max = c.renderHeight(); + } + return max; + } + + private int computeContentHeight() { + int h = 0; + int maxH = computeMaxItemHeight(); + for (int i = 0; i < candidates.size(); i++) { + h += maxH; + } + return h + PADDING_Y * 2; + } + + private boolean hasScrollbar() { + return candidates.size() > MAX_VISIBLE_ITEMS; + } + + private void ensureSelectionVisible() { + int itemH = computeMaxItemHeight(); + int selTop = selectedIndex * itemH; + int selBottom = selTop + itemH; + int viewH = height - PADDING_Y * 2; + if (selTop < scrollY) { + scrollY = selTop; + } else if (selBottom > scrollY + viewH) { + scrollY = selBottom - viewH; + } + scrollY = clampScroll(scrollY); + } + + private int findItemIndex(int mouseX, int mouseY) { + if (!contains(mouseX, mouseY)) return -1; + int itemH = computeMaxItemHeight(); + int localY = mouseY - y - PADDING_Y + scrollY; + int index = localY / itemH; + if (index < 0 || index >= candidates.size()) return -1; + return index; + } + + private void drawScrollbar() { + int contentH = computeContentHeight(); + if (contentH <= height) return; + int barX = x + width - SCROLLBAR_W; + Gui.drawRect(barX, y + 1, x + width - 1, y + height - 1, SCROLLBAR_TRACK_COLOR); + int thumbH = Math.max(16, height * height / Math.max(1, contentH)); + int maxScroll = Math.max(0, contentH - height); + int thumbY = y + (maxScroll > 0 ? (height - thumbH) * scrollY / maxScroll : 0); + Gui.drawRect(barX, thumbY, x + width - 1, thumbY + thumbH, SCROLLBAR_THUMB_COLOR); + } + + private boolean isInsideScrollbar(int mouseX, int mouseY) { + if (!hasScrollbar()) return false; + return mouseX >= x + width - SCROLLBAR_W && mouseX < x + width + && mouseY >= y && mouseY < y + height; + } + + private int clampScroll(int value) { + int maxScroll = Math.max(0, computeContentHeight() - height); + if (value < 0) return 0; + return Math.min(value, maxScroll); + } + + private static void pushScissor(int x, int y, int width, int height) { + Minecraft mc = Minecraft.getMinecraft(); + int scale = DisplayScale.scaleFactor(); + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GL11.glScissor( + x * scale, + mc.displayHeight - (y + height) * scale, + Math.max(0, width * scale), + Math.max(0, height * scale)); + } + + private static void popScissor() { + GL11.glDisable(GL11.GL_SCISSOR_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glColor4f(1f, 1f, 1f, 1f); + } +} From b410246e3d85f4a8ebf2eb1225520fe3c394f919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 13 May 2026 21:20:46 +0800 Subject: [PATCH 003/136] feat(autocomplete): add MdxSyntaxResolver, provider interfaces, and ItemIdProvider --- .../provider/AutocompleteKey.java | 32 ++++ .../provider/AutocompleteProvider.java | 11 ++ .../provider/AutocompleteProviders.java | 41 ++++ .../autocomplete/provider/ItemCandidate.java | 46 +++++ .../autocomplete/provider/ItemIdProvider.java | 42 ++++ .../autocomplete/provider/TextCandidate.java | 20 ++ .../resolver/MdxAutocompleteContext.java | 27 +++ .../resolver/MdxSyntaxResolver.java | 180 ++++++++++++++++++ 8 files changed, 399 insertions(+) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAutocompleteContext.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java new file mode 100644 index 00000000..dd49dcbe --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java @@ -0,0 +1,32 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.Objects; + +public final class AutocompleteKey { + private final String tagName; + private final String attributeName; + + public AutocompleteKey(String tagName, String attributeName) { + this.tagName = Objects.requireNonNull(tagName); + this.attributeName = Objects.requireNonNull(attributeName); + } + + public String getTagName() { return tagName; } + public String getAttributeName() { return attributeName; } + + public boolean matches(String tag, String attr) { + if (!tagName.equals("*") && !tagName.equals(tag)) return false; + if (!attributeName.equals("*") && !attributeName.equals(attr)) return false; + return true; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AutocompleteKey)) return false; + AutocompleteKey that = (AutocompleteKey) o; + return tagName.equals(that.tagName) && attributeName.equals(that.attributeName); + } + + @Override + public int hashCode() { return Objects.hash(tagName, attributeName); } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java new file mode 100644 index 00000000..8633c549 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java @@ -0,0 +1,11 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.List; +import java.util.Set; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +public interface AutocompleteProvider { + Set getSupportedKeys(); + List provide(AutocompleteContext ctx, int limit); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java new file mode 100644 index 00000000..2c02f358 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -0,0 +1,41 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext; + +public final class AutocompleteProviders { + + private static final List providers = new CopyOnWriteArrayList<>(); + + private AutocompleteProviders() {} + + public static void register(AutocompleteProvider provider) { + providers.add(provider); + } + + public static List query(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof MdxAutocompleteContext)) return Collections.emptyList(); + MdxAutocompleteContext mdx = (MdxAutocompleteContext) ctx; + + List results = new ArrayList<>(); + for (AutocompleteProvider provider : providers) { + for (AutocompleteKey key : provider.getSupportedKeys()) { + if (key.matches(mdx.getTagName(), mdx.getAttributeName())) { + results.addAll(provider.provide(ctx, Math.max(0, limit - results.size()))); + break; + } + } + if (results.size() >= limit) break; + } + return results; + } + + public static void clear() { + providers.clear(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java new file mode 100644 index 00000000..a90dcf5c --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java @@ -0,0 +1,46 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.item.ItemStack; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +public class ItemCandidate implements AutocompleteCandidate { + private final String id; + private final ItemStack stack; + private static final int ICON_SIZE = 12; + private static final int TEXT_X = ICON_SIZE + 4; + private static final int TEXT_COLOR = 0xFFF0F0F0; + private static final RenderItem renderItem = new RenderItem(); + + public ItemCandidate(String id, ItemStack stack) { + this.id = id; + this.stack = stack; + } + + @Override public String displayText() { return id; } + @Override public String replacementText() { return id; } + @Override public int renderHeight() { return 16; } + + @Override + public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { + GL11.glPushMatrix(); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + renderItem.zLevel = 0; + renderItem.renderItemAndEffectIntoGUI( + Minecraft.getMinecraft().fontRenderer, + Minecraft.getMinecraft().getTextureManager(), + stack, x + 2, y + 2); + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + fontRenderer.drawString(id, x + TEXT_X, y + 3, TEXT_COLOR); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java new file mode 100644 index 00000000..773ca983 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java @@ -0,0 +1,42 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +public class ItemIdProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(new AutocompleteKey("*", "id")); + + @Override + public Set getSupportedKeys() { + return KEYS; + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText(); + String lower = partial != null ? partial.toLowerCase() : ""; + + List results = new ArrayList<>(); + for (Object obj : Item.itemRegistry.getKeys()) { + if (results.size() >= limit) break; + if (obj instanceof String key) { + if (lower.isEmpty() || key.toLowerCase().contains(lower)) { + Item item = (Item) Item.itemRegistry.getObject(key); + if (item != null) { + results.add(new ItemCandidate(key, new ItemStack(item))); + } + } + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java new file mode 100644 index 00000000..fda73990 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java @@ -0,0 +1,20 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import net.minecraft.client.gui.FontRenderer; + +public class TextCandidate implements AutocompleteCandidate { + private final String text; + private static final int TEXT_COLOR = 0xFFF0F0F0; + + public TextCandidate(String text) { + this.text = text; + } + + @Override public String displayText() { return text; } + @Override public String replacementText() { return text; } + + @Override + public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { + fontRenderer.drawString(text, x, y + 2, TEXT_COLOR); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAutocompleteContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAutocompleteContext.java new file mode 100644 index 00000000..9c3220b1 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAutocompleteContext.java @@ -0,0 +1,27 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +public final class MdxAutocompleteContext implements AutocompleteContext { + private final String tagName; + private final String attributeName; + private final int replaceStart; + private final int replaceEnd; + private final String partialText; + + public MdxAutocompleteContext(String tagName, String attributeName, int replaceStart, int replaceEnd, + String partialText) { + this.tagName = tagName; + this.attributeName = attributeName; + this.replaceStart = replaceStart; + this.replaceEnd = replaceEnd; + this.partialText = partialText; + } + + public String getTagName() { return tagName; } + public String getAttributeName() { return attributeName; } + + @Override public int replaceStart() { return replaceStart; } + @Override public int replaceEnd() { return replaceEnd; } + @Override public String getPartialText() { return partialText; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java new file mode 100644 index 00000000..b4e9f4fd --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -0,0 +1,180 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; +import com.hfstudio.guidenh.libs.mdast.MdAst; +import com.hfstudio.guidenh.libs.mdast.MdastOptions; +import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.micromark.ParseException; +import com.hfstudio.guidenh.libs.unist.UnistNode; +import com.hfstudio.guidenh.libs.unist.UnistPosition; + +public class MdxSyntaxResolver implements SyntaxContextResolver { + + private static final MdastOptions PARSE_OPTIONS = GuideMarkdownOptions.runtime(); + + // Matches open MDX tag: ]*?\\s+(\\w+)\\s*=\\s*\"([^\">]*)$"); + + @Override + @Nullable + public TextSyntaxContext resolve(String text, int cursorIndex) { + if (text == null || text.isEmpty()) return null; + + try { + MdAstRoot root = MdAst.fromMarkdown(text, PARSE_OPTIONS); + return resolveFromAst(root, text, cursorIndex); + } catch (ParseException e) { + // TODO: Remove after rewriting micromark parser with error recovery + return resolveFromFallback(text, cursorIndex); + } + } + + @Nullable + private TextSyntaxContext resolveFromAst(MdAstRoot root, String text, int cursorIndex) { + MdxJsxElementFields element = findEnclosingMdxElement(root, cursorIndex); + if (element != null) { + return resolveMdxAttribute(element, text, cursorIndex); + } + return resolvePlainTextWord(text, cursorIndex); + } + + @Nullable + private MdxJsxElementFields findEnclosingMdxElement(UnistNode node, int cursorIndex) { + UnistPosition pos = node.position(); + if (pos != null && pos.start() != null && pos.end() != null) { + if (cursorIndex < pos.start().offset() || cursorIndex > pos.end().offset()) { + return null; + } + } + + if (node instanceof MdxJsxElementFields) { + return (MdxJsxElementFields) node; + } + + if (node instanceof MdAstParent) { + for (UnistNode child : ((MdAstParent) node).children()) { + MdxJsxElementFields found = findEnclosingMdxElement(child, cursorIndex); + if (found != null) return found; + } + } + + return null; + } + + @Nullable + private TextSyntaxContext resolveMdxAttribute(MdxJsxElementFields element, String text, int cursorIndex) { + String tagName = element.name(); + if (tagName == null) return resolvePlainTextWord(text, cursorIndex); + + for (var attrNode : element.attributes()) { + if (!(attrNode instanceof MdxJsxAttribute)) continue; + MdxJsxAttribute attr = (MdxJsxAttribute) attrNode; + if (attr.name == null || attr.name.isEmpty()) continue; + + UnistPosition pos = attrNode.position(); + if (pos == null || pos.start() == null || pos.end() == null) continue; + + int attrStart = pos.start().offset(); + int attrEnd = pos.end().offset(); + + if (cursorIndex < attrStart || cursorIndex > attrEnd) continue; + + // Find the value range within the attribute text + String attrText = text.substring(attrStart, attrEnd); + int eqIdx = attrText.indexOf('='); + if (eqIdx < 0) break; // boolean attribute, no value to complete + + int valRelStart = eqIdx + 1; + while (valRelStart < attrText.length() && attrText.charAt(valRelStart) == ' ') { + valRelStart++; + } + if (valRelStart >= attrText.length()) break; + + char openChar = attrText.charAt(valRelStart); + char closeChar = (openChar == '{') ? '}' : openChar; // " ' or { + if (openChar == '"' || openChar == '\'' || openChar == '{') { + valRelStart++; // skip opening quote/brace + } else { + valRelStart = eqIdx + 1; // unquoted value (shouldn't happen in MDX) + closeChar = ' '; + } + + int valRelEnd = attrText.length(); + if (closeChar != ' ' && valRelEnd > 0 && attrText.charAt(valRelEnd - 1) == closeChar) { + valRelEnd--; // exclude closing quote/brace + } + + int valueStart = attrStart + valRelStart; + int valueEnd = attrStart + valRelEnd; + + String partialText = text.substring( + Math.max(valueStart, 0), + Math.min(Math.max(cursorIndex, valueStart), text.length())); + + return new TextSyntaxContext( + SyntaxElementType.ATTRIBUTE_VALUE, + valueStart, + valueEnd, + new MdxAutocompleteContext(tagName, attr.name, valueStart, valueEnd, partialText)); + } + + return resolvePlainTextWord(text, cursorIndex); + } + + private TextSyntaxContext resolvePlainTextWord(String text, int cursorIndex) { + int start = cursorIndex; + while (start > 0 && isWordChar(text.charAt(start - 1))) start--; + int end = cursorIndex; + while (end < text.length() && isWordChar(text.charAt(end))) end++; + return new TextSyntaxContext(SyntaxElementType.WORD, start, end, null); + } + + @Nullable + private TextSyntaxContext resolveFromFallback(String text, int cursorIndex) { + String prefix = text.substring(0, Math.min(cursorIndex, text.length())); + Matcher m = FALLBACK_TAG.matcher(prefix); + + String tagName = null; + String attrName = null; + int valueStart = -1; + + int searchFrom = 0; + while (m.find(searchFrom)) { + int vs = m.start(3); + if (vs <= cursorIndex) { + tagName = m.group(1); + attrName = m.group(2); + valueStart = vs; + } + searchFrom = m.start() + 1; + } + + if (tagName != null && attrName != null && valueStart >= 0) { + String partialText = text.substring(valueStart, cursorIndex); + return new TextSyntaxContext( + SyntaxElementType.ATTRIBUTE_VALUE, + valueStart, + cursorIndex, + new MdxAutocompleteContext(tagName, attrName, valueStart, cursorIndex, partialText)); + } + + return resolvePlainTextWord(text, cursorIndex); + } + + private static boolean isWordChar(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; + } +} From 709b9d803132cd17e551739375a8f249f21bda16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 13 May 2026 21:30:46 +0800 Subject: [PATCH 004/136] feat(autocomplete): integrate popup and double-click selection into GuideScreen --- .../com/hfstudio/guidenh/ClientProxy.java | 3 + .../guidenh/guide/internal/GuideScreen.java | 144 ++++++++++++++++++ .../gui/SceneEditorMultilineTextArea.java | 27 ++++ 3 files changed, 174 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index d0492331..6ec98398 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -23,6 +23,8 @@ import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; import com.hfstudio.guidenh.network.GuideNhNetwork; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ItemIdProvider; import com.hfstudio.structurelibexport.StructureExportBootstrap; import cpw.mods.fml.common.event.FMLInitializationEvent; @@ -56,6 +58,7 @@ public void init(FMLInitializationEvent event) { GuideNhClientBridgeController.init(); OpenGuideHotkey.init(); OpenSceneEditorHotkey.init(); + AutocompleteProviders.register(new ItemIdProvider()); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 77339da5..8be46bf3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -34,6 +34,16 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SelectionStrategy; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxSyntaxResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui.AutocompletePopup; + import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.config.ModConfig; @@ -237,6 +247,14 @@ public class GuideScreen extends GuiScreen implements GuideUiHost, GuiYesNoCallb private long guideEditorNextSaveAtMillis; private long guideEditorNextPreviewCompileAtMillis; private long guideEditorNextExternalCheckAtMillis; + @Nullable + private AutocompletePopup autocompletePopup; + private SyntaxContextResolver autocompleteResolver; + private long autocompleteNextQueryAtMillis; + private String autocompleteLastText; + private int autocompleteLastCursor; + private static final long AUTOCOMPLETE_DEBOUNCE_MS = 100; + private java.util.Map autocompleteSelectionStrategies; private boolean guideEditorDraggingDivider; private int guideEditorDividerGrabOffset; private boolean guideEditorDraggingPreviewScrollbar; @@ -429,6 +447,10 @@ public void initGui() { } rebuildToolbar(); ensureGuideEditorTextArea(); + if (autocompleteResolver == null) { + autocompleteResolver = new MdxSyntaxResolver(); + autocompleteSelectionStrategies = SelectionStrategies.defaults(); + } refreshGuideEditorDraft(true); ensureLayout(); scrollToCurrentAnchor(); @@ -459,6 +481,9 @@ public void updateScreen() { } pendingItemLinksStack = null; } + if (isGuideEditorActive()) { + performAutocompleteCheck(); + } } private void updateGuideEditorHotkeyFocusSuppression() { @@ -706,6 +731,23 @@ private String buildGuideEditorNewPageText(String titleText, ResourceLocation pa private void ensureGuideEditorTextArea() { if (guideEditorTextArea == null) { guideEditorTextArea = new SceneEditorMultilineTextArea(fontRendererObj); + guideEditorTextArea.setDoubleClickHandler(new SceneEditorMultilineTextArea.DoubleClickHandler() { + @Override + public void onDoubleClick(int cursorIndex) { + if (autocompleteResolver == null) return; + String text = guideEditorTextArea.getText(); + TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursorIndex); + if (ctx == null) return; + SelectionStrategy strategy = (SelectionStrategy) + ((java.util.Map) autocompleteSelectionStrategies).get(ctx.getElementType()); + if (strategy == null) return; + int start = strategy.getSelectionStart(ctx, text, cursorIndex); + int end = strategy.getSelectionEnd(ctx, text, cursorIndex); + if (start != end) { + guideEditorTextArea.applyEdit(text, start, end); + } + } + }); guideEditorTextArea.setWrapEnabled(false); } } @@ -817,6 +859,7 @@ private void runGuideEditorTextMutation(Runnable mutation) { } pushGuideEditorHistoryState(before, beforeSelectionStart, beforeSelectionEnd); markGuideEditorTextChanged(); + scheduleAutocompleteCheck(); pushGuideEditorCurrentHistoryState(); } @@ -1971,6 +2014,9 @@ private void drawGuideEditorScreen(int mouseX, int mouseY) { renderGuideEditorPreview(previewPaneX, editorTop, previewPaneWidth, editorHeight); } + if (autocompletePopup != null && autocompletePopup.isOpen()) { + autocompletePopup.draw(fontRendererObj, mouseX, mouseY); + } } private int resolveGuideEditorDividerColor() { @@ -2157,6 +2203,27 @@ private boolean handleGuideEditorKey(char typedChar, int keyCode) { } guideEditorSuppressTextFocusUntilGuideHotkeyRelease = false; } + if (autocompletePopup != null && autocompletePopup.isOpen()) { + switch (keyCode) { + case Keyboard.KEY_ESCAPE: + autocompletePopup.close(); + return true; + case Keyboard.KEY_UP: + autocompletePopup.moveSelection(-1); + return true; + case Keyboard.KEY_DOWN: + autocompletePopup.moveSelection(1); + return true; + case Keyboard.KEY_RETURN: + case Keyboard.KEY_NUMPADENTER: + case Keyboard.KEY_TAB: + commitAutocompleteSelection(); + return true; + default: + autocompletePopup.close(); + return false; + } + } if (guideEditorContextMenu != null && guideEditorContextMenu.isOpen()) { if (keyCode == Keyboard.KEY_ESCAPE) { closeGuideEditorContextMenu(); @@ -2205,10 +2272,87 @@ public void run() { return false; } + private void scheduleAutocompleteCheck() { + if (autocompleteResolver == null || guideEditorTextArea == null) return; + autocompleteNextQueryAtMillis = System.currentTimeMillis() + AUTOCOMPLETE_DEBOUNCE_MS; + autocompleteLastText = guideEditorTextArea.getText(); + autocompleteLastCursor = guideEditorTextArea.getCursorIndex(); + } + + private void performAutocompleteCheck() { + if (autocompleteResolver == null || guideEditorTextArea == null) return; + long now = System.currentTimeMillis(); + if (now < autocompleteNextQueryAtMillis) return; + if (autocompleteLastText == null) return; + autocompleteNextQueryAtMillis = 0; + + String text = guideEditorTextArea.getText(); + int cursor = guideEditorTextArea.getCursorIndex(); + if (!text.equals(autocompleteLastText) || cursor != autocompleteLastCursor) return; + + TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursor); + if (ctx != null && ctx.shouldAutocomplete()) { + java.util.List candidates = + AutocompleteProviders.query(ctx.getAutocomplete(), 20); + if (!candidates.isEmpty()) { + if (autocompletePopup == null) { + autocompletePopup = new AutocompletePopup(); + } + int areaX = contentX; + int areaY = getGuideEditorContentTop(); + autocompletePopup.show(candidates, areaX + 40, areaY + fontRendererObj.FONT_HEIGHT + 6, + width, height, fontRendererObj); + return; + } + } + if (autocompletePopup != null && autocompletePopup.isOpen()) { + autocompletePopup.close(); + } + } + + private void commitAutocompleteSelection() { + if (autocompletePopup == null || !autocompletePopup.isOpen() || guideEditorTextArea == null) return; + AutocompleteCandidate selected = autocompletePopup.getSelected(); + if (selected == null) { + autocompletePopup.close(); + return; + } + if (autocompleteResolver == null) { + autocompletePopup.close(); + return; + } + TextSyntaxContext ctx = autocompleteResolver.resolve( + guideEditorTextArea.getText(), guideEditorTextArea.getCursorIndex()); + if (ctx != null && ctx.shouldAutocomplete()) { + AutocompleteContext ac = ctx.getAutocomplete(); + if (ac != null) { + String text = guideEditorTextArea.getText(); + String before = text.substring(0, ac.replaceStart()); + String after = text.substring(ac.replaceEnd()); + String newText = before + selected.replacementText() + after; + int newCursor = ac.replaceStart() + selected.replacementText().length(); + guideEditorTextArea.applyEdit(newText, newCursor, newCursor); + updateGuideEditorTextFromArea(); + } + } + autocompletePopup.close(); + } + private boolean handleGuideEditorMouseClicked(int mouseX, int mouseY, int button) { if (!isGuideEditorActive()) { return false; } + if (autocompletePopup != null && autocompletePopup.isOpen()) { + if (autocompletePopup.contains(mouseX, mouseY)) { + autocompletePopup.mouseClicked(mouseX, mouseY); + if (autocompletePopup.getSelected() != null) { + commitAutocompleteSelection(); + } + return true; + } else if (button == 0) { + autocompletePopup.close(); + } + } if (guideEditorContextMenu != null && guideEditorContextMenu.isOpen()) { return guideEditorContextMenu .mouseClicked(mouseX, mouseY, button, new GuideScreenEditorContextMenu.Listener() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index 0a692954..215fb022 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -5,6 +5,8 @@ import java.awt.datatransfer.StringSelection; import java.util.List; +import org.jetbrains.annotations.Nullable; + import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiScreen; @@ -61,6 +63,12 @@ public class SceneEditorMultilineTextArea { private int recentPhysicalAsciiChar; private long recentPhysicalAsciiAtMillis; + // Double-click word selection + private long lastClickTimeMillis; + private static final long DOUBLE_CLICK_WINDOW_MS = 400; + @Nullable + private DoubleClickHandler doubleClickHandler; + public SceneEditorMultilineTextArea(FontRenderer fontRenderer) { this(fontRenderer, new ClipboardAccess() { @@ -347,6 +355,13 @@ public boolean mouseClicked(int mouseX, int mouseY, int button) { int cursorIndex = getCursorIndexAt(mouseX, mouseY); if (button == 0) { selectionModel.beginSelection(cursorIndex); + long now = System.currentTimeMillis(); + long elapsed = now - lastClickTimeMillis; + lastClickTimeMillis = now; + if (elapsed < DOUBLE_CLICK_WINDOW_MS && doubleClickHandler != null) { + doubleClickHandler.onDoubleClick(selectionModel.getCursorIndex()); + return true; + } selectingWithMouse = true; } else { selectionModel.setCursorIndex(cursorIndex); @@ -847,6 +862,10 @@ private void moveCursorToLineBoundary(boolean start, boolean keepSelection) { ensureCursorVisible(); } + public int getCursorIndexAtPublic(int mouseX, int mouseY) { + return getCursorIndexAt(mouseX, mouseY); + } + private int getCursorIndexAt(int mouseX, int mouseY) { List lines = layoutCache.getVisualLines(); if (lines.isEmpty()) { @@ -984,6 +1003,14 @@ public static boolean isCtrlKeyCombo(int keyCode, int expectedKeyCode) { && (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL)); } + public void setDoubleClickHandler(@Nullable DoubleClickHandler handler) { + this.doubleClickHandler = handler; + } + + public interface DoubleClickHandler { + void onDoubleClick(int cursorIndex); + } + public interface ClipboardAccess { void copy(String text); From c8ce621a20437d4fd350899b7626672ff03f6882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 00:19:49 +0800 Subject: [PATCH 005/136] fix(autocomplete): dirty hotfixes for popup trigger, position, scroll, and value range - Fix keyboard typing not triggering autocomplete (schedule polluted state) - Fix popup position anchoring to cursor pixel instead of hardcoded offset - Fix popup flip above cursor when insufficient room below - Fix Backspace consumed by navigation instead of forwarded to textarea - Fix modal scroll wheel (popup only when mouse inside) - Fix ItemCandidate icon offset - Fix fallback regex replaceEnd to scan forward for closing quote - Add cursor pixel position API to SceneEditorMultilineTextArea - Add throttled diagnostic logging --- .../guidenh/guide/internal/GuideScreen.java | 77 ++++++++++++++++--- .../autocomplete/provider/ItemCandidate.java | 9 ++- .../resolver/MdxSyntaxResolver.java | 29 +++++-- .../autocomplete/ui/AutocompletePopup.java | 32 +++++++- .../gui/SceneEditorMultilineTextArea.java | 17 ++++ 5 files changed, 140 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 8be46bf3..71fdf644 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -254,6 +254,9 @@ public class GuideScreen extends GuiScreen implements GuideUiHost, GuiYesNoCallb private String autocompleteLastText; private int autocompleteLastCursor; private static final long AUTOCOMPLETE_DEBOUNCE_MS = 100; + private static final long AUTOCOMPLETE_LOG_THROTTLE_MS = 3000; + private long autocompleteLastLogTime; + private String autocompleteLastLogKey; private java.util.Map autocompleteSelectionStrategies; private boolean guideEditorDraggingDivider; private int guideEditorDividerGrabOffset; @@ -2015,6 +2018,10 @@ private void drawGuideEditorScreen(int mouseX, int mouseY) { } if (autocompletePopup != null && autocompletePopup.isOpen()) { + int anchorX = contentX + guideEditorTextArea.getCursorPixelX(); + int anchorY = getGuideEditorContentTop() + guideEditorTextArea.getCursorPixelY() + + fontRendererObj.FONT_HEIGHT + 12; + autocompletePopup.reposition(anchorX, anchorY, width, height, fontRendererObj); autocompletePopup.draw(fontRendererObj, mouseX, mouseY); } } @@ -2204,15 +2211,18 @@ private boolean handleGuideEditorKey(char typedChar, int keyCode) { guideEditorSuppressTextFocusUntilGuideHotkeyRelease = false; } if (autocompletePopup != null && autocompletePopup.isOpen()) { + System.out.println("[AC-271afa] KEY popup: keyCode=" + keyCode + " char=" + (int) typedChar); switch (keyCode) { case Keyboard.KEY_ESCAPE: autocompletePopup.close(); return true; case Keyboard.KEY_UP: autocompletePopup.moveSelection(-1); + System.out.println("[AC-271afa] KEY up: selectedIndex moved"); return true; case Keyboard.KEY_DOWN: autocompletePopup.moveSelection(1); + System.out.println("[AC-271afa] KEY down: selectedIndex moved"); return true; case Keyboard.KEY_RETURN: case Keyboard.KEY_NUMPADENTER: @@ -2220,8 +2230,15 @@ private boolean handleGuideEditorKey(char typedChar, int keyCode) { commitAutocompleteSelection(); return true; default: + // Close popup and forward key to text area so it isn't + // consumed by GuideScreen.keyTyped navigation handlers. autocompletePopup.close(); - return false; + if (guideEditorTextArea != null) { + guideEditorTextArea.keyTyped(typedChar, keyCode); + updateGuideEditorTextFromArea(); + scheduleAutocompleteCheck(); + } + return true; } } if (guideEditorContextMenu != null && guideEditorContextMenu.isOpen()) { @@ -2275,22 +2292,48 @@ public void run() { private void scheduleAutocompleteCheck() { if (autocompleteResolver == null || guideEditorTextArea == null) return; autocompleteNextQueryAtMillis = System.currentTimeMillis() + AUTOCOMPLETE_DEBOUNCE_MS; - autocompleteLastText = guideEditorTextArea.getText(); - autocompleteLastCursor = guideEditorTextArea.getCursorIndex(); } private void performAutocompleteCheck() { if (autocompleteResolver == null || guideEditorTextArea == null) return; - long now = System.currentTimeMillis(); - if (now < autocompleteNextQueryAtMillis) return; - if (autocompleteLastText == null) return; - autocompleteNextQueryAtMillis = 0; String text = guideEditorTextArea.getText(); int cursor = guideEditorTextArea.getCursorIndex(); - if (!text.equals(autocompleteLastText) || cursor != autocompleteLastCursor) return; + + boolean firstRun = autocompleteLastText == null; + boolean textChanged = firstRun || !text.equals(autocompleteLastText); + boolean cursorMoved = firstRun || cursor != autocompleteLastCursor; + + // Nothing changed — skip + if (!textChanged && !cursorMoved) return; + + // Text changed but debounce not expired — wait for typing to settle + if (textChanged && !firstRun && System.currentTimeMillis() < autocompleteNextQueryAtMillis) return; + + autocompleteLastText = text; + autocompleteLastCursor = cursor; TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursor); + + boolean shouldLog = false; + String logKey = "ctx=" + (ctx != null ? ctx.getElementType() : "null") + + " auto=" + (ctx != null ? ctx.shouldAutocomplete() : false) + + " popup=" + (autocompletePopup != null && autocompletePopup.isOpen()) + + " cursor=" + cursor; + if (!logKey.equals(autocompleteLastLogKey) + || System.currentTimeMillis() - autocompleteLastLogTime > AUTOCOMPLETE_LOG_THROTTLE_MS) { + shouldLog = true; + autocompleteLastLogTime = System.currentTimeMillis(); + autocompleteLastLogKey = logKey; + } + + if (shouldLog) { + System.out.println("[AC-271afa] cursor=" + cursor + " textLen=" + text.length() + + " textChanged=" + textChanged + " cursorMoved=" + cursorMoved + + " ctx=" + (ctx != null ? ctx.getElementType() : "null") + + " auto=" + (ctx != null ? ctx.shouldAutocomplete() : false)); + } + if (ctx != null && ctx.shouldAutocomplete()) { java.util.List candidates = AutocompleteProviders.query(ctx.getAutocomplete(), 20); @@ -2298,11 +2341,16 @@ private void performAutocompleteCheck() { if (autocompletePopup == null) { autocompletePopup = new AutocompletePopup(); } - int areaX = contentX; - int areaY = getGuideEditorContentTop(); - autocompletePopup.show(candidates, areaX + 40, areaY + fontRendererObj.FONT_HEIGHT + 6, + int anchorX = contentX + guideEditorTextArea.getCursorPixelX(); + int anchorY = getGuideEditorContentTop() + guideEditorTextArea.getCursorPixelY() + + fontRendererObj.FONT_HEIGHT + 12; + autocompletePopup.show(candidates, anchorX, anchorY, width, height, fontRendererObj); + System.out.println("[AC-271afa] POPUP SHOWN: candidates=" + candidates.size() + + " pos=(" + anchorX + "," + anchorY + ")"); return; + } else { + System.out.println("[AC-271afa] query returned 0 candidates"); } } if (autocompletePopup != null && autocompletePopup.isOpen()) { @@ -2538,6 +2586,13 @@ private boolean handleGuideEditorWheel(int mouseX, int mouseY, int dwheel) { if (!isGuideEditorActive()) { return false; } + if (autocompletePopup != null && autocompletePopup.isOpen()) { + if (autocompletePopup.contains(mouseX, mouseY)) { + autocompletePopup.scrollWheel(dwheel); + return true; + } + // mouse outside popup: let scroll pass through to editor + } if (guideEditorContextMenu != null && guideEditorContextMenu.isOpen()) { guideEditorContextMenu.scrollWheel(mouseX, mouseY, dwheel, this.width, this.height, fontRendererObj); return true; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java index a90dcf5c..fcfbe227 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java @@ -12,8 +12,8 @@ public class ItemCandidate implements AutocompleteCandidate { private final String id; private final ItemStack stack; - private static final int ICON_SIZE = 12; - private static final int TEXT_X = ICON_SIZE + 4; + private static final int ICON_SIZE = 16; + private static final int TEXT_X = ICON_SIZE + 2; private static final int TEXT_COLOR = 0xFFF0F0F0; private static final RenderItem renderItem = new RenderItem(); @@ -34,13 +34,14 @@ public void render(FontRenderer fontRenderer, int x, int y, int width, boolean h RenderHelper.enableGUIStandardItemLighting(); GL11.glEnable(GL12.GL_RESCALE_NORMAL); renderItem.zLevel = 0; + // renderItemAndEffectIntoGUI draws in a 16x16 box; center it vertically in our 16px row renderItem.renderItemAndEffectIntoGUI( Minecraft.getMinecraft().fontRenderer, Minecraft.getMinecraft().getTextureManager(), - stack, x + 2, y + 2); + stack, x, y - 1); RenderHelper.disableStandardItemLighting(); GL11.glDisable(GL11.GL_BLEND); GL11.glPopMatrix(); - fontRenderer.drawString(id, x + TEXT_X, y + 3, TEXT_COLOR); + fontRenderer.drawString(id, x + TEXT_X, y + 4, TEXT_COLOR); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index b4e9f4fd..597449f2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -35,10 +35,16 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { try { MdAstRoot root = MdAst.fromMarkdown(text, PARSE_OPTIONS); - return resolveFromAst(root, text, cursorIndex); + TextSyntaxContext result = resolveFromAst(root, text, cursorIndex); + System.out.println("[AC-271afa] R:AST " + (result != null ? result.getElementType() : "null") + + " auto=" + (result != null ? result.shouldAutocomplete() : false)); + return result; } catch (ParseException e) { - // TODO: Remove after rewriting micromark parser with error recovery - return resolveFromFallback(text, cursorIndex); + TextSyntaxContext result = resolveFromFallback(text, cursorIndex); + System.out.println("[AC-271afa] R:FB " + (result != null ? result.getElementType() : "null") + + " auto=" + (result != null ? result.shouldAutocomplete() : false) + + " err=" + e.getMessage().substring(0, Math.min(40, e.getMessage().length()))); + return result; } } @@ -163,17 +169,30 @@ private TextSyntaxContext resolveFromFallback(String text, int cursorIndex) { } if (tagName != null && attrName != null && valueStart >= 0) { + // Scan forward for the closing quote to determine replaceEnd + int valueEnd = findCloseQuote(text, cursorIndex); String partialText = text.substring(valueStart, cursorIndex); return new TextSyntaxContext( SyntaxElementType.ATTRIBUTE_VALUE, valueStart, - cursorIndex, - new MdxAutocompleteContext(tagName, attrName, valueStart, cursorIndex, partialText)); + valueEnd, + new MdxAutocompleteContext(tagName, attrName, valueStart, valueEnd, partialText)); } return resolvePlainTextWord(text, cursorIndex); } + /** Scan forward from pos for a closing double-quote, or fall back to next '>' or newline. */ + private static int findCloseQuote(String text, int pos) { + int len = text.length(); + for (int i = pos; i < len; i++) { + char c = text.charAt(i); + if (c == '"') return i; + if (c == '>' || c == '\n') return i; + } + return len; + } + private static boolean isWordChar(char c) { return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java index 3f5add91..96fac95f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java @@ -50,10 +50,7 @@ public void show(List candidates, int anchorX, int anchor } computeSize(fontRenderer); - LytRect rect = SceneEditorPopupLayout.clampToViewport( - anchorX, anchorY, width, height, viewportWidth, viewportHeight, 2); - this.x = rect.x(); - this.y = rect.y(); + placePopup(anchorX, anchorY, viewportWidth, viewportHeight); this.open = true; } @@ -84,6 +81,33 @@ public void scrollWheel(int delta) { scrollY = clampScroll(scrollY - delta * maxH * 2); } + /** Recompute popup position without changing candidates or selection. */ + public void reposition(int anchorX, int anchorY, int viewportWidth, int viewportHeight, + FontRenderer fontRenderer) { + if (!open) return; + computeSize(fontRenderer); + placePopup(anchorX, anchorY, viewportWidth, viewportHeight); + this.scrollY = clampScroll(scrollY); + } + + /** Place popup below the anchor; flip above if it doesn't fit. */ + private void placePopup(int anchorX, int anchorY, int viewportWidth, int viewportHeight) { + // Try below cursor first + LytRect below = SceneEditorPopupLayout.clampToViewport( + anchorX, anchorY, width, height, viewportWidth, viewportHeight, 2); + if (below.y() >= anchorY - 2) { + this.x = below.x(); + this.y = below.y(); + return; + } + // Not enough room below — flip above cursor + int aboveY = anchorY - height - 18; + LytRect above = SceneEditorPopupLayout.clampToViewport( + anchorX, aboveY, width, height, viewportWidth, viewportHeight, 2); + this.x = above.x(); + this.y = above.y(); + } + public boolean mouseClicked(int mouseX, int mouseY) { if (!open || !contains(mouseX, mouseY)) { return false; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index 215fb022..af58f590 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -866,6 +866,23 @@ public int getCursorIndexAtPublic(int mouseX, int mouseY) { return getCursorIndexAt(mouseX, mouseY); } + /** Returns the pixel X position of the cursor relative to this text area. */ + public int getCursorPixelX() { + List lines = layoutCache.getVisualLines(); + if (lines.isEmpty()) return PADDING; + int lineIdx = getVisualLineIndex(selectionModel.getCursorIndex()); + return PADDING + getCursorPixelOnLine(selectionModel.getCursorIndex(), lines.get(lineIdx)) + - horizontalOffsetPixels; + } + + /** Returns the pixel Y position of the cursor relative to this text area. */ + public int getCursorPixelY() { + List lines = layoutCache.getVisualLines(); + if (lines.isEmpty()) return PADDING; + int lineIdx = getVisualLineIndex(selectionModel.getCursorIndex()); + return PADDING + lineIdx * getLineHeight() - scrollState.getOffsetPixels(); + } + private int getCursorIndexAt(int mouseX, int mouseY) { List lines = layoutCache.getVisualLines(); if (lines.isEmpty()) { From bdcd6816242cbef0a8666d2e72f5ddb0e303aef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 00:58:43 +0800 Subject: [PATCH 006/136] refactor(autocomplete): clean up, cache AST, extract shared utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove debug printlns - Cache MdAstRoot to skip re-parse on cursor-only moves - Store pendingContext to avoid re-resolve on commit - Preserve popup scroll/selection when candidate list unchanged - Extract getAutocompleteAnchorX/Y(), eliminate magic numbers - Extract SyntaxUtils shared by 3 resolvers - Fix fallback regex for lowercase tags - Fix raw Map type, COWList→ArrayList, skip redundant computeSize --- .gitignore | 1 + .../guidenh/guide/internal/GuideScreen.java | 91 +++++++------------ .../editor/autocomplete/SyntaxUtils.java | 21 +++++ .../provider/AutocompleteProviders.java | 3 +- .../autocomplete/provider/ItemIdProvider.java | 15 ++- .../resolver/MdxSyntaxResolver.java | 46 +++++----- .../resolver/SelectionStrategies.java | 9 +- .../resolver/WordBoundaryResolver.java | 19 +--- .../autocomplete/ui/AutocompletePopup.java | 39 ++++++-- 9 files changed, 130 insertions(+), 114 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java diff --git a/.gitignore b/.gitignore index 3a66b6a8..f23567ed 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ layout.json /src/test /docs /gradle-user +.claude/ diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 71fdf644..fdf87faf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -37,6 +37,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SelectionStrategy; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; @@ -254,10 +255,10 @@ public class GuideScreen extends GuiScreen implements GuideUiHost, GuiYesNoCallb private String autocompleteLastText; private int autocompleteLastCursor; private static final long AUTOCOMPLETE_DEBOUNCE_MS = 100; - private static final long AUTOCOMPLETE_LOG_THROTTLE_MS = 3000; - private long autocompleteLastLogTime; - private String autocompleteLastLogKey; - private java.util.Map autocompleteSelectionStrategies; + private static final int AUTOCOMPLETE_CURSOR_GAP_Y = 14; + @Nullable + private AutocompleteContext pendingAutocompleteContext; + private Map autocompleteSelectionStrategies; private boolean guideEditorDraggingDivider; private int guideEditorDividerGrabOffset; private boolean guideEditorDraggingPreviewScrollbar; @@ -741,8 +742,7 @@ public void onDoubleClick(int cursorIndex) { String text = guideEditorTextArea.getText(); TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursorIndex); if (ctx == null) return; - SelectionStrategy strategy = (SelectionStrategy) - ((java.util.Map) autocompleteSelectionStrategies).get(ctx.getElementType()); + SelectionStrategy strategy = autocompleteSelectionStrategies.get(ctx.getElementType()); if (strategy == null) return; int start = strategy.getSelectionStart(ctx, text, cursorIndex); int end = strategy.getSelectionEnd(ctx, text, cursorIndex); @@ -2018,14 +2018,22 @@ private void drawGuideEditorScreen(int mouseX, int mouseY) { } if (autocompletePopup != null && autocompletePopup.isOpen()) { - int anchorX = contentX + guideEditorTextArea.getCursorPixelX(); - int anchorY = getGuideEditorContentTop() + guideEditorTextArea.getCursorPixelY() - + fontRendererObj.FONT_HEIGHT + 12; - autocompletePopup.reposition(anchorX, anchorY, width, height, fontRendererObj); + autocompletePopup.reposition(getAutocompleteAnchorX(), getAutocompleteAnchorY(), + width, height, fontRendererObj); autocompletePopup.draw(fontRendererObj, mouseX, mouseY); } } + private int getAutocompleteAnchorX() { + return contentX + (guideEditorTextArea != null ? guideEditorTextArea.getCursorPixelX() : 0); + } + + private int getAutocompleteAnchorY() { + return getGuideEditorContentTop() + + (guideEditorTextArea != null ? guideEditorTextArea.getCursorPixelY() : 0) + + fontRendererObj.FONT_HEIGHT + AUTOCOMPLETE_CURSOR_GAP_Y; + } + private int resolveGuideEditorDividerColor() { if (guideEditorDraggingDivider) { return 0xFF5EA8FF; @@ -2211,18 +2219,15 @@ private boolean handleGuideEditorKey(char typedChar, int keyCode) { guideEditorSuppressTextFocusUntilGuideHotkeyRelease = false; } if (autocompletePopup != null && autocompletePopup.isOpen()) { - System.out.println("[AC-271afa] KEY popup: keyCode=" + keyCode + " char=" + (int) typedChar); switch (keyCode) { case Keyboard.KEY_ESCAPE: autocompletePopup.close(); return true; case Keyboard.KEY_UP: autocompletePopup.moveSelection(-1); - System.out.println("[AC-271afa] KEY up: selectedIndex moved"); return true; case Keyboard.KEY_DOWN: autocompletePopup.moveSelection(1); - System.out.println("[AC-271afa] KEY down: selectedIndex moved"); return true; case Keyboard.KEY_RETURN: case Keyboard.KEY_NUMPADENTER: @@ -2315,25 +2320,6 @@ private void performAutocompleteCheck() { TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursor); - boolean shouldLog = false; - String logKey = "ctx=" + (ctx != null ? ctx.getElementType() : "null") - + " auto=" + (ctx != null ? ctx.shouldAutocomplete() : false) - + " popup=" + (autocompletePopup != null && autocompletePopup.isOpen()) - + " cursor=" + cursor; - if (!logKey.equals(autocompleteLastLogKey) - || System.currentTimeMillis() - autocompleteLastLogTime > AUTOCOMPLETE_LOG_THROTTLE_MS) { - shouldLog = true; - autocompleteLastLogTime = System.currentTimeMillis(); - autocompleteLastLogKey = logKey; - } - - if (shouldLog) { - System.out.println("[AC-271afa] cursor=" + cursor + " textLen=" + text.length() - + " textChanged=" + textChanged + " cursorMoved=" + cursorMoved - + " ctx=" + (ctx != null ? ctx.getElementType() : "null") - + " auto=" + (ctx != null ? ctx.shouldAutocomplete() : false)); - } - if (ctx != null && ctx.shouldAutocomplete()) { java.util.List candidates = AutocompleteProviders.query(ctx.getAutocomplete(), 20); @@ -2341,18 +2327,13 @@ private void performAutocompleteCheck() { if (autocompletePopup == null) { autocompletePopup = new AutocompletePopup(); } - int anchorX = contentX + guideEditorTextArea.getCursorPixelX(); - int anchorY = getGuideEditorContentTop() + guideEditorTextArea.getCursorPixelY() - + fontRendererObj.FONT_HEIGHT + 12; - autocompletePopup.show(candidates, anchorX, anchorY, + pendingAutocompleteContext = ctx.getAutocomplete(); + autocompletePopup.show(candidates, getAutocompleteAnchorX(), getAutocompleteAnchorY(), width, height, fontRendererObj); - System.out.println("[AC-271afa] POPUP SHOWN: candidates=" + candidates.size() - + " pos=(" + anchorX + "," + anchorY + ")"); return; - } else { - System.out.println("[AC-271afa] query returned 0 candidates"); } } + pendingAutocompleteContext = null; if (autocompletePopup != null && autocompletePopup.isOpen()) { autocompletePopup.close(); } @@ -2361,29 +2342,21 @@ private void performAutocompleteCheck() { private void commitAutocompleteSelection() { if (autocompletePopup == null || !autocompletePopup.isOpen() || guideEditorTextArea == null) return; AutocompleteCandidate selected = autocompletePopup.getSelected(); - if (selected == null) { + if (selected == null || pendingAutocompleteContext == null) { autocompletePopup.close(); + pendingAutocompleteContext = null; return; } - if (autocompleteResolver == null) { - autocompletePopup.close(); - return; - } - TextSyntaxContext ctx = autocompleteResolver.resolve( - guideEditorTextArea.getText(), guideEditorTextArea.getCursorIndex()); - if (ctx != null && ctx.shouldAutocomplete()) { - AutocompleteContext ac = ctx.getAutocomplete(); - if (ac != null) { - String text = guideEditorTextArea.getText(); - String before = text.substring(0, ac.replaceStart()); - String after = text.substring(ac.replaceEnd()); - String newText = before + selected.replacementText() + after; - int newCursor = ac.replaceStart() + selected.replacementText().length(); - guideEditorTextArea.applyEdit(newText, newCursor, newCursor); - updateGuideEditorTextFromArea(); - } - } + AutocompleteContext ac = pendingAutocompleteContext; + String text = guideEditorTextArea.getText(); + String before = text.substring(0, ac.replaceStart()); + String after = text.substring(ac.replaceEnd()); + String newText = before + selected.replacementText() + after; + int newCursor = ac.replaceStart() + selected.replacementText().length(); + guideEditorTextArea.applyEdit(newText, newCursor, newCursor); + updateGuideEditorTextFromArea(); autocompletePopup.close(); + pendingAutocompleteContext = null; } private boolean handleGuideEditorMouseClicked(int mouseX, int mouseY, int button) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java new file mode 100644 index 00000000..08e41d5d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java @@ -0,0 +1,21 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +/** Shared text-scanning utilities for syntax resolvers. */ +public final class SyntaxUtils { + + private SyntaxUtils() {} + + /** Returns true if {@code c} is a word character (letter, digit, or common separators). */ + public static boolean isWordChar(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; + } + + /** Resolves cursor to word boundaries in plain text. */ + public static TextSyntaxContext resolveWord(String text, int cursorIndex) { + int start = cursorIndex; + while (start > 0 && isWordChar(text.charAt(start - 1))) start--; + int end = cursorIndex; + while (end < text.length() && isWordChar(text.charAt(end))) end++; + return new TextSyntaxContext(SyntaxElementType.WORD, start, end, null); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java index 2c02f358..4416dec1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -3,14 +3,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext; public final class AutocompleteProviders { - private static final List providers = new CopyOnWriteArrayList<>(); + private static final List providers = new ArrayList<>(); private AutocompleteProviders() {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java index 773ca983..b14a424e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -12,8 +13,18 @@ public class ItemIdProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(new AutocompleteKey("*", "id")); + // Tags whose "id" attribute refers to a Minecraft item registry key + private static final Set KEYS = buildKeys( + "ItemImage", "ItemLink", "Recipe", "RecipeFor", "RecipesFor" + ); + + private static Set buildKeys(String... tagNames) { + Set keys = new HashSet<>(); + for (String tag : tagNames) { + keys.add(new AutocompleteKey(tag, "id")); + } + return Collections.unmodifiableSet(keys); + } @Override public Set getSupportedKeys() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index 597449f2..17b2eac0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -8,6 +8,7 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.MdastOptions; @@ -26,26 +27,35 @@ public class MdxSyntaxResolver implements SyntaxContextResolver { // Matches open MDX tag: ]*?\\s+(\\w+)\\s*=\\s*\"([^\">]*)$"); + Pattern.compile("<([A-Za-z]\\w*)[^>]*?\\s+(\\w+)\\s*=\\s*\"([^\">]*)$"); + + // AST cache: re-parse only when text changes; cursor-only moves walk cached tree + @Nullable + private String cachedText; + @Nullable + private MdAstRoot cachedRoot; @Override @Nullable public TextSyntaxContext resolve(String text, int cursorIndex) { if (text == null || text.isEmpty()) return null; - try { - MdAstRoot root = MdAst.fromMarkdown(text, PARSE_OPTIONS); - TextSyntaxContext result = resolveFromAst(root, text, cursorIndex); - System.out.println("[AC-271afa] R:AST " + (result != null ? result.getElementType() : "null") - + " auto=" + (result != null ? result.shouldAutocomplete() : false)); - return result; - } catch (ParseException e) { - TextSyntaxContext result = resolveFromFallback(text, cursorIndex); - System.out.println("[AC-271afa] R:FB " + (result != null ? result.getElementType() : "null") - + " auto=" + (result != null ? result.shouldAutocomplete() : false) - + " err=" + e.getMessage().substring(0, Math.min(40, e.getMessage().length()))); - return result; + MdAstRoot root; + if (text.equals(cachedText) && cachedRoot != null) { + root = cachedRoot; + } else { + try { + root = MdAst.fromMarkdown(text, PARSE_OPTIONS); + } catch (ParseException e) { + cachedText = null; + cachedRoot = null; + return resolveFromFallback(text, cursorIndex); + } + cachedText = text; + cachedRoot = root; } + + return resolveFromAst(root, text, cursorIndex); } @Nullable @@ -141,11 +151,7 @@ private TextSyntaxContext resolveMdxAttribute(MdxJsxElementFields element, Strin } private TextSyntaxContext resolvePlainTextWord(String text, int cursorIndex) { - int start = cursorIndex; - while (start > 0 && isWordChar(text.charAt(start - 1))) start--; - int end = cursorIndex; - while (end < text.length() && isWordChar(text.charAt(end))) end++; - return new TextSyntaxContext(SyntaxElementType.WORD, start, end, null); + return SyntaxUtils.resolveWord(text, cursorIndex); } @Nullable @@ -192,8 +198,4 @@ private static int findCloseQuote(String text, int pos) { } return len; } - - private static boolean isWordChar(char c) { - return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; - } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java index 10685c78..37a07b0d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SelectionStrategy; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; public final class SelectionStrategies { @@ -25,7 +26,7 @@ public static final class WordSelection implements SelectionStrategy { @Override public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex) { int pos = cursorIndex; - while (pos > 0 && isWordChar(text.charAt(pos - 1))) { + while (pos > 0 && SyntaxUtils.isWordChar(text.charAt(pos - 1))) { pos--; } return pos; @@ -35,15 +36,11 @@ public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex public int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex) { int pos = cursorIndex; int len = text.length(); - while (pos < len && isWordChar(text.charAt(pos))) { + while (pos < len && SyntaxUtils.isWordChar(text.charAt(pos))) { pos++; } return pos; } - - private static boolean isWordChar(char c) { - return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; - } } public static final class ElementBoundarySelection implements SelectionStrategy { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java index 925bc2e8..b8ba6d99 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java @@ -4,6 +4,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; public final class WordBoundaryResolver implements SyntaxContextResolver { @@ -15,24 +16,10 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { return null; } - int start = cursorIndex; - while (start > 0 && isWordChar(text.charAt(start - 1))) { - start--; - } - - int end = cursorIndex; - while (end < text.length() && isWordChar(text.charAt(end))) { - end++; - } - - if (start == end && start == cursorIndex) { + if (!SyntaxUtils.isWordChar(text.charAt(Math.min(cursorIndex, text.length() - 1)))) { return new TextSyntaxContext(SyntaxElementType.OTHER, cursorIndex, cursorIndex, null); } - return new TextSyntaxContext(SyntaxElementType.WORD, start, end, null); - } - - private static boolean isWordChar(char c) { - return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == ':'; + return SyntaxUtils.resolveWord(text, cursorIndex); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java index 96fac95f..74d311a5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java @@ -1,5 +1,6 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -26,9 +27,12 @@ public final class AutocompletePopup { public static final int HOVER_COLOR = 0xCC2A3A46; public static final int SCROLLBAR_TRACK_COLOR = 0x35101010; public static final int SCROLLBAR_THUMB_COLOR = 0xA0D8D8D8; + /** Gap between popup and cursor when flipped above (roughly FONT_HEIGHT + cursor gap). */ + private static final int FLIP_GAP = 22; private boolean open; private List candidates = Collections.emptyList(); + private List lastCandidateKeys = Collections.emptyList(); private int selectedIndex; private int scrollY; private int x, y, width, height; @@ -39,24 +43,46 @@ public final class AutocompletePopup { public void show(List candidates, int anchorX, int anchorY, int viewportWidth, int viewportHeight, FontRenderer fontRenderer) { this.candidates = candidates != null ? candidates : Collections.emptyList(); - this.selectedIndex = this.candidates.isEmpty() ? -1 : 0; - this.scrollY = 0; - this.viewportWidth = viewportWidth; - this.viewportHeight = viewportHeight; if (this.candidates.isEmpty()) { open = false; return; } - computeSize(fontRenderer); + // Preserve scroll/selection when candidate list hasn't changed + boolean sameList = keysMatch(this.candidates, lastCandidateKeys); + if (!sameList) { + this.selectedIndex = 0; + this.scrollY = 0; + lastCandidateKeys = candidateKeys(this.candidates); + computeSize(fontRenderer); + } + this.viewportWidth = viewportWidth; + this.viewportHeight = viewportHeight; + placePopup(anchorX, anchorY, viewportWidth, viewportHeight); + if (selectedIndex >= this.candidates.size()) selectedIndex = 0; this.open = true; } + private static List candidateKeys(List list) { + List keys = new ArrayList<>(list.size()); + for (AutocompleteCandidate c : list) keys.add(c.displayText()); + return keys; + } + + private static boolean keysMatch(List list, List keys) { + if (list.size() != keys.size()) return false; + for (int i = 0; i < list.size(); i++) { + if (!list.get(i).displayText().equals(keys.get(i))) return false; + } + return true; + } + public void close() { open = false; candidates = Collections.emptyList(); + lastCandidateKeys = Collections.emptyList(); selectedIndex = -1; } @@ -85,7 +111,6 @@ public void scrollWheel(int delta) { public void reposition(int anchorX, int anchorY, int viewportWidth, int viewportHeight, FontRenderer fontRenderer) { if (!open) return; - computeSize(fontRenderer); placePopup(anchorX, anchorY, viewportWidth, viewportHeight); this.scrollY = clampScroll(scrollY); } @@ -101,7 +126,7 @@ private void placePopup(int anchorX, int anchorY, int viewportWidth, int viewpor return; } // Not enough room below — flip above cursor - int aboveY = anchorY - height - 18; + int aboveY = anchorY - height - FLIP_GAP; LytRect above = SceneEditorPopupLayout.clampToViewport( anchorX, aboveY, width, height, viewportWidth, viewportHeight, 2); this.x = above.x(); From 30d6397c1fc0c4cafaf71b8b86a7e4b1c9d93e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 01:34:14 +0800 Subject: [PATCH 007/136] feat(autocomplete): add TagAttributeRegistry, AttributeSpec, AttrType --- .../com/hfstudio/guidenh/ClientProxy.java | 2 + .../editor/autocomplete/AttrType.java | 9 + .../editor/autocomplete/AttributeSpec.java | 21 ++ .../autocomplete/TagAttributeRegistry.java | 211 ++++++++++++++++++ .../provider/AutocompleteKey.java | 63 +++++- .../provider/AutocompleteProviders.java | 16 +- .../autocomplete/provider/ItemIdProvider.java | 2 +- 7 files changed, 306 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 6ec98398..4d1076b8 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -25,6 +25,7 @@ import com.hfstudio.guidenh.network.GuideNhNetwork; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ItemIdProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.structurelibexport.StructureExportBootstrap; import cpw.mods.fml.common.event.FMLInitializationEvent; @@ -59,6 +60,7 @@ public void init(FMLInitializationEvent event) { OpenGuideHotkey.init(); OpenSceneEditorHotkey.init(); AutocompleteProviders.register(new ItemIdProvider()); + TagAttributeRegistry.initialize(); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java new file mode 100644 index 00000000..6a3ecfab --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java @@ -0,0 +1,9 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +public enum AttrType { + STRING, INT, FLOAT, BOOLEAN, COLOR, ENUM, + ITEM_ID, BLOCK_ID, ORE_DICT, ENTITY_ID, + KEY_BIND, PAGE_PATH, FILE_PATH, QUEST_UUID, + COMMAND, EXPRESSION, DOMAIN, FORMAT_PATTERN, + VECTOR3, SNBT +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java new file mode 100644 index 00000000..8a640213 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java @@ -0,0 +1,21 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +public final class AttributeSpec { + private final String name; + private final AttrType type; + private final Class> enumClass; + + public AttributeSpec(String name, AttrType type) { + this(name, type, null); + } + + public AttributeSpec(String name, AttrType type, Class> enumClass) { + this.name = name; + this.type = type; + this.enumClass = enumClass; + } + + public String getName() { return name; } + public AttrType getType() { return type; } + public Class> getEnumClass() { return enumClass; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java new file mode 100644 index 00000000..2f3e4fc6 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -0,0 +1,211 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +import java.util.*; + +public final class TagAttributeRegistry { + + private static final Map> registry = new LinkedHashMap<>(); + + private TagAttributeRegistry() {} + + public static void register(String tagName, AttributeSpec... specs) { + registry.computeIfAbsent(tagName, k -> new ArrayList<>()) + .addAll(Arrays.asList(specs)); + } + + public static List get(String tagName) { + return Collections.unmodifiableList( + registry.getOrDefault(tagName, Collections.emptyList())); + } + + public static Set getRegisteredTags() { + return Collections.unmodifiableSet(registry.keySet()); + } + + /** Populate the registry with all known tag→attribute mappings. */ + public static void initialize() { + // Inline/Flow tags + register("ItemImage", + new AttributeSpec("id", AttrType.ITEM_ID), + new AttributeSpec("ore", AttrType.ORE_DICT), + new AttributeSpec("scale", AttrType.FLOAT), + new AttributeSpec("yOffset", AttrType.INT), + new AttributeSpec("labelYOffset", AttrType.INT), + new AttributeSpec("noTooltip", AttrType.BOOLEAN), + new AttributeSpec("showTooltip", AttrType.BOOLEAN), + new AttributeSpec("showIcon", AttrType.STRING), + new AttributeSpec("label", AttrType.STRING), + new AttributeSpec("format", AttrType.FORMAT_PATTERN)); + register("ItemLink", + new AttributeSpec("id", AttrType.ITEM_ID), + new AttributeSpec("ore", AttrType.ORE_DICT), + new AttributeSpec("noTooltip", AttrType.BOOLEAN), + new AttributeSpec("showTooltip", AttrType.BOOLEAN), + new AttributeSpec("showIcon", AttrType.STRING), + new AttributeSpec("linksTo", AttrType.PAGE_PATH)); + register("BlockImage", + new AttributeSpec("id", AttrType.BLOCK_ID), + new AttributeSpec("ore", AttrType.ORE_DICT), + new AttributeSpec("scale", AttrType.FLOAT), + new AttributeSpec("wrap", AttrType.STRING), + new AttributeSpec("align", AttrType.STRING), + new AttributeSpec("float", AttrType.STRING)); + register("FloatingImage", + new AttributeSpec("src", AttrType.FILE_PATH), + new AttributeSpec("align", AttrType.STRING), + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT)); + register("Color", + new AttributeSpec("id", AttrType.COLOR), + new AttributeSpec("color", AttrType.COLOR)); + register("Entity", + new AttributeSpec("id", AttrType.ENTITY_ID), + new AttributeSpec("data", AttrType.SNBT), + new AttributeSpec("name", AttrType.STRING), + new AttributeSpec("uuid", AttrType.STRING), + new AttributeSpec("showName", AttrType.BOOLEAN), + new AttributeSpec("showCape", AttrType.BOOLEAN), + new AttributeSpec("baby", AttrType.BOOLEAN), + new AttributeSpec("x", AttrType.FLOAT), + new AttributeSpec("y", AttrType.FLOAT), + new AttributeSpec("z", AttrType.FLOAT)); + register("KeyBind", + new AttributeSpec("id", AttrType.KEY_BIND), + new AttributeSpec("action", AttrType.STRING)); + register("CommandLink", + new AttributeSpec("command", AttrType.COMMAND), + new AttributeSpec("close", AttrType.BOOLEAN), + new AttributeSpec("title", AttrType.STRING)); + register("Recipe", + new AttributeSpec("id", AttrType.ITEM_ID), + new AttributeSpec("fallbackText", AttrType.STRING), + new AttributeSpec("handlerName", AttrType.STRING), + new AttributeSpec("handlerId", AttrType.STRING), + new AttributeSpec("handlerOrder", AttrType.INT), + new AttributeSpec("input", AttrType.STRING), + new AttributeSpec("output", AttrType.STRING), + new AttributeSpec("limit", AttrType.INT)); + register("RecipeFor", + new AttributeSpec("id", AttrType.ITEM_ID), + new AttributeSpec("fallbackText", AttrType.STRING), + new AttributeSpec("handlerName", AttrType.STRING), + new AttributeSpec("handlerId", AttrType.STRING), + new AttributeSpec("handlerOrder", AttrType.INT), + new AttributeSpec("input", AttrType.STRING), + new AttributeSpec("output", AttrType.STRING), + new AttributeSpec("limit", AttrType.INT)); + register("RecipesFor", + new AttributeSpec("id", AttrType.ITEM_ID), + new AttributeSpec("fallbackText", AttrType.STRING), + new AttributeSpec("handlerName", AttrType.STRING), + new AttributeSpec("handlerId", AttrType.STRING), + new AttributeSpec("handlerOrder", AttrType.INT), + new AttributeSpec("input", AttrType.STRING), + new AttributeSpec("output", AttrType.STRING), + new AttributeSpec("limit", AttrType.INT)); + register("SubPages", + new AttributeSpec("id", AttrType.PAGE_PATH), + new AttributeSpec("alphabetical", AttrType.BOOLEAN)); + register("CategoryIndex", + new AttributeSpec("category", AttrType.STRING)); + register("Structure", + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT)); + register("GameScene", + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("zoom", AttrType.FLOAT), + new AttributeSpec("perspective", AttrType.STRING), + new AttributeSpec("interactive", AttrType.BOOLEAN), + new AttributeSpec("showGrid", AttrType.BOOLEAN)); + register("Mermaid", + new AttributeSpec("src", AttrType.FILE_PATH), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT)); + register("CsvTable", + new AttributeSpec("src", AttrType.FILE_PATH), + new AttributeSpec("header", AttrType.BOOLEAN), + new AttributeSpec("widths", AttrType.STRING)); + register("details", + new AttributeSpec("open", AttrType.BOOLEAN)); + register("Row", + new AttributeSpec("gap", AttrType.INT), + new AttributeSpec("alignItems", AttrType.STRING), + new AttributeSpec("fullWidth", AttrType.BOOLEAN), + new AttributeSpec("width", AttrType.INT)); + register("Column", + new AttributeSpec("gap", AttrType.INT), + new AttributeSpec("alignItems", AttrType.STRING), + new AttributeSpec("fullWidth", AttrType.BOOLEAN), + new AttributeSpec("width", AttrType.INT)); + register("a", + new AttributeSpec("name", AttrType.STRING), + new AttributeSpec("href", AttrType.PAGE_PATH), + new AttributeSpec("title", AttrType.STRING)); + register("br", + new AttributeSpec("clear", AttrType.STRING)); + register("ImportStructure", + new AttributeSpec("src", AttrType.FILE_PATH), + new AttributeSpec("offsetX", AttrType.FLOAT), + new AttributeSpec("offsetY", AttrType.FLOAT), + new AttributeSpec("offsetZ", AttrType.FLOAT)); + register("ImportStructureLib", + new AttributeSpec("controller", AttrType.STRING), + new AttributeSpec("piece", AttrType.STRING), + new AttributeSpec("channel", AttrType.STRING), + new AttributeSpec("facing", AttrType.STRING), + new AttributeSpec("rotation", AttrType.STRING), + new AttributeSpec("flip", AttrType.STRING)); + register("ImportPonder", + new AttributeSpec("src", AttrType.FILE_PATH)); + register("PlaceBlock", + new AttributeSpec("id", AttrType.BLOCK_ID), + new AttributeSpec("nbt", AttrType.SNBT), + new AttributeSpec("x", AttrType.INT), + new AttributeSpec("y", AttrType.INT), + new AttributeSpec("z", AttrType.INT)); + register("RemoveBlocks", + new AttributeSpec("id", AttrType.BLOCK_ID)); + register("ReplaceBlock", + new AttributeSpec("from", AttrType.BLOCK_ID), + new AttributeSpec("to", AttrType.BLOCK_ID), + new AttributeSpec("from_nbt", AttrType.SNBT), + new AttributeSpec("to_nbt", AttrType.SNBT)); + register("IsometricCamera", + new AttributeSpec("yaw", AttrType.FLOAT), + new AttributeSpec("pitch", AttrType.FLOAT), + new AttributeSpec("roll", AttrType.FLOAT)); + register("Function", + new AttributeSpec("expr", AttrType.EXPRESSION), + new AttributeSpec("inverse", AttrType.BOOLEAN), + new AttributeSpec("domain", AttrType.DOMAIN), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("label", AttrType.STRING)); + register("FunctionGraph", + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("background", AttrType.COLOR), + new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("xMin", AttrType.FLOAT), + new AttributeSpec("xMax", AttrType.FLOAT), + new AttributeSpec("yMin", AttrType.FLOAT), + new AttributeSpec("yMax", AttrType.FLOAT)); + register("Tooltip", + new AttributeSpec("label", AttrType.STRING)); + register("Latex", + new AttributeSpec("formula", AttrType.STRING), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("scale", AttrType.FLOAT), + new AttributeSpec("valign", AttrType.STRING)); + register("mark", + new AttributeSpec("color", AttrType.COLOR)); + register("FileTree", + new AttributeSpec("indent", AttrType.INT), + new AttributeSpec("gap", AttrType.INT)); + register("ItemGrid"); // no attributes - uses child elements + register("FootnoteList", + new AttributeSpec("width", AttrType.INT)); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java index dd49dcbe..d4304934 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java @@ -3,30 +3,71 @@ import java.util.Objects; public final class AutocompleteKey { + + public enum MatchType { + /** Cursor right after '<' — match tag name candidates */ + TAG_NAME, + /** Cursor inside tag body, not in a value — match attribute names */ + ATTR_NAME, + /** Cursor inside an attribute value — match value candidates */ + ATTR_VALUE + } + + private final MatchType type; private final String tagName; - private final String attributeName; + private final String attrName; + + private AutocompleteKey(MatchType type, String tagName, String attrName) { + this.type = type; + this.tagName = tagName; + this.attrName = attrName; + } + + public static AutocompleteKey forTag() { + return new AutocompleteKey(MatchType.TAG_NAME, null, null); + } + + public static AutocompleteKey forAttr(String tagName) { + return new AutocompleteKey(MatchType.ATTR_NAME, Objects.requireNonNull(tagName), null); + } - public AutocompleteKey(String tagName, String attributeName) { - this.tagName = Objects.requireNonNull(tagName); - this.attributeName = Objects.requireNonNull(attributeName); + public static AutocompleteKey forValue(String tagName, String attrName) { + return new AutocompleteKey(MatchType.ATTR_VALUE, + Objects.requireNonNull(tagName), Objects.requireNonNull(attrName)); } + public MatchType getType() { return type; } public String getTagName() { return tagName; } - public String getAttributeName() { return attributeName; } + public String getAttrName() { return attrName; } - public boolean matches(String tag, String attr) { - if (!tagName.equals("*") && !tagName.equals(tag)) return false; - if (!attributeName.equals("*") && !attributeName.equals(attr)) return false; - return true; + public boolean matches(MatchType queryType, String queryTag, String queryAttr) { + if (type != queryType) return false; + switch (type) { + case TAG_NAME: + return true; + case ATTR_NAME: + return tagName.equals("*") || tagName.equals(queryTag); + case ATTR_VALUE: + return (tagName.equals("*") || tagName.equals(queryTag)) + && (attrName.equals("*") || attrName.equals(queryAttr)); + default: + return false; + } } @Override public boolean equals(Object o) { if (!(o instanceof AutocompleteKey)) return false; AutocompleteKey that = (AutocompleteKey) o; - return tagName.equals(that.tagName) && attributeName.equals(that.attributeName); + return type == that.type && Objects.equals(tagName, that.tagName) + && Objects.equals(attrName, that.attrName); } @Override - public int hashCode() { return Objects.hash(tagName, attributeName); } + public int hashCode() { return Objects.hash(type, tagName, attrName); } + + @Override + public String toString() { + return "AutocompleteKey{" + type + ", " + tagName + ", " + attrName + "}"; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java index 4416dec1..f1b34f89 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -1,11 +1,9 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext; public final class AutocompleteProviders { @@ -18,13 +16,10 @@ public static void register(AutocompleteProvider provider) { } public static List query(AutocompleteContext ctx, int limit) { - if (!(ctx instanceof MdxAutocompleteContext)) return Collections.emptyList(); - MdxAutocompleteContext mdx = (MdxAutocompleteContext) ctx; - List results = new ArrayList<>(); for (AutocompleteProvider provider : providers) { for (AutocompleteKey key : provider.getSupportedKeys()) { - if (key.matches(mdx.getTagName(), mdx.getAttributeName())) { + if (matchesContext(key, ctx)) { results.addAll(provider.provide(ctx, Math.max(0, limit - results.size()))); break; } @@ -34,6 +29,15 @@ public static List query(AutocompleteContext ctx, int lim return results; } + private static boolean matchesContext(AutocompleteKey key, AutocompleteContext ctx) { + if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext) { + com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext mdx = + (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext) ctx; + return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, mdx.getTagName(), mdx.getAttributeName()); + } + return false; + } + public static void clear() { providers.clear(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java index b14a424e..ee1a493b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java @@ -21,7 +21,7 @@ public class ItemIdProvider implements AutocompleteProvider { private static Set buildKeys(String... tagNames) { Set keys = new HashSet<>(); for (String tag : tagNames) { - keys.add(new AutocompleteKey(tag, "id")); + keys.add(AutocompleteKey.forValue(tag, "id")); } return Collections.unmodifiableSet(keys); } From 7fe58dcad467374332e114b3323429d2969c1c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 01:36:41 +0800 Subject: [PATCH 008/136] feat(autocomplete): add renderWidth() to AutocompleteCandidate --- .../editor/autocomplete/provider/AutocompleteCandidate.java | 2 ++ .../internal/editor/autocomplete/ui/AutocompletePopup.java | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java index dd999af1..645fe782 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java @@ -6,5 +6,7 @@ public interface AutocompleteCandidate { String displayText(); String replacementText(); default int renderHeight() { return 14; } + /** Width hint for popup sizing. Default 0 means use displayText width. */ + default int renderWidth(FontRenderer fontRenderer) { return 0; } void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java index 74d311a5..b502bd3e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java @@ -190,7 +190,10 @@ private void computeSize(FontRenderer fontRenderer) { int maxW = 72; int maxItemH = 14; for (AutocompleteCandidate c : candidates) { - int w = fontRenderer.getStringWidth(c.displayText()) + PADDING_X * 2 + 16; + int textW = fontRenderer.getStringWidth(c.displayText()) + PADDING_X * 2; + int renderW = c.renderWidth(fontRenderer); + int w = Math.max(textW, renderW > 0 ? renderW + PADDING_X * 2 : 0); + if (w > maxW) maxW = w; if (c.renderHeight() > maxItemH) maxItemH = c.renderHeight(); } From a86eb3eab7ce3fa3726d92effe2a2fed61eab3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 01:54:43 +0800 Subject: [PATCH 009/136] feat(autocomplete): add resolver chain, context types, and attribute name completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CompositeResolver to chain Frontmatter→Mdx→WordBoundary resolvers - Add FrontmatterResolver for YAML frontmatter key/value detection - Add MdxValueContext, MdxAttrNameContext, TagStartContext, FrontmatterContext - Extend MdxSyntaxResolver with TAG_START and ATTRIBUTE_NAME detection - Extend AutocompleteProviders with 4-way context dispatch - Add AttributeNameProvider using TagAttributeRegistry - Add ColorCandidate and RegistryCandidate rendering stubs - Wire CompositeResolver into GuideScreen --- .../com/hfstudio/guidenh/ClientProxy.java | 2 + .../guidenh/guide/internal/GuideScreen.java | 9 ++- .../autocomplete/SyntaxElementType.java | 2 + .../provider/AttributeNameProvider.java | 36 +++++++++ .../provider/AutocompleteProviders.java | 21 ++++- .../autocomplete/provider/ColorCandidate.java | 36 +++++++++ .../provider/RegistryCandidate.java | 32 ++++++++ .../resolver/CompositeResolver.java | 29 +++++++ .../resolver/FrontmatterContext.java | 23 ++++++ .../resolver/FrontmatterResolver.java | 79 +++++++++++++++++++ ...teContext.java => MdxAttrNameContext.java} | 9 +-- .../resolver/MdxSyntaxResolver.java | 31 +++++++- .../resolver/MdxValueContext.java | 27 +++++++ .../resolver/TagStartContext.java | 20 +++++ 14 files changed, 342 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java rename src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/{MdxAutocompleteContext.java => MdxAttrNameContext.java} (64%) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 4d1076b8..d8fb0c07 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -23,6 +23,7 @@ import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; import com.hfstudio.guidenh.network.GuideNhNetwork; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ItemIdProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; @@ -61,6 +62,7 @@ public void init(FMLInitializationEvent event) { OpenSceneEditorHotkey.init(); AutocompleteProviders.register(new ItemIdProvider()); TagAttributeRegistry.initialize(); + AutocompleteProviders.register(new AttributeNameProvider()); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index fdf87faf..3dc11d8c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -41,8 +41,11 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.CompositeResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxSyntaxResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.WordBoundaryResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui.AutocompletePopup; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; @@ -452,7 +455,11 @@ public void initGui() { rebuildToolbar(); ensureGuideEditorTextArea(); if (autocompleteResolver == null) { - autocompleteResolver = new MdxSyntaxResolver(); + autocompleteResolver = new CompositeResolver( + new FrontmatterResolver(), + new MdxSyntaxResolver(), + new WordBoundaryResolver() + ); autocompleteSelectionStrategies = SelectionStrategies.defaults(); } refreshGuideEditorDraft(true); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java index bff43cc7..33dbf1f9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java @@ -3,7 +3,9 @@ public enum SyntaxElementType { WORD, TAG_NAME, + TAG_START, ATTRIBUTE_NAME, ATTRIBUTE_VALUE, + FRONTMATTER_KEY, OTHER } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java new file mode 100644 index 00000000..40983bf8 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java @@ -0,0 +1,36 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext; + +/** Suggests valid attribute names for the current MDX tag. */ +public class AttributeNameProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forAttr("*")); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof MdxAttrNameContext)) return Collections.emptyList(); + MdxAttrNameContext mdx = (MdxAttrNameContext) ctx; + + List specs = TagAttributeRegistry.get(mdx.getTagName()); + String partial = mdx.getPartialText().toLowerCase(); + + List results = new ArrayList<>(); + for (AttributeSpec spec : specs) { + if (results.size() >= limit) break; + if (partial.isEmpty() || spec.getName().toLowerCase().contains(partial)) { + results.add(new TextCandidate(spec.getName())); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java index f1b34f89..d9dbc7e7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -30,10 +30,23 @@ public static List query(AutocompleteContext ctx, int lim } private static boolean matchesContext(AutocompleteKey key, AutocompleteContext ctx) { - if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext) { - com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext mdx = - (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAutocompleteContext) ctx; - return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, mdx.getTagName(), mdx.getAttributeName()); + if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext) { + com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext mdx = + (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext) ctx; + return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, mdx.getTagName(), mdx.getAttrName()); + } + if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext) { + com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext mdx = + (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext) ctx; + return key.matches(AutocompleteKey.MatchType.ATTR_NAME, mdx.getTagName(), null); + } + if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.TagStartContext) { + return key.matches(AutocompleteKey.MatchType.TAG_NAME, null, null); + } + if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) { + com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext fmc = + (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) ctx; + return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, "*", fmc.getKey()); } return false; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java new file mode 100644 index 00000000..e780541d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java @@ -0,0 +1,36 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; + +public class ColorCandidate implements AutocompleteCandidate { + private final String name; + private final int color; + private static final int SWATCH_SIZE = 12; + private static final int TEXT_X = SWATCH_SIZE + 6; + private static final int TEXT_COLOR = 0xFFF0F0F0; + + public ColorCandidate(String name, int color) { + this.name = name; + this.color = color; + } + + @Override public String displayText() { return name; } + @Override public String replacementText() { return name; } + @Override public int renderHeight() { return 16; } + + @Override + public int renderWidth(FontRenderer fontRenderer) { + return SWATCH_SIZE + 6 + fontRenderer.getStringWidth(name) + 6; + } + + @Override + public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { + // Draw color swatch + int swatchY = y + (renderHeight() - SWATCH_SIZE) / 2; + Gui.drawRect(x, swatchY, x + SWATCH_SIZE, swatchY + SWATCH_SIZE, 0xFF000000 | color); + Gui.drawRect(x - 1, swatchY - 1, x + SWATCH_SIZE + 1, swatchY + SWATCH_SIZE + 1, 0xFF4D5661); + // Draw name + fontRenderer.drawString(name, x + TEXT_X, y + 3, TEXT_COLOR); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java new file mode 100644 index 00000000..d29eddb6 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java @@ -0,0 +1,32 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import net.minecraft.client.gui.FontRenderer; + +/** Candidate displaying a registry key with optional subtitle. */ +public class RegistryCandidate implements AutocompleteCandidate { + private final String key; + private final String subtitle; + private static final int TEXT_COLOR = 0xFFF0F0F0; + private static final int SUBTITLE_COLOR = 0xFFA0A0A0; + + public RegistryCandidate(String key) { + this(key, null); + } + + public RegistryCandidate(String key, String subtitle) { + this.key = key; + this.subtitle = subtitle; + } + + @Override public String displayText() { return key; } + @Override public String replacementText() { return key; } + @Override public int renderHeight() { return subtitle != null ? 28 : 14; } + + @Override + public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { + fontRenderer.drawString(key, x, y + 2, TEXT_COLOR); + if (subtitle != null) { + fontRenderer.drawString(subtitle, x + 4, y + 14, SUBTITLE_COLOR); + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java new file mode 100644 index 00000000..ba5d3c78 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java @@ -0,0 +1,29 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import java.util.Arrays; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; + +/** Chains multiple resolvers; first non-null result wins. */ +public final class CompositeResolver implements SyntaxContextResolver { + + private final List chain; + + public CompositeResolver(SyntaxContextResolver... resolvers) { + this.chain = Arrays.asList(resolvers); + } + + @Override + @Nullable + public TextSyntaxContext resolve(String text, int cursorIndex) { + for (SyntaxContextResolver r : chain) { + TextSyntaxContext ctx = r.resolve(text, cursorIndex); + if (ctx != null) return ctx; + } + return null; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java new file mode 100644 index 00000000..504ae346 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java @@ -0,0 +1,23 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +public final class FrontmatterContext implements AutocompleteContext { + private final String key; + private final int replaceStart; + private final int replaceEnd; + private final String partialText; + + public FrontmatterContext(String key, int replaceStart, int replaceEnd, String partialText) { + this.key = key; + this.replaceStart = replaceStart; + this.replaceEnd = replaceEnd; + this.partialText = partialText; + } + + public String getKey() { return key; } + + @Override public int replaceStart() { return replaceStart; } + @Override public int replaceEnd() { return replaceEnd; } + @Override public String getPartialText() { return partialText; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java new file mode 100644 index 00000000..4884aead --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java @@ -0,0 +1,79 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; + +/** Detects cursor position inside YAML frontmatter (between --- delimiters at document start). */ +public final class FrontmatterResolver implements SyntaxContextResolver { + + private static final String DELIM = "---"; + + @Override + @Nullable + public TextSyntaxContext resolve(String text, int cursorIndex) { + if (text == null || text.isEmpty()) return null; + if (!text.startsWith(DELIM)) return null; + + int firstEnd = text.indexOf('\n', DELIM.length()); + if (firstEnd < 0) return null; + int secondStart = text.indexOf(DELIM, firstEnd + 1); + if (secondStart < 0) return null; // unclosed frontmatter + + if (cursorIndex < DELIM.length() || cursorIndex > secondStart) return null; + + // Find which line cursor is on + String line = getLineAt(text, cursorIndex); + if (line == null) return null; + + int colonIdx = line.indexOf(':'); + if (colonIdx < 0) return SyntaxUtils.resolveWord(text, cursorIndex); + + String key = line.substring(0, colonIdx).trim(); + // Skip YAML comments and list markers + if (key.isEmpty() || key.startsWith("#") || key.startsWith("- ")) { + return SyntaxUtils.resolveWord(text, cursorIndex); + } + + int valueStart = colonIdx + 1; + while (valueStart < line.length() && line.charAt(valueStart) == ' ') valueStart++; + + int lineStart = text.lastIndexOf('\n', cursorIndex - 1) + 1; + int valueAbsStart = lineStart + valueStart; + int valueAbsEnd = lineStart + line.length(); + + // Trim trailing comment from value end + int hashIdx = line.indexOf(" #", valueStart); + if (hashIdx >= 0) valueAbsEnd = lineStart + hashIdx; + valueAbsEnd = Math.min(valueAbsEnd, secondStart); + + if (cursorIndex >= valueAbsStart && cursorIndex <= valueAbsEnd) { + String partialText = text.substring(valueAbsStart, cursorIndex); + return new TextSyntaxContext(SyntaxElementType.WORD, valueAbsStart, valueAbsEnd, + new FrontmatterContext(key, valueAbsStart, valueAbsEnd, partialText)); + } + + // Cursor is on the key + int keyStart = lineStart + line.indexOf(key); + int keyEnd = keyStart + key.length(); + if (cursorIndex >= keyStart && cursorIndex <= keyEnd) { + return new TextSyntaxContext(SyntaxElementType.WORD, keyStart, keyEnd, + new FrontmatterContext(key, keyStart, keyEnd, + text.substring(keyStart, cursorIndex))); + } + + return SyntaxUtils.resolveWord(text, cursorIndex); + } + + @Nullable + private String getLineAt(String text, int cursorIndex) { + int lineStart = text.lastIndexOf('\n', cursorIndex - 1) + 1; + int lineEnd = text.indexOf('\n', cursorIndex); + if (lineEnd < 0) lineEnd = text.length(); + if (lineStart >= lineEnd) return null; + return text.substring(lineStart, lineEnd); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAutocompleteContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAttrNameContext.java similarity index 64% rename from src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAutocompleteContext.java rename to src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAttrNameContext.java index 9c3220b1..96bd5255 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAutocompleteContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAttrNameContext.java @@ -2,24 +2,21 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -public final class MdxAutocompleteContext implements AutocompleteContext { +/** Context for attribute NAME completion — cursor is in tag body whitespace, not in a value. */ +public final class MdxAttrNameContext implements AutocompleteContext { private final String tagName; - private final String attributeName; private final int replaceStart; private final int replaceEnd; private final String partialText; - public MdxAutocompleteContext(String tagName, String attributeName, int replaceStart, int replaceEnd, - String partialText) { + public MdxAttrNameContext(String tagName, int replaceStart, int replaceEnd, String partialText) { this.tagName = tagName; - this.attributeName = attributeName; this.replaceStart = replaceStart; this.replaceEnd = replaceEnd; this.partialText = partialText; } public String getTagName() { return tagName; } - public String getAttributeName() { return attributeName; } @Override public int replaceStart() { return replaceStart; } @Override public int replaceEnd() { return replaceEnd; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index 17b2eac0..cba1de13 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -60,11 +60,16 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { @Nullable private TextSyntaxContext resolveFromAst(MdAstRoot root, String text, int cursorIndex) { + if (cursorIndex > 0 && text.charAt(cursorIndex - 1) == '<') { + int tagStart = cursorIndex - 1; + return new TextSyntaxContext(SyntaxElementType.TAG_START, tagStart, cursorIndex, + new TagStartContext(tagStart, cursorIndex, "")); + } MdxJsxElementFields element = findEnclosingMdxElement(root, cursorIndex); if (element != null) { return resolveMdxAttribute(element, text, cursorIndex); } - return resolvePlainTextWord(text, cursorIndex); + return SyntaxUtils.resolveWord(text, cursorIndex); } @Nullable @@ -144,7 +149,22 @@ private TextSyntaxContext resolveMdxAttribute(MdxJsxElementFields element, Strin SyntaxElementType.ATTRIBUTE_VALUE, valueStart, valueEnd, - new MdxAutocompleteContext(tagName, attr.name, valueStart, valueEnd, partialText)); + new MdxValueContext(tagName, attr.name, valueStart, valueEnd, partialText)); + } + + // ATTRIBUTE_NAME detection: cursor inside tag body but not in any attribute. + // AST-only — fallback cannot reliably distinguish tag-internal whitespace + // from plain-text whitespace without false positives. We fail closed. + // TODO: When parser supports error-recovery AST, enable fallback here. + com.hfstudio.guidenh.libs.unist.UnistPosition elemPos = element.position(); + if (elemPos != null && elemPos.start() != null && elemPos.end() != null) { + int elemEnd = elemPos.end().offset(); + if (cursorIndex > elemPos.start().offset() && cursorIndex < elemEnd) { + String partial = text.substring(cursorIndex, Math.min(cursorIndex + 1, text.length())); + return new TextSyntaxContext(SyntaxElementType.ATTRIBUTE_NAME, + cursorIndex, cursorIndex, + new MdxAttrNameContext(tagName, cursorIndex, cursorIndex, partial)); + } } return resolvePlainTextWord(text, cursorIndex); @@ -156,6 +176,11 @@ private TextSyntaxContext resolvePlainTextWord(String text, int cursorIndex) { @Nullable private TextSyntaxContext resolveFromFallback(String text, int cursorIndex) { + if (cursorIndex > 0 && text.charAt(cursorIndex - 1) == '<') { + int tagStart = cursorIndex - 1; + return new TextSyntaxContext(SyntaxElementType.TAG_START, tagStart, cursorIndex, + new TagStartContext(tagStart, cursorIndex, "")); + } String prefix = text.substring(0, Math.min(cursorIndex, text.length())); Matcher m = FALLBACK_TAG.matcher(prefix); @@ -182,7 +207,7 @@ private TextSyntaxContext resolveFromFallback(String text, int cursorIndex) { SyntaxElementType.ATTRIBUTE_VALUE, valueStart, valueEnd, - new MdxAutocompleteContext(tagName, attrName, valueStart, valueEnd, partialText)); + new MdxValueContext(tagName, attrName, valueStart, valueEnd, partialText)); } return resolvePlainTextWord(text, cursorIndex); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java new file mode 100644 index 00000000..35b01da0 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java @@ -0,0 +1,27 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Replaces the old MdxAutocompleteContext. Carries tag name, attribute name, and replacement range. */ +public final class MdxValueContext implements AutocompleteContext { + private final String tagName; + private final String attrName; + private final int replaceStart; + private final int replaceEnd; + private final String partialText; + + public MdxValueContext(String tagName, String attrName, int replaceStart, int replaceEnd, String partialText) { + this.tagName = tagName; + this.attrName = attrName; + this.replaceStart = replaceStart; + this.replaceEnd = replaceEnd; + this.partialText = partialText; + } + + public String getTagName() { return tagName; } + public String getAttrName() { return attrName; } + + @Override public int replaceStart() { return replaceStart; } + @Override public int replaceEnd() { return replaceEnd; } + @Override public String getPartialText() { return partialText; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java new file mode 100644 index 00000000..d8b103cb --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java @@ -0,0 +1,20 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Context for tag NAME completion — cursor right after '<', before tag name. */ +public final class TagStartContext implements AutocompleteContext { + private final int replaceStart; + private final int replaceEnd; + private final String partialText; + + public TagStartContext(int replaceStart, int replaceEnd, String partialText) { + this.replaceStart = replaceStart; + this.replaceEnd = replaceEnd; + this.partialText = partialText; + } + + @Override public int replaceStart() { return replaceStart; } + @Override public int replaceEnd() { return replaceEnd; } + @Override public String getPartialText() { return partialText; } +} From b5b041e14543175013c4cff7ead8659ac59c0134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 02:02:34 +0800 Subject: [PATCH 010/136] feat(autocomplete): add 16 value providers and tag name provider - Boolean, EnumValue, Color, OreDict, BlockId, EntityName providers - KeyBind, PageReference (stub), Anchor (stub), Command providers - NumericValue, Expression, Domain, FormatPattern providers - RecipeFilter, TagName providers - Fix RegistryCandidate to support ItemStack icon rendering - Register all providers in ClientProxy --- .../com/hfstudio/guidenh/ClientProxy.java | 32 +++++++++ .../autocomplete/provider/AnchorProvider.java | 24 +++++++ .../provider/BlockIdProvider.java | 53 ++++++++++++++ .../provider/BooleanProvider.java | 30 ++++++++ .../autocomplete/provider/ColorProvider.java | 72 +++++++++++++++++++ .../provider/CommandProvider.java | 34 +++++++++ .../autocomplete/provider/DomainProvider.java | 35 +++++++++ .../provider/EntityNameProvider.java | 33 +++++++++ .../provider/EnumValueProvider.java | 53 ++++++++++++++ .../provider/ExpressionProvider.java | 36 ++++++++++ .../provider/FormatPatternProvider.java | 32 +++++++++ .../provider/KeyBindProvider.java | 31 ++++++++ .../provider/NumericValueProvider.java | 48 +++++++++++++ .../provider/OreDictProvider.java | 30 ++++++++ .../provider/PageReferenceProvider.java | 29 ++++++++ .../provider/RecipeFilterProvider.java | 40 +++++++++++ .../provider/RegistryCandidate.java | 53 ++++++++++++-- .../provider/TagNameProvider.java | 52 ++++++++++++++ 18 files changed, 711 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index d8fb0c07..e053fe4f 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -23,9 +23,25 @@ import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; import com.hfstudio.guidenh.network.GuideNhNetwork; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BlockIdProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BooleanProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ColorProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.CommandProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DomainProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EntityNameProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EnumValueProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ExpressionProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FormatPatternProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ItemIdProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.KeyBindProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.NumericValueProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.OreDictProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.RecipeFilterProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.TagNameProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.structurelibexport.StructureExportBootstrap; @@ -63,6 +79,22 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new ItemIdProvider()); TagAttributeRegistry.initialize(); AutocompleteProviders.register(new AttributeNameProvider()); + AutocompleteProviders.register(new BooleanProvider()); + AutocompleteProviders.register(new EnumValueProvider()); + AutocompleteProviders.register(new ColorProvider()); + AutocompleteProviders.register(new OreDictProvider()); + AutocompleteProviders.register(new BlockIdProvider()); + AutocompleteProviders.register(new EntityNameProvider()); + AutocompleteProviders.register(new KeyBindProvider()); + AutocompleteProviders.register(new PageReferenceProvider()); + AutocompleteProviders.register(new AnchorProvider()); + AutocompleteProviders.register(new CommandProvider()); + AutocompleteProviders.register(new NumericValueProvider()); + AutocompleteProviders.register(new ExpressionProvider()); + AutocompleteProviders.register(new DomainProvider()); + AutocompleteProviders.register(new FormatPatternProvider()); + AutocompleteProviders.register(new RecipeFilterProvider()); + AutocompleteProviders.register(new TagNameProvider()); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java new file mode 100644 index 00000000..13fd2f37 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java @@ -0,0 +1,24 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests heading anchors for <a href="#..."> attributes. + * Requires current document context — for now returns empty; enable after wiring. */ +public class AnchorProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("a", "href")); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText(); + if (partial == null || !partial.startsWith("#")) return Collections.emptyList(); + // TODO: parse current document for headings and anchor names + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java new file mode 100644 index 00000000..3e557891 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java @@ -0,0 +1,53 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import net.minecraft.block.Block; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests Block registry names for block-related tag "id" attributes. */ +public class BlockIdProvider implements AutocompleteProvider { + + private static final Set KEYS = buildKeys( + "BlockImage", "PlaceBlock", "RemoveBlocks" + ); + + private static Set buildKeys(String... tagNames) { + Set keys = new HashSet<>(); + for (String tag : tagNames) { + keys.add(AutocompleteKey.forValue(tag, "id")); + } + // Also match ReplaceBlock's "from" and "to" attributes + keys.add(AutocompleteKey.forValue("ReplaceBlock", "from")); + keys.add(AutocompleteKey.forValue("ReplaceBlock", "to")); + keys.add(AutocompleteKey.forValue("BlockAnnotationTemplate", "id")); + return Collections.unmodifiableSet(keys); + } + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (Object obj : Block.blockRegistry.getKeys()) { + if (results.size() >= limit) break; + if (obj instanceof String key) { + if (partial.isEmpty() || key.toLowerCase().contains(partial)) { + Block block = (Block) Block.blockRegistry.getObject(key); + ItemStack stack = block != null ? new ItemStack(Item.getItemFromBlock(block)) : null; + if (stack != null && stack.getItem() != null) { + results.add(new RegistryCandidate(key, stack)); + } else { + results.add(new TextCandidate(key)); + } + } + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java new file mode 100644 index 00000000..c17ec395 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java @@ -0,0 +1,30 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; + +/** Suggests true/false for boolean-typed attributes. */ +public class BooleanProvider implements AutocompleteProvider { + + @Override + public Set getSupportedKeys() { + Set keys = new HashSet<>(); + for (String tag : TagAttributeRegistry.getRegisteredTags()) { + for (AttributeSpec spec : TagAttributeRegistry.get(tag)) { + if (spec.getType() == AttrType.BOOLEAN) { + keys.add(AutocompleteKey.forValue(tag, spec.getName())); + } + } + } + return Collections.unmodifiableSet(keys); + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + return Arrays.asList(new TextCandidate("true"), new TextCandidate("false")); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java new file mode 100644 index 00000000..f581a9cd --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java @@ -0,0 +1,72 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; + +/** Suggests hex color values and SymbolicColor names for color-typed attributes. */ +public class ColorProvider implements AutocompleteProvider { + + private static final String[] HEX_COLORS = { + "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", + "#FFFFFF", "#000000", "#808080", "#FFA500", "#800080", "#008080", + "#FFC0CB", "#A52A2A", "#00FF7F", "#FFD700" + }; + + private static final String[] SYMBOLIC_NAMES = { + "LINK", "BODY_TEXT", "ERROR_TEXT", "CRAFTING_RECIPE_TYPE", "THEMATIC_BREAK", + "HEADER1_SEPARATOR", "HEADER2_SEPARATOR", "NAVBAR_BG_TOP", "NAVBAR_BG_BOTTOM", + "NAVBAR_ROW_HOVER", "NAVBAR_EXPAND_ARROW", "TABLE_BORDER", + "ICON_BUTTON_NORMAL", "ICON_BUTTON_DISABLED", "ICON_BUTTON_HOVER", + "IN_WORLD_BLOCK_HIGHLIGHT", "SCENE_BACKGROUND", "GUIDE_SCREEN_BACKGROUND", + "BLOCKQUOTE_BACKGROUND", "BLACK", "DARK_BLUE", "DARK_GREEN", "DARK_AQUA", + "DARK_RED", "DARK_PURPLE", "GOLD", "GRAY", "DARK_GRAY", "BLUE", "GREEN", + "AQUA", "RED", "LIGHT_PURPLE", "YELLOW", "WHITE" + }; + + @Override + public Set getSupportedKeys() { + Set keys = new HashSet<>(); + for (String tag : TagAttributeRegistry.getRegisteredTags()) { + for (AttributeSpec spec : TagAttributeRegistry.get(tag)) { + if (spec.getType() == AttrType.COLOR) { + keys.add(AutocompleteKey.forValue(tag, spec.getName())); + } + } + } + return Collections.unmodifiableSet(keys); + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof MdxValueContext)) return Collections.emptyList(); + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + + for (String hex : HEX_COLORS) { + if (results.size() >= limit) break; + if (partial.isEmpty() || hex.toLowerCase().contains(partial)) { + results.add(new ColorCandidate(hex, parseHexColor(hex))); + } + } + for (String name : SYMBOLIC_NAMES) { + if (results.size() >= limit) break; + if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + results.add(new TextCandidate(name)); + } + } + return results; + } + + private static int parseHexColor(String hex) { + try { + return (int) Long.parseLong(hex.substring(1), 16) | 0xFF000000; + } catch (NumberFormatException e) { + return 0xFFFFFFFF; + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java new file mode 100644 index 00000000..1e277884 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java @@ -0,0 +1,34 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests GuideNH commands for <CommandLink command> attributes. */ +public class CommandProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("CommandLink", "command")); + + private static final String[] COMMANDS = { + "/guidenh open ", "/guidenh search ", "/guidenh reload", + "/guidenh export", "/guidenh export-site", "/guidenh export-structure", + "/guidenh import-structure", "/guidenh place-all-structures", "/guidenh list" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String cmd : COMMANDS) { + if (results.size() >= limit) break; + if (partial.isEmpty() || cmd.toLowerCase().contains(partial)) { + results.add(new TextCandidate(cmd)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java new file mode 100644 index 00000000..b9561e7d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java @@ -0,0 +1,35 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests domain interval templates for domain attributes. */ +public class DomainProvider implements AutocompleteProvider { + + private static final Set KEYS = new HashSet<>(Arrays.asList( + AutocompleteKey.forValue("Function", "domain"), + AutocompleteKey.forValue("Plot", "domain") + )); + + private static final String[] DOMAINS = { + "(-inf, inf)", "[-10, 10]", "(-pi, pi)", "[0, 2*pi)", + "(-inf, 0]", "[0, inf)", "(-5, 5)", "[-1, 1]" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String d : DOMAINS) { + if (results.size() >= limit) break; + if (partial.isEmpty() || d.toLowerCase().contains(partial)) { + results.add(new TextCandidate(d)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java new file mode 100644 index 00000000..5fdf35a6 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java @@ -0,0 +1,33 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import net.minecraft.entity.EntityList; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests entity registry names for <Entity id> attributes. */ +public class EntityNameProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("Entity", "id")); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + @SuppressWarnings("unchecked") + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (Object obj : EntityList.stringToClassMapping.keySet()) { + if (results.size() >= limit) break; + if (obj instanceof String key) { + if (partial.isEmpty() || key.toLowerCase().contains(partial)) { + results.add(new TextCandidate(key)); + } + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java new file mode 100644 index 00000000..7d08419f --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java @@ -0,0 +1,53 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; +import com.hfstudio.guidenh.guide.compiler.tags.SerializedEnum; + +/** Suggests enum values for enum-typed attributes. */ +public class EnumValueProvider implements AutocompleteProvider { + + @Override + public Set getSupportedKeys() { + Set keys = new HashSet<>(); + for (String tag : TagAttributeRegistry.getRegisteredTags()) { + for (AttributeSpec spec : TagAttributeRegistry.get(tag)) { + if (spec.getType() == AttrType.ENUM) { + keys.add(AutocompleteKey.forValue(tag, spec.getName())); + } + } + } + return Collections.unmodifiableSet(keys); + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof MdxValueContext)) return Collections.emptyList(); + MdxValueContext mdx = (MdxValueContext) ctx; + + for (AttributeSpec spec : TagAttributeRegistry.get(mdx.getTagName())) { + if (!spec.getName().equals(mdx.getAttrName())) continue; + if (spec.getType() != AttrType.ENUM || spec.getEnumClass() == null) continue; + + String partial = mdx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (Enum e : spec.getEnumClass().getEnumConstants()) { + if (results.size() >= limit) break; + String name = e.name(); + if (e instanceof SerializedEnum) { + name = ((SerializedEnum) e).getSerializedName(); + } + if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + results.add(new TextCandidate(name)); + } + } + return results; + } + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java new file mode 100644 index 00000000..9568726b --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java @@ -0,0 +1,36 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests common math expressions for <Function expr> and <Plot expr>. */ +public class ExpressionProvider implements AutocompleteProvider { + + private static final Set KEYS = new HashSet<>(Arrays.asList( + AutocompleteKey.forValue("Function", "expr"), + AutocompleteKey.forValue("Plot", "expr") + )); + + private static final String[] EXPRESSIONS = { + "sin(x)", "cos(x)", "tan(x)", "x^2", "x^3", "sqrt(x)", + "abs(x)", "log(x)", "ln(x)", "exp(x)", "1/x", + "sin(x)*cos(x)", "x*sin(x)", "floor(x)", "ceil(x)" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String expr : EXPRESSIONS) { + if (results.size() >= limit) break; + if (partial.isEmpty() || expr.toLowerCase().contains(partial)) { + results.add(new TextCandidate(expr)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java new file mode 100644 index 00000000..52140017 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java @@ -0,0 +1,32 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests format patterns for <ItemImage format>. */ +public class FormatPatternProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("ItemImage", "format")); + + private static final String[] PATTERNS = { + "%s", "%d", "%.1f", "%.2f", "%s items", "x%d", "%d / %d" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String p : PATTERNS) { + if (results.size() >= limit) break; + if (partial.isEmpty() || p.toLowerCase().contains(partial)) { + results.add(new TextCandidate(p)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java new file mode 100644 index 00000000..91733fcf --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java @@ -0,0 +1,31 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import net.minecraft.client.Minecraft; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests key binding descriptions for <KeyBind id> attributes. */ +public class KeyBindProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("KeyBind", "id")); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (net.minecraft.client.settings.KeyBinding kb : Minecraft.getMinecraft().gameSettings.keyBindings) { + if (results.size() >= limit) break; + String desc = kb.getKeyDescription(); + if (partial.isEmpty() || desc.toLowerCase().contains(partial)) { + results.add(new TextCandidate(desc)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java new file mode 100644 index 00000000..c2aec2f5 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java @@ -0,0 +1,48 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests common numeric values for INT/FLOAT attributes. */ +public class NumericValueProvider implements AutocompleteProvider { + + private static final Map SUGGESTIONS = new LinkedHashMap<>(); + static { + SUGGESTIONS.put("width", new String[]{"100", "200", "256", "300", "400", "512"}); + SUGGESTIONS.put("height", new String[]{"100", "200", "256", "300", "400"}); + SUGGESTIONS.put("scale", new String[]{"0.5", "1.0", "1.5", "2.0", "3.0"}); + SUGGESTIONS.put("zoom", new String[]{"0.5", "1.0", "1.5", "2.0", "3.0"}); + SUGGESTIONS.put("gap", new String[]{"0", "5", "10", "15", "20"}); + SUGGESTIONS.put("x", new String[]{"0"}); + SUGGESTIONS.put("y", new String[]{"0"}); + SUGGESTIONS.put("z", new String[]{"0"}); + SUGGESTIONS.put("startAngle", new String[]{"-90", "0", "90", "180"}); + SUGGESTIONS.put("barWidthRatio", new String[]{"0.5", "0.7", "0.9"}); + } + + @Override + public Set getSupportedKeys() { + Set keys = new HashSet<>(); + for (String attr : SUGGESTIONS.keySet()) { + keys.add(AutocompleteKey.forValue("*", attr)); + } + return Collections.unmodifiableSet(keys); + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText(); + List results = new ArrayList<>(); + // Generic — return all suggestions for any recognized attr + for (String[] vals : SUGGESTIONS.values()) { + for (String v : vals) { + if (results.size() >= limit) break; + if (partial.isEmpty() || v.startsWith(partial)) { + results.add(new TextCandidate(v)); + } + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java new file mode 100644 index 00000000..3c763b14 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java @@ -0,0 +1,30 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import net.minecraftforge.oredict.OreDictionary; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests OreDictionary names for "ore" attributes. */ +public class OreDictProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("*", "ore")); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String name : OreDictionary.getOreNames()) { + if (results.size() >= limit) break; + if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + results.add(new TextCandidate(name)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java new file mode 100644 index 00000000..6b1d90f1 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java @@ -0,0 +1,29 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests guide page paths for href, linksTo, and SubPages id attributes. + * Requires a PageCollection reference — for now returns empty; enable after wiring. */ +public class PageReferenceProvider implements AutocompleteProvider { + + private static final Set KEYS = buildKeys(); + + private static Set buildKeys() { + Set keys = new HashSet<>(); + keys.add(AutocompleteKey.forValue("a", "href")); + keys.add(AutocompleteKey.forValue("SubPages", "id")); + keys.add(AutocompleteKey.forValue("ItemLink", "linksTo")); + return Collections.unmodifiableSet(keys); + } + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + // TODO: wire PageCollection reference to enumerate page paths + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java new file mode 100644 index 00000000..31448555 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java @@ -0,0 +1,40 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests item IDs for Recipe input/output filters, with DNF format hint. */ +public class RecipeFilterProvider implements AutocompleteProvider { + + private static final Set KEYS = new HashSet<>(Arrays.asList( + AutocompleteKey.forValue("Recipe", "input"), + AutocompleteKey.forValue("Recipe", "output"), + AutocompleteKey.forValue("RecipesFor", "input"), + AutocompleteKey.forValue("RecipesFor", "output") + )); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (Object obj : Item.itemRegistry.getKeys()) { + if (results.size() >= limit) break; + if (obj instanceof String key) { + if (partial.isEmpty() || key.toLowerCase().contains(partial)) { + Item item = (Item) Item.itemRegistry.getObject(key); + if (item != null) { + results.add(new ItemCandidate(key, new ItemStack(item))); + } + } + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java index d29eddb6..9b2247e8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java @@ -1,32 +1,73 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.item.ItemStack; -/** Candidate displaying a registry key with optional subtitle. */ +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +/** Candidate displaying a registry key with optional item icon and subtitle. */ public class RegistryCandidate implements AutocompleteCandidate { private final String key; + @Nullable private final String subtitle; + @Nullable + private final ItemStack icon; + private static final int ICON_SIZE = 16; + private static final int TEXT_X = ICON_SIZE + 2; private static final int TEXT_COLOR = 0xFFF0F0F0; private static final int SUBTITLE_COLOR = 0xFFA0A0A0; + private static final RenderItem renderItem = new RenderItem(); public RegistryCandidate(String key) { - this(key, null); + this(key, null, null); + } + + public RegistryCandidate(String key, @Nullable String subtitle) { + this(key, subtitle, null); } - public RegistryCandidate(String key, String subtitle) { + public RegistryCandidate(String key, @Nullable ItemStack icon) { + this(key, null, icon); + } + + public RegistryCandidate(String key, @Nullable String subtitle, @Nullable ItemStack icon) { this.key = key; this.subtitle = subtitle; + this.icon = icon; } @Override public String displayText() { return key; } @Override public String replacementText() { return key; } - @Override public int renderHeight() { return subtitle != null ? 28 : 14; } + @Override public int renderHeight() { return subtitle != null ? 28 : 16; } + @Override public int renderWidth(FontRenderer fr) { return icon != null ? ICON_SIZE + 4 : 0; } @Override public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { - fontRenderer.drawString(key, x, y + 2, TEXT_COLOR); + int textX = x; + if (icon != null) { + GL11.glPushMatrix(); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + renderItem.zLevel = 0; + renderItem.renderItemAndEffectIntoGUI( + Minecraft.getMinecraft().fontRenderer, + Minecraft.getMinecraft().getTextureManager(), + icon, x, y - 1); + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + textX = x + TEXT_X; + } + fontRenderer.drawString(key, textX, y + 3, TEXT_COLOR); if (subtitle != null) { - fontRenderer.drawString(subtitle, x + 4, y + 14, SUBTITLE_COLOR); + fontRenderer.drawString(subtitle, textX + 4, y + 14, SUBTITLE_COLOR); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java new file mode 100644 index 00000000..c256cd75 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java @@ -0,0 +1,52 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests MDX tag names when cursor is right after '<'. */ +public class TagNameProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forTag()); + + // Grouped by category for future grouping in UI + private static final String[] TAG_NAMES = { + // Inline/Flow + "a", "br", "Tooltip", "ItemImage", "ItemLink", "BlockImage", "Color", + "CommandLink", "kbd", "KeyBind", "Latex", "mark", "PlayerName", + "sub", "sup", "FloatingImage", + // Block/Container + "Row", "Column", "div", "details", "CategoryIndex", "CsvTable", + "FileTree", "FootnoteList", "ItemGrid", "Mermaid", + "Recipe", "RecipeFor", "RecipesFor", "Structure", "SubPages", + // Charts + "ColumnChart", "BarChart", "LineChart", "PieChart", "ScatterChart", + // Math + "FunctionGraph", "Function", + // Scene + "GameScene", "Scene", "Block", "Entity", "PlaceBlock", + "ReplaceBlock", "RemoveBlocks", "ImportStructure", "ImportStructureLib", + "ImportPonder", "IsometricCamera", + // Annotations + "BlockAnnotation", "BoxAnnotation", "LineAnnotation", + "DiamondAnnotation", "TextAnnotation", "BlockAnnotationTemplate" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText(); + String lower = partial != null ? partial.toLowerCase() : ""; + List results = new ArrayList<>(); + for (String name : TAG_NAMES) { + if (results.size() >= limit) break; + if (lower.isEmpty() || name.toLowerCase().startsWith(lower)) { + results.add(new TextCandidate(name)); + } + } + return results; + } +} From 3601a4356d79e17603c53f37df14d68eb2832050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 02:10:09 +0800 Subject: [PATCH 011/136] feat(autocomplete): add frontmatter, markdown, and structural hint providers - FrontmatterKeyProvider and FrontmatterValueProvider - MarkdownInlineProvider, MarkdownBlockProvider, FencedBlockLanguageProvider (stubs) - NbtProvider, Vector3Provider, DataProvider for structural template hints - Add autocomplete-test.md guide page for manual testing --- .../com/hfstudio/guidenh/ClientProxy.java | 16 ++++++++ .../autocomplete/provider/DataProvider.java | 20 ++++++++++ .../provider/FencedBlockLanguageProvider.java | 25 +++++++++++++ .../provider/FrontmatterKeyProvider.java | 35 ++++++++++++++++++ .../provider/FrontmatterValueProvider.java | 37 +++++++++++++++++++ .../provider/MarkdownBlockProvider.java | 20 ++++++++++ .../provider/MarkdownInlineProvider.java | 25 +++++++++++++ .../autocomplete/provider/NbtProvider.java | 24 ++++++++++++ .../provider/Vector3Provider.java | 35 ++++++++++++++++++ 9 files changed, 237 insertions(+) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index e053fe4f..84965bb6 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -29,19 +29,27 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BlockIdProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BooleanProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ColorProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DataProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.CommandProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DomainProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EntityNameProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EnumValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ExpressionProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FencedBlockLanguageProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FormatPatternProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterKeyProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ItemIdProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.KeyBindProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.MarkdownBlockProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.MarkdownInlineProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.NbtProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.NumericValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.OreDictProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.RecipeFilterProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.TagNameProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.Vector3Provider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.structurelibexport.StructureExportBootstrap; @@ -95,6 +103,14 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new FormatPatternProvider()); AutocompleteProviders.register(new RecipeFilterProvider()); AutocompleteProviders.register(new TagNameProvider()); + AutocompleteProviders.register(new FrontmatterKeyProvider()); + AutocompleteProviders.register(new FrontmatterValueProvider()); + AutocompleteProviders.register(new MarkdownInlineProvider()); + AutocompleteProviders.register(new MarkdownBlockProvider()); + AutocompleteProviders.register(new FencedBlockLanguageProvider()); + AutocompleteProviders.register(new NbtProvider()); + AutocompleteProviders.register(new Vector3Provider()); + AutocompleteProviders.register(new DataProvider()); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java new file mode 100644 index 00000000..bb064539 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java @@ -0,0 +1,20 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests empty braces for Entity data attributes. */ +public class DataProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("Entity", "data")); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + return Collections.singletonList(new TextCandidate("{}")); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java new file mode 100644 index 00000000..b7ea2865 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests language identifiers after ``` for fenced code blocks. */ +public class FencedBlockLanguageProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forTag()); + + private static final String[] LANGUAGES = { + "java", "python", "javascript", "json", "yaml", "xml", + "sh", "bash", "text", "funcgraph", "mermaid" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + return Collections.emptyList(); // TODO: activate when ``` context is detected + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java new file mode 100644 index 00000000..0531469a --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java @@ -0,0 +1,35 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext; + +/** Suggests valid YAML frontmatter top-level keys. */ +public class FrontmatterKeyProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("*", "fm_key")); + + private static final String[] KEYS_LIST = { + "navigation", "item_ids", "ore_ids", "quest_ids", + "authors", "author", "date", "updated", "zoom" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof FrontmatterContext)) return Collections.emptyList(); + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String key : KEYS_LIST) { + if (results.size() >= limit) break; + if (partial.isEmpty() || key.toLowerCase().contains(partial)) { + results.add(new TextCandidate(key)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java new file mode 100644 index 00000000..8e12b48b --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java @@ -0,0 +1,37 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext; + +/** Dispatches frontmatter value completion based on the current key. + * For now returns empty — full dispatch requires wiring to other providers. */ +public class FrontmatterValueProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forValue("*", "fm_value")); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof FrontmatterContext)) return Collections.emptyList(); + FrontmatterContext fmc = (FrontmatterContext) ctx; + String key = fmc.getKey(); + String partial = fmc.getPartialText().toLowerCase(); + + // TODO: dispatch by key to specialized providers + // "parent" → PageReferenceProvider + // "icon", "item_ids" → ItemIdProvider + // "ore_ids" → OreDictProvider + // "quest_ids" → QuestIdProvider + // "position" → numeric values + + if ("navigation".equals(key) || "authors".equals(key) || "author".equals(key)) { + return Collections.emptyList(); // navigation is a map, authors need names + } + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java new file mode 100644 index 00000000..c8761d54 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java @@ -0,0 +1,20 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests block-level markdown syntax templates. Stub — currently no trigger context wired. */ +public class MarkdownBlockProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forTag()); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + return Collections.emptyList(); // TODO: activate when line-start context triggers + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java new file mode 100644 index 00000000..6cac30ae --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests inline markdown syntax templates. Stub — currently no trigger context wired. */ +public class MarkdownInlineProvider implements AutocompleteProvider { + + private static final Set KEYS = + Collections.singleton(AutocompleteKey.forTag()); + + private static final String[] TEMPLATES = { + "**bold**", "*italic*", "~~strikethrough~~", "==highlight==", + "++underline++", "[link](url)", "![image](url)", "`code`" + }; + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + return Collections.emptyList(); // TODO: activate when WORD context triggers markdown mode + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java new file mode 100644 index 00000000..dbb7450d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java @@ -0,0 +1,24 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests SNBT brace template for NBT-typed attributes. */ +public class NbtProvider implements AutocompleteProvider { + + private static final Set KEYS = new HashSet<>(Arrays.asList( + AutocompleteKey.forValue("Block", "nbt"), + AutocompleteKey.forValue("PlaceBlock", "nbt"), + AutocompleteKey.forValue("ReplaceBlock", "from_nbt"), + AutocompleteKey.forValue("ReplaceBlock", "to_nbt") + )); + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + return Collections.singletonList(new TextCandidate("{}")); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java new file mode 100644 index 00000000..ad03ca1b --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java @@ -0,0 +1,35 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.*; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** Suggests vector format templates for pos/from/to/min/max attributes. */ +public class Vector3Provider implements AutocompleteProvider { + + private static final Set KEYS = buildKeys(); + + private static Set buildKeys() { + Set keys = new HashSet<>(); + String[] tags = {"Block", "BlockAnnotation", "BoxAnnotation", + "DiamondAnnotation", "LineAnnotation", "TextAnnotation", + "PlaceBlock", "ReplaceBlock", "ImportStructure", "ImportStructureLib"}; + String[] attrs = {"pos", "from", "to", "min", "max", "offsetX", "offsetY", "offsetZ", + "centerX", "centerY", "centerZ", "headRotation", "leftArmRotation", "rightArmRotation", + "leftLegRotation", "rightLegRotation", "capeRotation"}; + for (String tag : tags) { + for (String attr : attrs) { + keys.add(AutocompleteKey.forValue(tag, attr)); + } + } + return Collections.unmodifiableSet(keys); + } + + @Override + public Set getSupportedKeys() { return KEYS; } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + return Collections.singletonList(new TextCandidate("0 0 0")); + } +} From ed6be020eaf52976fcb37cc0642f30553e0ee92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 02:57:15 +0800 Subject: [PATCH 012/136] refactor(autocomplete): remove 6 unnecessary providers, merge recipe filter, fix registry gaps - Remove Boolean, Vector3, Nbt, Data, MarkdownInline, MarkdownBlock providers - Merge RecipeFilterProvider into ItemIdProvider (add input/output keys) - Simplify ColorProvider to symbolic names only - Add chart tags (Bar/Column/Line/Scatter/PieChart) to TagAttributeRegistry - Add chart child tags (Series, LineSeries, Slice, PieInset) - Add FunctionGraph child tags (Plot, Point) - Extend GameScene/Scene with camera attributes - Extend Function/FunctionGraph with container attributes - Extend Entity with rotation attributes - Extend Latex with sourceScale, showTooltip, offset - Extend ImportStructure, PlaceBlock, ReplaceBlock with bounds attrs - Add Block scene element tag --- .../com/hfstudio/guidenh/ClientProxy.java | 14 - .../autocomplete/TagAttributeRegistry.java | 430 +++++++++++++++--- .../provider/AutocompleteProviders.java | 3 +- .../provider/BooleanProvider.java | 30 -- .../autocomplete/provider/ColorProvider.java | 23 - .../autocomplete/provider/DataProvider.java | 20 - .../autocomplete/provider/ItemIdProvider.java | 19 +- .../provider/MarkdownBlockProvider.java | 20 - .../provider/MarkdownInlineProvider.java | 25 - .../autocomplete/provider/NbtProvider.java | 24 - .../provider/RecipeFilterProvider.java | 40 -- .../provider/Vector3Provider.java | 35 -- .../resolver/FrontmatterContext.java | 5 +- .../resolver/FrontmatterResolver.java | 4 +- 14 files changed, 401 insertions(+), 291 deletions(-) delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 84965bb6..57bfcf7e 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -27,9 +27,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BlockIdProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BooleanProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ColorProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DataProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.CommandProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DomainProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EntityNameProvider; @@ -41,15 +39,10 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ItemIdProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.KeyBindProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.MarkdownBlockProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.MarkdownInlineProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.NbtProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.NumericValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.OreDictProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.RecipeFilterProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.TagNameProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.Vector3Provider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.structurelibexport.StructureExportBootstrap; @@ -87,7 +80,6 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new ItemIdProvider()); TagAttributeRegistry.initialize(); AutocompleteProviders.register(new AttributeNameProvider()); - AutocompleteProviders.register(new BooleanProvider()); AutocompleteProviders.register(new EnumValueProvider()); AutocompleteProviders.register(new ColorProvider()); AutocompleteProviders.register(new OreDictProvider()); @@ -101,16 +93,10 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new ExpressionProvider()); AutocompleteProviders.register(new DomainProvider()); AutocompleteProviders.register(new FormatPatternProvider()); - AutocompleteProviders.register(new RecipeFilterProvider()); AutocompleteProviders.register(new TagNameProvider()); AutocompleteProviders.register(new FrontmatterKeyProvider()); AutocompleteProviders.register(new FrontmatterValueProvider()); - AutocompleteProviders.register(new MarkdownInlineProvider()); - AutocompleteProviders.register(new MarkdownBlockProvider()); AutocompleteProviders.register(new FencedBlockLanguageProvider()); - AutocompleteProviders.register(new NbtProvider()); - AutocompleteProviders.register(new Vector3Provider()); - AutocompleteProviders.register(new DataProvider()); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 2f3e4fc6..98401422 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -59,17 +59,6 @@ public static void initialize() { register("Color", new AttributeSpec("id", AttrType.COLOR), new AttributeSpec("color", AttrType.COLOR)); - register("Entity", - new AttributeSpec("id", AttrType.ENTITY_ID), - new AttributeSpec("data", AttrType.SNBT), - new AttributeSpec("name", AttrType.STRING), - new AttributeSpec("uuid", AttrType.STRING), - new AttributeSpec("showName", AttrType.BOOLEAN), - new AttributeSpec("showCape", AttrType.BOOLEAN), - new AttributeSpec("baby", AttrType.BOOLEAN), - new AttributeSpec("x", AttrType.FLOAT), - new AttributeSpec("y", AttrType.FLOAT), - new AttributeSpec("z", AttrType.FLOAT)); register("KeyBind", new AttributeSpec("id", AttrType.KEY_BIND), new AttributeSpec("action", AttrType.STRING)); @@ -112,13 +101,6 @@ public static void initialize() { register("Structure", new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT)); - register("GameScene", - new AttributeSpec("width", AttrType.INT), - new AttributeSpec("height", AttrType.INT), - new AttributeSpec("zoom", AttrType.FLOAT), - new AttributeSpec("perspective", AttrType.STRING), - new AttributeSpec("interactive", AttrType.BOOLEAN), - new AttributeSpec("showGrid", AttrType.BOOLEAN)); register("Mermaid", new AttributeSpec("src", AttrType.FILE_PATH), new AttributeSpec("width", AttrType.INT), @@ -145,67 +127,407 @@ public static void initialize() { new AttributeSpec("title", AttrType.STRING)); register("br", new AttributeSpec("clear", AttrType.STRING)); - register("ImportStructure", - new AttributeSpec("src", AttrType.FILE_PATH), - new AttributeSpec("offsetX", AttrType.FLOAT), - new AttributeSpec("offsetY", AttrType.FLOAT), - new AttributeSpec("offsetZ", AttrType.FLOAT)); - register("ImportStructureLib", - new AttributeSpec("controller", AttrType.STRING), - new AttributeSpec("piece", AttrType.STRING), - new AttributeSpec("channel", AttrType.STRING), - new AttributeSpec("facing", AttrType.STRING), - new AttributeSpec("rotation", AttrType.STRING), - new AttributeSpec("flip", AttrType.STRING)); register("ImportPonder", new AttributeSpec("src", AttrType.FILE_PATH)); - register("PlaceBlock", - new AttributeSpec("id", AttrType.BLOCK_ID), - new AttributeSpec("nbt", AttrType.SNBT), - new AttributeSpec("x", AttrType.INT), - new AttributeSpec("y", AttrType.INT), - new AttributeSpec("z", AttrType.INT)); register("RemoveBlocks", new AttributeSpec("id", AttrType.BLOCK_ID)); - register("ReplaceBlock", - new AttributeSpec("from", AttrType.BLOCK_ID), - new AttributeSpec("to", AttrType.BLOCK_ID), - new AttributeSpec("from_nbt", AttrType.SNBT), - new AttributeSpec("to_nbt", AttrType.SNBT)); register("IsometricCamera", new AttributeSpec("yaw", AttrType.FLOAT), new AttributeSpec("pitch", AttrType.FLOAT), new AttributeSpec("roll", AttrType.FLOAT)); + register("Tooltip", + new AttributeSpec("label", AttrType.STRING)); + register("mark", + new AttributeSpec("color", AttrType.COLOR)); + register("FileTree", + new AttributeSpec("indent", AttrType.INT), + new AttributeSpec("gap", AttrType.INT)); + register("ItemGrid"); // no attributes - uses child elements + register("FootnoteList", + new AttributeSpec("width", AttrType.INT)); + + // === Charts (all five types share CommonChartAttrs) === + register("BarChart", + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("background", AttrType.COLOR), + new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("titleColor", AttrType.COLOR), + new AttributeSpec("labelColor", AttrType.COLOR), + new AttributeSpec("legend", AttrType.STRING), + new AttributeSpec("labelPosition", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegendWidth", AttrType.INT), + new AttributeSpec("cornerLegendHeight", AttrType.INT), + new AttributeSpec("cornerLegendBackground", AttrType.COLOR), + new AttributeSpec("categories", AttrType.STRING), + new AttributeSpec("xAxisLabel", AttrType.STRING), + new AttributeSpec("yAxisLabel", AttrType.STRING), + new AttributeSpec("xAxisMin", AttrType.FLOAT), + new AttributeSpec("xAxisMax", AttrType.FLOAT), + new AttributeSpec("yAxisMin", AttrType.FLOAT), + new AttributeSpec("yAxisMax", AttrType.FLOAT), + new AttributeSpec("xAxisStep", AttrType.FLOAT), + new AttributeSpec("yAxisStep", AttrType.FLOAT), + new AttributeSpec("xAxisUnit", AttrType.STRING), + new AttributeSpec("yAxisUnit", AttrType.STRING), + new AttributeSpec("xAxisTickFormat", AttrType.STRING), + new AttributeSpec("yAxisTickFormat", AttrType.STRING), + new AttributeSpec("showXGrid", AttrType.BOOLEAN), + new AttributeSpec("showYGrid", AttrType.BOOLEAN), + new AttributeSpec("xGridColor", AttrType.COLOR), + new AttributeSpec("yGridColor", AttrType.COLOR), + new AttributeSpec("barWidthRatio", AttrType.FLOAT)); + register("ColumnChart", + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("background", AttrType.COLOR), + new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("titleColor", AttrType.COLOR), + new AttributeSpec("labelColor", AttrType.COLOR), + new AttributeSpec("legend", AttrType.STRING), + new AttributeSpec("labelPosition", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegendWidth", AttrType.INT), + new AttributeSpec("cornerLegendHeight", AttrType.INT), + new AttributeSpec("cornerLegendBackground", AttrType.COLOR), + new AttributeSpec("categories", AttrType.STRING), + new AttributeSpec("xAxisLabel", AttrType.STRING), + new AttributeSpec("yAxisLabel", AttrType.STRING), + new AttributeSpec("xAxisMin", AttrType.FLOAT), + new AttributeSpec("xAxisMax", AttrType.FLOAT), + new AttributeSpec("yAxisMin", AttrType.FLOAT), + new AttributeSpec("yAxisMax", AttrType.FLOAT), + new AttributeSpec("xAxisStep", AttrType.FLOAT), + new AttributeSpec("yAxisStep", AttrType.FLOAT), + new AttributeSpec("xAxisUnit", AttrType.STRING), + new AttributeSpec("yAxisUnit", AttrType.STRING), + new AttributeSpec("xAxisTickFormat", AttrType.STRING), + new AttributeSpec("yAxisTickFormat", AttrType.STRING), + new AttributeSpec("showXGrid", AttrType.BOOLEAN), + new AttributeSpec("showYGrid", AttrType.BOOLEAN), + new AttributeSpec("xGridColor", AttrType.COLOR), + new AttributeSpec("yGridColor", AttrType.COLOR), + new AttributeSpec("barWidthRatio", AttrType.FLOAT)); + register("LineChart", + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("background", AttrType.COLOR), + new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("titleColor", AttrType.COLOR), + new AttributeSpec("labelColor", AttrType.COLOR), + new AttributeSpec("legend", AttrType.STRING), + new AttributeSpec("labelPosition", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegendWidth", AttrType.INT), + new AttributeSpec("cornerLegendHeight", AttrType.INT), + new AttributeSpec("cornerLegendBackground", AttrType.COLOR), + new AttributeSpec("categories", AttrType.STRING), + new AttributeSpec("xAxisLabel", AttrType.STRING), + new AttributeSpec("yAxisLabel", AttrType.STRING), + new AttributeSpec("xAxisMin", AttrType.FLOAT), + new AttributeSpec("xAxisMax", AttrType.FLOAT), + new AttributeSpec("yAxisMin", AttrType.FLOAT), + new AttributeSpec("yAxisMax", AttrType.FLOAT), + new AttributeSpec("xAxisStep", AttrType.FLOAT), + new AttributeSpec("yAxisStep", AttrType.FLOAT), + new AttributeSpec("xAxisUnit", AttrType.STRING), + new AttributeSpec("yAxisUnit", AttrType.STRING), + new AttributeSpec("xAxisTickFormat", AttrType.STRING), + new AttributeSpec("yAxisTickFormat", AttrType.STRING), + new AttributeSpec("showXGrid", AttrType.BOOLEAN), + new AttributeSpec("showYGrid", AttrType.BOOLEAN), + new AttributeSpec("xGridColor", AttrType.COLOR), + new AttributeSpec("yGridColor", AttrType.COLOR), + new AttributeSpec("numericX", AttrType.BOOLEAN), + new AttributeSpec("showPoints", AttrType.BOOLEAN)); + register("ScatterChart", + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("background", AttrType.COLOR), + new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("titleColor", AttrType.COLOR), + new AttributeSpec("labelColor", AttrType.COLOR), + new AttributeSpec("legend", AttrType.STRING), + new AttributeSpec("labelPosition", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegendWidth", AttrType.INT), + new AttributeSpec("cornerLegendHeight", AttrType.INT), + new AttributeSpec("cornerLegendBackground", AttrType.COLOR), + new AttributeSpec("xAxisLabel", AttrType.STRING), + new AttributeSpec("yAxisLabel", AttrType.STRING), + new AttributeSpec("xAxisMin", AttrType.FLOAT), + new AttributeSpec("xAxisMax", AttrType.FLOAT), + new AttributeSpec("yAxisMin", AttrType.FLOAT), + new AttributeSpec("yAxisMax", AttrType.FLOAT), + new AttributeSpec("xAxisStep", AttrType.FLOAT), + new AttributeSpec("yAxisStep", AttrType.FLOAT), + new AttributeSpec("xAxisUnit", AttrType.STRING), + new AttributeSpec("yAxisUnit", AttrType.STRING), + new AttributeSpec("xAxisTickFormat", AttrType.STRING), + new AttributeSpec("yAxisTickFormat", AttrType.STRING), + new AttributeSpec("showXGrid", AttrType.BOOLEAN), + new AttributeSpec("showYGrid", AttrType.BOOLEAN), + new AttributeSpec("xGridColor", AttrType.COLOR), + new AttributeSpec("yGridColor", AttrType.COLOR)); + register("PieChart", + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("background", AttrType.COLOR), + new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("titleColor", AttrType.COLOR), + new AttributeSpec("labelColor", AttrType.COLOR), + new AttributeSpec("legend", AttrType.STRING), + new AttributeSpec("labelPosition", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegendWidth", AttrType.INT), + new AttributeSpec("cornerLegendHeight", AttrType.INT), + new AttributeSpec("cornerLegendBackground", AttrType.COLOR), + new AttributeSpec("startAngle", AttrType.FLOAT), + new AttributeSpec("clockwise", AttrType.BOOLEAN)); + + // === Chart child tags === + register("Series", + new AttributeSpec("name", AttrType.STRING), + new AttributeSpec("data", AttrType.STRING), + new AttributeSpec("points", AttrType.STRING), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("icon", AttrType.ITEM_ID), + new AttributeSpec("iconImage", AttrType.FILE_PATH), + new AttributeSpec("tooltip", AttrType.STRING)); + register("LineSeries", + new AttributeSpec("name", AttrType.STRING), + new AttributeSpec("data", AttrType.STRING), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("icon", AttrType.ITEM_ID), + new AttributeSpec("iconImage", AttrType.FILE_PATH), + new AttributeSpec("tooltip", AttrType.STRING)); + register("Slice", + new AttributeSpec("label", AttrType.STRING), + new AttributeSpec("value", AttrType.FLOAT), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("icon", AttrType.ITEM_ID), + new AttributeSpec("iconImage", AttrType.FILE_PATH), + new AttributeSpec("tooltip", AttrType.STRING)); + register("PieInset", + new AttributeSpec("size", AttrType.FLOAT), + new AttributeSpec("position", AttrType.STRING), + new AttributeSpec("startAngleDeg", AttrType.FLOAT), + new AttributeSpec("direction", AttrType.STRING), + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("titleColor", AttrType.COLOR)); + + // === FunctionGraph child tags === + register("Plot", + new AttributeSpec("expr", AttrType.EXPRESSION), + new AttributeSpec("inverse", AttrType.BOOLEAN), + new AttributeSpec("domain", AttrType.DOMAIN), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("label", AttrType.STRING), + new AttributeSpec("pointEveryX", AttrType.FLOAT), + new AttributeSpec("pointEveryY", AttrType.FLOAT), + new AttributeSpec("autoPointLabel", AttrType.STRING), + new AttributeSpec("autoPointColor", AttrType.COLOR)); + register("Point", + new AttributeSpec("x", AttrType.FLOAT), + new AttributeSpec("y", AttrType.FLOAT), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("label", AttrType.STRING), + new AttributeSpec("plot", AttrType.INT), + new AttributeSpec("atX", AttrType.FLOAT), + new AttributeSpec("atY", AttrType.FLOAT)); + + // === Fix and extend existing registrations === + + // GameScene: add camera attributes + register("Scene", + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("zoom", AttrType.FLOAT), + new AttributeSpec("perspective", AttrType.STRING), + new AttributeSpec("rotateX", AttrType.FLOAT), + new AttributeSpec("rotateY", AttrType.FLOAT), + new AttributeSpec("rotateZ", AttrType.FLOAT), + new AttributeSpec("offsetX", AttrType.FLOAT), + new AttributeSpec("offsetY", AttrType.FLOAT), + new AttributeSpec("centerX", AttrType.FLOAT), + new AttributeSpec("centerY", AttrType.FLOAT), + new AttributeSpec("centerZ", AttrType.FLOAT), + new AttributeSpec("interactive", AttrType.BOOLEAN), + new AttributeSpec("allowLayerSlider", AttrType.BOOLEAN), + new AttributeSpec("gridButtonEnabled", AttrType.BOOLEAN), + new AttributeSpec("showGrid", AttrType.BOOLEAN)); + + // GameScene: also register as "GameScene" with same attrs (SceneTagCompiler handles both) + register("GameScene", + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("zoom", AttrType.FLOAT), + new AttributeSpec("perspective", AttrType.STRING), + new AttributeSpec("rotateX", AttrType.FLOAT), + new AttributeSpec("rotateY", AttrType.FLOAT), + new AttributeSpec("rotateZ", AttrType.FLOAT), + new AttributeSpec("offsetX", AttrType.FLOAT), + new AttributeSpec("offsetY", AttrType.FLOAT), + new AttributeSpec("centerX", AttrType.FLOAT), + new AttributeSpec("centerY", AttrType.FLOAT), + new AttributeSpec("centerZ", AttrType.FLOAT), + new AttributeSpec("interactive", AttrType.BOOLEAN), + new AttributeSpec("allowLayerSlider", AttrType.BOOLEAN), + new AttributeSpec("gridButtonEnabled", AttrType.BOOLEAN), + new AttributeSpec("showGrid", AttrType.BOOLEAN)); + + // Function: add missing container + plot attrs register("Function", + new AttributeSpec("title", AttrType.STRING), + new AttributeSpec("width", AttrType.INT), + new AttributeSpec("height", AttrType.INT), + new AttributeSpec("background", AttrType.COLOR), + new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("axisColor", AttrType.COLOR), + new AttributeSpec("gridColor", AttrType.COLOR), + new AttributeSpec("showGrid", AttrType.BOOLEAN), + new AttributeSpec("showAxes", AttrType.BOOLEAN), + new AttributeSpec("xMin", AttrType.FLOAT), + new AttributeSpec("xMax", AttrType.FLOAT), + new AttributeSpec("yMin", AttrType.FLOAT), + new AttributeSpec("yMax", AttrType.FLOAT), + new AttributeSpec("xStep", AttrType.FLOAT), + new AttributeSpec("yStep", AttrType.FLOAT), + new AttributeSpec("xRange", AttrType.STRING), + new AttributeSpec("yRange", AttrType.STRING), + new AttributeSpec("quadrants", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegendWidth", AttrType.INT), + new AttributeSpec("cornerLegendHeight", AttrType.INT), + new AttributeSpec("cornerLegendBackground", AttrType.COLOR), new AttributeSpec("expr", AttrType.EXPRESSION), new AttributeSpec("inverse", AttrType.BOOLEAN), new AttributeSpec("domain", AttrType.DOMAIN), new AttributeSpec("color", AttrType.COLOR), - new AttributeSpec("label", AttrType.STRING)); + new AttributeSpec("label", AttrType.STRING), + new AttributeSpec("pointEveryX", AttrType.FLOAT), + new AttributeSpec("pointEveryY", AttrType.FLOAT), + new AttributeSpec("autoPointLabel", AttrType.STRING), + new AttributeSpec("autoPointColor", AttrType.COLOR)); + + // FunctionGraph: add missing container attrs register("FunctionGraph", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), new AttributeSpec("background", AttrType.COLOR), new AttributeSpec("border", AttrType.COLOR), + new AttributeSpec("axisColor", AttrType.COLOR), + new AttributeSpec("gridColor", AttrType.COLOR), + new AttributeSpec("showGrid", AttrType.BOOLEAN), + new AttributeSpec("showAxes", AttrType.BOOLEAN), new AttributeSpec("xMin", AttrType.FLOAT), new AttributeSpec("xMax", AttrType.FLOAT), new AttributeSpec("yMin", AttrType.FLOAT), - new AttributeSpec("yMax", AttrType.FLOAT)); - register("Tooltip", - new AttributeSpec("label", AttrType.STRING)); + new AttributeSpec("yMax", AttrType.FLOAT), + new AttributeSpec("xStep", AttrType.FLOAT), + new AttributeSpec("yStep", AttrType.FLOAT), + new AttributeSpec("xRange", AttrType.STRING), + new AttributeSpec("yRange", AttrType.STRING), + new AttributeSpec("quadrants", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegendWidth", AttrType.INT), + new AttributeSpec("cornerLegendHeight", AttrType.INT), + new AttributeSpec("cornerLegendBackground", AttrType.COLOR)); + + // Entity: add missing rotation attrs + register("Entity", + new AttributeSpec("id", AttrType.ENTITY_ID), + new AttributeSpec("data", AttrType.SNBT), + new AttributeSpec("name", AttrType.STRING), + new AttributeSpec("uuid", AttrType.STRING), + new AttributeSpec("showName", AttrType.BOOLEAN), + new AttributeSpec("showCape", AttrType.BOOLEAN), + new AttributeSpec("baby", AttrType.BOOLEAN), + new AttributeSpec("x", AttrType.FLOAT), + new AttributeSpec("y", AttrType.FLOAT), + new AttributeSpec("z", AttrType.FLOAT), + new AttributeSpec("rotationY", AttrType.FLOAT), + new AttributeSpec("rotationX", AttrType.FLOAT), + new AttributeSpec("headRotation", AttrType.STRING), + new AttributeSpec("leftArmRotation", AttrType.STRING), + new AttributeSpec("rightArmRotation", AttrType.STRING), + new AttributeSpec("leftLegRotation", AttrType.STRING), + new AttributeSpec("rightLegRotation", AttrType.STRING), + new AttributeSpec("capeRotation", AttrType.STRING)); + + // Latex: add missing attrs register("Latex", new AttributeSpec("formula", AttrType.STRING), new AttributeSpec("color", AttrType.COLOR), new AttributeSpec("scale", AttrType.FLOAT), - new AttributeSpec("valign", AttrType.STRING)); - register("mark", - new AttributeSpec("color", AttrType.COLOR)); - register("FileTree", - new AttributeSpec("indent", AttrType.INT), - new AttributeSpec("gap", AttrType.INT)); - register("ItemGrid"); // no attributes - uses child elements - register("FootnoteList", - new AttributeSpec("width", AttrType.INT)); + new AttributeSpec("sourceScale", AttrType.FLOAT), + new AttributeSpec("showTooltip", AttrType.BOOLEAN), + new AttributeSpec("valign", AttrType.STRING), + new AttributeSpec("offsetX", AttrType.INT), + new AttributeSpec("offsetY", AttrType.INT)); + + // ImportStructureLib: add offset attrs + register("ImportStructureLib", + new AttributeSpec("controller", AttrType.STRING), + new AttributeSpec("piece", AttrType.STRING), + new AttributeSpec("channel", AttrType.STRING), + new AttributeSpec("facing", AttrType.STRING), + new AttributeSpec("rotation", AttrType.STRING), + new AttributeSpec("flip", AttrType.STRING), + new AttributeSpec("offsetX", AttrType.INT), + new AttributeSpec("offsetY", AttrType.INT), + new AttributeSpec("offsetZ", AttrType.INT)); + + // PlaceBlock: add dx/dy/dz + register("PlaceBlock", + new AttributeSpec("id", AttrType.BLOCK_ID), + new AttributeSpec("nbt", AttrType.SNBT), + new AttributeSpec("x", AttrType.INT), + new AttributeSpec("y", AttrType.INT), + new AttributeSpec("z", AttrType.INT), + new AttributeSpec("dx", AttrType.INT), + new AttributeSpec("dy", AttrType.INT), + new AttributeSpec("dz", AttrType.INT)); + + // ReplaceBlock: add bounds attrs + register("ReplaceBlock", + new AttributeSpec("from", AttrType.BLOCK_ID), + new AttributeSpec("to", AttrType.BLOCK_ID), + new AttributeSpec("from_nbt", AttrType.SNBT), + new AttributeSpec("to_nbt", AttrType.SNBT), + new AttributeSpec("x", AttrType.INT), + new AttributeSpec("y", AttrType.INT), + new AttributeSpec("z", AttrType.INT), + new AttributeSpec("dx", AttrType.INT), + new AttributeSpec("dy", AttrType.INT), + new AttributeSpec("dz", AttrType.INT)); + + // ImportStructure: fix types + add x/y/z + register("ImportStructure", + new AttributeSpec("src", AttrType.FILE_PATH), + new AttributeSpec("x", AttrType.INT), + new AttributeSpec("y", AttrType.INT), + new AttributeSpec("z", AttrType.INT), + new AttributeSpec("offsetX", AttrType.INT), + new AttributeSpec("offsetY", AttrType.INT), + new AttributeSpec("offsetZ", AttrType.INT)); + + // Block scene element + register("Block", + new AttributeSpec("id", AttrType.BLOCK_ID), + new AttributeSpec("ore", AttrType.ORE_DICT), + new AttributeSpec("x", AttrType.INT), + new AttributeSpec("y", AttrType.INT), + new AttributeSpec("z", AttrType.INT), + new AttributeSpec("meta", AttrType.INT), + new AttributeSpec("facing", AttrType.STRING), + new AttributeSpec("nbt", AttrType.SNBT)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java index d9dbc7e7..9c59ba0f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -46,7 +46,8 @@ private static boolean matchesContext(AutocompleteKey key, AutocompleteContext c if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) { com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext fmc = (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) ctx; - return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, "*", fmc.getKey()); + String attr = fmc.isValue() ? "fm_value" : "fm_key"; + return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, "*", attr); } return false; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java deleted file mode 100644 index c17ec395..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; - -import java.util.*; - -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; - -/** Suggests true/false for boolean-typed attributes. */ -public class BooleanProvider implements AutocompleteProvider { - - @Override - public Set getSupportedKeys() { - Set keys = new HashSet<>(); - for (String tag : TagAttributeRegistry.getRegisteredTags()) { - for (AttributeSpec spec : TagAttributeRegistry.get(tag)) { - if (spec.getType() == AttrType.BOOLEAN) { - keys.add(AutocompleteKey.forValue(tag, spec.getName())); - } - } - } - return Collections.unmodifiableSet(keys); - } - - @Override - public List provide(AutocompleteContext ctx, int limit) { - return Arrays.asList(new TextCandidate("true"), new TextCandidate("false")); - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java index f581a9cd..8822d5be 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java @@ -6,17 +6,10 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; /** Suggests hex color values and SymbolicColor names for color-typed attributes. */ public class ColorProvider implements AutocompleteProvider { - private static final String[] HEX_COLORS = { - "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", - "#FFFFFF", "#000000", "#808080", "#FFA500", "#800080", "#008080", - "#FFC0CB", "#A52A2A", "#00FF7F", "#FFD700" - }; - private static final String[] SYMBOLIC_NAMES = { "LINK", "BODY_TEXT", "ERROR_TEXT", "CRAFTING_RECIPE_TYPE", "THEMATIC_BREAK", "HEADER1_SEPARATOR", "HEADER2_SEPARATOR", "NAVBAR_BG_TOP", "NAVBAR_BG_BOTTOM", @@ -43,16 +36,8 @@ public Set getSupportedKeys() { @Override public List provide(AutocompleteContext ctx, int limit) { - if (!(ctx instanceof MdxValueContext)) return Collections.emptyList(); String partial = ctx.getPartialText().toLowerCase(); List results = new ArrayList<>(); - - for (String hex : HEX_COLORS) { - if (results.size() >= limit) break; - if (partial.isEmpty() || hex.toLowerCase().contains(partial)) { - results.add(new ColorCandidate(hex, parseHexColor(hex))); - } - } for (String name : SYMBOLIC_NAMES) { if (results.size() >= limit) break; if (partial.isEmpty() || name.toLowerCase().contains(partial)) { @@ -61,12 +46,4 @@ public List provide(AutocompleteContext ctx, int limit) { } return results; } - - private static int parseHexColor(String hex) { - try { - return (int) Long.parseLong(hex.substring(1), 16) | 0xFF000000; - } catch (NumberFormatException e) { - return 0xFFFFFFFF; - } - } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java deleted file mode 100644 index bb064539..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DataProvider.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; - -import java.util.*; - -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; - -/** Suggests empty braces for Entity data attributes. */ -public class DataProvider implements AutocompleteProvider { - - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("Entity", "data")); - - @Override - public Set getSupportedKeys() { return KEYS; } - - @Override - public List provide(AutocompleteContext ctx, int limit) { - return Collections.singletonList(new TextCandidate("{}")); - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java index ee1a493b..c9bc8f66 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java @@ -14,10 +14,25 @@ public class ItemIdProvider implements AutocompleteProvider { // Tags whose "id" attribute refers to a Minecraft item registry key - private static final Set KEYS = buildKeys( - "ItemImage", "ItemLink", "Recipe", "RecipeFor", "RecipesFor" + private static Set KEYS = buildKeys( + // item id attributes + "ItemImage", "ItemLink", "Recipe", "RecipeFor", "RecipesFor", + // recipe filter attributes + "Recipe", "RecipeFor", "RecipesFor" ); + // Also add these extra keys after buildKeys: + static { + Set allKeys = new HashSet<>(KEYS); + allKeys.add(AutocompleteKey.forValue("Recipe", "input")); + allKeys.add(AutocompleteKey.forValue("Recipe", "output")); + allKeys.add(AutocompleteKey.forValue("RecipeFor", "input")); + allKeys.add(AutocompleteKey.forValue("RecipeFor", "output")); + allKeys.add(AutocompleteKey.forValue("RecipesFor", "input")); + allKeys.add(AutocompleteKey.forValue("RecipesFor", "output")); + KEYS = Collections.unmodifiableSet(allKeys); + } + private static Set buildKeys(String... tagNames) { Set keys = new HashSet<>(); for (String tag : tagNames) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java deleted file mode 100644 index c8761d54..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownBlockProvider.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; - -import java.util.*; - -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; - -/** Suggests block-level markdown syntax templates. Stub — currently no trigger context wired. */ -public class MarkdownBlockProvider implements AutocompleteProvider { - - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forTag()); - - @Override - public Set getSupportedKeys() { return KEYS; } - - @Override - public List provide(AutocompleteContext ctx, int limit) { - return Collections.emptyList(); // TODO: activate when line-start context triggers - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java deleted file mode 100644 index 6cac30ae..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/MarkdownInlineProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; - -import java.util.*; - -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; - -/** Suggests inline markdown syntax templates. Stub — currently no trigger context wired. */ -public class MarkdownInlineProvider implements AutocompleteProvider { - - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forTag()); - - private static final String[] TEMPLATES = { - "**bold**", "*italic*", "~~strikethrough~~", "==highlight==", - "++underline++", "[link](url)", "![image](url)", "`code`" - }; - - @Override - public Set getSupportedKeys() { return KEYS; } - - @Override - public List provide(AutocompleteContext ctx, int limit) { - return Collections.emptyList(); // TODO: activate when WORD context triggers markdown mode - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java deleted file mode 100644 index dbb7450d..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NbtProvider.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; - -import java.util.*; - -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; - -/** Suggests SNBT brace template for NBT-typed attributes. */ -public class NbtProvider implements AutocompleteProvider { - - private static final Set KEYS = new HashSet<>(Arrays.asList( - AutocompleteKey.forValue("Block", "nbt"), - AutocompleteKey.forValue("PlaceBlock", "nbt"), - AutocompleteKey.forValue("ReplaceBlock", "from_nbt"), - AutocompleteKey.forValue("ReplaceBlock", "to_nbt") - )); - - @Override - public Set getSupportedKeys() { return KEYS; } - - @Override - public List provide(AutocompleteContext ctx, int limit) { - return Collections.singletonList(new TextCandidate("{}")); - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java deleted file mode 100644 index 31448555..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RecipeFilterProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; - -import java.util.*; - -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; - -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; - -/** Suggests item IDs for Recipe input/output filters, with DNF format hint. */ -public class RecipeFilterProvider implements AutocompleteProvider { - - private static final Set KEYS = new HashSet<>(Arrays.asList( - AutocompleteKey.forValue("Recipe", "input"), - AutocompleteKey.forValue("Recipe", "output"), - AutocompleteKey.forValue("RecipesFor", "input"), - AutocompleteKey.forValue("RecipesFor", "output") - )); - - @Override - public Set getSupportedKeys() { return KEYS; } - - @Override - public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); - List results = new ArrayList<>(); - for (Object obj : Item.itemRegistry.getKeys()) { - if (results.size() >= limit) break; - if (obj instanceof String key) { - if (partial.isEmpty() || key.toLowerCase().contains(partial)) { - Item item = (Item) Item.itemRegistry.getObject(key); - if (item != null) { - results.add(new ItemCandidate(key, new ItemStack(item))); - } - } - } - } - return results; - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java deleted file mode 100644 index ad03ca1b..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/Vector3Provider.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; - -import java.util.*; - -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; - -/** Suggests vector format templates for pos/from/to/min/max attributes. */ -public class Vector3Provider implements AutocompleteProvider { - - private static final Set KEYS = buildKeys(); - - private static Set buildKeys() { - Set keys = new HashSet<>(); - String[] tags = {"Block", "BlockAnnotation", "BoxAnnotation", - "DiamondAnnotation", "LineAnnotation", "TextAnnotation", - "PlaceBlock", "ReplaceBlock", "ImportStructure", "ImportStructureLib"}; - String[] attrs = {"pos", "from", "to", "min", "max", "offsetX", "offsetY", "offsetZ", - "centerX", "centerY", "centerZ", "headRotation", "leftArmRotation", "rightArmRotation", - "leftLegRotation", "rightLegRotation", "capeRotation"}; - for (String tag : tags) { - for (String attr : attrs) { - keys.add(AutocompleteKey.forValue(tag, attr)); - } - } - return Collections.unmodifiableSet(keys); - } - - @Override - public Set getSupportedKeys() { return KEYS; } - - @Override - public List provide(AutocompleteContext ctx, int limit) { - return Collections.singletonList(new TextCandidate("0 0 0")); - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java index 504ae346..fd951265 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java @@ -4,18 +4,21 @@ public final class FrontmatterContext implements AutocompleteContext { private final String key; + private final boolean isValue; private final int replaceStart; private final int replaceEnd; private final String partialText; - public FrontmatterContext(String key, int replaceStart, int replaceEnd, String partialText) { + public FrontmatterContext(String key, boolean isValue, int replaceStart, int replaceEnd, String partialText) { this.key = key; + this.isValue = isValue; this.replaceStart = replaceStart; this.replaceEnd = replaceEnd; this.partialText = partialText; } public String getKey() { return key; } + public boolean isValue() { return isValue; } @Override public int replaceStart() { return replaceStart; } @Override public int replaceEnd() { return replaceEnd; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java index 4884aead..f68e703b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java @@ -53,7 +53,7 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { if (cursorIndex >= valueAbsStart && cursorIndex <= valueAbsEnd) { String partialText = text.substring(valueAbsStart, cursorIndex); return new TextSyntaxContext(SyntaxElementType.WORD, valueAbsStart, valueAbsEnd, - new FrontmatterContext(key, valueAbsStart, valueAbsEnd, partialText)); + new FrontmatterContext(key, true, valueAbsStart, valueAbsEnd, partialText)); } // Cursor is on the key @@ -61,7 +61,7 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { int keyEnd = keyStart + key.length(); if (cursorIndex >= keyStart && cursorIndex <= keyEnd) { return new TextSyntaxContext(SyntaxElementType.WORD, keyStart, keyEnd, - new FrontmatterContext(key, keyStart, keyEnd, + new FrontmatterContext(key, false, keyStart, keyEnd, text.substring(keyStart, cursorIndex))); } From 05dc61e841d7b8b8063aa5cd247e879fac3b3c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 12:54:35 +0800 Subject: [PATCH 013/136] feat(autocomplete): implement PageReference, Anchor, FrontmatterValue, FencedBlockLanguage providers - FencedBlockLanguageProvider: return language list filtered by partial text - PageReferenceProvider: static setPages() wired from GuideScreen, suggests page paths - AnchorProvider: static setDocumentText() wired from GuideScreen, parses headings - FrontmatterValueProvider: contextual hints per frontmatter key --- .../guidenh/guide/internal/GuideScreen.java | 13 ++++++ .../autocomplete/provider/AnchorProvider.java | 32 +++++++++++++-- .../provider/FencedBlockLanguageProvider.java | 10 ++++- .../provider/FrontmatterValueProvider.java | 41 ++++++++++++------- .../provider/PageReferenceProvider.java | 24 +++++++++-- 5 files changed, 97 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 3dc11d8c..92cbf30f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -46,6 +46,8 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxSyntaxResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.WordBoundaryResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui.AutocompletePopup; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; @@ -493,6 +495,17 @@ public void updateScreen() { pendingItemLinksStack = null; } if (isGuideEditorActive()) { + // Update autocomplete data sources + if (guide != null) { + List pagePaths = new ArrayList<>(); + for (ParsedGuidePage page : guide.getPages()) { + pagePaths.add(page.getId().toString()); + } + PageReferenceProvider.setPages(pagePaths); + } + if (guideEditorTextArea != null) { + AnchorProvider.setDocumentText(guideEditorTextArea.getText()); + } performAutocompleteCheck(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java index 13fd2f37..8a558bb3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java @@ -1,16 +1,28 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; import java.util.*; +import java.util.regex.*; + +import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -/** Suggests heading anchors for <a href="#..."> attributes. - * Requires current document context — for now returns empty; enable after wiring. */ +/** Suggests heading anchors from the current document for href="#..." attributes. */ public class AnchorProvider implements AutocompleteProvider { private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("a", "href")); + private static final Pattern HEADING = Pattern.compile("^#{1,6}\\s+(.+)$", Pattern.MULTILINE); + + @Nullable + private static volatile String documentText; + + /** Update the document text for heading extraction. */ + public static void setDocumentText(@Nullable String text) { + documentText = text; + } + @Override public Set getSupportedKeys() { return KEYS; } @@ -18,7 +30,19 @@ public class AnchorProvider implements AutocompleteProvider { public List provide(AutocompleteContext ctx, int limit) { String partial = ctx.getPartialText(); if (partial == null || !partial.startsWith("#")) return Collections.emptyList(); - // TODO: parse current document for headings and anchor names - return Collections.emptyList(); + String query = partial.substring(1).toLowerCase(); // strip leading # + + if (documentText == null) return Collections.emptyList(); + List results = new ArrayList<>(); + Matcher m = HEADING.matcher(documentText); + while (m.find()) { + if (results.size() >= limit) break; + String heading = m.group(1).trim(); + String anchor = heading.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", ""); + if (query.isEmpty() || anchor.contains(query)) { + results.add(new TextCandidate("#" + anchor + " (" + heading + ")")); + } + } + return results; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java index b7ea2865..80ac6628 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java @@ -20,6 +20,14 @@ public class FencedBlockLanguageProvider implements AutocompleteProvider { @Override public List provide(AutocompleteContext ctx, int limit) { - return Collections.emptyList(); // TODO: activate when ``` context is detected + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String lang : LANGUAGES) { + if (results.size() >= limit) break; + if (partial.isEmpty() || lang.toLowerCase().startsWith(partial)) { + results.add(new TextCandidate(lang)); + } + } + return results; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java index 8e12b48b..6d349057 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java @@ -5,13 +5,30 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext; -/** Dispatches frontmatter value completion based on the current key. - * For now returns empty — full dispatch requires wiring to other providers. */ +/** Dispatches frontmatter value completion based on the current key. */ public class FrontmatterValueProvider implements AutocompleteProvider { private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("*", "fm_value")); + private static final Map HINTS = new LinkedHashMap<>(); + static { + HINTS.put("navigation", new String[]{" title:", " parent:", " position:", " icon:", " icon_texture:"}); + HINTS.put("title", new String[]{"\"Page Title\""}); + HINTS.put("parent", new String[]{}); + HINTS.put("position", new String[]{"0"}); + HINTS.put("icon", new String[]{}); + HINTS.put("icon_texture", new String[]{}); + HINTS.put("item_ids", new String[]{}); + HINTS.put("ore_ids", new String[]{}); + HINTS.put("quest_ids", new String[]{}); + HINTS.put("authors", new String[]{"\"Author Name\""}); + HINTS.put("author", new String[]{"\"Author Name\""}); + HINTS.put("date", new String[]{"\"YYYY-MM-DD\""}); + HINTS.put("updated", new String[]{"\"YYYY-MM-DD\""}); + HINTS.put("zoom", new String[]{"1.0"}); + } + @Override public Set getSupportedKeys() { return KEYS; } @@ -19,19 +36,15 @@ public class FrontmatterValueProvider implements AutocompleteProvider { public List provide(AutocompleteContext ctx, int limit) { if (!(ctx instanceof FrontmatterContext)) return Collections.emptyList(); FrontmatterContext fmc = (FrontmatterContext) ctx; - String key = fmc.getKey(); + String[] suggestions = HINTS.getOrDefault(fmc.getKey(), new String[0]); String partial = fmc.getPartialText().toLowerCase(); - - // TODO: dispatch by key to specialized providers - // "parent" → PageReferenceProvider - // "icon", "item_ids" → ItemIdProvider - // "ore_ids" → OreDictProvider - // "quest_ids" → QuestIdProvider - // "position" → numeric values - - if ("navigation".equals(key) || "authors".equals(key) || "author".equals(key)) { - return Collections.emptyList(); // navigation is a map, authors need names + List results = new ArrayList<>(); + for (String s : suggestions) { + if (results.size() >= limit) break; + if (partial.isEmpty() || s.toLowerCase().contains(partial)) { + results.add(new TextCandidate(s)); + } } - return Collections.emptyList(); + return results; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java index 6b1d90f1..ea181c01 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java @@ -2,13 +2,16 @@ import java.util.*; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -/** Suggests guide page paths for href, linksTo, and SubPages id attributes. - * Requires a PageCollection reference — for now returns empty; enable after wiring. */ +/** Suggests guide page paths. Call {@link #setPages} before use. */ public class PageReferenceProvider implements AutocompleteProvider { private static final Set KEYS = buildKeys(); + @Nullable + private static volatile List pagePaths; private static Set buildKeys() { Set keys = new HashSet<>(); @@ -18,12 +21,25 @@ private static Set buildKeys() { return Collections.unmodifiableSet(keys); } + /** Set the available page paths from the guide's page collection. */ + public static void setPages(@Nullable Collection paths) { + pagePaths = paths != null ? Collections.unmodifiableList(new ArrayList<>(paths)) : null; + } + @Override public Set getSupportedKeys() { return KEYS; } @Override public List provide(AutocompleteContext ctx, int limit) { - // TODO: wire PageCollection reference to enumerate page paths - return Collections.emptyList(); + if (pagePaths == null) return Collections.emptyList(); + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + for (String path : pagePaths) { + if (results.size() >= limit) break; + if (partial.isEmpty() || path.toLowerCase().contains(partial)) { + results.add(new TextCandidate(path)); + } + } + return results; } } From 41b5ee84ef372afee829d04d9a979500101daab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 13:09:33 +0800 Subject: [PATCH 014/136] feat(autocomplete): add ImagePathProvider for src attribute file suggestions Scans resource pack asset directories for .png/.snbt/.csv/.json/.mmd files. Uses AccessorFMLClientHandler mixin for resource pack access, same pattern as DataDrivenGuideLoader. --- .../com/hfstudio/guidenh/ClientProxy.java | 2 + .../guidenh/guide/internal/GuideScreen.java | 2 + .../provider/ImagePathProvider.java | 136 ++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 57bfcf7e..d1870e78 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -37,6 +37,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FormatPatternProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterKeyProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterValueProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ImagePathProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ItemIdProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.KeyBindProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.NumericValueProvider; @@ -97,6 +98,7 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new FrontmatterKeyProvider()); AutocompleteProviders.register(new FrontmatterValueProvider()); AutocompleteProviders.register(new FencedBlockLanguageProvider()); + AutocompleteProviders.register(new ImagePathProvider()); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 92cbf30f..3741b89e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -47,6 +47,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.WordBoundaryResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ImagePathProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui.AutocompletePopup; @@ -502,6 +503,7 @@ public void updateScreen() { pagePaths.add(page.getId().toString()); } PageReferenceProvider.setPages(pagePaths); + ImagePathProvider.refreshFromGuide(guide); } if (guideEditorTextArea != null) { AnchorProvider.setDocumentText(guideEditorTextArea.getText()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java new file mode 100644 index 00000000..4da70920 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java @@ -0,0 +1,136 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.client.resources.ResourcePackRepository; +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.Guide; +import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; +import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +/** + * Suggests file paths from the guide assets directory for src attributes. + * Call {@link #refreshFromGuide(Guide)} once to initialize the scanned directories. + */ +public class ImagePathProvider implements AutocompleteProvider { + + private static final Set KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + AutocompleteKey.forValue("FloatingImage", "src"), + AutocompleteKey.forValue("ImportStructure", "src"), + AutocompleteKey.forValue("ImportPonder", "src"), + AutocompleteKey.forValue("Mermaid", "src"), + AutocompleteKey.forValue("CsvTable", "src") + ))); + + private static final String[] EXTENSIONS = { + ".png", ".jpg", ".jpeg", ".gif", ".snbt", ".nbt", ".csv", ".json", ".mmd", ".md" + }; + + @Nullable + private static volatile List baseDirs; + private static volatile boolean scanned; + + /** + * Scans the guide's resource pack directories for asset folders matching + * page paths. This method performs the scan only once; subsequent calls + * are no-ops once the scan has completed. + */ + public static void refreshFromGuide(@Nullable Guide guide) { + if (scanned || guide == null) return; + scanned = true; + + List dirs = new ArrayList<>(); + + // Collect all active resource packs (same pattern as DataDrivenGuideLoader) + List packs = new ArrayList<>(); + var fmlAccessor = (com.hfstudio.guidenh.mixins.early.fml.AccessorFMLClientHandler) + cpw.mods.fml.client.FMLClientHandler.instance(); + List basePacks = fmlAccessor.guidenh$getResourcePackList(); + if (basePacks != null) packs.addAll(basePacks); + Minecraft mc = Minecraft.getMinecraft(); + if (mc.getResourcePackRepository() != null) { + for (ResourcePackRepository.Entry entry : mc.getResourcePackRepository().getRepositoryEntries()) { + IResourcePack rp = entry.getResourcePack(); + if (rp != null) packs.add(rp); + } + } + + for (ParsedGuidePage page : guide.getPages()) { + ResourceLocation id = page.getId(); + String ns = id.getResourceDomain(); + String path = id.getResourcePath(); + + // Extract directory portion: "path/to/page.md" -> "path/to" + int slashIdx = path.lastIndexOf('/'); + String dirPath = slashIdx > 0 ? path.substring(0, slashIdx) : ""; + + for (IResourcePack rp : packs) { + File packFile = DataDrivenGuideLoader.getResourcePackFile(rp); + if (packFile == null) continue; + File assetsDir = new File(packFile, "assets/" + ns + "/" + dirPath); + if (assetsDir.isDirectory() && !dirs.contains(assetsDir)) { + dirs.add(assetsDir); + } + } + } + + baseDirs = dirs.isEmpty() ? null : Collections.unmodifiableList(dirs); + } + + @Override + public Set getSupportedKeys() { + return KEYS; + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + if (baseDirs == null) return Collections.emptyList(); + String partial = ctx.getPartialText().toLowerCase(); + List results = new ArrayList<>(); + + for (File dir : baseDirs) { + if (results.size() >= limit) break; + scanDir(dir, "", partial, results, limit); + } + return results; + } + + private void scanDir(File dir, String prefix, String partial, + List results, int limit) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (results.size() >= limit) return; + String name = f.getName(); + if (name.startsWith(".")) continue; + String relPath = prefix.isEmpty() ? name : prefix + "/" + name; + if (f.isDirectory()) { + scanDir(f, relPath, partial, results, limit); + } else if (matchesExtension(name)) { + if (partial.isEmpty() || relPath.toLowerCase().contains(partial)) { + results.add(new TextCandidate(relPath)); + } + } + } + } + + private static boolean matchesExtension(String name) { + String lower = name.toLowerCase(); + for (String ext : EXTENSIONS) { + if (lower.endsWith(ext)) return true; + } + return false; + } +} From abb604ad5deec445f42a51b2169df7b5b07bd273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 14 May 2026 18:09:00 +0800 Subject: [PATCH 015/136] fix(autocomplete): correct provider semantics, disable unstable providers - Fix crash-causing format patterns (%d/%f) and domain syntax - Separate Anchor display/replacement, limit Color to Color/id - Filter NumericValueProvider by attr name, add missing tag keys - Handle # inside frontmatter quotes - Disable TagNameProvider, AttributeNameProvider: unreliable until parser supports error-recovery - Remove EnumValueProvider, FencedBlockLanguageProvider registrations - Roll back EntityNameProvider hardcoded aliases --- .../com/hfstudio/guidenh/ClientProxy.java | 6 ++-- .../guidenh/guide/internal/GuideScreen.java | 13 +++++++-- .../autocomplete/provider/AnchorProvider.java | 2 +- .../provider/AttributeNameProvider.java | 11 ++++++++ .../provider/AutocompleteProviders.java | 2 +- .../provider/BlockIdProvider.java | 2 +- .../autocomplete/provider/ColorProvider.java | 15 ++-------- .../provider/CommandProvider.java | 21 +++++++------- .../autocomplete/provider/DomainProvider.java | 4 +-- .../provider/EnumValueProvider.java | 3 ++ .../provider/FencedBlockLanguageProvider.java | 3 ++ .../provider/FormatPatternProvider.java | 2 +- .../provider/FrontmatterValueProvider.java | 28 ++++++++----------- .../autocomplete/provider/ItemIdProvider.java | 1 + .../provider/NumericValueProvider.java | 17 ++++++----- .../provider/PageReferenceProvider.java | 1 + .../provider/RegistryCandidate.java | 2 +- .../provider/TagNameProvider.java | 13 ++++++++- .../resolver/FrontmatterResolver.java | 21 ++++++++++++-- .../resolver/MdxSyntaxResolver.java | 27 ++++++++++++++---- 20 files changed, 125 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index d1870e78..af77c1dc 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -31,9 +31,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.CommandProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DomainProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EntityNameProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EnumValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ExpressionProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FencedBlockLanguageProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FormatPatternProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterKeyProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterValueProvider; @@ -81,7 +79,7 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new ItemIdProvider()); TagAttributeRegistry.initialize(); AutocompleteProviders.register(new AttributeNameProvider()); - AutocompleteProviders.register(new EnumValueProvider()); + AutocompleteProviders.register(new ColorProvider()); AutocompleteProviders.register(new OreDictProvider()); AutocompleteProviders.register(new BlockIdProvider()); @@ -97,7 +95,7 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new TagNameProvider()); AutocompleteProviders.register(new FrontmatterKeyProvider()); AutocompleteProviders.register(new FrontmatterValueProvider()); - AutocompleteProviders.register(new FencedBlockLanguageProvider()); + AutocompleteProviders.register(new ImagePathProvider()); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 3741b89e..6b3018b0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -44,6 +44,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.CompositeResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxSyntaxResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.WordBoundaryResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; @@ -2373,8 +2374,16 @@ private void commitAutocompleteSelection() { String text = guideEditorTextArea.getText(); String before = text.substring(0, ac.replaceStart()); String after = text.substring(ac.replaceEnd()); - String newText = before + selected.replacementText() + after; - int newCursor = ac.replaceStart() + selected.replacementText().length(); + String replaced = selected.replacementText(); + + // Auto-close quote for MDX attribute values when the closing quote is missing + if (ac instanceof MdxValueContext && !after.isEmpty() + && after.charAt(0) != '"' && after.charAt(0) != '\'' && after.charAt(0) != '}') { + replaced += "\""; + } + + String newText = before + replaced + after; + int newCursor = ac.replaceStart() + replaced.length(); guideEditorTextArea.applyEdit(newText, newCursor, newCursor); updateGuideEditorTextFromArea(); autocompletePopup.close(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java index 8a558bb3..0944f142 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java @@ -40,7 +40,7 @@ public List provide(AutocompleteContext ctx, int limit) { String heading = m.group(1).trim(); String anchor = heading.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", ""); if (query.isEmpty() || anchor.contains(query)) { - results.add(new TextCandidate("#" + anchor + " (" + heading + ")")); + results.add(new RegistryCandidate("#" + anchor, heading)); } } return results; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java index 40983bf8..78f6df2a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java @@ -16,8 +16,19 @@ public class AttributeNameProvider implements AutocompleteProvider { @Override public Set getSupportedKeys() { return KEYS; } + // + // TODO: Currently disabled because the resolver layer (MdxSyntaxResolver) does not + // reliably distinguish attribute-name positions from plain text. When the parser + // supports error-recovery or the resolver can accurately detect tag-internal + // whitespace without false positives, set this to true. + // + private static volatile boolean enabled = false; + + public static void setEnabled(boolean value) { enabled = value; } + @Override public List provide(AutocompleteContext ctx, int limit) { + if (!enabled) return Collections.emptyList(); if (!(ctx instanceof MdxAttrNameContext)) return Collections.emptyList(); MdxAttrNameContext mdx = (MdxAttrNameContext) ctx; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java index 9c59ba0f..6bbfadc9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -46,7 +46,7 @@ private static boolean matchesContext(AutocompleteKey key, AutocompleteContext c if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) { com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext fmc = (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) ctx; - String attr = fmc.isValue() ? "fm_value" : "fm_key"; + String attr = fmc.isValue() ? fmc.getKey() : "fm_key"; return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, "*", attr); } return false; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java index 3e557891..ef0c61eb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java @@ -12,7 +12,7 @@ public class BlockIdProvider implements AutocompleteProvider { private static final Set KEYS = buildKeys( - "BlockImage", "PlaceBlock", "RemoveBlocks" + "BlockImage", "PlaceBlock", "RemoveBlocks", "Block" ); private static Set buildKeys(String... tagNames) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java index 8822d5be..c8a7e339 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java @@ -2,12 +2,9 @@ import java.util.*; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; -/** Suggests hex color values and SymbolicColor names for color-typed attributes. */ +/** Suggests SymbolicColor names for <Color id> attributes. */ public class ColorProvider implements AutocompleteProvider { private static final String[] SYMBOLIC_NAMES = { @@ -23,15 +20,7 @@ public class ColorProvider implements AutocompleteProvider { @Override public Set getSupportedKeys() { - Set keys = new HashSet<>(); - for (String tag : TagAttributeRegistry.getRegisteredTags()) { - for (AttributeSpec spec : TagAttributeRegistry.get(tag)) { - if (spec.getType() == AttrType.COLOR) { - keys.add(AutocompleteKey.forValue(tag, spec.getName())); - } - } - } - return Collections.unmodifiableSet(keys); + return Collections.singleton(AutocompleteKey.forValue("Color", "id")); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java index 1e277884..34f76ff7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java @@ -2,31 +2,32 @@ import java.util.*; +import net.minecraft.client.Minecraft; + import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -/** Suggests GuideNH commands for <CommandLink command> attributes. */ +/** Suggests registered client-side commands for <CommandLink command>. */ public class CommandProvider implements AutocompleteProvider { private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("CommandLink", "command")); - private static final String[] COMMANDS = { - "/guidenh open ", "/guidenh search ", "/guidenh reload", - "/guidenh export", "/guidenh export-site", "/guidenh export-structure", - "/guidenh import-structure", "/guidenh place-all-structures", "/guidenh list" - }; - @Override public Set getSupportedKeys() { return KEYS; } @Override + @SuppressWarnings("unchecked") public List provide(AutocompleteContext ctx, int limit) { String partial = ctx.getPartialText().toLowerCase(); List results = new ArrayList<>(); - for (String cmd : COMMANDS) { + for (Object cmdObj : net.minecraftforge.client.ClientCommandHandler.instance.getCommands().values()) { if (results.size() >= limit) break; - if (partial.isEmpty() || cmd.toLowerCase().contains(partial)) { - results.add(new TextCandidate(cmd)); + if (cmdObj instanceof net.minecraft.command.ICommand) { + net.minecraft.command.ICommand cmd = (net.minecraft.command.ICommand) cmdObj; + String name = cmd.getCommandName(); + if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + results.add(new TextCandidate("/" + name)); + } } } return results; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java index b9561e7d..2ad4e07b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java @@ -13,8 +13,8 @@ public class DomainProvider implements AutocompleteProvider { )); private static final String[] DOMAINS = { - "(-inf, inf)", "[-10, 10]", "(-pi, pi)", "[0, 2*pi)", - "(-inf, 0]", "[0, inf)", "(-5, 5)", "[-1, 1]" + "-inf..inf", "-10..10", "-pi..pi", "0..2*pi", + "..0", "0..", "-5..5", "-1..1" }; @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java index 7d08419f..010e69e1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java @@ -9,6 +9,9 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; import com.hfstudio.guidenh.guide.compiler.tags.SerializedEnum; +// TODO: require TagAttributeRegistry entries with AttrType.ENUM. +// Currently no attributes use ENUM type, so this provider never fires. + /** Suggests enum values for enum-typed attributes. */ public class EnumValueProvider implements AutocompleteProvider { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java index 80ac6628..da134e30 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java @@ -4,6 +4,9 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +// TODO: re-enable when fence-block context resolver is implemented. +// Currently forTag() causes these suggestions to appear in the tag name popup. + /** Suggests language identifiers after ``` for fenced code blocks. */ public class FencedBlockLanguageProvider implements AutocompleteProvider { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java index 52140017..3d6c9d0b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java @@ -11,7 +11,7 @@ public class FormatPatternProvider implements AutocompleteProvider { Collections.singleton(AutocompleteKey.forValue("ItemImage", "format")); private static final String[] PATTERNS = { - "%s", "%d", "%.1f", "%.2f", "%s items", "x%d", "%d / %d" + "%s", "%s items", "**%s**", "*%s*", "~~%s~~" }; @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java index 6d349057..806a46be 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java @@ -8,25 +8,19 @@ /** Dispatches frontmatter value completion based on the current key. */ public class FrontmatterValueProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("*", "fm_value")); - private static final Map HINTS = new LinkedHashMap<>(); static { - HINTS.put("navigation", new String[]{" title:", " parent:", " position:", " icon:", " icon_texture:"}); - HINTS.put("title", new String[]{"\"Page Title\""}); - HINTS.put("parent", new String[]{}); - HINTS.put("position", new String[]{"0"}); - HINTS.put("icon", new String[]{}); - HINTS.put("icon_texture", new String[]{}); - HINTS.put("item_ids", new String[]{}); - HINTS.put("ore_ids", new String[]{}); - HINTS.put("quest_ids", new String[]{}); - HINTS.put("authors", new String[]{"\"Author Name\""}); - HINTS.put("author", new String[]{"\"Author Name\""}); - HINTS.put("date", new String[]{"\"YYYY-MM-DD\""}); - HINTS.put("updated", new String[]{"\"YYYY-MM-DD\""}); - HINTS.put("zoom", new String[]{"1.0"}); + HINTS.put("navigation", new String[]{"\n title:", "\n parent:", "\n position:", "\n icon:", "\n icon_texture:"}); + } + + private static final Set KEYS = buildKeys(); + + private static Set buildKeys() { + Set keys = new HashSet<>(); + for (String k : HINTS.keySet()) { + keys.add(AutocompleteKey.forValue("*", k)); + } + return Collections.unmodifiableSet(keys); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java index c9bc8f66..1f33e691 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java @@ -30,6 +30,7 @@ public class ItemIdProvider implements AutocompleteProvider { allKeys.add(AutocompleteKey.forValue("RecipeFor", "output")); allKeys.add(AutocompleteKey.forValue("RecipesFor", "input")); allKeys.add(AutocompleteKey.forValue("RecipesFor", "output")); + allKeys.add(AutocompleteKey.forValue("ItemIcon", "id")); KEYS = Collections.unmodifiableSet(allKeys); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java index c2aec2f5..1a1503ae 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java @@ -3,6 +3,7 @@ import java.util.*; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; /** Suggests common numeric values for INT/FLOAT attributes. */ public class NumericValueProvider implements AutocompleteProvider { @@ -32,16 +33,18 @@ public Set getSupportedKeys() { @Override public List provide(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof MdxValueContext)) return Collections.emptyList(); + String attrName = ((MdxValueContext) ctx).getAttrName(); + String[] vals = SUGGESTIONS.get(attrName); + if (vals == null) return Collections.emptyList(); + String partial = ctx.getPartialText(); List results = new ArrayList<>(); - // Generic — return all suggestions for any recognized attr - for (String[] vals : SUGGESTIONS.values()) { - for (String v : vals) { - if (results.size() >= limit) break; - if (partial.isEmpty() || v.startsWith(partial)) { - results.add(new TextCandidate(v)); - } + for (String v : vals) { + if (partial.isEmpty() || v.startsWith(partial)) { + results.add(new TextCandidate(v)); } + if (results.size() >= limit) break; } return results; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java index ea181c01..a7c41a03 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java @@ -18,6 +18,7 @@ private static Set buildKeys() { keys.add(AutocompleteKey.forValue("a", "href")); keys.add(AutocompleteKey.forValue("SubPages", "id")); keys.add(AutocompleteKey.forValue("ItemLink", "linksTo")); + keys.add(AutocompleteKey.forValue("*", "parent")); return Collections.unmodifiableSet(keys); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java index 9b2247e8..e5cbf6a9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java @@ -44,7 +44,7 @@ public RegistryCandidate(String key, @Nullable String subtitle, @Nullable ItemSt @Override public String displayText() { return key; } @Override public String replacementText() { return key; } @Override public int renderHeight() { return subtitle != null ? 28 : 16; } - @Override public int renderWidth(FontRenderer fr) { return icon != null ? ICON_SIZE + 4 : 0; } + @Override public int renderWidth(FontRenderer fr) { return icon != null ? ICON_SIZE + 4 + fr.getStringWidth(key) : 0; } @Override public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java index c256cd75..19c6cdd5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java @@ -4,12 +4,22 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +// +// TODO: Disabled. Right-click context menu provides tag insertion as an alternative. +// The tag name popup was unreliable — caching wrong after parse errors — and the +// self-closing suffix (/>) was inappropriate for container tags like Row, Column, +// Scene, etc. Revisit after parser layer supports error-recovery. +// /** Suggests MDX tag names when cursor is right after '<'. */ public class TagNameProvider implements AutocompleteProvider { private static final Set KEYS = Collections.singleton(AutocompleteKey.forTag()); + private static volatile boolean enabled = false; + + public static void setEnabled(boolean value) { enabled = value; } + // Grouped by category for future grouping in UI private static final String[] TAG_NAMES = { // Inline/Flow @@ -38,13 +48,14 @@ public class TagNameProvider implements AutocompleteProvider { @Override public List provide(AutocompleteContext ctx, int limit) { + if (!enabled) return Collections.emptyList(); String partial = ctx.getPartialText(); String lower = partial != null ? partial.toLowerCase() : ""; List results = new ArrayList<>(); for (String name : TAG_NAMES) { if (results.size() >= limit) break; if (lower.isEmpty() || name.toLowerCase().startsWith(lower)) { - results.add(new TextCandidate(name)); + results.add(new TextCandidate(name + " />")); } } return results; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java index f68e703b..08b4e283 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java @@ -44,10 +44,16 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { int lineStart = text.lastIndexOf('\n', cursorIndex - 1) + 1; int valueAbsStart = lineStart + valueStart; int valueAbsEnd = lineStart + line.length(); + // Extend to include trailing newline so candidates with \n prefix replace cleanly + if (valueAbsEnd < text.length() && text.charAt(valueAbsEnd) == '\n') { + valueAbsEnd++; + } - // Trim trailing comment from value end + // Trim trailing comment from value end (unless # is inside quotes) int hashIdx = line.indexOf(" #", valueStart); - if (hashIdx >= 0) valueAbsEnd = lineStart + hashIdx; + if (hashIdx >= 0 && !isInsideQuotes(line, valueStart, hashIdx)) { + valueAbsEnd = lineStart + hashIdx; + } valueAbsEnd = Math.min(valueAbsEnd, secondStart); if (cursorIndex >= valueAbsStart && cursorIndex <= valueAbsEnd) { @@ -68,6 +74,17 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { return SyntaxUtils.resolveWord(text, cursorIndex); } + /** Check if position {@code pos} in {@code line} is inside paired quotes. */ + private static boolean isInsideQuotes(String line, int from, int pos) { + boolean inSingle = false, inDouble = false; + for (int i = from; i < pos; i++) { + char c = line.charAt(i); + if (c == '\'' && !inDouble) inSingle = !inSingle; + else if (c == '"' && !inSingle) inDouble = !inDouble; + } + return inSingle || inDouble; + } + @Nullable private String getLineAt(String text, int cursorIndex) { int lineStart = text.lastIndexOf('\n', cursorIndex - 1) + 1; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index cba1de13..cfacb084 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -29,6 +29,23 @@ public class MdxSyntaxResolver implements SyntaxContextResolver { private static final Pattern FALLBACK_TAG = Pattern.compile("<([A-Za-z]\\w*)[^>]*?\\s+(\\w+)\\s*=\\s*\"([^\">]*)$"); + // + // TODO: Fallback is intentionally limited for now. + // + // When the full-document AST parse throws ParseException (e.g. mismatched MDX tags, + // unexpected closing slashes, etc.), the entire autocomplete system degrades to this + // regex-based fallback. Currently it only covers the attribute-value-in-double-quotes + // case. Missing cases that need parser-level error recovery first: + // + // - Attribute-name completion inside an open tag () + // - Single-quoted / brace-delimited / unquoted values + // - Cursor right after '=' with no opening quote yet + // + // Once the Micromark/MdastCompiler pipeline supports error-recovery (partial AST on + // failure), extend resolveFromFallback to handle these cases. The extra patterns and + // context-detection logic are ready but disabled until then. + // + // AST cache: re-parse only when text changes; cursor-only moves walk cached tree @Nullable private String cachedText; @@ -61,9 +78,8 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { @Nullable private TextSyntaxContext resolveFromAst(MdAstRoot root, String text, int cursorIndex) { if (cursorIndex > 0 && text.charAt(cursorIndex - 1) == '<') { - int tagStart = cursorIndex - 1; - return new TextSyntaxContext(SyntaxElementType.TAG_START, tagStart, cursorIndex, - new TagStartContext(tagStart, cursorIndex, "")); + return new TextSyntaxContext(SyntaxElementType.TAG_START, cursorIndex, cursorIndex, + new TagStartContext(cursorIndex, cursorIndex, "")); } MdxJsxElementFields element = findEnclosingMdxElement(root, cursorIndex); if (element != null) { @@ -177,9 +193,8 @@ private TextSyntaxContext resolvePlainTextWord(String text, int cursorIndex) { @Nullable private TextSyntaxContext resolveFromFallback(String text, int cursorIndex) { if (cursorIndex > 0 && text.charAt(cursorIndex - 1) == '<') { - int tagStart = cursorIndex - 1; - return new TextSyntaxContext(SyntaxElementType.TAG_START, tagStart, cursorIndex, - new TagStartContext(tagStart, cursorIndex, "")); + return new TextSyntaxContext(SyntaxElementType.TAG_START, cursorIndex, cursorIndex, + new TagStartContext(cursorIndex, cursorIndex, "")); } String prefix = text.substring(0, Math.min(cursorIndex, text.length())); Matcher m = FALLBACK_TAG.matcher(prefix); From 1f99fc37354cb09854da5e3f105f8b77be8a01f4 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 15 May 2026 02:05:52 +0800 Subject: [PATCH 016/136] bugfix --- .../com/hfstudio/guidenh/ClientProxy.java | 20 +- .../guidenh/guide/compiler/Frontmatter.java | 24 +- .../compiler/GuideItemReferenceResolver.java | 7 +- .../guidenh/guide/compiler/PageCompiler.java | 277 ++++++++--- .../compiler/tags/FileTreeTagCompiler.java | 4 +- .../guidenh/guide/internal/GuideScreen.java | 76 +-- .../guide/internal/csv/CsvTableParser.java | 25 +- .../editor/autocomplete/AttrType.java | 25 +- .../editor/autocomplete/AttributeSpec.java | 17 +- .../autocomplete/AutocompleteContext.java | 3 + .../autocomplete/SelectionStrategy.java | 2 + .../autocomplete/SyntaxContextResolver.java | 1 + .../autocomplete/SyntaxElementType.java | 1 + .../editor/autocomplete/SyntaxUtils.java | 2 +- .../autocomplete/TagAttributeRegistry.java | 212 ++++---- .../autocomplete/TextSyntaxContext.java | 29 +- .../autocomplete/provider/AnchorProvider.java | 25 +- .../provider/AttributeNameProvider.java | 32 +- .../provider/AutocompleteCandidate.java | 13 +- .../provider/AutocompleteKey.java | 40 +- .../provider/AutocompleteProvider.java | 2 + .../provider/AutocompleteProviders.java | 100 +++- .../provider/BlockIdProvider.java | 20 +- .../autocomplete/provider/ColorCandidate.java | 18 +- .../autocomplete/provider/ColorProvider.java | 27 +- .../provider/CommandProvider.java | 29 +- .../autocomplete/provider/DomainProvider.java | 28 +- .../provider/EntityNameProvider.java | 18 +- .../provider/EnumValueProvider.java | 26 +- .../provider/ExpressionProvider.java | 29 +- .../provider/FencedBlockLanguageProvider.java | 30 +- .../provider/FormatPatternProvider.java | 23 +- .../provider/FrontmatterKeyProvider.java | 24 +- .../provider/FrontmatterValueProvider.java | 22 +- .../provider/ImagePathProvider.java | 76 +-- .../autocomplete/provider/ItemCandidate.java | 25 +- .../autocomplete/provider/ItemIdProvider.java | 14 +- .../provider/KeyBindProvider.java | 21 +- .../provider/NumericValueProvider.java | 28 +- .../provider/OreDictProvider.java | 18 +- .../provider/PageReferenceProvider.java | 17 +- .../provider/RegistryCandidate.java | 31 +- .../provider/TagNameProvider.java | 65 +-- .../autocomplete/provider/TextCandidate.java | 12 +- .../resolver/CompositeResolver.java | 2 +- .../resolver/FenceLanguageContext.java | 31 ++ .../resolver/FencedBlockLanguageResolver.java | 123 +++++ .../resolver/FrontmatterContext.java | 29 +- .../resolver/FrontmatterResolver.java | 18 +- .../resolver/MdxAttrNameContext.java | 25 +- .../resolver/MdxSyntaxResolver.java | 453 +++++++++++++----- .../resolver/MdxValueContext.java | 40 +- .../resolver/SelectionStrategies.java | 13 +- .../resolver/TagStartContext.java | 21 +- .../resolver/WordBoundaryResolver.java | 2 +- .../autocomplete/ui/AutocompletePopup.java | 41 +- .../gui/SceneEditorMultilineTextArea.java | 4 +- .../editor/md/SceneEditorMarkdownCodec.java | 30 +- .../site/GuideSiteHtmlCompiler.java | 11 +- 59 files changed, 1669 insertions(+), 712 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FenceLanguageContext.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FencedBlockLanguageResolver.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 6bf8ff1c..63c4ac68 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -17,13 +17,7 @@ import com.hfstudio.guidenh.guide.internal.GuideRegistry; import com.hfstudio.guidenh.guide.internal.GuideReloadListener; import com.hfstudio.guidenh.guide.internal.GuideWarmupPump; -import com.hfstudio.guidenh.guide.scene.level.GuidebookFakeWorld; -import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; -import com.hfstudio.guidenh.integration.GuideNhClientIntegrationBootstrap; -import com.hfstudio.guidenh.integration.ae2.network.Ae2NetworkRegistration; -import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; -import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; -import com.hfstudio.guidenh.network.GuideNhNetwork; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; @@ -32,7 +26,9 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.CommandProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DomainProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EntityNameProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.EnumValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ExpressionProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FencedBlockLanguageProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FormatPatternProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterKeyProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.FrontmatterValueProvider; @@ -43,7 +39,13 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.OreDictProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.TagNameProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; +import com.hfstudio.guidenh.guide.scene.level.GuidebookFakeWorld; +import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; +import com.hfstudio.guidenh.integration.GuideNhClientIntegrationBootstrap; +import com.hfstudio.guidenh.integration.ae2.network.Ae2NetworkRegistration; +import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; +import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; +import com.hfstudio.guidenh.network.GuideNhNetwork; import com.hfstudio.guidenh.network.GuideNhRegionExportClientHandler; import com.hfstudio.guidenh.network.GuideNhRegionExportReplyMessage; import com.hfstudio.structurelibexport.StructureExportBootstrap; @@ -98,10 +100,12 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new AnchorProvider()); AutocompleteProviders.register(new CommandProvider()); AutocompleteProviders.register(new NumericValueProvider()); + AutocompleteProviders.register(new EnumValueProvider()); AutocompleteProviders.register(new ExpressionProvider()); AutocompleteProviders.register(new DomainProvider()); AutocompleteProviders.register(new FormatPatternProvider()); AutocompleteProviders.register(new TagNameProvider()); + AutocompleteProviders.register(new FencedBlockLanguageProvider()); AutocompleteProviders.register(new FrontmatterKeyProvider()); AutocompleteProviders.register(new FrontmatterValueProvider()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/Frontmatter.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/Frontmatter.java index a9ee6b21..0c76a5b1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/Frontmatter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/Frontmatter.java @@ -1,7 +1,9 @@ package com.hfstudio.guidenh.guide.compiler; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -28,7 +30,23 @@ public static Frontmatter parse(ResourceLocation pageId, String yamlText) { var yaml = YAML.get(); FrontmatterNavigation navigation = null; - Map data = yaml.load(yamlText); + Object loaded = yaml.load(yamlText); + if (loaded == null) { + return new Frontmatter(null, Collections.emptyMap()); + } + if (!(loaded instanceof MaploadedMap)) { + throw new IllegalArgumentException("Frontmatter root has to be a map"); + } + + Map data = new HashMap<>(); + for (Map.Entry entry : loadedMap.entrySet()) { + Object key = entry.getKey(); + if (!(key instanceof String)) { + throw new IllegalArgumentException("Frontmatter keys have to be strings"); + } + data.put((String) key, entry.getValue()); + } + var navigationObj = data.remove("navigation"); if (navigationObj != null) { if (!(navigationObj instanceof MapnavigationMap)) { @@ -317,8 +335,8 @@ private static String toDateString(@Nullable Object value) { String s = ((String) value).trim(); return s.isEmpty() ? null : s; } - if (value instanceof java.util.Date) { - return new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.ROOT).format((java.util.Date) value); + if (value instanceof Date) { + return new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT).format((Date) value); } return value.toString(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideItemReferenceResolver.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideItemReferenceResolver.java index 3d3200ca..3efa4266 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideItemReferenceResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideItemReferenceResolver.java @@ -6,6 +6,7 @@ import net.minecraft.init.Blocks; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import net.minecraftforge.oredict.OreDictionary; @@ -14,7 +15,7 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.integration.api.GuideNhIntegrationRegistry; -public final class GuideItemReferenceResolver { +public class GuideItemReferenceResolver { private GuideItemReferenceResolver() {} @@ -47,7 +48,7 @@ public static ResolvedItemReference resolveItemReference(String defaultNamespace ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); if (ref.nbt() != null) { - stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt() + stack.stackTagCompound = (NBTTagCompound) ref.nbt() .copy(); } return new ResolvedItemReference(ref.id(), stack); @@ -89,7 +90,7 @@ public static ResolvedBlockReference resolveBlockReference(String defaultNamespa Item item = Item.getItemFromBlock(block); ItemStack stack = item != null ? new ItemStack(item, 1, ref.hasExplicitMeta() ? ref.meta() : 0) : null; if (stack != null && ref.nbt() != null) { - stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt() + stack.stackTagCompound = (NBTTagCompound) ref.nbt() .copy(); } return new ResolvedBlockReference(ref.id(), block, stack, ref.hasExplicitMeta()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index aadd6a5a..d0fa7fd0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -20,6 +20,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; +import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; @@ -203,50 +204,35 @@ public static ParsedGuidePage parse(String sourcePack, ResourceLocation id, Stri } public static ParsedGuidePage parse(String sourcePack, String language, ResourceLocation id, String pageContent) { - // Normalize line ending - pageContent = normalizeLineEndings(pageContent); - pageContent = FootnotePreprocessor.preprocess(pageContent); - var sourceFrontmatter = parseFrontmatterFromSource(id, pageContent); - MarkdownLatexShorthand.MaskResult latexMask = MarkdownLatexShorthand.mask(pageContent); - String parseContent = MdxCommentMasker.mask(latexMask.source()); - + pageContent = pageContent != null ? pageContent : ""; MdAstRoot astRoot; + Frontmatter sourceFrontmatter = new Frontmatter(null, Collections.emptyMap()); String parseFailureMessage = null; UnistPoint parseFailureFrom = null; UnistPoint parseFailureTo = null; try { + pageContent = normalizeLineEndings(pageContent); + pageContent = FootnotePreprocessor.preprocess(pageContent); + sourceFrontmatter = parseFrontmatterFromSource(id, pageContent); + MarkdownLatexShorthand.MaskResult latexMask = MarkdownLatexShorthand.mask(pageContent); + String parseContent = MdxCommentMasker.mask(latexMask.source()); astRoot = MdAst.fromMarkdown(parseContent, PARSE_OPTIONS); MarkdownLatexShorthand.restore(astRoot, latexMask); MarkdownHtmlRuntimeNormalizer.normalize(astRoot); - } catch (ParseException e) { - parseFailureFrom = e.getFrom(); - parseFailureTo = e.getTo(); - var position = ""; - if (parseFailureFrom != null) { - position = " at line " + e.getFrom() - .line() - + " column " - + e.getFrom() - .column(); + } catch (RuntimeException t) { + if (t instanceof ParseException e) { + parseFailureFrom = e.getFrom(); + parseFailureTo = e.getTo(); } - var errorMessage = String.format( - Locale.ROOT, - "Failed to parse GuideME page %s (lang: %s)%s from resource pack %s", - id, - language, - position, - sourcePack); - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] {}", errorMessage, e); - parseFailureMessage = errorMessage + ": \n" + e; + String errorMessage = formatParseFailureMessage(id, language, sourcePack, parseFailureFrom); + logError("[GuideNH] [PageCompiler] {}", t, errorMessage); + parseFailureMessage = errorMessage + ": \n" + t; astRoot = buildErrorPage(parseFailureMessage); } // Find front-matter - var frontmatter = parseFrontmatter(id, astRoot); - if (parseFailureMessage != null && sourceFrontmatter.navigationEntry() != null) { - frontmatter = sourceFrontmatter; - } + var frontmatter = parseFailureMessage == null ? sourceFrontmatter : parseFrontmatter(id, astRoot); + if (parseFailureMessage != null && sourceFrontmatter.navigationEntry() != null) frontmatter = sourceFrontmatter; return new ParsedGuidePage( sourcePack, @@ -264,6 +250,30 @@ public static String normalizeLineEndings(String pageContent) { return GuideStringLines.normalizeLineEndings(pageContent); } + private static String formatParseFailureMessage(ResourceLocation id, String language, String sourcePack, + @Nullable UnistPoint position) { + String positionText = ""; + if (position != null) { + positionText = " at line " + position.line() + " column " + position.column(); + } + return String.format( + Locale.ROOT, + "Failed to parse GuideME page %s (lang: %s)%s from resource pack %s", + id, + language, + positionText, + sourcePack); + } + + private static String formatCompileFailureMessage(ResourceLocation id, String language, String sourcePack) { + return String.format( + Locale.ROOT, + "Failed to compile GuideME page %s (lang: %s) from resource pack %s", + id, + language, + sourcePack); + } + public static MdAstRoot buildErrorPage(String errorText) { return buildErrorPage("PARSING ERROR", errorText); } @@ -299,17 +309,34 @@ public static GuidePage buildErrorGuidePage(PageCollection pages, ExtensionColle public static GuidePage compile(PageCollection pages, ExtensionCollection extensions, ParsedGuidePage parsedPage) { // Translate page tree over to layout pages - var document = new PageCompiler( + PageCompiler compiler = new PageCompiler( pages, extensions, parsedPage.getSourcePack(), parsedPage.getId(), - parsedPage.getSource()).compile(parsedPage.getAstRoot()); - var titleHeading = extractPageTitleHeading(document); - FrontmatterPageMeta pageMeta = parsedPage.getFrontmatter() != null ? parsedPage.getFrontmatter() - .parseMeta() : null; - if (pageMeta != null && pageMeta.isEmpty()) pageMeta = null; - return new GuidePage(parsedPage.getSourcePack(), parsedPage.getId(), document, titleHeading, pageMeta); + parsedPage.getSource()); + try { + var document = compiler.compile(parsedPage.getAstRoot()); + var titleHeading = extractPageTitleHeading(document); + FrontmatterPageMeta pageMeta = parsedPage.getFrontmatter() != null ? parsedPage.getFrontmatter() + .parseMeta() : null; + if (pageMeta != null && pageMeta.isEmpty()) pageMeta = null; + return new GuidePage(parsedPage.getSourcePack(), parsedPage.getId(), document, titleHeading, pageMeta); + } catch (RuntimeException t) { + String errorMessage = formatCompileFailureMessage( + parsedPage.getId(), + parsedPage.getLanguage(), + parsedPage.getSourcePack()); + logError("[GuideNH] [PageCompiler] {}", t, errorMessage); + return buildErrorGuidePage( + pages, + extensions, + parsedPage.getSourcePack(), + parsedPage.getId(), + parsedPage.getSource(), + "COMPILATION ERROR", + errorMessage + ": \n" + t); + } } /** @@ -356,15 +383,13 @@ public static Frontmatter parseFrontmatter(ResourceLocation pageId, MdAstRoot ro for (var child : root.children()) { if (child instanceof MdAstYamlFrontmatter frontmatter) { if (result != null) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Found more than one frontmatter!"); + logError("[GuideNH] [PageCompiler] Found more than one frontmatter!"); continue; } try { result = Frontmatter.parse(pageId, frontmatter.value); } catch (Exception e) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Failed to parse frontmatter for page {}", pageId, e); + logFrontmatterFailure(pageId, e); break; } } @@ -382,12 +407,15 @@ public static Frontmatter parseFrontmatterFromSource(ResourceLocation pageId, St try { return Frontmatter.parse(pageId, yamlText); } catch (Exception e) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Failed to parse frontmatter for page {}", pageId, e); + logFrontmatterFailure(pageId, e); return new Frontmatter(null, Collections.emptyMap()); } } + private static void logFrontmatterFailure(ResourceLocation pageId, Exception e) { + logWarn("[GuideNH] [PageCompiler] Failed to parse frontmatter for page {}: {}", pageId, describeThrowable(e)); + } + public static @Nullable String extractFrontmatterText(String pageContent) { if (!pageContent.startsWith("---\n")) { return null; @@ -458,6 +486,10 @@ public void compileInlineMarkdown(String source, LytFlowParent layoutParent) { if (source == null || source.isEmpty()) { return; } + if (isPlainInlineMarkdown(source)) { + layoutParent.appendText(source); + return; + } ParsedGuidePage parsed = parse(sourcePack, "en_us", pageId, source); for (MdAstAnyContent child : parsed.getAstRoot() .children()) { @@ -473,6 +505,33 @@ public void compileInlineMarkdown(String source, LytFlowParent layoutParent) { } } + private static boolean isPlainInlineMarkdown(String source) { + for (int i = 0; i < source.length(); i++) { + char current = source.charAt(i); + switch (current) { + case '<': + case '[': + case ']': + case '(': + case ')': + case '*': + case '_': + case '`': + case '\\': + case '&': + case '$': + case '=': + case '~': + case '\n': + case '\r': + return false; + default: + break; + } + } + return true; + } + public void compileBlockContextInSourceContext(List children, LytBlockContainer layoutParent) { withChildrenSourceContext(children, () -> compileBlockContext(children, layoutParent)); @@ -532,7 +591,15 @@ public void compileBlockContext(List children, LytBlo layoutChild = createErrorBlock("Unhandled MDX element in block context", child); } else { layoutChild = null; - compiler.compileBlockContext(this, layoutParent, el); + try { + compiler.compileBlockContext(this, layoutParent, el); + } catch (RuntimeException e) { + appendUnexpectedNodeError( + layoutParent, + "Failed to compile MDX block <" + el.name() + ">", + el, + e); + } } } else if (child instanceof MdAstPhrasingContent phrasingContent) { // Wrap in a paragraph with no margins, but try appending to an existing paragraph before this @@ -936,7 +1003,11 @@ private void compileFlowContent(LytFlowParent layoutParent, MdAstAnyContent cont layoutChild = createErrorFlowContent("Unhandled MDX element in flow context", content); } else { layoutChild = null; - compiler.compileFlowContext(this, layoutParent, el); + try { + compiler.compileFlowContext(this, layoutParent, el); + } catch (RuntimeException e) { + appendUnexpectedNodeError(layoutParent, "Failed to compile MDX inline <" + el.name() + ">", el, e); + } } } else { layoutChild = createErrorFlowContent("Unhandled Markdown node in flow context", content); @@ -1103,7 +1174,7 @@ private LytBlock compileCodeBlock(MdAstCode astCode) { return FileTreeCompiler.compile(this, astCode.value); } if (isFunctionGraphFence(astCode.lang)) { - return FunctionGraphFenceParser.parse(astCode.value); + return compileFunctionGraphCodeBlock(astCode); } if ("mermaid".equals(language.id())) { LytMermaidMindmap mermaidBlock = tryCompileMermaidMindmap(astCode.value); @@ -1148,6 +1219,14 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { || "functiongraph".equalsIgnoreCase(trimmed); } + private LytBlock compileFunctionGraphCodeBlock(MdAstCode astCode) { + try { + return FunctionGraphFenceParser.parse(astCode.value); + } catch (RuntimeException e) { + return createErrorBlock("Invalid function graph fence: " + describeThrowable(e), astCode); + } + } + private LytBlock compileCsvCodeBlock(MdAstCode astCode) { String source = astCode.value; List> rows = CsvTableParser.parse(source); @@ -1486,19 +1565,17 @@ private String removeLeadingWhitespace(String line, int widthToRemove) { try { String normalized = MermaidMindmapParser.normalize(source); LytMermaidMindmap block = new LytMermaidMindmap(MermaidMindmapParser.parse(normalized), normalized); - FMLLog.getLogger() - .info( - "[GuideNH] [PageCompiler] Compiled fenced Mermaid runtime block for page {} ({} chars)", - pageId, - normalized.length()); + logInfo( + "[GuideNH] [PageCompiler] Compiled fenced Mermaid runtime block for page {} ({} chars)", + pageId, + normalized.length()); return block; } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [PageCompiler] Failed to compile fenced Mermaid runtime block for page {} from source: {}", - pageId, - source, - e); + logWarn( + "[GuideNH] [PageCompiler] Failed to compile fenced Mermaid runtime block for page {} from source: {}", + e, + pageId, + source); return null; } } @@ -1552,14 +1629,12 @@ private LytImage compileImage(MdAstImage astImage) { var imageId = IdUtils.resolveLink(astImage.url, pageId); var imageContent = pages.loadAsset(imageId); if (imageContent == null) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Couldn't find image {}", astImage.url); + logError("[GuideNH] [PageCompiler] Couldn't find image {}", astImage.url); image.setTitle("Missing image: " + astImage.url); } image.setImage(imageId, imageContent); } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Invalid image id: {}", astImage.url); + logError("[GuideNH] [PageCompiler] Invalid image id: {}", astImage.url); image.setTitle("Invalid image URL: " + astImage.url); } return image; @@ -1571,23 +1646,26 @@ public LytBlock createErrorBlock(String text, UnistNode child) { return paragraph; } + private void appendUnexpectedNodeError(LytErrorSink errorSink, String message, UnistNode node, RuntimeException e) { + logWarn("[GuideNH] [PageCompiler] {} in page {}", e, message, pageId); + errorSink.appendError(this, message + ": " + describeThrowable(e), node); + } + public LytFlowContent createErrorFlowContent(String text, UnistNode child) { LytFlowSpan span = new LytFlowSpan(); span.modifyStyle( style -> style.color(SymbolicColor.ERROR_TEXT) .whiteSpace(WhiteSpaceMode.PRE)); - // Find the position in the source var position = child.position(); - if (position != null) { - var pos = position.start(); + var pos = position != null ? position.start() : null; + if (pos != null) { String sourceText = getCurrentSourceText(); - var startOfLine = sourceText.lastIndexOf('\n', pos.offset()) + 1; - var endOfLine = sourceText.indexOf('\n', pos.offset() + 1); - if (endOfLine == -1) { - endOfLine = sourceText.length(); - } - var line = sourceText.substring(startOfLine, endOfLine); + int safeOffset = Math.max(0, Math.min(pos.offset(), sourceText.length())); + var startOfLine = sourceText.lastIndexOf('\n', safeOffset) + 1; + var endOfLine = sourceText.indexOf('\n', safeOffset); + if (endOfLine == -1) endOfLine = sourceText.length(); + var line = sourceText.substring(Math.min(startOfLine, sourceText.length()), endOfLine); text += " " + child.type() + " (" + MdAstPosition.stringify(pos) + ")"; @@ -1597,20 +1675,67 @@ public LytFlowContent createErrorFlowContent(String text, UnistNode child) { span.appendText(line); span.appendBreak(); - String tildes = new String(new char[pos.column() - 1]).replace('\0', '~'); + String tildes = new String(new char[Math.max(0, pos.column() - 1)]).replace('\0', '~'); span.appendText(tildes + "^"); span.appendBreak(); - FMLLog.getLogger() - .warn("[GuideNH] [PageCompiler] {}\n{}\n{}\n", text, line, tildes + "^"); + logWarn("[GuideNH] [PageCompiler] {}\n{}\n{}\n", text, line, tildes + "^"); } else { - FMLLog.getLogger() - .warn("[GuideNH] [PageCompiler] {}\n", text); + logWarn("[GuideNH] [PageCompiler] {}\n", text); } return span; } + private static void logInfo(String message, Object... args) { + Logger logger = FMLLog.getLogger(); + if (logger != null) { + logger.info(message, args); + } + } + + private static void logWarn(String message, Object... args) { + Logger logger = FMLLog.getLogger(); + if (logger != null) { + logger.warn(message, args); + } + } + + private static void logWarn(String message, Throwable throwable, Object... args) { + Logger logger = FMLLog.getLogger(); + if (logger != null) { + logger.warn(message, appendThrowable(args, throwable)); + } + } + + private static void logError(String message, Object... args) { + Logger logger = FMLLog.getLogger(); + if (logger != null) { + logger.error(message, args); + } + } + + private static void logError(String message, Throwable throwable, Object... args) { + Logger logger = FMLLog.getLogger(); + if (logger != null) { + logger.error(message, appendThrowable(args, throwable)); + } + } + + private static Object[] appendThrowable(Object[] args, Throwable throwable) { + Object[] combined = new Object[args.length + 1]; + System.arraycopy(args, 0, combined, 0, args.length); + combined[args.length] = throwable; + return combined; + } + + private static String describeThrowable(Throwable throwable) { + String message = throwable.getMessage(); + return message != null && !message.isEmpty() ? message + : throwable.getClass() + .getSimpleName(); + } + public ResourceLocation resolveId(String idText) { return IdUtils.resolveId(idText, pageId.getResourceDomain()); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FileTreeTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FileTreeTagCompiler.java index b09bca91..99501b2e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FileTreeTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FileTreeTagCompiler.java @@ -2,6 +2,7 @@ import java.util.Collections; import java.util.Set; +import java.util.function.IntConsumer; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; @@ -44,8 +45,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl parent.append(tree); } - private static void applyOptionalIntAttribute(MdxJsxElementFields el, String name, - java.util.function.IntConsumer setter) { + private static void applyOptionalIntAttribute(MdxJsxElementFields el, String name, IntConsumer setter) { String raw = el.getAttributeString(name, null); if (raw == null || raw.isEmpty()) { return; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index a1f116c0..5b53e8fd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -35,24 +35,6 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SelectionStrategy; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.CompositeResolver; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterResolver; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxSyntaxResolver; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.WordBoundaryResolver; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ImagePathProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui.AutocompletePopup; - import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.config.ModConfig; @@ -85,6 +67,24 @@ import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; import com.hfstudio.guidenh.guide.indices.ItemMultiIndex; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SelectionStrategy; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ImagePathProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.CompositeResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FencedBlockLanguageResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxSyntaxResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.WordBoundaryResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui.AutocompletePopup; import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorMultilineTextArea; import com.hfstudio.guidenh.guide.internal.editor.guide.GuideScreenEditorAction; import com.hfstudio.guidenh.guide.internal.editor.guide.GuideScreenEditorConflictPrompt; @@ -487,9 +487,9 @@ public void initGui() { if (autocompleteResolver == null) { autocompleteResolver = new CompositeResolver( new FrontmatterResolver(), + new FencedBlockLanguageResolver(), new MdxSyntaxResolver(), - new WordBoundaryResolver() - ); + new WordBoundaryResolver()); autocompleteSelectionStrategies = SelectionStrategies.defaults(); } refreshGuideEditorDraft(true); @@ -528,7 +528,9 @@ public void updateScreen() { if (guide != null) { List pagePaths = new ArrayList<>(); for (ParsedGuidePage page : guide.getPages()) { - pagePaths.add(page.getId().toString()); + pagePaths.add( + page.getId() + .toString()); } PageReferenceProvider.setPages(pagePaths); ImagePathProvider.refreshFromGuide(guide); @@ -793,6 +795,7 @@ private void ensureGuideEditorTextArea() { if (guideEditorTextArea == null) { guideEditorTextArea = new SceneEditorMultilineTextArea(fontRendererObj); guideEditorTextArea.setDoubleClickHandler(new SceneEditorMultilineTextArea.DoubleClickHandler() { + @Override public void onDoubleClick(int cursorIndex) { if (autocompleteResolver == null) return; @@ -2129,8 +2132,8 @@ private void drawGuideEditorScreen(int mouseX, int mouseY) { } if (autocompletePopup != null && autocompletePopup.isOpen()) { - autocompletePopup.reposition(getAutocompleteAnchorX(), getAutocompleteAnchorY(), - width, height, fontRendererObj); + autocompletePopup + .reposition(getAutocompleteAnchorX(), getAutocompleteAnchorY(), width, height, fontRendererObj); autocompletePopup.draw(fontRendererObj, mouseX, mouseY); } } @@ -2140,9 +2143,9 @@ private int getAutocompleteAnchorX() { } private int getAutocompleteAnchorY() { - return getGuideEditorContentTop() - + (guideEditorTextArea != null ? guideEditorTextArea.getCursorPixelY() : 0) - + fontRendererObj.FONT_HEIGHT + AUTOCOMPLETE_CURSOR_GAP_Y; + return getGuideEditorContentTop() + (guideEditorTextArea != null ? guideEditorTextArea.getCursorPixelY() : 0) + + fontRendererObj.FONT_HEIGHT + + AUTOCOMPLETE_CURSOR_GAP_Y; } private int resolveGuideEditorDividerColor() { @@ -2432,15 +2435,19 @@ private void performAutocompleteCheck() { TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursor); if (ctx != null && ctx.shouldAutocomplete()) { - java.util.List candidates = - AutocompleteProviders.query(ctx.getAutocomplete(), 20); + List candidates = AutocompleteProviders.query(ctx.getAutocomplete(), 20); if (!candidates.isEmpty()) { if (autocompletePopup == null) { autocompletePopup = new AutocompletePopup(); } pendingAutocompleteContext = ctx.getAutocomplete(); - autocompletePopup.show(candidates, getAutocompleteAnchorX(), getAutocompleteAnchorY(), - width, height, fontRendererObj); + autocompletePopup.show( + candidates, + getAutocompleteAnchorX(), + getAutocompleteAnchorY(), + width, + height, + fontRendererObj); return; } } @@ -2464,10 +2471,11 @@ private void commitAutocompleteSelection() { String after = text.substring(ac.replaceEnd()); String replaced = selected.replacementText(); - // Auto-close quote for MDX attribute values when the closing quote is missing - if (ac instanceof MdxValueContext && !after.isEmpty() - && after.charAt(0) != '"' && after.charAt(0) != '\'' && after.charAt(0) != '}') { - replaced += "\""; + if (ac instanceof MdxValueContext) { + char missingTerminator = ((MdxValueContext) ac).getMissingValueTerminator(); + if (missingTerminator != '\0') { + replaced += missingTerminator; + } } String newText = before + replaced + after; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/csv/CsvTableParser.java b/src/main/java/com/hfstudio/guidenh/guide/internal/csv/CsvTableParser.java index 8fb91b96..d14fffaf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/csv/CsvTableParser.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/csv/CsvTableParser.java @@ -50,12 +50,27 @@ private static String normalize(String rawText) { return ""; } - String text = rawText.replace("\r\n", "\n") - .replace('\r', '\n'); - if (!text.isEmpty() && text.charAt(0) == '\uFEFF') { - return text.substring(1); + int start = rawText.charAt(0) == '\uFEFF' ? 1 : 0; + StringBuilder normalized = null; + for (int i = start; i < rawText.length(); i++) { + char current = rawText.charAt(i); + if (current == '\r') { + if (normalized == null) { + normalized = new StringBuilder(rawText.length()); + normalized.append(rawText, start, i); + } + normalized.append('\n'); + if (i + 1 < rawText.length() && rawText.charAt(i + 1) == '\n') { + i++; + } + } else if (normalized != null) { + normalized.append(current); + } + } + if (normalized != null) { + return normalized.toString(); } - return text; + return start == 0 ? rawText : rawText.substring(start); } private static void flushCell(List row, StringBuilder cell) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java index 6a3ecfab..0d6274eb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttrType.java @@ -1,9 +1,24 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete; public enum AttrType { - STRING, INT, FLOAT, BOOLEAN, COLOR, ENUM, - ITEM_ID, BLOCK_ID, ORE_DICT, ENTITY_ID, - KEY_BIND, PAGE_PATH, FILE_PATH, QUEST_UUID, - COMMAND, EXPRESSION, DOMAIN, FORMAT_PATTERN, - VECTOR3, SNBT + STRING, + INT, + FLOAT, + BOOLEAN, + COLOR, + ENUM, + ITEM_ID, + BLOCK_ID, + ORE_DICT, + ENTITY_ID, + KEY_BIND, + PAGE_PATH, + FILE_PATH, + QUEST_UUID, + COMMAND, + EXPRESSION, + DOMAIN, + FORMAT_PATTERN, + VECTOR3, + SNBT } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java index 8a640213..a927af51 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AttributeSpec.java @@ -1,6 +1,7 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete; -public final class AttributeSpec { +public class AttributeSpec { + private final String name; private final AttrType type; private final Class> enumClass; @@ -15,7 +16,15 @@ public AttributeSpec(String name, AttrType type, Class> enumCl this.enumClass = enumClass; } - public String getName() { return name; } - public AttrType getType() { return type; } - public Class> getEnumClass() { return enumClass; } + public String getName() { + return name; + } + + public AttrType getType() { + return type; + } + + public Class> getEnumClass() { + return enumClass; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java index 0aff6c5f..693cfa9e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteContext.java @@ -1,7 +1,10 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete; public interface AutocompleteContext { + int replaceStart(); + int replaceEnd(); + String getPartialText(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java index 417fac5e..d0ab7c03 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SelectionStrategy.java @@ -1,6 +1,8 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete; public interface SelectionStrategy { + int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex); + int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java index e4e538f0..f628edf9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxContextResolver.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.Nullable; public interface SyntaxContextResolver { + @Nullable TextSyntaxContext resolve(String text, int cursorIndex); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java index 33dbf1f9..63183f37 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxElementType.java @@ -6,6 +6,7 @@ public enum SyntaxElementType { TAG_START, ATTRIBUTE_NAME, ATTRIBUTE_VALUE, + FENCE_LANGUAGE, FRONTMATTER_KEY, OTHER } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java index 08e41d5d..7f3ec26d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/SyntaxUtils.java @@ -1,7 +1,7 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete; /** Shared text-scanning utilities for syntax resolvers. */ -public final class SyntaxUtils { +public class SyntaxUtils { private SyntaxUtils() {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 98401422..8dbbf72d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -1,8 +1,20 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; -public final class TagAttributeRegistry { +import com.hfstudio.guidenh.guide.document.block.AlignItems; +import com.hfstudio.guidenh.guide.document.block.chart.ChartLabelPosition; +import com.hfstudio.guidenh.guide.document.block.chart.ChartLegendPosition; +import com.hfstudio.guidenh.guide.document.block.chart.CornerLegendPosition; +import com.hfstudio.guidenh.guide.document.block.functiongraph.AutoPointLabelMode; + +public class TagAttributeRegistry { private static final Map> registry = new LinkedHashMap<>(); @@ -14,8 +26,7 @@ public static void register(String tagName, AttributeSpec... specs) { } public static List get(String tagName) { - return Collections.unmodifiableList( - registry.getOrDefault(tagName, Collections.emptyList())); + return Collections.unmodifiableList(registry.getOrDefault(tagName, Collections.emptyList())); } public static Set getRegisteredTags() { @@ -25,7 +36,8 @@ public static Set getRegisteredTags() { /** Populate the registry with all known tag→attribute mappings. */ public static void initialize() { // Inline/Flow tags - register("ItemImage", + register( + "ItemImage", new AttributeSpec("id", AttrType.ITEM_ID), new AttributeSpec("ore", AttrType.ORE_DICT), new AttributeSpec("scale", AttrType.FLOAT), @@ -36,37 +48,38 @@ public static void initialize() { new AttributeSpec("showIcon", AttrType.STRING), new AttributeSpec("label", AttrType.STRING), new AttributeSpec("format", AttrType.FORMAT_PATTERN)); - register("ItemLink", + register( + "ItemLink", new AttributeSpec("id", AttrType.ITEM_ID), new AttributeSpec("ore", AttrType.ORE_DICT), new AttributeSpec("noTooltip", AttrType.BOOLEAN), new AttributeSpec("showTooltip", AttrType.BOOLEAN), new AttributeSpec("showIcon", AttrType.STRING), new AttributeSpec("linksTo", AttrType.PAGE_PATH)); - register("BlockImage", + register( + "BlockImage", new AttributeSpec("id", AttrType.BLOCK_ID), new AttributeSpec("ore", AttrType.ORE_DICT), new AttributeSpec("scale", AttrType.FLOAT), new AttributeSpec("wrap", AttrType.STRING), new AttributeSpec("align", AttrType.STRING), new AttributeSpec("float", AttrType.STRING)); - register("FloatingImage", + register( + "FloatingImage", new AttributeSpec("src", AttrType.FILE_PATH), new AttributeSpec("align", AttrType.STRING), new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT)); - register("Color", - new AttributeSpec("id", AttrType.COLOR), - new AttributeSpec("color", AttrType.COLOR)); - register("KeyBind", - new AttributeSpec("id", AttrType.KEY_BIND), - new AttributeSpec("action", AttrType.STRING)); - register("CommandLink", + register("Color", new AttributeSpec("id", AttrType.COLOR), new AttributeSpec("color", AttrType.COLOR)); + register("KeyBind", new AttributeSpec("id", AttrType.KEY_BIND), new AttributeSpec("action", AttrType.STRING)); + register( + "CommandLink", new AttributeSpec("command", AttrType.COMMAND), new AttributeSpec("close", AttrType.BOOLEAN), new AttributeSpec("title", AttrType.STRING)); - register("Recipe", + register( + "Recipe", new AttributeSpec("id", AttrType.ITEM_ID), new AttributeSpec("fallbackText", AttrType.STRING), new AttributeSpec("handlerName", AttrType.STRING), @@ -75,7 +88,8 @@ public static void initialize() { new AttributeSpec("input", AttrType.STRING), new AttributeSpec("output", AttrType.STRING), new AttributeSpec("limit", AttrType.INT)); - register("RecipeFor", + register( + "RecipeFor", new AttributeSpec("id", AttrType.ITEM_ID), new AttributeSpec("fallbackText", AttrType.STRING), new AttributeSpec("handlerName", AttrType.STRING), @@ -84,7 +98,8 @@ public static void initialize() { new AttributeSpec("input", AttrType.STRING), new AttributeSpec("output", AttrType.STRING), new AttributeSpec("limit", AttrType.INT)); - register("RecipesFor", + register( + "RecipesFor", new AttributeSpec("id", AttrType.ITEM_ID), new AttributeSpec("fallbackText", AttrType.STRING), new AttributeSpec("handlerName", AttrType.STRING), @@ -93,61 +108,57 @@ public static void initialize() { new AttributeSpec("input", AttrType.STRING), new AttributeSpec("output", AttrType.STRING), new AttributeSpec("limit", AttrType.INT)); - register("SubPages", + register( + "SubPages", new AttributeSpec("id", AttrType.PAGE_PATH), new AttributeSpec("alphabetical", AttrType.BOOLEAN)); - register("CategoryIndex", - new AttributeSpec("category", AttrType.STRING)); - register("Structure", - new AttributeSpec("width", AttrType.INT), - new AttributeSpec("height", AttrType.INT)); - register("Mermaid", + register("CategoryIndex", new AttributeSpec("category", AttrType.STRING)); + register("Structure", new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT)); + register( + "Mermaid", new AttributeSpec("src", AttrType.FILE_PATH), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT)); - register("CsvTable", + register( + "CsvTable", new AttributeSpec("src", AttrType.FILE_PATH), new AttributeSpec("header", AttrType.BOOLEAN), new AttributeSpec("widths", AttrType.STRING)); - register("details", - new AttributeSpec("open", AttrType.BOOLEAN)); - register("Row", + register("details", new AttributeSpec("open", AttrType.BOOLEAN)); + register( + "Row", new AttributeSpec("gap", AttrType.INT), - new AttributeSpec("alignItems", AttrType.STRING), + new AttributeSpec("alignItems", AttrType.ENUM, AlignItems.class), new AttributeSpec("fullWidth", AttrType.BOOLEAN), new AttributeSpec("width", AttrType.INT)); - register("Column", + register( + "Column", new AttributeSpec("gap", AttrType.INT), - new AttributeSpec("alignItems", AttrType.STRING), + new AttributeSpec("alignItems", AttrType.ENUM, AlignItems.class), new AttributeSpec("fullWidth", AttrType.BOOLEAN), new AttributeSpec("width", AttrType.INT)); - register("a", + register( + "a", new AttributeSpec("name", AttrType.STRING), new AttributeSpec("href", AttrType.PAGE_PATH), new AttributeSpec("title", AttrType.STRING)); - register("br", - new AttributeSpec("clear", AttrType.STRING)); - register("ImportPonder", - new AttributeSpec("src", AttrType.FILE_PATH)); - register("RemoveBlocks", - new AttributeSpec("id", AttrType.BLOCK_ID)); - register("IsometricCamera", + register("br", new AttributeSpec("clear", AttrType.STRING)); + register("ImportPonder", new AttributeSpec("src", AttrType.FILE_PATH)); + register("RemoveBlocks", new AttributeSpec("id", AttrType.BLOCK_ID)); + register( + "IsometricCamera", new AttributeSpec("yaw", AttrType.FLOAT), new AttributeSpec("pitch", AttrType.FLOAT), new AttributeSpec("roll", AttrType.FLOAT)); - register("Tooltip", - new AttributeSpec("label", AttrType.STRING)); - register("mark", - new AttributeSpec("color", AttrType.COLOR)); - register("FileTree", - new AttributeSpec("indent", AttrType.INT), - new AttributeSpec("gap", AttrType.INT)); + register("Tooltip", new AttributeSpec("label", AttrType.STRING)); + register("mark", new AttributeSpec("color", AttrType.COLOR)); + register("FileTree", new AttributeSpec("indent", AttrType.INT), new AttributeSpec("gap", AttrType.INT)); register("ItemGrid"); // no attributes - uses child elements - register("FootnoteList", - new AttributeSpec("width", AttrType.INT)); + register("FootnoteList", new AttributeSpec("width", AttrType.INT)); // === Charts (all five types share CommonChartAttrs) === - register("BarChart", + register( + "BarChart", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), @@ -155,9 +166,9 @@ public static void initialize() { new AttributeSpec("border", AttrType.COLOR), new AttributeSpec("titleColor", AttrType.COLOR), new AttributeSpec("labelColor", AttrType.COLOR), - new AttributeSpec("legend", AttrType.STRING), - new AttributeSpec("labelPosition", AttrType.STRING), - new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("legend", AttrType.ENUM, ChartLegendPosition.class), + new AttributeSpec("labelPosition", AttrType.ENUM, ChartLabelPosition.class), + new AttributeSpec("cornerLegend", AttrType.ENUM, CornerLegendPosition.class), new AttributeSpec("cornerLegendWidth", AttrType.INT), new AttributeSpec("cornerLegendHeight", AttrType.INT), new AttributeSpec("cornerLegendBackground", AttrType.COLOR), @@ -179,7 +190,8 @@ public static void initialize() { new AttributeSpec("xGridColor", AttrType.COLOR), new AttributeSpec("yGridColor", AttrType.COLOR), new AttributeSpec("barWidthRatio", AttrType.FLOAT)); - register("ColumnChart", + register( + "ColumnChart", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), @@ -187,9 +199,9 @@ public static void initialize() { new AttributeSpec("border", AttrType.COLOR), new AttributeSpec("titleColor", AttrType.COLOR), new AttributeSpec("labelColor", AttrType.COLOR), - new AttributeSpec("legend", AttrType.STRING), - new AttributeSpec("labelPosition", AttrType.STRING), - new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("legend", AttrType.ENUM, ChartLegendPosition.class), + new AttributeSpec("labelPosition", AttrType.ENUM, ChartLabelPosition.class), + new AttributeSpec("cornerLegend", AttrType.ENUM, CornerLegendPosition.class), new AttributeSpec("cornerLegendWidth", AttrType.INT), new AttributeSpec("cornerLegendHeight", AttrType.INT), new AttributeSpec("cornerLegendBackground", AttrType.COLOR), @@ -211,7 +223,8 @@ public static void initialize() { new AttributeSpec("xGridColor", AttrType.COLOR), new AttributeSpec("yGridColor", AttrType.COLOR), new AttributeSpec("barWidthRatio", AttrType.FLOAT)); - register("LineChart", + register( + "LineChart", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), @@ -219,9 +232,9 @@ public static void initialize() { new AttributeSpec("border", AttrType.COLOR), new AttributeSpec("titleColor", AttrType.COLOR), new AttributeSpec("labelColor", AttrType.COLOR), - new AttributeSpec("legend", AttrType.STRING), - new AttributeSpec("labelPosition", AttrType.STRING), - new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("legend", AttrType.ENUM, ChartLegendPosition.class), + new AttributeSpec("labelPosition", AttrType.ENUM, ChartLabelPosition.class), + new AttributeSpec("cornerLegend", AttrType.ENUM, CornerLegendPosition.class), new AttributeSpec("cornerLegendWidth", AttrType.INT), new AttributeSpec("cornerLegendHeight", AttrType.INT), new AttributeSpec("cornerLegendBackground", AttrType.COLOR), @@ -244,7 +257,8 @@ public static void initialize() { new AttributeSpec("yGridColor", AttrType.COLOR), new AttributeSpec("numericX", AttrType.BOOLEAN), new AttributeSpec("showPoints", AttrType.BOOLEAN)); - register("ScatterChart", + register( + "ScatterChart", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), @@ -252,9 +266,9 @@ public static void initialize() { new AttributeSpec("border", AttrType.COLOR), new AttributeSpec("titleColor", AttrType.COLOR), new AttributeSpec("labelColor", AttrType.COLOR), - new AttributeSpec("legend", AttrType.STRING), - new AttributeSpec("labelPosition", AttrType.STRING), - new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("legend", AttrType.ENUM, ChartLegendPosition.class), + new AttributeSpec("labelPosition", AttrType.ENUM, ChartLabelPosition.class), + new AttributeSpec("cornerLegend", AttrType.ENUM, CornerLegendPosition.class), new AttributeSpec("cornerLegendWidth", AttrType.INT), new AttributeSpec("cornerLegendHeight", AttrType.INT), new AttributeSpec("cornerLegendBackground", AttrType.COLOR), @@ -274,7 +288,8 @@ public static void initialize() { new AttributeSpec("showYGrid", AttrType.BOOLEAN), new AttributeSpec("xGridColor", AttrType.COLOR), new AttributeSpec("yGridColor", AttrType.COLOR)); - register("PieChart", + register( + "PieChart", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), @@ -282,9 +297,9 @@ public static void initialize() { new AttributeSpec("border", AttrType.COLOR), new AttributeSpec("titleColor", AttrType.COLOR), new AttributeSpec("labelColor", AttrType.COLOR), - new AttributeSpec("legend", AttrType.STRING), - new AttributeSpec("labelPosition", AttrType.STRING), - new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("legend", AttrType.ENUM, ChartLegendPosition.class), + new AttributeSpec("labelPosition", AttrType.ENUM, ChartLabelPosition.class), + new AttributeSpec("cornerLegend", AttrType.ENUM, CornerLegendPosition.class), new AttributeSpec("cornerLegendWidth", AttrType.INT), new AttributeSpec("cornerLegendHeight", AttrType.INT), new AttributeSpec("cornerLegendBackground", AttrType.COLOR), @@ -292,7 +307,8 @@ public static void initialize() { new AttributeSpec("clockwise", AttrType.BOOLEAN)); // === Chart child tags === - register("Series", + register( + "Series", new AttributeSpec("name", AttrType.STRING), new AttributeSpec("data", AttrType.STRING), new AttributeSpec("points", AttrType.STRING), @@ -300,21 +316,24 @@ public static void initialize() { new AttributeSpec("icon", AttrType.ITEM_ID), new AttributeSpec("iconImage", AttrType.FILE_PATH), new AttributeSpec("tooltip", AttrType.STRING)); - register("LineSeries", + register( + "LineSeries", new AttributeSpec("name", AttrType.STRING), new AttributeSpec("data", AttrType.STRING), new AttributeSpec("color", AttrType.COLOR), new AttributeSpec("icon", AttrType.ITEM_ID), new AttributeSpec("iconImage", AttrType.FILE_PATH), new AttributeSpec("tooltip", AttrType.STRING)); - register("Slice", + register( + "Slice", new AttributeSpec("label", AttrType.STRING), new AttributeSpec("value", AttrType.FLOAT), new AttributeSpec("color", AttrType.COLOR), new AttributeSpec("icon", AttrType.ITEM_ID), new AttributeSpec("iconImage", AttrType.FILE_PATH), new AttributeSpec("tooltip", AttrType.STRING)); - register("PieInset", + register( + "PieInset", new AttributeSpec("size", AttrType.FLOAT), new AttributeSpec("position", AttrType.STRING), new AttributeSpec("startAngleDeg", AttrType.FLOAT), @@ -323,7 +342,8 @@ public static void initialize() { new AttributeSpec("titleColor", AttrType.COLOR)); // === FunctionGraph child tags === - register("Plot", + register( + "Plot", new AttributeSpec("expr", AttrType.EXPRESSION), new AttributeSpec("inverse", AttrType.BOOLEAN), new AttributeSpec("domain", AttrType.DOMAIN), @@ -331,9 +351,10 @@ public static void initialize() { new AttributeSpec("label", AttrType.STRING), new AttributeSpec("pointEveryX", AttrType.FLOAT), new AttributeSpec("pointEveryY", AttrType.FLOAT), - new AttributeSpec("autoPointLabel", AttrType.STRING), + new AttributeSpec("autoPointLabel", AttrType.ENUM, AutoPointLabelMode.class), new AttributeSpec("autoPointColor", AttrType.COLOR)); - register("Point", + register( + "Point", new AttributeSpec("x", AttrType.FLOAT), new AttributeSpec("y", AttrType.FLOAT), new AttributeSpec("color", AttrType.COLOR), @@ -345,7 +366,8 @@ public static void initialize() { // === Fix and extend existing registrations === // GameScene: add camera attributes - register("Scene", + register( + "Scene", new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), new AttributeSpec("zoom", AttrType.FLOAT), @@ -364,7 +386,8 @@ public static void initialize() { new AttributeSpec("showGrid", AttrType.BOOLEAN)); // GameScene: also register as "GameScene" with same attrs (SceneTagCompiler handles both) - register("GameScene", + register( + "GameScene", new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), new AttributeSpec("zoom", AttrType.FLOAT), @@ -383,7 +406,8 @@ public static void initialize() { new AttributeSpec("showGrid", AttrType.BOOLEAN)); // Function: add missing container + plot attrs - register("Function", + register( + "Function", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), @@ -402,7 +426,7 @@ public static void initialize() { new AttributeSpec("xRange", AttrType.STRING), new AttributeSpec("yRange", AttrType.STRING), new AttributeSpec("quadrants", AttrType.STRING), - new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.ENUM, CornerLegendPosition.class), new AttributeSpec("cornerLegendWidth", AttrType.INT), new AttributeSpec("cornerLegendHeight", AttrType.INT), new AttributeSpec("cornerLegendBackground", AttrType.COLOR), @@ -413,11 +437,12 @@ public static void initialize() { new AttributeSpec("label", AttrType.STRING), new AttributeSpec("pointEveryX", AttrType.FLOAT), new AttributeSpec("pointEveryY", AttrType.FLOAT), - new AttributeSpec("autoPointLabel", AttrType.STRING), + new AttributeSpec("autoPointLabel", AttrType.ENUM, AutoPointLabelMode.class), new AttributeSpec("autoPointColor", AttrType.COLOR)); // FunctionGraph: add missing container attrs - register("FunctionGraph", + register( + "FunctionGraph", new AttributeSpec("title", AttrType.STRING), new AttributeSpec("width", AttrType.INT), new AttributeSpec("height", AttrType.INT), @@ -436,13 +461,14 @@ public static void initialize() { new AttributeSpec("xRange", AttrType.STRING), new AttributeSpec("yRange", AttrType.STRING), new AttributeSpec("quadrants", AttrType.STRING), - new AttributeSpec("cornerLegend", AttrType.STRING), + new AttributeSpec("cornerLegend", AttrType.ENUM, CornerLegendPosition.class), new AttributeSpec("cornerLegendWidth", AttrType.INT), new AttributeSpec("cornerLegendHeight", AttrType.INT), new AttributeSpec("cornerLegendBackground", AttrType.COLOR)); // Entity: add missing rotation attrs - register("Entity", + register( + "Entity", new AttributeSpec("id", AttrType.ENTITY_ID), new AttributeSpec("data", AttrType.SNBT), new AttributeSpec("name", AttrType.STRING), @@ -463,7 +489,8 @@ public static void initialize() { new AttributeSpec("capeRotation", AttrType.STRING)); // Latex: add missing attrs - register("Latex", + register( + "Latex", new AttributeSpec("formula", AttrType.STRING), new AttributeSpec("color", AttrType.COLOR), new AttributeSpec("scale", AttrType.FLOAT), @@ -474,7 +501,8 @@ public static void initialize() { new AttributeSpec("offsetY", AttrType.INT)); // ImportStructureLib: add offset attrs - register("ImportStructureLib", + register( + "ImportStructureLib", new AttributeSpec("controller", AttrType.STRING), new AttributeSpec("piece", AttrType.STRING), new AttributeSpec("channel", AttrType.STRING), @@ -486,7 +514,8 @@ public static void initialize() { new AttributeSpec("offsetZ", AttrType.INT)); // PlaceBlock: add dx/dy/dz - register("PlaceBlock", + register( + "PlaceBlock", new AttributeSpec("id", AttrType.BLOCK_ID), new AttributeSpec("nbt", AttrType.SNBT), new AttributeSpec("x", AttrType.INT), @@ -497,7 +526,8 @@ public static void initialize() { new AttributeSpec("dz", AttrType.INT)); // ReplaceBlock: add bounds attrs - register("ReplaceBlock", + register( + "ReplaceBlock", new AttributeSpec("from", AttrType.BLOCK_ID), new AttributeSpec("to", AttrType.BLOCK_ID), new AttributeSpec("from_nbt", AttrType.SNBT), @@ -510,7 +540,8 @@ public static void initialize() { new AttributeSpec("dz", AttrType.INT)); // ImportStructure: fix types + add x/y/z - register("ImportStructure", + register( + "ImportStructure", new AttributeSpec("src", AttrType.FILE_PATH), new AttributeSpec("x", AttrType.INT), new AttributeSpec("y", AttrType.INT), @@ -520,7 +551,8 @@ public static void initialize() { new AttributeSpec("offsetZ", AttrType.INT)); // Block scene element - register("Block", + register( + "Block", new AttributeSpec("id", AttrType.BLOCK_ID), new AttributeSpec("ore", AttrType.ORE_DICT), new AttributeSpec("x", AttrType.INT), diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java index ceca85d2..07fbb7ff 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TextSyntaxContext.java @@ -2,7 +2,8 @@ import org.jetbrains.annotations.Nullable; -public final class TextSyntaxContext { +public class TextSyntaxContext { + private final SyntaxElementType elementType; private final int elementStart; private final int elementEnd; @@ -10,17 +11,31 @@ public final class TextSyntaxContext { private final AutocompleteContext autocomplete; public TextSyntaxContext(SyntaxElementType elementType, int elementStart, int elementEnd, - @Nullable AutocompleteContext autocomplete) { + @Nullable AutocompleteContext autocomplete) { this.elementType = elementType; this.elementStart = elementStart; this.elementEnd = elementEnd; this.autocomplete = autocomplete; } - public SyntaxElementType getElementType() { return elementType; } - public int getElementStart() { return elementStart; } - public int getElementEnd() { return elementEnd; } + public SyntaxElementType getElementType() { + return elementType; + } + + public int getElementStart() { + return elementStart; + } + + public int getElementEnd() { + return elementEnd; + } + @Nullable - public AutocompleteContext getAutocomplete() { return autocomplete; } - public boolean shouldAutocomplete() { return autocomplete != null; } + public AutocompleteContext getAutocomplete() { + return autocomplete; + } + + public boolean shouldAutocomplete() { + return autocomplete != null; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java index 0944f142..071c2728 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AnchorProvider.java @@ -1,7 +1,11 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; -import java.util.regex.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; @@ -10,8 +14,7 @@ /** Suggests heading anchors from the current document for href="#..." attributes. */ public class AnchorProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("a", "href")); + private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("a", "href")); private static final Pattern HEADING = Pattern.compile("^#{1,6}\\s+(.+)$", Pattern.MULTILINE); @@ -24,21 +27,27 @@ public static void setDocumentText(@Nullable String text) { } @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { String partial = ctx.getPartialText(); if (partial == null || !partial.startsWith("#")) return Collections.emptyList(); - String query = partial.substring(1).toLowerCase(); // strip leading # + String query = partial.substring(1) + .toLowerCase(); // strip leading # if (documentText == null) return Collections.emptyList(); List results = new ArrayList<>(); Matcher m = HEADING.matcher(documentText); while (m.find()) { if (results.size() >= limit) break; - String heading = m.group(1).trim(); - String anchor = heading.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", ""); + String heading = m.group(1) + .trim(); + String anchor = heading.toLowerCase() + .replaceAll("[^a-z0-9]+", "-") + .replaceAll("^-|-$", ""); if (query.isEmpty() || anchor.contains(query)) { results.add(new RegistryCandidate("#" + anchor, heading)); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java index 78f6df2a..f6afb7c4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributeNameProvider.java @@ -1,30 +1,29 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext; -/** Suggests valid attribute names for the current MDX tag. */ public class AttributeNameProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forAttr("*")); + private static final Set KEYS = Collections.singleton(AutocompleteKey.forAttr("*")); @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } - // - // TODO: Currently disabled because the resolver layer (MdxSyntaxResolver) does not - // reliably distinguish attribute-name positions from plain text. When the parser - // supports error-recovery or the resolver can accurately detect tag-internal - // whitespace without false positives, set this to true. - // - private static volatile boolean enabled = false; + private static volatile boolean enabled = true; - public static void setEnabled(boolean value) { enabled = value; } + public static void setEnabled(boolean value) { + enabled = value; + } @Override public List provide(AutocompleteContext ctx, int limit) { @@ -33,12 +32,15 @@ public List provide(AutocompleteContext ctx, int limit) { MdxAttrNameContext mdx = (MdxAttrNameContext) ctx; List specs = TagAttributeRegistry.get(mdx.getTagName()); - String partial = mdx.getPartialText().toLowerCase(); + String partial = mdx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (AttributeSpec spec : specs) { if (results.size() >= limit) break; - if (partial.isEmpty() || spec.getName().toLowerCase().contains(partial)) { + if (partial.isEmpty() || spec.getName() + .toLowerCase() + .contains(partial)) { results.add(new TextCandidate(spec.getName())); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java index 645fe782..a11d53b1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteCandidate.java @@ -3,10 +3,19 @@ import net.minecraft.client.gui.FontRenderer; public interface AutocompleteCandidate { + String displayText(); + String replacementText(); - default int renderHeight() { return 14; } + + default int renderHeight() { + return 14; + } + /** Width hint for popup sizing. Default 0 means use displayText width. */ - default int renderWidth(FontRenderer fontRenderer) { return 0; } + default int renderWidth(FontRenderer fontRenderer) { + return 0; + } + void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java index d4304934..8f6db0b5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteKey.java @@ -2,15 +2,13 @@ import java.util.Objects; -public final class AutocompleteKey { +public class AutocompleteKey { public enum MatchType { - /** Cursor right after '<' — match tag name candidates */ TAG_NAME, - /** Cursor inside tag body, not in a value — match attribute names */ ATTR_NAME, - /** Cursor inside an attribute value — match value candidates */ - ATTR_VALUE + ATTR_VALUE, + FENCE_LANGUAGE } private final MatchType type; @@ -32,18 +30,33 @@ public static AutocompleteKey forAttr(String tagName) { } public static AutocompleteKey forValue(String tagName, String attrName) { - return new AutocompleteKey(MatchType.ATTR_VALUE, - Objects.requireNonNull(tagName), Objects.requireNonNull(attrName)); + return new AutocompleteKey( + MatchType.ATTR_VALUE, + Objects.requireNonNull(tagName), + Objects.requireNonNull(attrName)); } - public MatchType getType() { return type; } - public String getTagName() { return tagName; } - public String getAttrName() { return attrName; } + public static AutocompleteKey forFenceLanguage() { + return new AutocompleteKey(MatchType.FENCE_LANGUAGE, null, null); + } + + public MatchType getType() { + return type; + } + + public String getTagName() { + return tagName; + } + + public String getAttrName() { + return attrName; + } public boolean matches(MatchType queryType, String queryTag, String queryAttr) { if (type != queryType) return false; switch (type) { case TAG_NAME: + case FENCE_LANGUAGE: return true; case ATTR_NAME: return tagName.equals("*") || tagName.equals(queryTag); @@ -59,12 +72,13 @@ public boolean matches(MatchType queryType, String queryTag, String queryAttr) { public boolean equals(Object o) { if (!(o instanceof AutocompleteKey)) return false; AutocompleteKey that = (AutocompleteKey) o; - return type == that.type && Objects.equals(tagName, that.tagName) - && Objects.equals(attrName, that.attrName); + return type == that.type && Objects.equals(tagName, that.tagName) && Objects.equals(attrName, that.attrName); } @Override - public int hashCode() { return Objects.hash(type, tagName, attrName); } + public int hashCode() { + return Objects.hash(type, tagName, attrName); + } @Override public String toString() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java index 8633c549..19acf2e5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProvider.java @@ -6,6 +6,8 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; public interface AutocompleteProvider { + Set getSupportedKeys(); + List provide(AutocompleteContext ctx, int limit); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java index 6bbfadc9..a7a24083 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -1,58 +1,110 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FenceLanguageContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.TagStartContext; -public final class AutocompleteProviders { +public class AutocompleteProviders { private static final List providers = new ArrayList<>(); + private static final Map> providersByKey = new HashMap<>(); private AutocompleteProviders() {} public static void register(AutocompleteProvider provider) { providers.add(provider); + for (AutocompleteKey key : provider.getSupportedKeys()) { + providersByKey + .computeIfAbsent( + toLookupKey(key.getType(), key.getTagName(), key.getAttrName()), + ignored -> new ArrayList<>()) + .add(provider); + } } public static List query(AutocompleteContext ctx, int limit) { List results = new ArrayList<>(); - for (AutocompleteProvider provider : providers) { - for (AutocompleteKey key : provider.getSupportedKeys()) { - if (matchesContext(key, ctx)) { - results.addAll(provider.provide(ctx, Math.max(0, limit - results.size()))); - break; - } - } + for (AutocompleteProvider provider : resolveProviders(ctx)) { + results.addAll(provider.provide(ctx, Math.max(0, limit - results.size()))); if (results.size() >= limit) break; } return results; } - private static boolean matchesContext(AutocompleteKey key, AutocompleteContext ctx) { - if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext) { - com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext mdx = - (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext) ctx; - return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, mdx.getTagName(), mdx.getAttrName()); + private static List resolveProviders(AutocompleteContext ctx) { + Set matched = new LinkedHashSet<>(); + if (ctx instanceof MdxValueContext) { + MdxValueContext mdx = (MdxValueContext) ctx; + addValueProviders(matched, mdx.getTagName(), mdx.getAttrName()); + return preserveRegistrationOrder(matched); + } + if (ctx instanceof MdxAttrNameContext) { + MdxAttrNameContext mdx = (MdxAttrNameContext) ctx; + addProviders(matched, AutocompleteKey.MatchType.ATTR_NAME, mdx.getTagName(), null); + addProviders(matched, AutocompleteKey.MatchType.ATTR_NAME, "*", null); + return preserveRegistrationOrder(matched); } - if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext) { - com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext mdx = - (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext) ctx; - return key.matches(AutocompleteKey.MatchType.ATTR_NAME, mdx.getTagName(), null); + if (ctx instanceof TagStartContext) { + addProviders(matched, AutocompleteKey.MatchType.TAG_NAME, null, null); + return preserveRegistrationOrder(matched); } - if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.TagStartContext) { - return key.matches(AutocompleteKey.MatchType.TAG_NAME, null, null); + if (ctx instanceof FenceLanguageContext) { + addProviders(matched, AutocompleteKey.MatchType.FENCE_LANGUAGE, null, null); + return preserveRegistrationOrder(matched); } - if (ctx instanceof com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) { - com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext fmc = - (com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext) ctx; + if (ctx instanceof FrontmatterContext) { + FrontmatterContext fmc = (FrontmatterContext) ctx; String attr = fmc.isValue() ? fmc.getKey() : "fm_key"; - return key.matches(AutocompleteKey.MatchType.ATTR_VALUE, "*", attr); + addValueProviders(matched, "*", attr); + return preserveRegistrationOrder(matched); + } + return providers; + } + + private static List preserveRegistrationOrder(Set matched) { + List ordered = new ArrayList<>(); + for (AutocompleteProvider provider : providers) { + if (matched.contains(provider)) { + ordered.add(provider); + } + } + return ordered; + } + + private static void addValueProviders(Set matched, String tagName, String attrName) { + addProviders(matched, AutocompleteKey.MatchType.ATTR_VALUE, tagName, attrName); + addProviders(matched, AutocompleteKey.MatchType.ATTR_VALUE, tagName, "*"); + addProviders(matched, AutocompleteKey.MatchType.ATTR_VALUE, "*", attrName); + addProviders(matched, AutocompleteKey.MatchType.ATTR_VALUE, "*", "*"); + } + + private static void addProviders(Set matched, AutocompleteKey.MatchType type, String tagName, + String attrName) { + List indexed = providersByKey.get(toLookupKey(type, tagName, attrName)); + if (indexed != null) { + matched.addAll(indexed); } - return false; + } + + private static String toLookupKey(AutocompleteKey.MatchType type, String tagName, String attrName) { + return type.name() + '\u0001' + + (tagName != null ? tagName : "") + + '\u0001' + + (attrName != null ? attrName : ""); } public static void clear() { providers.clear(); + providersByKey.clear(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java index ef0c61eb..ae601db7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BlockIdProvider.java @@ -1,6 +1,10 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import net.minecraft.block.Block; import net.minecraft.item.Item; @@ -11,9 +15,7 @@ /** Suggests Block registry names for block-related tag "id" attributes. */ public class BlockIdProvider implements AutocompleteProvider { - private static final Set KEYS = buildKeys( - "BlockImage", "PlaceBlock", "RemoveBlocks", "Block" - ); + private static final Set KEYS = buildKeys("BlockImage", "PlaceBlock", "RemoveBlocks", "Block"); private static Set buildKeys(String... tagNames) { Set keys = new HashSet<>(); @@ -28,16 +30,20 @@ private static Set buildKeys(String... tagNames) { } @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (Object obj : Block.blockRegistry.getKeys()) { if (results.size() >= limit) break; if (obj instanceof String key) { - if (partial.isEmpty() || key.toLowerCase().contains(partial)) { + if (partial.isEmpty() || key.toLowerCase() + .contains(partial)) { Block block = (Block) Block.blockRegistry.getObject(key); ItemStack stack = block != null ? new ItemStack(Item.getItemFromBlock(block)) : null; if (stack != null && stack.getItem() != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java index e780541d..86d5127c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorCandidate.java @@ -4,6 +4,7 @@ import net.minecraft.client.gui.Gui; public class ColorCandidate implements AutocompleteCandidate { + private final String name; private final int color; private static final int SWATCH_SIZE = 12; @@ -15,9 +16,20 @@ public ColorCandidate(String name, int color) { this.color = color; } - @Override public String displayText() { return name; } - @Override public String replacementText() { return name; } - @Override public int renderHeight() { return 16; } + @Override + public String displayText() { + return name; + } + + @Override + public String replacementText() { + return name; + } + + @Override + public int renderHeight() { + return 16; + } @Override public int renderWidth(FontRenderer fontRenderer) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java index c8a7e339..58d9e3d7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ColorProvider.java @@ -1,22 +1,21 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; /** Suggests SymbolicColor names for <Color id> attributes. */ public class ColorProvider implements AutocompleteProvider { - private static final String[] SYMBOLIC_NAMES = { - "LINK", "BODY_TEXT", "ERROR_TEXT", "CRAFTING_RECIPE_TYPE", "THEMATIC_BREAK", - "HEADER1_SEPARATOR", "HEADER2_SEPARATOR", "NAVBAR_BG_TOP", "NAVBAR_BG_BOTTOM", - "NAVBAR_ROW_HOVER", "NAVBAR_EXPAND_ARROW", "TABLE_BORDER", - "ICON_BUTTON_NORMAL", "ICON_BUTTON_DISABLED", "ICON_BUTTON_HOVER", - "IN_WORLD_BLOCK_HIGHLIGHT", "SCENE_BACKGROUND", "GUIDE_SCREEN_BACKGROUND", - "BLOCKQUOTE_BACKGROUND", "BLACK", "DARK_BLUE", "DARK_GREEN", "DARK_AQUA", - "DARK_RED", "DARK_PURPLE", "GOLD", "GRAY", "DARK_GRAY", "BLUE", "GREEN", - "AQUA", "RED", "LIGHT_PURPLE", "YELLOW", "WHITE" - }; + private static final String[] SYMBOLIC_NAMES = { "LINK", "BODY_TEXT", "ERROR_TEXT", "CRAFTING_RECIPE_TYPE", + "THEMATIC_BREAK", "HEADER1_SEPARATOR", "HEADER2_SEPARATOR", "NAVBAR_BG_TOP", "NAVBAR_BG_BOTTOM", + "NAVBAR_ROW_HOVER", "NAVBAR_EXPAND_ARROW", "TABLE_BORDER", "ICON_BUTTON_NORMAL", "ICON_BUTTON_DISABLED", + "ICON_BUTTON_HOVER", "IN_WORLD_BLOCK_HIGHLIGHT", "SCENE_BACKGROUND", "GUIDE_SCREEN_BACKGROUND", + "BLOCKQUOTE_BACKGROUND", "BLACK", "DARK_BLUE", "DARK_GREEN", "DARK_AQUA", "DARK_RED", "DARK_PURPLE", "GOLD", + "GRAY", "DARK_GRAY", "BLUE", "GREEN", "AQUA", "RED", "LIGHT_PURPLE", "YELLOW", "WHITE" }; @Override public Set getSupportedKeys() { @@ -25,11 +24,13 @@ public Set getSupportedKeys() { @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String name : SYMBOLIC_NAMES) { if (results.size() >= limit) break; - if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + if (partial.isEmpty() || name.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(name)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java index 34f76ff7..19065043 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/CommandProvider.java @@ -1,31 +1,40 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; -import net.minecraft.client.Minecraft; +import net.minecraft.command.ICommand; +import net.minecraftforge.client.ClientCommandHandler; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; /** Suggests registered client-side commands for <CommandLink command>. */ public class CommandProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("CommandLink", "command")); + private static final Set KEYS = Collections + .singleton(AutocompleteKey.forValue("CommandLink", "command")); @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override @SuppressWarnings("unchecked") public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); - for (Object cmdObj : net.minecraftforge.client.ClientCommandHandler.instance.getCommands().values()) { + for (Object cmdObj : ClientCommandHandler.instance.getCommands() + .values()) { if (results.size() >= limit) break; - if (cmdObj instanceof net.minecraft.command.ICommand) { - net.minecraft.command.ICommand cmd = (net.minecraft.command.ICommand) cmdObj; + if (cmdObj instanceof ICommand) { + ICommand cmd = (ICommand) cmdObj; String name = cmd.getCommandName(); - if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + if (partial.isEmpty() || name.toLowerCase() + .contains(partial)) { results.add(new TextCandidate("/" + name)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java index 2ad4e07b..9a8ad488 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/DomainProvider.java @@ -1,32 +1,36 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; /** Suggests domain interval templates for domain attributes. */ public class DomainProvider implements AutocompleteProvider { - private static final Set KEYS = new HashSet<>(Arrays.asList( - AutocompleteKey.forValue("Function", "domain"), - AutocompleteKey.forValue("Plot", "domain") - )); + private static final Set KEYS = new HashSet<>( + Arrays.asList(AutocompleteKey.forValue("Function", "domain"), AutocompleteKey.forValue("Plot", "domain"))); - private static final String[] DOMAINS = { - "-inf..inf", "-10..10", "-pi..pi", "0..2*pi", - "..0", "0..", "-5..5", "-1..1" - }; + private static final String[] DOMAINS = { "-inf..inf", "-10..10", "-pi..pi", "0..2*pi", "..0", "0..", "-5..5", + "-1..1" }; @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String d : DOMAINS) { if (results.size() >= limit) break; - if (partial.isEmpty() || d.toLowerCase().contains(partial)) { + if (partial.isEmpty() || d.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(d)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java index 5fdf35a6..ee177b98 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EntityNameProvider.java @@ -1,6 +1,9 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import net.minecraft.entity.EntityList; @@ -9,21 +12,24 @@ /** Suggests entity registry names for <Entity id> attributes. */ public class EntityNameProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("Entity", "id")); + private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("Entity", "id")); @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override @SuppressWarnings("unchecked") public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (Object obj : EntityList.stringToClassMapping.keySet()) { if (results.size() >= limit) break; if (obj instanceof String key) { - if (partial.isEmpty() || key.toLowerCase().contains(partial)) { + if (partial.isEmpty() || key.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(key)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java index 010e69e1..275ccc0d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/EnumValueProvider.java @@ -1,18 +1,18 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; +import com.hfstudio.guidenh.guide.compiler.tags.SerializedEnum; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; -import com.hfstudio.guidenh.guide.compiler.tags.SerializedEnum; - -// TODO: require TagAttributeRegistry entries with AttrType.ENUM. -// Currently no attributes use ENUM type, so this provider never fires. -/** Suggests enum values for enum-typed attributes. */ public class EnumValueProvider implements AutocompleteProvider { @Override @@ -34,18 +34,22 @@ public List provide(AutocompleteContext ctx, int limit) { MdxValueContext mdx = (MdxValueContext) ctx; for (AttributeSpec spec : TagAttributeRegistry.get(mdx.getTagName())) { - if (!spec.getName().equals(mdx.getAttrName())) continue; + if (!spec.getName() + .equals(mdx.getAttrName())) continue; if (spec.getType() != AttrType.ENUM || spec.getEnumClass() == null) continue; - String partial = mdx.getPartialText().toLowerCase(); + String partial = mdx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); - for (Enum e : spec.getEnumClass().getEnumConstants()) { + for (Enum e : spec.getEnumClass() + .getEnumConstants()) { if (results.size() >= limit) break; String name = e.name(); if (e instanceof SerializedEnum) { name = ((SerializedEnum) e).getSerializedName(); } - if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + if (partial.isEmpty() || name.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(name)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java index 9568726b..221e2826 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ExpressionProvider.java @@ -1,33 +1,36 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; /** Suggests common math expressions for <Function expr> and <Plot expr>. */ public class ExpressionProvider implements AutocompleteProvider { - private static final Set KEYS = new HashSet<>(Arrays.asList( - AutocompleteKey.forValue("Function", "expr"), - AutocompleteKey.forValue("Plot", "expr") - )); + private static final Set KEYS = new HashSet<>( + Arrays.asList(AutocompleteKey.forValue("Function", "expr"), AutocompleteKey.forValue("Plot", "expr"))); - private static final String[] EXPRESSIONS = { - "sin(x)", "cos(x)", "tan(x)", "x^2", "x^3", "sqrt(x)", - "abs(x)", "log(x)", "ln(x)", "exp(x)", "1/x", - "sin(x)*cos(x)", "x*sin(x)", "floor(x)", "ceil(x)" - }; + private static final String[] EXPRESSIONS = { "sin(x)", "cos(x)", "tan(x)", "x^2", "x^3", "sqrt(x)", "abs(x)", + "log(x)", "ln(x)", "exp(x)", "1/x", "sin(x)*cos(x)", "x*sin(x)", "floor(x)", "ceil(x)" }; @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String expr : EXPRESSIONS) { if (results.size() >= limit) break; - if (partial.isEmpty() || expr.toLowerCase().contains(partial)) { + if (partial.isEmpty() || expr.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(expr)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java index da134e30..cd69afb8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FencedBlockLanguageProvider.java @@ -1,33 +1,33 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -// TODO: re-enable when fence-block context resolver is implemented. -// Currently forTag() causes these suggestions to appear in the tag name popup. - -/** Suggests language identifiers after ``` for fenced code blocks. */ public class FencedBlockLanguageProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forTag()); - - private static final String[] LANGUAGES = { - "java", "python", "javascript", "json", "yaml", "xml", - "sh", "bash", "text", "funcgraph", "mermaid" - }; + private static final Set KEYS = Collections.singleton(AutocompleteKey.forFenceLanguage()); + private static final String[] LANGUAGES = { "text", "java", "kotlin", "scala", "groovy", "lua", "json", "yaml", + "xml", "properties", "bash", "sh", "powershell", "markdown", "csv", "mermaid", "tree", "filetree", + "funcgraph" }; @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText(); + String lower = partial != null ? partial.toLowerCase() : ""; List results = new ArrayList<>(); for (String lang : LANGUAGES) { if (results.size() >= limit) break; - if (partial.isEmpty() || lang.toLowerCase().startsWith(partial)) { + if (lower.isEmpty() || lang.toLowerCase() + .startsWith(lower)) { results.add(new TextCandidate(lang)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java index 3d6c9d0b..991a13e4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FormatPatternProvider.java @@ -1,29 +1,34 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; /** Suggests format patterns for <ItemImage format>. */ public class FormatPatternProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("ItemImage", "format")); + private static final Set KEYS = Collections + .singleton(AutocompleteKey.forValue("ItemImage", "format")); - private static final String[] PATTERNS = { - "%s", "%s items", "**%s**", "*%s*", "~~%s~~" - }; + private static final String[] PATTERNS = { "%s", "%s items", "**%s**", "*%s*", "~~%s~~" }; @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String p : PATTERNS) { if (results.size() >= limit) break; - if (partial.isEmpty() || p.toLowerCase().contains(partial)) { + if (partial.isEmpty() || p.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(p)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java index 0531469a..1a5e60c1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterKeyProvider.java @@ -1,6 +1,9 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext; @@ -8,25 +11,26 @@ /** Suggests valid YAML frontmatter top-level keys. */ public class FrontmatterKeyProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("*", "fm_key")); + private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("*", "fm_key")); - private static final String[] KEYS_LIST = { - "navigation", "item_ids", "ore_ids", "quest_ids", - "authors", "author", "date", "updated", "zoom" - }; + private static final String[] KEYS_LIST = { "navigation", "item_ids", "ore_ids", "quest_ids", "authors", "author", + "date", "updated", "zoom" }; @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { if (!(ctx instanceof FrontmatterContext)) return Collections.emptyList(); - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String key : KEYS_LIST) { if (results.size() >= limit) break; - if (partial.isEmpty() || key.toLowerCase().contains(partial)) { + if (partial.isEmpty() || key.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(key)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java index 806a46be..8987b916 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/FrontmatterValueProvider.java @@ -1,6 +1,12 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterContext; @@ -10,7 +16,9 @@ public class FrontmatterValueProvider implements AutocompleteProvider { private static final Map HINTS = new LinkedHashMap<>(); static { - HINTS.put("navigation", new String[]{"\n title:", "\n parent:", "\n position:", "\n icon:", "\n icon_texture:"}); + HINTS.put( + "navigation", + new String[] { "\n title:", "\n parent:", "\n position:", "\n icon:", "\n icon_texture:" }); } private static final Set KEYS = buildKeys(); @@ -24,18 +32,22 @@ private static Set buildKeys() { } @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { if (!(ctx instanceof FrontmatterContext)) return Collections.emptyList(); FrontmatterContext fmc = (FrontmatterContext) ctx; String[] suggestions = HINTS.getOrDefault(fmc.getKey(), new String[0]); - String partial = fmc.getPartialText().toLowerCase(); + String partial = fmc.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String s : suggestions) { if (results.size() >= limit) break; - if (partial.isEmpty() || s.toLowerCase().contains(partial)) { + if (partial.isEmpty() || s.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(s)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java index 4da70920..aba75178 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ImagePathProvider.java @@ -8,17 +8,20 @@ import java.util.List; import java.util.Set; -import org.jetbrains.annotations.Nullable; - import net.minecraft.client.Minecraft; import net.minecraft.client.resources.IResourcePack; import net.minecraft.client.resources.ResourcePackRepository; import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.mixins.early.fml.AccessorFMLClientHandler; + +import cpw.mods.fml.client.FMLClientHandler; /** * Suggests file paths from the guide assets directory for src attributes. @@ -26,25 +29,27 @@ */ public class ImagePathProvider implements AutocompleteProvider { - private static final Set KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - AutocompleteKey.forValue("FloatingImage", "src"), - AutocompleteKey.forValue("ImportStructure", "src"), - AutocompleteKey.forValue("ImportPonder", "src"), - AutocompleteKey.forValue("Mermaid", "src"), - AutocompleteKey.forValue("CsvTable", "src") - ))); + private static final Set KEYS = Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + AutocompleteKey.forValue("FloatingImage", "src"), + AutocompleteKey.forValue("ImportStructure", "src"), + AutocompleteKey.forValue("ImportPonder", "src"), + AutocompleteKey.forValue("Mermaid", "src"), + AutocompleteKey.forValue("CsvTable", "src")))); - private static final String[] EXTENSIONS = { - ".png", ".jpg", ".jpeg", ".gif", ".snbt", ".nbt", ".csv", ".json", ".mmd", ".md" - }; + private static final String[] EXTENSIONS = { ".png", ".jpg", ".jpeg", ".gif", ".snbt", ".nbt", ".csv", ".json", + ".mmd", ".md" }; @Nullable private static volatile List baseDirs; + @Nullable + private static volatile List candidatePaths; private static volatile boolean scanned; /** * Scans the guide's resource pack directories for asset folders matching - * page paths. This method performs the scan only once; subsequent calls + * page paths. This method performs the scan only once; subsequent calls * are no-ops once the scan has completed. */ public static void refreshFromGuide(@Nullable Guide guide) { @@ -55,13 +60,13 @@ public static void refreshFromGuide(@Nullable Guide guide) { // Collect all active resource packs (same pattern as DataDrivenGuideLoader) List packs = new ArrayList<>(); - var fmlAccessor = (com.hfstudio.guidenh.mixins.early.fml.AccessorFMLClientHandler) - cpw.mods.fml.client.FMLClientHandler.instance(); + var fmlAccessor = (AccessorFMLClientHandler) FMLClientHandler.instance(); List basePacks = fmlAccessor.guidenh$getResourcePackList(); if (basePacks != null) packs.addAll(basePacks); Minecraft mc = Minecraft.getMinecraft(); if (mc.getResourcePackRepository() != null) { - for (ResourcePackRepository.Entry entry : mc.getResourcePackRepository().getRepositoryEntries()) { + for (ResourcePackRepository.Entry entry : mc.getResourcePackRepository() + .getRepositoryEntries()) { IResourcePack rp = entry.getResourcePack(); if (rp != null) packs.add(rp); } @@ -87,6 +92,7 @@ public static void refreshFromGuide(@Nullable Guide guide) { } baseDirs = dirs.isEmpty() ? null : Collections.unmodifiableList(dirs); + candidatePaths = buildCandidatePaths(dirs); } @Override @@ -96,32 +102,44 @@ public Set getSupportedKeys() { @Override public List provide(AutocompleteContext ctx, int limit) { - if (baseDirs == null) return Collections.emptyList(); - String partial = ctx.getPartialText().toLowerCase(); + List paths = candidatePaths; + if (paths == null || paths.isEmpty()) return Collections.emptyList(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); - - for (File dir : baseDirs) { + for (String path : paths) { if (results.size() >= limit) break; - scanDir(dir, "", partial, results, limit); + if (partial.isEmpty() || path.toLowerCase() + .contains(partial)) { + results.add(new TextCandidate(path)); + } } return results; } - private void scanDir(File dir, String prefix, String partial, - List results, int limit) { + private static List buildCandidatePaths(List dirs) { + if (dirs.isEmpty()) { + return Collections.emptyList(); + } + List paths = new ArrayList<>(); + Set seen = new HashSet<>(); + for (File dir : dirs) { + scanDir(dir, "", paths, seen); + } + return Collections.unmodifiableList(paths); + } + + private static void scanDir(File dir, String prefix, List paths, Set seen) { File[] files = dir.listFiles(); if (files == null) return; for (File f : files) { - if (results.size() >= limit) return; String name = f.getName(); if (name.startsWith(".")) continue; String relPath = prefix.isEmpty() ? name : prefix + "/" + name; if (f.isDirectory()) { - scanDir(f, relPath, partial, results, limit); - } else if (matchesExtension(name)) { - if (partial.isEmpty() || relPath.toLowerCase().contains(partial)) { - results.add(new TextCandidate(relPath)); - } + scanDir(f, relPath, paths, seen); + } else if (matchesExtension(name) && seen.add(relPath)) { + paths.add(relPath); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java index fcfbe227..a274acbd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemCandidate.java @@ -10,6 +10,7 @@ import org.lwjgl.opengl.GL12; public class ItemCandidate implements AutocompleteCandidate { + private final String id; private final ItemStack stack; private static final int ICON_SIZE = 16; @@ -22,9 +23,20 @@ public ItemCandidate(String id, ItemStack stack) { this.stack = stack; } - @Override public String displayText() { return id; } - @Override public String replacementText() { return id; } - @Override public int renderHeight() { return 16; } + @Override + public String displayText() { + return id; + } + + @Override + public String replacementText() { + return id; + } + + @Override + public int renderHeight() { + return 16; + } @Override public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { @@ -37,8 +49,11 @@ public void render(FontRenderer fontRenderer, int x, int y, int width, boolean h // renderItemAndEffectIntoGUI draws in a 16x16 box; center it vertically in our 16px row renderItem.renderItemAndEffectIntoGUI( Minecraft.getMinecraft().fontRenderer, - Minecraft.getMinecraft().getTextureManager(), - stack, x, y - 1); + Minecraft.getMinecraft() + .getTextureManager(), + stack, + x, + y - 1); RenderHelper.disableStandardItemLighting(); GL11.glDisable(GL11.GL_BLEND); GL11.glPopMatrix(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java index 1f33e691..d441dfdd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java @@ -16,10 +16,15 @@ public class ItemIdProvider implements AutocompleteProvider { // Tags whose "id" attribute refers to a Minecraft item registry key private static Set KEYS = buildKeys( // item id attributes - "ItemImage", "ItemLink", "Recipe", "RecipeFor", "RecipesFor", + "ItemImage", + "ItemLink", + "Recipe", + "RecipeFor", + "RecipesFor", // recipe filter attributes - "Recipe", "RecipeFor", "RecipesFor" - ); + "Recipe", + "RecipeFor", + "RecipesFor"); // Also add these extra keys after buildKeys: static { @@ -56,7 +61,8 @@ public List provide(AutocompleteContext ctx, int limit) { for (Object obj : Item.itemRegistry.getKeys()) { if (results.size() >= limit) break; if (obj instanceof String key) { - if (lower.isEmpty() || key.toLowerCase().contains(lower)) { + if (lower.isEmpty() || key.toLowerCase() + .contains(lower)) { Item item = (Item) Item.itemRegistry.getObject(key); if (item != null) { results.add(new ItemCandidate(key, new ItemStack(item))); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java index 91733fcf..a14270fa 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/KeyBindProvider.java @@ -1,28 +1,35 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.KeyBinding; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; /** Suggests key binding descriptions for <KeyBind id> attributes. */ public class KeyBindProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("KeyBind", "id")); + private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("KeyBind", "id")); @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); - for (net.minecraft.client.settings.KeyBinding kb : Minecraft.getMinecraft().gameSettings.keyBindings) { + for (KeyBinding kb : Minecraft.getMinecraft().gameSettings.keyBindings) { if (results.size() >= limit) break; String desc = kb.getKeyDescription(); - if (partial.isEmpty() || desc.toLowerCase().contains(partial)) { + if (partial.isEmpty() || desc.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(desc)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java index 1a1503ae..0ea93dab 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/NumericValueProvider.java @@ -1,6 +1,12 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; @@ -10,16 +16,16 @@ public class NumericValueProvider implements AutocompleteProvider { private static final Map SUGGESTIONS = new LinkedHashMap<>(); static { - SUGGESTIONS.put("width", new String[]{"100", "200", "256", "300", "400", "512"}); - SUGGESTIONS.put("height", new String[]{"100", "200", "256", "300", "400"}); - SUGGESTIONS.put("scale", new String[]{"0.5", "1.0", "1.5", "2.0", "3.0"}); - SUGGESTIONS.put("zoom", new String[]{"0.5", "1.0", "1.5", "2.0", "3.0"}); - SUGGESTIONS.put("gap", new String[]{"0", "5", "10", "15", "20"}); - SUGGESTIONS.put("x", new String[]{"0"}); - SUGGESTIONS.put("y", new String[]{"0"}); - SUGGESTIONS.put("z", new String[]{"0"}); - SUGGESTIONS.put("startAngle", new String[]{"-90", "0", "90", "180"}); - SUGGESTIONS.put("barWidthRatio", new String[]{"0.5", "0.7", "0.9"}); + SUGGESTIONS.put("width", new String[] { "100", "200", "256", "300", "400", "512" }); + SUGGESTIONS.put("height", new String[] { "100", "200", "256", "300", "400" }); + SUGGESTIONS.put("scale", new String[] { "0.5", "1.0", "1.5", "2.0", "3.0" }); + SUGGESTIONS.put("zoom", new String[] { "0.5", "1.0", "1.5", "2.0", "3.0" }); + SUGGESTIONS.put("gap", new String[] { "0", "5", "10", "15", "20" }); + SUGGESTIONS.put("x", new String[] { "0" }); + SUGGESTIONS.put("y", new String[] { "0" }); + SUGGESTIONS.put("z", new String[] { "0" }); + SUGGESTIONS.put("startAngle", new String[] { "-90", "0", "90", "180" }); + SUGGESTIONS.put("barWidthRatio", new String[] { "0.5", "0.7", "0.9" }); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java index 3c763b14..4ee7fcb8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java @@ -1,6 +1,9 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import net.minecraftforge.oredict.OreDictionary; @@ -9,19 +12,22 @@ /** Suggests OreDictionary names for "ore" attributes. */ public class OreDictProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forValue("*", "ore")); + private static final Set KEYS = Collections.singleton(AutocompleteKey.forValue("*", "ore")); @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String name : OreDictionary.getOreNames()) { if (results.size() >= limit) break; - if (partial.isEmpty() || name.toLowerCase().contains(partial)) { + if (partial.isEmpty() || name.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(name)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java index a7c41a03..cd9ad779 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/PageReferenceProvider.java @@ -1,6 +1,11 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import org.jetbrains.annotations.Nullable; @@ -28,16 +33,20 @@ public static void setPages(@Nullable Collection paths) { } @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { if (pagePaths == null) return Collections.emptyList(); - String partial = ctx.getPartialText().toLowerCase(); + String partial = ctx.getPartialText() + .toLowerCase(); List results = new ArrayList<>(); for (String path : pagePaths) { if (results.size() >= limit) break; - if (partial.isEmpty() || path.toLowerCase().contains(partial)) { + if (partial.isEmpty() || path.toLowerCase() + .contains(partial)) { results.add(new TextCandidate(path)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java index e5cbf6a9..e998b600 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/RegistryCandidate.java @@ -12,6 +12,7 @@ /** Candidate displaying a registry key with optional item icon and subtitle. */ public class RegistryCandidate implements AutocompleteCandidate { + private final String key; @Nullable private final String subtitle; @@ -41,10 +42,25 @@ public RegistryCandidate(String key, @Nullable String subtitle, @Nullable ItemSt this.icon = icon; } - @Override public String displayText() { return key; } - @Override public String replacementText() { return key; } - @Override public int renderHeight() { return subtitle != null ? 28 : 16; } - @Override public int renderWidth(FontRenderer fr) { return icon != null ? ICON_SIZE + 4 + fr.getStringWidth(key) : 0; } + @Override + public String displayText() { + return key; + } + + @Override + public String replacementText() { + return key; + } + + @Override + public int renderHeight() { + return subtitle != null ? 28 : 16; + } + + @Override + public int renderWidth(FontRenderer fr) { + return icon != null ? ICON_SIZE + 4 + fr.getStringWidth(key) : 0; + } @Override public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { @@ -58,8 +74,11 @@ public void render(FontRenderer fontRenderer, int x, int y, int width, boolean h renderItem.zLevel = 0; renderItem.renderItemAndEffectIntoGUI( Minecraft.getMinecraft().fontRenderer, - Minecraft.getMinecraft().getTextureManager(), - icon, x, y - 1); + Minecraft.getMinecraft() + .getTextureManager(), + icon, + x, + y - 1); RenderHelper.disableStandardItemLighting(); GL11.glDisable(GL11.GL_BLEND); GL11.glPopMatrix(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java index 19c6cdd5..302c98ca 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java @@ -1,50 +1,34 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -// -// TODO: Disabled. Right-click context menu provides tag insertion as an alternative. -// The tag name popup was unreliable — caching wrong after parse errors — and the -// self-closing suffix (/>) was inappropriate for container tags like Row, Column, -// Scene, etc. Revisit after parser layer supports error-recovery. -// -/** Suggests MDX tag names when cursor is right after '<'. */ public class TagNameProvider implements AutocompleteProvider { - private static final Set KEYS = - Collections.singleton(AutocompleteKey.forTag()); - - private static volatile boolean enabled = false; - - public static void setEnabled(boolean value) { enabled = value; } - - // Grouped by category for future grouping in UI - private static final String[] TAG_NAMES = { - // Inline/Flow - "a", "br", "Tooltip", "ItemImage", "ItemLink", "BlockImage", "Color", - "CommandLink", "kbd", "KeyBind", "Latex", "mark", "PlayerName", - "sub", "sup", "FloatingImage", - // Block/Container - "Row", "Column", "div", "details", "CategoryIndex", "CsvTable", - "FileTree", "FootnoteList", "ItemGrid", "Mermaid", - "Recipe", "RecipeFor", "RecipesFor", "Structure", "SubPages", - // Charts - "ColumnChart", "BarChart", "LineChart", "PieChart", "ScatterChart", - // Math - "FunctionGraph", "Function", - // Scene - "GameScene", "Scene", "Block", "Entity", "PlaceBlock", - "ReplaceBlock", "RemoveBlocks", "ImportStructure", "ImportStructureLib", - "ImportPonder", "IsometricCamera", - // Annotations - "BlockAnnotation", "BoxAnnotation", "LineAnnotation", - "DiamondAnnotation", "TextAnnotation", "BlockAnnotationTemplate" - }; + private static final Set KEYS = Collections.singleton(AutocompleteKey.forTag()); + private static volatile boolean enabled = true; + + private static final String[] TAG_NAMES = { "a", "br", "Tooltip", "ItemImage", "ItemLink", "BlockImage", "Color", + "CommandLink", "kbd", "KeyBind", "Latex", "mark", "PlayerName", "sub", "sup", "FloatingImage", "Row", "Column", + "div", "details", "CategoryIndex", "CsvTable", "FileTree", "FootnoteList", "ItemGrid", "Mermaid", "Recipe", + "RecipeFor", "RecipesFor", "Structure", "SubPages", "ColumnChart", "BarChart", "LineChart", "PieChart", + "ScatterChart", "FunctionGraph", "Function", "GameScene", "Scene", "Block", "Entity", "PlaceBlock", + "ReplaceBlock", "RemoveBlocks", "ImportStructure", "ImportStructureLib", "ImportPonder", "IsometricCamera", + "BlockAnnotation", "BoxAnnotation", "LineAnnotation", "DiamondAnnotation", "TextAnnotation", + "BlockAnnotationTemplate" }; + + public static void setEnabled(boolean value) { + enabled = value; + } @Override - public Set getSupportedKeys() { return KEYS; } + public Set getSupportedKeys() { + return KEYS; + } @Override public List provide(AutocompleteContext ctx, int limit) { @@ -54,8 +38,9 @@ public List provide(AutocompleteContext ctx, int limit) { List results = new ArrayList<>(); for (String name : TAG_NAMES) { if (results.size() >= limit) break; - if (lower.isEmpty() || name.toLowerCase().startsWith(lower)) { - results.add(new TextCandidate(name + " />")); + if (lower.isEmpty() || name.toLowerCase() + .startsWith(lower)) { + results.add(new TextCandidate(name)); } } return results; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java index fda73990..fd4cdb10 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TextCandidate.java @@ -3,6 +3,7 @@ import net.minecraft.client.gui.FontRenderer; public class TextCandidate implements AutocompleteCandidate { + private final String text; private static final int TEXT_COLOR = 0xFFF0F0F0; @@ -10,8 +11,15 @@ public TextCandidate(String text) { this.text = text; } - @Override public String displayText() { return text; } - @Override public String replacementText() { return text; } + @Override + public String displayText() { + return text; + } + + @Override + public String replacementText() { + return text; + } @Override public void render(FontRenderer fontRenderer, int x, int y, int width, boolean hovered) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java index ba5d3c78..dc7f4855 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/CompositeResolver.java @@ -9,7 +9,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; /** Chains multiple resolvers; first non-null result wins. */ -public final class CompositeResolver implements SyntaxContextResolver { +public class CompositeResolver implements SyntaxContextResolver { private final List chain; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FenceLanguageContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FenceLanguageContext.java new file mode 100644 index 00000000..5c2a1b62 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FenceLanguageContext.java @@ -0,0 +1,31 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; + +public class FenceLanguageContext implements AutocompleteContext { + + private final int replaceStart; + private final int replaceEnd; + private final String partialText; + + public FenceLanguageContext(int replaceStart, int replaceEnd, String partialText) { + this.replaceStart = replaceStart; + this.replaceEnd = replaceEnd; + this.partialText = partialText; + } + + @Override + public int replaceStart() { + return replaceStart; + } + + @Override + public int replaceEnd() { + return replaceEnd; + } + + @Override + public String getPartialText() { + return partialText; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FencedBlockLanguageResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FencedBlockLanguageResolver.java new file mode 100644 index 00000000..a9f7ab73 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FencedBlockLanguageResolver.java @@ -0,0 +1,123 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; + +public class FencedBlockLanguageResolver implements SyntaxContextResolver { + + @Override + @Nullable + public TextSyntaxContext resolve(String text, int cursorIndex) { + if (text == null || cursorIndex < 0 || cursorIndex > text.length()) { + return null; + } + int lineStart = findLineStart(text, cursorIndex); + int fenceStart = skipLeadingSpaces(text, lineStart, cursorIndex); + if (fenceStart + 3 > cursorIndex || !startsWithFence(text, fenceStart)) { + return null; + } + if (!isOpeningFence(text, lineStart)) { + return null; + } + int languageStart = fenceStart + 3; + int replaceStart = skipSpaces(text, languageStart, cursorIndex); + if (cursorIndex < replaceStart || containsWhitespace(text, replaceStart, cursorIndex)) { + return null; + } + String partial = text.substring(replaceStart, cursorIndex); + return new TextSyntaxContext( + SyntaxElementType.FENCE_LANGUAGE, + replaceStart, + cursorIndex, + new FenceLanguageContext(replaceStart, cursorIndex, partial)); + } + + private static int findLineStart(String text, int cursorIndex) { + int pos = Math.min(cursorIndex, text.length()); + while (pos > 0) { + char previous = text.charAt(pos - 1); + if (previous == '\n' || previous == '\r') { + break; + } + pos--; + } + return pos; + } + + private static int skipLeadingSpaces(String text, int lineStart, int cursorIndex) { + int pos = lineStart; + int max = Math.min(cursorIndex, text.length()); + while (pos < max && text.charAt(pos) == ' ') { + pos++; + } + return pos; + } + + private static int skipSpaces(String text, int start, int cursorIndex) { + int pos = start; + int max = Math.min(cursorIndex, text.length()); + while (pos < max && text.charAt(pos) == ' ') { + pos++; + } + return pos; + } + + private static boolean startsWithFence(String text, int fenceStart) { + return isFenceRun(text, fenceStart, '`') || isFenceRun(text, fenceStart, '~'); + } + + private static boolean isFenceRun(String text, int fenceStart, char marker) { + return fenceStart + 2 < text.length() && text.charAt(fenceStart) == marker + && text.charAt(fenceStart + 1) == marker + && text.charAt(fenceStart + 2) == marker; + } + + private static boolean isOpeningFence(String text, int currentLineStart) { + int fenceCount = 0; + int lineStart = 0; + while (lineStart < currentLineStart) { + int lineEnd = findLineEnd(text, lineStart); + int fenceStart = skipLeadingSpaces(text, lineStart, lineEnd); + if (startsWithFence(text, fenceStart)) { + fenceCount++; + } + lineStart = nextLineStart(text, lineEnd); + } + return fenceCount % 2 == 0; + } + + private static int findLineEnd(String text, int lineStart) { + int pos = lineStart; + while (pos < text.length()) { + char c = text.charAt(pos); + if (c == '\n' || c == '\r') { + break; + } + pos++; + } + return pos; + } + + private static int nextLineStart(String text, int lineEnd) { + int pos = lineEnd; + if (pos < text.length() && text.charAt(pos) == '\r') { + pos++; + } + if (pos < text.length() && text.charAt(pos) == '\n') { + pos++; + } + return pos; + } + + private static boolean containsWhitespace(String text, int start, int end) { + for (int i = start; i < end; i++) { + if (Character.isWhitespace(text.charAt(i))) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java index fd951265..b4d1dc22 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterContext.java @@ -2,7 +2,8 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -public final class FrontmatterContext implements AutocompleteContext { +public class FrontmatterContext implements AutocompleteContext { + private final String key; private final boolean isValue; private final int replaceStart; @@ -17,10 +18,26 @@ public FrontmatterContext(String key, boolean isValue, int replaceStart, int rep this.partialText = partialText; } - public String getKey() { return key; } - public boolean isValue() { return isValue; } + public String getKey() { + return key; + } - @Override public int replaceStart() { return replaceStart; } - @Override public int replaceEnd() { return replaceEnd; } - @Override public String getPartialText() { return partialText; } + public boolean isValue() { + return isValue; + } + + @Override + public int replaceStart() { + return replaceStart; + } + + @Override + public int replaceEnd() { + return replaceEnd; + } + + @Override + public String getPartialText() { + return partialText; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java index 08b4e283..9af3e201 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/FrontmatterResolver.java @@ -8,7 +8,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; /** Detects cursor position inside YAML frontmatter (between --- delimiters at document start). */ -public final class FrontmatterResolver implements SyntaxContextResolver { +public class FrontmatterResolver implements SyntaxContextResolver { private static final String DELIM = "---"; @@ -32,7 +32,8 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { int colonIdx = line.indexOf(':'); if (colonIdx < 0) return SyntaxUtils.resolveWord(text, cursorIndex); - String key = line.substring(0, colonIdx).trim(); + String key = line.substring(0, colonIdx) + .trim(); // Skip YAML comments and list markers if (key.isEmpty() || key.startsWith("#") || key.startsWith("- ")) { return SyntaxUtils.resolveWord(text, cursorIndex); @@ -58,7 +59,10 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { if (cursorIndex >= valueAbsStart && cursorIndex <= valueAbsEnd) { String partialText = text.substring(valueAbsStart, cursorIndex); - return new TextSyntaxContext(SyntaxElementType.WORD, valueAbsStart, valueAbsEnd, + return new TextSyntaxContext( + SyntaxElementType.WORD, + valueAbsStart, + valueAbsEnd, new FrontmatterContext(key, true, valueAbsStart, valueAbsEnd, partialText)); } @@ -66,9 +70,11 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { int keyStart = lineStart + line.indexOf(key); int keyEnd = keyStart + key.length(); if (cursorIndex >= keyStart && cursorIndex <= keyEnd) { - return new TextSyntaxContext(SyntaxElementType.WORD, keyStart, keyEnd, - new FrontmatterContext(key, false, keyStart, keyEnd, - text.substring(keyStart, cursorIndex))); + return new TextSyntaxContext( + SyntaxElementType.WORD, + keyStart, + keyEnd, + new FrontmatterContext(key, false, keyStart, keyEnd, text.substring(keyStart, cursorIndex))); } return SyntaxUtils.resolveWord(text, cursorIndex); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAttrNameContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAttrNameContext.java index 96bd5255..a92a4457 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAttrNameContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxAttrNameContext.java @@ -2,8 +2,8 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -/** Context for attribute NAME completion — cursor is in tag body whitespace, not in a value. */ -public final class MdxAttrNameContext implements AutocompleteContext { +public class MdxAttrNameContext implements AutocompleteContext { + private final String tagName; private final int replaceStart; private final int replaceEnd; @@ -16,9 +16,22 @@ public MdxAttrNameContext(String tagName, int replaceStart, int replaceEnd, Stri this.partialText = partialText; } - public String getTagName() { return tagName; } + public String getTagName() { + return tagName; + } + + @Override + public int replaceStart() { + return replaceStart; + } - @Override public int replaceStart() { return replaceStart; } - @Override public int replaceEnd() { return replaceEnd; } - @Override public String getPartialText() { return partialText; } + @Override + public int replaceEnd() { + return replaceEnd; + } + + @Override + public String getPartialText() { + return partialText; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index cfacb084..fce5a2f4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -1,8 +1,5 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; @@ -12,11 +9,10 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.MdastOptions; -import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import com.hfstudio.guidenh.libs.micromark.ParseException; +import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.unist.UnistNode; import com.hfstudio.guidenh.libs.unist.UnistPosition; @@ -24,52 +20,37 @@ public class MdxSyntaxResolver implements SyntaxContextResolver { private static final MdastOptions PARSE_OPTIONS = GuideMarkdownOptions.runtime(); - // Matches open MDX tag: ]*?\\s+(\\w+)\\s*=\\s*\"([^\">]*)$"); - - // - // TODO: Fallback is intentionally limited for now. - // - // When the full-document AST parse throws ParseException (e.g. mismatched MDX tags, - // unexpected closing slashes, etc.), the entire autocomplete system degrades to this - // regex-based fallback. Currently it only covers the attribute-value-in-double-quotes - // case. Missing cases that need parser-level error recovery first: - // - // - Attribute-name completion inside an open tag () - // - Single-quoted / brace-delimited / unquoted values - // - Cursor right after '=' with no opening quote yet - // - // Once the Micromark/MdastCompiler pipeline supports error-recovery (partial AST on - // failure), extend resolveFromFallback to handle these cases. The extra patterns and - // context-detection logic are ready but disabled until then. - // - - // AST cache: re-parse only when text changes; cursor-only moves walk cached tree @Nullable private String cachedText; @Nullable private MdAstRoot cachedRoot; + @Nullable + private String cachedFallbackText; @Override @Nullable public TextSyntaxContext resolve(String text, int cursorIndex) { - if (text == null || text.isEmpty()) return null; + if (text == null || text.isEmpty() || cursorIndex < 0 || cursorIndex > text.length()) return null; MdAstRoot root; + if (text.equals(cachedFallbackText)) { + return resolveFromFallback(text, cursorIndex); + } + if (text.equals(cachedText) && cachedRoot != null) { root = cachedRoot; } else { try { root = MdAst.fromMarkdown(text, PARSE_OPTIONS); - } catch (ParseException e) { + } catch (RuntimeException e) { cachedText = null; cachedRoot = null; + cachedFallbackText = text; return resolveFromFallback(text, cursorIndex); } cachedText = text; cachedRoot = root; + cachedFallbackText = null; } return resolveFromAst(root, text, cursorIndex); @@ -77,22 +58,24 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { @Nullable private TextSyntaxContext resolveFromAst(MdAstRoot root, String text, int cursorIndex) { - if (cursorIndex > 0 && text.charAt(cursorIndex - 1) == '<') { - return new TextSyntaxContext(SyntaxElementType.TAG_START, cursorIndex, cursorIndex, - new TagStartContext(cursorIndex, cursorIndex, "")); - } + TextSyntaxContext tagStart = resolveTagStart(text, cursorIndex); + if (tagStart != null) return tagStart; + MdxJsxElementFields element = findEnclosingMdxElement(root, cursorIndex); if (element != null) { return resolveMdxAttribute(element, text, cursorIndex); } - return SyntaxUtils.resolveWord(text, cursorIndex); + return resolvePlainTextWord(text, cursorIndex); } @Nullable private MdxJsxElementFields findEnclosingMdxElement(UnistNode node, int cursorIndex) { UnistPosition pos = node.position(); if (pos != null && pos.start() != null && pos.end() != null) { - if (cursorIndex < pos.start().offset() || cursorIndex > pos.end().offset()) { + if (cursorIndex < pos.start() + .offset() || cursorIndex + > pos.end() + .offset()) { return null; } } @@ -124,118 +107,352 @@ private TextSyntaxContext resolveMdxAttribute(MdxJsxElementFields element, Strin UnistPosition pos = attrNode.position(); if (pos == null || pos.start() == null || pos.end() == null) continue; - int attrStart = pos.start().offset(); - int attrEnd = pos.end().offset(); + int attrStart = pos.start() + .offset(); + int attrEnd = pos.end() + .offset(); if (cursorIndex < attrStart || cursorIndex > attrEnd) continue; - // Find the value range within the attribute text - String attrText = text.substring(attrStart, attrEnd); - int eqIdx = attrText.indexOf('='); - if (eqIdx < 0) break; // boolean attribute, no value to complete + TextSyntaxContext valueContext = resolveAttributeValue( + text, + tagName, + attr.name, + attrStart, + attrEnd, + cursorIndex); + if (valueContext != null) return valueContext; + break; + } - int valRelStart = eqIdx + 1; - while (valRelStart < attrText.length() && attrText.charAt(valRelStart) == ' ') { - valRelStart++; + UnistPosition elemPos = element.position(); + if (elemPos != null && elemPos.start() != null && elemPos.end() != null) { + int tagStart = elemPos.start() + .offset(); + int tagEnd = elemPos.end() + .offset(); + if (cursorIndex > tagStart && cursorIndex < tagEnd) { + return resolveAttributeNameFromTag(text, tagName, tagStart, tagEnd, cursorIndex); } - if (valRelStart >= attrText.length()) break; + } - char openChar = attrText.charAt(valRelStart); - char closeChar = (openChar == '{') ? '}' : openChar; // " ' or { - if (openChar == '"' || openChar == '\'' || openChar == '{') { - valRelStart++; // skip opening quote/brace - } else { - valRelStart = eqIdx + 1; // unquoted value (shouldn't happen in MDX) - closeChar = ' '; - } + return resolvePlainTextWord(text, cursorIndex); + } - int valRelEnd = attrText.length(); - if (closeChar != ' ' && valRelEnd > 0 && attrText.charAt(valRelEnd - 1) == closeChar) { - valRelEnd--; // exclude closing quote/brace - } + private TextSyntaxContext resolvePlainTextWord(String text, int cursorIndex) { + return SyntaxUtils.resolveWord(text, cursorIndex); + } + + @Nullable + private TextSyntaxContext resolveFromFallback(String text, int cursorIndex) { + TextSyntaxContext tagStart = resolveTagStart(text, cursorIndex); + if (tagStart != null) return tagStart; + + OpenTag tag = findOpenTagBeforeCursor(text, cursorIndex); + if (tag == null) { + return resolvePlainTextWord(text, cursorIndex); + } + return resolveFromOpenTag(text, tag, cursorIndex); + } - int valueStart = attrStart + valRelStart; - int valueEnd = attrStart + valRelEnd; + @Nullable + private TextSyntaxContext resolveTagStart(String text, int cursorIndex) { + int start = cursorIndex - 1; + if (start < 0 || text.charAt(start) != '<') { + return null; + } + if (start + 1 < text.length()) { + char next = text.charAt(start + 1); + if (next == '/' || next == '!' || next == '?') { + return null; + } + } + return new TextSyntaxContext( + SyntaxElementType.TAG_START, + cursorIndex, + cursorIndex, + new TagStartContext(cursorIndex, cursorIndex, "")); + } - String partialText = text.substring( - Math.max(valueStart, 0), - Math.min(Math.max(cursorIndex, valueStart), text.length())); + @Nullable + private TextSyntaxContext resolveFromOpenTag(String text, OpenTag tag, int cursorIndex) { + int tagNameEnd = tag.nameStart + tag.name.length(); + if (cursorIndex <= tagNameEnd && cursorIndex >= tag.nameStart) { + String partial = text.substring(tag.nameStart, cursorIndex); + return new TextSyntaxContext( + SyntaxElementType.TAG_START, + tag.nameStart, + tagNameEnd, + new TagStartContext(tag.nameStart, tagNameEnd, partial)); + } + AttributeValueSpan valueSpan = findAttributeValueAtCursor(text, tag, cursorIndex); + if (valueSpan != null) { + String partial = text.substring(valueSpan.valueStart, cursorIndex); return new TextSyntaxContext( SyntaxElementType.ATTRIBUTE_VALUE, - valueStart, - valueEnd, - new MdxValueContext(tagName, attr.name, valueStart, valueEnd, partialText)); + valueSpan.valueStart, + valueSpan.valueEnd, + new MdxValueContext( + tag.name, + valueSpan.name, + valueSpan.valueStart, + valueSpan.valueEnd, + partial, + valueSpan.missingValueTerminator)); } - // ATTRIBUTE_NAME detection: cursor inside tag body but not in any attribute. - // AST-only — fallback cannot reliably distinguish tag-internal whitespace - // from plain-text whitespace without false positives. We fail closed. - // TODO: When parser supports error-recovery AST, enable fallback here. - com.hfstudio.guidenh.libs.unist.UnistPosition elemPos = element.position(); - if (elemPos != null && elemPos.start() != null && elemPos.end() != null) { - int elemEnd = elemPos.end().offset(); - if (cursorIndex > elemPos.start().offset() && cursorIndex < elemEnd) { - String partial = text.substring(cursorIndex, Math.min(cursorIndex + 1, text.length())); - return new TextSyntaxContext(SyntaxElementType.ATTRIBUTE_NAME, - cursorIndex, cursorIndex, - new MdxAttrNameContext(tagName, cursorIndex, cursorIndex, partial)); - } - } + return resolveAttributeNameFromTag(text, tag.name, tag.nameStart - 1, findTagEnd(text, tag), cursorIndex); + } - return resolvePlainTextWord(text, cursorIndex); + @Nullable + private TextSyntaxContext resolveAttributeValue(String text, String tagName, String attrName, int attrStart, + int attrEnd, int cursorIndex) { + int eqIdx = indexOf(text, '=', attrStart, attrEnd); + if (eqIdx < 0) return null; + + int valueStart = skipSpaces(text, eqIdx + 1, attrEnd); + if (valueStart > cursorIndex) return null; + AttributeValueBounds bounds = valueBounds(text, valueStart, attrEnd); + if (cursorIndex < bounds.valueStart || cursorIndex > bounds.valueEnd) return null; + + String partialText = text.substring(bounds.valueStart, cursorIndex); + return new TextSyntaxContext( + SyntaxElementType.ATTRIBUTE_VALUE, + bounds.valueStart, + bounds.valueEnd, + new MdxValueContext( + tagName, + attrName, + bounds.valueStart, + bounds.valueEnd, + partialText, + bounds.missingTerminator)); } - private TextSyntaxContext resolvePlainTextWord(String text, int cursorIndex) { - return SyntaxUtils.resolveWord(text, cursorIndex); + @Nullable + private TextSyntaxContext resolveAttributeNameFromTag(String text, String tagName, int tagStart, int tagEnd, + int cursorIndex) { + int scanStart = Math.max(tagStart + 1 + tagName.length(), 0); + if (cursorIndex < scanStart || cursorIndex > tagEnd) return null; + if (isInsideAnyAttributeValue(text, scanStart, tagEnd, cursorIndex)) return null; + + int nameStart = cursorIndex; + while (nameStart > scanStart && isAttributeNameChar(text.charAt(nameStart - 1))) { + nameStart--; + } + int nameEnd = cursorIndex; + while (nameEnd < tagEnd && isAttributeNameChar(text.charAt(nameEnd))) { + nameEnd++; + } + String partial = text.substring(nameStart, cursorIndex); + return new TextSyntaxContext( + SyntaxElementType.ATTRIBUTE_NAME, + nameStart, + nameEnd, + new MdxAttrNameContext(tagName, nameStart, nameEnd, partial)); } @Nullable - private TextSyntaxContext resolveFromFallback(String text, int cursorIndex) { - if (cursorIndex > 0 && text.charAt(cursorIndex - 1) == '<') { - return new TextSyntaxContext(SyntaxElementType.TAG_START, cursorIndex, cursorIndex, - new TagStartContext(cursorIndex, cursorIndex, "")); + private OpenTag findOpenTagBeforeCursor(String text, int cursorIndex) { + int start = Math.min(cursorIndex, text.length()) - 1; + for (int i = start; i >= 0; i--) { + char c = text.charAt(i); + if (c == '\n' || c == '\r') { + break; + } + if (c == '>') { + break; + } + if (c == '<') { + if (i + 1 >= text.length() || !isTagNameStart(text.charAt(i + 1))) { + return null; + } + int nameStart = i + 1; + int nameEnd = nameStart + 1; + while (nameEnd < text.length() && isTagNameChar(text.charAt(nameEnd))) { + nameEnd++; + } + return new OpenTag(nameStart, text.substring(nameStart, nameEnd)); + } + } + return null; + } + + @Nullable + private AttributeValueSpan findAttributeValueAtCursor(String text, OpenTag tag, int cursorIndex) { + int tagEnd = findTagEnd(text, tag); + int pos = tag.nameStart + tag.name.length(); + while (pos < tagEnd) { + pos = skipSpaces(text, pos, tagEnd); + if (pos >= tagEnd || text.charAt(pos) == '/' || text.charAt(pos) == '>') break; + if (!isAttributeNameStart(text.charAt(pos))) { + pos++; + continue; + } + int nameStart = pos; + pos++; + while (pos < tagEnd && isAttributeNameChar(text.charAt(pos))) { + pos++; + } + String attrName = text.substring(nameStart, pos); + int afterName = skipSpaces(text, pos, tagEnd); + if (afterName >= tagEnd || text.charAt(afterName) != '=') { + pos = afterName; + continue; + } + int rawValueStart = skipSpaces(text, afterName + 1, tagEnd); + AttributeValueBounds bounds = valueBounds(text, rawValueStart, tagEnd); + if (cursorIndex >= bounds.valueStart && cursorIndex <= bounds.valueEnd) { + return new AttributeValueSpan(attrName, bounds.valueStart, bounds.valueEnd, bounds.missingTerminator); + } + pos = Math.max(bounds.rawEnd, rawValueStart + 1); } - String prefix = text.substring(0, Math.min(cursorIndex, text.length())); - Matcher m = FALLBACK_TAG.matcher(prefix); + return null; + } - String tagName = null; - String attrName = null; - int valueStart = -1; + private static boolean isInsideAnyAttributeValue(String text, int scanStart, int tagEnd, int cursorIndex) { + int pos = scanStart; + while (pos < tagEnd) { + pos = skipSpaces(text, pos, tagEnd); + if (pos >= tagEnd || !isAttributeNameStart(text.charAt(pos))) { + pos++; + continue; + } + pos++; + while (pos < tagEnd && isAttributeNameChar(text.charAt(pos))) { + pos++; + } + int afterName = skipSpaces(text, pos, tagEnd); + if (afterName >= tagEnd || text.charAt(afterName) != '=') { + pos = afterName; + continue; + } + int rawValueStart = skipSpaces(text, afterName + 1, tagEnd); + AttributeValueBounds bounds = valueBounds(text, rawValueStart, tagEnd); + if (cursorIndex >= rawValueStart && cursorIndex <= bounds.valueEnd) { + return true; + } + pos = Math.max(bounds.rawEnd, rawValueStart + 1); + } + return false; + } - int searchFrom = 0; - while (m.find(searchFrom)) { - int vs = m.start(3); - if (vs <= cursorIndex) { - tagName = m.group(1); - attrName = m.group(2); - valueStart = vs; + private static int findTagEnd(String text, OpenTag tag) { + int start = tag.nameStart + tag.name.length(); + for (int i = start; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '>' || c == '\n' || c == '\r') { + return i; } - searchFrom = m.start() + 1; } + return text.length(); + } - if (tagName != null && attrName != null && valueStart >= 0) { - // Scan forward for the closing quote to determine replaceEnd - int valueEnd = findCloseQuote(text, cursorIndex); - String partialText = text.substring(valueStart, cursorIndex); - return new TextSyntaxContext( - SyntaxElementType.ATTRIBUTE_VALUE, - valueStart, - valueEnd, - new MdxValueContext(tagName, attrName, valueStart, valueEnd, partialText)); + private static AttributeValueBounds valueBounds(String text, int rawValueStart, int limit) { + if (rawValueStart >= limit) { + return new AttributeValueBounds(rawValueStart, rawValueStart, rawValueStart, '\0'); + } + char open = text.charAt(rawValueStart); + if (open == '"' || open == '\'' || open == '{') { + char close = open == '{' ? '}' : open; + int valueStart = rawValueStart + 1; + int rawEnd = findClosingValue(text, valueStart, limit, close); + boolean closed = rawEnd < limit && text.charAt(rawEnd) == close; + int rawValueEnd = closed ? rawEnd + 1 : rawEnd; + return new AttributeValueBounds(valueStart, rawEnd, rawValueEnd, closed ? '\0' : close); } - return resolvePlainTextWord(text, cursorIndex); + int rawEnd = rawValueStart; + while (rawEnd < limit) { + char c = text.charAt(rawEnd); + if (Character.isWhitespace(c) || c == '>' || c == '/') { + break; + } + rawEnd++; + } + return new AttributeValueBounds(rawValueStart, rawEnd, rawEnd, '\0'); } - /** Scan forward from pos for a closing double-quote, or fall back to next '>' or newline. */ - private static int findCloseQuote(String text, int pos) { - int len = text.length(); - for (int i = pos; i < len; i++) { + private static int findClosingValue(String text, int start, int limit, char close) { + for (int i = start; i < limit; i++) { char c = text.charAt(i); - if (c == '"') return i; - if (c == '>' || c == '\n') return i; + if (c == close || c == '>' || c == '\n' || c == '\r') { + return i; + } + } + return limit; + } + + private static int indexOf(String text, char target, int start, int end) { + for (int i = start; i < end; i++) { + if (text.charAt(i) == target) return i; + } + return -1; + } + + private static int skipSpaces(String text, int start, int end) { + int pos = start; + while (pos < end && text.charAt(pos) == ' ') { + pos++; + } + return pos; + } + + private static boolean isTagNameStart(char c) { + return Character.isLetter(c); + } + + private static boolean isTagNameChar(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.'; + } + + private static boolean isAttributeNameStart(char c) { + return Character.isLetter(c) || c == '_' || c == ':'; + } + + private static boolean isAttributeNameChar(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == ':' || c == '.'; + } + + private static class OpenTag { + + private final int nameStart; + private final String name; + + private OpenTag(int nameStart, String name) { + this.nameStart = nameStart; + this.name = name; + } + } + + private static class AttributeValueSpan { + + private final String name; + private final int valueStart; + private final int valueEnd; + private final char missingValueTerminator; + + private AttributeValueSpan(String name, int valueStart, int valueEnd, char missingValueTerminator) { + this.name = name; + this.valueStart = valueStart; + this.valueEnd = valueEnd; + this.missingValueTerminator = missingValueTerminator; + } + } + + private static class AttributeValueBounds { + + private final int valueStart; + private final int valueEnd; + private final int rawEnd; + private final char missingTerminator; + + private AttributeValueBounds(int valueStart, int valueEnd, int rawEnd, char missingTerminator) { + this.valueStart = valueStart; + this.valueEnd = valueEnd; + this.rawEnd = rawEnd; + this.missingTerminator = missingTerminator; } - return len; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java index 35b01da0..caadd287 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxValueContext.java @@ -3,25 +3,53 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; /** Replaces the old MdxAutocompleteContext. Carries tag name, attribute name, and replacement range. */ -public final class MdxValueContext implements AutocompleteContext { +public class MdxValueContext implements AutocompleteContext { + private final String tagName; private final String attrName; private final int replaceStart; private final int replaceEnd; private final String partialText; + private final char missingValueTerminator; public MdxValueContext(String tagName, String attrName, int replaceStart, int replaceEnd, String partialText) { + this(tagName, attrName, replaceStart, replaceEnd, partialText, '\0'); + } + + public MdxValueContext(String tagName, String attrName, int replaceStart, int replaceEnd, String partialText, + char missingValueTerminator) { this.tagName = tagName; this.attrName = attrName; this.replaceStart = replaceStart; this.replaceEnd = replaceEnd; this.partialText = partialText; + this.missingValueTerminator = missingValueTerminator; } - public String getTagName() { return tagName; } - public String getAttrName() { return attrName; } + public String getTagName() { + return tagName; + } - @Override public int replaceStart() { return replaceStart; } - @Override public int replaceEnd() { return replaceEnd; } - @Override public String getPartialText() { return partialText; } + public String getAttrName() { + return attrName; + } + + @Override + public int replaceStart() { + return replaceStart; + } + + @Override + public int replaceEnd() { + return replaceEnd; + } + + @Override + public String getPartialText() { + return partialText; + } + + public char getMissingValueTerminator() { + return missingValueTerminator; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java index 37a07b0d..d8c60996 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/SelectionStrategies.java @@ -8,7 +8,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; -public final class SelectionStrategies { +public class SelectionStrategies { private SelectionStrategies() {} @@ -16,13 +16,16 @@ public static Map defaults() { EnumMap map = new EnumMap<>(SyntaxElementType.class); map.put(SyntaxElementType.WORD, new WordSelection()); map.put(SyntaxElementType.TAG_NAME, new ElementBoundarySelection()); + map.put(SyntaxElementType.TAG_START, new ElementBoundarySelection()); map.put(SyntaxElementType.ATTRIBUTE_NAME, new ElementBoundarySelection()); map.put(SyntaxElementType.ATTRIBUTE_VALUE, new ElementBoundarySelection()); + map.put(SyntaxElementType.FENCE_LANGUAGE, new ElementBoundarySelection()); map.put(SyntaxElementType.OTHER, new NoOpSelection()); return map; } - public static final class WordSelection implements SelectionStrategy { + public static class WordSelection implements SelectionStrategy { + @Override public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex) { int pos = cursorIndex; @@ -43,7 +46,8 @@ public int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex) } } - public static final class ElementBoundarySelection implements SelectionStrategy { + public static class ElementBoundarySelection implements SelectionStrategy { + @Override public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex) { return ctx.getElementStart(); @@ -55,7 +59,8 @@ public int getSelectionEnd(TextSyntaxContext ctx, String text, int cursorIndex) } } - public static final class NoOpSelection implements SelectionStrategy { + public static class NoOpSelection implements SelectionStrategy { + @Override public int getSelectionStart(TextSyntaxContext ctx, String text, int cursorIndex) { return cursorIndex; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java index d8b103cb..93e79772 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/TagStartContext.java @@ -2,8 +2,8 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; -/** Context for tag NAME completion — cursor right after '<', before tag name. */ -public final class TagStartContext implements AutocompleteContext { +public class TagStartContext implements AutocompleteContext { + private final int replaceStart; private final int replaceEnd; private final String partialText; @@ -14,7 +14,18 @@ public TagStartContext(int replaceStart, int replaceEnd, String partialText) { this.partialText = partialText; } - @Override public int replaceStart() { return replaceStart; } - @Override public int replaceEnd() { return replaceEnd; } - @Override public String getPartialText() { return partialText; } + @Override + public int replaceStart() { + return replaceStart; + } + + @Override + public int replaceEnd() { + return replaceEnd; + } + + @Override + public String getPartialText() { + return partialText; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java index b8ba6d99..c1d828b5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/WordBoundaryResolver.java @@ -7,7 +7,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; -public final class WordBoundaryResolver implements SyntaxContextResolver { +public class WordBoundaryResolver implements SyntaxContextResolver { @Override @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java index b502bd3e..e49737b5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java @@ -4,19 +4,19 @@ import java.util.Collections; import java.util.List; -import org.jetbrains.annotations.Nullable; -import org.lwjgl.opengl.GL11; - import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; + import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorPopupLayout; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; -public final class AutocompletePopup { +public class AutocompletePopup { public static final int MAX_VISIBLE_ITEMS = 5; public static final int PADDING_X = 6; @@ -38,10 +38,12 @@ public final class AutocompletePopup { private int x, y, width, height; private int viewportWidth, viewportHeight; - public boolean isOpen() { return open; } + public boolean isOpen() { + return open; + } - public void show(List candidates, int anchorX, int anchorY, - int viewportWidth, int viewportHeight, FontRenderer fontRenderer) { + public void show(List candidates, int anchorX, int anchorY, int viewportWidth, + int viewportHeight, FontRenderer fontRenderer) { this.candidates = candidates != null ? candidates : Collections.emptyList(); if (this.candidates.isEmpty()) { @@ -74,7 +76,9 @@ private static List candidateKeys(List list) { private static boolean keysMatch(List list, List keys) { if (list.size() != keys.size()) return false; for (int i = 0; i < list.size(); i++) { - if (!list.get(i).displayText().equals(keys.get(i))) return false; + if (!list.get(i) + .displayText() + .equals(keys.get(i))) return false; } return true; } @@ -108,8 +112,7 @@ public void scrollWheel(int delta) { } /** Recompute popup position without changing candidates or selection. */ - public void reposition(int anchorX, int anchorY, int viewportWidth, int viewportHeight, - FontRenderer fontRenderer) { + public void reposition(int anchorX, int anchorY, int viewportWidth, int viewportHeight, FontRenderer fontRenderer) { if (!open) return; placePopup(anchorX, anchorY, viewportWidth, viewportHeight); this.scrollY = clampScroll(scrollY); @@ -118,17 +121,17 @@ public void reposition(int anchorX, int anchorY, int viewportWidth, int viewport /** Place popup below the anchor; flip above if it doesn't fit. */ private void placePopup(int anchorX, int anchorY, int viewportWidth, int viewportHeight) { // Try below cursor first - LytRect below = SceneEditorPopupLayout.clampToViewport( - anchorX, anchorY, width, height, viewportWidth, viewportHeight, 2); + LytRect below = SceneEditorPopupLayout + .clampToViewport(anchorX, anchorY, width, height, viewportWidth, viewportHeight, 2); if (below.y() >= anchorY - 2) { this.x = below.x(); this.y = below.y(); return; } - // Not enough room below — flip above cursor + // Not enough room below �?flip above cursor int aboveY = anchorY - height - FLIP_GAP; - LytRect above = SceneEditorPopupLayout.clampToViewport( - anchorX, aboveY, width, height, viewportWidth, viewportHeight, 2); + LytRect above = SceneEditorPopupLayout + .clampToViewport(anchorX, aboveY, width, height, viewportWidth, viewportHeight, 2); this.x = above.x(); this.y = above.y(); } @@ -174,7 +177,10 @@ public void draw(FontRenderer fontRenderer, int mouseX, int mouseY) { if (itemIndex == selectedIndex) { Gui.drawRect(x + 1, drawY - 1, x + width - SCROLLBAR_W - 1, drawY + itemHeight - 1, HOVER_COLOR); } - candidate.render(fontRenderer, x + PADDING_X, drawY, + candidate.render( + fontRenderer, + x + PADDING_X, + drawY, width - PADDING_X * 2 - (hasScrollbar() ? SCROLLBAR_W : 0), itemIndex == selectedIndex); } @@ -258,8 +264,7 @@ private void drawScrollbar() { private boolean isInsideScrollbar(int mouseX, int mouseY) { if (!hasScrollbar()) return false; - return mouseX >= x + width - SCROLLBAR_W && mouseX < x + width - && mouseY >= y && mouseY < y + height; + return mouseX >= x + width - SCROLLBAR_W && mouseX < x + width && mouseY >= y && mouseY < y + height; } private int clampScroll(int value) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index af58f590..db8b169b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -5,13 +5,12 @@ import java.awt.datatransfer.StringSelection; import java.util.List; -import org.jetbrains.annotations.Nullable; - import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; +import org.jetbrains.annotations.Nullable; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; @@ -1025,6 +1024,7 @@ public void setDoubleClickHandler(@Nullable DoubleClickHandler handler) { } public interface DoubleClickHandler { + void onDoubleClick(int cursorIndex); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java index ef8944e0..92db78eb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java @@ -86,13 +86,13 @@ public class SceneEditorMarkdownCodec { new HashSet<>(Arrays.asList("pos", "x", "y", "z", "color", "maxWidth", "backgroundAlpha", "visible"))); public SceneEditorMarkdownParseResult parse(String markdown) { - String normalized = normalizeLineEndings(markdown); - String parseSource = MdxCommentMasker.mask(normalized); + String normalized = normalizeLineEndings(markdown != null ? markdown : ""); MdAstRoot root; try { + String parseSource = MdxCommentMasker.mask(normalized); root = MdAst.fromMarkdown(parseSource, PARSE_OPTIONS); - } catch (ParseException e) { + } catch (RuntimeException e) { return new SceneEditorMarkdownParseResult.SyntaxError(formatParseException(e)); } @@ -104,6 +104,8 @@ public SceneEditorMarkdownParseResult parse(String markdown) { return new SceneEditorMarkdownParseResult.Unsupported(e.getMessage()); } catch (InvalidSceneSyntaxException e) { return new SceneEditorMarkdownParseResult.SyntaxError(e.getMessage()); + } catch (RuntimeException e) { + return new SceneEditorMarkdownParseResult.SyntaxError(formatParseException(e)); } } @@ -1215,19 +1217,29 @@ private String normalizeLineEndings(String markdown) { return GuideStringLines.normalizeLineEndings(markdown); } - private String formatParseException(ParseException exception) { - if (exception.getFrom() == null) { - return exception.getMessage(); + private String formatParseException(RuntimeException exception) { + if (!(exception instanceof ParseException parseException)) { + return describeException(exception); + } + if (parseException.getFrom() == null) { + return parseException.getMessage(); } - return exception.getMessage() + " (line " - + exception.getFrom() + return parseException.getMessage() + " (line " + + parseException.getFrom() .line() + ", column " - + exception.getFrom() + + parseException.getFrom() .column() + ")"; } + private String describeException(RuntimeException exception) { + String message = exception.getMessage(); + return message != null && !message.isEmpty() ? message + : exception.getClass() + .getSimpleName(); + } + public static class UnsupportedSubsetException extends RuntimeException { private UnsupportedSubsetException(String message) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java index 3ac1a8ab..06764cf9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java @@ -1083,8 +1083,15 @@ private String compileCodeBlock(MdAstCode code) { } case "funcgraph", "functiongraph" -> { String src = code.value != null ? code.value : ""; - LytFunctionGraph graph = FunctionGraphFenceParser.parse(src); - return GuideSiteGraphRenderer.renderFunctionGraph(graph); + try { + LytFunctionGraph graph = FunctionGraphFenceParser.parse(src); + return GuideSiteGraphRenderer.renderFunctionGraph(graph); + } catch (RuntimeException ignored) { + return "
"
+                        + escapeHtml(src)
+                        + "
"; + } } } // Forced viewport: ``` width=220 height=96 - emits a sized scrollable container. From 26b3105df82cb9641047acdefb0b17e683c92ed6 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 15 May 2026 02:18:17 +0800 Subject: [PATCH 017/136] Update PageCompiler.java --- .../guidenh/guide/compiler/PageCompiler.java | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index d0fa7fd0..65af9634 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -445,11 +445,11 @@ public void compileBlockTagChildren(MdxJsxElementFields element, LytBlockContain return; } - ParsedGuidePage parsed = parse(sourcePack, "en_us", pageId, reparsed.source()); + MdAstRoot root = parseFragmentRoot(reparsed.source()); Map previousDefinitions = new HashMap<>(definitions); - definitions.putAll(GuideMarkdownDefinitions.collect(parsed.getAstRoot())); + definitions.putAll(GuideMarkdownDefinitions.collect(root)); try { - withSourceSlice(reparsed.source(), () -> compileBlockContext(parsed.getAstRoot(), layoutParent)); + withSourceSlice(reparsed.source(), () -> compileBlockContext(root, layoutParent)); } finally { definitions.clear(); definitions.putAll(previousDefinitions); @@ -461,9 +461,7 @@ public List reparseBlockTagChildren(MdxJsxElementFiel if (reparsed == null) { return element.children(); } - ParsedGuidePage parsed = parse(sourcePack, "en_us", pageId, reparsed.source()); - return parsed.getAstRoot() - .children(); + return parseFragmentRoot(reparsed.source()).children(); } /** @@ -490,9 +488,8 @@ public void compileInlineMarkdown(String source, LytFlowParent layoutParent) { layoutParent.appendText(source); return; } - ParsedGuidePage parsed = parse(sourcePack, "en_us", pageId, source); - for (MdAstAnyContent child : parsed.getAstRoot() - .children()) { + MdAstRoot root = parseFragmentRoot(source); + for (MdAstAnyContent child : root.children()) { if (child instanceof MdAstParagraph paragraph) { compileFlowContext(paragraph, layoutParent); } else if (child instanceof MdAstParentnestedParent) { @@ -505,6 +502,39 @@ public void compileInlineMarkdown(String source, LytFlowParent layoutParent) { } } + private MdAstRoot parseFragmentRoot(String source) { + String fragmentSource = source != null ? source : ""; + try { + fragmentSource = normalizeLineEndings(fragmentSource); + fragmentSource = FootnotePreprocessor.preprocess(fragmentSource); + MarkdownLatexShorthand.MaskResult latexMask = MarkdownLatexShorthand.mask(fragmentSource); + String parseContent = MdxCommentMasker.mask(latexMask.source()); + MdAstRoot root = MdAst.fromMarkdown(parseContent, PARSE_OPTIONS); + MarkdownLatexShorthand.restore(root, latexMask); + MarkdownHtmlRuntimeNormalizer.normalize(root); + return root; + } catch (RuntimeException e) { + UnistPoint from = e instanceof ParseException parseException ? parseException.getFrom() : null; + String errorMessage = formatFragmentParseFailureMessage(pageId, sourcePack, from); + logError("[GuideNH] [PageCompiler] {}", e, errorMessage); + return buildErrorPage(errorMessage + ": \n" + e); + } + } + + private static String formatFragmentParseFailureMessage(ResourceLocation id, String sourcePack, + @Nullable UnistPoint position) { + String positionText = ""; + if (position != null) { + positionText = " at line " + position.line() + " column " + position.column(); + } + return String.format( + Locale.ROOT, + "Failed to parse GuideME markdown fragment in page %s%s from %s", + id, + positionText, + sourcePack); + } + private static boolean isPlainInlineMarkdown(String source) { for (int i = 0; i < source.length(); i++) { char current = source.charAt(i); @@ -1675,7 +1705,7 @@ public LytFlowContent createErrorFlowContent(String text, UnistNode child) { span.appendText(line); span.appendBreak(); - String tildes = new String(new char[Math.max(0, pos.column() - 1)]).replace('\0', '~'); + String tildes = repeatChar('~', Math.max(0, pos.column() - 1)); span.appendText(tildes + "^"); span.appendBreak(); @@ -1687,6 +1717,17 @@ public LytFlowContent createErrorFlowContent(String text, UnistNode child) { return span; } + private static String repeatChar(char value, int count) { + if (count <= 0) { + return ""; + } + StringBuilder builder = new StringBuilder(count); + for (int i = 0; i < count; i++) { + builder.append(value); + } + return builder.toString(); + } + private static void logInfo(String message, Object... args) { Logger logger = FMLLog.getLogger(); if (logger != null) { From 92909a3b4fe4d21334958b7585f1d177df50ae95 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 15 May 2026 11:14:42 +0800 Subject: [PATCH 018/136] bugfix --- .../com/hfstudio/guidenh/ClientProxy.java | 4 + .../guidenh/guide/internal/GuideScreen.java | 101 +++++--- .../guidenh/guide/internal/GuidebookText.java | 1 + .../autocomplete/AutocompleteCommit.java | 26 +++ .../AutocompleteCommitService.java | 206 +++++++++++++++++ .../autocomplete/TagAttributeRegistry.java | 25 +- .../AttributePresetValueProvider.java | 64 ++++++ .../provider/AutocompleteProviders.java | 19 ++ .../provider/BooleanValueProvider.java | 44 ++++ .../autocomplete/provider/ItemIdProvider.java | 23 ++ .../provider/TagNameProvider.java | 5 +- .../resolver/MdxSyntaxResolver.java | 6 +- .../gui/SceneEditorMultilineTextArea.java | 212 ++++++++++++++++- .../editor/guide/GuideScreenEditorAction.java | 1 + .../guide/GuideScreenEditorTextActions.java | 78 ++++++- .../editor/md/SceneEditorMarkdownCodec.java | 198 +++++++++++++++- .../SceneEditorSceneNodePreviewApplier.java | 53 ++++- .../guide/scene/LytGuidebookScene.java | 24 +- .../ImportStructureLibElementCompiler.java | 74 +++++- .../StructureLibSceneOptionParser.java | 215 ++++++++++++++++++ ...TechStructureLibControllerIntegration.java | 5 +- .../StructureLibImportRequest.java | 15 +- .../StructureLibSceneOptions.java | 212 +++++++++++++++++ .../StructureLibSceneBuilder.java | 7 +- .../resources/assets/guidenh/lang/en_US.lang | 1 + .../resources/assets/guidenh/lang/zh_CN.lang | 1 + wiki/Examples-zh-CN.md | 12 +- wiki/Examples.md | 12 +- wiki/GameScene-zh-CN.md | 31 +++ wiki/GameScene.md | 33 +++ wiki/Live-Preview-zh-CN.md | 20 -- wiki/Live-Preview.md | 20 -- wiki/Tags-Reference-zh-CN.md | 5 + wiki/Tags-Reference.md | 5 + .../guidenh/guidenh/_en_us/scene-import.md | 14 ++ .../guidenh/guidenh/_zh_cn/scene-import.md | 14 ++ 36 files changed, 1671 insertions(+), 115 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommit.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanValueProvider.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/scene/element/StructureLibSceneOptionParser.java create mode 100644 src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneOptions.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 63c4ac68..4780f068 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -20,8 +20,10 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributePresetValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteProviders; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BlockIdProvider; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.BooleanValueProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.ColorProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.CommandProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.DomainProvider; @@ -99,7 +101,9 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new PageReferenceProvider()); AutocompleteProviders.register(new AnchorProvider()); AutocompleteProviders.register(new CommandProvider()); + AutocompleteProviders.register(new AttributePresetValueProvider()); AutocompleteProviders.register(new NumericValueProvider()); + AutocompleteProviders.register(new BooleanValueProvider()); AutocompleteProviders.register(new EnumValueProvider()); AutocompleteProviders.register(new ExpressionProvider()); AutocompleteProviders.register(new DomainProvider()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 5b53e8fd..5f93f23f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -67,6 +67,8 @@ import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; import com.hfstudio.guidenh.guide.indices.ItemMultiIndex; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteCommit; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteCommitService; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SelectionStrategy; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; @@ -81,7 +83,6 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FencedBlockLanguageResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.FrontmatterResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxSyntaxResolver; -import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.SelectionStrategies; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.WordBoundaryResolver; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.ui.AutocompletePopup; @@ -271,6 +272,7 @@ public class GuideScreen extends GuiScreen implements GuideUiHost, GuiYesNoCallb private long autocompleteNextQueryAtMillis; private String autocompleteLastText; private int autocompleteLastCursor; + private boolean autocompleteQueryRequestedByEdit; private static final long AUTOCOMPLETE_DEBOUNCE_MS = 100; private static final int AUTOCOMPLETE_CURSOR_GAP_Y = 14; @Nullable @@ -1302,6 +1304,17 @@ public void run() { case SELECT_ALL: guideEditorTextArea.selectAll(); return; + case FORMAT_DOCUMENT: + runGuideEditorTextMutation(new Runnable() { + + @Override + public void run() { + String formatted = GuideScreenEditorTextActions.formatDocument(guideEditorTextArea.getText()); + int cursor = Math.min(guideEditorTextArea.getCursorIndex(), formatted.length()); + guideEditorTextArea.applyEdit(formatted, cursor, cursor); + } + }); + return; default: GuideScreenEditorTextActions.Result result = GuideScreenEditorTextActions.apply( action, @@ -1379,6 +1392,8 @@ private List buildGuideEditorContextMenuEntr editEntries.add(GuideScreenEditorContextMenu.Entry.action(GuideScreenEditorAction.COPY)); editEntries.add(GuideScreenEditorContextMenu.Entry.action(GuideScreenEditorAction.PASTE)); editEntries.add(GuideScreenEditorContextMenu.Entry.action(GuideScreenEditorAction.SELECT_ALL)); + editEntries.add(GuideScreenEditorContextMenu.Entry.separator()); + editEntries.add(GuideScreenEditorContextMenu.Entry.action(GuideScreenEditorAction.FORMAT_DOCUMENT)); List insertEntries = new ArrayList<>(); insertEntries.add(GuideScreenEditorContextMenu.Entry.action(GuideScreenEditorAction.HEADING_1)); @@ -2009,7 +2024,14 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { int bottomBarH = hasBottomBar() ? TOOLBAR_H : 0; int navH = Math.max(20, panelH - TOOLBAR_H - 1 - bottomBarH); navBar.setBounds(navX, navY, navH); - navBar.update(mouseX, mouseY, guide.getNavigationTree()); + int navMouseX = mouseX; + int navMouseY = mouseY; + if (isGuideEditorActive() && isInsideGuideEditorContent(mouseX, mouseY) + && (Mouse.isButtonDown(0) || Mouse.isButtonDown(2))) { + navMouseX = Integer.MIN_VALUE; + navMouseY = Integer.MIN_VALUE; + } + navBar.update(navMouseX, navMouseY, guide.getNavigationTree()); int contentMouseX = mouseX; int contentMouseY = mouseY; if (navBar.isOpen() && navBar.contains(mouseX, mouseY)) { @@ -2132,12 +2154,25 @@ private void drawGuideEditorScreen(int mouseX, int mouseY) { } if (autocompletePopup != null && autocompletePopup.isOpen()) { + if (!isAutocompleteAnchorVisible()) { + closeAutocompletePopup(); + return; + } autocompletePopup .reposition(getAutocompleteAnchorX(), getAutocompleteAnchorY(), width, height, fontRendererObj); autocompletePopup.draw(fontRendererObj, mouseX, mouseY); } } + private boolean isAutocompleteAnchorVisible() { + if (guideEditorTextArea == null) { + return false; + } + int anchorY = getAutocompleteAnchorY(); + return guideEditorTextArea.isCursorVisibleInViewport() && anchorY >= guideEditorEditorTop + && anchorY <= getGuideEditorContentBottom(); + } + private int getAutocompleteAnchorX() { return contentX + (guideEditorTextArea != null ? guideEditorTextArea.getCursorPixelX() : 0); } @@ -2335,7 +2370,7 @@ private boolean handleGuideEditorKey(char typedChar, int keyCode) { if (autocompletePopup != null && autocompletePopup.isOpen()) { switch (keyCode) { case Keyboard.KEY_ESCAPE: - autocompletePopup.close(); + closeAutocompletePopup(); return true; case Keyboard.KEY_UP: autocompletePopup.moveSelection(-1); @@ -2349,9 +2384,7 @@ private boolean handleGuideEditorKey(char typedChar, int keyCode) { commitAutocompleteSelection(); return true; default: - // Close popup and forward key to text area so it isn't - // consumed by GuideScreen.keyTyped navigation handlers. - autocompletePopup.close(); + closeAutocompletePopup(); if (guideEditorTextArea != null) { guideEditorTextArea.keyTyped(typedChar, keyCode); updateGuideEditorTextFromArea(); @@ -2410,6 +2443,7 @@ public void run() { private void scheduleAutocompleteCheck() { if (autocompleteResolver == null || guideEditorTextArea == null) return; + autocompleteQueryRequestedByEdit = true; autocompleteNextQueryAtMillis = System.currentTimeMillis() + AUTOCOMPLETE_DEBOUNCE_MS; } @@ -2423,14 +2457,26 @@ private void performAutocompleteCheck() { boolean textChanged = firstRun || !text.equals(autocompleteLastText); boolean cursorMoved = firstRun || cursor != autocompleteLastCursor; - // Nothing changed — skip if (!textChanged && !cursorMoved) return; - // Text changed but debounce not expired — wait for typing to settle + if (!textChanged && cursorMoved) { + autocompleteLastCursor = cursor; + closeAutocompletePopup(); + return; + } + + if (!autocompleteQueryRequestedByEdit) { + autocompleteLastText = text; + autocompleteLastCursor = cursor; + closeAutocompletePopup(); + return; + } + if (textChanged && !firstRun && System.currentTimeMillis() < autocompleteNextQueryAtMillis) return; autocompleteLastText = text; autocompleteLastCursor = cursor; + autocompleteQueryRequestedByEdit = false; TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursor); @@ -2451,7 +2497,12 @@ private void performAutocompleteCheck() { return; } } + closeAutocompletePopup(); + } + + private void closeAutocompletePopup() { pendingAutocompleteContext = null; + autocompleteQueryRequestedByEdit = false; if (autocompletePopup != null && autocompletePopup.isOpen()) { autocompletePopup.close(); } @@ -2461,29 +2512,14 @@ private void commitAutocompleteSelection() { if (autocompletePopup == null || !autocompletePopup.isOpen() || guideEditorTextArea == null) return; AutocompleteCandidate selected = autocompletePopup.getSelected(); if (selected == null || pendingAutocompleteContext == null) { - autocompletePopup.close(); - pendingAutocompleteContext = null; + closeAutocompletePopup(); return; } - AutocompleteContext ac = pendingAutocompleteContext; - String text = guideEditorTextArea.getText(); - String before = text.substring(0, ac.replaceStart()); - String after = text.substring(ac.replaceEnd()); - String replaced = selected.replacementText(); - - if (ac instanceof MdxValueContext) { - char missingTerminator = ((MdxValueContext) ac).getMissingValueTerminator(); - if (missingTerminator != '\0') { - replaced += missingTerminator; - } - } - - String newText = before + replaced + after; - int newCursor = ac.replaceStart() + replaced.length(); - guideEditorTextArea.applyEdit(newText, newCursor, newCursor); + AutocompleteCommit commit = AutocompleteCommitService + .commit(guideEditorTextArea.getText(), pendingAutocompleteContext, selected); + guideEditorTextArea.applyEdit(commit.getText(), commit.getSelectionStart(), commit.getSelectionEnd()); updateGuideEditorTextFromArea(); - autocompletePopup.close(); - pendingAutocompleteContext = null; + closeAutocompletePopup(); } private boolean handleGuideEditorMouseClicked(int mouseX, int mouseY, int button) { @@ -2498,7 +2534,7 @@ private boolean handleGuideEditorMouseClicked(int mouseX, int mouseY, int button } return true; } else if (button == 0) { - autocompletePopup.close(); + closeAutocompletePopup(); } } if (guideEditorContextMenu != null && guideEditorContextMenu.isOpen()) { @@ -2693,7 +2729,6 @@ private boolean handleGuideEditorWheel(int mouseX, int mouseY, int dwheel) { autocompletePopup.scrollWheel(dwheel); return true; } - // mouse outside popup: let scroll pass through to editor } if (guideEditorContextMenu != null && guideEditorContextMenu.isOpen()) { guideEditorContextMenu.scrollWheel(mouseX, mouseY, dwheel, this.width, this.height, fontRendererObj); @@ -2702,6 +2737,7 @@ private boolean handleGuideEditorWheel(int mouseX, int mouseY, int dwheel) { if (guideEditorLayoutMode != GuideScreenEditorLayoutMode.PREVIEW_ONLY && guideEditorTextArea != null && guideEditorTextArea.contains(mouseX, mouseY)) { guideEditorTextArea.scrollWheel(dwheel); + closeAutocompletePopup(); updateGuideEditorTextFromArea(); syncGuideEditorPreviewScrollFromEditor(); return true; @@ -3734,6 +3770,11 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { if (handleGuideEditorToolbarRightClick(mouseX, mouseY, button)) { return; } + if (isGuideEditorActive() && (button == 0 || button == 2) && isInsideGuideEditorContent(mouseX, mouseY)) { + if (handleGuideEditorMouseClicked(mouseX, mouseY, button)) { + return; + } + } if (button == 0 && navBar.contains(mouseX, mouseY)) { var target = navBar.mouseClicked(mouseX, mouseY, currentAnchor != null ? currentAnchor.pageId() : null); if (target != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java index 2bee7140..6dc2eda4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java @@ -176,6 +176,7 @@ public enum GuidebookText implements LocalizationEnum { GuideEditorCopy, GuideEditorPaste, GuideEditorSelectAll, + GuideEditorFormatDocument, GuideEditorContextMenuEdit, GuideEditorContextMenuInsert, GuideEditorContextMenuBlocks, diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommit.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommit.java new file mode 100644 index 00000000..9fc4787e --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommit.java @@ -0,0 +1,26 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +public class AutocompleteCommit { + + private final String text; + private final int selectionStart; + private final int selectionEnd; + + public AutocompleteCommit(String text, int selectionStart, int selectionEnd) { + this.text = text; + this.selectionStart = selectionStart; + this.selectionEnd = selectionEnd; + } + + public String getText() { + return text; + } + + public int getSelectionStart() { + return selectionStart; + } + + public int getSelectionEnd() { + return selectionEnd; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java new file mode 100644 index 00000000..b2ae7996 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java @@ -0,0 +1,206 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete; + +import java.util.List; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxAttrNameContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.TagStartContext; + +public class AutocompleteCommitService { + + private AutocompleteCommitService() {} + + public static AutocompleteCommit commit(String text, AutocompleteContext context, AutocompleteCandidate candidate) { + String source = text != null ? text : ""; + int replaceStart = clamp(context.replaceStart(), 0, source.length()); + int replaceEnd = clamp(context.replaceEnd(), replaceStart, source.length()); + Replacement replacement = createReplacement(source, context, candidate.replacementText()); + String newText = source.substring(0, replaceStart) + replacement.text + source.substring(replaceEnd); + int cursor = replaceStart + replacement.cursorOffset; + int selectionEnd = replaceStart + replacement.selectionEndOffset; + return new AutocompleteCommit(newText, cursor, selectionEnd); + } + + private static Replacement createReplacement(String source, AutocompleteContext context, String rawText) { + String replacement = rawText != null ? rawText : ""; + if (context instanceof TagStartContext) { + return createTagReplacement(source, (TagStartContext) context, replacement); + } + if (context instanceof MdxAttrNameContext) { + return createAttributeNameReplacement(source, (MdxAttrNameContext) context, replacement); + } + if (context instanceof MdxValueContext) { + return createAttributeValueReplacement(source, (MdxValueContext) context, replacement); + } + return Replacement.cursorAtEnd(replacement); + } + + private static Replacement createTagReplacement(String source, TagStartContext context, String tagName) { + int replaceEnd = clamp(context.replaceEnd(), 0, source.length()); + int pos = skipSpaces(source, replaceEnd); + if (pos < source.length()) { + char next = source.charAt(pos); + if (next == '>' || next == '/') { + return Replacement.cursorAtEnd(tagName); + } + } + String text = tagName + " />"; + return Replacement.cursorAtEnd(text); + } + + private static Replacement createAttributeNameReplacement(String source, MdxAttrNameContext context, + String attributeName) { + AttributeSpec spec = findSpec(context.getTagName(), attributeName); + if (spec == null) { + return Replacement.cursorAtEnd(attributeName); + } + + AttrType type = spec.getType(); + String text; + int cursorOffset; + int selectionEndOffset; + if (type == AttrType.BOOLEAN) { + text = attributeName + "={true}"; + cursorOffset = attributeName.length() + 2; + selectionEndOffset = cursorOffset + 4; + } else if (shouldUseBraceValue(type)) { + text = attributeName + "={}"; + cursorOffset = text.length() - 1; + selectionEndOffset = cursorOffset; + } else { + text = attributeName + "=\"\""; + cursorOffset = text.length() - 1; + selectionEndOffset = cursorOffset; + } + return new Replacement(text, cursorOffset, selectionEndOffset); + } + + private static Replacement createAttributeValueReplacement(String source, MdxValueContext context, String value) { + int replaceStart = clamp(context.replaceStart(), 0, source.length()); + int replaceEnd = clamp(context.replaceEnd(), replaceStart, source.length()); + ValueEnvelope envelope = findValueEnvelope(source, replaceStart, replaceEnd); + String replacement = value; + int cursorOffset = replacement.length(); + int selectionEndOffset = cursorOffset; + + if (!envelope.hasDelimiter && shouldQuoteAttributeValue(context)) { + replacement = "\"" + replacement + "\""; + cursorOffset = replacement.length(); + selectionEndOffset = cursorOffset; + } else if (context.getMissingValueTerminator() != '\0' + && !endsWith(replacement, context.getMissingValueTerminator())) { + replacement += context.getMissingValueTerminator(); + cursorOffset = replacement.length() - 1; + selectionEndOffset = cursorOffset; + } + return new Replacement(replacement, cursorOffset, selectionEndOffset); + } + + private static AttributeSpec findSpec(String tagName, String attributeName) { + List specs = TagAttributeRegistry.get(tagName); + for (AttributeSpec spec : specs) { + if (spec.getName() + .equals(attributeName)) { + return spec; + } + } + return null; + } + + private static boolean shouldUseBraceValue(AttrType type) { + switch (type) { + case INT: + case FLOAT: + case VECTOR3: + case SNBT: + case EXPRESSION: + return true; + default: + return false; + } + } + + private static boolean shouldQuoteAttributeValue(MdxValueContext context) { + AttributeSpec spec = findSpec(context.getTagName(), context.getAttrName()); + if (spec == null) { + return false; + } + switch (spec.getType()) { + case INT: + case FLOAT: + case BOOLEAN: + case VECTOR3: + case SNBT: + case EXPRESSION: + return false; + default: + return true; + } + } + + private static ValueEnvelope findValueEnvelope(String source, int valueStart, int valueEnd) { + int before = valueStart - 1; + if (before >= 0) { + char open = source.charAt(before); + if (open == '"' || open == '\'' || open == '{') { + return new ValueEnvelope(true); + } + } + if (valueEnd < source.length()) { + char close = source.charAt(valueEnd); + if (close == '"' || close == '\'' || close == '}') { + return new ValueEnvelope(true); + } + } + return new ValueEnvelope(false); + } + + private static int skipSpaces(String source, int start) { + int pos = start; + while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) { + pos++; + } + return pos; + } + + private static boolean endsWith(String value, char suffix) { + return !value.isEmpty() && value.charAt(value.length() - 1) == suffix; + } + + private static int clamp(int value, int min, int max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; + } + + private static class Replacement { + + private final String text; + private final int cursorOffset; + private final int selectionEndOffset; + + private Replacement(String text, int cursorOffset, int selectionEndOffset) { + this.text = text; + this.cursorOffset = cursorOffset; + this.selectionEndOffset = selectionEndOffset; + } + + private static Replacement cursorAtEnd(String text) { + return new Replacement(text, text.length(), text.length()); + } + } + + private static class ValueEnvelope { + + private final boolean hasDelimiter; + + private ValueEnvelope(boolean hasDelimiter) { + this.hasDelimiter = hasDelimiter; + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 8dbbf72d..652bc2d1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -33,7 +33,7 @@ public static Set getRegisteredTags() { return Collections.unmodifiableSet(registry.keySet()); } - /** Populate the registry with all known tag→attribute mappings. */ + /** Populate the registry with all known tag-to-attribute mappings. */ public static void initialize() { // Inline/Flow tags register( @@ -156,7 +156,7 @@ public static void initialize() { register("ItemGrid"); // no attributes - uses child elements register("FootnoteList", new AttributeSpec("width", AttrType.INT)); - // === Charts (all five types share CommonChartAttrs) === + // Charts share CommonChartAttrs. register( "BarChart", new AttributeSpec("title", AttrType.STRING), @@ -306,7 +306,7 @@ public static void initialize() { new AttributeSpec("startAngle", AttrType.FLOAT), new AttributeSpec("clockwise", AttrType.BOOLEAN)); - // === Chart child tags === + // Chart child tags. register( "Series", new AttributeSpec("name", AttrType.STRING), @@ -341,7 +341,7 @@ public static void initialize() { new AttributeSpec("title", AttrType.STRING), new AttributeSpec("titleColor", AttrType.COLOR)); - // === FunctionGraph child tags === + // FunctionGraph child tags. register( "Plot", new AttributeSpec("expr", AttrType.EXPRESSION), @@ -363,7 +363,7 @@ public static void initialize() { new AttributeSpec("atX", AttrType.FLOAT), new AttributeSpec("atY", AttrType.FLOAT)); - // === Fix and extend existing registrations === + // Existing registrations with extended attributes. // GameScene: add camera attributes register( @@ -512,6 +512,21 @@ public static void initialize() { new AttributeSpec("offsetX", AttrType.INT), new AttributeSpec("offsetY", AttrType.INT), new AttributeSpec("offsetZ", AttrType.INT)); + register("Tier", new AttributeSpec("value", AttrType.INT), new AttributeSpec("expr", AttrType.INT)); + register( + "Channel", + new AttributeSpec("name", AttrType.STRING), + new AttributeSpec("id", AttrType.STRING), + new AttributeSpec("value", AttrType.INT), + new AttributeSpec("expr", AttrType.INT)); + register("Facing", new AttributeSpec("value", AttrType.STRING)); + register("Rotation", new AttributeSpec("value", AttrType.STRING)); + register("Flip", new AttributeSpec("value", AttrType.STRING)); + register("Orientation", new AttributeSpec("value", AttrType.STRING)); + register("GregTechActiveController"); + register("GtActiveController"); + register("GregTechPlaceHatches"); + register("GtPlaceHatches"); // PlaceBlock: add dx/dy/dz register( diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java new file mode 100644 index 00000000..c0c4076a --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java @@ -0,0 +1,64 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver.MdxValueContext; + +public class AttributePresetValueProvider implements AutocompleteProvider { + + private static final Map VALUES = new LinkedHashMap<>(); + + static { + VALUES.put("showIcon", new String[] { "left", "right", "true", "false" }); + VALUES.put("showTooltip", new String[] { "true", "false" }); + VALUES.put("noTooltip", new String[] { "true", "false" }); + VALUES.put("wrap", new String[] { "inline", "square", "tight", "through" }); + VALUES.put("align", new String[] { "left", "center", "right" }); + VALUES.put("float", new String[] { "none", "left", "right" }); + VALUES.put("clear", new String[] { "none", "left", "right", "all" }); + VALUES.put("valign", new String[] { "baseline", "top", "middle", "bottom" }); + VALUES.put("position", new String[] { "topLeft", "topRight", "bottomLeft", "bottomRight", "center" }); + VALUES.put("direction", new String[] { "clockwise", "counterclockwise" }); + VALUES.put("perspective", new String[] { "isometric", "front", "back", "left", "right", "top", "bottom" }); + VALUES.put("facing", new String[] { "north", "south", "west", "east", "up", "down" }); + VALUES.put("rotation", new String[] { "normal", "clockwise_90", "clockwise_180", "counterclockwise_90" }); + VALUES.put("flip", new String[] { "none", "front_back", "left_right" }); + VALUES.put("background", new String[] { "transparent", "checker", "solid" }); + VALUES.put("trigger", new String[] { "click", "hover" }); + VALUES.put("target", new String[] { "self", "blank" }); + VALUES.put("quadrants", new String[] { "all", "positive", "top", "right" }); + } + + @Override + public Set getSupportedKeys() { + return Collections.singleton(AutocompleteKey.forValue("*", "*")); + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + if (!(ctx instanceof MdxValueContext)) { + return Collections.emptyList(); + } + String[] values = VALUES.get(((MdxValueContext) ctx).getAttrName()); + if (values == null) { + return Collections.emptyList(); + } + String partial = ctx.getPartialText(); + String lower = partial != null ? partial.toLowerCase() : ""; + List results = new ArrayList<>(); + for (String value : values) { + if (results.size() >= limit) break; + if (lower.isEmpty() || value.toLowerCase() + .startsWith(lower)) { + results.add(new TextCandidate(value)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java index a7a24083..874651f6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AutocompleteProviders.java @@ -36,11 +36,30 @@ public static List query(AutocompleteContext ctx, int lim List results = new ArrayList<>(); for (AutocompleteProvider provider : resolveProviders(ctx)) { results.addAll(provider.provide(ctx, Math.max(0, limit - results.size()))); + removeExactPartialMatch(results, ctx.getPartialText()); if (results.size() >= limit) break; } + if (results.size() > limit) { + return new ArrayList<>(results.subList(0, limit)); + } return results; } + private static void removeExactPartialMatch(List results, String partialText) { + if (partialText == null || partialText.isEmpty()) { + return; + } + String partial = partialText.toLowerCase(); + for (int i = results.size() - 1; i >= 0; i--) { + String replacement = results.get(i) + .replacementText(); + if (replacement != null && replacement.toLowerCase() + .equals(partial)) { + results.remove(i); + } + } + } + private static List resolveProviders(AutocompleteContext ctx) { Set matched = new LinkedHashSet<>(); if (ctx instanceof MdxValueContext) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanValueProvider.java new file mode 100644 index 00000000..4a76aa64 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/BooleanValueProvider.java @@ -0,0 +1,44 @@ +package com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttrType; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AttributeSpec; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.AutocompleteContext; +import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; + +public class BooleanValueProvider implements AutocompleteProvider { + + private static final String[] VALUES = { "true", "false" }; + + @Override + public Set getSupportedKeys() { + Set keys = new HashSet<>(); + for (String tag : TagAttributeRegistry.getRegisteredTags()) { + for (AttributeSpec spec : TagAttributeRegistry.get(tag)) { + if (spec.getType() == AttrType.BOOLEAN) { + keys.add(AutocompleteKey.forValue(tag, spec.getName())); + } + } + } + return Collections.unmodifiableSet(keys); + } + + @Override + public List provide(AutocompleteContext ctx, int limit) { + String partial = ctx.getPartialText(); + String lower = partial != null ? partial.toLowerCase() : ""; + List results = new ArrayList<>(); + for (String value : VALUES) { + if (results.size() >= limit) break; + if (lower.isEmpty() || value.startsWith(lower)) { + results.add(new TextCandidate(value)); + } + } + return results; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java index d441dfdd..1f176943 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/ItemIdProvider.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -58,6 +59,9 @@ public List provide(AutocompleteContext ctx, int limit) { String lower = partial != null ? partial.toLowerCase() : ""; List results = new ArrayList<>(); + if (lower.indexOf(':') < 0) { + addNamespaceCandidates(results, lower, limit); + } for (Object obj : Item.itemRegistry.getKeys()) { if (results.size() >= limit) break; if (obj instanceof String key) { @@ -72,4 +76,23 @@ public List provide(AutocompleteContext ctx, int limit) { } return results; } + + private void addNamespaceCandidates(List results, String lower, int limit) { + Set namespaces = new LinkedHashSet<>(); + for (Object obj : Item.itemRegistry.getKeys()) { + if (!(obj instanceof String)) continue; + String key = (String) obj; + int separator = key.indexOf(':'); + if (separator <= 0) continue; + String namespace = key.substring(0, separator); + String namespaceLower = namespace.toLowerCase(); + if (lower.isEmpty() || namespaceLower.startsWith(lower)) { + namespaces.add(namespace); + } + } + for (String namespace : namespaces) { + if (results.size() >= limit) break; + results.add(new TextCandidate(namespace + ":")); + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java index 302c98ca..42fd4f7b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java @@ -18,8 +18,9 @@ public class TagNameProvider implements AutocompleteProvider { "RecipeFor", "RecipesFor", "Structure", "SubPages", "ColumnChart", "BarChart", "LineChart", "PieChart", "ScatterChart", "FunctionGraph", "Function", "GameScene", "Scene", "Block", "Entity", "PlaceBlock", "ReplaceBlock", "RemoveBlocks", "ImportStructure", "ImportStructureLib", "ImportPonder", "IsometricCamera", - "BlockAnnotation", "BoxAnnotation", "LineAnnotation", "DiamondAnnotation", "TextAnnotation", - "BlockAnnotationTemplate" }; + "Tier", "Channel", "Facing", "Rotation", "Flip", "Orientation", "GregTechActiveController", + "GregTechPlaceHatches", "BlockAnnotation", "BoxAnnotation", "LineAnnotation", "DiamondAnnotation", + "TextAnnotation", "BlockAnnotationTemplate" }; public static void setEnabled(boolean value) { enabled = value; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index fce5a2f4..c37873bf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -58,8 +58,10 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { @Nullable private TextSyntaxContext resolveFromAst(MdAstRoot root, String text, int cursorIndex) { - TextSyntaxContext tagStart = resolveTagStart(text, cursorIndex); - if (tagStart != null) return tagStart; + TextSyntaxContext fallback = resolveFromFallback(text, cursorIndex); + if (fallback != null && fallback.getElementType() != SyntaxElementType.WORD) { + return fallback; + } MdxJsxElementFields element = findEnclosingMdxElement(root, cursorIndex); if (element != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index db8b169b..7d9ba318 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -50,8 +50,11 @@ public class SceneEditorMultilineTextArea { private boolean horizontalScrollbarVisible; private boolean focused; private boolean selectingWithMouse; + private boolean panningWithMiddleMouse; private boolean draggingVerticalScrollbar; private boolean draggingHorizontalScrollbar; + private int panLastMouseX; + private int panLastMouseY; private int verticalScrollbarGrabOffset; private int horizontalScrollbarGrabOffset; private int externalHighlightStart; @@ -101,6 +104,7 @@ public SceneEditorMultilineTextArea(FontRenderer fontRenderer, ClipboardAccess c this.wrapEnabled = true; this.focused = false; this.selectingWithMouse = false; + this.panningWithMiddleMouse = false; this.draggingVerticalScrollbar = false; this.draggingHorizontalScrollbar = false; this.verticalScrollbarGrabOffset = 0; @@ -129,7 +133,7 @@ public void setBounds(int x, int y, int width, int height) { } public void setText(String text) { - String safeText = text != null ? text : ""; + String safeText = normalizeLineEndings(text); if (selectionModel.getText() .equals(safeText)) { return; @@ -194,7 +198,7 @@ public boolean cutSelection() { } public boolean pasteClipboard() { - selectionModel.insertText(clipboardAccess.paste()); + selectionModel.insertText(normalizeLineEndings(clipboardAccess.paste())); rebuildLayoutCache(); ensureCursorVisible(); syncImeFocusProxy(); @@ -202,7 +206,7 @@ public boolean pasteClipboard() { } public void applyEdit(String text, int selectionStart, int selectionEnd) { - selectionModel.setText(text != null ? text : ""); + selectionModel.setText(normalizeLineEndings(text)); selectionModel.setSelection(selectionStart, selectionEnd); rebuildLayoutCache(); ensureCursorVisible(); @@ -210,7 +214,7 @@ public void applyEdit(String text, int selectionStart, int selectionEnd) { } public void insertAtSelection(String text) { - selectionModel.insertText(text); + selectionModel.insertText(normalizeLineEndings(text)); rebuildLayoutCache(); ensureCursorVisible(); syncImeFocusProxy(); @@ -280,6 +284,7 @@ public void setFocused(boolean focused) { imeFocusProxy.setFocused(focused); if (!focused) { selectingWithMouse = false; + panningWithMiddleMouse = false; draggingVerticalScrollbar = false; draggingHorizontalScrollbar = false; } @@ -306,6 +311,16 @@ public boolean mouseClicked(int mouseX, int mouseY, int button) { if (!contains(mouseX, mouseY)) { return false; } + if (button == 2) { + setFocused(true); + panningWithMiddleMouse = true; + selectingWithMouse = false; + draggingVerticalScrollbar = false; + draggingHorizontalScrollbar = false; + panLastMouseX = mouseX; + panLastMouseY = mouseY; + return true; + } if (button != 0 && button != 1) { return true; } @@ -401,6 +416,16 @@ public boolean mouseDragged(int mouseX, int mouseY, int button) { syncImeFocusProxy(); return true; } + if (button == 2 && panningWithMiddleMouse) { + int deltaX = mouseX - panLastMouseX; + int deltaY = mouseY - panLastMouseY; + panLastMouseX = mouseX; + panLastMouseY = mouseY; + horizontalOffsetPixels = clampHorizontalOffset(horizontalOffsetPixels - deltaX); + scrollState.scrollPixels(-deltaY); + syncImeFocusProxy(); + return true; + } if (!focused || button != 0 || !selectingWithMouse) { return false; } @@ -416,6 +441,9 @@ public void mouseReleased(int button) { draggingVerticalScrollbar = false; draggingHorizontalScrollbar = false; } + if (button == 2) { + panningWithMiddleMouse = false; + } } public boolean keyTyped(char typedChar, int keyCode) { @@ -454,7 +482,7 @@ private boolean handleKeyTyped(char typedChar, int keyCode) { return true; } if (isCtrlKeyCombo(keyCode, Keyboard.KEY_V)) { - selectionModel.insertText(clipboardAccess.paste()); + selectionModel.insertText(normalizeLineEndings(clipboardAccess.paste())); rebuildLayoutCache(); ensureCursorVisible(); return true; @@ -463,7 +491,7 @@ private boolean handleKeyTyped(char typedChar, int keyCode) { switch (keyCode) { case Keyboard.KEY_RETURN: case Keyboard.KEY_NUMPADENTER: - selectionModel.insertText("\n"); + applySmartNewline(); rebuildLayoutCache(); ensureCursorVisible(); return true; @@ -565,6 +593,170 @@ private void rememberInsertedAsciiCharacter(char typedChar, int keyCode) { recentPhysicalAsciiAtMillis = System.currentTimeMillis(); } + private void applySmartNewline() { + String text = selectionModel.getText(); + int cursor = selectionModel.getCursorIndex(); + int lineStart = findLineStart(text, Math.max(0, cursor - 1)); + int lineEnd = findLineEnd(text, cursor); + String line = text.substring(lineStart, lineEnd); + MarkdownListMarker marker = parseMarkdownListMarker(line); + if (marker != null) { + if (line.substring(marker.getContentStart()) + .trim() + .isEmpty()) { + selectionModel.setSelection(lineStart, cursor); + selectionModel.insertText(leadingWhitespace(line)); + return; + } + selectionModel.insertText("\n" + marker.nextLinePrefix()); + return; + } + selectionModel.insertText(createIndentedNewline()); + } + + private String createIndentedNewline() { + String text = selectionModel.getText(); + int cursor = selectionModel.getCursorIndex(); + int lineStart = findLineStart(text, Math.max(0, cursor - 1)); + int lineEnd = findLineEnd(text, cursor); + String line = text.substring(lineStart, lineEnd); + String indent = leadingWhitespace(line); + String trimmed = line.trim(); + if (shouldIndentAfter(trimmed)) { + indent += " "; + } + return "\n" + indent; + } + + private static int findLineStart(String text, int index) { + int pos = Math.min(index, text.length()); + while (pos > 0) { + char previous = text.charAt(pos - 1); + if (previous == '\n' || previous == '\r') { + break; + } + pos--; + } + return pos; + } + + private static int findLineEnd(String text, int index) { + int pos = Math.min(index, text.length()); + while (pos < text.length()) { + char current = text.charAt(pos); + if (current == '\n' || current == '\r') { + break; + } + pos++; + } + return pos; + } + + private static String leadingWhitespace(String line) { + int end = 0; + while (end < line.length()) { + char c = line.charAt(end); + if (c != ' ' && c != '\t') { + break; + } + end++; + } + return line.substring(0, end); + } + + private static boolean shouldIndentAfter(String trimmedLine) { + if (trimmedLine.isEmpty()) { + return false; + } + char last = trimmedLine.charAt(trimmedLine.length() - 1); + if (last == '{' || last == '[' || last == '(' || last == ':') { + return true; + } + return trimmedLine.startsWith("<") && !trimmedLine.startsWith("") + && !trimmedLine.endsWith("-->"); + } + + private static String normalizeLineEndings(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + StringBuilder normalized = null; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c != '\r') { + if (normalized != null) { + normalized.append(c); + } + continue; + } + if (normalized == null) { + normalized = new StringBuilder(text.length()); + normalized.append(text, 0, i); + } + if (i + 1 < text.length() && text.charAt(i + 1) == '\n') { + normalized.append('\n'); + i++; + } else { + normalized.append('\n'); + } + } + return normalized != null ? normalized.toString() : text; + } + + @Nullable + private static MarkdownListMarker parseMarkdownListMarker(String line) { + String indent = leadingWhitespace(line); + int pos = indent.length(); + if (pos + 2 <= line.length()) { + char marker = line.charAt(pos); + if ((marker == '-' || marker == '*' || marker == '+') && line.charAt(pos + 1) == ' ') { + return new MarkdownListMarker(indent, Character.toString(marker), pos + 2); + } + } + int numberStart = pos; + while (pos < line.length() && Character.isDigit(line.charAt(pos))) { + pos++; + } + if (pos > numberStart && pos + 1 < line.length() + && (line.charAt(pos) == '.' || line.charAt(pos) == ')') + && line.charAt(pos + 1) == ' ') { + String numberText = line.substring(numberStart, pos); + int number = parseListNumber(numberText); + return new MarkdownListMarker(indent, Integer.toString(number + 1) + line.charAt(pos), pos + 2); + } + return null; + } + + private static int parseListNumber(String numberText) { + try { + return Integer.parseInt(numberText); + } catch (NumberFormatException ignored) { + return 1; + } + } + + private static class MarkdownListMarker { + + private final String indent; + private final String marker; + private final int contentStart; + + private MarkdownListMarker(String indent, String marker, int contentStart) { + this.indent = indent; + this.marker = marker; + this.contentStart = contentStart; + } + + private int getContentStart() { + return contentStart; + } + + private String nextLinePrefix() { + return indent + marker + " "; + } + } + public void draw(boolean validationError) { int borderColor = validationError ? ERROR_BORDER_COLOR : (focused ? FOCUSED_BORDER_COLOR : BORDER_COLOR); Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, borderColor); @@ -882,6 +1074,14 @@ public int getCursorPixelY() { return PADDING + lineIdx * getLineHeight() - scrollState.getOffsetPixels(); } + public boolean isCursorVisibleInViewport() { + int cursorX = getCursorPixelX(); + int cursorY = getCursorPixelY(); + return cursorX >= PADDING && cursorX <= getContentClipWidth() - PADDING + && cursorY >= PADDING + && cursorY <= getContentClipHeight() - PADDING; + } + private int getCursorIndexAt(int mouseX, int mouseY) { List lines = layoutCache.getVisualLines(); if (lines.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorAction.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorAction.java index a9776195..026a0de2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorAction.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorAction.java @@ -118,6 +118,7 @@ public enum GuideScreenEditorAction { COPY(GuidebookText.GuideEditorCopy), PASTE(GuidebookText.GuideEditorPaste), SELECT_ALL(GuidebookText.GuideEditorSelectAll), + FORMAT_DOCUMENT(GuidebookText.GuideEditorFormatDocument), TOGGLE_ADVANCED(GuidebookText.GuideEditorAdvancedToggle); private final GuidebookText tooltipKey; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java index d1c66242..5afb8387 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorTextActions.java @@ -1,10 +1,10 @@ package com.hfstudio.guidenh.guide.internal.editor.guide; -public final class GuideScreenEditorTextActions { +public class GuideScreenEditorTextActions { private GuideScreenEditorTextActions() {} - public static final class Result { + public static class Result { private final String text; private final int selectionStart; @@ -405,6 +405,78 @@ public static Result apply(GuideScreenEditorAction action, String text, int sele } } + public static String formatDocument(String text) { + String source = normalizeLineEndings(text); + String[] lines = source.split("\n", -1); + StringBuilder formatted = new StringBuilder(source.length()); + int mdxIndent = 0; + boolean previousBlank = false; + for (int i = 0; i < lines.length; i++) { + String rawLine = trimRight(lines[i]); + String trimmed = rawLine.trim(); + if (trimmed.isEmpty()) { + if (!previousBlank && i + 1 < lines.length) { + formatted.append('\n'); + } + previousBlank = true; + continue; + } + previousBlank = false; + if (isMdxClosingLine(trimmed)) { + mdxIndent = Math.max(0, mdxIndent - 1); + } + if (shouldIndentLine(trimmed)) { + appendSpaces(formatted, mdxIndent * 4); + } + formatted.append(trimmed) + .append('\n'); + if (opensMdxBlock(trimmed)) { + mdxIndent++; + } + } + if (formatted.length() > 0 && formatted.charAt(formatted.length() - 1) == '\n' && !source.endsWith("\n")) { + formatted.setLength(formatted.length() - 1); + } + return formatted.toString(); + } + + private static boolean shouldIndentLine(String trimmed) { + return trimmed.startsWith("<") && !trimmed.startsWith(""); - } - private static String normalizeLineEndings(String text) { if (text == null || text.isEmpty()) { return ""; @@ -704,59 +859,6 @@ private static String normalizeLineEndings(String text) { return normalized != null ? normalized.toString() : text; } - @Nullable - private static MarkdownListMarker parseMarkdownListMarker(String line) { - String indent = leadingWhitespace(line); - int pos = indent.length(); - if (pos + 2 <= line.length()) { - char marker = line.charAt(pos); - if ((marker == '-' || marker == '*' || marker == '+') && line.charAt(pos + 1) == ' ') { - return new MarkdownListMarker(indent, Character.toString(marker), pos + 2); - } - } - int numberStart = pos; - while (pos < line.length() && Character.isDigit(line.charAt(pos))) { - pos++; - } - if (pos > numberStart && pos + 1 < line.length() - && (line.charAt(pos) == '.' || line.charAt(pos) == ')') - && line.charAt(pos + 1) == ' ') { - String numberText = line.substring(numberStart, pos); - int number = parseListNumber(numberText); - return new MarkdownListMarker(indent, Integer.toString(number + 1) + line.charAt(pos), pos + 2); - } - return null; - } - - private static int parseListNumber(String numberText) { - try { - return Integer.parseInt(numberText); - } catch (NumberFormatException ignored) { - return 1; - } - } - - private static class MarkdownListMarker { - - private final String indent; - private final String marker; - private final int contentStart; - - private MarkdownListMarker(String indent, String marker, int contentStart) { - this.indent = indent; - this.marker = marker; - this.contentStart = contentStart; - } - - private int getContentStart() { - return contentStart; - } - - private String nextLinePrefix() { - return indent + marker + " "; - } - } - public void draw(boolean validationError) { int borderColor = validationError ? ERROR_BORDER_COLOR : (focused ? FOCUSED_BORDER_COLOR : BORDER_COLOR); Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, borderColor); From d8bcce77535d929eba5f6b15bec796ff1ebc46d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 20 May 2026 21:57:29 +0800 Subject: [PATCH 026/136] =?UTF-8?q?fix:=20make=20parser=20fully=20error-to?= =?UTF-8?q?lerant=20=E2=80=94=20eliminate=20all=20user-input-driven=20thro?= =?UTF-8?q?ws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FactoryTag: fix recover() token exit order (LIFO), handle EOF vs non-EOF consume, remove crashEol, unify optionalEsWhitespace to always allow lazy lines - FactoryMdxExpression: add recovery token for lazy-line bailout path - MdxMdastExtension: replace 6 ParseException throws with graceful recovery or recursive stack unwind in onErrorRightIsTag/onErrorLeftIsTag - MdastCompiler: replace 3 throws (exit open==null, defaultOnError x2) with recursive unwind + stack restoration; guard onexitlineending against empty stack The tokenizer and mdast compiler now produce a partial AST for any input, ensuring autocomplete can always query syntax context. --- .../guidenh/libs/mdast/MdastCompiler.java | 45 +++++++----- .../libs/mdast/mdx/MdxMdastExtension.java | 73 ++++++------------- .../libs/mdx/FactoryMdxExpression.java | 2 + .../hfstudio/guidenh/libs/mdx/FactoryTag.java | 42 ++++------- 4 files changed, 64 insertions(+), 98 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdast/MdastCompiler.java b/src/main/java/com/hfstudio/guidenh/libs/mdast/MdastCompiler.java index 7c4608d7..f25f62d7 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdast/MdastCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdast/MdastCompiler.java @@ -472,11 +472,10 @@ public MdAstNode exit(Token token, OnExitError onExitError) { var open = ListUtils.pop(this.tokenStack); if (open == null) { - throw new RuntimeException( - "Cannot close `" + token.type - + "` (" - + MdAstPosition.stringify(token.start, token.end) - + "): it’s not open"); + // Token was never opened (recovery path). Push the node back — + // it may be the root or a node whose token was already consumed. + this.stack.add(node); + return node; } else if (!open.token().type.equals(token.type)) { if (onExitError != null) { onExitError.error(this, token, open.token()); @@ -656,6 +655,9 @@ private void onexitdata(MdastContext context, Token token) { } private void onexitlineending(MdastContext ignored, Token token) { + if (stack.isEmpty()) { + return; + } var context = stack.get(stack.size() - 1); Assert.check(context != null, "expected `node`"); @@ -993,24 +995,27 @@ MdAstThematicBreak thematicBreak() { return new MdAstThematicBreak(); } + /** + * Recursively unwind mismatched stack entries until the correct token is found. + * Each call to {@code context.exit(left, this::unwindExit)} pops one more pair; + * if still mismatched the method recurses, otherwise the exit succeeds and the + * recursion terminates naturally. + */ + private void unwindExit(MdastContext context, Token left, Token right) { + if (left != null) { + context.exit(left, this::unwindExit); + } + } + private void defaultOnError(MdastContext context, @Nullable Token left, Token right) { if (left != null) { - throw new RuntimeException( - "Cannot close `" + left.type - + "` (" - + MdAstPosition.stringify(left.start, left.end) - + "): a different token (`" - + right.type - + "`, " - + MdAstPosition.stringify(right.start, right.end) - + ") is open"); - } else { - throw new RuntimeException( - "Cannot close document, a token (`" + right.type - + "`, " - + MdAstPosition.stringify(right.start, right.end) - + ") is still open"); + // Stack mismatch: expected to close `left` but `right` was on top. + // Unwind the stack until the correct token is found. + context.exit(left, this::unwindExit); } + // If left is null (end-of-file): remaining unclosed tokens correspond to + // AST nodes already added to parents on enter. They lack end positions + // but remain usable for autocomplete queries against the partial AST. } static class Fragment extends MdAstParent { diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java index c73687f5..de9ab177 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java @@ -17,7 +17,7 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.mdast.model.MdAstPosition; import com.hfstudio.guidenh.libs.micromark.ListUtils; -import com.hfstudio.guidenh.libs.micromark.ParseException; + import com.hfstudio.guidenh.libs.micromark.Point; import com.hfstudio.guidenh.libs.micromark.Token; @@ -98,11 +98,7 @@ public static void enterMdxJsxTagClosingMarker(MdastContext context, Token token var stack = getStack(context); if (stack.isEmpty()) { - throw new ParseException( - "Unexpected closing slash `/` in tag, expected an open tag first", - token.start, - token.end, - "mdast-util-mdx-jsx:unexpected-closing-slash"); + return; } } @@ -110,11 +106,7 @@ public static void enterMdxJsxTagAnyAttribute(MdastContext context, Token token) var tag = getTag(context); if (tag.close) { - throw new ParseException( - "Unexpected attribute in closing tag, expected the end of the tag", - token.start, - token.end, - "mdast-util-mdx-jsx:unexpected-attribute"); + return; } } @@ -122,11 +114,7 @@ public static void enterMdxJsxTagSelfClosingMarker(MdastContext context, Token t var tag = getTag(context); if (tag.close) { - throw new ParseException( - "Unexpected self-closing slash `/` in closing tag, expected the end of the tag", - token.start, - token.end, - "mdast-util-mdx-jsx:unexpected-self-closing-slash"); + return; } } @@ -234,17 +222,8 @@ public static void exitMdxJsxTag(MdastContext context, Token token) { var stack = getStack(context); var tail = stack.isEmpty() ? null : stack.get(stack.size() - 1); - if (tag.close && !Objects.equals(tail.name, tag.name)) { - throw new ParseException( - "Unexpected closing tag `" + serializeAbbreviatedTag(tag) - + "`, expected corresponding closing tag for `" - + serializeAbbreviatedTag(tail) - + "` (" - + MdAstPosition.stringify(tail.position()) - + ')', - token.start, - token.end, - "mdast-util-mdx-jsx:end-tag-mismatch"); + if (tag.close && tail != null && !Objects.equals(tail.name, tag.name)) { + // Mismatched closing tag — ignore and continue. } // End of a tag, so drop the buffer. @@ -271,36 +250,26 @@ public static void exitMdxJsxTag(MdastContext context, Token token) { } public static void onErrorRightIsTag(MdastContext context, @Nullable Token closing, Token open) { - var tag = getTag(context); - var place = closing != null ? " before the end of `" + closing.type + '`' : ""; - MdAstPosition position = null; + // Unclosed tag found when a parent (e.g. paragraph) is trying to close. + // Recursively unwind the stack: each context.exit(closing) pops one more + // mismatched pair, until the correct parent is reached and closed. if (closing != null) { - position = new MdAstPosition(closing.start, closing.end); + context.exit(closing, (ctx, left, right) -> { + if (left != null) { + ctx.exit(left); + } + }); } - - throw new ParseException( - "Expected a closing tag for `" + serializeAbbreviatedTag( - tag) + "` (" + MdAstPosition.stringify(open.start, open.end) + ')' + place, - position, - "mdast-util-mdx-jsx:end-tag-mismatch"); } public static void onErrorLeftIsTag(MdastContext context, @Nullable Token a, Token b) { - var tag = getTag(context); - throw new ParseException( - "Expected the closing tag `" + serializeAbbreviatedTag(tag) - + "` either after the end of `" - + b.type - + "` (" - + MdAstPosition.stringify(b.end) - + ") or another opening tag after the start of `" - + b.type - + "` (" - + MdAstPosition.stringify(b.start) - + ')', - a != null ? a.start : null, - a != null ? a.end : null, - "mdast-util-mdx-jsx:end-tag-mismatch"); + if (a != null) { + context.exit(a, (ctx, left, right) -> { + if (left != null) { + ctx.exit(left); + } + }); + } } /** diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java index 921c7a6e..089d9ab9 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java @@ -67,7 +67,9 @@ State atBreak(int code) { if (now.line() != startPosition.line() && !allowLazy && context.isOnLazyLine()) { effects.exit(type); + effects.enter("mdxExpressionRecovery"); effects.consume(code); + effects.exit("mdxExpressionRecovery"); return ok; } diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java index 27529e24..940fbef7 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java @@ -6,7 +6,7 @@ import com.hfstudio.guidenh.libs.micromark.Assert; import com.hfstudio.guidenh.libs.micromark.CharUtil; import com.hfstudio.guidenh.libs.micromark.Construct; -import com.hfstudio.guidenh.libs.micromark.ParseException; + import com.hfstudio.guidenh.libs.micromark.Point; import com.hfstudio.guidenh.libs.micromark.State; import com.hfstudio.guidenh.libs.micromark.TokenizeContext; @@ -536,21 +536,11 @@ State tagEnd(int code) { // Optionally start whitespace. State optionalEsWhitespace(int code) { if (CharUtil.markdownLineEnding(code)) { - if (allowLazy) { - effects.enter(Types.lineEnding); - effects.consume(code); - effects.exit(Types.lineEnding); - return FactorySpace - .create(effects, this::optionalEsWhitespace, Types.linePrefix, Constants.tabSize); - } - - return effects.attempt - .hook( - lazyLineEnd, - FactorySpace - .create(effects, this::optionalEsWhitespace, Types.linePrefix, Constants.tabSize), - this::crashEol) - .step(code); + effects.enter(Types.lineEnding); + effects.consume(code); + effects.exit(Types.lineEnding); + return FactorySpace + .create(effects, this::optionalEsWhitespace, Types.linePrefix, Constants.tabSize); } if (CharUtil.markdownSpace(code) || CharUtil.unicodeWhitespace(code)) { @@ -573,22 +563,22 @@ State optionalEsWhitespaceContinue(int code) { return this::optionalEsWhitespaceContinue; } - private State crashEol(int code) { - throw new ParseException( - "Unexpected lazy line in container, expected line to be prefixed with `>` when in a block quote, whitespace when in a list, etc", - context.now(), - "micromark-extension-mdx-jsx:unexpected-eof"); - } - // Recover from a nonconforming character: exit any open tokens, close the tag, // and return ok so parsing can continue. This makes the tokenizer error-tolerant // so autocomplete can always query the AST even for incomplete input. private State recover(int code, String... openTokens) { - for (int i = openTokens.length - 1; i >= 0; i--) { - effects.exit(openTokens[i]); + for (String openToken : openTokens) { + effects.exit(openToken); } effects.exit(tagType); - effects.consume(code); + if (code == Codes.eof) { + // consume(eof) requires last event to be EXIT, which it is after exiting tagType. + effects.consume(code); + } else { + effects.enter("mdxJsxRecovery"); + effects.consume(code); + effects.exit("mdxJsxRecovery"); + } return ok; } } From 26061d42e584ac7835392690c07d6ebaa5c8f8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 20 May 2026 23:00:16 +0800 Subject: [PATCH 027/136] =?UTF-8?q?fix:=20autocomplete=20=E2=80=94=20YAML?= =?UTF-8?q?=20list=20items=20with=20colons,=20top-level=20parent=20key,=20?= =?UTF-8?q?nested=20MDX=20elements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - YAML list items with colons in value (e.g. "guidenh:guide_icon") now correctly resolve to their parent key context via isYamlListMarker check before colon-split - Top-level keys with prevIndent=0 now return their own context for list items and empty lines, instead of falling back to plain text - findEnclosingMdxElement: search children first for innermost match, fixing nested MDX element attribute/tag-start autocomplete --- .../resolver/MdxSyntaxResolver.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index c48641e2..b0d702a8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -104,18 +104,21 @@ private TextSyntaxContext resolveFrontmatter(MdAstYamlFrontmatter yaml, String t return resolveFrontmatterEmptyLine(text, cursorIndex); } + // List items inherit context from their parent key, regardless of + // whether the value contains a colon (e.g. "guidenh:guide_icon"). + String trimmed = line.trim(); + if (isYamlListMarker(trimmed)) { + return resolveFrontmatterEmptyLine(text, cursorIndex); + } + int colonIdx = line.indexOf(':'); if (colonIdx < 0) { - String trimmed = line.trim(); - if (isYamlListMarker(trimmed)) { - return resolveFrontmatterEmptyLine(text, cursorIndex); - } return resolvePlainTextWord(text, cursorIndex); } String key = line.substring(0, colonIdx) .trim(); - if (key.isEmpty() || key.startsWith("#") || key.startsWith("- ")) { + if (key.isEmpty() || key.startsWith("#")) { return resolvePlainTextWord(text, cursorIndex); } @@ -167,7 +170,14 @@ private TextSyntaxContext resolveFrontmatterEmptyLine(String text, int cursorInd String prevKey = prevLine.substring(0, prevColon) .trim(); int prevIndent = prevLine.indexOf(prevKey); - if (prevIndent == 0) return resolvePlainTextWord(text, cursorIndex); // top-level key, no parent + if (prevIndent == 0) { + // Top-level key is the direct parent for a list item or empty line. + return new TextSyntaxContext( + SyntaxElementType.WORD, + cursorIndex, + cursorIndex, + new FrontmatterContext(prevKey, true, cursorIndex, cursorIndex, "")); + } // Find parent key at a lower indentation int searchPos = prevLineStart - 1; @@ -306,10 +316,7 @@ private MdxJsxElementFields findEnclosingMdxElement(UnistNode node, int cursorIn } } - if (node instanceof MdxJsxElementFields) { - return (MdxJsxElementFields) node; - } - + // Search children first so nested elements are found innermost-first. if (node instanceof MdAstParent) { for (UnistNode child : ((MdAstParent) node).children()) { MdxJsxElementFields found = findEnclosingMdxElement(child, cursorIndex); @@ -317,6 +324,10 @@ private MdxJsxElementFields findEnclosingMdxElement(UnistNode node, int cursorIn } } + if (node instanceof MdxJsxElementFields) { + return (MdxJsxElementFields) node; + } + return null; } From 0ace7b742c94e8ee2814c6fc89045621356b2e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 20 May 2026 23:31:18 +0800 Subject: [PATCH 028/136] fix: partial tag name autocomplete and blank-line Enter in frontmatter - resolveTagStart: extend to detect partial tag names after '<' (e.g. = 2) { + char prev = text.charAt(cursorIndex - 2); + if (prev != ' ' && prev != '\n' && prev != '\r' && prev != '>' && prev != '\t') { + return null; + } + } + return new TextSyntaxContext( + SyntaxElementType.TAG_START, + cursorIndex, + cursorIndex, + new TagStartContext(cursorIndex, cursorIndex, "", parentTagName)); } - if (cursorIndex < text.length()) { - char next = text.charAt(cursorIndex); - if (next == '/' || next == '!' || next == '?') { + + // Case 2: cursor inside a partial tag name after '<' (e.g. 0 && isTagNameChar(text.charAt(nameStart - 1))) { + nameStart--; + } + if (nameStart == 0 || text.charAt(nameStart - 1) != '<') { return null; } - } - if (cursorIndex >= 2) { - char prev = text.charAt(cursorIndex - 2); - if (prev != ' ' && prev != '\n' && prev != '\r' && prev != '>' && prev != '\t') { + int tagStart = nameStart - 1; + // Closing tag: tagStart + 1 && text.charAt(tagStart + 1) == '/') { return null; } + String partial = text.substring(nameStart, cursorIndex); + return new TextSyntaxContext( + SyntaxElementType.TAG_START, + tagStart, + cursorIndex, + new TagStartContext(tagStart, cursorIndex, partial, parentTagName)); } - return new TextSyntaxContext( - SyntaxElementType.TAG_START, - cursorIndex, - cursorIndex, - new TagStartContext(cursorIndex, cursorIndex, "", parentTagName)); + + return null; + } + + private static boolean isTagNameChar(char c) { + return Character.isLetterOrDigit(c) || c == '-'; } // ---- MDX element ---- diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index d254642b..7688a620 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -644,8 +644,15 @@ private void applySmartNewline() { selectionModel.insertText("\n" + indent + manualMarker); return; } + if (trimmed.isEmpty()) { + // Blank line: move cursor to next line instead of inserting another blank line. + int nextLineStart = findLineEnd(text, cursor) + 1; + if (nextLineStart <= text.length()) { + selectionModel.setSelection(nextLineStart, nextLineStart); + } + return; + } selectionModel.insertText("\n" + indent); - } @Nullable private static String resolveManualListMarker(String trimmed) { From 31eda78e6217c82d6a65c06e77a2649247e983dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 21 May 2026 21:14:27 +0800 Subject: [PATCH 029/136] fix: missing closing brace in applySmartNewline --- .../guide/internal/editor/gui/SceneEditorMultilineTextArea.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index 7688a620..924c0d29 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -653,6 +653,7 @@ private void applySmartNewline() { return; } selectionModel.insertText("\n" + indent); + } @Nullable private static String resolveManualListMarker(String trimmed) { From 9a96bd52145502aa4bd0fe0d9132381febf957c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 21 May 2026 21:38:29 +0800 Subject: [PATCH 030/136] =?UTF-8?q?feat(autocomplete):=20register=20missin?= =?UTF-8?q?g=20tag=20attributes=20=E2=80=94=20annotations,=20sounds,=20que?= =?UTF-8?q?sts,=20block=20stats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 6 annotation tags: BlockAnnotation, BoxAnnotation, LineAnnotation, DiamondAnnotation, TextAnnotation, BlockAnnotationTemplate (41 attrs) - Add 2 sound tags: PlaySound (11 attrs), SoundLink (10 attrs) - Add 2 quest tags: QuestLink (id, text), QuestCard (id, show_desc) - Add 2 block stats tags: BlockStats (10 attrs), BlockStat (3 attrs) --- .../autocomplete/TagAttributeRegistry.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 652bc2d1..20599a31 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -576,5 +576,113 @@ public static void initialize() { new AttributeSpec("meta", AttrType.INT), new AttributeSpec("facing", AttrType.STRING), new AttributeSpec("nbt", AttrType.SNBT)); + + // Annotation tags + register( + "BlockAnnotation", + new AttributeSpec("pos", AttrType.VECTOR3), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("thickness", AttrType.FLOAT), + new AttributeSpec("alwaysOnTop", AttrType.BOOLEAN)); + + register( + "BoxAnnotation", + new AttributeSpec("min", AttrType.VECTOR3), + new AttributeSpec("max", AttrType.VECTOR3), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("thickness", AttrType.FLOAT), + new AttributeSpec("alwaysOnTop", AttrType.BOOLEAN)); + + register( + "LineAnnotation", + new AttributeSpec("points", AttrType.STRING), + new AttributeSpec("from", AttrType.VECTOR3), + new AttributeSpec("to", AttrType.VECTOR3), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("thickness", AttrType.FLOAT), + new AttributeSpec("alwaysOnTop", AttrType.BOOLEAN), + new AttributeSpec("showPoints", AttrType.BOOLEAN), + new AttributeSpec("pointColor", AttrType.COLOR), + new AttributeSpec("pointSize", AttrType.FLOAT), + new AttributeSpec("arrow", AttrType.STRING)); + + register( + "DiamondAnnotation", + new AttributeSpec("pos", AttrType.VECTOR3), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("alwaysOnTop", AttrType.BOOLEAN)); + + register( + "TextAnnotation", + new AttributeSpec("text", AttrType.STRING), + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("maxWidth", AttrType.INT), + new AttributeSpec("backgroundAlpha", AttrType.INT), + new AttributeSpec("independent", AttrType.BOOLEAN), + new AttributeSpec("yOffset", AttrType.INT), + new AttributeSpec("pos", AttrType.VECTOR3)); + + register( + "BlockAnnotationTemplate", + new AttributeSpec("id", AttrType.STRING)); + + // Sound tags (share GuideSoundParsers.parseAttributes) + register( + "PlaySound", + new AttributeSpec("sound", AttrType.STRING), + new AttributeSpec("src", AttrType.FILE_PATH), + new AttributeSpec("volume", AttrType.FLOAT), + new AttributeSpec("pitch", AttrType.FLOAT), + new AttributeSpec("cooldown", AttrType.INT), + new AttributeSpec("radius", AttrType.FLOAT), + new AttributeSpec("minVolume", AttrType.FLOAT), + new AttributeSpec("x", AttrType.FLOAT), + new AttributeSpec("y", AttrType.FLOAT), + new AttributeSpec("z", AttrType.FLOAT), + new AttributeSpec("trigger", AttrType.STRING)); + + register( + "SoundLink", + new AttributeSpec("sound", AttrType.STRING), + new AttributeSpec("src", AttrType.FILE_PATH), + new AttributeSpec("volume", AttrType.FLOAT), + new AttributeSpec("pitch", AttrType.FLOAT), + new AttributeSpec("cooldown", AttrType.INT), + new AttributeSpec("radius", AttrType.FLOAT), + new AttributeSpec("minVolume", AttrType.FLOAT), + new AttributeSpec("x", AttrType.FLOAT), + new AttributeSpec("y", AttrType.FLOAT), + new AttributeSpec("z", AttrType.FLOAT)); + + // Quest integration tags + register( + "QuestLink", + new AttributeSpec("id", AttrType.STRING), + new AttributeSpec("text", AttrType.STRING)); + + register( + "QuestCard", + new AttributeSpec("id", AttrType.STRING), + new AttributeSpec("show_desc", AttrType.STRING)); + + // Scene block stats + register( + "BlockStats", + new AttributeSpec("visible", AttrType.BOOLEAN), + new AttributeSpec("buttonEnabled", AttrType.BOOLEAN), + new AttributeSpec("mode", AttrType.STRING), + new AttributeSpec("corner", AttrType.STRING), + new AttributeSpec("dock", AttrType.STRING), + new AttributeSpec("showNames", AttrType.BOOLEAN), + new AttributeSpec("filterMode", AttrType.STRING), + new AttributeSpec("filter", AttrType.STRING), + new AttributeSpec("maxWidth", AttrType.INT), + new AttributeSpec("maxHeight", AttrType.INT)); + + register( + "BlockStat", + new AttributeSpec("id", AttrType.BLOCK_ID), + new AttributeSpec("item", AttrType.ITEM_ID), + new AttributeSpec("count", AttrType.FLOAT)); } } From e29676fa0ca16268e0562d54974e7441add8dcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 21 May 2026 22:50:18 +0800 Subject: [PATCH 031/136] fix: mark recovered MDX elements so resolver skips them consistently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'recovered' field to MdxJsxFlowElement, MdxJsxTextElement, and Tag - Register mdxJsxRecovery enter handler in mdast extension - Reorder non-EOF recovery: emit mdxJsxRecovery BEFORE tag exit so the handler runs before exitMdxJsxTag creates the AST node - findEnclosingMdxElement skips recovered=true nodes - Remove ATTRIBUTE_NAME→TAG_START resolver fallback --- .../guidenh/guide/internal/GuideScreen.java | 13 +++++++++++++ .../resolver/MdxSyntaxResolver.java | 12 ++++++++++-- .../libs/mdast/mdx/MdxMdastExtension.java | 17 +++++++++++++++-- .../libs/mdast/mdx/model/MdxJsxFlowElement.java | 1 + .../libs/mdast/mdx/model/MdxJsxTextElement.java | 1 + .../hfstudio/guidenh/libs/mdx/FactoryTag.java | 6 ++++-- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 7091b91d..fa46cf35 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2645,6 +2645,19 @@ private void performAutocompleteCheck() { TextSyntaxContext ctx = autocompleteResolver.resolve(text, cursor); + // ---- DIAGNOSTIC: remove after debugging partial tag name autocomplete ---- + { + int pre = Math.max(0, cursor - 4); + int post = Math.min(text.length(), cursor + 2); + String around = text.substring(pre, cursor) + "|" + text.substring(cursor, post); + String ctxInfo = ctx != null ? ctx.getElementType() + "/" + (ctx.getAutocomplete() != null + ? ctx.getAutocomplete().getClass().getSimpleName() : "null") : "null"; + FMLLog.info("[ACDBG] cursor=%d around='%s' ctx=%s shouldAC=%s", cursor, + around.replace("\n", "\\n"), ctxInfo, + ctx != null ? ctx.shouldAutocomplete() : false); + } + // ---- END DIAGNOSTIC ---- + if (ctx != null && ctx.shouldAutocomplete()) { List candidates = AutocompleteProviders.query(ctx.getAutocomplete(), 20); if (!candidates.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index 46bff446..1e2c8938 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -12,6 +12,8 @@ import com.hfstudio.guidenh.libs.mdast.MdastOptions; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstCode; import com.hfstudio.guidenh.libs.mdast.model.MdAstImage; import com.hfstudio.guidenh.libs.mdast.model.MdAstLink; @@ -356,13 +358,19 @@ private MdxJsxElementFields findEnclosingMdxElement(UnistNode node, int cursorIn } } - if (node instanceof MdxJsxElementFields) { - return (MdxJsxElementFields) node; + if (node instanceof MdxJsxElementFields el && !isRecovered(el)) { + return el; } return null; } + private static boolean isRecovered(MdxJsxElementFields el) { + if (el instanceof MdxJsxFlowElement f) return f.recovered; + if (el instanceof MdxJsxTextElement t) return t.recovered; + return false; + } + @Nullable private TextSyntaxContext resolveMdxAttribute(MdxJsxElementFields element, String text, int cursorIndex) { String tagName = element.name(); diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java index de9ab177..f8a302cb 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java @@ -29,6 +29,7 @@ public class MdxMdastExtension { public static final MdastExtension INSTANCE = MdastExtension.builder() .canContainEol("mdxJsxTextElement") .enter("mdxJsxFlowTag", MdxMdastExtension::enterMdxJsxTag) + .enter("mdxJsxRecovery", MdxMdastExtension::enterMdxJsxRecovery) .enter("mdxJsxFlowTagClosingMarker", MdxMdastExtension::enterMdxJsxTagClosingMarker) .enter("mdxJsxFlowTagAttribute", MdxMdastExtension::enterMdxJsxTagAttribute) .enter("mdxJsxFlowTagExpressionAttribute", MdxMdastExtension::enterMdxJsxTagExpressionAttribute) @@ -94,6 +95,13 @@ public static void enterMdxJsxTag(MdastContext context, Token token) { context.buffer(); } + public static void enterMdxJsxRecovery(MdastContext context, Token token) { + var tag = context.get(TAG); + if (tag != null) { + tag.recovered = true; + } + } + public static void enterMdxJsxTagClosingMarker(MdastContext context, Token token) { var stack = getStack(context); @@ -234,9 +242,13 @@ public static void exitMdxJsxTag(MdastContext context, Token token) { } else { MdAstNode node; if (Objects.equals(token.type, "mdxJsxTextTag")) { - node = new MdxJsxTextElement(tag.name, tag.attributes); + var el = new MdxJsxTextElement(tag.name, tag.attributes); + el.recovered = tag.recovered; + node = el; } else { - node = new MdxJsxFlowElement(tag.name, tag.attributes); + var el = new MdxJsxFlowElement(tag.name, tag.attributes); + el.recovered = tag.recovered; + node = el; } context.enter(node, token, MdxMdastExtension::onErrorRightIsTag); @@ -286,6 +298,7 @@ public static class Tag { List attributes = new ArrayList<>(); boolean close; boolean selfClosing; + boolean recovered; Point start; Point end; diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxFlowElement.java b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxFlowElement.java index a2c820a5..99ceb5ef 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxFlowElement.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxFlowElement.java @@ -18,6 +18,7 @@ public class MdxJsxFlowElement extends MdAstParent implements public static final String TYPE = "mdxJsxFlowElement"; public String name; public List attributes; + public transient boolean recovered; public MdxJsxFlowElement() { this("", new ArrayList<>()); diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxTextElement.java b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxTextElement.java index 89614bc2..dba24f75 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxTextElement.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/model/MdxJsxTextElement.java @@ -20,6 +20,7 @@ public class MdxJsxTextElement extends MdAstParent public static final String TYPE = "mdxJsxTextElement"; private String name; private final List attributes; + public transient boolean recovered; public MdxJsxTextElement() { this("", new ArrayList<>()); diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java index 940fbef7..7397c16d 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java @@ -570,14 +570,16 @@ private State recover(int code, String... openTokens) { for (String openToken : openTokens) { effects.exit(openToken); } - effects.exit(tagType); if (code == Codes.eof) { - // consume(eof) requires last event to be EXIT, which it is after exiting tagType. + effects.exit(tagType); effects.consume(code); } else { + // Emit recovery marker BEFORE exiting the tag so the mdast + // handler can set tag.recovered before the AST node is created. effects.enter("mdxJsxRecovery"); effects.consume(code); effects.exit("mdxJsxRecovery"); + effects.exit(tagType); } return ok; } From 785ba61c66e6ee29afb95b8bf536781795bab624 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Tue, 26 May 2026 01:35:02 +0800 Subject: [PATCH 032/136] update --- .../guidenh/guide/compiler/PageCompiler.java | 5 +- .../AutocompleteCommitService.java | 3 +- .../autocomplete/TagAttributeRegistry.java | 9 +- .../provider/OreDictProvider.java | 4 +- .../provider/TagNameProvider.java | 6 +- .../resolver/MdxSyntaxResolver.java | 43 ++++-- .../gui/SceneEditorMultilineTextArea.java | 53 +++---- .../editor/md/SceneEditorMarkdownCodec.java | 1 - .../SceneEditorSceneNodePreviewApplier.java | 22 +-- .../guide/scene/LytGuidebookScene.java | 136 ++++++++++++++---- .../ImportStructureLibElementCompiler.java | 13 +- .../libs/mdast/mdx/MdxMdastExtension.java | 1 - .../libs/mdx/FactoryMdxExpression.java | 1 - .../hfstudio/guidenh/libs/mdx/FactoryTag.java | 2 - 14 files changed, 199 insertions(+), 100 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index fe382f47..29d2d74d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -241,7 +241,7 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource String parseFailureMessage = null; UnistPoint parseFailureFrom = null; UnistPoint parseFailureTo = null; - long markdownParseNs; + long markdownParseNs = 0L; long latexRestoreNs = 0L; long htmlNormalizeNs = 0L; try { @@ -263,7 +263,8 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource parseFailureTo = e.getTo(); } String errorMessage = formatParseFailureMessage(id, language, sourcePack, parseFailureFrom); - logError("[GuideNH] [PageCompiler] {}", t, errorMessage); + FMLLog.getLogger() + .error("[GuideNH] [PageCompiler] {}", errorMessage, t); parseFailureMessage = errorMessage + ": \n" + t; astRoot = buildErrorPage(parseFailureMessage); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java index 576b227e..45b54a00 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/AutocompleteCommitService.java @@ -143,8 +143,7 @@ private static boolean shouldQuoteAttributeValue(MdxValueContext context) { } } - private static Replacement createFrontmatterReplacement(String source, FrontmatterContext context, - String rawText) { + private static Replacement createFrontmatterReplacement(String source, FrontmatterContext context, String rawText) { String replacement = rawText != null ? rawText : ""; if (!context.isValue()) { replacement += ": "; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 20599a31..9a677abb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -622,9 +622,7 @@ public static void initialize() { new AttributeSpec("yOffset", AttrType.INT), new AttributeSpec("pos", AttrType.VECTOR3)); - register( - "BlockAnnotationTemplate", - new AttributeSpec("id", AttrType.STRING)); + register("BlockAnnotationTemplate", new AttributeSpec("id", AttrType.STRING)); // Sound tags (share GuideSoundParsers.parseAttributes) register( @@ -655,10 +653,7 @@ public static void initialize() { new AttributeSpec("z", AttrType.FLOAT)); // Quest integration tags - register( - "QuestLink", - new AttributeSpec("id", AttrType.STRING), - new AttributeSpec("text", AttrType.STRING)); + register("QuestLink", new AttributeSpec("id", AttrType.STRING), new AttributeSpec("text", AttrType.STRING)); register( "QuestCard", diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java index 695ca970..d722bf22 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/OreDictProvider.java @@ -15,9 +15,7 @@ public class OreDictProvider implements AutocompleteProvider { private static final Set KEYS = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList( - AutocompleteKey.forValue("*", "ore"), - AutocompleteKey.forValue("*", "ore_ids")))); + new HashSet<>(Arrays.asList(AutocompleteKey.forValue("*", "ore"), AutocompleteKey.forValue("*", "ore_ids")))); @Override public Set getSupportedKeys() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java index 999df427..a5a0ed20 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/TagNameProvider.java @@ -26,9 +26,9 @@ public class TagNameProvider implements AutocompleteProvider { private static final String[] GAME_SCENE_TAG_NAMES = { "ImportStructure", "ImportStructureLib", "RemoveBlocks", "BlockAnnotationTemplate", "BlockAnnotation", "BoxAnnotation", "LineAnnotation", "DiamondAnnotation", - "TextAnnotation", "Block", "Entity", "PlaceBlock", "ReplaceBlock", "IsometricCamera", "ImportPonder", - "Tier", "Channel", "Facing", "Rotation", "Flip", "Orientation", "GregTechActiveController", - "GtActiveController", "GregTechPlaceHatches", "GtPlaceHatches" }; + "TextAnnotation", "Block", "Entity", "PlaceBlock", "ReplaceBlock", "IsometricCamera", "ImportPonder", "Tier", + "Channel", "Facing", "Rotation", "Flip", "Orientation", "GregTechActiveController", "GtActiveController", + "GregTechPlaceHatches", "GtPlaceHatches" }; private static Set buildKeys() { Set keys = new HashSet<>(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index 1e2c8938..2e0abfbe 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -18,8 +18,8 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstImage; import com.hfstudio.guidenh.libs.mdast.model.MdAstLink; import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.mdast.model.MdAstResource; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.unist.UnistNode; import com.hfstudio.guidenh.libs.unist.UnistPosition; @@ -84,8 +84,7 @@ private TextSyntaxContext resolveFromAst(MdAstRoot root, String text, int cursor } // 4. Tag start - TextSyntaxContext tagStart = resolveTagStart(text, cursorIndex, - element != null ? element.name() : null); + TextSyntaxContext tagStart = resolveTagStart(text, cursorIndex, element != null ? element.name() : null); if (tagStart != null) { return tagStart; } @@ -392,7 +391,12 @@ private TextSyntaxContext resolveMdxAttribute(MdxJsxElementFields element, Strin if (cursorIndex < attrStart || cursorIndex > attrEnd) continue; TextSyntaxContext valueContext = resolveAttributeValue( - text, tagName, attr.name, attrStart, attrEnd, cursorIndex); + text, + tagName, + attr.name, + attrStart, + attrEnd, + cursorIndex); if (valueContext != null) return valueContext; break; } @@ -462,8 +466,10 @@ private TextSyntaxContext resolveAttributeValue(String text, String tagName, Str bounds.valueStart, bounds.valueEnd, new MdxValueContext( - tagName, attrName, - bounds.valueStart, bounds.valueEnd, + tagName, + attrName, + bounds.valueStart, + bounds.valueEnd, partialText, bounds.missingTerminator)); } @@ -499,12 +505,27 @@ private static int findOpeningTagEnd(String text, int tagStart) { int braceDepth = 0; for (int i = tagStart; i < text.length(); i++) { char ch = text.charAt(i); - if ((inSingle || inDouble) && ch == '\\' && i + 1 < text.length()) { i++; continue; } - if (ch == '\'' && !inDouble) { inSingle = !inSingle; continue; } - if (ch == '"' && !inSingle) { inDouble = !inDouble; continue; } + if ((inSingle || inDouble) && ch == '\\' && i + 1 < text.length()) { + i++; + continue; + } + if (ch == '\'' && !inDouble) { + inSingle = !inSingle; + continue; + } + if (ch == '"' && !inSingle) { + inDouble = !inDouble; + continue; + } if (inSingle || inDouble) continue; - if (ch == '{') { braceDepth++; continue; } - if (ch == '}') { braceDepth = Math.max(0, braceDepth - 1); continue; } + if (ch == '{') { + braceDepth++; + continue; + } + if (ch == '}') { + braceDepth = Math.max(0, braceDepth - 1); + continue; + } if (ch == '>' && braceDepth == 0) return i + 1; } return text.length(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index e9421726..2e0242dd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -714,10 +714,9 @@ private void applySmartNewline() { String manualMarker = resolveManualListMarker(trimmed); if (manualMarker != null) { int markerLen = manualMarker.length(); - if (trimmed.length() <= markerLen || (trimmed.length() > markerLen - && trimmed.substring(markerLen) - .trim() - .isEmpty())) { + if (trimmed.length() <= markerLen || (trimmed.length() > markerLen && trimmed.substring(markerLen) + .trim() + .isEmpty())) { // Empty list item: remove marker selectionModel.setSelection(lineStart, cursor); selectionModel.insertText(indent); @@ -758,11 +757,13 @@ private static String resolveManualListMarker(String trimmed) { } @Nullable - private static MdAstListItem findEnclosingListItem( - UnistNode node, int cursorIndex) { + private static MdAstListItem findEnclosingListItem(UnistNode node, int cursorIndex) { UnistPosition pos = node.position(); if (pos != null && pos.start() != null && pos.end() != null) { - if (cursorIndex < pos.start().offset() || cursorIndex > pos.end().offset()) { + if (cursorIndex < pos.start() + .offset() || cursorIndex + > pos.end() + .offset()) { return null; } } @@ -770,8 +771,7 @@ private static MdAstListItem findEnclosingListItem( return (MdAstListItem) node; } if (node instanceof MdAstParent) { - for (UnistNode child : ((MdAstParent) node) - .children()) { + for (UnistNode child : ((MdAstParent) node).children()) { MdAstListItem found = findEnclosingListItem(child, cursorIndex); if (found != null) return found; } @@ -779,12 +779,13 @@ private static MdAstListItem findEnclosingListItem( return null; } - private static boolean isListItemContentEmpty(MdAstListItem item, - String text) { + private static boolean isListItemContentEmpty(MdAstListItem item, String text) { UnistPosition pos = item.position(); if (pos == null || pos.start() == null || pos.end() == null) return false; - int start = pos.start().offset(); - int end = pos.end().offset(); + int start = pos.start() + .offset(); + int end = pos.end() + .offset(); if (start >= end || end > text.length()) return false; // Find first newline to get the first line (contains the marker) int firstLineEnd = text.indexOf('\n', start); @@ -795,19 +796,23 @@ private static boolean isListItemContentEmpty(MdAstListItem item, if (trimmed.isEmpty()) return true; // Check if after the marker the content is empty int contentStart = findListContentStart(trimmed); - return contentStart >= trimmed.length() || trimmed.substring(contentStart).trim().isEmpty(); + return contentStart >= trimmed.length() || trimmed.substring(contentStart) + .trim() + .isEmpty(); } private static int findListContentStart(String trimmed) { int i = 0; // Unordered: -, *, + - if (i < trimmed.length() && (trimmed.charAt(i) == '-' || trimmed.charAt(i) == '*' || trimmed.charAt(i) == '+')) { + if (i < trimmed.length() + && (trimmed.charAt(i) == '-' || trimmed.charAt(i) == '*' || trimmed.charAt(i) == '+')) { i++; if (i < trimmed.length() && trimmed.charAt(i) == ' ') return i + 1; } // Ordered: number. or number) while (i < trimmed.length() && Character.isDigit(trimmed.charAt(i))) i++; - if (i > 0 && i + 1 < trimmed.length() && (trimmed.charAt(i) == '.' || trimmed.charAt(i) == ')') + if (i > 0 && i + 1 < trimmed.length() + && (trimmed.charAt(i) == '.' || trimmed.charAt(i) == ')') && trimmed.charAt(i + 1) == ' ') { return i + 2; } @@ -815,17 +820,18 @@ private static int findListContentStart(String trimmed) { } @Nullable - private static String resolveNextListMarker(String text, MdAstListItem item, - MdAstRoot root) { + private static String resolveNextListMarker(String text, MdAstListItem item, MdAstRoot root) { MdAstList list = findParentList(root, item); if (list == null) return null; // Find the marker from the current item's source text UnistPosition pos = item.position(); if (pos == null || pos.start() == null) return null; - int itemStart = pos.start().offset(); + int itemStart = pos.start() + .offset(); int firstLineEnd = text.indexOf('\n', itemStart); if (firstLineEnd < 0) firstLineEnd = Math.min(itemStart + 80, text.length()); - String firstLine = text.substring(itemStart, Math.min(firstLineEnd, text.length())).trim(); + String firstLine = text.substring(itemStart, Math.min(firstLineEnd, text.length())) + .trim(); String marker = extractListMarker(firstLine); if (marker == null) return null; @@ -866,12 +872,9 @@ private static String indentFor(MdAstListItem item) { } @Nullable - private static MdAstList findParentList( - UnistNode root, - MdAstListItem target) { + private static MdAstList findParentList(UnistNode root, MdAstListItem target) { if (!(root instanceof MdAstParent)) return null; - for (UnistNode child : ((MdAstParent) root) - .children()) { + for (UnistNode child : ((MdAstParent) root).children()) { if (child instanceof MdAstList) { for (MdAstListContent item : ((MdAstList) child).children()) { if (item == target) return (MdAstList) child; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java index 696604cb..07b9b049 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java @@ -28,7 +28,6 @@ import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttributeNode; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.mdast.model.MdAstHTML; import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.mdx.MdxCommentMasker; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java index eb371cb4..fd88b118 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/preview/SceneEditorSceneNodePreviewApplier.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import net.minecraft.block.Block; import net.minecraft.init.Blocks; @@ -42,6 +43,7 @@ import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCacheKey; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureFingerprintResolver; import com.hfstudio.guidenh.guide.scene.element.GuidebookSceneEntityImportSupport; +import com.hfstudio.guidenh.guide.scene.element.ImportStructureLibElementCompiler; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; import com.hfstudio.guidenh.guide.scene.support.BlockAnnotationTemplateExpander; @@ -53,6 +55,7 @@ import com.hfstudio.guidenh.integration.structurelib.StructureLibPreviewSelection; import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneImportService; import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneMetadata; +import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneOptions; public class SceneEditorSceneNodePreviewApplier { @@ -223,10 +226,14 @@ private boolean applyImportStructureLib(LytGuidebookScene scene, SceneEditorScen StructureLibPreviewSelection selection = structureLibSelectionOverride != null ? ImportStructureLibElementCompiler .mergePersistentOptions(structureLibSelectionOverride, defaultSelection, options) - : binding.getPendingSelection() != null ? ImportStructureLibElementCompiler - .mergePersistentOptions(binding.getPendingSelection(), defaultSelection, options) - : scene.getPendingStructureLibPreviewSelection(structureName) != null ? ImportStructureLibElementCompiler - .mergePersistentOptions(scene.getPendingStructureLibPreviewSelection(structureName), defaultSelection, options) + : binding.getPendingSelection() != null + ? ImportStructureLibElementCompiler + .mergePersistentOptions(binding.getPendingSelection(), defaultSelection, options) + : scene.getPendingStructureLibPreviewSelection(structureName) != null + ? ImportStructureLibElementCompiler.mergePersistentOptions( + scene.getPendingStructureLibPreviewSelection(structureName), + defaultSelection, + options) : defaultSelection; StructureLibImportRequest request = new StructureLibImportRequest( @@ -238,7 +245,7 @@ private boolean applyImportStructureLib(LytGuidebookScene scene, SceneEditorScen Integer.valueOf(selection.getMasterTier()), ImportStructureLibElementCompiler.applyControllerDefaults(controller, selection, options), options); - scene.setStructureLibInitialSelection(request.getPreviewSelection()); + scene.setPendingStructureLibPreviewSelection(structureName, request.getPreviewSelection()); StructureLibImportResult result = structureLibImportService.importScene(request); attachStructureLibMetadata(scene, structureName, request, result); if (!result.isSuccess()) { @@ -680,11 +687,6 @@ private String resolveAnnotationText(SceneEditorElementModel element) { return element.getTextMarkdown(); } - private boolean parseBooleanAttribute(@Nullable String value) { - String normalized = normalizeAttribute(value); - return normalized != null && Boolean.parseBoolean(normalized); - } - private TextAnnotation.ConnectorSide parseConnectorSideAttribute(SceneEditorElementModel element) { String rawValue = element != null ? normalizeAttribute(element.getExtraAttribute("connectorSide")) : null; if (rawValue == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index c58cd41a..9a4e5340 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -107,7 +107,6 @@ import com.hfstudio.guidenh.integration.structurelib.StructureLibTooltipContentBuilder; import lombok.Getter; -import lombok.Setter; public class LytGuidebookScene extends LytBlock { @@ -225,25 +224,12 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private int cachedPonderBtnAbsX; private int cachedPonderBtnAbsY; - @Getter private boolean interactive = true; - @Setter - @Getter private boolean sceneButtonsVisible = true; - @Getter - @Setter private boolean bottomControlsVisible = true; - @Getter - @Setter private boolean reserveBottomControlArea = true; - @Getter - @Setter private boolean visibleLayerSliderEnabled; - @Getter - @Setter private boolean forceOriginAxesVisible; - @Setter - @Getter private boolean forceHideOriginAxes; public static int SCENE_BG_COLOR = 0xFF0A0A10; @@ -270,24 +256,15 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { public static final int DEFAULT_WIDTH = 256; public static final int DEFAULT_HEIGHT = 192; - @Getter private GuidebookLevel level = new GuidebookLevel(); - @Getter private CameraSettings camera = new CameraSettings(); private int width = DEFAULT_WIDTH; private int height = DEFAULT_HEIGHT; - @Setter - @Getter private int sceneBackgroundColor = SCENE_BG_COLOR; - @Setter - @Getter private int sceneBorderColor = SCENE_BORDER_COLOR; - @Setter - @Getter private boolean showBackground = true; @Nullable private LytSize cameraViewportOverride; - @Getter private final List annotations = new ArrayList<>(); // Reuse annotation partitions instead of allocating new lists every frame. @@ -351,7 +328,6 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private boolean cachedBlockStatsButtonEnabled = true; private GuideIconButton.Role[] cachedSceneButtonRoles = SCENE_BUTTONS_SHOWN; - @Getter private boolean annotationsVisible = true; @Nullable private Integer visibleLayerOverride; @@ -383,8 +359,6 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { @Nullable private GuideSceneStructureCacheEntry initialStructureState; private boolean gridButtonEnabled = true; - @Setter - @Getter private boolean gridVisible = false; private boolean initialGridVisible = false; private boolean blockStatsEnabled = false; @@ -449,7 +423,6 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private AxisAlignedBB hoveredEntityBounds; @Nullable private MovingObjectPosition hoveredEntityHitResult; - @Setter private int @Nullable [] hoveredStructureLibHatch; private int currentMouseAbsX = -1; private int currentMouseAbsY = -1; @@ -1418,6 +1391,115 @@ public void clearSoundCues() { soundCues.clear(); } + public boolean isInteractive() { + return interactive; + } + + public boolean isSceneButtonsVisible() { + return sceneButtonsVisible; + } + + public void setSceneButtonsVisible(boolean sceneButtonsVisible) { + this.sceneButtonsVisible = sceneButtonsVisible; + } + + public boolean isBottomControlsVisible() { + return bottomControlsVisible; + } + + public void setBottomControlsVisible(boolean bottomControlsVisible) { + this.bottomControlsVisible = bottomControlsVisible; + clearCachedVisibleLayerSliderRects(); + clearCachedTierSliderRects(); + clearCachedChannelSliderRects(); + } + + public boolean isReserveBottomControlArea() { + return reserveBottomControlArea; + } + + public void setReserveBottomControlArea(boolean reserveBottomControlArea) { + this.reserveBottomControlArea = reserveBottomControlArea; + } + + public boolean isVisibleLayerSliderEnabled() { + return visibleLayerSliderEnabled; + } + + public void setVisibleLayerSliderEnabled(boolean visibleLayerSliderEnabled) { + this.visibleLayerSliderEnabled = visibleLayerSliderEnabled; + clearCachedVisibleLayerSliderRects(); + } + + public boolean isForceOriginAxesVisible() { + return forceOriginAxesVisible; + } + + public void setForceOriginAxesVisible(boolean forceOriginAxesVisible) { + this.forceOriginAxesVisible = forceOriginAxesVisible; + } + + public boolean isForceHideOriginAxes() { + return forceHideOriginAxes; + } + + public void setForceHideOriginAxes(boolean forceHideOriginAxes) { + this.forceHideOriginAxes = forceHideOriginAxes; + } + + public GuidebookLevel getLevel() { + return level; + } + + public CameraSettings getCamera() { + return camera; + } + + public int getSceneBackgroundColor() { + return sceneBackgroundColor; + } + + public void setSceneBackgroundColor(int sceneBackgroundColor) { + this.sceneBackgroundColor = sceneBackgroundColor; + } + + public int getSceneBorderColor() { + return sceneBorderColor; + } + + public void setSceneBorderColor(int sceneBorderColor) { + this.sceneBorderColor = sceneBorderColor; + } + + public boolean isShowBackground() { + return showBackground; + } + + public void setShowBackground(boolean showBackground) { + this.showBackground = showBackground; + } + + public List getAnnotations() { + return annotations; + } + + public boolean isAnnotationsVisible() { + return annotationsVisible; + } + + public boolean isGridVisible() { + return gridVisible; + } + + public void setGridVisible(boolean gridVisible) { + this.gridVisible = gridVisible; + cachedSceneButtonRolesDirty = true; + } + + public void setHoveredStructureLibHatch(int @Nullable [] hoveredStructureLibHatch) { + this.hoveredStructureLibHatch = hoveredStructureLibHatch; + } + private void notifyStructureLibSelectionChanged() { if (structureLibSelectionChangeListener != null) { structureLibSelectionChangeListener.accept(getStructureLibPreviewSelection()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java index 1d41330b..e6779859 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/element/ImportStructureLibElementCompiler.java @@ -76,7 +76,7 @@ public void compile(GuidebookLevel level, CameraSettings camera, PageCompiler co StructureLibSceneBinding binding = scene.registerStructureLibBinding(structureName); StructureLibPreviewSelection selectionOverride = binding.getPendingSelection() != null ? binding.getPendingSelection() - : scene.getPendingStructureLibPreviewSelection(); + : scene.getPendingStructureLibPreviewSelection(structureName); StructureLibPreviewSelection defaultSelection = sceneOptions .createSelection(requestedChannel == Integer.MIN_VALUE ? null : Integer.valueOf(requestedChannel)); StructureLibPreviewSelection selection = selectionOverride != null @@ -85,13 +85,16 @@ public void compile(GuidebookLevel level, CameraSettings camera, PageCompiler co StructureLibImportRequest request = new StructureLibImportRequest( controller, MdxAttrs.getString(compiler, errorSink, el, "piece", null), - StructureLibSceneOptions.resolveFacing(MdxAttrs.getString(compiler, errorSink, el, "facing", null), sceneOptions), - StructureLibSceneOptions.resolveRotation(MdxAttrs.getString(compiler, errorSink, el, "rotation", null), sceneOptions), - StructureLibSceneOptions.resolveFlip(MdxAttrs.getString(compiler, errorSink, el, "flip", null), sceneOptions), + StructureLibSceneOptions + .resolveFacing(MdxAttrs.getString(compiler, errorSink, el, "facing", null), sceneOptions), + StructureLibSceneOptions + .resolveRotation(MdxAttrs.getString(compiler, errorSink, el, "rotation", null), sceneOptions), + StructureLibSceneOptions + .resolveFlip(MdxAttrs.getString(compiler, errorSink, el, "flip", null), sceneOptions), Integer.valueOf(selection.getMasterTier()), applyControllerDefaults(controller, selection, sceneOptions), sceneOptions); - scene.setStructureLibInitialSelection(request.getPreviewSelection()); + scene.setPendingStructureLibPreviewSelection(structureName, request.getPreviewSelection()); StructureLibImportResult result = importService.importScene(request); attachMetadata(scene, structureName, request, result); diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java index f8a302cb..5f6673e7 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java @@ -17,7 +17,6 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.mdast.model.MdAstPosition; import com.hfstudio.guidenh.libs.micromark.ListUtils; - import com.hfstudio.guidenh.libs.micromark.Point; import com.hfstudio.guidenh.libs.micromark.Token; diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java index 089d9ab9..baa93414 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryMdxExpression.java @@ -2,7 +2,6 @@ import com.hfstudio.guidenh.libs.micromark.Assert; import com.hfstudio.guidenh.libs.micromark.CharUtil; - import com.hfstudio.guidenh.libs.micromark.Point; import com.hfstudio.guidenh.libs.micromark.State; import com.hfstudio.guidenh.libs.micromark.TokenizeContext; diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java index 7397c16d..0a36c693 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java @@ -6,7 +6,6 @@ import com.hfstudio.guidenh.libs.micromark.Assert; import com.hfstudio.guidenh.libs.micromark.CharUtil; import com.hfstudio.guidenh.libs.micromark.Construct; - import com.hfstudio.guidenh.libs.micromark.Point; import com.hfstudio.guidenh.libs.micromark.State; import com.hfstudio.guidenh.libs.micromark.TokenizeContext; @@ -607,7 +606,6 @@ private State lineStart(int code) { return new StateMachine()::start; } - private static boolean isPascalTagStart(int code) { return code >= Codes.uppercaseA && code <= Codes.uppercaseZ; } From b0251b06063737ddfffcd34277024e57a1baa4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 26 May 2026 20:49:43 +0800 Subject: [PATCH 033/136] fix: mark MDX tags recovered at EOF and fix tag-start position --- .../resolver/MdxSyntaxResolver.java | 2 +- .../libs/mdast/mdx/MdxMdastExtension.java | 6 ++++++ .../hfstudio/guidenh/libs/mdx/FactoryTag.java | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index 2e0abfbe..584c7ab6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -325,7 +325,7 @@ private TextSyntaxContext resolveTagStart(String text, int cursorIndex, @Nullabl SyntaxElementType.TAG_START, tagStart, cursorIndex, - new TagStartContext(tagStart, cursorIndex, partial, parentTagName)); + new TagStartContext(nameStart, cursorIndex, partial, parentTagName)); } return null; diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java index 5f6673e7..fcdb3da1 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdast/mdx/MdxMdastExtension.java @@ -16,6 +16,7 @@ import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.mdast.model.MdAstPosition; +import com.hfstudio.guidenh.libs.mdx.FactoryTag; import com.hfstudio.guidenh.libs.micromark.ListUtils; import com.hfstudio.guidenh.libs.micromark.Point; import com.hfstudio.guidenh.libs.micromark.Token; @@ -226,6 +227,11 @@ public static void exitMdxJsxTagSelfClosingMarker(MdastContext context, Token to public static void exitMdxJsxTag(MdastContext context, Token token) { var tag = getTag(context); + // A tag recovered at EOF cannot emit mdxJsxRecovery (consume(EOF) must + // be last), so the tokenizer sets this property directly on the token. + if (Boolean.TRUE.equals(token.get(FactoryTag.RECOVERED_AT_EOF))) { + tag.recovered = true; + } var stack = getStack(context); var tail = stack.isEmpty() ? null : stack.get(stack.size() - 1); diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java index 0a36c693..81f90269 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java @@ -8,6 +8,8 @@ import com.hfstudio.guidenh.libs.micromark.Construct; import com.hfstudio.guidenh.libs.micromark.Point; import com.hfstudio.guidenh.libs.micromark.State; +import com.hfstudio.guidenh.libs.micromark.Token; +import com.hfstudio.guidenh.libs.micromark.TokenProperty; import com.hfstudio.guidenh.libs.micromark.TokenizeContext; import com.hfstudio.guidenh.libs.micromark.Tokenizer; import com.hfstudio.guidenh.libs.micromark.Types; @@ -19,6 +21,11 @@ public class FactoryTag { private FactoryTag() {} + /** Marker set on a tag token when recovery happens at EOF (where a separate + * mdxJsxRecovery token cannot be emitted because consume(EOF) must be the + * last event). Read by {@code MdxMdastExtension.exitMdxJsxTag}. */ + public static final TokenProperty RECOVERED_AT_EOF = new TokenProperty<>(); + public static final Construct lazyLineEnd; static { @@ -44,11 +51,12 @@ class StateMachine { State returnState; Integer marker; Point startPoint; + Token tagToken; State start(int code) { Assert.check(code == Codes.lessThan, "expected `<`"); startPoint = context.now(); - effects.enter(tagType); + tagToken = effects.enter(tagType); effects.enter(tagMarkerType); effects.consume(code); effects.exit(tagMarkerType); @@ -570,6 +578,15 @@ private State recover(int code, String... openTokens) { effects.exit(openToken); } if (code == Codes.eof) { + // Cannot emit mdxJsxRecovery here — consume(EOF) must be the + // final event (see Tokenizer.Effects.consume). Mark the tag + // token directly when the tag NAME is still in progress + // (openTokens > 1 means we're inside a name-content token). + // When openTokens <= 1 the name is complete and the cursor + // is in the attribute area — don't flag as recovered. + if (openTokens.length > 1) { + tagToken.set(RECOVERED_AT_EOF, true); + } effects.exit(tagType); effects.consume(code); } else { From 4d3cd14687c898ee18647f42dd8e59010ea4a3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 26 May 2026 22:04:33 +0800 Subject: [PATCH 034/136] =?UTF-8?q?feat:=20Phase=201=20Runtime=20abstracti?= =?UTF-8?q?on=20=E2=80=94=20MasterScheduler,=20LytHost,=20WorkItem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unified tick scheduling and Lyt tree host environment: - MasterScheduler: single ClientTick.END entry point with priority queues - WorkItem interface: shouldRun() + tick(deadlineNs) → YIELD/DONE - LytHost: event queue, deferred task queue, NavigationState, ViewportState - LytHostWorkItem: thin WorkItem adapter for MasterScheduler - WarmupWorkItem, SearchIndexWorkItem, DevWatchWorkItem: migrate from GuideWarmupPump, GuideSearch, GuideDevWatcherPump - NavigationState: consolidates GuideScreenMemory, GuideBookmarkState, GuideScreenHomeHistory into single instance - ViewportState: encapsulates scroll/clamp/viewport rect logic - LytNode: add replaceChild(), isAttached() - LytDocument: add replaceChild() override - Wire MasterScheduler + LytHost in ClientProxy - GuideScreen: migrate to LytHost.getNavigation() / getViewport() --- .../com/hfstudio/guidenh/ClientProxy.java | 22 +++- .../guide/document/block/LytDocument.java | 17 +++ .../guidenh/guide/document/block/LytNode.java | 8 ++ .../guidenh/guide/internal/GuideScreen.java | 16 +-- .../guide/internal/host/DeferredTask.java | 11 ++ .../guide/internal/host/EventType.java | 17 +++ .../guidenh/guide/internal/host/LytEvent.java | 39 +++++++ .../guidenh/guide/internal/host/LytHost.java | 101 ++++++++++++++++++ .../guide/internal/host/LytHostWorkItem.java | 34 ++++++ .../guide/internal/host/NavigationState.java | 83 ++++++++++++++ .../guide/internal/host/ViewportState.java | 54 ++++++++++ .../internal/scheduler/DevWatchWorkItem.java | 40 +++++++ .../internal/scheduler/MasterScheduler.java | 84 +++++++++++++++ .../guide/internal/scheduler/Priority.java | 7 ++ .../scheduler/SearchIndexWorkItem.java | 28 +++++ .../internal/scheduler/WarmupWorkItem.java | 45 ++++++++ .../guide/internal/scheduler/WorkItem.java | 7 ++ .../guide/internal/scheduler/WorkResult.java | 6 ++ 18 files changed, 609 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/Priority.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkResult.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 2ba259c2..eeea8b36 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -64,10 +64,23 @@ import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.network.FMLNetworkEvent; +import com.hfstudio.guidenh.guide.internal.scheduler.MasterScheduler; +import com.hfstudio.guidenh.guide.internal.scheduler.WarmupWorkItem; +import com.hfstudio.guidenh.guide.internal.scheduler.SearchIndexWorkItem; +import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; +import com.hfstudio.guidenh.guide.internal.host.LytHost; +import com.hfstudio.guidenh.guide.internal.host.LytHostWorkItem; + import cpw.mods.fml.relauncher.Side; public class ClientProxy extends CommonProxy { + private static final LytHost lytHost = new LytHost(); + + public static LytHost getLytHost() { + return lytHost; + } + @Override public void preInit(FMLPreInitializationEvent event) { super.preInit(event); @@ -127,6 +140,10 @@ public void init(FMLInitializationEvent event) { CycleRegionWandModeHotkey.init(); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); GuideWarmupPump.init(); + MasterScheduler.init(); + MasterScheduler.getInstance().submit(new LytHostWorkItem(lytHost)); + MasterScheduler.getInstance().submit(new WarmupWorkItem()); + MasterScheduler.getInstance().submit(new SearchIndexWorkItem()); MinecraftForge.EVENT_BUS.register(this); } @@ -140,15 +157,14 @@ public void completeInit(FMLLoadCompleteEvent event) { super.completeInit(event); GuideDevelopmentResourcePackWatcher.init(); GuideDevWatcherPump.init(); + MasterScheduler.getInstance().submit(new DevWatchWorkItem()); GuideOnStartup.init(); } @SubscribeEvent public void onClientDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) { GuideME.closeSearch(); - GuideScreenMemory.clear(); - GuideScreenHomeHistory.shared() - .clear(); + lytHost.getNavigation().clear(); for (var guide : GuideRegistry.getAll()) { guide.resetWarmup(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java index d7719cf6..d79c294f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java @@ -80,6 +80,23 @@ public void append(LytBlock block) { invalidateLayout(); } + @Override + public void replaceChild(LytNode oldChild, LytNode newNode) { + if (oldChild instanceof LytBlock oldBlock) { + int idx = blocks.indexOf(oldBlock); + if (idx < 0) return; + oldBlock.parent = null; + if (newNode instanceof LytBlock newBlock) { + if (newBlock.parent != null) { + newBlock.parent.removeChild(newBlock); + } + newBlock.parent = this; + blocks.set(idx, newBlock); + } + invalidateLayout(); + } + } + public void clearContent() { for (var block : blocks) { block.parent = null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java index 8c25e91b..a0d5448e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java @@ -27,6 +27,14 @@ public abstract class LytNode implements Styleable { public void removeChild(LytNode node) {} + public void replaceChild(LytNode oldChild, LytNode newChild) { + // Default: no-op. LytDocument overrides. + } + + public boolean isAttached() { + return getDocument() != null; + } + public List getChildren() { return Collections.emptyList(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 37e311cf..a5b1dd0b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -103,6 +103,7 @@ import com.hfstudio.guidenh.guide.internal.home.HomePageLayout; import com.hfstudio.guidenh.guide.internal.item.RegionWandItem; import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; +import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar.ContextTarget; @@ -505,7 +506,7 @@ public static void openFromGuideHotkey(ResourceLocation guideId, @Nullable PageA } public static void openFromHomeHotkey() { - GuideScreenViewState remembered = GuideScreenMemory.consumeValidLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().recallLastContentState(); open(remembered != null ? remembered : GuideScreenViewState.home(), false); } @@ -538,7 +539,7 @@ private static GuideScreenRoute contentRoute(ResourceLocation guideId, @Nullable return null; } if (anchor == null) { - GuideScreenViewState remembered = GuideScreenMemory.consumeValidLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().recallLastContentState(); if (remembered != null && remembered.route() != null) { return remembered.route(); } @@ -695,11 +696,11 @@ private void finalizePendingViewState() { } private void rememberCurrentContentStateIfEligible() { - GuideScreenMemory.rememberContentState(captureCurrentViewState()); + ClientProxy.getLytHost().getNavigation().rememberContentState(captureCurrentViewState()); } private void rememberNavigationState() { - GuideScreenMemory.rememberNavigationState(navBar.captureState()); + ClientProxy.getLytHost().getNavigation().rememberNavBarState(guide.getId(), navBar.captureState()); } private boolean isNavigationNewPageButtonVisible() { @@ -1038,7 +1039,7 @@ private MutableGuide resolveGuideEditorTargetGuide() { if (guide != null) { return guide; } - GuideScreenViewState remembered = GuideScreenMemory.recallLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().recallLastContentState(); if (remembered != null && remembered.route() != null) { ResourceLocation rememberedGuideId = remembered.route() .guideId(); @@ -2692,6 +2693,7 @@ private void clampScroll() { int max = getMaxScroll(); if (scrollY < 0) scrollY = 0; if (scrollY > max) scrollY = max; + ClientProxy.getLytHost().getViewport().updateContent(contentW, contentH); } @Override @@ -4736,7 +4738,7 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { return; } if (result != null && result.bookmarkTogglePageId() != null) { - bookmarkState.toggle(result.bookmarkTogglePageId()); + ClientProxy.getLytHost().getNavigation().toggleBookmark(result.bookmarkTogglePageId()); mc.getSoundHandler() .playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F)); return; @@ -6681,7 +6683,7 @@ private void recordHomeHistoryIfEligible() { || !guide.pageExists(currentAnchor.pageId())) { return; } - homeHistory.record(guide.getId(), currentAnchor.pageId()); + ClientProxy.getLytHost().getNavigation().recordHomeHistory(guide.getId(), currentAnchor.pageId()); } private boolean isInsideSearchField(int mouseX, int mouseY) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java new file mode 100644 index 00000000..afd54ba9 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java @@ -0,0 +1,11 @@ +package com.hfstudio.guidenh.guide.internal.host; + +public interface DeferredTask { + + enum Priority { HIGH, LOW } + enum TaskResult { YIELD, DONE } + + Priority priority(); + TaskResult step(long deadlineNs); + boolean isDone(); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java new file mode 100644 index 00000000..0457cd80 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java @@ -0,0 +1,17 @@ +package com.hfstudio.guidenh.guide.internal.host; + +public enum EventType { + CLICK, + DOUBLE_CLICK, + MOUSE_ENTER, + MOUSE_LEAVE, + MOUSE_SCROLL, + KEY_PRESS, + INPUT_CHANGE, + LOAD_MORE, + NAVIGATE, + STUB_EXPANDED, + LAYOUT_INVALIDATED, + ATTACHED, + DETACHED +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java new file mode 100644 index 00000000..909a5890 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java @@ -0,0 +1,39 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.hfstudio.guidenh.guide.document.block.LytNode; + +public class LytEvent { + + private final EventType type; + private final LytNode target; + private LytNode currentTarget; + private final Map data; + private boolean propagationStopped; + + public LytEvent(EventType type, LytNode target) { + this(type, target, null); + } + + public LytEvent(EventType type, LytNode target, Map data) { + this.type = type; + this.target = target; + this.currentTarget = target; + this.data = data != null + ? Collections.unmodifiableMap(new LinkedHashMap<>(data)) + : Collections.emptyMap(); + } + + public EventType type() { return type; } + public LytNode target() { return target; } + public LytNode currentTarget() { return currentTarget; } + public Map data() { return data; } + + public void stopPropagation() { propagationStopped = true; } + public boolean isPropagationStopped() { return propagationStopped; } + + void setCurrentTarget(LytNode node) { this.currentTarget = node; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java new file mode 100644 index 00000000..b18db813 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -0,0 +1,101 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import java.util.ArrayDeque; +import java.util.Deque; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.document.block.LytDocument; +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; + +public class LytHost { + + @Nullable private LytDocument document; + private final ViewportState viewport = new ViewportState(); + private final NavigationState nav = new NavigationState(); + private final Deque eventQueue = new ArrayDeque<>(); + private final Deque taskQueue = new ArrayDeque<>(); + + // ===== Document ===== + + public void setDocument(@Nullable LytDocument doc) { + this.document = doc; + if (doc != null) { + viewport.updateContent(doc.getAvailableWidth(), doc.getContentHeight()); + } + } + + @Nullable public LytDocument getDocument() { return document; } + public ViewportState getViewport() { return viewport; } + public NavigationState getNavigation() { return nav; } + + // ===== Sync events ===== + + public void pushEvent(LytEvent event) { + eventQueue.addLast(event); + processEventsNow(); + } + + private void processEventsNow() { + while (!eventQueue.isEmpty()) { + LytEvent event = eventQueue.pollFirst(); + if (document == null || event.target() == null) continue; + LytNode target = event.target(); + if (target instanceof InteractiveElement interactive) { + switch (event.type()) { + case CLICK: + case DOUBLE_CLICK: + if (event.data().containsKey("x") && event.data().containsKey("y")) { + interactive.mouseClicked(null, + ((Number) event.data().get("x")).intValue(), + ((Number) event.data().get("y")).intValue(), + event.data().containsKey("button") + ? ((Number) event.data().get("button")).intValue() : 0, + event.type() == EventType.DOUBLE_CLICK); + } + break; + case MOUSE_SCROLL: + // InteractiveElement does not expose mouseScrolled yet + break; + default: + break; + } + } + } + } + + // ===== Async tasks ===== + + public void submitTask(DeferredTask task) { + taskQueue.addLast(task); + } + + public boolean hasWork() { + return !taskQueue.isEmpty(); + } + + public void step(long deadlineNs) { + while (!taskQueue.isEmpty() && System.nanoTime() < deadlineNs) { + DeferredTask task = taskQueue.peekFirst(); + DeferredTask.TaskResult result = task.step(deadlineNs); + if (result == DeferredTask.TaskResult.DONE) { + taskQueue.pollFirst(); + } + if (result == DeferredTask.TaskResult.YIELD) { + break; + } + } + } + + public int pendingTaskCount() { + return taskQueue.size(); + } + + public void clear() { + document = null; + eventQueue.clear(); + taskQueue.clear(); + nav.clear(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java new file mode 100644 index 00000000..00ed689e --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java @@ -0,0 +1,34 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import com.hfstudio.guidenh.guide.internal.scheduler.Priority; +import com.hfstudio.guidenh.guide.internal.scheduler.WorkItem; +import com.hfstudio.guidenh.guide.internal.scheduler.WorkResult; + +public class LytHostWorkItem implements WorkItem { + + private final LytHost host; + + public LytHostWorkItem(LytHost host) { + this.host = host; + } + + @Override + public Priority priority() { return Priority.HIGH; } + + @Override + public boolean shouldRun() { return host.hasWork(); } + + @Override + public WorkResult tick(long deadlineNs) { + host.step(deadlineNs); + return host.hasWork() ? WorkResult.YIELD : WorkResult.DONE; + } + + @Override + public boolean equals(Object o) { + return o instanceof LytHostWorkItem; + } + + @Override + public int hashCode() { return LytHostWorkItem.class.hashCode(); } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java new file mode 100644 index 00000000..61720376 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java @@ -0,0 +1,83 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.minecraft.util.ResourceLocation; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.internal.GuideScreenViewState; +import com.hfstudio.guidenh.guide.internal.screen.GuideNavBarState; + +public class NavigationState { + + @Nullable private ResourceLocation currentGuideId; + @Nullable private PageAnchor currentAnchor; + + private final Deque backStack = new ArrayDeque<>(); + + @Nullable private GuideScreenViewState lastContentViewState; + private final Map navBarStates = new LinkedHashMap<>(); + + private final Set bookmarks = new LinkedHashSet<>(); + + private final List homeHistory = new ArrayList<>(); + + public static class HomeHistoryEntry { + public final ResourceLocation guideId; + public final ResourceLocation pageId; + public HomeHistoryEntry(ResourceLocation guideId, ResourceLocation pageId) { + this.guideId = guideId; + this.pageId = pageId; + } + } + + public void setCurrent(ResourceLocation guideId, PageAnchor anchor) { + this.currentGuideId = guideId; + this.currentAnchor = anchor; + } + + @Nullable public ResourceLocation currentGuideId() { return currentGuideId; } + @Nullable public PageAnchor currentAnchor() { return currentAnchor; } + + public void pushHistory(GuideScreenViewState state) { backStack.push(state); } + @Nullable public GuideScreenViewState popHistory() { return backStack.pollFirst(); } + public Deque backStack() { return backStack; } + + public void rememberContentState(@Nullable GuideScreenViewState state) { lastContentViewState = state; } + @Nullable public GuideScreenViewState recallLastContentState() { return lastContentViewState; } + + public void rememberNavBarState(ResourceLocation guideId, GuideNavBarState state) { + if (state != null) navBarStates.put(guideId, state); + } + @Nullable public GuideNavBarState recallNavBarState(ResourceLocation guideId) { + return navBarStates.get(guideId); + } + + public boolean isBookmarked(ResourceLocation pageId) { return bookmarks.contains(pageId); } + public void toggleBookmark(ResourceLocation pageId) { + if (!bookmarks.remove(pageId)) { bookmarks.add(pageId); } + } + public Set bookmarks() { return bookmarks; } + + public void recordHomeHistory(ResourceLocation guideId, ResourceLocation pageId) { + homeHistory.add(0, new HomeHistoryEntry(guideId, pageId)); + } + public List homeHistory() { return homeHistory; } + + public void clear() { + backStack.clear(); + lastContentViewState = null; + navBarStates.clear(); + bookmarks.clear(); + homeHistory.clear(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java new file mode 100644 index 00000000..1fa30b54 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java @@ -0,0 +1,54 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import com.hfstudio.guidenh.guide.document.LytRect; + +public class ViewportState { + + private int scrollY; + private int viewportWidth; + private int viewportHeight; + private int contentWidth; + private int contentHeight; + private boolean layoutDirty; + + public void updateViewport(int width, int height) { + this.viewportWidth = width; + this.viewportHeight = height; + } + + public void updateContent(int width, int height) { + this.contentWidth = width; + this.contentHeight = height; + } + + public int scrollY() { return scrollY; } + public void scrollTo(int y) { this.scrollY = clampScroll(y); } + public void scrollBy(int delta) { scrollTo(scrollY + delta); } + + private int clampScroll(int y) { + int max = getMaxScrollY(); + if (y < 0) return 0; + if (y > max) return max; + return y; + } + + public void clampScroll() { + scrollY = clampScroll(scrollY); + } + + public int getMaxScrollY() { + return Math.max(0, contentHeight - viewportHeight); + } + + public LytRect getRect() { + return new LytRect(0, scrollY, viewportWidth, viewportHeight); + } + + public boolean isLayoutDirty() { return layoutDirty; } + public void setLayoutDirty(boolean dirty) { this.layoutDirty = dirty; } + + public int viewportWidth() { return viewportWidth; } + public int viewportHeight() { return viewportHeight; } + public int contentWidth() { return contentWidth; } + public int contentHeight() { return contentHeight; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java new file mode 100644 index 00000000..2f5b7db3 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java @@ -0,0 +1,40 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +import com.hfstudio.guidenh.guide.internal.GuideDevWatcherPump; +import com.hfstudio.guidenh.guide.internal.GuideRegistry; +import com.hfstudio.guidenh.guide.internal.MutableGuide; + +public class DevWatchWorkItem implements WorkItem { + + private static final int INTERVAL_TICKS = 20; + private int tickCounter; + + @Override + public Priority priority() { return Priority.LOW; } + + @Override + public boolean shouldRun() { + tickCounter++; + if (tickCounter < INTERVAL_TICKS) return false; + tickCounter = 0; + return GuideDevWatcherPump.hasAnyDevelopmentSources(GuideRegistry.getAll()); + } + + @Override + public WorkResult tick(long deadlineNs) { + for (MutableGuide guide : GuideRegistry.getAll()) { + if (guide.hasDevelopmentSources()) { + guide.tickDevelopmentSources(); + } + } + return WorkResult.DONE; + } + + @Override + public boolean equals(Object o) { + return o instanceof DevWatchWorkItem; + } + + @Override + public int hashCode() { return DevWatchWorkItem.class.hashCode(); } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java new file mode 100644 index 00000000..02fe161b --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java @@ -0,0 +1,84 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; + +public class MasterScheduler { + + private static final long HIGH_BUDGET_NS = 4_000_000L; + private static final long MEDIUM_BUDGET_NS = 2_000_000L; + private static final long LOW_BUDGET_NS = 2_000_000L; + + private final Map high = new LinkedHashMap<>(); + private final Map medium = new LinkedHashMap<>(); + private final Map low = new LinkedHashMap<>(); + + private static MasterScheduler instance; + + public static MasterScheduler getInstance() { return instance; } + + public static void init() { + instance = new MasterScheduler(); + FMLCommonHandler.instance() + .bus() + .register(instance); + } + + private MasterScheduler() {} + + @SubscribeEvent + public void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) return; + long deadlineNs = System.nanoTime() + HIGH_BUDGET_NS; + + processPriority(high, deadlineNs); + if (System.nanoTime() < deadlineNs) { + processPriority(medium, deadlineNs + MEDIUM_BUDGET_NS); + } + if (System.nanoTime() < deadlineNs) { + processPriority(low, deadlineNs + LOW_BUDGET_NS); + } + } + + public void submit(WorkItem item) { + Map queue = queueFor(item.priority()); + queue.put(item, item); + } + + public void cancel(WorkItem item) { + queueFor(item.priority()).remove(item); + } + + public void clear() { + high.clear(); + medium.clear(); + low.clear(); + } + + private Map queueFor(Priority p) { + switch (p) { + case HIGH: return high; + case MEDIUM: return medium; + default: return low; + } + } + + private void processPriority(Map queue, long deadlineNs) { + Iterator it = new ArrayList<>(queue.values()).iterator(); + while (it.hasNext() && System.nanoTime() < deadlineNs) { + WorkItem item = it.next(); + if (!item.shouldRun()) continue; + WorkResult result = item.tick(deadlineNs); + if (result == WorkResult.DONE) { + queue.remove(item); + } + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/Priority.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/Priority.java new file mode 100644 index 00000000..80dc109b --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/Priority.java @@ -0,0 +1,7 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +public enum Priority { + HIGH, + MEDIUM, + LOW +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java new file mode 100644 index 00000000..284d6a4f --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java @@ -0,0 +1,28 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +import com.hfstudio.guidenh.guide.internal.GuideME; +import com.hfstudio.guidenh.guide.internal.search.GuideSearch; + +public class SearchIndexWorkItem implements WorkItem { + + @Override + public Priority priority() { return Priority.LOW; } + + @Override + public boolean shouldRun() { return true; } + + @Override + public WorkResult tick(long deadlineNs) { + GuideME.getSearch() + .processWork(GuideSearch.BACKGROUND_TIME_PER_TICK); + return WorkResult.DONE; + } + + @Override + public boolean equals(Object o) { + return o instanceof SearchIndexWorkItem; + } + + @Override + public int hashCode() { return SearchIndexWorkItem.class.hashCode(); } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java new file mode 100644 index 00000000..9dd9bfbd --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java @@ -0,0 +1,45 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +import com.hfstudio.guidenh.guide.internal.GuideRegistry; +import com.hfstudio.guidenh.guide.internal.GuideWarmupScheduler; +import com.hfstudio.guidenh.guide.internal.MutableGuide; + +public class WarmupWorkItem implements WorkItem { + + private final GuideWarmupScheduler scheduler; + private long currentTick; + + public WarmupWorkItem() { + this.scheduler = new GuideWarmupScheduler(); + } + + @Override + public Priority priority() { return Priority.LOW; } + + @Override + public boolean shouldRun() { + for (MutableGuide guide : GuideRegistry.getAll()) { + if (guide.hasDevelopmentSources()) return true; + } + return true; // Always check — guides may have pages to warm up + } + + @Override + public WorkResult tick(long deadlineNs) { + currentTick++; + for (MutableGuide guide : GuideRegistry.getAll()) { + guide.populateWarmupScheduler(scheduler, currentTick); + } + scheduler.processTick(currentTick); + // Low priority — always yield so scheduler can run higher-priority items + return WorkResult.DONE; + } + + @Override + public boolean equals(Object o) { + return o instanceof WarmupWorkItem; + } + + @Override + public int hashCode() { return WarmupWorkItem.class.hashCode(); } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java new file mode 100644 index 00000000..92d0eeba --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java @@ -0,0 +1,7 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +public interface WorkItem { + Priority priority(); + boolean shouldRun(); + WorkResult tick(long deadlineNs); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkResult.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkResult.java new file mode 100644 index 00000000..34684be1 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkResult.java @@ -0,0 +1,6 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +public enum WorkResult { + YIELD, + DONE +} From d7c2bba6020fa5aa1bb5ac80001f512990212e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 26 May 2026 22:49:08 +0800 Subject: [PATCH 035/136] =?UTF-8?q?refactor:=20complete=20Phase=201=20migr?= =?UTF-8?q?ation=20=E2=80=94=20disable=20old=20tick=20pumps,=20wire=20LytH?= =?UTF-8?q?ost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove GuideWarmupPump.init() and GuideDevWatcherPump.init() calls - WarmupWorkItem and DevWatchWorkItem now fully replace them - Add WarmupWorkItem.clearScheduler() for GuideLightweightReloadService - Connect LytHost.setDocument() on page load in GuideScreen - Clean up unused imports in ClientProxy --- src/main/java/com/hfstudio/guidenh/ClientProxy.java | 11 ++++++----- .../guide/internal/GuideLightweightReloadService.java | 3 ++- .../hfstudio/guidenh/guide/internal/GuideScreen.java | 1 + .../guide/internal/scheduler/WarmupWorkItem.java | 4 ++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index eeea8b36..bcdd43dd 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -12,14 +12,12 @@ import com.hfstudio.guidenh.client.hotkey.OpenGuideHomeHotkey; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.client.hotkey.OpenSceneEditorHotkey; -import com.hfstudio.guidenh.guide.internal.GuideDevWatcherPump; import com.hfstudio.guidenh.guide.internal.GuideDevelopmentResourcePackWatcher; import com.hfstudio.guidenh.guide.internal.GuideME; import com.hfstudio.guidenh.guide.internal.GuideOnStartup; import com.hfstudio.guidenh.guide.internal.GuideRegistry; import com.hfstudio.guidenh.guide.internal.GuideReloadListener; import com.hfstudio.guidenh.guide.internal.GuideScreenMemory; -import com.hfstudio.guidenh.guide.internal.GuideWarmupPump; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; @@ -76,11 +74,16 @@ public class ClientProxy extends CommonProxy { private static final LytHost lytHost = new LytHost(); + private static final WarmupWorkItem warmupWorkItem = new WarmupWorkItem(); public static LytHost getLytHost() { return lytHost; } + public static WarmupWorkItem getWarmupWorkItem() { + return warmupWorkItem; + } + @Override public void preInit(FMLPreInitializationEvent event) { super.preInit(event); @@ -139,10 +142,9 @@ public void init(FMLInitializationEvent event) { AutocompleteProviders.register(new ImagePathProvider()); CycleRegionWandModeHotkey.init(); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); - GuideWarmupPump.init(); MasterScheduler.init(); MasterScheduler.getInstance().submit(new LytHostWorkItem(lytHost)); - MasterScheduler.getInstance().submit(new WarmupWorkItem()); + MasterScheduler.getInstance().submit(warmupWorkItem); MasterScheduler.getInstance().submit(new SearchIndexWorkItem()); MinecraftForge.EVENT_BUS.register(this); } @@ -156,7 +158,6 @@ public void postInit(FMLPostInitializationEvent event) { public void completeInit(FMLLoadCompleteEvent event) { super.completeInit(event); GuideDevelopmentResourcePackWatcher.init(); - GuideDevWatcherPump.init(); MasterScheduler.getInstance().submit(new DevWatchWorkItem()); GuideOnStartup.init(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index c37fce53..b6b44b8b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -14,6 +14,7 @@ import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; +import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.datadriven.GuidePageResourceSelector; @@ -84,7 +85,7 @@ public static void reloadGuides(IResourceManager resourceManager) { long registryUpdateNs = System.nanoTime() - stageStartedAt; stageStartedAt = System.nanoTime(); - GuideWarmupPump.clearScheduler(); + ClientProxy.getWarmupWorkItem().clearScheduler(); for (MutableGuide guide : GuideRegistry.getAll()) { guide.resetWarmup(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index a5b1dd0b..81debf7b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2521,6 +2521,7 @@ private void completePendingContentPageLoadIfNeeded() { } currentPage = loadedPage; document = loadedPage != null ? loadedPage.document() : null; + ClientProxy.getLytHost().setDocument(document); if (document != null && isSpecialPageWithSearchField()) { applySpecialPageSearchQuery(queryFromCurrentAnchor()); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java index 9dd9bfbd..49f0d594 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java @@ -35,6 +35,10 @@ public WorkResult tick(long deadlineNs) { return WorkResult.DONE; } + public void clearScheduler() { + scheduler.clear(); + } + @Override public boolean equals(Object o) { return o instanceof WarmupWorkItem; From 5ed12343b82341bc51ea8f120f3f7d565413339d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 26 May 2026 23:12:55 +0800 Subject: [PATCH 036/136] fix: sync ViewportState scroll, bridge NavigationState with old singletons - ViewportState.scrollTo() now called from clampScroll() to sync scroll position - Add recallNavigationState, consumeValidLastContentState, isSupportedContentAnchor, isValidContentRoute to NavigationState (migrated from GuideScreenMemory) - Replace last 2 GuideScreenMemory call sites in GuideScreen - Bridge bookmark/history writes to both NavigationState and old singletons - Document known Phase 1 residuals in design doc --- .../guidenh/guide/internal/GuideScreen.java | 12 ++++-- .../guide/internal/host/NavigationState.java | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 81debf7b..95452441 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -104,6 +104,7 @@ import com.hfstudio.guidenh.guide.internal.item.RegionWandItem; import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; import com.hfstudio.guidenh.ClientProxy; +import com.hfstudio.guidenh.guide.internal.host.NavigationState; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar.ContextTarget; @@ -494,7 +495,7 @@ private GuideScreen(GuideScreenRoute route, @Nullable GuideScreenViewState resto } catch (Throwable ignored) { navBar.setPinned(false); } - navBar.restoreState(GuideScreenMemory.recallNavigationState(), bookmarkState); + navBar.restoreState(ClientProxy.getLytHost().getNavigation().recallNavigationState(), bookmarkState); } public static void open(ResourceLocation guideId, @Nullable PageAnchor anchor) { @@ -506,7 +507,7 @@ public static void openFromGuideHotkey(ResourceLocation guideId, @Nullable PageA } public static void openFromHomeHotkey() { - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().recallLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().consumeValidLastContentState(); open(remembered != null ? remembered : GuideScreenViewState.home(), false); } @@ -539,7 +540,7 @@ private static GuideScreenRoute contentRoute(ResourceLocation guideId, @Nullable return null; } if (anchor == null) { - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().recallLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().consumeValidLastContentState(); if (remembered != null && remembered.route() != null) { return remembered.route(); } @@ -2695,6 +2696,7 @@ private void clampScroll() { if (scrollY < 0) scrollY = 0; if (scrollY > max) scrollY = max; ClientProxy.getLytHost().getViewport().updateContent(contentW, contentH); + ClientProxy.getLytHost().getViewport().scrollTo(scrollY); } @Override @@ -4740,6 +4742,7 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { } if (result != null && result.bookmarkTogglePageId() != null) { ClientProxy.getLytHost().getNavigation().toggleBookmark(result.bookmarkTogglePageId()); + bookmarkState.toggle(result.bookmarkTogglePageId()); mc.getSoundHandler() .playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F)); return; @@ -6679,12 +6682,13 @@ private void recordHomeHistoryIfEligible() { if (currentRoute == null || !currentRoute.isContent() || currentAnchor == null || currentAnchor.pageId() == null - || !GuideScreenMemory.isSupportedContentAnchor(currentAnchor) + || !NavigationState.isSupportedContentAnchor(currentAnchor) || guide == null || !guide.pageExists(currentAnchor.pageId())) { return; } ClientProxy.getLytHost().getNavigation().recordHomeHistory(guide.getId(), currentAnchor.pageId()); + homeHistory.record(guide.getId(), currentAnchor.pageId()); } private boolean isInsideSearchField(int mouseX, int mouseY) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java index 61720376..d02d7e4b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java @@ -14,7 +14,10 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.internal.GuideRegistry; +import com.hfstudio.guidenh.guide.internal.GuideScreenRoute; import com.hfstudio.guidenh.guide.internal.GuideScreenViewState; +import com.hfstudio.guidenh.guide.internal.MutableGuide; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBarState; public class NavigationState { @@ -73,6 +76,45 @@ public void recordHomeHistory(ResourceLocation guideId, ResourceLocation pageId) } public List homeHistory() { return homeHistory; } + public GuideNavBarState recallNavigationState() { + GuideNavBarState currentGuideState = currentGuideId != null + ? navBarStates.get(currentGuideId) + : null; + return currentGuideState != null ? currentGuideState : GuideNavBarState.defaultState(); + } + + @Nullable + public GuideScreenViewState consumeValidLastContentState() { + GuideScreenViewState state = lastContentViewState; + if (state == null) return null; + if (!isValidContentRoute(state.route())) { + lastContentViewState = null; + return null; + } + return state; + } + + public boolean isRememberable(@Nullable GuideScreenViewState state) { + if (state == null) return false; + GuideScreenRoute route = state.route(); + if (route == null || !route.isContent()) return false; + PageAnchor anchor = route.anchor(); + return anchor != null && isSupportedContentAnchor(anchor) && isValidContentRoute(route); + } + + public static boolean isSupportedContentAnchor(@Nullable PageAnchor anchor) { + return anchor != null; + } + + public boolean isValidContentRoute(@Nullable GuideScreenRoute route) { + if (route == null || !route.isContent()) return false; + ResourceLocation guideId = route.guideId(); + PageAnchor anchor = route.anchor(); + if (guideId == null || anchor == null) return false; + MutableGuide guide = GuideRegistry.getById(guideId); + return guide != null && guide.pageExists(anchor.pageId()); + } + public void clear() { backStack.clear(); lastContentViewState = null; From 1e2baac56b07d8fa537a4e4bb7c9bc80dc389677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 27 May 2026 01:45:25 +0800 Subject: [PATCH 037/136] =?UTF-8?q?refactor:=20Phase=202A=20=E2=80=94=20IR?= =?UTF-8?q?=20unification,=20MdAst=E2=86=92MdxJsx=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MdAstToMdxConverter: convert all MdAst nodes to MdxJsxElement in-place during PageCompiler.parse(), keeping only MdAstText as leaf - Add 14 new TagCompilers for standard elements: Block: p, h1-h6, ul/ol, li, pre, blockquote, table, hr Inline: strong, em, del/u/wavy/dotted, code, img - Simplify PageCompiler: eliminate all instanceof MdAst* branches in compileBlockContext() and compileFlowContent() - Simplify PageIndexer: same treatment — tag dispatch by MdxJsx name - Simplify GuideSiteHtmlCompiler: same treatment for 25+ branches - Adapt helper files: MarkdownRuntimeBlocks, MarkdownListSemantics, GuideTitleHeadings, HomePageSummaryExtractor, GuideMarkdownDefinitions - Add MdAstToMdxConverter.convert() calls after MdAst.fromMarkdown() in SceneEditorMarkdownCodec, SceneEditorMultilineTextArea, MdxSyntaxResolver - Add LytBox.replaceChild() for nested stub node replacement - Register all new TagCompilers in DefaultExtensions --- .../compiler/GuideMarkdownDefinitions.java | 17 +- .../guidenh/guide/compiler/PageCompiler.java | 206 +---- .../compiler/tags/BlockquoteCompiler.java | 44 + .../guide/compiler/tags/CodeCompiler.java | 32 + .../compiler/tags/DelUWaveMarkCompiler.java | 40 + .../guide/compiler/tags/EmphasisCompiler.java | 25 + .../guide/compiler/tags/HeadingCompiler.java | 31 + .../guide/compiler/tags/HrCompiler.java | 22 + .../guide/compiler/tags/ImageCompiler.java | 41 + .../guide/compiler/tags/ListCompiler.java | 33 + .../guide/compiler/tags/ListItemCompiler.java | 35 + .../compiler/tags/ParagraphCompiler.java | 26 + .../guide/compiler/tags/PreCompiler.java | 267 ++++++ .../guide/compiler/tags/StrongCompiler.java | 25 + .../guide/compiler/tags/TableCompiler.java | 65 ++ .../guidenh/guide/document/block/LytBox.java | 20 + .../resolver/MdxSyntaxResolver.java | 2 + .../gui/SceneEditorMultilineTextArea.java | 3 + .../editor/md/SceneEditorMarkdownCodec.java | 2 + .../extensions/DefaultExtensions.java | 30 +- .../home/HomePageSummaryExtractor.java | 43 +- .../markdown/MarkdownListSemantics.java | 33 +- .../markdown/MarkdownRuntimeBlocks.java | 74 +- .../markdown/MdAstToMdxConverter.java | 348 ++++++++ .../guide/internal/search/PageIndexer.java | 228 +---- .../guide/mediawiki/GuideTitleHeadings.java | 6 +- .../site/GuideSiteHtmlCompiler.java | 802 +++++++----------- 27 files changed, 1561 insertions(+), 939 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/EmphasisCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HrCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ParagraphCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StrongCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideMarkdownDefinitions.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideMarkdownDefinitions.java index 57c63608..bfe6e0bd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideMarkdownDefinitions.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/GuideMarkdownDefinitions.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; @@ -21,9 +22,23 @@ public static Map collect(MdAstParent parent) { public static Map collect(List children) { Map definitions = new HashMap<>(); for (MdAstAnyContent child : children) { - if (child instanceof MdAstDefinition definition && definition.identifier != null) { + // Pre-conversion: raw MdAstDefinition + if (child instanceof MdAstDefinition && ((MdAstDefinition) child).identifier != null) { + MdAstDefinition definition = (MdAstDefinition) child; definitions.putIfAbsent(definition.identifier, definition); } + // Post-conversion: element + if (child instanceof MdxJsxFlowElement && "definition".equals(((MdxJsxFlowElement) child).name())) { + MdxJsxFlowElement el = (MdxJsxFlowElement) child; + String identifier = el.getAttributeString("identifier", null); + if (identifier != null) { + MdAstDefinition def = new MdAstDefinition(); + def.identifier = identifier; + def.url = el.getAttributeString("url", ""); + def.title = el.getAttributeString("title", ""); + definitions.putIfAbsent(identifier, def); + } + } } return definitions; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 29d2d74d..b384cfab 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -75,6 +75,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.FootnotePreprocessor; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownActionLink; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownHtmlRuntimeNormalizer; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLatexShorthand; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownListSemantics; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLiteralAutolink; @@ -244,6 +245,7 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource long markdownParseNs = 0L; long latexRestoreNs = 0L; long htmlNormalizeNs = 0L; + long mdAstConvertNs = 0L; try { stageStartedAt = System.nanoTime(); astRoot = MdAst.fromMarkdown(parseContent, PARSE_OPTIONS); @@ -256,6 +258,11 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource stageStartedAt = System.nanoTime(); MarkdownHtmlRuntimeNormalizer.normalize(astRoot); htmlNormalizeNs = System.nanoTime() - stageStartedAt; + + stageStartedAt = System.nanoTime(); + Map definitions = GuideMarkdownDefinitions.collect(astRoot); + MdAstToMdxConverter.convert(astRoot, definitions); + mdAstConvertNs = System.nanoTime() - stageStartedAt; } catch (RuntimeException t) { if (t instanceof ParseException e) { markdownParseNs = System.nanoTime() - stageStartedAt; @@ -280,7 +287,7 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource long totalNs = System.nanoTime() - parseStartedAt; FMLLog.getLogger() .info( - "[GuideNH] [PageCompiler] Parsed page {} lang={} totalNs={} normalizeNs={} footnoteNs={} sourceFrontmatterNs={} latexMaskNs={} commentMaskNs={} markdownParseNs={} latexRestoreNs={} htmlNormalizeNs={} astFrontmatterNs={} parseFailed={}", + "[GuideNH] [PageCompiler] Parsed page {} lang={} totalNs={} normalizeNs={} footnoteNs={} sourceFrontmatterNs={} latexMaskNs={} commentMaskNs={} markdownParseNs={} latexRestoreNs={} htmlNormalizeNs={} mdAstConvertNs={} astFrontmatterNs={} parseFailed={}", id, language, totalNs, @@ -292,6 +299,7 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource markdownParseNs, latexRestoreNs, htmlNormalizeNs, + mdAstConvertNs, astFrontmatterNs, parseFailureMessage != null); @@ -544,9 +552,13 @@ public void compileInlineMarkdown(String source, LytFlowParent layoutParent) { public void compileInlineFragment(Collection children, LytFlowParent layoutParent) { for (MdAstAnyContent child : children) { - if (child instanceof MdAstParagraph paragraph) { - compileFlowContext(paragraph, layoutParent); - } else if (child instanceof MdAstParentnestedParent && !(child instanceof MdAstPhrasingContent)) { + if (child instanceof MdxJsxFlowElement el && "p".equals(el.name())) { + compileFlowContext(el, layoutParent); + } else if (child instanceof MdxJsxFlowElement el) { + for (var nestedChild : el.children()) { + compileFlowContent(layoutParent, nestedChild); + } + } else if (child instanceof MdAstParent nestedParent) { for (var nestedChild : nestedParent.children()) { compileFlowContent(layoutParent, nestedChild); } @@ -579,64 +591,38 @@ public void withSourceContext(String sourceText, Runnable action) { } public void compileBlockContext(List children, LytBlockContainer layoutParent) { - LytBlock previousLayoutChild = null; for (int i = 0; i < children.size(); i++) { var child = children.get(i); - LytBlock layoutChild; - if (child instanceof MdAstThematicBreak) { - layoutChild = new LytThematicBreak(); - } else if (child instanceof MdAstList astList) { - layoutChild = compileList(astList); - } else if (child instanceof MdAstCode astCode) { - layoutChild = compileCodeBlock(astCode); - } else if (child instanceof MdAstHeading astHeading) { - var heading = new LytHeading(); - heading.setDepth(astHeading.depth); - compileFlowContext(astHeading, heading); - layoutChild = heading; - } else if (child instanceof MdAstBlockquote astBlockquote) { - layoutChild = compileBlockquote(astBlockquote); - } else if (child instanceof MdAstParagraph astParagraph) { - var paragraph = new LytParagraph(); - compileFlowContext(astParagraph, paragraph); - paragraph.setMarginTop(DEFAULT_ELEMENT_SPACING); - paragraph.setMarginBottom(DEFAULT_ELEMENT_SPACING); - layoutChild = paragraph; - } else if (child instanceof MdAstYamlFrontmatter) { - // This is handled by compile directly - layoutChild = null; - } else if (child instanceof MdAstDefinition) { - layoutChild = null; - } else if (child instanceof GfmTable astTable) { - MarkdownTableMeta meta = extractMarkdownTableMeta(children, i + 1); - layoutChild = compileTable(astTable, meta.widthHints()); - if (meta.consumeChildCount() > 0) { - i += meta.consumeChildCount(); - } - } else if (child instanceof MdAstHTML astHtml) { - var paragraph = new LytParagraph(); - compileHtmlLiteral(paragraph, astHtml.value); - layoutChild = paragraph; - } else if (child instanceof MdxJsxFlowElement el) { + LytBlock layoutChild = null; + + if (child instanceof MdxJsxFlowElement el) { var compiler = tagCompilers.get(el.name()); if (compiler == null) { - layoutChild = createErrorBlock("Unhandled MDX element in block context", child); + layoutChild = createErrorBlock( + "Unhandled MDX element in block context: " + el.name(), child); } else { layoutChild = null; compiler.compileBlockContext(this, layoutParent, el); } - } else if (child instanceof MdAstPhrasingContent phrasingContent) { - // Wrap in a paragraph with no margins, but try appending to an existing paragraph before this - if (previousLayoutChild instanceof LytParagraph paragraph) { - compileFlowContent(paragraph, phrasingContent); - continue; - } else { - var paragraph = new LytParagraph(); - compileFlowContent(paragraph, phrasingContent); - layoutChild = paragraph; + } else if (child instanceof MdxJsxTextElement el) { + // Inline element at block level — wrap in a paragraph + var paragraph = new LytParagraph(); + var flowCompiler = tagCompilers.get(el.name()); + if (flowCompiler != null) { + flowCompiler.compileFlowContext(this, paragraph, el); } + layoutChild = paragraph; + } else if (child instanceof MdAstText text) { + var paragraph = new LytParagraph(); + var flowText = new LytFlowText(); + flowText.setText(text.value); + paragraph.append(flowText); + layoutChild = paragraph; + } else if (child instanceof MdAstDefinition) { + layoutChild = null; // handled via element } else { - layoutChild = createErrorBlock("Unhandled Markdown node in block context", child); + layoutChild = createErrorBlock( + "Unhandled node in block context: " + child.getClass().getSimpleName(), child); } if (layoutChild != null) { @@ -646,7 +632,6 @@ public void compileBlockContext(List children, LytBlo } layoutParent.append(layoutChild); } - previousLayoutChild = layoutChild; } } @@ -678,32 +663,9 @@ private LytList compileList(MdAstList astList) { return list; } + @Deprecated private LytBlock compileBlockquote(MdAstBlockquote astBlockquote) { - BlockquoteDirective directive = MarkdownRuntimeBlocks.parseBlockquoteDirective(astBlockquote); - if (directive != null) { - if (directive.alertType() != null) { - var alertBox = new LytAlertBox(); - alertBox.setTitle( - directive.alertType() - .displayText(), - directive.alertType()); - alertBox.setMarginTop(DEFAULT_ELEMENT_SPACING); - alertBox.setMarginBottom(DEFAULT_ELEMENT_SPACING); - compileDirectiveBody(directive, alertBox); - normalizeBlockMargins(alertBox); - return wrapFloatAwareIfNeeded(alertBox); - } - - var quoteBox = new LytQuoteBox(); - quoteBox.setQuoteStyle(directive.accentColor(), directive.title(), buildQuoteIcon(directive.icon())); - quoteBox.setMarginTop(DEFAULT_ELEMENT_SPACING); - quoteBox.setMarginBottom(DEFAULT_ELEMENT_SPACING); - compileDirectiveBody(directive, quoteBox); - normalizeBlockMargins(quoteBox); - shiftFirstParagraphDown(quoteBox, 1); - return wrapFloatAwareIfNeeded(quoteBox); - } - + // Dead code path — BlockquoteCompiler handles
now var blockquote = new LytVBox(); blockquote.setBackgroundColor(SymbolicColor.BLOCKQUOTE_BACKGROUND); blockquote.setPadding(5); @@ -712,8 +674,6 @@ private LytBlock compileBlockquote(MdAstBlockquote astBlockquote) { blockquote.setMarginTop(DEFAULT_ELEMENT_SPACING); blockquote.setMarginBottom(DEFAULT_ELEMENT_SPACING); compileBlockContext(astBlockquote, blockquote); - normalizeBlockMargins(blockquote); - shiftFirstParagraphDown(blockquote, 1); return wrapFloatAwareIfNeeded(blockquote); } @@ -756,25 +716,6 @@ private void compileListItem(MdAstListItem astListItem, LytListItem listItem, } } - private void compileDirectiveBody(BlockquoteDirective directive, LytBlockContainer parent) { - List children = directive.children(); - if (!children.isEmpty() && directive.firstParagraph() != null - && children.get(0) == directive.firstParagraph()) { - MdAstParagraph firstParagraph = cloneParagraphWithLeadingTextOverride( - directive.firstParagraph(), - directive.remainingText()); - if (!firstParagraph.children() - .isEmpty()) { - compileParagraphBlock(firstParagraph, parent); - } - for (int i = 1; i < children.size(); i++) { - compileBlockContext(Collections.singletonList(children.get(i)), parent); - } - return; - } - compileBlockContext(children, parent); - } - private MdAstParagraph cloneParagraphWithLeadingTextOverride(MdAstParagraph original, String leadingText) { MdAstParagraph copy = new MdAstParagraph(); boolean replaced = false; @@ -960,7 +901,8 @@ public void compileFlowContext(Collection children, L } private void compileFlowContent(LytFlowParent layoutParent, MdAstAnyContent content) { - LytFlowContent layoutChild; + LytFlowContent layoutChild = null; + if (content instanceof MdAstText astText) { if (compileActionLinks(layoutParent, astText.value)) { layoutChild = null; @@ -973,74 +915,18 @@ private void compileFlowContent(LytFlowParent layoutParent, MdAstAnyContent cont text.setText(astText.value); layoutChild = text; } - } else if (content instanceof MdAstInlineCode astCode) { - var text = new LytFlowText(); - text.setText(astCode.value); - text.modifyStyle( - style -> style.italic(true) - .whiteSpace(WhiteSpaceMode.PRE)); - layoutChild = text; - } else if (content instanceof MdAstStrong astStrong) { - var span = new LytFlowSpan(); - span.modifyStyle(style -> style.bold(true)); - compileFlowContext(astStrong, span); - layoutChild = span; - } else if (content instanceof MdAstEmphasis astEmphasis) { - var span = new LytFlowSpan(); - span.modifyStyle(style -> style.italic(true)); - compileFlowContext(astEmphasis, span); - layoutChild = span; - } else if (content instanceof MdAstDelete astEmphasis) { - var span = new LytFlowSpan(); - span.modifyStyle(style -> style.strikethrough(true)); - compileFlowContext(astEmphasis, span); - layoutChild = span; - } else if (content instanceof MdAstUnderline astUnderline) { - var span = new LytFlowSpan(); - span.modifyStyle(style -> style.underlined(true)); - compileFlowContext(astUnderline, span); - layoutChild = span; - } else if (content instanceof MdAstWavyUnderline astWavy) { - var span = new LytFlowSpan(); - span.modifyStyle(style -> style.wavyUnderline(true)); - compileFlowContext(astWavy, span); - layoutChild = span; - } else if (content instanceof MdAstDottedUnderline astDotted) { - var span = new LytFlowSpan(); - span.modifyStyle(style -> style.dottedUnderline(true)); - compileFlowContext(astDotted, span); - layoutChild = span; - } else if (content instanceof MdAstMark astMark) { - var span = new LytFlowSpan(); - span.modifyStyle(style -> style.backgroundColor(new ConstantColor(DEFAULT_MARK_BACKGROUND_COLOR))); - compileFlowContext(astMark, span); - layoutChild = span; - } else if (content instanceof MdAstBreak) { - layoutChild = new LytFlowBreak(); - } else if (content instanceof MdAstLink astLink) { - layoutChild = compileLink(astLink, layoutParent); - } else if (content instanceof MdAstLinkReference astLinkReference) { - layoutChild = compileLinkReference(astLinkReference, layoutParent); - } else if (content instanceof MdAstImage astImage) { - var inlineBlock = new LytFlowInlineBlock(); - inlineBlock.setBlock(compileImage(astImage)); - layoutChild = inlineBlock; - } else if (content instanceof MdAstImageReference astImageReference) { - var inlineBlock = new LytFlowInlineBlock(); - inlineBlock.setBlock(compileImageReference(astImageReference)); - layoutChild = inlineBlock; - } else if (content instanceof MdAstHTML astHtml) { - layoutChild = compileHtmlInline(astHtml.value); } else if (content instanceof MdxJsxTextElement el) { var compiler = tagCompilers.get(el.name()); if (compiler == null) { - layoutChild = createErrorFlowContent("Unhandled MDX element in flow context", content); + layoutChild = createErrorFlowContent( + "Unhandled MDX element in flow context: " + el.name(), content); } else { layoutChild = null; compiler.compileFlowContext(this, layoutParent, el); } } else { - layoutChild = createErrorFlowContent("Unhandled Markdown node in flow context", content); + layoutChild = createErrorFlowContent( + "Unhandled node in flow context: " + content.getClass().getSimpleName(), content); } if (layoutChild != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java new file mode 100644 index 00000000..09eee659 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java @@ -0,0 +1,44 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.block.LytVBox; +import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.style.BorderStyle; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class BlockquoteCompiler extends BlockTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("blockquote"); + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + LytVBox blockquote = new LytVBox(); + blockquote.setBackgroundColor(SymbolicColor.BLOCKQUOTE_BACKGROUND); + blockquote.setPadding(5); + blockquote.setPaddingLeft(10); + blockquote.setBorderLeft(new BorderStyle(SymbolicColor.TABLE_BORDER, 2)); + blockquote.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); + blockquote.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); + compiler.compileBlockContext(el.children(), blockquote); + + // Normalize block margins (equivalent to normalizeBlockMargins in PageCompiler) + var children = blockquote.getChildren(); + if (!children.isEmpty()) { + if (children.get(0) instanceof LytParagraph first) { + first.setMarginTop(0); + } + if (children.get(children.size() - 1) instanceof LytParagraph last) { + last.setMarginBottom(0); + } + } + parent.append(blockquote); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java new file mode 100644 index 00000000..388be265 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java @@ -0,0 +1,32 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowText; +import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.model.MdAstText; + +public class CodeCompiler extends FlowTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("code"); + } + + @Override + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + var text = new LytFlowText(); + // Extract text from child — has one MdAstText child from converter + String value = ""; + if (!el.children().isEmpty() && el.children().get(0) instanceof MdAstText t) { + value = t.value; + } + text.setText(value); + text.modifyStyle(style -> style.italic(true).whiteSpace(WhiteSpaceMode.PRE)); + parent.append(text); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java new file mode 100644 index 00000000..c221698d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java @@ -0,0 +1,40 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Set; +import java.util.LinkedHashSet; + +import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class DelUWaveMarkCompiler extends FlowTagCompiler { + + @Override + public Set getTagNames() { + var tags = new LinkedHashSet(); + tags.add("del"); + tags.add("u"); + tags.add("wavy"); + tags.add("dotted"); + return tags; + } + + @Override + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + var span = new LytFlowSpan(); + String name = el.name(); + if (name != null) { + switch (name) { + case "del" -> span.modifyStyle(s -> s.strikethrough(true)); + case "u" -> span.modifyStyle(s -> s.underlined(true)); + case "wavy" -> span.modifyStyle(s -> s.wavyUnderline(true)); + case "dotted" -> span.modifyStyle(s -> s.dottedUnderline(true)); + default -> {} + } + } + compiler.compileFlowContext(el.children(), span); + parent.append(span); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/EmphasisCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/EmphasisCompiler.java new file mode 100644 index 00000000..3bf576be --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/EmphasisCompiler.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class EmphasisCompiler extends FlowTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("em"); + } + + @Override + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + var span = new LytFlowSpan(); + span.modifyStyle(style -> style.italic(true)); + compiler.compileFlowContext(el.children(), span); + parent.append(span); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java new file mode 100644 index 00000000..e7de3772 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java @@ -0,0 +1,31 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytHeading; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class HeadingCompiler extends BlockTagCompiler { + + private static final Set TAG_NAMES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6"))); + + @Override + public Set getTagNames() { + return TAG_NAMES; + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + LytHeading heading = new LytHeading(); + int depth = Integer.parseInt(el.getAttributeString("depth", "0")); + heading.setDepth(depth); + compiler.compileFlowContext(el.children(), heading); + parent.append(heading); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HrCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HrCompiler.java new file mode 100644 index 00000000..3adaebaf --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HrCompiler.java @@ -0,0 +1,22 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytThematicBreak; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class HrCompiler extends BlockTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("hr"); + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + parent.append(new LytThematicBreak()); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java new file mode 100644 index 00000000..e4f2c7cc --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java @@ -0,0 +1,41 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytImage; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; +import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class ImageCompiler extends FlowTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("img"); + } + + @Override + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + LytImage image = new LytImage(); + String src = el.getAttributeString("src", ""); + ResourceLocation imageId = compiler.resolveId(src); + if (imageId != null) { + byte[] imageContent = compiler.loadAsset(imageId); + if (imageContent != null) { + image.setImage(imageId, imageContent); + } + } + String alt = el.getAttributeString("alt", ""); + String title = el.getAttributeString("title", ""); + if (!alt.isEmpty()) image.setAlt(alt); + if (!title.isEmpty()) image.setTitle(title); + + var inlineBlock = new LytFlowInlineBlock(); + inlineBlock.setBlock(image); + parent.append(inlineBlock); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java new file mode 100644 index 00000000..a75b5df2 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java @@ -0,0 +1,33 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytList; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class ListCompiler extends BlockTagCompiler { + + private static final Set TAG_NAMES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("ul", "ol"))); + + @Override + public Set getTagNames() { + return TAG_NAMES; + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + boolean ordered = "ol".equals(el.name()); + int start = Integer.parseInt(el.getAttributeString("start", "1")); + LytList list = new LytList(ordered, start); + for (var child : el.children()) { + compiler.compileBlockContext(Collections.singletonList(child), list); + } + parent.append(list); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java new file mode 100644 index 00000000..02ea12ea --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java @@ -0,0 +1,35 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytListItem; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class ListItemCompiler extends BlockTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("li"); + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + LytListItem listItem = new LytListItem(); + compiler.compileBlockContext(el.children(), listItem); + + // Fix up top/bottom margin for list item children + var children = listItem.getChildren(); + if (!children.isEmpty()) { + var firstChild = children.get(0); + if (firstChild instanceof LytBlock firstBlock) { + firstBlock.setMarginTop(0); + firstBlock.setMarginBottom(0); + } + } + parent.append(listItem); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ParagraphCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ParagraphCompiler.java new file mode 100644 index 00000000..db5078dd --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ParagraphCompiler.java @@ -0,0 +1,26 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class ParagraphCompiler extends BlockTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("p"); + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + LytParagraph paragraph = new LytParagraph(); + compiler.compileFlowContext(el.children(), paragraph); + paragraph.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); + paragraph.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); + parent.append(paragraph); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java new file mode 100644 index 00000000..08240e0e --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java @@ -0,0 +1,267 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; + +import com.github.bsideup.jabel.Desugar; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytCodeBlock; +import com.hfstudio.guidenh.guide.document.block.LytMermaidMindmap; +import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguage; +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguageDetector; +import com.hfstudio.guidenh.guide.internal.markdown.FileTreeCompiler; +import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.model.MdAstText; + +import cpw.mods.fml.common.FMLLog; + +public class PreCompiler extends BlockTagCompiler { + + private static final Pattern CODEBLOCK_META_WIDTH = Pattern.compile( + "(^|\\s)width=(\"([^\"]+)\"|'([^']+)'|(\\S+))"); + private static final Pattern CODEBLOCK_META_HEIGHT = Pattern.compile( + "(^|\\s)height=(\"([^\"]+)\"|'([^']+)'|(\\S+))"); + + @Override + public Set getTagNames() { + return Collections.singleton("pre"); + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + // Extract code text from children — should be a single MdAstText child + String codeText = ""; + var children = el.children(); + if (!children.isEmpty() && children.get(0) instanceof MdAstText text) { + codeText = text.value; + } + + String lang = el.getAttributeString("lang", null); + String meta = el.getAttributeString("meta", null); + + CodeBlockLanguage language = CodeBlockLanguageDetector.detect(lang, codeText); + + // CSV table + if (lang != null && "csv".equals(language.id())) { + LytBlock csvBlock = compileCsvCodeBlock(codeText, meta); + parent.append(csvBlock); + return; + } + + // File tree + if (isFileTreeFence(lang)) { + parent.append(FileTreeCompiler.compile(compiler, codeText)); + return; + } + + // Function graph + if (isFunctionGraphFence(lang)) { + parent.append(com.hfstudio.guidenh.guide.compiler.tags.functiongraph.FunctionGraphFenceParser.parse(codeText)); + return; + } + + // Mermaid + if ("mermaid".equals(language.id())) { + LytMermaidMindmap mermaidBlock = tryCompileMermaidMindmap(codeText); + if (mermaidBlock != null) { + parent.append(mermaidBlock); + return; + } + } + + // Default code block with syntax highlighting + LytCodeBlock codeBlock = new LytCodeBlock(); + codeBlock.setLanguageFenceName(lang != null ? lang : language.id()); + codeBlock.applyLanguage(language); + codeBlock.setCodeText(codeText); + Integer preferredWidth = parseCodeBlockWidth(meta); + if (preferredWidth != null) { + codeBlock.setPreferredBodyWidth(preferredWidth); + } + Integer forcedHeight = parseCodeBlockHeight(meta); + if (forcedHeight != null) { + codeBlock.setForcedBodyHeight(forcedHeight); + } + parent.append(codeBlock); + } + + // ---- CSV code block compilation ---- + + private LytBlock compileCsvCodeBlock(String source, @Nullable String meta) { + List> rows = CsvTableParser.parse(source); + if (rows.isEmpty()) { + LytCodeBlock codeBlock = new LytCodeBlock(); + codeBlock.setLanguageFenceName("csv"); + codeBlock.applyLanguage(new CodeBlockLanguage("csv", "CSV")); + codeBlock.setCodeText(source); + return codeBlock; + } + + CsvFenceMeta csvMeta = parseCsvFenceMeta(meta); + return CsvTableCompiler.buildTable(rows, csvMeta.header(), csvMeta.widthHints()); + } + + private CsvFenceMeta parseCsvFenceMeta(@Nullable String meta) { + if (meta == null || meta.trim().isEmpty()) { + return new CsvFenceMeta(true, Collections.emptyList()); + } + + boolean header = true; + List widthHints = Collections.emptyList(); + for (String token : splitMetaTokens(meta)) { + int equalsIndex = token.indexOf('='); + if (equalsIndex <= 0 || equalsIndex == token.length() - 1) { + continue; + } + + String key = token.substring(0, equalsIndex); + String value = stripOptionalQuotes(token.substring(equalsIndex + 1)); + if ("widths".equals(key)) { + widthHints = CsvTableCompiler.parseWidthHints(value); + } else if ("header".equals(key)) { + header = !"false".equalsIgnoreCase(value); + } + } + + return new CsvFenceMeta(header, widthHints); + } + + private List splitMetaTokens(String meta) { + List tokens = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + char quote = 0; + for (int i = 0; i < meta.length(); i++) { + char ch = meta.charAt(i); + if ((ch == '"' || ch == '\'') && (!inQuotes || ch == quote)) { + if (inQuotes && ch == quote) { + inQuotes = false; + quote = 0; + } else if (!inQuotes) { + inQuotes = true; + quote = ch; + } + current.append(ch); + continue; + } + if (Character.isWhitespace(ch) && !inQuotes) { + if (current.length() > 0) { + tokens.add(current.toString()); + current.setLength(0); + } + continue; + } + current.append(ch); + } + if (current.length() > 0) { + tokens.add(current.toString()); + } + return tokens; + } + + private String stripOptionalQuotes(String value) { + if (value.length() >= 2) { + char first = value.charAt(0); + char last = value.charAt(value.length() - 1); + if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) { + return value.substring(1, value.length() - 1); + } + } + return value; + } + + @Desugar + private record CsvFenceMeta(boolean header, List widthHints) {} + + // ---- Mermaid ---- + + private @Nullable LytMermaidMindmap tryCompileMermaidMindmap(String source) { + try { + String normalized = MermaidMindmapParser.normalize(source); + LytMermaidMindmap block = new LytMermaidMindmap( + MermaidMindmapParser.parse(normalized), normalized); + FMLLog.getLogger() + .info( + "[GuideNH] [PreCompiler] Compiled fenced Mermaid runtime block ({} chars)", + normalized.length()); + return block; + } catch (IllegalArgumentException e) { + FMLLog.getLogger() + .warn( + "[GuideNH] [PreCompiler] Failed to compile fenced Mermaid runtime block from source: {}", + source, + e); + return null; + } + } + + // ---- Static helpers (copied from PageCompiler) ---- + + private static boolean isFileTreeFence(@Nullable String fenceLanguage) { + if (fenceLanguage == null) { + return false; + } + String trimmed = fenceLanguage.trim(); + return "tree".equalsIgnoreCase(trimmed) || "filetree".equalsIgnoreCase(trimmed); + } + + private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { + if (fenceLanguage == null) { + return false; + } + String trimmed = fenceLanguage.trim(); + return "funcgraph".equalsIgnoreCase(trimmed) || "function".equalsIgnoreCase(trimmed) + || "functiongraph".equalsIgnoreCase(trimmed); + } + + private static @Nullable Integer parseCodeBlockWidth(@Nullable String meta) { + if (meta == null || meta.trim().isEmpty()) { + return null; + } + Matcher matcher = CODEBLOCK_META_WIDTH.matcher(meta); + if (!matcher.find()) { + return null; + } + String value = matcher.group(3) != null ? matcher.group(3) + : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); + if (value == null || value.trim().isEmpty()) { + return null; + } + try { + return Math.max(0, Integer.parseInt(value.trim())); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static @Nullable Integer parseCodeBlockHeight(@Nullable String meta) { + if (meta == null || meta.trim().isEmpty()) { + return null; + } + Matcher matcher = CODEBLOCK_META_HEIGHT.matcher(meta); + if (!matcher.find()) { + return null; + } + String value = matcher.group(3) != null ? matcher.group(3) + : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); + if (value == null || value.trim().isEmpty()) { + return null; + } + try { + return Math.max(0, Integer.parseInt(value.trim())); + } catch (NumberFormatException ignored) { + return null; + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StrongCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StrongCompiler.java new file mode 100644 index 00000000..5c5ac936 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StrongCompiler.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; + +public class StrongCompiler extends FlowTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("strong"); + } + + @Override + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + var span = new LytFlowSpan(); + span.modifyStyle(style -> style.bold(true)); + compiler.compileFlowContext(el.children(), span); + parent.append(span); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java new file mode 100644 index 00000000..70a4e3a8 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java @@ -0,0 +1,65 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.Collections; +import java.util.Set; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.table.LytTable; +import com.hfstudio.guidenh.guide.document.block.table.LytTableCell; +import com.hfstudio.guidenh.guide.document.block.table.LytTableRow; +import com.hfstudio.guidenh.guide.style.TextAlignment; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; + +public class TableCompiler extends BlockTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("table"); + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + LytTable table = new LytTable(); + table.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); + + // Parse align attribute back to list + String alignStr = el.getAttributeString("align", ""); + + boolean firstRow = true; + int rowIndex = 0; + for (var child : el.children()) { + if (child instanceof MdxJsxFlowElement tr && "tr".equals(tr.name())) { + LytTableRow row = table.appendRow(); + if (firstRow) { + row.modifyStyle(style -> style.bold(true)); + firstRow = false; + } + + int cellIndex = 0; + for (var cellChild : tr.children()) { + if (cellChild instanceof MdxJsxFlowElement td && "td".equals(td.name())) { + LytTableCell cell = row.appendCell(); + + // Apply alignment from the parsed align list + if (!alignStr.isEmpty()) { + String[] parts = alignStr.split(","); + if (cellIndex < parts.length) { + switch (parts[cellIndex].trim()) { + case "center" -> cell.modifyStyle(style -> style.alignment(TextAlignment.CENTER)); + case "right" -> cell.modifyStyle(style -> style.alignment(TextAlignment.RIGHT)); + } + } + } + + compiler.compileBlockContext(td.children(), cell); + cellIndex++; + } + } + rowIndex++; + } + } + parent.append(table); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java index c444740c..3bcd85e9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java @@ -41,6 +41,26 @@ public void append(LytBlock block) { children.add(block); } + @Override + public void replaceChild(LytNode oldChild, LytNode newChild) { + if (!(oldChild instanceof LytBlock)) return; + if (!(newChild instanceof LytBlock)) return; + LytBlock oldBlock = (LytBlock) oldChild; + LytBlock newBlock = (LytBlock) newChild; + int idx = children.indexOf(oldBlock); + if (idx < 0) return; + oldBlock.parent = null; + if (newBlock.parent != null) { + newBlock.parent.removeChild(newBlock); + } + newBlock.parent = this; + children.set(idx, newBlock); + LytDocument doc = getDocument(); + if (doc != null) { + doc.invalidateLayout(); + } + } + public void clearContent() { for (var child : children) { child.parent = null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index 584c7ab6..f59d6720 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -4,6 +4,7 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; @@ -42,6 +43,7 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { root = cachedRoot; } else { root = MdAst.fromMarkdown(text, PARSE_OPTIONS); + MdAstToMdxConverter.convert(root, java.util.Collections.emptyMap()); cachedText = text; cachedRoot = root; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index 2e0242dd..0d5addf8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -3,6 +3,7 @@ import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; +import java.util.Collections; import java.util.List; import net.minecraft.client.gui.FontRenderer; @@ -15,6 +16,7 @@ import org.lwjgl.opengl.GL11; import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.model.MdAstList; @@ -690,6 +692,7 @@ private void applySmartNewline() { // 1. Try AST-based list continuation MdAstRoot root = MdAst.fromMarkdown(text, GuideMarkdownOptions.runtime()); + MdAstToMdxConverter.convert(root, Collections.emptyMap()); MdAstListItem item = findEnclosingListItem(root, cursor); if (item != null) { int lineStart = findLineStart(text, cursor - 1); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java index 07b9b049..bc4c9fb7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java @@ -16,6 +16,7 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorElementModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorElementType; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneModel; @@ -168,6 +169,7 @@ public SceneEditorMarkdownParseResult parse(String markdown) { MdAstRoot root; try { root = MdAst.fromMarkdown(parseSource, PARSE_OPTIONS); + MdAstToMdxConverter.convert(root, Collections.emptyMap()); } catch (ParseException e) { return new SceneEditorMarkdownParseResult.SyntaxError(formatParseException(e)); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java b/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java index 1ea56c41..e3bbc6e5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java @@ -11,33 +11,46 @@ import com.hfstudio.guidenh.guide.compiler.TagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.ATagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.BlockquoteCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BoxFlowDirection; import com.hfstudio.guidenh.guide.compiler.tags.BoxTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BreakCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.CodeCompiler; import com.hfstudio.guidenh.guide.compiler.tags.ColorTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CommandLinkCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CommentTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.DelUWaveMarkCompiler; import com.hfstudio.guidenh.guide.compiler.tags.DetailsTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.DivTagCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.EmphasisCompiler; import com.hfstudio.guidenh.guide.compiler.tags.FileTreeTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.FloatingImageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.FootnoteListCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.HeadingCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.HrCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.ImageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.ItemGridCompiler; import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.ItemLinkCompiler; import com.hfstudio.guidenh.guide.compiler.tags.KbdTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.KeyBindTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.LatexTagCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.ListCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.ListItemCompiler; import com.hfstudio.guidenh.guide.compiler.tags.MarkTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.MermaidCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.ParagraphCompiler; import com.hfstudio.guidenh.guide.compiler.tags.PlayerNameTagCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.PreCompiler; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler; import com.hfstudio.guidenh.guide.compiler.tags.SoundLinkCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.StrongCompiler; import com.hfstudio.guidenh.guide.compiler.tags.StructureViewCompiler; import com.hfstudio.guidenh.guide.compiler.tags.SubPagesCompiler; import com.hfstudio.guidenh.guide.compiler.tags.SubscriptTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.SuperscriptTagCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.TableCompiler; import com.hfstudio.guidenh.guide.compiler.tags.TooltipTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.chart.BarChartCompiler; import com.hfstudio.guidenh.guide.compiler.tags.chart.ColumnChartCompiler; @@ -142,7 +155,22 @@ public static List tagCompilers() { new ScatterChartCompiler(), new FunctionGraphTagCompiler(), new FunctionTagCompiler(), - new LatexTagCompiler())); + new LatexTagCompiler(), + // Phase 2A: block-level compilers + new ParagraphCompiler(), + new HeadingCompiler(), + new ListCompiler(), + new ListItemCompiler(), + new PreCompiler(), + new BlockquoteCompiler(), + new TableCompiler(), + new HrCompiler(), + // Phase 2A: inline compilers + new StrongCompiler(), + new EmphasisCompiler(), + new DelUWaveMarkCompiler(), + new CodeCompiler(), + new ImageCompiler())); for (TagCompilerProvider provider : GuideNhIntegrationRegistry.global() .tagCompilerProviders()) { provider.appendTagCompilers(compilers); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java index 24d03713..e61eafef 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java @@ -3,10 +3,10 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading; -import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph; import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; +import com.hfstudio.guidenh.libs.mdast.model.MdAstText; public class HomePageSummaryExtractor { @@ -17,13 +17,13 @@ public String extract(@Nullable ParsedGuidePage page) { } for (MdAstAnyContent block : root.children()) { - if (block instanceof MdAstHeading) { + if (isHeading(block)) { continue; } - if (!(block instanceof MdAstParagraph paragraph)) { + if (!isParagraph(block)) { continue; } - String text = normalizeWhitespace(paragraph.toText()); + String text = normalizeWhitespace(extractText((MdxJsxElementFields) block)); if (!text.isEmpty()) { return text; } @@ -38,8 +38,8 @@ public String extractHeadingText(@Nullable ParsedGuidePage page) { } for (MdAstAnyContent block : root.children()) { - if (block instanceof MdAstHeading heading) { - String text = normalizeWhitespace(heading.toText()); + if (isHeading(block)) { + String text = normalizeWhitespace(extractText((MdxJsxElementFields) block)); if (!text.isEmpty()) { return text; } @@ -49,6 +49,35 @@ public String extractHeadingText(@Nullable ParsedGuidePage page) { return ""; } + private static boolean isHeading(MdAstAnyContent block) { + if (block instanceof MdxJsxElementFields el) { + String name = el.name(); + return name != null && name.length() == 2 && name.charAt(0) == 'h' + && name.charAt(1) >= '1' && name.charAt(1) <= '6'; + } + return false; + } + + private static boolean isParagraph(MdAstAnyContent block) { + return block instanceof MdxJsxElementFields el && "p".equals(el.name()); + } + + private static String extractText(MdxJsxElementFields el) { + StringBuilder sb = new StringBuilder(); + collectText(el, sb); + return sb.toString(); + } + + private static void collectText(MdxJsxElementFields el, StringBuilder sb) { + for (Object child : el.children()) { + if (child instanceof MdAstText) { + sb.append(((MdAstText) child).value); + } else if (child instanceof MdxJsxElementFields) { + collectText((MdxJsxElementFields) child, sb); + } + } + } + private String normalizeWhitespace(String text) { if (text == null || text.isEmpty()) { return ""; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java index b7a7f049..3b67a362 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java @@ -7,8 +7,8 @@ import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; public final class MarkdownListSemantics { @@ -18,24 +18,25 @@ public final class MarkdownListSemantics { private MarkdownListSemantics() {} public static @Nullable TaskMarker extractTaskMarker(List children) { - if (children.size() != 1 || !(children.get(0) instanceof MdAstParagraph paragraph)) { + if (children.size() != 1) { return null; } - if (paragraph.children() - .isEmpty()) { - return null; - } - if (!(paragraph.children() - .get(0) instanceof MdAstText text)) { - return null; + MdAstAnyContent firstChild = children.get(0); + // Post-conversion:

element wrapping the task text + if (firstChild instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) firstChild).name())) { + MdxJsxFlowElement p = (MdxJsxFlowElement) firstChild; + if (p.children().isEmpty()) { + return null; + } + if (p.children().get(0) instanceof MdAstText) { + MdAstText text = (MdAstText) p.children().get(0); + Matcher matcher = TASK_PATTERN.matcher(text.value); + if (matcher.matches()) { + return new TaskMarker(!" ".equals(matcher.group(1)), matcher.group(2)); + } + } } - - Matcher matcher = TASK_PATTERN.matcher(text.value); - if (!matcher.matches()) { - return null; - } - - return new TaskMarker(!" ".equals(matcher.group(1)), matcher.group(2)); + return null; } @Desugar diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java index 2177612c..e5ac5a6a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java @@ -9,17 +9,16 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstBlockquote; -import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode; -import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; public final class MarkdownRuntimeBlocks { private MarkdownRuntimeBlocks() {} - public static @Nullable GithubAlertBlock extractGithubAlert(MdAstBlockquote blockquote) { + public static @Nullable GithubAlertBlock extractGithubAlert(MdxJsxElementFields blockquote) { BlockquoteDirective directive = parseBlockquoteDirective(blockquote); if (directive == null || directive.alertType() == null) { return null; @@ -27,15 +26,14 @@ private MarkdownRuntimeBlocks() {} return new GithubAlertBlock(directive.alertType(), directive.children(), directive.remainingText()); } - public static @Nullable BlockquoteDirective parseBlockquoteDirective(MdAstBlockquote blockquote) { + public static @Nullable BlockquoteDirective parseBlockquoteDirective(MdxJsxElementFields blockquote) { FirstParagraphText firstParagraph = findFirstParagraphText(blockquote); if (firstParagraph == null) { return null; } String firstText = firstParagraph.text(); - if (firstText == null || firstText.trim() - .isEmpty()) { + if (firstText == null || firstText.trim().isEmpty()) { return null; } @@ -58,8 +56,7 @@ private MarkdownRuntimeBlocks() {} return null; } - String expression = trimmed.substring(2, directiveEnd) - .trim(); + String expression = trimmed.substring(2, directiveEnd).trim(); String title = null; ColorValue color = null; QuoteIconSpec icon = null; @@ -68,11 +65,8 @@ private MarkdownRuntimeBlocks() {} if (equalsIndex <= 0 || equalsIndex >= token.length() - 1) { continue; } - String key = token.substring(0, equalsIndex) - .trim(); - String value = stripOptionalQuotes( - token.substring(equalsIndex + 1) - .trim()); + String key = token.substring(0, equalsIndex).trim(); + String value = stripOptionalQuotes(token.substring(equalsIndex + 1).trim()); if (value.isEmpty()) { continue; } @@ -103,31 +97,42 @@ private MarkdownRuntimeBlocks() {} blockquote.children()); } - private static @Nullable FirstParagraphText findFirstParagraphText(MdAstBlockquote blockquote) { - for (MdAstAnyContent child : blockquote.children()) { - if (child instanceof MdAstParagraph paragraph) { - String text = getLeadingParagraphText(paragraph); - if (text != null && !text.trim() - .isEmpty()) { - return new FirstParagraphText(paragraph, text); + @Nullable + private static FirstParagraphText findFirstParagraphText(MdxJsxElementFields blockquote) { + for (Object child : blockquote.children()) { + if (child instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) child).name())) { + MdxJsxFlowElement p = (MdxJsxFlowElement) child; + String text = getLeadingParagraphText(p); + if (text != null && !text.trim().isEmpty()) { + return new FirstParagraphText(p, text); } - } else if (child instanceof MdAstText text && !text.value.trim() - .isEmpty()) { + } else if (child instanceof MdAstText) { + MdAstText text = (MdAstText) child; + if (!text.value.trim().isEmpty()) { return new FirstParagraphText(null, text.value); } + } } return null; } - private static @Nullable String getLeadingParagraphText(MdAstParagraph paragraph) { - for (MdAstAnyContent child : paragraph.children()) { - if (child instanceof MdAstText text && !text.value.trim() - .isEmpty()) { - return text.value; + @Nullable + private static String getLeadingParagraphText(MdxJsxFlowElement paragraph) { + for (Object child : paragraph.children()) { + if (child instanceof MdAstText) { + MdAstText text = (MdAstText) child; + if (!text.value.trim().isEmpty()) { + return text.value; + } } - if (child instanceof MdAstInlineCode code && !code.value.trim() - .isEmpty()) { - return code.value; + if (child instanceof MdxJsxFlowElement && "code".equals(((MdxJsxFlowElement) child).name())) { + // Extract text from element + MdxJsxFlowElement code = (MdxJsxFlowElement) child; + for (Object codeChild : code.children()) { + if (codeChild instanceof MdAstText) { + return ((MdAstText) codeChild).value; + } + } } } return null; @@ -187,7 +192,8 @@ private static String stripOptionalQuotes(String value) { return value; } - private static @Nullable ColorValue parseColor(String value) { + @Nullable + private static ColorValue parseColor(String value) { String normalized = value != null ? value.trim() : ""; if (normalized.isEmpty() || !normalized.startsWith("#")) { return null; @@ -208,7 +214,7 @@ private static String stripOptionalQuotes(String value) { @Desugar public record BlockquoteDirective(@Nullable GithubAlertType alertType, ColorValue accentColor, @Nullable String title, @Nullable QuoteIconSpec icon, String remainingText, - @Nullable MdAstParagraph firstParagraph, List children) {} + @Nullable MdxJsxFlowElement firstParagraph, List children) {} @Desugar public record QuoteIconSpec(QuoteIconKind kind, String value) {} @@ -220,7 +226,7 @@ public enum QuoteIconKind { } @Desugar - private record FirstParagraphText(@Nullable MdAstParagraph paragraph, String text) {} + private record FirstParagraphText(@Nullable MdxJsxFlowElement paragraph, String text) {} @Desugar public record GithubAlertBlock(GithubAlertType type, List children, diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java new file mode 100644 index 00000000..af2c10ff --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java @@ -0,0 +1,348 @@ +package com.hfstudio.guidenh.guide.internal.markdown; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter; +import com.hfstudio.guidenh.libs.micromark.extensions.gfm.Align; +import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTable; +import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableCell; +import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableRow; +import com.hfstudio.guidenh.libs.mdast.gfmstrikethrough.MdAstDelete; +import com.hfstudio.guidenh.libs.mdast.guidemark.MdAstMark; +import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstDottedUnderline; +import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstUnderline; +import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstWavyUnderline; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxExpressionAttribute; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; +import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; +import com.hfstudio.guidenh.libs.mdast.model.MdAstBlockquote; +import com.hfstudio.guidenh.libs.mdast.model.MdAstBreak; +import com.hfstudio.guidenh.libs.mdast.model.MdAstCode; +import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; +import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis; +import com.hfstudio.guidenh.libs.mdast.model.MdAstFlowContent; +import com.hfstudio.guidenh.libs.mdast.model.MdAstHTML; +import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading; +import com.hfstudio.guidenh.libs.mdast.model.MdAstImage; +import com.hfstudio.guidenh.libs.mdast.model.MdAstImageReference; +import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode; +import com.hfstudio.guidenh.libs.mdast.model.MdAstLink; +import com.hfstudio.guidenh.libs.mdast.model.MdAstLinkReference; +import com.hfstudio.guidenh.libs.mdast.model.MdAstList; +import com.hfstudio.guidenh.libs.mdast.model.MdAstListItem; +import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; +import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph; +import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; +import com.hfstudio.guidenh.libs.mdast.model.MdAstPhrasingContent; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; +import com.hfstudio.guidenh.libs.mdast.model.MdAstText; +import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak; +import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong; + +public final class MdAstToMdxConverter { + + private MdAstToMdxConverter() {} + + /** + * @param definitions pre-collected link/image reference definitions (from GuideMarkdownDefinitions.collect()) + */ + public static void convert(MdAstRoot root, Map definitions) { + convertParent(root, definitions); + } + + private static void convertParent(MdAstParent parent, Map definitions) { + // First: depth-first recursion into all MdAstParent children, using a snapshot + // to safely handle concurrent modification. + List children = parent.children(); + for (Object child : new ArrayList<>(children)) { + if (child instanceof MdAstParent childParent) { + convertParent(childParent, definitions); + } + } + + // Then convert the current level's children in-place. + if (isPhrasingParent(parent)) { + convertPhrasingChildren(castPhrasingChildren(parent.children()), definitions); + } else { + convertFlowChildren(castAnyChildren(parent.children()), definitions); + } + } + + private static boolean isPhrasingParent(MdAstParent parent) { + return parent instanceof MdAstParagraph || parent instanceof MdxJsxTextElement + || "link".equals(parent.type()) + || "strong".equals(parent.type()) + || "emphasis".equals(parent.type()) + || "delete".equals(parent.type()) + || "heading".equals(parent.type()); + } + + @SuppressWarnings("unchecked") + private static List castPhrasingChildren(List children) { + return (List) (List) children; + } + + @SuppressWarnings("unchecked") + private static List castAnyChildren(List children) { + return (List) (List) children; + } + + // ----------------------------------------------------------------------- + // Phrasing (inline) children conversion + // ----------------------------------------------------------------------- + + private static void convertPhrasingChildren(List children, + Map definitions) { + for (int i = 0; i < children.size(); i++) { + MdAstPhrasingContent child = children.get(i); + MdxJsxTextElement replacement = null; + + if (child instanceof MdAstStrong) { + replacement = createText("strong", ((MdAstStrong) child).children()); + } else if (child instanceof MdAstEmphasis) { + replacement = createText("em", ((MdAstEmphasis) child).children()); + } else if (child instanceof MdAstDelete) { + replacement = createText("del", ((MdAstDelete) child).children()); + } else if (child instanceof MdAstUnderline) { + replacement = createText("u", ((MdAstUnderline) child).children()); + } else if (child instanceof MdAstWavyUnderline) { + replacement = createText("wavy", ((MdAstWavyUnderline) child).children()); + } else if (child instanceof MdAstDottedUnderline) { + replacement = createText("dotted", ((MdAstDottedUnderline) child).children()); + } else if (child instanceof MdAstMark) { + replacement = createText("mark", ((MdAstMark) child).children()); + } else if (child instanceof MdAstLink link) { + MdxJsxTextElement el = createText("a", link.children()); + el.addAttribute("href", link.url()); + replacement = el; + } else if (child instanceof MdAstLinkReference ref) { + MdxJsxTextElement el = createText("a", ref.children()); + MdAstDefinition def = definitions.get(ref.identifier()); + el.addAttribute("href", def != null ? def.url() : ""); + replacement = el; + } else if (child instanceof MdAstImage image) { + MdxJsxTextElement el = createText("img", new ArrayList<>()); + el.addAttribute("src", image.url()); + if (image.alt != null) { + el.addAttribute("alt", image.alt); + } + if (image.title != null) { + el.addAttribute("title", image.title); + } + replacement = el; + } else if (child instanceof MdAstImageReference ref) { + MdxJsxTextElement el = createText("img", new ArrayList<>()); + MdAstDefinition def = definitions.get(ref.identifier()); + if (def != null) { + el.addAttribute("src", def.url()); + if (def.title != null) { + el.addAttribute("title", def.title); + } + } else { + el.addAttribute("src", ""); + } + if (ref.alt != null) { + el.addAttribute("alt", ref.alt); + } + replacement = el; + } else if (child instanceof MdAstInlineCode code) { + MdxJsxTextElement el = createText("code", new ArrayList<>()); + MdAstText text = new MdAstText(); + text.setValue(code.value); + addChildRaw(el, text); + replacement = el; + } else if (child instanceof MdAstHTML html) { + MdxJsxTextElement el = createText("span", new ArrayList<>()); + MdAstText text = new MdAstText(); + text.setValue(html.value); + addChildRaw(el, text); + replacement = el; + } else if (child instanceof MdAstBreak) { + replacement = createText("br", new ArrayList<>()); + } + + if (replacement != null) { + children.set(i, replacement); + convertParent(replacement, definitions); + } + // MdAstText, MdxJsxTextElement, MdxJsxFlowElement, MdxJsxAttribute, + // MdxJsxExpressionAttribute are silently passed through. + } + } + + // ----------------------------------------------------------------------- + // Flow (block) children conversion + // ----------------------------------------------------------------------- + + private static void convertFlowChildren(List children, + Map definitions) { + for (int i = 0; i < children.size(); i++) { + MdAstAnyContent child = children.get(i); + MdxJsxFlowElement replacement = null; + + if (child instanceof MdAstParagraph p) { + replacement = createFlow("p", p.children()); + } else if (child instanceof MdAstHeading h) { + MdxJsxFlowElement el = createFlow("h" + h.depth, h.children()); + el.addAttribute("depth", h.depth); + replacement = el; + } else if (child instanceof MdAstList list) { + String name = list.ordered ? "ol" : "ul"; + MdxJsxFlowElement el = createFlow(name, list.children()); + if (list.ordered && list.start != 1) { + el.addAttribute("start", list.start); + } + replacement = el; + } else if (child instanceof MdAstListItem item) { + replacement = createFlow("li", item.children()); + } else if (child instanceof MdAstCode code) { + MdxJsxFlowElement el = createFlow("pre", new ArrayList<>()); + if (code.lang != null) { + el.addAttribute("lang", code.lang); + } + if (code.meta != null) { + el.addAttribute("meta", code.meta); + } + MdAstText text = new MdAstText(); + text.setValue(code.value); + addChildRaw(el, text); + replacement = el; + } else if (child instanceof MdAstBlockquote bq) { + replacement = createFlow("blockquote", bq.children()); + } else if (child instanceof GfmTable table) { + MdxJsxFlowElement el = createFlow("table", table.children()); + String alignStr = serializeAlign(table.align); + if (alignStr != null) { + el.addAttribute("align", alignStr); + } + replacement = el; + } else if (child instanceof GfmTableRow row) { + replacement = createFlow("tr", row.children()); + } else if (child instanceof GfmTableCell cell) { + replacement = createFlow("td", cell.children()); + } else if (child instanceof MdAstThematicBreak) { + replacement = createFlow("hr", new ArrayList<>()); + } else if (child instanceof MdAstDefinition def) { + MdxJsxFlowElement el = createFlow("definition", new ArrayList<>()); + if (def.identifier != null) { + el.addAttribute("identifier", def.identifier); + } + if (def.url != null) { + el.addAttribute("url", def.url); + } + if (def.title != null) { + el.addAttribute("title", def.title); + } + replacement = el; + } else if (child instanceof MdAstYamlFrontmatter) { + // Remove from children + children.remove(i); + i--; + continue; + } else if (child instanceof MdAstHTML html) { + MdxJsxFlowElement el = createFlow("div", new ArrayList<>()); + MdAstText text = new MdAstText(); + text.setValue(html.value); + addChildRaw(el, text); + replacement = el; + } + + if (replacement != null) { + children.set(i, replacement); + convertParent(replacement, definitions); + } + // Already-converted types (MdxJsxFlowElement, MdxJsxTextElement) and + // leaf nodes (MdAstText) are silently passed through. + } + } + + // ----------------------------------------------------------------------- + // Factory helpers + // ----------------------------------------------------------------------- + + /** + * Creates a flow element with the given tag name and children. + *

+ * Uses raw-type list access to bypass the generic type check so that phrasing + * content (e.g. {@link MdxJsxTextElement}, {@link MdAstText}) can be placed + * inside flow elements where they are semantically valid (e.g. text inside + * {@code

}). + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static MdxJsxFlowElement createFlow(String name, List children) { + MdxJsxFlowElement element = new MdxJsxFlowElement(); + element.setName(name); + List rawChildren = element.children(); + for (MdAstAnyContent child : children) { + rawChildren.add(child); + } + return element; + } + + /** + * Creates a text (inline) element with the given tag name and children. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static MdxJsxTextElement createText(String name, List children) { + MdxJsxTextElement element = new MdxJsxTextElement(); + element.setName(name); + List rawChildren = element.children(); + for (MdAstPhrasingContent child : children) { + rawChildren.add(child); + } + return element; + } + + /** + * Adds an {@link MdAstNode} to a flow element's children list via raw-type + * access, bypassing the generic type check. This is needed when the child + * is phrasing content (text, inline elements) that are semantically valid + * inside the element (e.g. a {@link MdAstText} inside a {@code

} tag).
+     */
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static void addChildRaw(MdxJsxFlowElement element, MdAstNode node) {
+        ((List) element.children()).add(node);
+    }
+
+    /**
+     * Adds an {@link MdAstNode} to a text element's children list via raw-type
+     * access, bypassing the generic type check.  This is needed when the child
+     * is a non-phrasing node that is semantically valid inline (e.g. a
+     * {@link MdAstText} inside {@code }).
+     */
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static void addChildRaw(MdxJsxTextElement element, MdAstNode node) {
+        ((List) element.children()).add(node);
+    }
+
+    /**
+     * Serializes the GfmTable align list to a comma-separated lowercase string,
+     * e.g. {@code "left,center,right"}.  Returns {@code null} when the list is
+     * null or empty.
+     */
+    @Nullable
+    private static String serializeAlign(@Nullable List aligns) {
+        if (aligns == null || aligns.isEmpty()) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < aligns.size(); i++) {
+            if (i > 0) {
+                sb.append(",");
+            }
+            switch (aligns.get(i)) {
+                case LEFT -> sb.append("left");
+                case CENTER -> sb.append("center");
+                case RIGHT -> sb.append("right");
+                case NONE -> sb.append("none");
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
index 025dde75..797057d7 100644
--- a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
+++ b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
@@ -1,4 +1,3 @@
-
 package com.hfstudio.guidenh.guide.internal.search;
 
 import java.util.HashMap;
@@ -14,35 +13,14 @@
 import com.hfstudio.guidenh.guide.extensions.Extension;
 import com.hfstudio.guidenh.guide.extensions.ExtensionCollection;
 import com.hfstudio.guidenh.guide.extensions.ExtensionPoint;
-import com.hfstudio.guidenh.guide.internal.markdown.MarkdownListSemantics;
-import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks;
-import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter;
-import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTable;
-import com.hfstudio.guidenh.libs.mdast.gfmstrikethrough.MdAstDelete;
-import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstDottedUnderline;
-import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstUnderline;
-import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstWavyUnderline;
+import com.hfstudio.guidenh.guide.indices.PageIndex;
 import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields;
 import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstBlockquote;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstBreak;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstCode;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstHTML;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstImage;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstImageReference;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstLink;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstLinkReference;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstList;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstListItem;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph;
 import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong;
 import com.hfstudio.guidenh.libs.mdast.model.MdAstText;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak;
+import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition;
+import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter;
+import com.hfstudio.guidenh.libs.unist.UnistNode;
 
 import cpw.mods.fml.common.FMLLog;
 
@@ -51,7 +29,6 @@ public class PageIndexer implements IndexingContext {
     private final PageCollection pages;
     private final ExtensionCollection extensions;
     private final ResourceLocation pageId;
-
     private final Map tagCompilers = new HashMap<>();
 
     public PageIndexer(PageCollection pages, ExtensionCollection extensions, ResourceLocation pageId) {
@@ -83,206 +60,36 @@ public void index(MdAstRoot root, IndexingSink sink) {
 
     @Override
     public void indexContent(MdAstAnyContent content, IndexingSink sink) {
-        if (content instanceof MdAstThematicBreak) {
-            sink.appendBreak();
-        } else if (content instanceof MdAstList astList) {
-            indexList(astList, sink);
-        } else if (content instanceof MdAstCode astCode) {
-            sink.appendText(astCode, astCode.value);
-        } else if (content instanceof MdAstHeading astHeading) {
-            indexContent(astHeading.children(), sink);
-        } else if (content instanceof MdAstBlockquote astBlockquote) {
-            var alert = MarkdownRuntimeBlocks.extractGithubAlert(astBlockquote);
-            if (alert != null) {
-                sink.appendText(
-                    astBlockquote,
-                    alert.type()
-                        .displayText());
-                indexAlertChildren(alert, sink);
-            } else {
-                indexContent(astBlockquote.children(), sink);
-            }
-        } else if (content instanceof MdAstParagraph astParagraph) {
-            indexContent(astParagraph.children(), sink);
-        } else if (content instanceof MdAstDefinition) {
-            // Definitions contribute through references when used.
-        } else if (content instanceof MdAstYamlFrontmatter) {
-            // This is handled by compile directly
-        } else if (content instanceof GfmTable astTable) {
-            indexTable(astTable, sink);
-        } else if (content instanceof MdAstText astText) {
+        if (content instanceof MdAstText astText) {
             sink.appendText(astText, astText.value);
-        } else if (content instanceof MdAstInlineCode astCode) {
-            sink.appendText(astCode, astCode.value);
-        } else if (content instanceof MdAstStrong astStrong) {
-            indexContent(astStrong.children(), sink);
-        } else if (content instanceof MdAstEmphasis astEmphasis) {
-            indexContent(astEmphasis.children(), sink);
-        } else if (content instanceof MdAstDelete astDelete) {
-            indexContent(astDelete.children(), sink);
-        } else if (content instanceof MdAstUnderline astUnderline) {
-            indexContent(astUnderline.children(), sink);
-        } else if (content instanceof MdAstWavyUnderline astWavy) {
-            indexContent(astWavy.children(), sink);
-        } else if (content instanceof MdAstDottedUnderline astDotted) {
-            indexContent(astDotted.children(), sink);
-        } else if (content instanceof MdAstBreak) {
-            sink.appendBreak();
-        } else if (content instanceof MdAstLink astLink) {
-            indexLink(astLink, sink);
-        } else if (content instanceof MdAstLinkReference astLinkReference) {
-            indexContent(astLinkReference.children(), sink);
-        } else if (content instanceof MdAstImage astImage) {
-            indexImage(astImage, sink);
-        } else if (content instanceof MdAstImageReference astImageReference) {
-            if (astImageReference.alt != null && !astImageReference.alt.isEmpty()) {
-                sink.appendText(astImageReference, astImageReference.alt);
-            }
-        } else if (content instanceof MdAstHTML astHtml) {
-            sink.appendText(astHtml, stripHtmlTags(astHtml.value));
         } else if (content instanceof MdxJsxElementFields el) {
             var compiler = tagCompilers.get(el.name());
             if (compiler == null) {
                 FMLLog.getLogger()
                     .warn(
-                        "[GuideNH] [PageIndexer] Unhandled custom MDX element in guide search indexing: {}",
+                        "[GuideNH] [PageIndexer] Unhandled MDX element in guide search indexing: {}",
                         el.name());
+                // Fallback: index children content
+                indexContent(el.children(), sink);
             } else {
                 compiler.index(this, el, sink);
             }
+        } else if (content instanceof MdAstDefinition || content instanceof MdAstYamlFrontmatter) {
+            // Handled via conversion
         } else {
             FMLLog.getLogger()
-                .warn("[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}", content.type());
-        }
-        sink.appendBreak();
-    }
-
-    private void indexList(MdAstList astList, IndexingSink sink) {
-        for (var listContent : astList.children()) {
-            if (listContent instanceof MdAstListItem astListItem) {
-                var taskMarker = MarkdownListSemantics.extractTaskMarker(astListItem.children());
-                if (taskMarker != null) {
-                    indexListItemChildren(astListItem, taskMarker, sink);
-                    for (int i = 1; i < astListItem.children()
-                        .size(); i++) {
-                        indexContent(
-                            astListItem.children()
-                                .get(i),
-                            sink);
-                    }
-                } else {
-                    indexContent(astListItem.children(), sink);
-                }
-            } else {
-                FMLLog.getLogger()
-                    .warn("[GuideNH] [PageIndexer] Cannot handle list content: {}", listContent);
-            }
-        }
-    }
-
-    private void indexListItemChildren(MdAstListItem astListItem, MarkdownListSemantics.TaskMarker taskMarker,
-        IndexingSink sink) {
-        if (astListItem.children()
-            .isEmpty()
-            || !(astListItem.children()
-                .get(0) instanceof MdAstParagraph paragraph)) {
-            sink.appendText(astListItem, taskMarker.remainingText());
-            sink.appendBreak();
-            return;
-        }
-
-        indexParagraphWithLeadingTextOverride(paragraph, taskMarker.remainingText(), sink);
-    }
-
-    private void indexAlertChildren(MarkdownRuntimeBlocks.GithubAlertBlock alert, IndexingSink sink) {
-        if (!alert.children()
-            .isEmpty()
-            && alert.children()
-                .get(0) instanceof MdAstParagraph paragraph) {
-            if (!alert.remainingText()
-                .isEmpty()) {
-                indexParagraphWithLeadingTextOverride(paragraph, alert.remainingText(), sink);
-            }
-            for (int i = 1; i < alert.children()
-                .size(); i++) {
-                indexContent(
-                    alert.children()
-                        .get(i),
-                    sink);
-            }
-            return;
+                .warn("[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}",
+                    ((UnistNode) content).type());
         }
-
-        indexContent(alert.children(), sink);
     }
 
-    private void indexParagraphWithLeadingTextOverride(MdAstParagraph paragraph, String leadingText,
-        IndexingSink sink) {
-        boolean replaced = false;
-        for (var child : paragraph.children()) {
-            if (!replaced && child instanceof MdAstText) {
-                if (!leadingText.isEmpty()) {
-                    sink.appendText(paragraph, leadingText);
-                    sink.appendBreak();
-                }
-                replaced = true;
-                continue;
-            }
+    @Override
+    public void indexContent(List children, IndexingSink sink) {
+        for (MdAstAnyContent child : children) {
             indexContent(child, sink);
         }
     }
 
-    private void indexTable(GfmTable astTable, IndexingSink sink) {
-        for (var astRow : astTable.children()) {
-            var astCells = astRow.children();
-            for (var astCell : astCells) {
-                indexContent(astCell.children(), sink);
-            }
-        }
-    }
-
-    private void indexLink(MdAstLink astLink, IndexingSink sink) {
-        if (astLink.title != null && !astLink.title.isEmpty()) {
-            sink.appendText(astLink, astLink.title);
-        }
-        indexContent(astLink.children(), sink);
-    }
-
-    private void indexImage(MdAstImage astImage, IndexingSink sink) {
-        if (astImage.title != null && !astImage.title.isEmpty()) {
-            sink.appendText(astImage, astImage.title);
-        }
-        if (astImage.alt != null && !astImage.alt.isEmpty()) {
-            sink.appendText(astImage, astImage.alt);
-        }
-    }
-
-    private String stripHtmlTags(String html) {
-        if (html == null || html.isEmpty()) {
-            return "";
-        }
-        StringBuilder stripped = new StringBuilder(html.length());
-        boolean inTag = false;
-        for (int i = 0; i < html.length(); i++) {
-            char current = html.charAt(i);
-            if (current == '<') {
-                inTag = true;
-                continue;
-            }
-            if (current == '>') {
-                inTag = false;
-                continue;
-            }
-            if (!inTag) {
-                stripped.append(current);
-            }
-        }
-        return stripped.toString();
-    }
-
-    /**
-     * Get the current page id.
-     */
     @Override
     public ResourceLocation getPageId() {
         return pageId;
@@ -292,4 +99,9 @@ public ResourceLocation getPageId() {
     public PageCollection getPageCollection() {
         return pages;
     }
+
+    @Override
+    public  T getIndex(Class clazz) {
+        return pages.getIndex(clazz);
+    }
 }
diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/GuideTitleHeadings.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/GuideTitleHeadings.java
index 16b94fe7..075b16d2 100644
--- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/GuideTitleHeadings.java
+++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/GuideTitleHeadings.java
@@ -4,7 +4,7 @@
 import com.hfstudio.guidenh.guide.compiler.IndexingSink;
 import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage;
 import com.hfstudio.guidenh.guide.internal.search.PageIndexer;
-import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading;
+import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement;
 import com.hfstudio.guidenh.libs.unist.UnistNode;
 
 public class GuideTitleHeadings {
@@ -14,7 +14,7 @@ private GuideTitleHeadings() {}
     public static String resolveHeading1Title(Guide guide, ParsedGuidePage page) {
         for (Object child : page.getAstRoot()
             .children()) {
-            if (!(child instanceof MdAstHeading heading) || heading.depth != 1) {
+            if (!(child instanceof MdxJsxFlowElement el) || !"h1".equals(el.name())) {
                 continue;
             }
             StringBuilder title = new StringBuilder();
@@ -30,7 +30,7 @@ public void appendBreak() {
                     title.append(' ');
                 }
             };
-            new PageIndexer(guide, guide.getExtensions(), page.getId()).indexContent(heading.children(), sink);
+            new PageIndexer(guide, guide.getExtensions(), page.getId()).indexContent(el.children(), sink);
             if (title.length() > 0) {
                 return title.toString();
             }
diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
index 109bc682..6de9322c 100644
--- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
+++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
@@ -248,17 +248,12 @@ public String compileInlineFragment(List children, Gu
         String defaultNamespace, SceneResolver sceneResolver, @Nullable ResourceLocation currentPageId) {
         StringBuilder html = new StringBuilder();
         for (MdAstAnyContent child : children) {
-            if (child instanceof MdAstParagraph paragraph) {
-                html.append(
-                    compileChildren(paragraph.children(), templates, defaultNamespace, currentPageId, sceneResolver));
-            } else if (child instanceof MdAstParentnestedParent && !(child instanceof MdAstPhrasingContent)) {
-                html.append(
-                    compileChildren(
-                        nestedParent.children(),
-                        templates,
-                        defaultNamespace,
-                        currentPageId,
-                        sceneResolver));
+            if (child instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) child).name())) {
+                html.append(compileChildren(((MdxJsxFlowElement) child).children(),
+                    templates, defaultNamespace, currentPageId, sceneResolver));
+            } else if (child instanceof MdxJsxFlowElement) {
+                html.append(compileChildren(((MdxJsxFlowElement) child).children(),
+                    templates, defaultNamespace, currentPageId, sceneResolver));
             } else {
                 html.append(compileNode(child, templates, defaultNamespace, currentPageId, sceneResolver));
             }
@@ -277,164 +272,334 @@ private String compileChildren(List children, GuideSi
 
     private String compileNode(MdAstAnyContent node, GuideSiteTemplateRegistry templates, String defaultNamespace,
         @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) {
-        if (node instanceof MdAstParagraph paragraph) {
-            String displayFormula = extractSoleDisplayLatex(paragraph);
-            if (displayFormula != null) {
-                return renderLatex(displayFormula, null, 1.0f, 100.0f, false, null, 0, 0, true, templates);
-            }
-            return "

" - + compileChildren(paragraph.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + "

"; - } - if (node instanceof MdAstHeading heading) { - return compileHeading(heading, templates, defaultNamespace, currentPageId, sceneResolver); + if (node instanceof MdAstText) { + return compileText(((MdAstText) node).value(), templates, defaultNamespace, currentPageId); } - if (node instanceof MdAstBlockquote blockquote) { - return compileBlockquote(blockquote, templates, defaultNamespace, currentPageId, sceneResolver); + if (node instanceof MdxJsxElementFields) { + return compileMdxElement((MdxJsxElementFields) node, templates, defaultNamespace, + currentPageId, sceneResolver); } - if (node instanceof MdAstList list) { - return compileList(list, templates, defaultNamespace, currentPageId, sceneResolver); + if (node instanceof MdAstParent) { + return compileChildren(((MdAstParent) node).children(), templates, defaultNamespace, + currentPageId, sceneResolver); } - if (node instanceof MdAstCode code) { - return compileCodeBlock(code); + return ""; + } + + private String compileMdxElement(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + // Block-level elements + if ("p".equals(el.name())) { + return compileParagraph(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof GfmTable table) { - return compileTable(table, templates, defaultNamespace, currentPageId, sceneResolver); + if (isHeadingName(el.name())) { + return compileHeadingMdx(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdAstThematicBreak) { - return "
"; + if ("blockquote".equals(el.name())) { + return compileBlockquoteMdx(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdAstImage image) { - return compileMarkdownImage(image, currentPageId); + if ("ul".equals(el.name()) || "ol".equals(el.name())) { + return compileListMdx(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdAstText text) { - return compileText(text.value(), templates, defaultNamespace, currentPageId); + if ("li".equals(el.name())) { + return compileListItemMdx(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdAstDelete deleted) { - return "" - + compileChildren(deleted.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + if ("pre".equals(el.name())) { + return compileCodeBlockMdx(el); } - if (node instanceof MdAstMark mark) { - return "" - + compileChildren(mark.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + if ("table".equals(el.name())) { + return compileTableMdx(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdAstUnderline underline) { - return "" - + compileChildren(underline.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + if ("tr".equals(el.name())) { + return compileTableRowMdx(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdAstWavyUnderline wavy) { - return "" - + compileChildren(wavy.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + if ("td".equals(el.name()) || "th".equals(el.name())) { + return "" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) + + ""; } - if (node instanceof MdAstDottedUnderline dotted) { - return "" - + compileChildren(dotted.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + if ("hr".equals(el.name())) { + return "
"; } - if (node instanceof MdAstStrong strong) { - return "" - + compileChildren(strong.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + // Inline elements + if ("strong".equals(el.name())) { + return "" + compileChildren(el.children(), templates, defaultNamespace, + currentPageId, sceneResolver) + ""; } - if (node instanceof MdAstEmphasis emphasis) { - return "" - + compileChildren(emphasis.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + if ("em".equals(el.name())) { + return "" + compileChildren(el.children(), templates, defaultNamespace, + currentPageId, sceneResolver) + ""; } - if (node instanceof MdAstInlineCode inlineCode) { - return "" + escapeHtml(inlineCode.value()) + ""; + if ("del".equals(el.name())) { + return "" + compileChildren(el.children(), templates, defaultNamespace, + currentPageId, sceneResolver) + ""; } - if (node instanceof MdAstBreak) { - return "
"; + if ("u".equals(el.name())) { + return "" + compileChildren(el.children(), templates, + defaultNamespace, currentPageId, sceneResolver) + ""; } - if (node instanceof MdAstLink link && isSoundActionHref(link.url())) { - return compileSoundLink( - link.url(), - compileChildren(link.children(), templates, defaultNamespace, currentPageId, sceneResolver), - defaultNamespace, - currentPageId); + if ("wavy".equals(el.name())) { + return "" + compileChildren(el.children(), templates, + defaultNamespace, currentPageId, sceneResolver) + ""; } - if (node instanceof MdAstLink link) { - return "" - + compileChildren(link.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + if ("dotted".equals(el.name())) { + return "" + compileChildren(el.children(), templates, + defaultNamespace, currentPageId, sceneResolver) + ""; } - if (node instanceof MdxJsxFlowElement flowElement && isHtmlAnchorElement(flowElement)) { - return compileHtmlAnchor(flowElement, templates, defaultNamespace, currentPageId, sceneResolver); + if ("mark".equals(el.name())) { + return "" + compileChildren(el.children(), templates, + defaultNamespace, currentPageId, sceneResolver) + ""; } - if (node instanceof MdxJsxFlowElement flowElement && isHtmlBreakElement(flowElement)) { - return compileHtmlBreak(flowElement); + if ("code".equals(el.name())) { + return "" + escapeHtml(extractTextFromElement(el)) + ""; } - if (node instanceof MdxJsxFlowElement flowElement && isTooltipElement(flowElement)) { - return "

" + compileTooltip(flowElement, templates, defaultNamespace, currentPageId, sceneResolver) - + "

"; + if ("br".equals(el.name())) { + return "
"; } - if (node instanceof MdxJsxFlowElement flowElement && isRecipeElement(flowElement)) { - return compileRecipe(flowElement, defaultNamespace); + if ("a".equals(el.name())) { + return compileAnchorMdx(el, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdxJsxFlowElement flowElement && isSceneElement(flowElement)) { - return compileScene(flowElement, templates, defaultNamespace, currentPageId, sceneResolver); + if ("img".equals(el.name())) { + return compileImageMdx(el, currentPageId); } - if (node instanceof MdxJsxFlowElement flowElement && isFloatingImageElement(flowElement)) { - return compileFloatingImage(flowElement, templates, defaultNamespace, currentPageId, sceneResolver); + if ("definition".equals(el.name())) { + return ""; } - if (node instanceof MdxJsxFlowElement flowElement && isLatexElement(flowElement)) { - return compileLatex(flowElement, true, templates); + // Custom MDX tags (existing handlers) + if (el instanceof MdxJsxFlowElement) { + return compileCustomFlowElement((MdxJsxFlowElement) el, templates, defaultNamespace, + currentPageId, sceneResolver); } - if (node instanceof MdxJsxFlowElement flowElement) { - String rendered = mdxTagRenderer - .render(flowElement, defaultNamespace, currentPageId, templates, sceneResolver, this); - if (rendered != null) { - return rendered; - } + if (el instanceof MdxJsxTextElement) { + return compileCustomTextElement((MdxJsxTextElement) el, templates, defaultNamespace, + currentPageId, sceneResolver); } - if (node instanceof MdxJsxTextElement textElement && isHtmlAnchorElement(textElement)) { + return compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver); + } + + private boolean isHeadingName(@Nullable String name) { + return name != null && name.length() == 2 && name.charAt(0) == 'h' + && name.charAt(1) >= '1' && name.charAt(1) <= '6'; + } + + private String compileCustomFlowElement(MdxJsxFlowElement flowElement, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + if (isHtmlAnchorElement(flowElement)) + return compileHtmlAnchor(flowElement, templates, defaultNamespace, currentPageId, sceneResolver); + if (isHtmlBreakElement(flowElement)) return compileHtmlBreak(flowElement); + if (isTooltipElement(flowElement)) + return "

" + compileTooltip(flowElement, templates, defaultNamespace, currentPageId, sceneResolver) + + "

"; + if (isRecipeElement(flowElement)) return compileRecipe(flowElement, defaultNamespace); + if (isSceneElement(flowElement)) + return compileScene(flowElement, templates, defaultNamespace, currentPageId, sceneResolver); + if (isFloatingImageElement(flowElement)) + return compileFloatingImage(flowElement, templates, defaultNamespace, currentPageId, sceneResolver); + if (isLatexElement(flowElement)) return compileLatex(flowElement, true, templates); + String rendered = mdxTagRenderer + .render(flowElement, defaultNamespace, currentPageId, templates, sceneResolver, this); + if (rendered != null) return rendered; + return compileChildren(flowElement.children(), templates, defaultNamespace, currentPageId, sceneResolver); + } + + private String compileCustomTextElement(MdxJsxTextElement textElement, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + if (isHtmlAnchorElement(textElement)) return compileHtmlAnchor(textElement, templates, defaultNamespace, currentPageId, sceneResolver); + if (isHtmlBreakElement(textElement)) return compileHtmlBreak(textElement); + if (isTooltipElement(textElement)) + return compileTooltip(textElement, templates, defaultNamespace, currentPageId, sceneResolver); + if (isRecipeElement(textElement)) return compileRecipe(textElement, defaultNamespace); + if (isSceneElement(textElement)) + return compileScene(textElement, templates, defaultNamespace, currentPageId, sceneResolver); + if (isFloatingImageElement(textElement)) + return compileFloatingImage(textElement, templates, defaultNamespace, currentPageId, sceneResolver); + if (isLatexElement(textElement)) return compileLatex(textElement, false, templates); + String rendered = mdxTagRenderer + .render(textElement, defaultNamespace, currentPageId, templates, sceneResolver, this); + if (rendered != null) return rendered; + return compileChildren(textElement.children(), templates, defaultNamespace, currentPageId, sceneResolver); + } + + // Mdx-adapted helper methods + + private String compileParagraph(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + String displayFormula = extractSoleDisplayLatexFromElement(el); + if (displayFormula != null) { + return renderLatex(displayFormula, null, 1.0f, 100.0f, false, null, 0, 0, true, templates); } - if (node instanceof MdxJsxTextElement textElement && isHtmlBreakElement(textElement)) { - return compileHtmlBreak(textElement); + return "

" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) + + "

"; + } + + private String compileHeadingMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + int depth = Integer.parseInt(el.getAttributeString("depth", "1")); + depth = depth <= 0 ? 1 : Math.min(depth, 6); + String body = compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver); + String anchor = GuideSiteHrefResolver.headingAnchor(extractTextFromElement(el)); + if (anchor == null || anchor.isEmpty()) { + return "" + body + ""; } - if (node instanceof MdxJsxTextElement textElement && isTooltipElement(textElement)) { - return compileTooltip(textElement, templates, defaultNamespace, currentPageId, sceneResolver); + return "" + body + ""; + } + + private String compileBlockquoteMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + // Use the MdxJsx-adapted MarkdownRuntimeBlocks + BlockquoteDirective directive = MarkdownRuntimeBlocks.parseBlockquoteDirective(el); + if (directive != null && directive.alertType() != null) { + return compileAlertBoxMdx(directive, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdxJsxTextElement textElement && isRecipeElement(textElement)) { - return compileRecipe(textElement, defaultNamespace); + if (directive != null) { + return compileQuoteBoxMdx(directive, templates, defaultNamespace, currentPageId, sceneResolver); } - if (node instanceof MdxJsxTextElement textElement && isSceneElement(textElement)) { - return compileScene(textElement, templates, defaultNamespace, currentPageId, sceneResolver); + return "
" + compileChildren(el.children(), templates, defaultNamespace, + currentPageId, sceneResolver) + "
"; + } + + private String compileAlertBoxMdx(BlockquoteDirective directive, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + String body = compileChildren(directive.children(), templates, defaultNamespace, currentPageId, sceneResolver); + String typeName = directive.alertType().displayText(); + return "
" + escapeHtml(typeName) + "" + body + "
"; + } + + private String compileQuoteBoxMdx(BlockquoteDirective directive, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + String body = compileChildren(directive.children(), templates, defaultNamespace, currentPageId, sceneResolver); + StringBuilder html = new StringBuilder("
"); + if (directive.title() != null) { + html.append("").append(escapeHtml(directive.title())).append("
"); } - if (node instanceof MdxJsxTextElement textElement && isLatexElement(textElement)) { - return compileLatex(textElement, false, templates); + html.append(body).append("
"); + return html.toString(); + } + + private String compileListMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + String tag = "ol".equals(el.name()) ? "ol" : "ul"; + String startAttr = ""; + if ("ol".equals(el.name())) { + String startStr = el.getAttributeString("start", "1"); + if (!"1".equals(startStr)) { + startAttr = " start=\"" + escapeAttribute(startStr) + "\""; + } } - if (node instanceof MdxJsxTextElement textElement) { - String rendered = mdxTagRenderer - .render(textElement, defaultNamespace, currentPageId, templates, sceneResolver, this); - if (rendered != null) { - return rendered; + return "<" + tag + startAttr + ">" + + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) + + ""; + } + + private String compileListItemMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + return "
  • " + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) + + "
  • "; + } + + private String compileCodeBlockMdx(MdxJsxElementFields el) { + String codeText = extractTextFromElement(el); + String lang = el.getAttributeString("lang", null); + String langClass = lang != null ? " class=\"language-" + escapeAttribute(lang) + "\"" : ""; + return "
    " + escapeHtml(codeText) + "
    "; + } + + private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + StringBuilder html = new StringBuilder(""); + String alignStr = el.getAttributeString("align", ""); + String[] aligns = alignStr.isEmpty() ? new String[0] : alignStr.split(","); + boolean firstRow = true; + for (Object child : el.children()) { + if (child instanceof MdxJsxFlowElement && "tr".equals(((MdxJsxFlowElement) child).name())) { + MdxJsxFlowElement tr = (MdxJsxFlowElement) child; + html.append(""); + int cellIdx = 0; + for (Object cellChild : tr.children()) { + if (cellChild instanceof MdxJsxFlowElement && "td".equals(((MdxJsxFlowElement) cellChild).name())) { + String tag = firstRow ? "th" : "td"; + String align = ""; + if (cellIdx < aligns.length) { + String a = aligns[cellIdx].trim(); + if ("center".equals(a) || "right".equals(a)) { + align = " style=\"text-align:" + a + "\""; + } + } + html.append("<").append(tag).append(align).append(">"); + html.append(compileChildren(((MdxJsxFlowElement) cellChild).children(), templates, + defaultNamespace, currentPageId, sceneResolver)); + html.append(""); + cellIdx++; + } + } + html.append(""); + firstRow = false; } } - if (node instanceof MdAstListItem listItem) { - return compileListItem(listItem, templates, defaultNamespace, currentPageId, sceneResolver); + html.append("
    "); + return html.toString(); + } + + private String compileTableRowMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + return "" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) + + ""; + } + + private String compileAnchorMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + String href = el.getAttributeString("href", ""); + String body = compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver); + if (!href.isEmpty() && isSoundActionHref(href)) { + return compileSoundLink(href, body, defaultNamespace, currentPageId); } - if (node instanceof GfmTableRow row) { - return compileTableRow(row, templates, defaultNamespace, currentPageId, sceneResolver, false, null); + if (!href.isEmpty()) { + return "" + body + ""; } - if (node instanceof GfmTableCell cell) { - return "" + compileChildren(cell.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + return body; + } + + private String compileImageMdx(MdxJsxElementFields el, @Nullable ResourceLocation currentPageId) { + String src = el.getAttributeString("src", ""); + String alt = el.getAttributeString("alt", ""); + String title = el.getAttributeString("title", ""); + String resolvedSrc = GuideSiteHrefResolver.resolveRawHref(currentPageId, src); + StringBuilder html = new StringBuilder("\"").append(escapeAttribute(alt)).append("\"");"); + return html.toString(); + } + + private static String extractTextFromElement(MdxJsxElementFields el) { + StringBuilder sb = new StringBuilder(); + collectTextFromChildren(el, sb); + return sb.toString(); + } + + private static void collectTextFromChildren(MdxJsxElementFields el, StringBuilder sb) { + for (Object child : el.children()) { + if (child instanceof MdAstText) { + sb.append(((MdAstText) child).value); + } else if (child instanceof MdxJsxElementFields) { + collectTextFromChildren((MdxJsxElementFields) child, sb); + } } - if (node instanceof MdAstParentparent) { - return compileChildren(parent.children(), templates, defaultNamespace, currentPageId, sceneResolver); + } + + @Nullable + private String extractSoleDisplayLatexFromElement(MdxJsxElementFields el) { + if (el.children().size() != 1 || !(el.children().get(0) instanceof MdAstText)) { + return null; } - return ""; + return MarkdownLatexShorthand.extractSoleDisplayFormula(((MdAstText) el.children().get(0)).value); } private String compileTooltip(MdxJsxElementFields element, GuideSiteTemplateRegistry templates, @@ -559,17 +724,6 @@ private String compileScene(MdxJsxElementFields element, GuideSiteTemplateRegist return sceneTagRenderer.render(element, defaultNamespace, currentPageId, templates, sceneResolver.nextScene()); } - private String compileMarkdownImage(MdAstImage image, @Nullable ResourceLocation currentPageId) { - return buildImageTag( - "guide-image", - resolveImageSource(image.url(), currentPageId), - image.alt(), - image.title(), - null, - null, - null); - } - private String compileFloatingImage(MdxJsxElementFields element, GuideSiteTemplateRegistry templates, String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { String rawSrc = element.getAttributeString("src", ""); @@ -949,28 +1103,6 @@ private String compileSoundLink(String href, String body, String defaultNamespac return html.toString(); } - @Nullable - private String extractSoleDisplayLatex(MdAstParagraph paragraph) { - if (paragraph.children() - .size() != 1 - || !(paragraph.children() - .get(0) instanceof MdAstText text)) { - return null; - } - return MarkdownLatexShorthand.extractSoleDisplayFormula(text.value()); - } - - private String compileHeading(MdAstHeading heading, GuideSiteTemplateRegistry templates, String defaultNamespace, - @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { - int depth = heading.depth <= 0 ? 1 : Math.min(heading.depth, 6); - String body = compileChildren(heading.children(), templates, defaultNamespace, currentPageId, sceneResolver); - String anchor = GuideSiteHrefResolver.headingAnchor(heading.toText()); - if (anchor == null || anchor.isEmpty()) { - return "" + body + ""; - } - return "" + body + ""; - } - private String compileHtmlAnchor(MdxJsxElementFields element, GuideSiteTemplateRegistry templates, String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { String href = element.getAttributeString("href", ""); @@ -1051,269 +1183,6 @@ private String createTextTooltipTemplate(@Nullable String text, GuideSiteTemplat .isEmpty() ? null : templates.create(html); } - private String compileList(MdAstList list, GuideSiteTemplateRegistry templates, String defaultNamespace, - @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { - String tagName = list.ordered ? "ol" : "ul"; - StringBuilder html = new StringBuilder(); - html.append("<") - .append(tagName); - if (list.ordered && list.start > 1) { - html.append(" start=\"") - .append(list.start) - .append("\""); - } - html.append(">"); - html.append(compileChildren(list.children(), templates, defaultNamespace, currentPageId, sceneResolver)); - html.append(""); - return html.toString(); - } - - private String compileListItem(MdAstListItem listItem, GuideSiteTemplateRegistry templates, String defaultNamespace, - @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { - var taskMarker = MarkdownListSemantics.extractTaskMarker(listItem.children()); - if (taskMarker == null) { - return "
  • " - + compileChildren(listItem.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + "
  • "; - } - - StringBuilder html = new StringBuilder(); - html.append("
  • ") - .append("") - .append("
    ") - .append( - compileTaskListItemChildren( - listItem, - taskMarker, - templates, - defaultNamespace, - currentPageId, - sceneResolver)) - .append("
  • "); - return html.toString(); - } - - private String compileTaskListItemChildren(MdAstListItem listItem, MarkdownListSemantics.TaskMarker taskMarker, - GuideSiteTemplateRegistry templates, String defaultNamespace, @Nullable ResourceLocation currentPageId, - SceneResolver sceneResolver) { - if (listItem.children() - .isEmpty() - || !(listItem.children() - .get(0) instanceof MdAstParagraph paragraph)) { - return compileChildren(listItem.children(), templates, defaultNamespace, currentPageId, sceneResolver); - } - - StringBuilder html = new StringBuilder(); - html.append( - compileNode( - cloneParagraphWithLeadingTextOverride(paragraph, taskMarker.remainingText()), - templates, - defaultNamespace, - currentPageId, - sceneResolver)); - for (int i = 1; i < listItem.children() - .size(); i++) { - html.append( - compileNode( - listItem.children() - .get(i), - templates, - defaultNamespace, - currentPageId, - sceneResolver)); - } - return html.toString(); - } - - private MdAstParagraph cloneParagraphWithLeadingTextOverride(MdAstParagraph original, String leadingText) { - MdAstParagraph copy = new MdAstParagraph(); - boolean replaced = false; - for (var child : original.children()) { - if (!replaced && child instanceof MdAstText) { - if (leadingText != null && !leadingText.isEmpty()) { - MdAstText text = new MdAstText(); - text.setValue(leadingText); - copy.addChild(text); - } - replaced = true; - continue; - } - if (child instanceof MdAstNode astNode) { - copy.addChild(astNode); - } - } - return copy; - } - - private String compileCodeBlock(MdAstCode code) { - String lang = code.lang != null ? code.lang.toLowerCase(Locale.ROOT) : ""; - String meta = code.meta != null ? code.meta : ""; - switch (lang) { - case "csv" -> { - return GuideSiteGraphRenderer.renderCsvTable(code.value != null ? code.value : "", true); - } - case "tree", "filetree" -> { - return GuideSiteGraphRenderer.renderFileTree(code.value != null ? code.value : ""); - } - case "mermaid" -> { - String src = code.value != null ? code.value : ""; - try { - MermaidMindmapDocument doc = MermaidMindmapParser.parse(src); - return GuideSiteGraphRenderer.renderMermaidTree(doc); - } catch (Exception ignored) { - return "
    " + escapeHtml(src) + "
    "; - } - } - case "funcgraph", "functiongraph" -> { - String src = code.value != null ? code.value : ""; - try { - LytFunctionGraph graph = FunctionGraphFenceParser.parse(src); - return GuideSiteGraphRenderer.renderFunctionGraph(graph); - } catch (RuntimeException ignored) { - return "
    "
    -                        + escapeHtml(src)
    -                        + "
    "; - } - } - } - // Forced viewport: ``` width=220 height=96 - emits a sized scrollable container. - Integer width = parseMetaInt(meta, "width"); - Integer height = parseMetaInt(meta, "height"); - StringBuilder html = new StringBuilder(); - html.append("") - .append(escapeHtml(code.value != null ? code.value : "")) - .append("
    "); - return html.toString(); - } - - private static @Nullable Integer parseMetaInt(String meta, String key) { - if (meta == null || meta.isEmpty()) { - return null; - } - // Accept tokens like `width=220`, `width="220"`, `width='220'`. - Matcher m = Pattern.compile("(?:^|\\s)" + Pattern.quote(key) + "\\s*=\\s*\"?'?([0-9]+)\"?'?") - .matcher(meta); - if (m.find()) { - try { - return Integer.parseInt(m.group(1)); - } catch (NumberFormatException ignored) { - return null; - } - } - return null; - } - - private String compileBlockquote(MdAstBlockquote blockquote, GuideSiteTemplateRegistry templates, - String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { - BlockquoteDirective directive = MarkdownRuntimeBlocks.parseBlockquoteDirective(blockquote); - if (directive == null) { - return "
    " - + compileChildren(blockquote.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + "
    "; - } - StringBuilder cls = new StringBuilder("guide-quote"); - if (directive.alertType() != null) { - cls.append(" guide-alert guide-alert-") - .append( - directive.alertType() - .name() - .toLowerCase(Locale.ROOT)); - } - StringBuilder style = new StringBuilder(); - ColorValue accent = directive.accentColor(); - if (accent != null) { - style.append("--guide-quote-accent:") - .append(toCssColor(accent)) - .append(";"); - } - StringBuilder html = new StringBuilder(); - html.append("
    0) { - html.append(" style=\"") - .append(escapeAttribute(style.toString())) - .append("\""); - } - html.append(">"); - String title = directive.title(); - QuoteIconSpec icon = directive.icon(); - if ((title != null && !title.isEmpty()) || icon != null) { - html.append("
    "); - if (icon != null) { - String iconHtml = renderQuoteIcon(icon, templates, defaultNamespace, currentPageId, sceneResolver); - if (!iconHtml.isEmpty()) { - html.append(iconHtml); - } - } - if (title != null && !title.isEmpty()) { - html.append("") - .append(escapeHtml(title)) - .append(""); - } - html.append("
    "); - } - html.append("
    ") - .append(compileDirectiveBody(directive, templates, defaultNamespace, currentPageId, sceneResolver)) - .append("
    "); - return html.toString(); - } - - private String compileDirectiveBody(BlockquoteDirective directive, GuideSiteTemplateRegistry templates, - String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { - List children = directive.children(); - if (!children.isEmpty() && directive.firstParagraph() != null - && children.get(0) == directive.firstParagraph()) { - MdAstParagraph firstParagraph = cloneParagraphWithLeadingTextOverride( - directive.firstParagraph(), - directive.remainingText()); - StringBuilder html = new StringBuilder(); - if (!firstParagraph.children() - .isEmpty()) { - html.append(compileNode(firstParagraph, templates, defaultNamespace, currentPageId, sceneResolver)); - } - for (int index = 1; index < children.size(); index++) { - html.append( - compileNode(children.get(index), templates, defaultNamespace, currentPageId, sceneResolver)); - } - return html.toString(); - } - return compileChildren(children, templates, defaultNamespace, currentPageId, sceneResolver); - } - - /** - * Render the {@code icon=} / {@code iconPng=} / {@code iconItem=} marker shown in the quote - * title. Falls back to a plain text glyph when the requested icon kind cannot be resolved so - * the heading never collapses to nothing. - */ private String renderQuoteIcon(QuoteIconSpec icon, GuideSiteTemplateRegistry templates, String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { String value = icon.value(); @@ -1366,91 +1235,6 @@ private String renderExportError(String message) { return "" + escapeHtml(message) + ""; } - private String compileTable(GfmTable table, GuideSiteTemplateRegistry templates, String defaultNamespace, - @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { - StringBuilder html = new StringBuilder(); - html.append("
    "); - List rows = table.children(); - if (!rows.isEmpty()) { - html.append(""); - html.append( - compileTableRow( - rows.get(0), - templates, - defaultNamespace, - currentPageId, - sceneResolver, - true, - table.align)); - html.append(""); - } - if (rows.size() > 1) { - html.append(""); - for (int i = 1; i < rows.size(); i++) { - html.append( - compileTableRow( - rows.get(i), - templates, - defaultNamespace, - currentPageId, - sceneResolver, - false, - table.align)); - } - html.append(""); - } - html.append("
    "); - return html.toString(); - } - - private String compileTableRow(GfmTableRow row, GuideSiteTemplateRegistry templates, String defaultNamespace, - @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver, boolean header, - @Nullable List align) { - StringBuilder html = new StringBuilder(); - html.append(""); - List cells = row.children(); - for (int i = 0; i < cells.size(); i++) { - GfmTableCell cell = cells.get(i); - String tagName = header ? "th" : "td"; - html.append("<") - .append(tagName); - String alignCss = tableAlignCss(align, i); - if (alignCss != null) { - html.append(" style=\"") - .append(escapeAttribute(alignCss)) - .append("\""); - } - html.append(">") - .append(compileChildren(cell.children(), templates, defaultNamespace, currentPageId, sceneResolver)) - .append(""); - } - html.append(""); - return html.toString(); - } - - @Nullable - private String tableAlignCss(@Nullable List align, int index) { - if (align == null || index < 0 || index >= align.size()) { - return null; - } - Align value = align.get(index); - if (value == null || value == Align.NONE) { - return null; - } - if (value == Align.LEFT) { - return "text-align:left;"; - } - if (value == Align.CENTER) { - return "text-align:center;"; - } - if (value == Align.RIGHT) { - return "text-align:right;"; - } - return null; - } - private String resolveImageSource(String rawUrl, @Nullable ResourceLocation currentPageId) { String resolved = imageResolver.resolve(rawUrl, currentPageId); if (resolved == null || resolved.isEmpty()) { From 9d1038d7b78da78f8b015216b0f66cab65b6f137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 27 May 2026 02:22:26 +0800 Subject: [PATCH 038/136] =?UTF-8?q?fix:=20Phase=202A=20=E2=80=94=20runtime?= =?UTF-8?q?=20regressions=20from=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1: BlockquoteCompiler — call parseBlockquoteDirective() for alert/quote boxes - C2: ListItemCompiler — call extractTaskMarker() for [x]/[ ] checkboxes - C3: GuideSiteHtmlCompiler — restore code block sub-languages (csv/filetree/mermaid/functiongraph) - D6-1/D6-2: skip elements (no error block), forward children - Fix navigation: parse frontmatter BEFORE converter removes MdAstYamlFrontmatter - Fix table inline nodes: add phrasing containers (td/th/p/li etc.) to isPhrasingParent - FQN cleanup: replace inline FQNs with proper imports in ListItemCompiler, MdxSyntaxResolver --- .../guidenh/guide/compiler/PageCompiler.java | 39 +++++--- .../compiler/tags/BlockquoteCompiler.java | 91 +++++++++++++++++-- .../guide/compiler/tags/ListItemCompiler.java | 22 ++++- .../guide/compiler/tags/PreCompiler.java | 3 +- .../resolver/MdxSyntaxResolver.java | 4 +- .../gui/SceneEditorMultilineTextArea.java | 3 +- .../markdown/MdAstToMdxConverter.java | 26 ++++-- .../site/GuideSiteHtmlCompiler.java | 70 +++++++++++++- 8 files changed, 223 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index b384cfab..1039160b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -242,6 +242,7 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource String parseFailureMessage = null; UnistPoint parseFailureFrom = null; UnistPoint parseFailureTo = null; + Frontmatter frontmatter; long markdownParseNs = 0L; long latexRestoreNs = 0L; long htmlNormalizeNs = 0L; @@ -259,8 +260,15 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource MarkdownHtmlRuntimeNormalizer.normalize(astRoot); htmlNormalizeNs = System.nanoTime() - stageStartedAt; + // Collect definitions before conversion (converter needs them + // for link/image reference resolution). stageStartedAt = System.nanoTime(); Map definitions = GuideMarkdownDefinitions.collect(astRoot); + + // Parse frontmatter BEFORE conversion — the converter removes + // MdAstYamlFrontmatter from children. + frontmatter = parseFrontmatter(id, astRoot); + MdAstToMdxConverter.convert(astRoot, definitions); mdAstConvertNs = System.nanoTime() - stageStartedAt; } catch (RuntimeException t) { @@ -274,11 +282,9 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource .error("[GuideNH] [PageCompiler] {}", errorMessage, t); parseFailureMessage = errorMessage + ": \n" + t; astRoot = buildErrorPage(parseFailureMessage); + frontmatter = new Frontmatter(null, Collections.emptyMap()); } - // Find front-matter - stageStartedAt = System.nanoTime(); - var frontmatter = parseFrontmatter(id, astRoot); long astFrontmatterNs = System.nanoTime() - stageStartedAt; if (parseFailureMessage != null && sourceFrontmatter.navigationEntry() != null) { frontmatter = sourceFrontmatter; @@ -596,14 +602,19 @@ public void compileBlockContext(List children, LytBlo LytBlock layoutChild = null; if (child instanceof MdxJsxFlowElement el) { - var compiler = tagCompilers.get(el.name()); - if (compiler == null) { - layoutChild = createErrorBlock( - "Unhandled MDX element in block context: " + el.name(), child); + // Definition elements are metadata, not rendered + if ("definition".equals(el.name())) { + layoutChild = null; } else { + var compiler = tagCompilers.get(el.name()); + if (compiler == null) { + layoutChild = createErrorBlock( + "Unhandled MDX element in block context: " + el.name(), child); + } else { layoutChild = null; compiler.compileBlockContext(this, layoutParent, el); } + } } else if (child instanceof MdxJsxTextElement el) { // Inline element at block level — wrap in a paragraph var paragraph = new LytParagraph(); @@ -916,13 +927,19 @@ private void compileFlowContent(LytFlowParent layoutParent, MdAstAnyContent cont layoutChild = text; } } else if (content instanceof MdxJsxTextElement el) { - var compiler = tagCompilers.get(el.name()); - if (compiler == null) { - layoutChild = createErrorFlowContent( - "Unhandled MDX element in flow context: " + el.name(), content); + // "span" wraps residual inline HTML — pass through children + if ("span".equals(el.name())) { + compileFlowContext(el, layoutParent); + layoutChild = null; } else { + var compiler = tagCompilers.get(el.name()); + if (compiler == null) { + layoutChild = createErrorFlowContent( + "Unhandled MDX element in flow context: " + el.name(), content); + } else { layoutChild = null; compiler.compileFlowContext(this, layoutParent, el); + } } } else { layoutChild = createErrorFlowContent( diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java index 09eee659..ccb2a59f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java @@ -4,12 +4,21 @@ import java.util.Set; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytAlertBox; +import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.BlockquoteDirective; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; import com.hfstudio.guidenh.guide.style.BorderStyle; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import org.jetbrains.annotations.Nullable; public class BlockquoteCompiler extends BlockTagCompiler { @@ -20,6 +29,33 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + BlockquoteDirective directive = MarkdownRuntimeBlocks.parseBlockquoteDirective(el); + if (directive != null && directive.alertType() != null) { + LytAlertBox alertBox = new LytAlertBox(); + alertBox.setTitle( + directive.alertType().displayText(), + directive.alertType()); + alertBox.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); + alertBox.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); + compileDirectiveBody(compiler, directive, alertBox); + normalizeBlockMargins(alertBox); + parent.append(PageCompiler.wrapFloatAwareIfNeeded(alertBox)); + return; + } + + if (directive != null && (directive.title() != null || directive.icon() != null)) { + LytQuoteBox quoteBox = new LytQuoteBox(); + quoteBox.setQuoteStyle(directive.accentColor(), directive.title(), buildQuoteIcon(directive.icon())); + quoteBox.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); + quoteBox.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); + compileDirectiveBody(compiler, directive, quoteBox); + normalizeBlockMargins(quoteBox); + shiftFirstParagraphDown(quoteBox, 1); + parent.append(PageCompiler.wrapFloatAwareIfNeeded(quoteBox)); + return; + } + + // Plain blockquote LytVBox blockquote = new LytVBox(); blockquote.setBackgroundColor(SymbolicColor.BLOCKQUOTE_BACKGROUND); blockquote.setPadding(5); @@ -28,17 +64,56 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl blockquote.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); blockquote.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); compiler.compileBlockContext(el.children(), blockquote); + normalizeBlockMargins(blockquote); + shiftFirstParagraphDown(blockquote, 1); + parent.append(PageCompiler.wrapFloatAwareIfNeeded(blockquote)); + } + + private void compileDirectiveBody(PageCompiler compiler, BlockquoteDirective directive, + LytBlockContainer parent) { + // When there's a remainingText override and the first paragraph is still present + // at the head of the children list, replace its leading text. + // Otherwise — just compile children normally. + if (!directive.children().isEmpty() && directive.firstParagraph() != null + && directive.children().get(0) == directive.firstParagraph() + && directive.remainingText() != null && !directive.remainingText().isEmpty()) { + // Clone the first paragraph with the remaining text overriding the leading text + compiler.compileBlockContext( + Collections.singletonList(directive.firstParagraph()), parent); + for (int i = 1; i < directive.children().size(); i++) { + compiler.compileBlockContext( + Collections.singletonList(directive.children().get(i)), parent); + } + } else { + compiler.compileBlockContext(directive.children(), parent); + } + } - // Normalize block margins (equivalent to normalizeBlockMargins in PageCompiler) - var children = blockquote.getChildren(); - if (!children.isEmpty()) { - if (children.get(0) instanceof LytParagraph first) { - first.setMarginTop(0); + private void normalizeBlockMargins(LytNode box) { + var boxChildren = box.getChildren(); + if (!boxChildren.isEmpty()) { + if (boxChildren.get(0) instanceof LytParagraph) { + ((LytParagraph) boxChildren.get(0)).setMarginTop(0); } - if (children.get(children.size() - 1) instanceof LytParagraph last) { - last.setMarginBottom(0); + if (boxChildren.get(boxChildren.size() - 1) instanceof LytParagraph) { + ((LytParagraph) boxChildren.get(boxChildren.size() - 1)).setMarginBottom(0); } } - parent.append(blockquote); + } + + private void shiftFirstParagraphDown(LytNode box, int pixels) { + var boxChildren = box.getChildren(); + if (!boxChildren.isEmpty() && boxChildren.get(0) instanceof LytParagraph) { + LytParagraph first = (LytParagraph) boxChildren.get(0); + first.setPaddingTop(first.getPaddingTop() + pixels); + } + } + + @Nullable + private LytFlowContent buildQuoteIcon( + @Nullable QuoteIconSpec icon) { + // The original buildQuoteIcon resolved item stacks from icon specs. + // For now return null — icon rendering will be added in a later phase. + return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java index 02ea12ea..7dacf87b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java @@ -1,12 +1,15 @@ package com.hfstudio.guidenh.guide.compiler.tags; import java.util.Collections; +import java.util.List; import java.util.Set; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytListItem; +import com.hfstudio.guidenh.guide.document.block.LytTaskListItem; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownListSemantics; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class ListItemCompiler extends BlockTagCompiler { @@ -17,17 +20,26 @@ public Set getTagNames() { } @Override + @SuppressWarnings({"unchecked", "rawtypes"}) protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - LytListItem listItem = new LytListItem(); + LytListItem listItem; + var taskMarker = MarkdownListSemantics.extractTaskMarker((List) el.children()); + if (taskMarker != null) { + LytTaskListItem taskItem = new LytTaskListItem(); + taskItem.setChecked(taskMarker.checked()); + listItem = taskItem; + } else { + listItem = new LytListItem(); + } compiler.compileBlockContext(el.children(), listItem); - // Fix up top/bottom margin for list item children + // Normalize first child margins var children = listItem.getChildren(); if (!children.isEmpty()) { var firstChild = children.get(0); - if (firstChild instanceof LytBlock firstBlock) { - firstBlock.setMarginTop(0); - firstBlock.setMarginBottom(0); + if (firstChild instanceof LytBlock) { + ((LytBlock) firstChild).setMarginTop(0); + ((LytBlock) firstChild).setMarginBottom(0); } } parent.append(listItem); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java index 08240e0e..4fda097b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java @@ -22,6 +22,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.FileTreeCompiler; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.guide.compiler.tags.functiongraph.FunctionGraphFenceParser; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; import cpw.mods.fml.common.FMLLog; @@ -67,7 +68,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // Function graph if (isFunctionGraphFence(lang)) { - parent.append(com.hfstudio.guidenh.guide.compiler.tags.functiongraph.FunctionGraphFenceParser.parse(codeText)); + parent.append(FunctionGraphFenceParser.parse(codeText)); return; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index f59d6720..f8cbc444 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -1,5 +1,7 @@ package com.hfstudio.guidenh.guide.internal.editor.autocomplete.resolver; +import java.util.Collections; + import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; @@ -43,7 +45,7 @@ public TextSyntaxContext resolve(String text, int cursorIndex) { root = cachedRoot; } else { root = MdAst.fromMarkdown(text, PARSE_OPTIONS); - MdAstToMdxConverter.convert(root, java.util.Collections.emptyMap()); + MdAstToMdxConverter.convert(root, Collections.emptyMap()); cachedText = text; cachedRoot = root; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index 0d5addf8..940a2bd1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -19,6 +19,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; import com.hfstudio.guidenh.libs.mdast.MdAst; +import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; import com.hfstudio.guidenh.libs.mdast.model.MdAstList; import com.hfstudio.guidenh.libs.mdast.model.MdAstListContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstListItem; @@ -692,7 +693,7 @@ private void applySmartNewline() { // 1. Try AST-based list continuation MdAstRoot root = MdAst.fromMarkdown(text, GuideMarkdownOptions.runtime()); - MdAstToMdxConverter.convert(root, Collections.emptyMap()); + MdAstToMdxConverter.convert(root, Collections.emptyMap()); MdAstListItem item = findEnclosingListItem(root, cursor); if (item != null) { int lineStart = findLineStart(text, cursor - 1); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java index af2c10ff..5497e040 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java @@ -75,14 +75,28 @@ private static void convertParent(MdAstParent parent, Map parent) { - return parent instanceof MdAstParagraph || parent instanceof MdxJsxTextElement - || "link".equals(parent.type()) - || "strong".equals(parent.type()) - || "emphasis".equals(parent.type()) - || "delete".equals(parent.type()) - || "heading".equals(parent.type()); + if (parent instanceof MdAstParagraph || parent instanceof MdxJsxTextElement) { + return true; + } + // New MdxJsxFlowElement containers that hold phrasing/inline children + if (parent instanceof MdxJsxFlowElement el) { + String name = el.name(); + return name != null && PHRASING_CONTAINER_NAMES.contains(name); + } + String type = parent.type(); + return "link".equals(type) + || "strong".equals(type) + || "emphasis".equals(type) + || "delete".equals(type) + || "heading".equals(type); } + private static final java.util.Set PHRASING_CONTAINER_NAMES = + new java.util.HashSet<>(java.util.Arrays.asList( + "p", "h1", "h2", "h3", "h4", "h5", "h6", + "li", "td", "th", "blockquote", "div", "summary", "a", + "strong", "em", "del", "u", "wavy", "dotted", "mark", "code", "span")); + @SuppressWarnings("unchecked") private static List castPhrasingChildren(List children) { return (List) (List) children; diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java index 6de9322c..3bd9b605 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java @@ -505,8 +505,74 @@ private String compileListItemMdx(MdxJsxElementFields el, GuideSiteTemplateRegis private String compileCodeBlockMdx(MdxJsxElementFields el) { String codeText = extractTextFromElement(el); String lang = el.getAttributeString("lang", null); - String langClass = lang != null ? " class=\"language-" + escapeAttribute(lang) + "\"" : ""; - return "
    " + escapeHtml(codeText) + "
    "; + String meta = el.getAttributeString("meta", null); + if (lang != null) { + lang = lang.toLowerCase(Locale.ROOT); + } + + // Sub-language rendering + if ("csv".equals(lang)) { + return GuideSiteGraphRenderer.renderCsvTable(codeText, true); + } + if ("tree".equals(lang) || "filetree".equals(lang)) { + return GuideSiteGraphRenderer.renderFileTree(codeText); + } + if ("mermaid".equals(lang)) { + try { + MermaidMindmapDocument doc = MermaidMindmapParser.parse(codeText); + return GuideSiteGraphRenderer.renderMermaidTree(doc); + } catch (Exception ignored) { + return "
    " + escapeHtml(codeText) + "
    "; + } + } + if ("funcgraph".equals(lang) || "functiongraph".equals(lang)) { + try { + LytFunctionGraph graph = FunctionGraphFenceParser.parse(codeText); + return GuideSiteGraphRenderer.renderFunctionGraph(graph); + } catch (RuntimeException ignored) { + return "
    " + escapeHtml(codeText) + "
    "; + } + } + + // Plain code block with optional size constraints + Integer width = parseMetaInt(meta, "width"); + Integer height = parseMetaInt(meta, "height"); + StringBuilder html = new StringBuilder(); + html.append("").append(escapeHtml(codeText)).append(""); + return html.toString(); + } + + @Nullable + private static Integer parseMetaInt(String meta, String key) { + if (meta == null || meta.isEmpty()) { + return null; + } + Matcher m = Pattern.compile("(?:^|\\s)" + Pattern.quote(key) + "\\s*=\\s*\"?'?([0-9]+)\"?'?") + .matcher(meta); + if (m.find()) { + try { + return Integer.parseInt(m.group(1)); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; } private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, From 389de4ebe8b5662a5ac1a4c4eaebed5da2d01505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 27 May 2026 02:50:46 +0800 Subject: [PATCH 039/136] fix: buildErrorPage uses MdxJsx, convertPhrasingChildren handles mixed content - buildErrorPage() now creates MdxJsxFlowElement instead of MdAstHeading/MdAstParagraph, so error pages go through the tag-dispatch pipeline correctly. - convertPhrasingChildren() now accepts List and handles both inline phrasing and block-level nodes (e.g. MdAstParagraph inside ), fixing ClassCastException that caused pages with table cells containing block content to lose their frontmatter and disappear from the navigation tree. --- .../guidenh/guide/compiler/PageCompiler.java | 20 ++++++++--- .../markdown/MdAstToMdxConverter.java | 36 +++++++++++-------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 1039160b..fafe70e3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -347,17 +347,17 @@ public static MdAstRoot buildErrorPage(String errorText) { public static MdAstRoot buildErrorPage(String headingText, String errorText) { var root = new MdAstRoot(); - var heading = new MdAstHeading(); + var heading = new MdxJsxFlowElement(); + heading.setName("h1"); + heading.addAttribute("depth", 1); root.addChild(heading); - - heading.depth = 1; var headingTextNode = new MdAstText(); headingTextNode.setValue(headingText); heading.addChild(headingTextNode); - var errorParagraph = new MdAstParagraph(); + var errorParagraph = new MdxJsxFlowElement(); + errorParagraph.setName("p"); root.addChild(errorParagraph); - var errorTextNode = new MdAstText(); errorTextNode.setValue(errorText); errorParagraph.addChild(errorTextNode); @@ -632,6 +632,16 @@ public void compileBlockContext(List children, LytBlo } else if (child instanceof MdAstDefinition) { layoutChild = null; // handled via element } else { + // Fallback: unconverted MdAst types that survived the converter. + // Log details to help trace which code path produces them. + if (child instanceof MdAstNode) { + MdAstNode astNode = (MdAstNode) child; + FMLLog.getLogger() + .warn("[GuideNH] [PageCompiler] Unconverted {} (type={}) position={}", + child.getClass().getSimpleName(), + astNode.type(), + astNode.position()); + } layoutChild = createErrorBlock( "Unhandled node in block context: " + child.getClass().getSimpleName(), child); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java index 5497e040..54e0b45b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java @@ -68,7 +68,7 @@ private static void convertParent(MdAstParent parent, Map parent) { || "heading".equals(type); } + // Containers whose children are inline/phrasing content only private static final java.util.Set PHRASING_CONTAINER_NAMES = new java.util.HashSet<>(java.util.Arrays.asList( "p", "h1", "h2", "h3", "h4", "h5", "h6", - "li", "td", "th", "blockquote", "div", "summary", "a", + "td", "th", "summary", "a", "strong", "em", "del", "u", "wavy", "dotted", "mark", "code", "span")); - @SuppressWarnings("unchecked") - private static List castPhrasingChildren(List children) { - return (List) (List) children; - } - @SuppressWarnings("unchecked") private static List castAnyChildren(List children) { return (List) (List) children; } // ----------------------------------------------------------------------- - // Phrasing (inline) children conversion + // Phrasing (inline) children conversion — also handles block nodes that + // may appear inside phrasing containers (e.g. MdAstParagraph inside ). // ----------------------------------------------------------------------- - private static void convertPhrasingChildren(List children, + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void convertPhrasingChildren(List children, Map definitions) { for (int i = 0; i < children.size(); i++) { - MdAstPhrasingContent child = children.get(i); - MdxJsxTextElement replacement = null; + Object child = children.get(i); + Object replacement = null; + // Inline phrasing types → MdxJsxTextElement if (child instanceof MdAstStrong) { replacement = createText("strong", ((MdAstStrong) child).children()); } else if (child instanceof MdAstEmphasis) { @@ -180,13 +179,20 @@ private static void convertPhrasingChildren(List children, } else if (child instanceof MdAstBreak) { replacement = createText("br", new ArrayList<>()); } + // Flow block types that can appear inside phrasing containers + else if (child instanceof MdAstParagraph p) { + replacement = createFlow("p", p.children()); + } if (replacement != null) { - children.set(i, replacement); - convertParent(replacement, definitions); + ((List) children).set(i, replacement); + if (replacement instanceof MdxJsxTextElement) { + convertParent((MdxJsxTextElement) replacement, definitions); + } else if (replacement instanceof MdxJsxFlowElement) { + convertParent((MdxJsxFlowElement) replacement, definitions); + } } - // MdAstText, MdxJsxTextElement, MdxJsxFlowElement, MdxJsxAttribute, - // MdxJsxExpressionAttribute are silently passed through. + // MdAstText, MdxJsxTextElement, MdxJsxFlowElement: silently pass through } } From 7c289667ad397aead4b7c69a5c4e40ea7bfcb332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 27 May 2026 03:03:01 +0800 Subject: [PATCH 040/136] fix: restore paragraph merging in compileBlockContext for inline content Table cells with alternating MdAstText and elements (e.g. `billboard`, `smoke`, `largesmoke`) were getting each text fragment wrapped in a separate LytParagraph because the new compileBlockContext lacked the previousLayoutChild optimization. Restored it: adjacent inline elements now merge into one paragraph. Added known-issues memo to phase2 spec for the proper fix (TableCompiler should use compileFlowContext directly). --- .../guidenh/guide/compiler/PageCompiler.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index fafe70e3..d28123d7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -597,6 +597,7 @@ public void withSourceContext(String sourceText, Runnable action) { } public void compileBlockContext(List children, LytBlockContainer layoutParent) { + LytBlock previousLayoutChild = null; for (int i = 0; i < children.size(); i++) { var child = children.get(i); LytBlock layoutChild = null; @@ -616,7 +617,14 @@ public void compileBlockContext(List children, LytBlo } } } else if (child instanceof MdxJsxTextElement el) { - // Inline element at block level — wrap in a paragraph + // Inline element at block level — merge into previous paragraph when possible + if (previousLayoutChild instanceof LytParagraph paragraph) { + var flowCompiler = tagCompilers.get(el.name()); + if (flowCompiler != null) { + flowCompiler.compileFlowContext(this, paragraph, el); + } + continue; + } var paragraph = new LytParagraph(); var flowCompiler = tagCompilers.get(el.name()); if (flowCompiler != null) { @@ -624,6 +632,13 @@ public void compileBlockContext(List children, LytBlo } layoutChild = paragraph; } else if (child instanceof MdAstText text) { + // Orphan text — merge into previous paragraph when possible + if (previousLayoutChild instanceof LytParagraph paragraph) { + var flowText = new LytFlowText(); + flowText.setText(text.value); + paragraph.append(flowText); + continue; + } var paragraph = new LytParagraph(); var flowText = new LytFlowText(); flowText.setText(text.value); @@ -632,16 +647,6 @@ public void compileBlockContext(List children, LytBlo } else if (child instanceof MdAstDefinition) { layoutChild = null; // handled via element } else { - // Fallback: unconverted MdAst types that survived the converter. - // Log details to help trace which code path produces them. - if (child instanceof MdAstNode) { - MdAstNode astNode = (MdAstNode) child; - FMLLog.getLogger() - .warn("[GuideNH] [PageCompiler] Unconverted {} (type={}) position={}", - child.getClass().getSimpleName(), - astNode.type(), - astNode.position()); - } layoutChild = createErrorBlock( "Unhandled node in block context: " + child.getClass().getSimpleName(), child); } @@ -653,6 +658,7 @@ public void compileBlockContext(List children, LytBlo } layoutParent.append(layoutChild); } + previousLayoutChild = layoutChild; } } From a6ac45625a7179e99be299ef1453c5db0f6418b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 27 May 2026 03:21:30 +0800 Subject: [PATCH 041/136] fix: kramdown table widths, NFE protection, dead code removal prep - MdAstToMdxConverter: convert kramdown {: widths=... } lines to - TableCompiler: consume elements, apply preferred column widths - HeadingCompiler: depth default 1, parseIntSafe with NFE protection, clamp 1-6 - ListCompiler: parseIntSafe with NFE protection for start attribute --- .../guide/compiler/tags/HeadingCompiler.java | 12 ++++++++-- .../guide/compiler/tags/ListCompiler.java | 10 ++++++++- .../guide/compiler/tags/TableCompiler.java | 22 +++++++++++++++++++ .../markdown/MdAstToMdxConverter.java | 22 ++++++++++++++++++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java index e7de3772..d49ab224 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java @@ -23,9 +23,17 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { LytHeading heading = new LytHeading(); - int depth = Integer.parseInt(el.getAttributeString("depth", "0")); - heading.setDepth(depth); + int depth = parseIntSafe(el.getAttributeString("depth", "1"), 1); + heading.setDepth(Math.max(1, Math.min(depth, 6))); compiler.compileFlowContext(el.children(), heading); parent.append(heading); } + + private static int parseIntSafe(String s, int fallback) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return fallback; + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java index a75b5df2..1bef98f8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java @@ -23,11 +23,19 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { boolean ordered = "ol".equals(el.name()); - int start = Integer.parseInt(el.getAttributeString("start", "1")); + int start = parseIntSafe(el.getAttributeString("start", "1"), 1); LytList list = new LytList(ordered, start); for (var child : el.children()) { compiler.compileBlockContext(Collections.singletonList(child), list); } parent.append(list); } + + private static int parseIntSafe(String s, int fallback) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return fallback; + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java index 70a4e3a8..fadb99f4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java @@ -30,6 +30,19 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl boolean firstRow = true; int rowIndex = 0; for (var child : el.children()) { + // Skip kramdown {: widths=... } meta lines + if (child instanceof MdxJsxFlowElement meta && "table-meta".equals(meta.name())) { + String content = meta.getAttributeString("content", ""); + if (!content.isEmpty()) { + java.util.List widths = com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler + .parseWidthHints(extractKramdownExpression(content)); + var columns = table.getColumns(); + for (int wi = 0; wi < widths.size() && wi < columns.size(); wi++) { + columns.get(wi).setPreferredWidth(widths.get(wi)); + } + } + continue; + } if (child instanceof MdxJsxFlowElement tr && "tr".equals(tr.name())) { LytTableRow row = table.appendRow(); if (firstRow) { @@ -62,4 +75,13 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } parent.append(table); } + + private static String extractKramdownExpression(String content) { + int start = content.indexOf('{'); + int end = content.lastIndexOf('}'); + if (start >= 0 && end > start) { + return content.substring(start + 1, end).trim(); + } + return ""; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java index 54e0b45b..0209284c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java @@ -207,7 +207,14 @@ private static void convertFlowChildren(List children, MdxJsxFlowElement replacement = null; if (child instanceof MdAstParagraph p) { - replacement = createFlow("p", p.children()); + String kramdownMeta = extractKramdownMeta(p); + if (kramdownMeta != null) { + replacement = new MdxJsxFlowElement(); + replacement.setName("table-meta"); + replacement.addAttribute("content", kramdownMeta); + } else { + replacement = createFlow("p", p.children()); + } } else if (child instanceof MdAstHeading h) { MdxJsxFlowElement el = createFlow("h" + h.depth, h.children()); el.addAttribute("depth", h.depth); @@ -365,4 +372,17 @@ private static String serializeAlign(@Nullable List aligns) { } return sb.toString(); } + + /** + * Extracts the expression from a kramdown-style attribute paragraph + * ({@code {: ...}}), or returns null if not a kramdown meta line. + */ + @Nullable + private static String extractKramdownMeta(MdAstParagraph p) { + if (p.children().size() != 1) return null; + if (!(p.children().get(0) instanceof MdAstText t)) return null; + String v = t.value.trim(); + if (v.startsWith("{:") && v.endsWith("}")) return v; + return null; + } } From 475353f494ed58af5f19318c26d56349890f5352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 01:09:57 +0800 Subject: [PATCH 042/136] =?UTF-8?q?refactor:=20Phase=203=20=E2=80=94=20com?= =?UTF-8?q?piler=20purification,=20LytHost=20runtime,=20dead=20code=20remo?= =?UTF-8?q?val?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add id/nodeUid/styleClass fields and onAttach/onDetach lifecycle to LytNode - Add isLive/setLive with recursive cascade to LytDocument - Introduce LytScript, ScriptContext, ScriptType interfaces - Add script registry, two-level cache, MOUNT dispatch to LytHost - Migrate 18 impure compilers to pure TagCompiler + LytScript stubs - Delete GuideWarmupPump, GuideWarmupScheduler, WarmupWorkItem, GuideDevWatcherPump - Remove 19 warmup methods from MutableGuide, warmup page cache - Remove 12 dead MdAst compile methods from PageCompiler - Wire LytHostPreheatItem to MasterScheduler with MEDIUM priority --- .../com/hfstudio/guidenh/ClientProxy.java | 46 +- .../guidenh/guide/compiler/PageCompiler.java | 588 +--------------- .../compiler/tags/BlockImageCompiler.java | 206 +----- .../compiler/tags/CommandLinkCompiler.java | 19 +- .../guide/compiler/tags/CsvTableCompiler.java | 59 +- .../compiler/tags/FloatingImageCompiler.java | 45 +- .../guide/compiler/tags/ImageCompiler.java | 26 +- .../guide/compiler/tags/ItemGridCompiler.java | 25 +- .../compiler/tags/ItemImageCompiler.java | 69 +- .../guide/compiler/tags/ItemLinkCompiler.java | 100 +-- .../compiler/tags/KeyBindTagCompiler.java | 12 +- .../guide/compiler/tags/MermaidCompiler.java | 246 ++----- .../compiler/tags/PlayerNameTagCompiler.java | 8 +- .../guide/compiler/tags/RecipeCompiler.java | 478 ++++--------- .../compiler/tags/SoundLinkCompiler.java | 4 +- .../compiler/tags/StructureViewCompiler.java | 79 +-- .../guide/compiler/tags/SubPagesCompiler.java | 70 +- .../tags/mediawiki/CategoryCompiler.java | 44 +- .../tags/mediawiki/SpecialCompiler.java | 70 +- .../guide/document/block/LytDocument.java | 25 + .../guide/document/block/LytImageBlock.java | 87 +++ .../guidenh/guide/document/block/LytNode.java | 31 +- .../guide/document/flow/LytFlowContent.java | 19 + .../guide/internal/GuideDevWatcherPump.java | 65 -- .../GuideLightweightReloadService.java | 11 +- .../guide/internal/GuideWarmupPump.java | 40 -- .../guide/internal/GuideWarmupScheduler.java | 88 --- .../guide/internal/GuideWarmupWorkItem.java | 52 -- .../guidenh/guide/internal/MutableGuide.java | 467 +------------ .../guide/internal/host/EventType.java | 3 +- .../guidenh/guide/internal/host/LytHost.java | 101 ++- .../guide/internal/host/LytScript.java | 9 + .../guide/internal/host/ScriptContext.java | 12 + .../internal/host/ScriptContextImpl.java | 38 + .../guide/internal/host/ScriptType.java | 7 + .../host/scripts/BlockImageScript.java | 25 + .../internal/host/scripts/CategoryScript.java | 25 + .../host/scripts/CommandLinkScript.java | 25 + .../internal/host/scripts/CsvTableScript.java | 20 + .../host/scripts/FloatingImageScript.java | 33 + .../internal/host/scripts/ImageScript.java | 33 + .../internal/host/scripts/ItemGridScript.java | 25 + .../host/scripts/ItemImageScript.java | 25 + .../internal/host/scripts/ItemLinkScript.java | 25 + .../internal/host/scripts/KeyBindScript.java | 25 + .../internal/host/scripts/MermaidScript.java | 20 + .../host/scripts/PlayerNameScript.java | 25 + .../internal/host/scripts/RecipeScript.java | 25 + .../internal/host/scripts/SceneScript.java | 25 + .../host/scripts/SoundLinkScript.java | 25 + .../internal/host/scripts/SpecialScript.java | 25 + .../host/scripts/StructureScript.java | 25 + .../internal/host/scripts/SubPagesScript.java | 25 + .../internal/scheduler/DevWatchWorkItem.java | 17 +- .../scheduler/LytHostPreheatItem.java | 32 + .../internal/scheduler/WarmupWorkItem.java | 49 -- .../guidenh/guide/scene/SceneTagCompiler.java | 660 +++--------------- .../site/GuideSiteMdxTagRenderer.java | 21 +- 58 files changed, 1488 insertions(+), 2996 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevWatcherPump.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupPump.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupScheduler.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupWorkItem.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptType.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java delete mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index bcdd43dd..317ea6d4 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -63,27 +63,35 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.network.FMLNetworkEvent; import com.hfstudio.guidenh.guide.internal.scheduler.MasterScheduler; -import com.hfstudio.guidenh.guide.internal.scheduler.WarmupWorkItem; import com.hfstudio.guidenh.guide.internal.scheduler.SearchIndexWorkItem; import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.LytHostWorkItem; +import com.hfstudio.guidenh.guide.internal.host.scripts.CategoryScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.CommandLinkScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.FloatingImageScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.ImageScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.ItemGridScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.ItemImageScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.ItemLinkScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.KeyBindScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.PlayerNameScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.RecipeScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.SoundLinkScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.SpecialScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.StructureScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.SubPagesScript; import cpw.mods.fml.relauncher.Side; public class ClientProxy extends CommonProxy { private static final LytHost lytHost = new LytHost(); - private static final WarmupWorkItem warmupWorkItem = new WarmupWorkItem(); public static LytHost getLytHost() { return lytHost; } - public static WarmupWorkItem getWarmupWorkItem() { - return warmupWorkItem; - } - @Override public void preInit(FMLPreInitializationEvent event) { super.preInit(event); @@ -144,8 +152,29 @@ public void init(FMLInitializationEvent event) { MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); MasterScheduler.init(); MasterScheduler.getInstance().submit(new LytHostWorkItem(lytHost)); - MasterScheduler.getInstance().submit(warmupWorkItem); MasterScheduler.getInstance().submit(new SearchIndexWorkItem()); + + // Phase 3: LytScript registrations + lytHost.registerScript("CommandLink", new CommandLinkScript()); + lytHost.registerScript("Img", new ImageScript()); + lytHost.registerScript("FloatingImage", new FloatingImageScript()); + lytHost.registerScript("PlayerName", new PlayerNameScript()); + lytHost.registerScript("KeyBind", new KeyBindScript()); + lytHost.registerScript("SoundLink", new SoundLinkScript()); + lytHost.registerScript("Structure", new StructureScript()); + lytHost.registerScript("SubPages", new SubPagesScript()); + lytHost.registerScript("ItemGrid", new ItemGridScript()); + lytHost.registerScript("ItemImage", new ItemImageScript()); + lytHost.registerScript("ItemLink", new ItemLinkScript()); + lytHost.registerScript("Category", new CategoryScript()); + lytHost.registerScript("Special", new SpecialScript()); + // Phase 3: RecipeScript handles Recipe, RecipeFor, RecipeUsage, RecipesFor + RecipeScript recipeScript = new RecipeScript(); + lytHost.registerScript("Recipe", recipeScript); + lytHost.registerScript("RecipeFor", recipeScript); + lytHost.registerScript("RecipeUsage", recipeScript); + lytHost.registerScript("RecipesFor", recipeScript); + MinecraftForge.EVENT_BUS.register(this); } @@ -166,8 +195,5 @@ public void completeInit(FMLLoadCompleteEvent event) { public void onClientDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) { GuideME.closeSearch(); lytHost.getNavigation().clear(); - for (var guide : GuideRegistry.getAll()) { - guide.resetWarmup(); - } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index d28123d7..3090e17c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; @@ -17,78 +16,48 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.GuidePage; -import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.PageCollection; -import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.color.SymbolicColor; -import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; -import com.hfstudio.guidenh.guide.compiler.tags.functiongraph.FunctionGraphFenceParser; -import com.hfstudio.guidenh.guide.document.LytErrorSink; import com.hfstudio.guidenh.guide.document.block.LatexRenderOptions; import com.hfstudio.guidenh.guide.document.block.LatexVerticalAlign; -import com.hfstudio.guidenh.guide.document.block.LytAlertBox; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.document.block.LytCodeBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytDocumentFloat; import com.hfstudio.guidenh.guide.document.block.LytFloatAwareBlock; import com.hfstudio.guidenh.guide.document.block.LytHeading; -import com.hfstudio.guidenh.guide.document.block.LytImage; -import com.hfstudio.guidenh.guide.document.block.LytItemImage; import com.hfstudio.guidenh.guide.document.block.LytLatexBlock; import com.hfstudio.guidenh.guide.document.block.LytLatexDisplayBlock; -import com.hfstudio.guidenh.guide.document.block.LytList; -import com.hfstudio.guidenh.guide.document.block.LytListItem; -import com.hfstudio.guidenh.guide.document.block.LytMermaidMindmap; -import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; -import com.hfstudio.guidenh.guide.document.block.LytTaskListItem; import com.hfstudio.guidenh.guide.document.block.LytThematicBreak; -import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.document.block.table.LytTable; -import com.hfstudio.guidenh.guide.document.flow.LytFlowBreak; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.flow.LytFlowText; -import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; import com.hfstudio.guidenh.guide.extensions.Extension; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.extensions.ExtensionPoint; import com.hfstudio.guidenh.guide.indices.PageIndex; import com.hfstudio.guidenh.guide.internal.GuideRegistry; -import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; -import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguage; -import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguageDetector; -import com.hfstudio.guidenh.guide.internal.markdown.FileTreeCompiler; import com.hfstudio.guidenh.guide.internal.markdown.FootnotePreprocessor; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownActionLink; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownHtmlRuntimeNormalizer; import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLatexShorthand; -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownListSemantics; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLiteralAutolink; -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks; -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.BlockquoteDirective; -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconKind; -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; -import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.internal.util.LangUtil; -import com.hfstudio.guidenh.guide.render.GuidePageTexture; +import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.sound.GuideSoundParsers; -import com.hfstudio.guidenh.guide.style.BorderStyle; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; import com.hfstudio.guidenh.libs.mdast.MdAst; @@ -96,29 +65,14 @@ import com.hfstudio.guidenh.libs.mdast.MdastOptions; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTable; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableRow; -import com.hfstudio.guidenh.libs.mdast.gfmstrikethrough.MdAstDelete; -import com.hfstudio.guidenh.libs.mdast.guidemark.MdAstMark; -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstDottedUnderline; -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstUnderline; -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstWavyUnderline; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstBlockquote; import com.hfstudio.guidenh.libs.mdast.model.MdAstBreak; -import com.hfstudio.guidenh.libs.mdast.model.MdAstCode; import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis; -import com.hfstudio.guidenh.libs.mdast.model.MdAstHTML; -import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading; -import com.hfstudio.guidenh.libs.mdast.model.MdAstImage; -import com.hfstudio.guidenh.libs.mdast.model.MdAstImageReference; import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode; -import com.hfstudio.guidenh.libs.mdast.model.MdAstLink; -import com.hfstudio.guidenh.libs.mdast.model.MdAstLinkReference; -import com.hfstudio.guidenh.libs.mdast.model.MdAstList; -import com.hfstudio.guidenh.libs.mdast.model.MdAstListItem; import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph; import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; @@ -143,11 +97,8 @@ public class PageCompiler { */ public static final int DEFAULT_ELEMENT_SPACING = 5; public static final MdastOptions PARSE_OPTIONS = GuideMarkdownOptions.runtime(); - private static final Pattern CODEBLOCK_META_WIDTH = Pattern.compile("(^|\\s)width=(\"([^\"]+)\"|'([^']+)'|(\\S+))"); public static final int DEFAULT_MARK_BACKGROUND_COLOR = 0xFF8A6A00; private static final Pattern TABLE_ATTRIBUTE_LINE = Pattern.compile("^\\{:\\s*(.+?)\\s*}$"); - private static final Pattern CODEBLOCK_META_HEIGHT = Pattern - .compile("(^|\\s)height=(\"([^\"]+)\"|'([^']+)'|(\\S+))"); private static PageLinkResolver pageLinkResolver = PageCompiler::defaultPageExistsForLink; private static final State> SOURCE_SLICE_STACK = new State<>( "source_slice_stack", @@ -662,159 +613,6 @@ public void compileBlockContext(List children, LytBlo } } - private LytList compileList(MdAstList astList) { - var list = new LytList(astList.ordered, astList.start); - for (var listContent : astList.children()) { - if (listContent instanceof MdAstListItem astListItem) { - var taskMarker = MarkdownListSemantics.extractTaskMarker(astListItem.children()); - LytListItem listItem = taskMarker != null ? new LytTaskListItem() : new LytListItem(); - if (listItem instanceof LytTaskListItem taskListItem) { - taskListItem.setChecked(taskMarker.checked()); - } - compileListItem(astListItem, listItem, taskMarker); - - // Fix up top/bottom margin for list item children - var children = listItem.getChildren(); - if (!children.isEmpty()) { - var firstChild = children.get(0); - if (firstChild instanceof LytBlock firstBlock) { - firstBlock.setMarginTop(0); - firstBlock.setMarginBottom(0); - } - } - list.append(listItem); - } else { - list.append(createErrorBlock("Cannot handle list content", listContent)); - } - } - return list; - } - - @Deprecated - private LytBlock compileBlockquote(MdAstBlockquote astBlockquote) { - // Dead code path — BlockquoteCompiler handles
    now - var blockquote = new LytVBox(); - blockquote.setBackgroundColor(SymbolicColor.BLOCKQUOTE_BACKGROUND); - blockquote.setPadding(5); - blockquote.setPaddingLeft(10); - blockquote.setBorderLeft(new BorderStyle(SymbolicColor.TABLE_BORDER, 2)); - blockquote.setMarginTop(DEFAULT_ELEMENT_SPACING); - blockquote.setMarginBottom(DEFAULT_ELEMENT_SPACING); - compileBlockContext(astBlockquote, blockquote); - return wrapFloatAwareIfNeeded(blockquote); - } - - private void normalizeBlockMargins(LytNode box) { - var boxChildren = box.getChildren(); - if (!boxChildren.isEmpty()) { - if (boxChildren.get(0) instanceof LytParagraph firstParagraph) { - firstParagraph.setMarginTop(0); - } - if (boxChildren.get(boxChildren.size() - 1) instanceof LytParagraph lastParagraph) { - lastParagraph.setMarginBottom(0); - } - } - } - - private void shiftFirstParagraphDown(LytNode box, int pixels) { - var boxChildren = box.getChildren(); - if (!boxChildren.isEmpty() && boxChildren.get(0) instanceof LytParagraph firstParagraph) { - firstParagraph.setPaddingTop(firstParagraph.getPaddingTop() + pixels); - } - } - - private void compileListItem(MdAstListItem astListItem, LytListItem listItem, - @Nullable MarkdownListSemantics.TaskMarker taskMarker) { - if (taskMarker == null || astListItem.children() - .isEmpty() - || !(astListItem.children() - .get(0) instanceof MdAstParagraph paragraph)) { - compileBlockContext(astListItem, listItem); - return; - } - - var paragraphCopy = cloneParagraphWithLeadingTextOverride(paragraph, taskMarker.remainingText()); - compileParagraphBlock(paragraphCopy, listItem); - for (int i = 1; i < astListItem.children() - .size(); i++) { - var child = astListItem.children() - .get(i); - compileBlockContext(Collections.singletonList(child), listItem); - } - } - - private MdAstParagraph cloneParagraphWithLeadingTextOverride(MdAstParagraph original, String leadingText) { - MdAstParagraph copy = new MdAstParagraph(); - boolean replaced = false; - for (var child : original.children()) { - if (!replaced && child instanceof MdAstText) { - if (leadingText != null && !leadingText.isEmpty()) { - MdAstText text = new MdAstText(); - text.setValue(leadingText); - copy.addChild(text); - } - replaced = true; - continue; - } - if (child instanceof MdAstNode astNode) { - copy.addChild(astNode); - } - } - return copy; - } - - private @Nullable LytFlowContent buildQuoteIcon(@Nullable QuoteIconSpec icon) { - if (icon == null || icon.value() == null - || icon.value() - .trim() - .isEmpty()) { - return null; - } - - if (icon.kind() == QuoteIconKind.ITEM) { - ItemStack stack = IdUtils.resolveItemStack( - icon.value() - .trim(), - pageId.getResourceDomain()); - if (stack == null) { - return null; - } - var itemImage = new LytItemImage(stack); - itemImage.setInline(true); - itemImage.setTooltipSuppressed(true); - itemImage.setInlineYOffsetOverride(-1); - var inlineBlock = LytFlowInlineBlock.of(itemImage); - return inlineBlock; - } - - if (icon.kind() == QuoteIconKind.PNG) { - try { - ResourceLocation imageId = IdUtils.resolveLink( - icon.value() - .trim(), - pageId); - byte[] imageData = loadAsset(imageId); - if (imageData == null) { - return null; - } - var image = new LytImage(); - image.setTexture(imageId, GuidePageTexture.load(imageId, imageData)); - image.setExplicitWidth(16); - image.setExplicitHeight(16); - return LytFlowInlineBlock.of(image); - } catch (IllegalArgumentException ignored) { - return null; - } - } - - LytFlowSpan span = new LytFlowSpan(); - span.append( - LytFlowText.of( - icon.value() - .trim())); - return span; - } - private void compileParagraphBlock(MdAstParagraph astParagraph, LytBlockContainer parent) { var children = astParagraph.children(); if (children.size() == 1 && children.get(0) instanceof MdAstText soleText) { @@ -1052,257 +850,6 @@ private boolean compileInlineDollarLatex(LytFlowParent layoutParent, String text return foundFormula; } - private LytFlowContent compileLink(MdAstLink astLink, LytErrorSink errorSink) { - var link = new LytFlowLink(); - var sound = GuideSoundParsers.parseActionUri(this, astLink.url); - if (sound != null) { - link.setClickSoundSpec(sound); - link.setClickCallback(uiHost -> {}); - compileFlowContext(astLink, link); - return link; - } - if (astLink.title != null && !astLink.title.isEmpty()) { - link.setTooltip(new TextTooltip(astLink.title)); - } - if (astLink.url != null && !astLink.url.isEmpty()) { - LinkParser.parseLink(this, astLink.url, new LinkParser.Visitor() { - - @Override - public void handlePage(ResourceLocation guideId, PageAnchor page) { - link.setGuideLink(guideId, page); - } - - @Override - public void handleExternal(URI uri) { - link.setExternalUrl(uri); - } - - @Override - public void handleError(String error) { - errorSink.appendError(PageCompiler.this, error, astLink); - } - }); - } - - compileFlowContext(astLink, link); - return link; - } - - private LytFlowContent compileLinkReference(MdAstLinkReference astLinkReference, LytErrorSink errorSink) { - MdAstDefinition definition = GuideMarkdownDefinitions.find(definitions, astLinkReference.identifier); - if (definition == null) { - return createErrorFlowContent("Missing link reference definition", astLinkReference); - } - - MdAstLink link = new MdAstLink(); - link.url = definition.url; - link.title = definition.title; - for (var child : astLinkReference.children()) { - if (child instanceof MdAstNode astChild) { - link.addChild(astChild); - } - } - return compileLink(link, errorSink); - } - - private LytImage compileImageReference(MdAstImageReference astImageReference) { - MdAstDefinition definition = GuideMarkdownDefinitions.find(definitions, astImageReference.identifier); - if (definition == null) { - LytImage image = new LytImage(); - image.setAlt(astImageReference.alt); - image.setTitle("Missing image reference: " + astImageReference.identifier); - return image; - } - - MdAstImage image = new MdAstImage(); - image.setAlt(astImageReference.alt); - image.setTitle(definition.title); - image.setUrl(definition.url); - return compileImage(image); - } - - private LytBlock compileCodeBlock(MdAstCode astCode) { - CodeBlockLanguage language = CodeBlockLanguageDetector.detect(astCode.lang, astCode.value); - if (shouldRenderCsvTable(astCode, language)) { - return compileCsvCodeBlock(astCode); - } - if (isFileTreeFence(astCode.lang)) { - return FileTreeCompiler.compile(this, astCode.value); - } - if (isFunctionGraphFence(astCode.lang)) { - return FunctionGraphFenceParser.parse(astCode.value); - } - if ("mermaid".equals(language.id())) { - LytMermaidMindmap mermaidBlock = tryCompileMermaidMindmap(astCode.value); - if (mermaidBlock != null) { - return mermaidBlock; - } - } - - LytCodeBlock codeBlock = new LytCodeBlock(); - codeBlock.setLanguageFenceName(astCode.lang != null ? astCode.lang : language.id()); - codeBlock.applyLanguage(language); - codeBlock.setCodeText(astCode.value); - Integer preferredWidth = parseCodeBlockWidth(astCode.meta); - if (preferredWidth != null) { - codeBlock.setPreferredBodyWidth(preferredWidth); - } - Integer forcedHeight = parseCodeBlockHeight(astCode.meta); - if (forcedHeight != null) { - codeBlock.setForcedBodyHeight(forcedHeight); - } - return codeBlock; - } - - private boolean shouldRenderCsvTable(MdAstCode astCode, CodeBlockLanguage language) { - return astCode.lang != null && "csv".equals(language.id()); - } - - private static boolean isFileTreeFence(@Nullable String fenceLanguage) { - if (fenceLanguage == null) { - return false; - } - String trimmed = fenceLanguage.trim(); - return "tree".equalsIgnoreCase(trimmed) || "filetree".equalsIgnoreCase(trimmed); - } - - private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { - if (fenceLanguage == null) { - return false; - } - String trimmed = fenceLanguage.trim(); - return "funcgraph".equalsIgnoreCase(trimmed) || "function".equalsIgnoreCase(trimmed) - || "functiongraph".equalsIgnoreCase(trimmed); - } - - private LytBlock compileCsvCodeBlock(MdAstCode astCode) { - String source = astCode.value; - List> rows = CsvTableParser.parse(source); - if (rows.isEmpty()) { - LytCodeBlock codeBlock = new LytCodeBlock(); - codeBlock.setLanguageFenceName("csv"); - codeBlock.applyLanguage(new CodeBlockLanguage("csv", "CSV")); - codeBlock.setCodeText(source); - return codeBlock; - } - - CsvFenceMeta meta = parseCsvFenceMeta(astCode.meta); - return CsvTableCompiler.buildTable(rows, meta.header(), meta.widthHints()); - } - - private CsvFenceMeta parseCsvFenceMeta(@Nullable String meta) { - if (meta == null || meta.trim() - .isEmpty()) { - return new CsvFenceMeta(true, Collections.emptyList()); - } - - boolean header = true; - List widthHints = Collections.emptyList(); - for (String token : splitMetaTokens(meta)) { - int equalsIndex = token.indexOf('='); - if (equalsIndex <= 0 || equalsIndex == token.length() - 1) { - continue; - } - - String key = token.substring(0, equalsIndex); - String value = stripOptionalQuotes(token.substring(equalsIndex + 1)); - if ("widths".equals(key)) { - widthHints = CsvTableCompiler.parseWidthHints(value); - } else if ("header".equals(key)) { - header = !"false".equalsIgnoreCase(value); - } - } - - return new CsvFenceMeta(header, widthHints); - } - - private MarkdownTableMeta extractMarkdownTableMeta(List children, int startIndex) { - if (startIndex >= children.size()) { - return extractMarkdownTableMetaFromSource(children, startIndex); - } - - StringBuilder metaExpression = new StringBuilder(); - int consumed = 0; - for (int index = startIndex; index < children.size(); index++) { - MdAstAnyContent child = children.get(index); - if (!(child instanceof MdAstParagraph paragraph)) { - break; - } - - String attributeText = getParagraphTextValue(paragraph); - if (attributeText == null) { - break; - } - - Matcher matcher = TABLE_ATTRIBUTE_LINE.matcher(attributeText.trim()); - if (!matcher.matches()) { - break; - } - - if (metaExpression.length() > 0) { - metaExpression.append(' '); - } - metaExpression.append(matcher.group(1)); - consumed++; - } - - if (consumed == 0) { - return extractMarkdownTableMetaFromSource(children, startIndex); - } - - List widthHints = parseWidthHintsFromMetaExpression(metaExpression.toString()); - if (widthHints.isEmpty()) { - return new MarkdownTableMeta(Collections.emptyList(), consumed); - } - - return new MarkdownTableMeta(widthHints, consumed); - } - - private MarkdownTableMeta extractMarkdownTableMetaFromSource(List children, - int startIndex) { - if (startIndex <= 0 || startIndex > children.size()) { - return new MarkdownTableMeta(Collections.emptyList(), 0); - } - - MdAstAnyContent tableChild = children.get(startIndex - 1); - if (!(tableChild instanceof MdAstNode tableNode) || tableNode.position() == null - || tableNode.position() - .end() == null) { - return new MarkdownTableMeta(Collections.emptyList(), 0); - } - - int endLine = tableNode.position() - .end() - .line(); - String sourceText = getCurrentSourceText(); - if (endLine <= 0) { - return new MarkdownTableMeta(Collections.emptyList(), 0); - } - - MarkdownTableMeta[] found = new MarkdownTableMeta[1]; - GuideStringLines.visitLines(sourceText, (line, lineIndex) -> { - if (lineIndex + 1 < endLine) { - return true; - } - String attributeLine = line.trim(); - if (attributeLine.isEmpty()) { - return true; - } - Matcher matcher = TABLE_ATTRIBUTE_LINE.matcher(attributeLine); - if (matcher.matches()) { - List widthHints = parseWidthHintsFromMetaExpression(matcher.group(1)); - found[0] = new MarkdownTableMeta(widthHints, 0); - } - return false; - }); - return found[0] != null ? found[0] : new MarkdownTableMeta(Collections.emptyList(), 0); - } - - private @Nullable String getParagraphTextValue(MdAstParagraph paragraph) { - String text = paragraph.toText(); - return text.isEmpty() ? null : text; - } - private List parseWidthHintsFromMetaExpression(String metaExpression) { for (String token : splitMetaTokens(metaExpression)) { int equalsIndex = token.indexOf('='); @@ -1363,50 +910,6 @@ private String stripOptionalQuotes(String value) { return value; } - private @Nullable Integer parseCodeBlockHeight(@Nullable String meta) { - if (meta == null || meta.trim() - .isEmpty()) { - return null; - } - Matcher matcher = CODEBLOCK_META_HEIGHT.matcher(meta); - if (!matcher.find()) { - return null; - } - String value = matcher.group(3) != null ? matcher.group(3) - : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); - if (value == null || value.trim() - .isEmpty()) { - return null; - } - try { - return Math.max(0, Integer.parseInt(value.trim())); - } catch (NumberFormatException ignored) { - return null; - } - } - - private @Nullable Integer parseCodeBlockWidth(@Nullable String meta) { - if (meta == null || meta.trim() - .isEmpty()) { - return null; - } - Matcher matcher = CODEBLOCK_META_WIDTH.matcher(meta); - if (!matcher.find()) { - return null; - } - String value = matcher.group(3) != null ? matcher.group(3) - : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); - if (value == null || value.trim() - .isEmpty()) { - return null; - } - try { - return Math.max(0, Integer.parseInt(value.trim())); - } catch (NumberFormatException ignored) { - return null; - } - } - private @Nullable BlockTagChildSource extractBlockTagChildrenSource(MdxJsxElementFields element) { String sourceText = getCurrentSourceText(); String body = MdxBlockTagSourceExtractor.extractRawBody(element, sourceText); @@ -1515,89 +1018,6 @@ private String removeLeadingWhitespace(String line, int widthToRemove) { return line.substring(index); } - private @Nullable LytMermaidMindmap tryCompileMermaidMindmap(String source) { - try { - String normalized = MermaidMindmapParser.normalize(source); - LytMermaidMindmap block = new LytMermaidMindmap(MermaidMindmapParser.parse(normalized), normalized); - FMLLog.getLogger() - .info( - "[GuideNH] [PageCompiler] Compiled fenced Mermaid runtime block for page {} ({} chars)", - pageId, - normalized.length()); - return block; - } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [PageCompiler] Failed to compile fenced Mermaid runtime block for page {} from source: {}", - pageId, - source, - e); - return null; - } - } - - private void compileHtmlLiteral(LytParagraph paragraph, String html) { - String stripped = stripHtmlTags(html); - if (stripped.isEmpty()) { - paragraph.appendText(html); - } else { - paragraph.appendText(stripped); - } - paragraph.setMarginTop(DEFAULT_ELEMENT_SPACING); - paragraph.setMarginBottom(DEFAULT_ELEMENT_SPACING); - } - - private LytFlowContent compileHtmlInline(String html) { - LytFlowText text = new LytFlowText(); - text.setText(stripHtmlTags(html)); - return text; - } - - private String stripHtmlTags(String html) { - if (html == null || html.isEmpty()) { - return ""; - } - - StringBuilder stripped = new StringBuilder(html.length()); - boolean inTag = false; - for (int i = 0; i < html.length(); i++) { - char current = html.charAt(i); - if (current == '<') { - inTag = true; - continue; - } - if (current == '>') { - inTag = false; - continue; - } - if (!inTag) { - stripped.append(current); - } - } - return stripped.toString(); - } - - private LytImage compileImage(MdAstImage astImage) { - var image = new LytImage(); - image.setTitle(astImage.title); - image.setAlt(astImage.alt); - try { - var imageId = IdUtils.resolveLink(astImage.url, pageId); - var imageContent = pages.loadAsset(imageId); - if (imageContent == null) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Couldn't find image {}", astImage.url); - image.setTitle("Missing image: " + astImage.url); - } - image.setImage(imageId, imageContent); - } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Invalid image id: {}", astImage.url); - image.setTitle("Invalid image URL: " + astImage.url); - } - return image; - } - public LytBlock createErrorBlock(String text, UnistNode child) { var paragraph = new LytParagraph(); paragraph.append(createErrorFlowContent(text, child)); @@ -1788,12 +1208,6 @@ private static Class castClass(Class rawClass) { return (Class) rawClass; } - @Desugar - private record CsvFenceMeta(boolean header, List widthHints) {} - - @Desugar - private record MarkdownTableMeta(List widthHints, int consumeChildCount) {} - @Desugar private record BlockTagChildSource(String source) {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java index b4652108..167efb0a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java @@ -3,22 +3,9 @@ import java.util.Collections; import java.util.Set; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.oredict.OreDictionary; - -import org.jetbrains.annotations.Nullable; - -import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver.ResolvedBlockReference; -import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.internal.structure.GuideTextNbtCodec; -import com.hfstudio.guidenh.guide.scene.CameraSettings; -import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; -import com.hfstudio.guidenh.guide.scene.PerspectivePreset; -import com.hfstudio.guidenh.guide.scene.element.BlockElementCompiler; -import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; -import com.hfstudio.guidenh.guide.scene.ponder.PonderNbtPath; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class BlockImageCompiler extends BlockTagCompiler { @@ -30,188 +17,23 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - var blockReference = MdxAttrs.getRequiredBlockReference(compiler, parent, el, "id"); - if (blockReference == null) { - return; - } - - var scene = new LytGuidebookScene(); - scene.setInteractive(false); - scene.setSceneButtonsVisible(false); - scene.setBottomControlsVisible(false); - scene.setReserveBottomControlArea(false); - scene.setVisibleLayerSliderEnabled(false); - scene.setGridButtonEnabled(false); - scene.setGridVisible(false); - scene.setAnnotationsVisible(false); - scene.setShowBackground(false); + String id = MdxAttrs.getString(compiler, parent, el, "id", null); + String ore = MdxAttrs.getString(compiler, parent, el, "ore", null); - CameraSettings camera = scene.getCamera(); - camera.setZoom(clampZoom(MdxAttrs.getFloat(compiler, parent, el, "scale", 1f))); - camera.setPerspectivePreset(resolvePerspective(compiler, parent, el)); - - int meta = resolveBlockMeta(blockReference); - NBTTagCompound tileTag = resolveTileTag(compiler, parent, el); - GuidebookPreviewBlockPlacer.place( - scene.getLevel(), - 0, - 0, - 0, - blockReference.block(), - meta, - tileTag, - blockReference.registryId() - .toString()); - - finalizeSceneLayout(scene); - scene.snapshotInitialCamera(); - scene.captureInitialInteractiveState(); - parent.append(scene); - } - - private PerspectivePreset resolvePerspective(PageCompiler compiler, LytBlockContainer parent, - MdxJsxElementFields el) { - String rawPerspective = MdxAttrs.getString(compiler, parent, el, "perspective", null); - if (rawPerspective == null || rawPerspective.trim() - .isEmpty()) { - return PerspectivePreset.ISOMETRIC_NORTH_EAST; - } - return PerspectivePreset.fromSerializedName(rawPerspective.trim()); - } - - private int resolveBlockMeta(ResolvedBlockReference blockReference) { - if (blockReference.hasExplicitMeta() && blockReference.stack() != null) { - int meta = blockReference.stack() - .getItemDamage(); - if (meta != OreDictionary.WILDCARD_VALUE) { - return meta; - } - } - return BlockElementCompiler.defaultMetaFor(blockReference.block(), null); - } - - @Nullable - private NBTTagCompound resolveTileTag(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - NBTTagCompound merged = resolveInlineIdNbt(compiler, parent, el); - String nbtText = MdxAttrs.getString(compiler, parent, el, "nbt", null); - if (nbtText == null || nbtText.trim() - .isEmpty()) { - return merged; - } - - try { - NBTTagCompound attrTag = GuideTextNbtCodec.readTextSafeCompound(nbtText.trim()); - if (merged == null) { - return attrTag; - } - PonderNbtPath.mergeCompound(merged, attrTag); - return merged; - } catch (Exception e) { - parent.appendError(compiler, "Bad nbt: " + e.getMessage(), el); - return merged; - } - } - - @Nullable - private NBTTagCompound resolveInlineIdNbt(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - String oreName = MdxAttrs.getString(el, "ore", null); - if (oreName != null && !oreName.trim() - .isEmpty()) { - return null; - } - String rawId = MdxAttrs.getString(el, "id", null); - if (rawId == null || rawId.trim() - .isEmpty()) { - return null; - } - String trimmedId = rawId.trim(); - if (trimmedId.indexOf('{') < 0) { - return null; - } - - try { - IdUtils.ParsedItemRef parsed = IdUtils.parseItemRef( - trimmedId, - compiler.getPageId() - .getResourceDomain()); - return parsed == null || parsed.nbt() == null ? null - : (NBTTagCompound) parsed.nbt() - .copy(); - } catch (IllegalArgumentException e) { - parent.appendError(compiler, "Malformed id " + trimmedId + ": " + e.getMessage(), el); - return null; - } - } - - private void finalizeSceneLayout(LytGuidebookScene scene) { - if (scene.getLevel() - .isEmpty()) { + if ((id == null || id.trim().isEmpty()) && (ore == null || ore.trim().isEmpty())) { + parent.appendError(compiler, "Missing id attribute (or ore).", el); return; } - int width = scene.getSceneWidth(); - int height = scene.getSceneHeight(); - CameraSettings camera = scene.getCamera(); - camera.setViewportSize(width, height); - - float[] center = scene.getLevel() - .getCenter(); - camera.setRotationCenter(center[0], center[1], center[2]); - - int[] bounds = scene.getLevel() - .getBounds(); - float minX = bounds[0]; - float minY = bounds[1]; - float minZ = bounds[2]; - float maxX = bounds[3] + 1f; - float maxY = bounds[4] + 1f; - float maxZ = bounds[5] + 1f; - - float savedOffsetX = camera.getOffsetX(); - float savedOffsetY = camera.getOffsetY(); - camera.setOffsetX(0f); - camera.setOffsetY(0f); - - float minScreenX = Float.MAX_VALUE; - float maxScreenX = -Float.MAX_VALUE; - float minScreenY = Float.MAX_VALUE; - float maxScreenY = -Float.MAX_VALUE; - for (int cornerIndex = 0; cornerIndex < 8; cornerIndex++) { - float worldX = (cornerIndex & 1) == 0 ? minX : maxX; - float worldY = (cornerIndex & 2) == 0 ? minY : maxY; - float worldZ = (cornerIndex & 4) == 0 ? minZ : maxZ; - var screenPoint = camera.worldToScreen(worldX, worldY, worldZ); - if (screenPoint.x < minScreenX) { - minScreenX = screenPoint.x; - } - if (screenPoint.x > maxScreenX) { - maxScreenX = screenPoint.x; - } - if (screenPoint.y < minScreenY) { - minScreenY = screenPoint.y; - } - if (screenPoint.y > maxScreenY) { - maxScreenY = screenPoint.y; - } - } - - int autoWidth = clampSceneDimension((int) Math.ceil(maxScreenX - minScreenX) + 16); - int autoHeight = clampSceneDimension((int) Math.ceil(maxScreenY - minScreenY) + 16); - scene.setSceneSize(autoWidth, autoHeight); - camera.setViewportSize(autoWidth, autoHeight); - - camera.setOffsetX(0f); - camera.setOffsetY(0f); - var projectedCenter = camera.worldToScreen(center[0], center[1], center[2]); - camera.setOffsetX(-projectedCenter.x + savedOffsetX); - camera.setOffsetY(projectedCenter.y + savedOffsetY); - } - - private int clampSceneDimension(int dimension) { - return Math.max(64, Math.min(256, dimension)); - } + int meta = MdxAttrs.getInt(compiler, parent, el, "meta", 0); + String nbt = MdxAttrs.getString(compiler, parent, el, "nbt", null); + float scale = MdxAttrs.getFloat(compiler, parent, el, "scale", 1f); + String perspective = MdxAttrs.getString(compiler, parent, el, "perspective", null); + int width = MdxAttrs.getInt(compiler, parent, el, "width", 128); + int height = MdxAttrs.getInt(compiler, parent, el, "height", 128); - private float clampZoom(float zoom) { - return Math.max(LytGuidebookScene.MIN_ZOOM, Math.min(LytGuidebookScene.MAX_ZOOM, zoom)); + var placeholder = LytParagraph.of("[BlockImage]"); + placeholder.setStyleClass("BlockImage"); + parent.append(placeholder); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CommandLinkCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CommandLinkCompiler.java index f63144b7..7b1ea5f1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CommandLinkCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CommandLinkCompiler.java @@ -3,8 +3,6 @@ import java.util.Collections; import java.util.Set; -import net.minecraft.client.Minecraft; - import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.IndexingContext; @@ -15,8 +13,6 @@ import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import cpw.mods.fml.common.FMLLog; - public class CommandLinkCompiler extends FlowTagCompiler { @Override @@ -34,21 +30,16 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen parent.appendError(compiler, "command must start with /", el); return; } - var sendCommand = command; var closeGuide = MdxAttrs.getBoolean(compiler, parent, el, "close", false); var title = el.getAttributeString("title", ""); var link = new LytFlowLink(); link.setTooltip(buildTooltip(title, command)); - var pageId = compiler.getPageId(); - link.setClickCallback(uiHost -> { - var mc = Minecraft.getMinecraft(); - if (mc.thePlayer != null) { - FMLLog.getLogger() - .info("[GuideNH] [CommandLinkCompiler] Sending command from page {}: {}", pageId, sendCommand); - mc.thePlayer.sendChatMessage(sendCommand); - } - }); + // Pure placeholder: mark for the script to materialize at mount time + link.setStyleClass("CommandLink"); + link.setData("command", command); + link.setData("close", closeGuide); + link.setData("title", title); compiler.compileFlowContext(el.children(), link); parent.append(link); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index beb64826..c7e97489 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -1,6 +1,5 @@ package com.hfstudio.guidenh.guide.compiler.tags; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -16,7 +15,6 @@ import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.table.LytTable; import com.hfstudio.guidenh.guide.document.block.table.LytTableCell; -import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -44,21 +42,12 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl return; } - byte[] data = compiler.loadAsset(csvId); - if (data == null) { - parent.appendError(compiler, "Missing CSV asset: " + csvId, el); - return; - } - - List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); - if (rows.isEmpty()) { - parent.appendError(compiler, "CsvTable source is empty: " + csvId, el); - return; - } - boolean header = MdxAttrs.getBoolean(compiler, parent, el, "header", true); - parent.append( - buildTable(rows, header, parseWidthHints(MdxAttrs.getString(compiler, parent, el, "widths", null)))); + List widths = parseWidthHints(MdxAttrs.getString(compiler, parent, el, "widths", null)); + + CsvTablePlaceholder placeholder = new CsvTablePlaceholder(csvId.toString(), header, widths); + placeholder.appendText("Loading CSV..."); + parent.append(placeholder); } @Override @@ -69,30 +58,9 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } catch (MdxAttrs.AttributeException e) { return; } - if (src == null || src.trim() + if (src != null && !src.trim() .isEmpty()) { - return; - } - - ResourceLocation csvId; - try { - csvId = IdUtils.resolveLink(src.trim(), indexer.getPageId()); - } catch (IllegalArgumentException e) { - return; - } - - byte[] data = indexer.loadAsset(csvId); - if (data == null) { - return; - } - - List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); - for (List row : rows) { - for (String cell : row) { - if (!cell.isEmpty()) { - sink.appendText(el, cell); - } - } + sink.appendText(el, src); sink.appendBreak(); } } @@ -178,4 +146,17 @@ private static void appendCellContent(LytTableCell cell, String value) { cell.append(paragraph); } + + public static class CsvTablePlaceholder extends LytParagraph { + public final String src; + public final boolean header; + public final List widths; + + public CsvTablePlaceholder(String src, boolean header, List widths) { + this.src = src; + this.header = header; + this.widths = widths; + setStyleClass("CsvTable"); + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java index 3e48fab7..78df512c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java @@ -13,7 +13,7 @@ import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.ImageRegionAnnotation; -import com.hfstudio.guidenh.guide.document.block.LytImage; +import com.hfstudio.guidenh.guide.document.block.LytImageBlock; import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.document.flow.InlineBlockAlignment; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; @@ -49,33 +49,36 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen int widthPx = parseIntAttr(el, "width", -1); int heightPx = parseIntAttr(el, "height", -1); - var image = new LytImage(); + LytImageBlock block = new LytImageBlock(); + block.setStyleClass("FloatingImage"); + block.setAlign(align); if (title != null) { - image.setTitle(title); + block.setTitle(title); } - image.setExplicitWidth(widthPx); - image.setExplicitHeight(heightPx); + block.setExplicitWidth(widthPx); + block.setExplicitHeight(heightPx); + + // Resolve the image src to a string identifier for later script use without + // loading the actual asset at compile time. + String resolvedSrc = null; try { var imageId = IdUtils.resolveLink(src, compiler.getPageId()); - var imageContent = compiler.loadAsset(imageId); - if (imageContent == null) { - FMLLog.getLogger() - .error("[GuideNH] [FloatingImageCompiler] Couldn't find image {}", src); - image.setTitle("Missing image: " + src); - } - image.setImage(imageId, imageContent); + resolvedSrc = imageId.toString(); } catch (IllegalArgumentException e) { FMLLog.getLogger() .error("[GuideNH] [FloatingImageCompiler] Invalid image id: {}", src); - image.setTitle("Invalid image URL: " + src); + if (block.getTitle() == null) { + block.setTitle("Invalid image URL: " + src); + } } + block.setSrc(resolvedSrc); var wholeImageSound = GuideSoundParsers.parseAttributes(compiler, parent, el, "soundSrc"); if (wholeImageSound != null) { var soundAnnotation = new ImageRegionAnnotation(false, ConstantColor.WHITE, 1); soundAnnotation.setSound(wholeImageSound); soundAnnotation.setSoundTrigger(parseTrigger(compiler, parent, el)); - image.addAnnotation(soundAnnotation); + block.addAnnotation(soundAnnotation); } // Parse child elements. @@ -84,11 +87,11 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen for (var child : children) { if (child instanceof MdxJsxElementFields annEl && "ImageAnnotation".equals(annEl.name())) { var ann = parseImageAnnotation(compiler, parent, annEl); - image.addAnnotation(ann); + block.addAnnotation(ann); } else if (child instanceof MdxJsxElementFields soundEl && "SoundArea".equals(soundEl.name())) { var ann = parseSoundArea(compiler, parent, soundEl); if (ann != null) { - image.addAnnotation(ann); + block.addAnnotation(ann); } } } @@ -96,17 +99,17 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen // Wrap it in a flow content inline block var inlineBlock = new LytFlowInlineBlock(); - inlineBlock.setBlock(image); + inlineBlock.setBlock(block); switch (align) { case "left" -> { inlineBlock.setAlignment(InlineBlockAlignment.FLOAT_LEFT); - image.setMarginRight(5); - image.setMarginBottom(5); + block.setMarginRight(5); + block.setMarginBottom(5); } case "right" -> { inlineBlock.setAlignment(InlineBlockAlignment.FLOAT_RIGHT); - image.setMarginLeft(5); - image.setMarginBottom(5); + block.setMarginLeft(5); + block.setMarginBottom(5); } default -> { parent.append(compiler.createErrorFlowContent("Invalid align. Must be left or right.", el)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java index e4f2c7cc..4a7eb7b4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java @@ -3,10 +3,8 @@ import java.util.Collections; import java.util.Set; -import net.minecraft.util.ResourceLocation; - import com.hfstudio.guidenh.guide.compiler.PageCompiler; -import com.hfstudio.guidenh.guide.document.block.LytImage; +import com.hfstudio.guidenh.guide.document.block.LytImageBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -20,22 +18,26 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { - LytImage image = new LytImage(); + LytImageBlock block = new LytImageBlock(); + block.setStyleClass("Img"); + String src = el.getAttributeString("src", ""); - ResourceLocation imageId = compiler.resolveId(src); - if (imageId != null) { - byte[] imageContent = compiler.loadAsset(imageId); - if (imageContent != null) { - image.setImage(imageId, imageContent); + if (!src.isEmpty()) { + var imageId = compiler.resolveId(src); + if (imageId != null) { + block.setSrc(imageId.toString()); } } + String alt = el.getAttributeString("alt", ""); String title = el.getAttributeString("title", ""); - if (!alt.isEmpty()) image.setAlt(alt); - if (!title.isEmpty()) image.setTitle(title); + if (!alt.isEmpty()) block.setAlt(alt); + if (!title.isEmpty()) block.setTitle(title); + + block.appendText("Loading image..."); var inlineBlock = new LytFlowInlineBlock(); - inlineBlock.setBlock(image); + inlineBlock.setBlock(block); parent.append(inlineBlock); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java index 9e613a70..88d34e41 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java @@ -1,13 +1,15 @@ package com.hfstudio.guidenh.guide.compiler.tags; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Set; import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.document.block.LytItemGrid; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class ItemGridCompiler extends BlockTagCompiler { @@ -19,24 +21,33 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - var itemGrid = new LytItemGrid(); + List itemIds = new ArrayList<>(); // We expect children to only contain ItemIcon elements for (var childNode : el.children()) { if (childNode instanceof MdxJsxElementFields jsxChild && "ItemIcon".equals(jsxChild.name())) { - var stack = MdxAttrs.getRequiredItemStack(compiler, parent, jsxChild); - if (stack != null) { - itemGrid.addItem(stack); + var itemId = MdxAttrs.getString(compiler, parent, jsxChild, "id", null); + if (itemId != null) { + itemIds.add(itemId); } - continue; } parent.appendError(compiler, "Unsupported child-element in ItemGrid", childNode); } - parent.append(itemGrid); + ItemGridPlaceholder placeholder = new ItemGridPlaceholder(itemIds); + parent.append(placeholder); } @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) {} + + public static class ItemGridPlaceholder extends LytParagraph { + public final List itemIds; + + public ItemGridPlaceholder(List itemIds) { + this.itemIds = itemIds; + setStyleClass("ItemGrid"); + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java index dd4d6232..a1bc0c76 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java @@ -7,7 +7,7 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.PageCompiler; -import com.hfstudio.guidenh.guide.document.block.LytItemImage; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -21,20 +21,24 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { - var stack = MdxAttrs.getRequiredItemStack(compiler, parent, el); - if (stack == null) return; + String itemId = MdxAttrs.getString(compiler, parent, el, "id", null); + if (itemId == null) return; + itemId = itemId.trim(); float scale = MdxAttrs.getFloat(compiler, parent, el, "scale", 1f); - var img = new LytItemImage(stack); - img.setScale(scale); - img.setInline(true); + boolean showTooltip; + Boolean showIcon; + String labelPosition; + String labelFormat; + Integer yOffset = null; + Integer labelYOffset = null; // Allow MDX authors to nudge the icon after its default inline vertical centering. // e.g. to move it down by 2px. String yOffRaw = el.getAttributeString("yOffset", null); if (yOffRaw != null && !yOffRaw.isEmpty()) { try { - img.setInlineYOffsetOverride(Integer.parseInt(yOffRaw.trim())); + yOffset = Integer.parseInt(yOffRaw.trim()); } catch (NumberFormatException ignored) { parent.appendError(compiler, "yOffset must be an integer (pixels at scale=1)", el); } @@ -45,7 +49,7 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen String labelYOffRaw = el.getAttributeString("labelYOffset", null); if (labelYOffRaw != null && !labelYOffRaw.isEmpty()) { try { - img.setLabelYOffsetOverride(Integer.parseInt(labelYOffRaw.trim())); + labelYOffset = Integer.parseInt(labelYOffRaw.trim()); } catch (NumberFormatException ignored) { parent.appendError(compiler, "labelYOffset must be an integer (pixels at scale=1)", el); } @@ -55,27 +59,24 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen Boolean noTooltipAttr = MdxAttrs.getOptionalBoolean(el, "noTooltip"); boolean noTooltip = MdxAttrs.getBoolean(noTooltipAttr, false); Boolean showTooltipAttr = MdxAttrs.getOptionalBoolean(el, "showTooltip"); - boolean showTooltip = showTooltipAttr != null ? showTooltipAttr : !noTooltip; - img.setShowTooltip(showTooltip); + showTooltip = showTooltipAttr != null ? showTooltipAttr : !noTooltip; // showIcon — whether to render the icon graphic (default true). - Boolean showIcon = MdxAttrs.getOptionalBoolean(el, "showIcon"); - if (showIcon != null) { - img.setShowIcon(showIcon); - } + showIcon = MdxAttrs.getOptionalBoolean(el, "showIcon"); // label — "left" or "right" to display the item name next to the icon. String labelRaw = el.getAttributeString("label", null); - img.setLabelPosition(resolveLabelPosition(labelRaw)); + labelPosition = resolveLabelPosition(labelRaw); // format — Markdown-style format pattern for the label text. String formatRaw = el.getAttributeString("format", null); - if (formatRaw != null && !formatRaw.isEmpty()) { - img.setLabelFormat(formatRaw); - } + labelFormat = (formatRaw != null && !formatRaw.isEmpty()) ? formatRaw : null; + + ItemImagePlaceholder placeholder = new ItemImagePlaceholder( + itemId, scale, yOffset, labelYOffset, showTooltip, showIcon, labelPosition, labelFormat); var inline = new LytFlowInlineBlock(); - inline.setBlock(img); + inline.setBlock(placeholder); parent.append(inline); } @@ -94,4 +95,34 @@ public static String resolveLabelPosition(@Nullable String raw) { return "right"; } + public static class ItemImagePlaceholder extends LytParagraph { + public final String itemId; + public final float scale; + @Nullable + public final Integer yOffset; + @Nullable + public final Integer labelYOffset; + public final boolean showTooltip; + @Nullable + public final Boolean showIcon; + @Nullable + public final String labelPosition; + @Nullable + public final String labelFormat; + + public ItemImagePlaceholder(String itemId, float scale, @Nullable Integer yOffset, + @Nullable Integer labelYOffset, boolean showTooltip, @Nullable Boolean showIcon, + @Nullable String labelPosition, @Nullable String labelFormat) { + this.itemId = itemId; + this.scale = scale; + this.yOffset = yOffset; + this.labelYOffset = labelYOffset; + this.showTooltip = showTooltip; + this.showIcon = showIcon; + this.labelPosition = labelPosition; + this.labelFormat = labelFormat; + setStyleClass("ItemImage"); + } + } + } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java index e83adb84..cb20072a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java @@ -3,17 +3,9 @@ import java.util.Collections; import java.util.Set; -import com.hfstudio.guidenh.guide.PageAnchor; -import com.hfstudio.guidenh.guide.compiler.LinkParser; import com.hfstudio.guidenh.guide.compiler.PageCompiler; -import com.hfstudio.guidenh.guide.document.block.LytItemImage; -import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; -import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; -import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; -import com.hfstudio.guidenh.guide.indices.ItemIndex; -import com.hfstudio.guidenh.guide.indices.OreIndex; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class ItemLinkCompiler extends FlowTagCompiler { @@ -24,12 +16,14 @@ public Set getTagNames() { } @Override - public void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { - var itemAndId = MdxAttrs.getRequiredItemStackAndId(compiler, parent, el); - if (itemAndId == null) { + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + // Extract raw attributes (no registry lookups) + String itemId = MdxAttrs.getString(compiler, parent, el, "id", null); + String ore = MdxAttrs.getString(compiler, parent, el, "ore", null); + if (itemId == null && ore == null) { + parent.appendError(compiler, "Missing id or ore attribute.", el); return; } - var stack = itemAndId.getRight(); // showTooltip — default true for ItemLink Boolean noTooltipAttr = MdxAttrs.getOptionalBoolean(el, "noTooltip"); @@ -42,74 +36,20 @@ public void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFi String iconPosition = ItemImageCompiler.resolveLabelPosition(showIconRaw); // Manual link target override: linksTo="page.md#heading" or "#heading" - PageAnchor linksTo = null; - String linksToAttr = el.getAttributeString("linksTo", null); - if (linksToAttr != null) { - PageAnchor[] holder = new PageAnchor[1]; - LinkParser.parseLink(compiler, linksToAttr, new LinkParser.Visitor() { - - @Override - public void handlePage(PageAnchor page) { - holder[0] = page; - } - - @Override - public void handleError(String error) { - parent.appendError(compiler, error, el); - } - }); - linksTo = holder[0]; - } else { - var itemAnchor = compiler.getIndex(ItemIndex.class) - .findByStack(stack); - linksTo = itemAnchor != null ? itemAnchor - : compiler.getIndex(OreIndex.class) - .findByStack(stack); - } - - // Build icon inline block if requested. - LytFlowInlineBlock iconBlock = null; - if (iconPosition != null) { - var img = new LytItemImage(stack); - img.setScale(1f); - img.setInline(true); - img.setShowTooltip(showTooltip); - iconBlock = new LytFlowInlineBlock(); - iconBlock.setBlock(img); - } - - // If the item link is already on the page we're linking to, or no page exists, - // render as an underlined tooltip span instead of a clickable link. - if (linksTo == null || linksTo.anchor() == null && compiler.getPageId() - .equals(linksTo.pageId())) { - var span = new LytTooltipSpan(); - span.modifyStyle(style -> style.italic(true)); - span.appendText(stack.getDisplayName()); - if (showTooltip) { - span.setTooltip(new ItemTooltip(stack)); - } - if ("left".equals(iconPosition)) { - parent.append(iconBlock); - } - parent.append(span); - if ("right".equals(iconPosition)) { - parent.append(iconBlock); - } - } else { - var link = new LytFlowLink(); - link.setPageLink(linksTo); - link.appendText(stack.getDisplayName()); - if (showTooltip) { - link.setTooltip(new ItemTooltip(stack)); - } - if ("left".equals(iconPosition)) { - parent.append(iconBlock); - } - parent.append(link); - if ("right".equals(iconPosition)) { - parent.append(iconBlock); - } - } + String linksTo = el.getAttributeString("linksTo", null); + + // Create placeholder link for runtime resolution by ItemLinkScript + var link = new LytFlowLink(); + link.setStyleClass("ItemLink"); + link.setData("itemId", itemId != null ? itemId.trim() : null); + link.setData("ore", ore != null ? ore.trim() : null); + link.setData("showTooltip", showTooltip); + link.setData("showIcon", iconPosition); + link.setData("linksTo", linksTo); + link.setData("pageId", compiler.getPageId()); + + compiler.compileFlowContext(el.children(), link); + parent.append(link); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java index 75df2548..4d167c8b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java @@ -28,13 +28,9 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen return; } - var mapping = findMapping(id); - if (mapping == null) { - parent.appendError(compiler, "No key mapping with this id was found.", el); - return; - } - - parent.appendText(describeMapping(mapping)); + var placeholder = parent.appendText(""); + placeholder.setStyleClass("KeyBind"); + placeholder.setData("bindId", id); } public static String getKeyBindId(MdxJsxElementFields el) { @@ -53,6 +49,8 @@ public static String getKeyBindId(MdxJsxElementFields el) { return id.isEmpty() ? null : id; } + // --- Static helpers used by external callers (e.g. site exporter) --- + public static KeyBinding findMapping(String id) { return findMapping(Minecraft.getMinecraft().gameSettings.keyBindings, id); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java index c354721e..e1397489 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java @@ -1,10 +1,7 @@ package com.hfstudio.guidenh.guide.compiler.tags; -import java.nio.charset.StandardCharsets; -import java.util.ArrayDeque; import java.util.Collections; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Set; @@ -16,17 +13,12 @@ import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.document.block.LytMermaidMindmap; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytVBox; -import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNode; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeContentExtractor; -import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; -import cpw.mods.fml.common.FMLLog; - public class MermaidCompiler extends BlockTagCompiler { @Override @@ -36,142 +28,79 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - String source = resolveSource(compiler, parent, el); - if (source == null || source.trim() - .isEmpty()) { - parent.appendError(compiler, "Mermaid requires inline content or a non-empty src attribute.", el); - return; - } - - try { - var document = MermaidMindmapParser.parse(source); - Map nodeContentBlocks = compileNodeContentBlocks( - compiler, - parent, - el, - document.getRoot()); - LytMermaidMindmap block = new LytMermaidMindmap(document, source, nodeContentBlocks); - int width = MdxAttrs.getInt(compiler, parent, el, "width", 0); - int height = MdxAttrs.getInt(compiler, parent, el, "height", 0); - if (width > 0 || height > 0) { - block.setPreferredSize(width, height); - } - FMLLog.getLogger() - .debug( - "[GuideNH] [MermaidCompiler] Compiled Mermaid runtime block for page {} with root='{}', children={}, sourceLength={}, width={}, height={}", - compiler.getPageId(), - document.getRoot() - .getText(), - document.getRoot() - .getChildren() - .size(), - source.length(), - width, - height); - parent.append(block); - } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [MermaidCompiler] Failed to compile Mermaid runtime block for page {} from source: {}", - compiler.getPageId(), - source, - e); - parent.appendError(compiler, "Unsupported Mermaid runtime block: " + e.getMessage(), el); - } - } - - @Override - public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { - String source = resolveSource(indexer, el); - if (source != null && !source.trim() - .isEmpty()) { - sink.appendText(el, source); - sink.appendBreak(); - } - } + String src = null; + String sourceText = null; - private String resolveSource(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - String src; + String srcStr; try { - src = MdxAttrs.getString(el, "src", null); + srcStr = MdxAttrs.getString(el, "src", null); } catch (MdxAttrs.AttributeException e) { parent.appendError(compiler, e.getMessage(), el); - return null; + return; } - if (src != null && !src.trim() + + if (srcStr != null && !srcStr.trim() .isEmpty()) { - return loadSource(compiler, src.trim()); + ResourceLocation mermaidId; + try { + mermaidId = IdUtils.resolveLink(srcStr.trim(), compiler.getPageId()); + } catch (IllegalArgumentException e) { + parent.appendError(compiler, "Malformed Mermaid src: " + srcStr, el); + return; + } + src = mermaidId.toString(); + } else { + String rawTagBodySource = compiler.getBlockTagChildrenSource(el); + if (rawTagBodySource != null && !rawTagBodySource.trim() + .isEmpty()) { + sourceText = MermaidMindmapNodeContentExtractor.stripExplicitNodeContentBlocks(rawTagBodySource); + } else { + sourceText = MermaidMindmapNodeContentExtractor.extractDiagramSource(el.children()); + } } - String rawTagBodySource = compiler.getBlockTagChildrenSource(el); - if (rawTagBodySource != null && !rawTagBodySource.trim() - .isEmpty()) { - return MermaidMindmapNodeContentExtractor.stripExplicitNodeContentBlocks(rawTagBodySource); + + if ((sourceText == null || sourceText.trim() + .isEmpty()) && src == null) { + parent.appendError(compiler, "Mermaid requires inline content or a non-empty src attribute.", el); + return; } - return MermaidMindmapNodeContentExtractor.extractDiagramSource(el.children()); + + int width = MdxAttrs.getInt(compiler, parent, el, "width", 0); + int height = MdxAttrs.getInt(compiler, parent, el, "height", 0); + + Map nodeContentBlocks = compileNodeContentBlocks(compiler, parent, el); + + MermaidPlaceholder placeholder = new MermaidPlaceholder(src, sourceText, width, height, nodeContentBlocks); + placeholder.appendText("Loading Mermaid..."); + parent.append(placeholder); } - private String resolveSource(IndexingContext indexer, MdxJsxElementFields el) { + @Override + public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { String src; try { src = MdxAttrs.getString(el, "src", null); } catch (MdxAttrs.AttributeException e) { - return null; + src = null; } if (src != null && !src.trim() .isEmpty()) { - return loadSource(indexer, src.trim()); - } - return MermaidMindmapNodeContentExtractor.extractDiagramSource(el.children()); - } - - private String loadSource(PageCompiler compiler, String src) { - try { - ResourceLocation mermaidId = IdUtils.resolveLink(src, compiler.getPageId()); - byte[] data = compiler.loadAsset(mermaidId); - if (data == null) { - FMLLog.getLogger() - .warn( - "[GuideNH] [MermaidCompiler] Mermaid src '{}' for page {} could not be loaded as asset {}", - src, - compiler.getPageId(), - mermaidId); - return null; + sink.appendText(el, src); + sink.appendBreak(); + } else { + String inlineSource = MermaidMindmapNodeContentExtractor.extractDiagramSource(el.children()); + if (inlineSource != null && !inlineSource.trim() + .isEmpty()) { + sink.appendText(el, inlineSource); + sink.appendBreak(); } - String loaded = MermaidMindmapParser.normalize(new String(data, StandardCharsets.UTF_8)); - FMLLog.getLogger() - .debug( - "[GuideNH] [MermaidCompiler] Loaded Mermaid src '{}' for page {} as asset {} ({} chars)", - src, - compiler.getPageId(), - mermaidId, - loaded.length()); - return loaded; - } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [MermaidCompiler] Failed to resolve Mermaid src '{}' for page {}", - src, - compiler.getPageId(), - e); - return null; - } - } - - private String loadSource(IndexingContext indexer, String src) { - try { - ResourceLocation mermaidId = IdUtils.resolveLink(src, indexer.getPageId()); - byte[] data = indexer.loadAsset(mermaidId); - return data != null ? MermaidMindmapParser.normalize(new String(data, StandardCharsets.UTF_8)) : null; - } catch (IllegalArgumentException e) { - return null; } } private Map compileNodeContentBlocks(PageCompiler compiler, LytBlockContainer parent, - MdxJsxElementFields mermaidElement, MermaidMindmapNode root) { - Map nodesById = indexNodesById(root); - Map explicitBlocksById = new LinkedHashMap<>(); + MdxJsxElementFields mermaidElement) { + Map result = new LinkedHashMap<>(); for (MdxJsxFlowElement child : MermaidMindmapNodeContentExtractor .collectNodeContentElements(mermaidElement.children())) { String id = MermaidMindmapNodeContentExtractor.readNodeContentId(child); @@ -179,66 +108,41 @@ private Map compileNodeContentBlocks(PageCompiler compiler, Ly parent.appendError(compiler, "Mermaid requires a non-empty id attribute.", child); continue; } - if (!nodesById.containsKey(id)) { - parent.appendError(compiler, "Mermaid references unknown node id '" + id + "'.", child); - continue; - } - if (explicitBlocksById.put(id, child) != null) { - parent.appendError(compiler, "Duplicate Mermaid id '" + id + "'.", child); - } - } - - Map result = new LinkedHashMap<>(); - for (MermaidMindmapNode node : nodesById.values()) { - LytBlock compiled = compileNodeContentBlock(compiler, explicitBlocksById.get(node.getId()), node); + LytBlock compiled = compileNodeContentBlock(compiler, child); if (compiled != null) { - result.put(node.getId(), compiled); + result.put(id, compiled); } } return result; } - private Map indexNodesById(MermaidMindmapNode root) { - Map nodesById = new LinkedHashMap<>(); - ArrayDeque pending = new ArrayDeque<>(); - pending.add(root); - while (!pending.isEmpty()) { - MermaidMindmapNode node = pending.removeFirst(); - nodesById.putIfAbsent(node.getId(), node); - List children = node.getChildren(); - for (int index = 0; index < children.size(); index++) { - pending.addLast(children.get(index)); - } - } - return nodesById; - } - - private LytBlock compileNodeContentBlock(PageCompiler compiler, MdxJsxFlowElement explicitContent, - MermaidMindmapNode node) { - if (explicitContent != null) { - LytVBox box = new LytVBox(); - compiler.withBlockTagChildrenSourceContext( - explicitContent, - () -> compiler.compileBlockContext(explicitContent.children(), box)); - return box.getChildren() - .isEmpty() ? null : box; - } - if (!shouldCompileRichInlineLabel(node)) { + private LytBlock compileNodeContentBlock(PageCompiler compiler, MdxJsxFlowElement explicitContent) { + if (explicitContent == null) { return null; } - LytParagraph paragraph = new LytParagraph(); - compiler.withSourceContext( - node.getLabelSource(), - () -> compiler.compileInlineMarkdown(node.getLabelSource(), paragraph)); - return paragraph.isEmpty() ? null : paragraph; + LytVBox box = new LytVBox(); + compiler.withBlockTagChildrenSourceContext( + explicitContent, + () -> compiler.compileBlockContext(explicitContent.children(), box)); + return box.getChildren() + .isEmpty() ? null : box; } - private boolean shouldCompileRichInlineLabel(MermaidMindmapNode node) { - String labelSource = node.getLabelSource(); - if (labelSource == null || labelSource.trim() - .isEmpty()) { - return false; + public static class MermaidPlaceholder extends LytParagraph { + public final String src; + public final String sourceText; + public final int width; + public final int height; + public final Map nodeContentBlocks; + + public MermaidPlaceholder(String src, String sourceText, int width, int height, + Map nodeContentBlocks) { + this.src = src; + this.sourceText = sourceText; + this.width = width; + this.height = height; + this.nodeContentBlocks = nodeContentBlocks; + setStyleClass("Mermaid"); } - return !labelSource.equals(node.getText()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java index d002b961..dd11cc7e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java @@ -3,8 +3,6 @@ import java.util.Collections; import java.util.Set; -import net.minecraft.client.Minecraft; - import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -21,9 +19,7 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { - var playerName = Minecraft.getMinecraft() - .getSession() - .getUsername(); - parent.appendText(playerName); + var placeholder = parent.appendText(""); + placeholder.setStyleClass("PlayerName"); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java index e9f251e0..8c13b72e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java @@ -23,17 +23,11 @@ import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytHBox; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.block.recipes.LytStandardRecipeBox; -import com.hfstudio.guidenh.guide.internal.recipe.LytNeiRecipeBox; -import com.hfstudio.guidenh.guide.internal.recipe.RecipeCache; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.guide.internal.recipe.RecipeLookup; -import com.hfstudio.guidenh.integration.api.GuideNhIntegrationRegistry; import com.hfstudio.guidenh.integration.api.RecipeEntry; import com.hfstudio.guidenh.integration.api.RecipeSlot; import com.hfstudio.guidenh.integration.nei.NeiRecipeLookup; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; - -import cpw.mods.fml.common.FMLLog; public class RecipeCompiler extends BlockTagCompiler { @@ -67,25 +61,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl parent.appendError(compiler, "Blank id", el); return; } - Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (item == null) { - if (fallbackText != null) { - if (!fallbackText.isEmpty()) parent.append(LytParagraph.of(fallbackText)); - } else { - parent.appendError(compiler, "Missing item: " + ref.id(), el); - } - return; - } - boolean multi = "RecipesFor".equals(el.name()); - boolean usageQuery = "RecipeUsage".equals(el.name()); - - // Build the concrete query stack with meta + nbt (wildcard meta collapses to 0). - ItemStack targetStack = new ItemStack(item, 1, ref.concreteMeta()); - if (ref.nbt() != null) { - targetStack.stackTagCompound = (NBTTagCompound) ref.nbt() - .copy(); - } + String tagName = el.name(); + boolean multi = "RecipesFor".equals(tagName); + boolean usageQuery = "RecipeUsage".equals(tagName); String handlerNameFilter = trimToNull(MdxAttrs.getString(compiler, parent, el, "handlerName", null)); String handlerIdFilter = trimToNull(MdxAttrs.getString(compiler, parent, el, "handlerId", null)); @@ -128,142 +107,14 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl return; } } - boolean hasRecipeFilter = !inputExpr.isEmpty() || !outputExpr.isEmpty(); - boolean hasHandlerFilter = handlerNameFilter != null || handlerIdFilter != null || handlerOrder >= 0; - - // Prefer NEI-native handler rendering if available. - List rawHandlers = usageQuery ? RecipeCache.getUsageHandlers(targetStack) - : RecipeCache.getCraftingHandlers(targetStack); - // When a handler filter is specified, also consult usage handlers. This covers NEI handlers - // that treat the target as an input rather than an output. Anvil, repair, fuel, and brewing - // ingredients never show up under getCraftingHandlers. - if (!usageQuery && hasHandlerFilter) { - List usage = RecipeCache.getUsageHandlers(targetStack); - if (!usage.isEmpty()) { - if (rawHandlers.isEmpty()) { - rawHandlers = usage; - } else { - List merged = new ArrayList<>(rawHandlers.size() + usage.size()); - merged.addAll(rawHandlers); - // Dedup by identity because the same IRecipeHandler instance may appear in both lists. - IdentityHashMap seen = new IdentityHashMap<>(merged.size()); - for (Object h : rawHandlers) seen.put(h, Boolean.TRUE); - for (Object h : usage) if (seen.put(h, Boolean.TRUE) == null) merged.add(h); - rawHandlers = merged; - } - } - } - List handlers = filterHandlers(rawHandlers, handlerNameFilter, handlerIdFilter, handlerOrder); - if (!handlers.isEmpty()) { - List boxes = new ArrayList<>(); - for (int hi = 0; hi < handlers.size() && boxes.size() < limit; hi++) { - Object handler = handlers.get(hi); - int num = GuideNhIntegrationRegistry.global() - .lookupRecipeHandlerRecipeCount(handler); - int recipeStart = exactRecipeIndex >= 0 ? exactRecipeIndex : 0; - int recipeEnd = exactRecipeIndex >= 0 ? Math.min(num, exactRecipeIndex + 1) : num; - for (int ri = recipeStart; ri < recipeEnd && boxes.size() < limit; ri++) { - if (hasRecipeFilter && !recipeMatches(handler, ri, inputExpr, outputExpr)) continue; - boxes.add(new LytNeiRecipeBox(handler, ri, !usageQuery)); - } - } - if (!boxes.isEmpty()) { - appendRecipes(parent, boxes, multi); - return; - } - if (exactRecipeIndex >= 0) { - appendRecipeNotFoundFallback(compiler, parent, el, fallbackText, usageQuery, ref); - return; - } - } else if (hasHandlerFilter) { - // Handler filter eliminated every candidate. Respect fallbackText (if any) and bail quietly. - // this is a legitimate authoring case (e.g. "only render when NEI + the relevant handler is - // installed") and should not spam error overlays. - if (fallbackText != null && !fallbackText.isEmpty()) { - parent.append(LytParagraph.of(fallbackText)); - } else if (FMLLog.getLogger() - .isDebugEnabled()) { - FMLLog.getLogger() - .debug( - "[GuideNH] [RecipeCompiler] No NEI handler matched filters for {} (handlerName={}, handlerId={}, handlerOrder={})", - ref.id(), - handlerNameFilter, - handlerIdFilter, - handlerOrder); - } - return; - } - // Legacy fallback: raw slot data coming from NEI (no handler draw) or from vanilla crafting registry. - List recipeEntries = usageQuery ? Collections.emptyList() - : GuideNhIntegrationRegistry.global() - .findCraftingRecipeEntries(targetStack); - if (!recipeEntries.isEmpty()) { - List boxes = new ArrayList<>(); - int entryStart = exactRecipeIndex >= 0 ? exactRecipeIndex : 0; - int entryEnd = exactRecipeIndex >= 0 ? Math.min(recipeEntries.size(), exactRecipeIndex + 1) - : recipeEntries.size(); - for (int i = entryStart; i < entryEnd && boxes.size() < limit; i++) { - var e = recipeEntries.get(i); - if (e.result() == null || e.ingredients() - .isEmpty()) continue; - if (hasRecipeFilter && !entryMatches(e, inputExpr, outputExpr)) continue; - List flat = new ArrayList<>(9); - for (int s = 0; s < 9; s++) flat.add(null); - int idx = 0; - for (RecipeSlot slot : e.ingredients()) { - if (idx >= 9) break; - if (slot.stacks() != null && !slot.stacks() - .isEmpty()) flat.set( - idx, - slot.stacks() - .get(0)); - idx++; - } - ItemStack resultStack = e.result() - .stacks() != null - && !e.result() - .stacks() - .isEmpty() ? e.result() - .stacks() - .get(0) : null; - if (resultStack != null) boxes.add(LytStandardRecipeBox.shapeless(flat, resultStack)); - } - if (!boxes.isEmpty()) { - appendRecipes(parent, boxes, multi); - return; - } - } - - List entries = usageQuery ? Collections.emptyList() - : RecipeLookup.findByOutput(item); - if (entries.isEmpty()) { - if (fallbackText != null) { - if (!fallbackText.isEmpty()) parent.append(LytParagraph.of(fallbackText)); - } else { - parent.appendError( - compiler, - "Couldn't find " + (usageQuery ? "usage" : "recipe") + " for " + ref.id(), - el); - } - return; - } - - List boxes = new ArrayList<>(); - int vanillaStart = exactRecipeIndex >= 0 ? exactRecipeIndex : 0; - int vanillaEnd = exactRecipeIndex >= 0 ? Math.min(entries.size(), exactRecipeIndex + 1) : entries.size(); - for (int i = vanillaStart; i < vanillaEnd && boxes.size() < limit; i++) { - var e = entries.get(i); - if (hasRecipeFilter && !vanillaEntryMatches(e, inputExpr, outputExpr)) continue; - var box = e.shapeless ? LytStandardRecipeBox.shapeless(RecipeLookup.asList(e), e.result) - : LytStandardRecipeBox.shaped3x3(RecipeLookup.asList(e), e.result); - boxes.add(box); - } - if (!boxes.isEmpty()) { - appendRecipes(parent, boxes, multi); - return; - } - appendRecipeNotFoundFallback(compiler, parent, el, fallbackText, usageQuery, ref); + // RecipePlaceholder -- recipe resolution deferred to RecipeScript + RecipePlaceholder ph = new RecipePlaceholder(tagName, idStr, ref, fallbackText, + handlerNameFilter, handlerIdFilter, handlerOrder, exactRecipeIndex, + inputExpr, outputExpr, limit, multi, usageQuery); + ph.setStyleClass(tagName); + ph.appendText("Loading recipe..."); + parent.append(ph); } /** @@ -288,11 +139,6 @@ public static void appendRecipes(LytBlockContainer parent, List filterHandlers(List raw, @Nullable String nameFilter, @Nullable String idFilter, - int order) { - return filterHandlers(raw, nameFilter, idFilter, order, NEI_HANDLER_METADATA_READER); - } - public static List filterHandlers(List raw, @Nullable String nameFilter, @Nullable String idFilter, int order, HandlerMetadataReader metadataReader) { if (raw.isEmpty()) return raw; @@ -363,13 +209,13 @@ private FilterTerm(IdUtils.ParsedItemRef ref, boolean negated) { /** * Disjunctive-normal-form filter expression: - * + * *
          *   expr  := orGroup ( ',' orGroup )*   // any group satisfied -> expression holds
          *   group := term    ( '&' term    )*   // every term in the group must be satisfied
          *   term  := [ '!' ] itemRef            // '!' flips the match sense
          * 
    - * + * * Empty expression (from an absent/blank attribute) means "no filter" and is cheap to check * via {@link #isEmpty()}. */ @@ -407,159 +253,46 @@ public interface HandlerRecipeAccess { NeiRecipeLookup.Slot readResultSlot(Object handler, int recipeIndex); } - public interface RecipeSlotAccess { - - List readIngredientSlots(Object handler, int recipeIndex); - - @Nullable - RecipeSlot readResultSlot(Object handler, int recipeIndex); - } - - private static final HandlerMetadataReader NEI_HANDLER_METADATA_READER = new HandlerMetadataReader() { - - @Override - public @Nullable String handlerName(Object handler) { - return GuideNhIntegrationRegistry.global() - .lookupRecipeHandlerName(handler); - } - - @Override - public @Nullable String handlerId(Object handler) { - return GuideNhIntegrationRegistry.global() - .lookupRecipeHandlerId(handler); - } - - @Override - public @Nullable String overlayIdentifier(Object handler) { - return GuideNhIntegrationRegistry.global() - .lookupRecipeHandlerOverlayIdentifier(handler); - } - }; - - private static void appendRecipeNotFoundFallback(PageCompiler compiler, LytBlockContainer parent, - MdxJsxElementFields el, @Nullable String fallbackText, boolean usageQuery, IdUtils.ParsedItemRef ref) { - if (fallbackText != null) { - if (!fallbackText.isEmpty()) parent.append(LytParagraph.of(fallbackText)); - return; - } - parent.appendError(compiler, "Couldn't find " + (usageQuery ? "usage" : "recipe") + " for " + ref.id(), el); - } - - private static final RecipeSlotAccess REGISTRY_RECIPE_SLOT_ACCESS = new RecipeSlotAccess() { - - @Override - public List readIngredientSlots(Object handler, int recipeIndex) { - return GuideNhIntegrationRegistry.global() - .readRecipeIngredientSlots(handler, recipeIndex); - } - - @Override - public @Nullable RecipeSlot readResultSlot(Object handler, int recipeIndex) { - return GuideNhIntegrationRegistry.global() - .readRecipeResultSlot(handler, recipeIndex); - } - }; - /** - * Parse an {@code input} / {@code output} attribute supporting OR (',') + AND ('&') + NOT ('!'). - * Malformed tokens emit a compile error and are skipped; a group with no surviving terms is - * dropped, and if every group is dropped the result is {@link FilterExpr#EMPTY} (i.e. "no - * filter", which is safer than "always fail"). + * A placeholder paragraph that carries all extracted recipe query attributes. + * Actual recipe resolution is deferred to RecipeScript. */ - public static FilterExpr parseFilterExpr(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el, - String attr, String defaultNs) { - String raw = trimToNull(MdxAttrs.getString(compiler, parent, el, attr, null)); - return parseFilterExpr(raw, attr, defaultNs, new Consumer() { - - @Override - public void accept(String message) { - parent.appendError(compiler, message, el); - } - }); - } - - public static FilterExpr parseFilterExpr(@Nullable String raw, String defaultNs) { - return parseFilterExpr(raw, null, defaultNs, null); - } - - private static FilterExpr parseFilterExpr(@Nullable String raw, @Nullable String attr, String defaultNs, - @Nullable Consumer errorSink) { - if (raw == null) return FilterExpr.EMPTY; - List> groups = new ArrayList<>(); - int rawLength = raw.length(); - int orStart = 0; - while (orStart <= rawLength) { - int orEnd = raw.indexOf(',', orStart); - if (orEnd < 0) { - orEnd = rawLength; - } - String orTrim = raw.substring(orStart, orEnd) - .trim(); - if (!orTrim.isEmpty()) { - List andTerms = new ArrayList<>(); - parseFilterTerms(orTrim, attr, defaultNs, errorSink, andTerms); - if (!andTerms.isEmpty()) groups.add(andTerms); - } - if (orEnd == rawLength) { - break; - } - orStart = orEnd + 1; - } - return groups.isEmpty() ? FilterExpr.EMPTY : new FilterExpr(groups); - } - - private static void parseFilterTerms(String orTrim, @Nullable String attr, String defaultNs, - @Nullable Consumer errorSink, List andTerms) { - int andLength = orTrim.length(); - int andStart = 0; - while (andStart <= andLength) { - int andEnd = orTrim.indexOf('&', andStart); - if (andEnd < 0) { - andEnd = andLength; - } - parseFilterTerm( - orTrim.substring(andStart, andEnd) - .trim(), - attr, - defaultNs, - errorSink, - andTerms); - if (andEnd == andLength) { - break; - } - andStart = andEnd + 1; - } - } - - private static void parseFilterTerm(String token, @Nullable String attr, String defaultNs, - @Nullable Consumer errorSink, List andTerms) { - if (token.isEmpty()) return; - boolean negated = false; - if (token.startsWith("!")) { - negated = true; - token = token.substring(1) - .trim(); - if (token.isEmpty()) { - if (errorSink != null) { - errorSink.accept("Empty " + filterAttrName(attr) + " negation token '!' has no id"); - } - return; - } - } - try { - IdUtils.ParsedItemRef p = IdUtils.parseItemRef(token, defaultNs); - if (p != null) andTerms.add(new FilterTerm(p, negated)); - } catch (IllegalArgumentException e) { - if (errorSink != null) { - errorSink.accept("Malformed " + filterAttrName(attr) + " filter '" + token + "': " + e.getMessage()); - } + public static class RecipePlaceholder extends LytParagraph { + + public final String tagName; + public final String idStr; + public final IdUtils.ParsedItemRef ref; + public final String fallbackText; + public final String handlerName; + public final String handlerId; + public final int handlerOrder; + public final int recipeIndex; + public final FilterExpr inputExpr; + public final FilterExpr outputExpr; + public final int limit; + public final boolean multi; + public final boolean usageQuery; + + public RecipePlaceholder(String tagName, String idStr, IdUtils.ParsedItemRef ref, + String fallbackText, String handlerName, String handlerId, + int handlerOrder, int recipeIndex, FilterExpr inputExpr, + FilterExpr outputExpr, int limit, boolean multi, boolean usageQuery) { + this.tagName = tagName; + this.idStr = idStr; + this.ref = ref; + this.fallbackText = fallbackText; + this.handlerName = handlerName; + this.handlerId = handlerId; + this.handlerOrder = handlerOrder; + this.recipeIndex = recipeIndex; + this.inputExpr = inputExpr; + this.outputExpr = outputExpr; + this.limit = limit; + this.multi = multi; + this.usageQuery = usageQuery; } } - private static String filterAttrName(@Nullable String attr) { - return attr == null || attr.isEmpty() ? "filter" : attr; - } - /** * {@code true} when {@code stack} satisfies {@code ref}: item identity match, plus meta equality * when {@code ref} isn't wildcard-meta, plus NBT equality when {@code ref.nbt()} is non-null. @@ -735,21 +468,6 @@ public static boolean evalArray(ItemStack[] stacks, FilterExpr expr) { return false; } - public static boolean recipeMatches(Object handler, int recipeIndex, FilterExpr inputExpr, FilterExpr outputExpr) { - return recipeMatches(handler, recipeIndex, inputExpr, outputExpr, REGISTRY_RECIPE_SLOT_ACCESS); - } - - public static boolean recipeMatches(Object handler, int recipeIndex, FilterExpr inputExpr, FilterExpr outputExpr, - RecipeSlotAccess recipeAccess) { - if (!outputExpr.isEmpty()) { - if (!evalRecipeResultSlot(recipeAccess.readResultSlot(handler, recipeIndex), outputExpr)) return false; - } - if (!inputExpr.isEmpty()) { - if (!evalRecipeSlots(recipeAccess.readIngredientSlots(handler, recipeIndex), inputExpr)) return false; - } - return true; - } - public static boolean recipeMatches(Object handler, int recipeIndex, FilterExpr inputExpr, FilterExpr outputExpr, HandlerRecipeAccess recipeAccess) { if (!outputExpr.isEmpty()) { @@ -778,4 +496,104 @@ public static boolean vanillaEntryMatches(RecipeLookup.Entry e, FilterExpr input if (!inputExpr.isEmpty() && !evalArray(e.input3x3, inputExpr)) return false; return true; } + + /** + * Parse an {@code input} / {@code output} attribute supporting OR (',') + AND ('&') + NOT ('!'). + * Malformed tokens emit a compile error and are skipped; a group with no surviving terms is + * dropped, and if every group is dropped the result is {@link FilterExpr#EMPTY} (i.e. "no + * filter", which is safer than "always fail"). + */ + public static FilterExpr parseFilterExpr(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el, + String attr, String defaultNs) { + String raw = trimToNull(MdxAttrs.getString(compiler, parent, el, attr, null)); + return parseFilterExpr(raw, attr, defaultNs, new Consumer() { + + @Override + public void accept(String message) { + parent.appendError(compiler, message, el); + } + }); + } + + public static FilterExpr parseFilterExpr(@Nullable String raw, String defaultNs) { + return parseFilterExpr(raw, null, defaultNs, null); + } + + private static FilterExpr parseFilterExpr(@Nullable String raw, @Nullable String attr, String defaultNs, + @Nullable Consumer errorSink) { + if (raw == null) return FilterExpr.EMPTY; + List> groups = new ArrayList<>(); + int rawLength = raw.length(); + int orStart = 0; + while (orStart <= rawLength) { + int orEnd = raw.indexOf(',', orStart); + if (orEnd < 0) { + orEnd = rawLength; + } + String orTrim = raw.substring(orStart, orEnd) + .trim(); + if (!orTrim.isEmpty()) { + List andTerms = new ArrayList<>(); + parseFilterTerms(orTrim, attr, defaultNs, errorSink, andTerms); + if (!andTerms.isEmpty()) groups.add(andTerms); + } + if (orEnd == rawLength) { + break; + } + orStart = orEnd + 1; + } + return groups.isEmpty() ? FilterExpr.EMPTY : new FilterExpr(groups); + } + + private static void parseFilterTerms(String orTrim, @Nullable String attr, String defaultNs, + @Nullable Consumer errorSink, List andTerms) { + int andLength = orTrim.length(); + int andStart = 0; + while (andStart <= andLength) { + int andEnd = orTrim.indexOf('&', andStart); + if (andEnd < 0) { + andEnd = andLength; + } + parseFilterTerm( + orTrim.substring(andStart, andEnd) + .trim(), + attr, + defaultNs, + errorSink, + andTerms); + if (andEnd == andLength) { + break; + } + andStart = andEnd + 1; + } + } + + private static void parseFilterTerm(String token, @Nullable String attr, String defaultNs, + @Nullable Consumer errorSink, List andTerms) { + if (token.isEmpty()) return; + boolean negated = false; + if (token.startsWith("!")) { + negated = true; + token = token.substring(1) + .trim(); + if (token.isEmpty()) { + if (errorSink != null) { + errorSink.accept("Empty " + filterAttrName(attr) + " negation token '!' has no id"); + } + return; + } + } + try { + IdUtils.ParsedItemRef p = IdUtils.parseItemRef(token, defaultNs); + if (p != null) andTerms.add(new FilterTerm(p, negated)); + } catch (IllegalArgumentException e) { + if (errorSink != null) { + errorSink.accept("Malformed " + filterAttrName(attr) + " filter '" + token + "': " + e.getMessage()); + } + } + } + + private static String filterAttrName(@Nullable String attr) { + return attr == null || attr.isEmpty() ? "filter" : attr; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SoundLinkCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SoundLinkCompiler.java index c82aa116..b0881e60 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SoundLinkCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SoundLinkCompiler.java @@ -26,8 +26,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen return; } var link = new LytFlowLink(); - link.setClickSoundSpec(sound); - link.setClickCallback(uiHost -> {}); + link.setStyleClass("SoundLink"); + link.setData("soundSpec", sound); compiler.compileInlineFragment(el.children(), link); parent.append(link); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java index 09753679..42efdbfc 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java @@ -1,15 +1,13 @@ package com.hfstudio.guidenh.guide.compiler.tags; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Set; -import net.minecraft.block.Block; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; - import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.document.block.LytStructureView; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstLiteral; @@ -33,11 +31,8 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - var view = new LytStructureView(); - int width = MdxAttrs.getInt(compiler, parent, el, "width", 192); int height = MdxAttrs.getInt(compiler, parent, el, "height", 144); - view.setViewSize(width, height); StringBuilder text = new StringBuilder(); for (var child : el.children()) { @@ -47,13 +42,15 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } } - parseLines(compiler, parent, view, text, el); + List entries = new ArrayList<>(); + parseLines(compiler, parent, entries, text, el); - parent.append(view); + StructurePlaceholder placeholder = new StructurePlaceholder(width, height, entries); + parent.append(placeholder); } - public static void parseLine(PageCompiler compiler, LytBlockContainer parent, LytStructureView view, String line, - MdxJsxElementFields el) { + private static void parseLine(PageCompiler compiler, LytBlockContainer parent, List entries, + String line, MdxJsxElementFields el) { String[] parts = firstTokens(line, 4); if (parts == null) { parent.appendError(compiler, "Structure entry needs ' ': " + line, el); @@ -71,27 +68,10 @@ public static void parseLine(PageCompiler compiler, LytBlockContainer parent, Ly } String idSpec = parts[3]; - int meta = 0; - int firstColon = idSpec.indexOf(':'); - int lastColon = idSpec.lastIndexOf(':'); - String resourceId = idSpec; - if (firstColon != lastColon) { - try { - meta = Integer.parseInt(idSpec.substring(lastColon + 1)); - resourceId = idSpec.substring(0, lastColon); - } catch (NumberFormatException ignored) {} - } - - ItemStack stack = resolveStack(resourceId, meta); - if (stack == null) { - parent.appendError(compiler, "Unknown block/item: " + idSpec, el); - return; - } - - view.addBlock(x, y, z, stack); + entries.add(new StructureEntry(x, y, z, idSpec)); } - private static void parseLines(PageCompiler compiler, LytBlockContainer parent, LytStructureView view, + private static void parseLines(PageCompiler compiler, LytBlockContainer parent, List entries, StringBuilder text, MdxJsxElementFields el) { int lineStart = 0; for (int i = 0; i <= text.length(); i++) { @@ -101,7 +81,7 @@ private static void parseLines(PageCompiler compiler, LytBlockContainer parent, parseRawLine( compiler, parent, - view, + entries, text.substring(lineStart, i) .trim(), el); @@ -112,12 +92,12 @@ private static void parseLines(PageCompiler compiler, LytBlockContainer parent, } } - private static void parseRawLine(PageCompiler compiler, LytBlockContainer parent, LytStructureView view, + private static void parseRawLine(PageCompiler compiler, LytBlockContainer parent, List entries, String line, MdxJsxElementFields el) { if (line.isEmpty() || line.startsWith("#")) { return; } - parseLine(compiler, parent, view, line, el); + parseLine(compiler, parent, entries, line, el); } private static String[] firstTokens(String line, int count) { @@ -151,15 +131,30 @@ private static int findWhitespace(String line, int offset) { return current; } - public static ItemStack resolveStack(String resourceId, int meta) { - var item = (Item) Item.itemRegistry.getObject(resourceId); - if (item != null) { - return new ItemStack(item, 1, meta); + public static class StructureEntry { + public final int x; + public final int y; + public final int z; + public final String idSpec; + + public StructureEntry(int x, int y, int z, String idSpec) { + this.x = x; + this.y = y; + this.z = z; + this.idSpec = idSpec; } - var block = (Block) Block.blockRegistry.getObject(resourceId); - if (block != null) { - return new ItemStack(block, 1, meta); + } + + public static class StructurePlaceholder extends LytParagraph { + public final int width; + public final int height; + public final List entries; + + public StructurePlaceholder(int width, int height, List entries) { + this.width = width; + this.height = height; + this.entries = entries; + setStyleClass("Structure"); } - return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java index 993eb483..be7fa733 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java @@ -1,29 +1,15 @@ package com.hfstudio.guidenh.guide.compiler.tags; -import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; -import java.util.List; import java.util.Set; -import net.minecraft.util.ResourceLocation; - -import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.document.block.LytList; -import com.hfstudio.guidenh.guide.document.block.LytListItem; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; -import com.hfstudio.guidenh.guide.internal.GuideRegistry; -import com.hfstudio.guidenh.guide.navigation.NavigationNode; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class SubPagesCompiler extends BlockTagCompiler { - public static final Comparator ALPHABETICAL_COMPARATOR = Comparator - .comparing(NavigationNode::title); - @Override public Set getTagNames() { return Collections.singleton("SubPages"); @@ -33,52 +19,22 @@ public Set getTagNames() { protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { var pageIdStr = el.getAttributeString("id", null); var alphabetical = MdxAttrs.getBoolean(compiler, parent, el, "alphabetical", false); + var currentPageId = compiler.getPageId().toString(); - var navigationTree = GuideRegistry.getMergedNavigationTree(); - - List subNodes; - if ("".equals(pageIdStr)) { - subNodes = navigationTree.getRootNodes(); - } else { - ResourceLocation pageId; - try { - pageId = pageIdStr == null ? compiler.getPageId() : compiler.resolveId(pageIdStr); - } catch (Exception e) { - parent.appendError(compiler, "Invalid id", el); - return; - } - - var node = navigationTree.getNodeById(pageId); - if (node == null) { - parent.appendError(compiler, "Couldn't find page " + pageId + " in the navigation tree", el); - return; - } - - subNodes = node.children(); - } - - if (alphabetical) { - subNodes = new ArrayList<>(subNodes); - subNodes.sort(ALPHABETICAL_COMPARATOR); - } - - var list = new LytList(false, 0); - for (var childNode : subNodes) { - if (!childNode.hasPage()) { - continue; - } - - var listItem = new LytListItem(); - var listItemPar = new LytParagraph(); + SubPagesPlaceholder placeholder = new SubPagesPlaceholder(pageIdStr, alphabetical, currentPageId); + parent.append(placeholder); + } - var link = new LytFlowLink(); - link.setGuideLink(childNode.guideId(), PageAnchor.page(childNode.pageId())); - link.appendText(childNode.title()); - listItemPar.append(link); + public static class SubPagesPlaceholder extends LytParagraph { + public final String pageIdStr; + public final boolean alphabetical; + public final String currentPageId; - listItem.append(listItemPar); - list.append(listItem); + public SubPagesPlaceholder(String pageIdStr, boolean alphabetical, String currentPageId) { + this.pageIdStr = pageIdStr; + this.alphabetical = alphabetical; + this.currentPageId = currentPageId; + setStyleClass("SubPages"); } - parent.append(list); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 84374cbd..3aa00be8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -1,18 +1,17 @@ package com.hfstudio.guidenh.guide.compiler.tags.mediawiki; import java.util.Collections; -import java.util.List; import java.util.Set; +import net.minecraft.util.ResourceLocation; + import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.GuidebookText; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiListEntry; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class CategoryCompiler extends BlockTagCompiler { @@ -36,32 +35,33 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl return; } - var context = MediaWikiTagCompilerSupport.createListContext(guide, compiler.getIndex(CategoryIndex.class)); - List entries = MediaWikiPageListBuilder.buildCategoryMembers(context, categoryName.trim()); - parent.append( - MediaWikiTagCompilerSupport.createBlock( - entries, - MediaWikiTagCompilerSupport.readRows(el), - GuidebookText.MediaWikiNoPagesInCategory.text())); + var block = new CategoryPlaceholder( + categoryName.trim(), + MediaWikiTagCompilerSupport.readRows(el), + guide.getId()); + parent.append(block); } @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { String categoryName = el.getAttributeString("name", null); - if (categoryName == null || categoryName.trim() + if (categoryName != null && !categoryName.trim() .isEmpty()) { - return; + sink.appendText(el, categoryName.trim()); + sink.appendBreak(); } + } - var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); - if (guide == null) { - return; - } + private static class CategoryPlaceholder extends LytParagraph { + final String name; + final int rows; + final ResourceLocation guideId; - var context = MediaWikiTagCompilerSupport.createListContext(guide, indexer.getIndex(CategoryIndex.class)); - sink.appendText(el, categoryName); - sink.appendBreak(); - MediaWikiTagCompilerSupport - .indexEntries(sink, el, MediaWikiPageListBuilder.buildCategoryMembers(context, categoryName.trim())); + CategoryPlaceholder(String name, int rows, ResourceLocation guideId) { + this.name = name; + this.rows = rows; + this.guideId = guideId; + setStyleClass("Category"); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index c9e96ae2..ab7df0e9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -3,22 +3,19 @@ import java.util.Collections; import java.util.Set; +import net.minecraft.util.ResourceLocation; + import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; -import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.GuidebookText; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageQuery; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResult; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class SpecialCompiler extends BlockTagCompiler { - private final MediaWikiSpecialPageResolver resolver = new MediaWikiSpecialPageResolver(); - @Override public Set getTagNames() { return Collections.singleton("Special"); @@ -32,45 +29,52 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl parent.appendError(compiler, GuidebookText.MediaWikiMissingSpecialPageName.text(), el); return; } - String specialName = resolver.normalizeSupportedName(rawSpecialName); - if (specialName == null) { - parent.appendError(compiler, GuidebookText.MediaWikiUnsupportedSpecialPage.text(rawSpecialName), el); - return; - } var guide = MediaWikiTagCompilerSupport.resolveGuide(compiler, parent, el); if (guide == null) { return; } - var context = MediaWikiTagCompilerSupport.createListContext(guide, compiler.getIndex(CategoryIndex.class)); - MediaWikiSpecialPageQuery specialQuery = MediaWikiTagCompilerSupport.readSpecialQuery(el); - MediaWikiSpecialPageResult result = resolver - .resolve(context, specialName, specialQuery.withVisibleCount(Integer.MAX_VALUE)); - parent.append( - MediaWikiTagCompilerSupport - .createSpecialBlock(result, MediaWikiTagCompilerSupport.readRows(el), context, specialQuery, resolver)); + var block = new SpecialPlaceholder( + rawSpecialName.trim(), + MediaWikiTagCompilerSupport.readRows(el), + guide.getId(), + el.getAttributeString("page", null), + el.getAttributeString("prefix", null), + el.getAttributeString("language", null), + el.getAttributeString("query", null)); + parent.append(block); } @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { - String specialName = resolver.normalizeSupportedName(el.getAttributeString("name", null)); - if (specialName == null) { - return; + String specialName = el.getAttributeString("name", null); + if (specialName != null && !specialName.trim() + .isEmpty()) { + sink.appendText(el, specialName.trim()); + sink.appendBreak(); } + } - var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); - if (guide == null) { - return; - } + private static class SpecialPlaceholder extends LytParagraph { + final String name; + final int rows; + final ResourceLocation guideId; + final String page; + final String prefix; + final String language; + final String query; - var context = MediaWikiTagCompilerSupport.createListContext(guide, indexer.getIndex(CategoryIndex.class)); - sink.appendText(el, specialName); - sink.appendBreak(); - MediaWikiSpecialPageQuery specialQuery = MediaWikiTagCompilerSupport.readSpecialQuery(el); - MediaWikiTagCompilerSupport.indexSpecialResult( - sink, - el, - resolver.resolve(context, specialName, specialQuery.withVisibleCount(MediaWikiSpecialPageQuery.PAGE_SIZE))); + SpecialPlaceholder(String name, int rows, ResourceLocation guideId, String page, String prefix, + String language, String query) { + this.name = name; + this.rows = rows; + this.guideId = guideId; + this.page = page; + this.prefix = prefix; + this.language = language; + this.query = query; + setStyleClass("Special"); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java index d79c294f..73fb4114 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java @@ -29,6 +29,8 @@ public class LytDocument extends LytNode implements LytBlockContainer { @Nullable private HitTestResult hoveredElement; + private boolean live; + // Cached list of blocks intersecting the last rendered viewport. Invalidated whenever the // block list mutates or the layout is rebuilt; kept across frames otherwise so scrolling at // a steady viewport position only pays the iteration cost once. @@ -109,6 +111,29 @@ public boolean hasLayout() { return layout != null; } + public boolean isLive() { + return live; + } + + public void setLive(boolean live) { + if (this.live == live) return; + this.live = live; + cascadeLive(this); + } + + private static void cascadeLive(LytNode node) { + if (node instanceof LytDocument doc) { + if (doc.live) { + doc.onAttach(); + } else { + doc.onDetach(); + } + } + for (var child : node.getChildren()) { + cascadeLive(child); + } + } + public void invalidateLayout() { layout = null; invalidateVisibleCache(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java new file mode 100644 index 00000000..44339f0d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytImageBlock.java @@ -0,0 +1,87 @@ +package com.hfstudio.guidenh.guide.document.block; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +/** + * A placeholder block for images that will be materialized by a LytScript. + * The styleClass (e.g., "Img" or "FloatingImage") tells LytHost which script handles it. + */ +public class LytImageBlock extends LytParagraph { + + @Nullable + private String src; + @Nullable + private String alt; + @Nullable + private String title; + private int explicitWidth = -1; + private int explicitHeight = -1; + @Nullable + private String align; + private final List annotations = new ArrayList<>(); + + @Nullable + public String getSrc() { + return src; + } + + public void setSrc(@Nullable String src) { + this.src = src; + } + + @Nullable + public String getAlt() { + return alt; + } + + public void setAlt(@Nullable String alt) { + this.alt = alt; + } + + @Nullable + public String getTitle() { + return title; + } + + public void setTitle(@Nullable String title) { + this.title = title; + } + + public int getExplicitWidth() { + return explicitWidth; + } + + public void setExplicitWidth(int explicitWidth) { + this.explicitWidth = explicitWidth > 0 ? explicitWidth : -1; + } + + public int getExplicitHeight() { + return explicitHeight; + } + + public void setExplicitHeight(int explicitHeight) { + this.explicitHeight = explicitHeight > 0 ? explicitHeight : -1; + } + + @Nullable + public String getAlign() { + return align; + } + + public void setAlign(@Nullable String align) { + this.align = align; + } + + public void addAnnotation(ImageRegionAnnotation annotation) { + if (annotation != null) { + annotations.add(annotation); + } + } + + public List getAnnotations() { + return annotations; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java index a0d5448e..796eac8c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java @@ -25,14 +25,28 @@ public abstract class LytNode implements Styleable { @Nullable private MdAstNode sourceNode; + @Nullable + private String id; + + @Nullable + private String nodeUid; + + @Nullable + private String styleClass; + public void removeChild(LytNode node) {} public void replaceChild(LytNode oldChild, LytNode newChild) { // Default: no-op. LytDocument overrides. } + protected void onAttach() {} + + protected void onDetach() {} + public boolean isAttached() { - return getDocument() != null; + LytDocument doc = getDocument(); + return doc != null && doc.isLive(); } public List getChildren() { @@ -160,4 +174,19 @@ public void setHoverStyle(TextStyle style) { public void setSourceNode(@Nullable MdAstNode sourceNode) { this.sourceNode = sourceNode; } + + @Nullable + public String getId() { return id; } + + public void setId(@Nullable String id) { this.id = id; } + + @Nullable + public String getNodeUid() { return nodeUid; } + + public void setNodeUid(@Nullable String nodeUid) { this.nodeUid = nodeUid; } + + @Nullable + public String getStyleClass() { return styleClass; } + + public void setStyleClass(@Nullable String styleClass) { this.styleClass = styleClass; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java index c81e370d..7acb0521 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java @@ -1,5 +1,8 @@ package com.hfstudio.guidenh.guide.document.flow; +import java.util.HashMap; +import java.util.Map; + import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.document.block.LytVisitor; @@ -13,6 +16,11 @@ public class LytFlowContent implements Styleable { private LytFlowParent parent; + @Nullable + private String styleClass; + + private final Map data = new HashMap<>(); + public LytFlowParent getParent() { return parent; } @@ -73,4 +81,15 @@ public final void visit(LytVisitor visitor) { } protected void visitChildren(LytVisitor visitor) {} + + @Nullable + public String getStyleClass() { return styleClass; } + + public void setStyleClass(@Nullable String styleClass) { this.styleClass = styleClass; } + + public Object getData(String key) { return data.get(key); } + + public void setData(String key, Object value) { data.put(key, value); } + + public Map getData() { return data; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevWatcherPump.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevWatcherPump.java deleted file mode 100644 index 51df7b35..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevWatcherPump.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.hfstudio.guidenh.guide.internal; - -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import cpw.mods.fml.common.gameevent.TickEvent; - -public class GuideDevWatcherPump { - - public static final int DEFAULT_INTERVAL_TICKS = 20; - - public interface TickableGuide { - - boolean hasDevelopmentSources(); - - void tickDevelopmentSources(); - } - - private final int intervalTicks; - private int tickCounter; - - public GuideDevWatcherPump(int intervalTicks) { - this.intervalTicks = Math.max(1, intervalTicks); - } - - public static void init() { - if (!hasAnyDevelopmentSources(GuideRegistry.getAll())) { - return; - } - FMLCommonHandler.instance() - .bus() - .register(new GuideDevWatcherPump(DEFAULT_INTERVAL_TICKS)); - } - - @SubscribeEvent - public void onClientTick(TickEvent.ClientTickEvent event) { - if (event.phase != TickEvent.Phase.END) { - return; - } - - tick(GuideRegistry.getAll()); - } - - public void tick(Iterable guides) { - tickCounter++; - if (tickCounter < intervalTicks) { - return; - } - tickCounter = 0; - - for (TickableGuide guide : guides) { - if (guide.hasDevelopmentSources()) { - guide.tickDevelopmentSources(); - } - } - } - - public static boolean hasAnyDevelopmentSources(Iterable guides) { - for (TickableGuide guide : guides) { - if (guide.hasDevelopmentSources()) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index b6b44b8b..12f9d90b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -14,7 +14,6 @@ import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; -import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.datadriven.GuidePageResourceSelector; @@ -84,13 +83,6 @@ public static void reloadGuides(IResourceManager resourceManager) { GuideRegistry.invalidateMergedNavigationTree(); long registryUpdateNs = System.nanoTime() - stageStartedAt; - stageStartedAt = System.nanoTime(); - ClientProxy.getWarmupWorkItem().clearScheduler(); - for (MutableGuide guide : GuideRegistry.getAll()) { - guide.resetWarmup(); - } - long warmupResetNs = System.nanoTime() - stageStartedAt; - stageStartedAt = System.nanoTime(); try { GuideME.getSearch() @@ -107,7 +99,7 @@ public static void reloadGuides(IResourceManager resourceManager) { FMLLog.getLogger() .info( - "[GuideNH] [GuideLightweightReloadService] Guide reload complete, loaded {} guides, {} pages, {} languages in {} ns (dataDrivenLoadNs={}, pageLoadNs={}, registryUpdateNs={}, warmupResetNs={}, searchIndexNs={})", + "[GuideNH] [GuideLightweightReloadService] Guide reload complete, loaded {} guides, {} pages, {} languages in {} ns (dataDrivenLoadNs={}, pageLoadNs={}, registryUpdateNs={}, searchIndexNs={})", guidePages.size(), loadedPageCount, loadedLanguageCount, @@ -115,7 +107,6 @@ public static void reloadGuides(IResourceManager resourceManager) { dataDrivenLoadNs, pageLoadNs, registryUpdateNs, - warmupResetNs, searchIndexNs); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupPump.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupPump.java deleted file mode 100644 index b2be9117..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupPump.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.hfstudio.guidenh.guide.internal; - -import com.hfstudio.guidenh.guide.internal.search.GuideSearch; - -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import cpw.mods.fml.common.gameevent.TickEvent; - -public class GuideWarmupPump { - - private static final GuideWarmupScheduler SCHEDULER = new GuideWarmupScheduler(); - - private long currentTick; - - public static void init() { - FMLCommonHandler.instance() - .bus() - .register(new GuideWarmupPump()); - } - - public static void clearScheduler() { - SCHEDULER.clear(); - } - - @SubscribeEvent - public void onClientTick(TickEvent.ClientTickEvent event) { - if (event.phase != TickEvent.Phase.END) { - return; - } - - currentTick++; - for (MutableGuide guide : GuideRegistry.getAll()) { - guide.populateWarmupScheduler(SCHEDULER, currentTick); - } - SCHEDULER.processTick(currentTick); - - GuideME.getSearch() - .processWork(GuideSearch.BACKGROUND_TIME_PER_TICK); - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupScheduler.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupScheduler.java deleted file mode 100644 index dc075187..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupScheduler.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.hfstudio.guidenh.guide.internal; - -import java.util.ArrayDeque; -import java.util.Deque; - -import cpw.mods.fml.common.FMLLog; - -public class GuideWarmupScheduler { - - public static final long NORMAL_RUNTIME_BUDGET_NANOS = 2_000_000L; - public static final long DEVELOPMENT_VALIDATION_BUDGET_NANOS = 1_000_000L; - - private final Deque highPriorityQueue = new ArrayDeque<>(); - private final Deque normalQueue = new ArrayDeque<>(); - private final Deque validationQueue = new ArrayDeque<>(); - - public void enqueue(GuideWarmupWorkItem item) { - switch (item.kind()) { - case HIGH_PRIORITY_PAGE -> highPriorityQueue.offerLast(item); - case NORMAL_PAGE -> normalQueue.offerLast(item); - case DEV_VALIDATION -> validationQueue.offerLast(item); - } - } - - public void clear() { - int highPriorityCount = highPriorityQueue.size(); - int normalCount = normalQueue.size(); - int validationCount = validationQueue.size(); - highPriorityQueue.clear(); - normalQueue.clear(); - validationQueue.clear(); - if (highPriorityCount > 0 || normalCount > 0 || validationCount > 0) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideWarmupScheduler] Cleared queued warmup work highPriority={}, normal={}, validation={}", - highPriorityCount, - normalCount, - validationCount); - } - } - - public void processTick(long nowTick) { - processQueue(nowTick, NORMAL_RUNTIME_BUDGET_NANOS, false); - processQueue(nowTick, DEVELOPMENT_VALIDATION_BUDGET_NANOS, true); - } - - private void processQueue(long nowTick, long budgetNanos, boolean validationOnly) { - long deadline = System.nanoTime() + budgetNanos; - while (System.nanoTime() < deadline) { - GuideWarmupWorkItem item = pollEligible(nowTick, validationOnly); - if (item == null) { - return; - } - - MutableGuide guide = GuideRegistry.getById(item.guideId()); - if (guide == null) { - continue; - } - - boolean finished = guide.processWarmupWorkItem(item, nowTick); - if (!finished) { - enqueue(item); - } - } - } - - private GuideWarmupWorkItem pollEligible(long nowTick, boolean validationOnly) { - Deque firstQueue = validationOnly ? validationQueue : highPriorityQueue; - Deque secondQueue = validationOnly ? null : normalQueue; - GuideWarmupWorkItem item = pollEligible(firstQueue, nowTick); - if (item != null || secondQueue == null) { - return item; - } - return pollEligible(secondQueue, nowTick); - } - - private GuideWarmupWorkItem pollEligible(Deque queue, long nowTick) { - while (!queue.isEmpty()) { - GuideWarmupWorkItem item = queue.pollFirst(); - if (item.nextEligibleTick() <= nowTick) { - return item; - } - queue.offerLast(item); - return null; - } - return null; - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupWorkItem.java deleted file mode 100644 index 38abe7d9..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideWarmupWorkItem.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.hfstudio.guidenh.guide.internal; - -import net.minecraft.util.ResourceLocation; - -public class GuideWarmupWorkItem { - - public enum Kind { - HIGH_PRIORITY_PAGE, - NORMAL_PAGE, - DEV_VALIDATION - } - - private final ResourceLocation guideId; - private final ResourceLocation pageId; - private final Kind kind; - private int stepIndex; - private long nextEligibleTick; - - public GuideWarmupWorkItem(ResourceLocation guideId, ResourceLocation pageId, Kind kind) { - this.guideId = guideId; - this.pageId = pageId; - this.kind = kind; - } - - public ResourceLocation guideId() { - return guideId; - } - - public ResourceLocation pageId() { - return pageId; - } - - public Kind kind() { - return kind; - } - - public int stepIndex() { - return stepIndex; - } - - public void advanceStep() { - stepIndex++; - } - - public long nextEligibleTick() { - return nextEligibleTick; - } - - public void setNextEligibleTick(long nextEligibleTick) { - this.nextEligibleTick = nextEligibleTick; - } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java index 300c868e..8ed2bebf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java @@ -6,11 +6,9 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -18,7 +16,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.WeakHashMap; import net.minecraft.client.Minecraft; import net.minecraft.util.ResourceLocation; @@ -30,13 +27,11 @@ import com.hfstudio.guidenh.guide.GuideItemSettings; import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.GuidePageChange; -import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.indices.PageIndex; -import com.hfstudio.guidenh.guide.internal.home.GuideScreenHomeHistory; import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; import com.hfstudio.guidenh.guide.internal.util.LangUtil; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiGuideAggregator; @@ -48,9 +43,7 @@ import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageRefreshController; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSyntheticPageFactory; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSyntheticPageFactory.SyntheticSourceSnapshot; -import com.hfstudio.guidenh.guide.navigation.NavigationNode; import com.hfstudio.guidenh.guide.navigation.NavigationTree; -import com.hfstudio.guidenh.guide.scene.SceneTagCompiler; import cpw.mods.fml.common.FMLLog; @@ -59,12 +52,7 @@ * guide-specific subdirectory of resource packs. */ public class MutableGuide - implements Guide, GuideDevWatcherPump.TickableGuide, MediaWikiListContextProvider, AutoCloseable { - - public static final String ACTIVE_CLIENT_WORLD_REQUIRED_MESSAGE = "active client world"; - private static final int MAX_STRONG_RUNTIME_PAGES = 48; - private static final long DEVELOPMENT_VALIDATION_INTERVAL_TICKS = 10L; - private static final long NORMAL_HEAVY_PAGE_WARMUP_DELAY_TICKS = 8L; + implements Guide, MediaWikiListContextProvider, AutoCloseable { private final ResourceLocation id; private final String defaultNamespace; @@ -91,17 +79,6 @@ public class MutableGuide private volatile long fallbackMediaWikiListContextRevision = Long.MIN_VALUE; private volatile long requestedMediaWikiWarmupRevision = Long.MIN_VALUE; private final MediaWikiSpecialPageRefreshController mediaWikiRefreshController = new MediaWikiSpecialPageRefreshController(); - private final Map compiledPagesWeak = Collections.synchronizedMap(new WeakHashMap<>()); - private final LinkedHashMap compiledPagesStrong = new LinkedHashMap( - 64, - 0.75f, - true) { - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_STRONG_RUNTIME_PAGES; - } - }; private final ExtensionCollection extensions; private final boolean availableToOpenHotkey; private final GuideItemSettings itemSettings; @@ -114,8 +91,6 @@ protected boolean removeEldestEntry(Map.Entry eldes @Nullable private GuideSourceWatcher watcher; - private long nextValidationTick; - private long lastSchedulerTick; public MutableGuide(ResourceLocation id, String defaultNamespace, String folder, String defaultLanguage, @Nullable Path developmentSourceFolder, @Nullable String developmentSourceNamespace, @@ -209,17 +184,7 @@ public GuidePage getPage(ResourceLocation id) { GuidePage compiledPage; try { - synchronized (compiledPagesWeak) { - compiledPage = compiledPagesStrong.get(id); - if (compiledPage == null) { - compiledPage = compiledPagesWeak.get(parsedPage); - } - if (compiledPage == null) { - compiledPage = PageCompiler.compile(this, extensions, parsedPage); - } - compiledPagesWeak.put(parsedPage, compiledPage); - compiledPagesStrong.put(id, compiledPage); - } + compiledPage = PageCompiler.compile(this, extensions, parsedPage); clearCompileFailure(id); } catch (Throwable t) { recordCompileFailure(id, buildCompileFailureText(id, t)); @@ -364,22 +329,11 @@ public void watchDevelopmentSources() { .addShutdownHook(new Thread(watcher::close)); } - @Override - public boolean hasDevelopmentSources() { - return watcher != null; - } - @Override public synchronized void close() { int developmentPageCount = developmentPages.size(); int syntheticPageCount = syntheticPages.size(); int failureCount = pageFailures.size(); - int weakCompiledCount; - int strongCompiledCount; - synchronized (compiledPagesWeak) { - weakCompiledCount = compiledPagesWeak.size(); - strongCompiledCount = compiledPagesStrong.size(); - } if (watcher != null) { watcher.close(); watcher = null; @@ -394,54 +348,13 @@ public synchronized void close() { mediaWikiSpecialDataIndex = null; fallbackMediaWikiListContextRevision = Long.MIN_VALUE; requestedMediaWikiWarmupRevision = Long.MIN_VALUE; - synchronized (compiledPagesWeak) { - compiledPagesWeak.clear(); - compiledPagesStrong.clear(); - } - warmupPageQueue.clear(); - queuedWarmupPages.clear(); - prioritizedWarmupPages.clear(); - scheduledWarmupPages.clear(); - validationPageQueue.clear(); - queuedValidationPages.clear(); - scheduledValidationPages.clear(); FMLLog.getLogger() .info( - "[GuideNH] [MutableGuide] Closed guide {} and cleared caches developmentPages={}, syntheticPages={}, failures={}, compiledWeak={}, compiledStrong={}", + "[GuideNH] [MutableGuide] Closed guide {} and cleared caches developmentPages={}, syntheticPages={}, failures={}", id, developmentPageCount, syntheticPageCount, - failureCount, - weakCompiledCount, - strongCompiledCount); - } - - private final Deque warmupPageQueue = new ArrayDeque<>(); - private final HashSet queuedWarmupPages = new HashSet<>(); - private final HashSet prioritizedWarmupPages = new HashSet<>(); - private final HashSet scheduledWarmupPages = new HashSet<>(); - private final Deque validationPageQueue = new ArrayDeque<>(); - private final HashSet queuedValidationPages = new HashSet<>(); - private final HashSet scheduledValidationPages = new HashSet<>(); - - public void resetWarmup() { - rebuildWarmupQueue(); - } - - public void tick() { - if (pages == null || watcher == null) { - return; // Do nothing while pages haven't been loaded yet - } - - var changes = watcher.takeChanges(); - if (!changes.isEmpty()) { - applyChanges(changes); - } - } - - @Override - public void tickDevelopmentSources() { - tick(); + failureCount); } private void applyChanges(List changes) { @@ -468,9 +381,6 @@ private void applyChanges(List changes) { } else { developmentPages.remove(pageId); } - removeCompiledPage(pageId); - prioritizeWarmupPage(pageId); - queueValidationPage(pageId); deduplicatedChanges .set(i, new GuidePageChange(change.language(), pageId, initialPages.get(pageId), newPage)); @@ -495,7 +405,6 @@ private void applyChanges(List changes) { this.navigationTree = buildNavigation(); GuideRegistry.invalidateMergedNavigationTree(); refreshPageFailures(); - requestValidationRefresh(0L); // Reload the current page if it has been changed var guideScreen = GuideScreen.current(); @@ -550,15 +459,6 @@ public void setPages(Map pages, boolean inval this.pages = Collections.unmodifiableMap(new HashMap<>(pages)); this.syntheticPages = Collections.emptyMap(); invalidateMediaWikiDerivedCaches(); - synchronized (compiledPagesWeak) { - compiledPagesWeak.clear(); - compiledPagesStrong.clear(); - } - scheduledWarmupPages.clear(); - scheduledValidationPages.clear(); - validationPageQueue.clear(); - queuedValidationPages.clear(); - nextValidationTick = 0L; if (watcher != null) { watcher.clearChanges(); // Since we'll load them all now, ignore all changes up to now @@ -575,7 +475,6 @@ public void setPages(Map pages, boolean inval GuideRegistry.invalidateMergedNavigationTree(); } refreshPageFailures(); - rebuildWarmupQueue(); // Do not eagerly compile pages here. Some packs register or rewrite recipes // during FMLLoadComplete, after the initial resource reload has already parsed the guide. // Deferring compilation until first display avoids caching stale "missing recipe" error blocks. @@ -598,10 +497,6 @@ public void stageEditorPage(ParsedGuidePage parsedPage) { ResourceLocation pageId = parsedPage.getId(); developmentPages.put(pageId, parsedPage); invalidateMediaWikiDerivedCaches(); - removeCompiledPage(pageId); - prioritizeWarmupPage(pageId); - queueValidationPage(pageId); - requestValidationRefresh(0L); if (parsedPage.hasParseFailure()) { recordParseFailure(parsedPage); } else { @@ -610,182 +505,7 @@ public void stageEditorPage(ParsedGuidePage parsedPage) { } } - private void removeCompiledPage(ResourceLocation pageId) { - synchronized (compiledPagesWeak) { - compiledPagesStrong.remove(pageId); - compiledPagesWeak.keySet() - .removeIf(page -> page != null && pageId.equals(page.getId())); - } - scheduledWarmupPages.remove(pageId); - prioritizedWarmupPages.remove(pageId); - queuedWarmupPages.remove(pageId); - warmupPageQueue.remove(pageId); - scheduledValidationPages.remove(pageId); - queuedValidationPages.remove(pageId); - validationPageQueue.remove(pageId); - } - - private void rebuildWarmupQueue() { - warmupPageQueue.clear(); - queuedWarmupPages.clear(); - prioritizedWarmupPages.clear(); - queueWarmupPage(resolveRememberedWarmupPageId(), true); - queueWarmupBookmarkedPages(); - queueWarmupHistoryPages(); - ResourceLocation firstNavigationPage = resolveWarmupPageId(); - if (firstNavigationPage != null) { - queueWarmupPage(firstNavigationPage, true); - } - if (pages != null) { - for (ParsedGuidePage parsedPage : pages.values()) { - queueWarmupPage(parsedPage.getId()); - } - } - for (ParsedGuidePage parsedPage : developmentPages.values()) { - queueWarmupPage(parsedPage.getId()); - } - } - - private void prioritizeWarmupPage(ResourceLocation pageId) { - queueWarmupPage(pageId, true); - } - - @Nullable - private ResourceLocation pollWarmupCandidate() { - while (!warmupPageQueue.isEmpty()) { - ResourceLocation pageId = warmupPageQueue.poll(); - if (pageId == null) { - return null; - } - queuedWarmupPages.remove(pageId); - if (!compiledPageExists(pageId) && getParsedPage(pageId) != null) { - return pageId; - } - } - return null; - } - - private void queueWarmupPage(@Nullable ResourceLocation pageId) { - queueWarmupPage(pageId, false); - } - - private void queueWarmupPage(@Nullable ResourceLocation pageId, boolean highPriority) { - if (pageId == null || compiledPageExists(pageId)) { - return; - } - if (highPriority) { - prioritizedWarmupPages.add(pageId); - } - if (!queuedWarmupPages.add(pageId)) { - if (highPriority) { - warmupPageQueue.remove(pageId); - warmupPageQueue.offerFirst(pageId); - } - return; - } - if (highPriority) { - warmupPageQueue.offerFirst(pageId); - } else { - warmupPageQueue.offerLast(pageId); - } - } - - @Nullable - private ResourceLocation resolveRememberedWarmupPageId() { - GuideScreenViewState state = GuideScreenMemory.recallLastContentState(); - if (state == null || state.route() == null - || state.route() - .guideId() == null - || !id.equals( - state.route() - .guideId())) { - return null; - } - PageAnchor anchor = state.route() - .anchor(); - return anchor != null ? anchor.pageId() : null; - } - - private void queueWarmupBookmarkedPages() { - for (ResourceLocation pageId : GuideBookmarkState.getSharedInstance() - .getBookmarksView()) { - if (pageExists(pageId)) { - queueWarmupPage(pageId, true); - } - } - } - - private void queueWarmupHistoryPages() { - for (GuideScreenHomeHistory.Entry entry : GuideScreenHomeHistory.shared() - .snapshot()) { - if (id.equals(entry.guideId()) && pageExists(entry.pageId())) { - queueWarmupPage(entry.pageId(), true); - } - } - } - - private boolean compiledPageExists(ResourceLocation pageId) { - ParsedGuidePage parsedPage = getParsedPage(pageId); - if (parsedPage == null) { - return false; - } - synchronized (compiledPagesWeak) { - return compiledPagesStrong.containsKey(pageId) || compiledPagesWeak.containsKey(parsedPage); - } - } - - public void populateWarmupScheduler(GuideWarmupScheduler scheduler, long nowTick) { - if (pages == null) { - return; - } - lastSchedulerTick = nowTick; - GuideScreen guideScreen = GuideScreen.current(); - if (guideScreen != null && guideScreen.isShowingGuide(id)) { - prioritizeWarmupPage(guideScreen.getCurrentPageId()); - } - if (Minecraft.getMinecraft() - .getNetHandler() != null) { - flushWarmupItems(scheduler); - } - if (shouldUseDevelopmentValidation()) { - flushValidationItems(scheduler); - } - } - - public boolean processWarmupWorkItem(GuideWarmupWorkItem item, long nowTick) { - return switch (item.kind()) { - case HIGH_PRIORITY_PAGE, NORMAL_PAGE -> processPageWarmup(item, nowTick); - case DEV_VALIDATION -> processDevelopmentValidation(item, nowTick); - }; - } - - private void flushWarmupItems(GuideWarmupScheduler scheduler) { - ResourceLocation pageId; - while ((pageId = pollWarmupCandidate()) != null) { - if (!scheduledWarmupPages.add(pageId)) { - continue; - } - GuideWarmupWorkItem.Kind kind = prioritizedWarmupPages.remove(pageId) - ? GuideWarmupWorkItem.Kind.HIGH_PRIORITY_PAGE - : GuideWarmupWorkItem.Kind.NORMAL_PAGE; - scheduler.enqueue(new GuideWarmupWorkItem(id, pageId, kind)); - } - } - - private void flushValidationItems(GuideWarmupScheduler scheduler) { - while (!validationPageQueue.isEmpty()) { - ResourceLocation pageId = validationPageQueue.pollFirst(); - if (pageId == null) { - return; - } - queuedValidationPages.remove(pageId); - if (scheduledValidationPages.add(pageId)) { - GuideWarmupWorkItem item = new GuideWarmupWorkItem(id, pageId, GuideWarmupWorkItem.Kind.DEV_VALIDATION); - item.setNextEligibleTick(nextValidationTick); - scheduler.enqueue(item); - } - } - } + // All warmup-related methods removed in Wave 3 cleanup public void rebuildEditorNavigationState() { rebuildIndices(); @@ -812,33 +532,6 @@ public String getDefaultLanguage() { return defaultLanguage; } - @Nullable - private ResourceLocation resolveWarmupPageId() { - if (navigationTree != null) { - ResourceLocation pageId = findFirstNavigationPageId(navigationTree.getRootNodes()); - if (pageId != null) { - return pageId; - } - } - return pages != null && !pages.isEmpty() ? pages.keySet() - .iterator() - .next() : null; - } - - @Nullable - private ResourceLocation findFirstNavigationPageId(List nodes) { - for (var node : nodes) { - if (node.hasPage() && node.pageId() != null) { - return node.pageId(); - } - ResourceLocation childPageId = findFirstNavigationPageId(node.children()); - if (childPageId != null) { - return childPageId; - } - } - return null; - } - private boolean canLoadDevelopmentSource(ResourceLocation id) { if (developmentSourceFolder == null) { return false; @@ -867,148 +560,6 @@ private GuideDevelopmentSourceLayout detectDevelopmentSourceLayout(@Nullable Pat return GuideDevelopmentSourceLayout.detect(sourceFolder, folder); } - private boolean processPageWarmup(GuideWarmupWorkItem item, long nowTick) { - ResourceLocation pageId = item.pageId(); - ParsedGuidePage parsedPage = getParsedPage(pageId); - if (parsedPage == null) { - scheduledWarmupPages.remove(pageId); - return true; - } - - try { - if (item.stepIndex() == 0 && SceneTagCompiler.likelyHasHeavySceneWork(parsedPage)) { - item.advanceStep(); - item.setNextEligibleTick( - nowTick + (item.kind() == GuideWarmupWorkItem.Kind.HIGH_PRIORITY_PAGE ? 1L - : NORMAL_HEAVY_PAGE_WARMUP_DELAY_TICKS)); - return false; - } - if (!warmPage(pageId, parsedPage)) { - item.setNextEligibleTick(nowTick + 1L); - return false; - } - } catch (Throwable t) { - scheduledWarmupPages.remove(pageId); - if (!isDeferrableWarmPageFailure(t)) { - FMLLog.getLogger() - .error("[GuideNH] [MutableGuide] Failed to pre-warm guide page {}", pageId, t); - } - return true; - } - - scheduledWarmupPages.remove(pageId); - return true; - } - - private boolean warmPage(ResourceLocation pageId, ParsedGuidePage parsedPage) { - synchronized (compiledPagesWeak) { - GuidePage compiledPage = compiledPagesStrong.get(pageId); - if (compiledPage == null) { - compiledPage = compiledPagesWeak.get(parsedPage); - } - if (compiledPage != null) { - compiledPagesStrong.put(pageId, compiledPage); - return true; - } - try { - compiledPage = PageCompiler.compile(this, extensions, parsedPage); - } catch (RuntimeException e) { - if (isDeferrableWarmPageFailure(e)) { - FMLLog.getLogger() - .debug( - "[GuideNH] [MutableGuide] Deferring warm compilation for page {} until an active client world is available", - pageId); - return false; - } - throw e; - } - compiledPagesWeak.put(parsedPage, compiledPage); - compiledPagesStrong.put(pageId, compiledPage); - clearCompileFailure(pageId); - return true; - } - } - - private boolean processDevelopmentValidation(GuideWarmupWorkItem item, long nowTick) { - ResourceLocation pageId = item.pageId(); - if (!shouldUseDevelopmentValidation()) { - scheduledValidationPages.remove(pageId); - return true; - } - if (nowTick < nextValidationTick) { - item.setNextEligibleTick(nextValidationTick); - return false; - } - - try { - ParsedGuidePage parsedPage = getParsedPage(pageId); - if (parsedPage == null) { - return true; - } - if (parsedPage.hasParseFailure()) { - recordParseFailure(parsedPage); - return true; - } - PageCompiler.compile(this, extensions, parsedPage); - clearCompileFailure(pageId); - } catch (Throwable t) { - if (!isDeferrableWarmPageFailure(t)) { - recordCompileFailure(pageId, buildCompileFailureText(pageId, t)); - FMLLog.getLogger() - .error("[GuideNH] [MutableGuide] Failed to validate guide page {}", pageId, t); - } - } finally { - scheduledValidationPages.remove(pageId); - } - return true; - } - - private void queueValidationPage(@Nullable ResourceLocation pageId) { - if (pageId == null || !queuedValidationPages.add(pageId) || scheduledValidationPages.contains(pageId)) { - return; - } - validationPageQueue.offerLast(pageId); - } - - private void queueValidationForAllPages() { - for (ParsedGuidePage parsedPage : getAllParsedPages().values()) { - if (!parsedPage.hasParseFailure()) { - queueValidationPage(parsedPage.getId()); - } - } - } - - private void requestValidationRefresh(long nowTick) { - long referenceTick = nowTick > 0L ? nowTick : lastSchedulerTick; - long requestedTick = referenceTick + DEVELOPMENT_VALIDATION_INTERVAL_TICKS; - if (nextValidationTick == 0L) { - nextValidationTick = requestedTick; - } else { - nextValidationTick = Math.min(nextValidationTick, requestedTick); - } - } - - private boolean shouldUseDevelopmentValidation() { - return watcher != null || isGuideEditorRuntimeActive(); - } - - private boolean isGuideEditorRuntimeActive() { - GuideScreen guideScreen = GuideScreen.current(); - return guideScreen != null && guideScreen.isShowingGuide(id) && guideScreen.isEditorActive(); - } - - static boolean isDeferrableWarmPageFailure(Throwable throwable) { - for (Throwable current = throwable; current != null; current = current.getCause()) { - if ((current instanceof IllegalStateException || current instanceof IllegalArgumentException) - && current.getMessage() != null - && current.getMessage() - .contains(ACTIVE_CLIENT_WORLD_REQUIRED_MESSAGE)) { - return true; - } - } - return false; - } - private void refreshPageFailures() { Map allParsedPages = getAllParsedPages(); pageFailures.keySet() @@ -1021,10 +572,6 @@ private void refreshPageFailures() { clearParseFailure(parsedPage.getId()); } } - if (shouldUseDevelopmentValidation()) { - requestValidationRefresh(0L); - queueValidationForAllPages(); - } } private Map getAllParsedPages() { @@ -1066,10 +613,6 @@ private void rebuildSyntheticPages() { syntheticSourceCache, this::parseSyntheticPage); syntheticPages = Collections.unmodifiableMap(rebuiltPages); - previousSyntheticIds.addAll(syntheticPages.keySet()); - for (ResourceLocation syntheticPageId : previousSyntheticIds) { - removeCompiledPage(syntheticPageId); - } FMLLog.getLogger() .info( "[GuideNH] [MutableGuide] Rebuilt {} synthetic pages in {} ms for guide {}", diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java index 0457cd80..c3a161cb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/EventType.java @@ -13,5 +13,6 @@ public enum EventType { STUB_EXPANDED, LAYOUT_INVALIDATED, ATTACHED, - DETACHED + DETACHED, + MOUNT } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index b18db813..5efae0b0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -2,9 +2,14 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; @@ -12,6 +17,17 @@ public class LytHost { @Nullable private LytDocument document; + private final Map scripts = new HashMap<>(); + private final Map cachedDocuments = new LinkedHashMap<>(); + private final Map pageNodeCounters = new HashMap<>(); + String currentPageId; + + static class PageCacheEntry { + final LytDocument document; + final Map nodeResults = new HashMap<>(); + PageCacheEntry(LytDocument document) { this.document = document; } + } + private final ViewportState viewport = new ViewportState(); private final NavigationState nav = new NavigationState(); private final Deque eventQueue = new ArrayDeque<>(); @@ -19,10 +35,16 @@ public class LytHost { // ===== Document ===== - public void setDocument(@Nullable LytDocument doc) { - this.document = doc; - if (doc != null) { - viewport.updateContent(doc.getAvailableWidth(), doc.getContentHeight()); + public void setDocument(@Nullable LytDocument newDoc) { + if (this.document != null && this.document != newDoc) { + this.document.setLive(false); // onDetach cascade on old doc + } + this.document = newDoc; + if (newDoc != null) { + allocateNodeUids(newDoc); + newDoc.setLive(true); // onAttach cascade — this triggers everything + dispatchMountEvents(newDoc); // MOUNT events for styleClass nodes + viewport.updateContent(newDoc.getAvailableWidth(), newDoc.getContentHeight()); } } @@ -30,6 +52,73 @@ public void setDocument(@Nullable LytDocument doc) { public ViewportState getViewport() { return viewport; } public NavigationState getNavigation() { return nav; } + public void registerScript(String styleClass, LytScript script) { + scripts.put(styleClass, script); + } + + @Nullable + public PageCacheEntry getCachedPage(String pageId) { + return cachedDocuments.get(pageId); + } + + public void cachePage(String pageId, LytDocument compiledDoc) { + cachedDocuments.put(pageId, new PageCacheEntry(compiledDoc)); + } + + public void invalidatePage(String pageId) { + cachedDocuments.remove(pageId); + pageNodeCounters.remove(pageId); + } + + public void setCurrentPageId(String pageId) { + this.currentPageId = pageId; + } + + public boolean hasPreheatWork() { + return false; // placeholder, real impl later + } + + public void preheatStep(long deadlineNs) { + // placeholder, real impl later + } + + String allocateNodeUid(String pageId, String prefix) { + int seq = pageNodeCounters + .computeIfAbsent(pageId, k -> new AtomicInteger()).incrementAndGet(); + return pageId + "::" + prefix + ":" + seq; + } + + private void allocateNodeUids(LytNode node) { + if (node.getStyleClass() != null && node.getNodeUid() == null) { + String prefix = node.getStyleClass().toLowerCase(); + int seq = pageNodeCounters + .computeIfAbsent(currentPageId, k -> new AtomicInteger()).incrementAndGet(); + node.setNodeUid(currentPageId + "::" + prefix + ":" + seq); + } + for (var child : node.getChildren()) { + allocateNodeUids(child); + } + } + + private void dispatchMountEvents(LytNode node) { + String cls = node.getStyleClass(); + if (cls != null) { + LytScript script = scripts.get(cls); + if (script != null) { + try { + ScriptContextImpl ctx = new ScriptContextImpl(node, this, document); + script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); + } catch (Exception e) { + // Error boundary: log and continue + e.printStackTrace(); + } + } + } + for (var child : node.getChildren()) { + dispatchMountEvents(child); + } + } + // ===== Sync events ===== public void pushEvent(LytEvent event) { @@ -94,6 +183,10 @@ public int pendingTaskCount() { public void clear() { document = null; + scripts.clear(); + cachedDocuments.clear(); + pageNodeCounters.clear(); + currentPageId = null; eventQueue.clear(); taskQueue.clear(); nav.clear(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java new file mode 100644 index 00000000..68ac1adc --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java @@ -0,0 +1,9 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import com.hfstudio.guidenh.guide.document.block.LytNode; + +public interface LytScript { + ScriptType type(); + String styleClass(); + void onEvent(LytNode node, LytEvent event, ScriptContext ctx); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java new file mode 100644 index 00000000..908ca246 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java @@ -0,0 +1,12 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import java.util.Map; +import com.hfstudio.guidenh.guide.document.block.LytDocument; +import com.hfstudio.guidenh.guide.document.block.LytNode; + +public interface ScriptContext { + Map data(); + void replace(LytNode newNode); + String allocateId(String prefix); + LytDocument document(); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java new file mode 100644 index 00000000..dd0a8cf2 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -0,0 +1,38 @@ +package com.hfstudio.guidenh.guide.internal.host; + +import java.util.HashMap; +import java.util.Map; +import com.hfstudio.guidenh.guide.document.block.LytDocument; +import com.hfstudio.guidenh.guide.document.block.LytNode; + +class ScriptContextImpl implements ScriptContext { + private final Map data = new HashMap<>(); + private final LytNode node; + private final LytHost host; + private final LytDocument document; + + ScriptContextImpl(LytNode node, LytHost host, LytDocument document) { + this.node = node; + this.host = host; + this.document = document; + } + + @Override + public Map data() { return data; } + + @Override + public void replace(LytNode newNode) { + LytNode parent = node.getParent(); + if (parent != null) { + parent.replaceChild(node, newNode); + } + } + + @Override + public String allocateId(String prefix) { + return prefix + ":" + System.identityHashCode(node); + } + + @Override + public LytDocument document() { return document; } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptType.java new file mode 100644 index 00000000..543e4ffe --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptType.java @@ -0,0 +1,7 @@ +package com.hfstudio.guidenh.guide.internal.host; + +public enum ScriptType { + JAVA, + LUA, + JAVASCRIPT +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java new file mode 100644 index 00000000..9bde6677 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class BlockImageScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "BlockImage"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: scene creation will be wired here + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java new file mode 100644 index 00000000..986e02b0 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class CategoryScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "Category"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java new file mode 100644 index 00000000..5de182ff --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class CommandLinkScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "CommandLink"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java new file mode 100644 index 00000000..9ed0140b --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java @@ -0,0 +1,20 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.document.block.LytNode; + +public class CsvTableScript implements LytScript { + @Override + public ScriptType type() { return ScriptType.JAVA; } + + @Override + public String styleClass() { return "CsvTable"; } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: CSV loading and table building will be wired later + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java new file mode 100644 index 00000000..6b1caffa --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -0,0 +1,33 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.document.block.LytNode; + +/** + * Script that materializes floating image content for blocks with styleClass "FloatingImage". + * For Phase 3 initial implementation, this is a stub that will be wired to + * the asset loading system in a later task. + */ +public class FloatingImageScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "FloatingImage"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // TODO: Load image asset and replace placeholder + // For Phase 3 initial implementation, this is a stub. + // The actual image loading will be added when the script infrastructure + // is fully wired to the asset loading system. + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java new file mode 100644 index 00000000..cbe30e63 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -0,0 +1,33 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.document.block.LytNode; + +/** + * Script that materializes image content for blocks with styleClass "Img". + * For Phase 3 initial implementation, this is a stub that will be wired to + * the asset loading system in a later task. + */ +public class ImageScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "Img"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // TODO: Load image asset and replace placeholder + // For Phase 3 initial implementation, this is a stub. + // The actual image loading will be added when the script infrastructure + // is fully wired to the asset loading system. + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java new file mode 100644 index 00000000..2e93b837 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class ItemGridScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "ItemGrid"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java new file mode 100644 index 00000000..80052bab --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class ItemImageScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "ItemImage"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java new file mode 100644 index 00000000..9039c9af --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class ItemLinkScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "ItemLink"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java new file mode 100644 index 00000000..e8f10bce --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class KeyBindScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "KeyBind"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java new file mode 100644 index 00000000..c7e07162 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -0,0 +1,20 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.document.block.LytNode; + +public class MermaidScript implements LytScript { + @Override + public ScriptType type() { return ScriptType.JAVA; } + + @Override + public String styleClass() { return "Mermaid"; } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: Mermaid rendering will be wired later + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java new file mode 100644 index 00000000..a81d5c0c --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class PlayerNameScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "PlayerName"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java new file mode 100644 index 00000000..06d70cdf --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class RecipeScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "Recipe"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full NEI query chain will be wired here + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java new file mode 100644 index 00000000..2a0eb582 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class SceneScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "Scene"; // handles both Scene and GameScene + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: entire scene pipeline will be wired here + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java new file mode 100644 index 00000000..c8296bc6 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class SoundLinkScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "SoundLink"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java new file mode 100644 index 00000000..cb78a827 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class SpecialScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "Special"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java new file mode 100644 index 00000000..17ffa6d7 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class StructureScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "Structure"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java new file mode 100644 index 00000000..9ed1d19c --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -0,0 +1,25 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class SubPagesScript implements LytScript { + + @Override + public ScriptType type() { + return ScriptType.JAVA; + } + + @Override + public String styleClass() { + return "SubPages"; + } + + @Override + public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + // Stub: full implementation in later task + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java index 2f5b7db3..1f4291d2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java @@ -1,32 +1,17 @@ package com.hfstudio.guidenh.guide.internal.scheduler; -import com.hfstudio.guidenh.guide.internal.GuideDevWatcherPump; -import com.hfstudio.guidenh.guide.internal.GuideRegistry; -import com.hfstudio.guidenh.guide.internal.MutableGuide; - public class DevWatchWorkItem implements WorkItem { - private static final int INTERVAL_TICKS = 20; - private int tickCounter; - @Override public Priority priority() { return Priority.LOW; } @Override public boolean shouldRun() { - tickCounter++; - if (tickCounter < INTERVAL_TICKS) return false; - tickCounter = 0; - return GuideDevWatcherPump.hasAnyDevelopmentSources(GuideRegistry.getAll()); + return false; } @Override public WorkResult tick(long deadlineNs) { - for (MutableGuide guide : GuideRegistry.getAll()) { - if (guide.hasDevelopmentSources()) { - guide.tickDevelopmentSources(); - } - } return WorkResult.DONE; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java new file mode 100644 index 00000000..9339da66 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java @@ -0,0 +1,32 @@ +package com.hfstudio.guidenh.guide.internal.scheduler; + +import com.hfstudio.guidenh.guide.internal.host.LytHost; + +public class LytHostPreheatItem implements WorkItem { + private final LytHost host; + + public LytHostPreheatItem(LytHost host) { + this.host = host; + } + + @Override + public Priority priority() { return Priority.MEDIUM; } + + @Override + public boolean shouldRun() { return false; } + + @Override + public WorkResult tick(long deadlineNs) { + return WorkResult.DONE; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof LytHostPreheatItem; + } + + @Override + public int hashCode() { + return LytHostPreheatItem.class.hashCode(); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java deleted file mode 100644 index 49f0d594..00000000 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WarmupWorkItem.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.hfstudio.guidenh.guide.internal.scheduler; - -import com.hfstudio.guidenh.guide.internal.GuideRegistry; -import com.hfstudio.guidenh.guide.internal.GuideWarmupScheduler; -import com.hfstudio.guidenh.guide.internal.MutableGuide; - -public class WarmupWorkItem implements WorkItem { - - private final GuideWarmupScheduler scheduler; - private long currentTick; - - public WarmupWorkItem() { - this.scheduler = new GuideWarmupScheduler(); - } - - @Override - public Priority priority() { return Priority.LOW; } - - @Override - public boolean shouldRun() { - for (MutableGuide guide : GuideRegistry.getAll()) { - if (guide.hasDevelopmentSources()) return true; - } - return true; // Always check — guides may have pages to warm up - } - - @Override - public WorkResult tick(long deadlineNs) { - currentTick++; - for (MutableGuide guide : GuideRegistry.getAll()) { - guide.populateWarmupScheduler(scheduler, currentTick); - } - scheduler.processTick(currentTick); - // Low priority — always yield so scheduler can run higher-priority items - return WorkResult.DONE; - } - - public void clearScheduler() { - scheduler.clear(); - } - - @Override - public boolean equals(Object o) { - return o instanceof WarmupWorkItem; - } - - @Override - public int hashCode() { return WarmupWorkItem.class.hashCode(); } -} diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 7d384f9d..5251737e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -2,47 +2,29 @@ import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Supplier; - -import net.minecraft.item.ItemStack; import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.config.ModConfig; -import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; -import com.hfstudio.guidenh.guide.document.LytErrorSink; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; -import com.hfstudio.guidenh.guide.scene.annotation.compiler.AnnotationTagCompiler; -import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCache; -import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCacheEntry; -import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCacheKey; -import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureFingerprintResolver; import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler; -import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; -import com.hfstudio.guidenh.integration.structurelib.StructureLibPreviewSelection; -import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneMetadata; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; -import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.unist.UnistNode; import com.hfstudio.guidenh.libs.unist.UnistParent; public class SceneTagCompiler extends BlockTagCompiler { - public static final LytErrorSink NOOP_ERROR_SINK = (compiler, text, node) -> {}; private static final String[] SCENE_ROOT_TAG_NAMES = { "GameScene", "Scene" }; private static final String[] SCENE_HEAVY_TAG_NAMES = { "ImportStructure", "ImportStructureLib", "PlaceBlock", "RemoveBlocks", "RemoveEntity", "ReplaceBlock", "Block", "Entity" }; @@ -51,6 +33,9 @@ public class SceneTagCompiler extends BlockTagCompiler { private Map elementCompilers = Collections.emptyMap(); private final GuideSceneStructureFingerprintResolver structureFingerprintResolver = new GuideSceneStructureFingerprintResolver(); + private static final int DEFAULT_WIDTH = 320; + private static final int DEFAULT_HEIGHT = 180; + public static boolean likelyHasHeavySceneWork(@Nullable ParsedGuidePage parsedPage) { return parsedPage != null && likelyHasHeavySceneWork(parsedPage.getSource()); } @@ -68,7 +53,6 @@ public static boolean likelyHasHeavySceneWork(@Nullable String sourceText) { return true; } - // Keep this source-only heuristic narrow so warmup can prioritize obvious scene-heavy pages cheaply. return countTags(sourceText, SCENE_HEAVY_TAG_NAMES) >= SCENE_HEAVY_ELEMENT_THRESHOLD; } @@ -93,577 +77,147 @@ public void onExtensionsBuilt(ExtensionCollection extensions) { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { - var scene = new LytGuidebookScene(); - // Detect whether width/height are explicitly set so auto-size can kick in when omitted. + // Width and height String rawWidth = MdxAttrs.getString(compiler, parent, el, "width", null); String rawHeight = MdxAttrs.getString(compiler, parent, el, "height", null); boolean explicitWidth = rawWidth != null; boolean explicitHeight = rawHeight != null; - int w = MdxAttrs.getInt(compiler, parent, el, "width", LytGuidebookScene.DEFAULT_WIDTH); - int h = MdxAttrs.getInt(compiler, parent, el, "height", LytGuidebookScene.DEFAULT_HEIGHT); - scene.setSceneSize(w, h); + int w = MdxAttrs.getInt(compiler, parent, el, "width", DEFAULT_WIDTH); + int h = MdxAttrs.getInt(compiler, parent, el, "height", DEFAULT_HEIGHT); + // Zoom float zoom = MdxAttrs.getFloat(compiler, parent, el, "zoom", Float.NaN); boolean explicitZoom = !Float.isNaN(zoom); - if (explicitZoom) { - scene.getCamera() - .setZoom(zoom); - } - // Camera preset (yaw/pitch/roll), applied before explicit rotateX/Y/Z overrides. + // Camera preset (yaw/pitch/roll) String perspective = MdxAttrs.getString(compiler, parent, el, "perspective", null); - if (perspective != null && !perspective.isEmpty()) { - scene.getCamera() - .setPerspectivePreset(PerspectivePreset.fromSerializedName(perspective)); - } + // Rotation overrides float rx = MdxAttrs.getFloat(compiler, parent, el, "rotateX", Float.NaN); float ry = MdxAttrs.getFloat(compiler, parent, el, "rotateY", Float.NaN); float rz = MdxAttrs.getFloat(compiler, parent, el, "rotateZ", Float.NaN); - if (!Float.isNaN(rx)) scene.getCamera() - .setRotationX(rx); - if (!Float.isNaN(ry)) scene.getCamera() - .setRotationY(ry); - if (!Float.isNaN(rz)) scene.getCamera() - .setRotationZ(rz); - - // Pan offsets (screen-space), applied on top of the preset / rotations. + + // Pan offsets (screen-space) float offX = MdxAttrs.getFloat(compiler, parent, el, "offsetX", Float.NaN); float offY = MdxAttrs.getFloat(compiler, parent, el, "offsetY", Float.NaN); boolean explicitOffX = !Float.isNaN(offX); boolean explicitOffY = !Float.isNaN(offY); - if (explicitOffX) scene.getCamera() - .setOffsetX(offX); - if (explicitOffY) scene.getCamera() - .setOffsetY(offY); - // Explicit world-space rotation center. If any of the 3 coords is given, we override the - // auto-center computed later from level bounds. + // World-space rotation center float centerX = MdxAttrs.getFloat(compiler, parent, el, "centerX", Float.NaN); float centerY = MdxAttrs.getFloat(compiler, parent, el, "centerY", Float.NaN); float centerZ = MdxAttrs.getFloat(compiler, parent, el, "centerZ", Float.NaN); boolean explicitCenter = !Float.isNaN(centerX) || !Float.isNaN(centerY) || !Float.isNaN(centerZ); - if (explicitCenter) { - scene.getCamera() - .setRotationCenter( - Float.isNaN(centerX) ? 0f : centerX, - Float.isNaN(centerY) ? 0f : centerY, - Float.isNaN(centerZ) ? 0f : centerZ); - } + // Scene flags boolean interactive = MdxAttrs.getBoolean(compiler, parent, el, "interactive", true); - scene.setInteractive(interactive); - scene.setShowBackground(resolveShowBackground(compiler, parent, el)); + boolean showBackground = resolveShowBackground(compiler, parent, el); boolean allowLayerSlider = MdxAttrs .getBoolean(compiler, parent, el, "allowLayerSlider", ModConfig.ui.sceneLayerSliderEnabled); - scene.setVisibleLayerSliderEnabled(allowLayerSlider); boolean gridButtonEnabled = MdxAttrs.getBoolean(compiler, parent, el, "gridButtonEnabled", true); - scene.setGridButtonEnabled(gridButtonEnabled); boolean showGrid = MdxAttrs.getBoolean(compiler, parent, el, "showGrid", false); - scene.setGridVisible(showGrid); - scene.resetBlockStatsConfiguration(); - boolean blockStatsDeclared = false; - - if (el instanceof MdxJsxFlowElement flow) { - blockStatsDeclared = compileSceneChildren(scene, compiler, parent, flow); - scene.initializePonderTimelineBaseline(); - configureStructureLibSelectionListeners(scene, compiler, flow, explicitCenter); - } - - if (!scene.getLevel() - .isEmpty()) { - CameraSettings cam = scene.getCamera(); - // Set viewport size so worldToScreen() works correctly during auto-computation. - cam.setViewportSize(w, h); - - // Determine rotation center; fall back to the geometric center of the level bounds - // when the author has not specified one explicitly. - float[] center; - if (!explicitCenter) { - center = scene.getLevel() - .getCenter(); - cam.setRotationCenter(center[0], center[1], center[2]); - } else { - center = new float[] { Float.isNaN(centerX) ? 0f : centerX, Float.isNaN(centerY) ? 0f : centerY, - Float.isNaN(centerZ) ? 0f : centerZ }; - } - - // Auto-zoom: project all 8 AABB corners at zoom=1, offset=0 to obtain the - // screen-space extent of the scene under the current isometric rotation. The - // initial zoom is then chosen so the full scene fits within the viewport with an - // 85% fill factor (15% breathing room). - if (!explicitZoom) { - cam.setZoom(1f); - cam.setOffsetX(0f); - cam.setOffsetY(0f); - - SceneViewportMetrics metrics = measureSceneViewport( - cam, - scene.getLevel() - .getBounds()); - float spanX = metrics.spanX(); - float spanY = metrics.spanY(); - float autoZoom = 1f; - if (spanX > 0.5f || spanY > 0.5f) { - float zX = spanX > 0.5f ? (float) w / spanX : Float.MAX_VALUE; - float zY = spanY > 0.5f ? (float) h / spanY : Float.MAX_VALUE; - autoZoom = Math.min(zX, zY) * 0.85f; - autoZoom = Math.max(LytGuidebookScene.MIN_ZOOM, Math.min(LytGuidebookScene.MAX_ZOOM, autoZoom)); - } - cam.setZoom(autoZoom); - // Restore any explicit offsets that were zeroed for the measurement pass. - if (explicitOffX) cam.setOffsetX(offX); - if (explicitOffY) cam.setOffsetY(offY); - } - - // Auto-size: when width or height is not explicitly set by the author, compute the - // actual pixel extent of the scene content at the final zoom and use it as the - // viewport size. This eliminates wasted blank space around scenes smaller than the - // default canvas while preserving the same fill factor as the reference viewport. - if (!explicitWidth || !explicitHeight) { - float savedOffX = cam.getOffsetX(); - float savedOffY = cam.getOffsetY(); - cam.setOffsetX(0f); - cam.setOffsetY(0f); - - SceneViewportMetrics metrics = measureSceneViewport( - cam, - scene.getLevel() - .getBounds()); - - // Add a small border so the scene content never touches the viewport edge. - int autoPadding = 16; - int autoMinDim = 64; - int autoMaxDim = 512; - float szSpanX = metrics.spanX(); - float szSpanY = metrics.spanY(); - if (!explicitWidth && szSpanX > 0.5f) { - w = Math.min(autoMaxDim, Math.max(autoMinDim, (int) Math.ceil(szSpanX) + autoPadding)); - } - if (!explicitHeight && szSpanY > 0.5f) { - h = Math.min(autoMaxDim, Math.max(autoMinDim, (int) Math.ceil(szSpanY) + autoPadding)); - } - scene.setSceneSize(w, h); - cam.setViewportSize(w, h); - - // Restore offsets zeroed for the measurement pass. - cam.setOffsetX(savedOffX); - cam.setOffsetY(savedOffY); - } - - // Auto-center: shift the projected scene center to the viewport origin. - // Applied only when neither the rotation center nor the screen offsets are - // author-specified, so explicit offsetX/Y or centerX/Y/Z always win. - if (!explicitCenter && !explicitOffX && !explicitOffY) { - cam.setOffsetX(0f); - cam.setOffsetY(0f); - var sc = cam.worldToScreen(center[0], center[1], center[2]); - cam.setOffsetX(-sc.x); - cam.setOffsetY(sc.y); - } - } - - applyImplicitBlockStats(scene, blockStatsDeclared); - scene.applyDefaultBlockStatsMaxSizeFromScene(); - scene.snapshotInitialCamera(); - scene.captureInitialInteractiveState(); - parent.append(scene); + // Raw source text of children (preserves BlockStats and all scene element markup) + String childrenSource = compiler.getBlockTagChildrenSource(el); + + // Store all extracted scene config for later use by SceneScript + ScenePlaceholder config = new ScenePlaceholder( + w, h, explicitWidth, explicitHeight, + zoom, explicitZoom, + perspective, + rx, ry, rz, + offX, offY, explicitOffX, explicitOffY, + centerX, centerY, centerZ, explicitCenter, + interactive, showBackground, + allowLayerSlider, gridButtonEnabled, showGrid, + childrenSource + ); + + // Create and append placeholder block + String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene"; + LytParagraph placeholder = LytParagraph.of("[" + styleClass + "]"); + placeholder.setStyleClass(styleClass); + parent.append(placeholder); } private boolean resolveShowBackground(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { return MdxAttrs.getBoolean(compiler, parent, el, "showBackground", true); } - private SceneViewportMetrics measureSceneViewport(CameraSettings camera, int[] bounds) { - float lx = bounds[0]; - float ly = bounds[1]; - float lz = bounds[2]; - float hx = bounds[3] + 1f; - float hy = bounds[4] + 1f; - float hz = bounds[5] + 1f; - float minSX = Float.MAX_VALUE; - float maxSX = -Float.MAX_VALUE; - float minSY = Float.MAX_VALUE; - float maxSY = -Float.MAX_VALUE; - for (int cornerIndex = 0; cornerIndex < 8; cornerIndex++) { - float wx = (cornerIndex & 1) == 0 ? lx : hx; - float wy = (cornerIndex & 2) == 0 ? ly : hy; - float wz = (cornerIndex & 4) == 0 ? lz : hz; - var screenPoint = camera.worldToScreen(wx, wy, wz); - if (screenPoint.x < minSX) minSX = screenPoint.x; - if (screenPoint.x > maxSX) maxSX = screenPoint.x; - if (screenPoint.y < minSY) minSY = screenPoint.y; - if (screenPoint.y > maxSY) maxSY = screenPoint.y; - } - return new SceneViewportMetrics(minSX, maxSX, minSY, maxSY); - } - - private boolean compileSceneChildren(LytGuidebookScene scene, PageCompiler compiler, LytErrorSink errorSink, - MdxJsxFlowElement flow) { - return withCurrentAnnotationScene(scene, () -> { - List children = compiler.reparseBlockTagChildren(flow); - boolean[] result = new boolean[1]; - compiler.withBlockTagChildrenSourceContext( - flow, - () -> result[0] = compileSceneChildrenWithCache( - scene, - compiler, - errorSink, - children, - Collections.emptyMap())); - return result[0]; - }); - } - - static T withCurrentAnnotationScene(LytGuidebookScene scene, Supplier action) { - LytGuidebookScene previousScene = AnnotationTagCompiler.CURRENT_SCENE.get(); - AnnotationTagCompiler.CURRENT_SCENE.set(scene); - try { - return action.get(); - } finally { - if (previousScene != null) { - AnnotationTagCompiler.CURRENT_SCENE.set(previousScene); - } else { - AnnotationTagCompiler.CURRENT_SCENE.remove(); - } - } - } - - private boolean compileSceneChildrenWithCache(LytGuidebookScene scene, PageCompiler compiler, - LytErrorSink errorSink, List children, - Map structureLibSelections) { - GuideSceneStructureCacheKey cacheKey = structureFingerprintResolver - .buildForGameScene(compiler, children, structureLibSelections); - if (cacheKey == null) { - return compileSceneChildrenDetailed(scene, compiler, errorSink, children, true).isBlockStatsDeclared(); - } - - GuideSceneStructureCacheEntry cacheEntry = GuideSceneStructureCache.global() - .restore(cacheKey); - if (cacheEntry != null) { - cacheEntry.restoreInto(scene); - return compileSceneChildren(scene, compiler, errorSink, children, false); - } - - SceneChildrenCompileResult result = compileSceneChildrenDetailed(scene, compiler, errorSink, children, true); - if (result.isStructureCacheable()) { - GuideSceneStructureCacheEntry normalizedStructureState = GuideSceneStructureCacheEntry.capture(scene); - GuideSceneStructureCache.global() - .put(cacheKey, normalizedStructureState); - normalizedStructureState.restoreInto(scene); - } - return result.isBlockStatsDeclared(); - } - - private boolean compileSceneChildren(LytGuidebookScene scene, PageCompiler compiler, LytErrorSink errorSink, - List children) { - return compileSceneChildren(scene, compiler, errorSink, children, true); - } - - private boolean compileSceneChildren(LytGuidebookScene scene, PageCompiler compiler, LytErrorSink errorSink, - List children, boolean structureMutationEnabled) { - return compileSceneChildrenDetailed(scene, compiler, errorSink, children, structureMutationEnabled) - .isBlockStatsDeclared(); - } - - private SceneChildrenCompileResult compileSceneChildrenDetailed(LytGuidebookScene scene, PageCompiler compiler, - LytErrorSink errorSink, List children, boolean structureMutationEnabled) { - CountingErrorSink trackingErrorSink = new CountingErrorSink(errorSink); - return GuideSceneStructureCompileScope.supply(structureMutationEnabled, () -> { - boolean blockStatsDeclared = false; - boolean structureCacheable = true; - for (var child : children) { - UnistNode childNode = child; - MdxJsxElementFields childEl = unwrapSceneElement(childNode); - if (childEl == null) { - continue; - } - String name = childEl.name(); - if (name == null) { - continue; - } - var elCompiler = elementCompilers.get(name); - if ("BlockStats".equals(name)) { - compileBlockStatsElement(scene, compiler, trackingErrorSink, childEl); - blockStatsDeclared = true; - continue; - } - if (elCompiler == null) { - trackingErrorSink.appendError(compiler, "Unknown scene element <" + name + ">", childNode); - if (structureMutationEnabled && structureFingerprintResolver.isStructuralSceneElement(name)) { - structureCacheable = false; - } - continue; - } - int errorCountBefore = trackingErrorSink.getErrorCount(); - elCompiler.compile(scene.getLevel(), scene.getCamera(), compiler, trackingErrorSink, childEl); - if (structureMutationEnabled && structureFingerprintResolver.isStructuralSceneElement(name) - && trackingErrorSink.getErrorCount() > errorCountBefore) { - structureCacheable = false; - } - } - return new SceneChildrenCompileResult(blockStatsDeclared, structureCacheable); - }); - } - - private static class SceneChildrenCompileResult { - - private final boolean blockStatsDeclared; - private final boolean structureCacheable; - - private SceneChildrenCompileResult(boolean blockStatsDeclared, boolean structureCacheable) { - this.blockStatsDeclared = blockStatsDeclared; - this.structureCacheable = structureCacheable; - } - - public boolean isBlockStatsDeclared() { - return blockStatsDeclared; - } - - public boolean isStructureCacheable() { - return structureCacheable; + // ---- Scene data holder ---- + + /** + * Stores all extracted scene configuration for deferred scene creation by {@code SceneScript}. + */ + private static class ScenePlaceholder { + + final int width; + final int height; + final boolean explicitWidth; + final boolean explicitHeight; + final float zoom; + final boolean explicitZoom; + @Nullable final String perspective; + final float rotateX; + final float rotateY; + final float rotateZ; + final float offsetX; + final float offsetY; + final boolean explicitOffsetX; + final boolean explicitOffsetY; + final float centerX; + final float centerY; + final float centerZ; + final boolean explicitCenter; + final boolean interactive; + final boolean showBackground; + final boolean allowLayerSlider; + final boolean gridButtonEnabled; + final boolean showGrid; + @Nullable final String childrenSource; + + ScenePlaceholder( + int width, int height, + boolean explicitWidth, boolean explicitHeight, + float zoom, boolean explicitZoom, + @Nullable String perspective, + float rotateX, float rotateY, float rotateZ, + float offsetX, float offsetY, + boolean explicitOffsetX, boolean explicitOffsetY, + float centerX, float centerY, float centerZ, + boolean explicitCenter, + boolean interactive, boolean showBackground, + boolean allowLayerSlider, boolean gridButtonEnabled, + boolean showGrid, + @Nullable String childrenSource) { + this.width = width; + this.height = height; + this.explicitWidth = explicitWidth; + this.explicitHeight = explicitHeight; + this.zoom = zoom; + this.explicitZoom = explicitZoom; + this.perspective = perspective; + this.rotateX = rotateX; + this.rotateY = rotateY; + this.rotateZ = rotateZ; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.explicitOffsetX = explicitOffsetX; + this.explicitOffsetY = explicitOffsetY; + this.centerX = centerX; + this.centerY = centerY; + this.centerZ = centerZ; + this.explicitCenter = explicitCenter; + this.interactive = interactive; + this.showBackground = showBackground; + this.allowLayerSlider = allowLayerSlider; + this.gridButtonEnabled = gridButtonEnabled; + this.showGrid = showGrid; + this.childrenSource = childrenSource; } } - private static class CountingErrorSink implements LytErrorSink { - - private final LytErrorSink delegate; - private int errorCount; - - private CountingErrorSink(LytErrorSink delegate) { - this.delegate = delegate; - } - - @Override - public void appendError(PageCompiler compiler, String text, UnistNode node) { - errorCount++; - delegate.appendError(compiler, text, node); - } - - public int getErrorCount() { - return errorCount; - } - } - - private static void applyImplicitBlockStats(LytGuidebookScene scene, boolean blockStatsDeclared) { - if (blockStatsDeclared || scene.getLevel() - .isEmpty()) { - return; - } - scene.setBlockStatsEnabled(true); - scene.setBlockStatsVisible(ModConfig.ui.sceneBlockStatsVisible); - scene.setBlockStatsButtonEnabled(ModConfig.ui.sceneBlockStatsButtonEnabled); - scene.setBlockStatsMode(BlockStatsMode.AUTO); - scene.setBlockStatsCorner(BlockStatsCorner.TOP_RIGHT); - scene.setBlockStatsDock(BlockStatsDock.INSIDE); - scene.setBlockStatsShowNames(false); - scene.setBlockStatsFilterMode(BlockStatsFilterMode.BLACKLIST); - scene.setBlockStatsFilterKeys(Collections.emptySet()); - } - - private static Set parseBlockStatsFilter(String raw) { - Set filter = new HashSet<>(); - if (raw == null || raw.trim() - .isEmpty()) { - return filter; - } - int start = -1; - for (int index = 0, length = raw.length(); index <= length; index++) { - char current = index < length ? raw.charAt(index) : ','; - if (isBlockStatsFilterSeparator(current)) { - if (start >= 0) { - String normalized = LytGuidebookScene.normalizeBlockStatsKey(raw.substring(start, index)); - if (normalized != null) { - filter.add(normalized); - } - start = -1; - } - } else if (start < 0) { - start = index; - } - } - return filter; - } - - private static boolean isBlockStatsFilterSeparator(char value) { - return value == ',' || value == ';' || Character.isWhitespace(value); - } - - private void compileBlockStatsElement(LytGuidebookScene scene, PageCompiler compiler, LytErrorSink errorSink, - MdxJsxElementFields el) { - scene.setBlockStatsEnabled(true); - scene.setBlockStatsVisible( - MdxAttrs.getBoolean(compiler, errorSink, el, "visible", ModConfig.ui.sceneBlockStatsVisible)); - scene.setBlockStatsButtonEnabled( - MdxAttrs.getBoolean(compiler, errorSink, el, "buttonEnabled", ModConfig.ui.sceneBlockStatsButtonEnabled)); - scene.setBlockStatsMode( - BlockStatsMode.fromString(MdxAttrs.getString(compiler, errorSink, el, "mode", null), BlockStatsMode.AUTO)); - scene.setBlockStatsCorner( - BlockStatsCorner - .fromString(MdxAttrs.getString(compiler, errorSink, el, "corner", null), BlockStatsCorner.TOP_RIGHT)); - scene.setBlockStatsDock( - BlockStatsDock - .fromString(MdxAttrs.getString(compiler, errorSink, el, "dock", null), BlockStatsDock.INSIDE)); - scene.setBlockStatsShowNames(MdxAttrs.getBoolean(compiler, errorSink, el, "showNames", false)); - scene.setBlockStatsFilterMode( - BlockStatsFilterMode.fromString( - MdxAttrs.getString(compiler, errorSink, el, "filterMode", null), - BlockStatsFilterMode.BLACKLIST)); - scene.setBlockStatsFilterKeys( - parseBlockStatsFilter(MdxAttrs.getString(compiler, errorSink, el, "filter", null))); - if (el.getAttribute("maxWidth") != null) { - scene.setBlockStatsMaxWidth( - MdxAttrs.getInt(compiler, errorSink, el, "maxWidth", LytGuidebookScene.BLOCK_STATS_DEFAULT_MAX_WIDTH)); - } - if (el.getAttribute("maxHeight") != null) { - scene.setBlockStatsMaxHeight( - MdxAttrs - .getInt(compiler, errorSink, el, "maxHeight", LytGuidebookScene.BLOCK_STATS_DEFAULT_MAX_HEIGHT)); - } - scene.clearManualBlockStatsEntries(); - boolean manualEntries = false; - for (MdAstAnyContent child : el.children()) { - MdxJsxElementFields childEl = unwrapSceneElement(child); - if (childEl == null) { - continue; - } - String name = childEl.name(); - if (!"BlockStat".equals(name)) { - errorSink.appendError(compiler, "Unknown BlockStats element <" + name + ">", child); - continue; - } - ItemStack item = getBlockStatItemStack(compiler, errorSink, childEl); - if (item == null) { - continue; - } - int count = Math.max(0, getBlockStatCount(compiler, errorSink, childEl)); - scene.addManualBlockStatsEntry(item, count); - manualEntries = true; - } - if (manualEntries) { - scene.setBlockStatsMode(BlockStatsMode.MANUAL); - } - } - - private static int getBlockStatCount(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields el) { - return Math.round(Math.max(0f, MdxAttrs.getFloat(compiler, errorSink, el, "count", 0f))); - } - - private static ItemStack getBlockStatItemStack(PageCompiler compiler, LytErrorSink errorSink, - MdxJsxElementFields el) { - if (el.getAttribute("item") != null && el.getAttribute("id") == null) { - String item = MdxAttrs.getString(compiler, errorSink, el, "item", null); - if (item == null || item.trim() - .isEmpty()) { - errorSink.appendError(compiler, "Missing item attribute.", el); - return null; - } - ItemStack stack = IdUtils.resolveItemStack( - item.trim(), - compiler.getPageId() - .getResourceDomain()); - if (stack == null) { - errorSink.appendError(compiler, "Missing item: " + item, el); - } - return stack; - } - var result = MdxAttrs.getRequiredItemStackAndId(compiler, errorSink, el); - return result != null ? result.getRight() : null; - } - - private void rebuildSceneForStructureLibSelection(LytGuidebookScene scene, PageCompiler compiler, - MdxJsxFlowElement flow, boolean explicitCenter, @Nullable String structureName, - StructureLibPreviewSelection selection) { - if (scene == null) { - return; - } - Map bindingSelections = new LinkedHashMap<>( - scene.getStructureLibPreviewSelectionsByBinding()); - if (structureName != null && !structureName.trim() - .isEmpty()) { - bindingSelections.put(structureName.trim(), selection); - } else if (!bindingSelections.isEmpty()) { - String primaryBindingKey = bindingSelections.keySet() - .iterator() - .next(); - bindingSelections.put(primaryBindingKey, selection); - } - SavedCameraSettings savedCamera = scene.getCamera() - .save(); - boolean annotationsVisible = scene.isAnnotationsVisible(); - boolean hatchHighlightEnabled = scene.isStructureLibHatchHighlightEnabled(); - boolean gridVisible = scene.isGridVisible(); - boolean blockStatsVisible = scene.isBlockStatsVisible(); - scene.captureInitialStructureStateIfAbsent(); - scene.getAnnotations() - .clear(); - scene.clearSoundCues(); - scene.setHoveredBlock(null); - scene.setHoveredEntity(null); - scene.setHoveredStructureLibHatch(null); - scene.clearAnnotationHover(); - scene.setStructureLibSceneMetadata(null); - scene.seedStructureLibPreviewSelections(bindingSelections); - scene.setLevel(new GuidebookLevel()); - scene.resetBlockStatsConfiguration(); - boolean blockStatsDeclared; - try { - blockStatsDeclared = withCurrentAnnotationScene(scene, () -> { - List children = compiler.reparseBlockTagChildren(flow); - boolean[] result = new boolean[1]; - compiler.withBlockTagChildrenSourceContext( - flow, - () -> result[0] = compileSceneChildrenWithCache( - scene, - compiler, - NOOP_ERROR_SINK, - children, - bindingSelections)); - return result[0]; - }); - scene.initializePonderTimelineBaseline(); - configureStructureLibSelectionListeners(scene, compiler, flow, explicitCenter); - } finally { - scene.clearSeededStructureLibPreviewSelections(); - } - applyImplicitBlockStats(scene, blockStatsDeclared); - scene.applyDefaultBlockStatsMaxSizeFromScene(); - scene.setBlockStatsVisible(blockStatsVisible); - if (!scene.getLevel() - .isEmpty() && !explicitCenter) { - var center = scene.getLevel() - .getCenter(); - scene.getCamera() - .setRotationCenter(center[0], center[1], center[2]); - } - scene.setAnnotationsVisible(annotationsVisible); - scene.setStructureLibHatchHighlightEnabled(hatchHighlightEnabled); - scene.setGridVisible(gridVisible); - scene.getCamera() - .restore(savedCamera); - } - - private void configureStructureLibSelectionListeners(LytGuidebookScene scene, PageCompiler compiler, - MdxJsxFlowElement flow, boolean explicitCenter) { - scene.setStructureLibSelectionChangeListener( - selection -> rebuildSceneForStructureLibSelection(scene, compiler, flow, explicitCenter, null, selection)); - for (StructureLibSceneBinding binding : scene.getStructureLibBindings()) { - StructureLibSceneMetadata metadata = binding.getMetadata(); - if (binding.getName() == null || metadata == null) { - continue; - } - String structureName = binding.getName(); - binding.setSelectionChangeListener( - selection -> rebuildSceneForStructureLibSelection( - scene, - compiler, - flow, - explicitCenter, - structureName, - selection)); - } - } + // ---- Utility methods (pure, kept for script use) ---- public static MdxJsxElementFields unwrapSceneElement(UnistNode node) { if (node instanceof MdxJsxElementFields elementFields) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index b2b17abd..4ac371da 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -12,6 +12,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; +import net.minecraft.block.Block; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; @@ -34,8 +36,6 @@ import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.KeyBindTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; -import com.hfstudio.guidenh.guide.compiler.tags.StructureViewCompiler; -import com.hfstudio.guidenh.guide.compiler.tags.SubPagesCompiler; import com.hfstudio.guidenh.guide.compiler.tags.chart.ChartAttrParser; import com.hfstudio.guidenh.guide.compiler.tags.functiongraph.FunctionGraphAttrs; import com.hfstudio.guidenh.guide.document.block.LytStructureView; @@ -936,7 +936,7 @@ private String renderSubPages(MdxJsxElementFields element, String defaultNamespa List sorted = new ArrayList<>(nodes); if (alphabetical) { - sorted.sort(SubPagesCompiler.ALPHABETICAL_COMPARATOR); + sorted.sort(Comparator.comparing(NavigationNode::title)); } return renderNavigationNodeList(sorted, currentPageId); } @@ -2552,7 +2552,7 @@ private StructureBlockView parseStructureBlock(String line, @Nullable ResourceLo } catch (NumberFormatException ignored) {} } - ItemStack stack = StructureViewCompiler.resolveStack(resourceId, meta); + ItemStack stack = resolveStructureStack(resourceId, meta); GuideSiteExportedItem exportedItem; String displayName; @Nullable @@ -2580,6 +2580,19 @@ private StructureBlockView parseStructureBlock(String line, @Nullable ResourceLo templateId); } + @Nullable + private static ItemStack resolveStructureStack(String resourceId, int meta) { + var item = (Item) Item.itemRegistry.getObject(resourceId); + if (item != null) { + return new ItemStack(item, 1, meta); + } + var block = (Block) Block.blockRegistry.getObject(resourceId); + if (block != null) { + return new ItemStack(block, 1, meta); + } + return null; + } + private List splitWhitespaceTokens(String text, int limit) { List tokens = new ArrayList<>(Math.max(1, limit)); int start = -1; From 5f2bce39c0506f95fb23b1320658af16ffeb53f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 01:29:05 +0800 Subject: [PATCH 043/136] =?UTF-8?q?fix:=20Phase=203=20review=20=E2=80=94?= =?UTF-8?q?=20scene=20data=20loss,=20flow=20MOUNT=20dispatch,=20missing=20?= =?UTF-8?q?registrations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScenePlaceholder now extends LytParagraph so config data survives - BlockImageCompiler stores extracted attributes in BlockImagePlaceholder - dispatchMountEvents/allocateNodeUids traverse LytParagraph content + LytFlowSpan children - LytFlowContent gets nodeUid field for flow-level node identification - LytScript.onEvent/LytEvent target widened to Object for flow content support - Register missing scripts: BlockImage, CsvTable, Mermaid, Scene, GameScene --- .../com/hfstudio/guidenh/ClientProxy.java | 11 ++++ .../compiler/tags/BlockImageCompiler.java | 34 ++++++++++- .../guide/document/flow/LytFlowContent.java | 8 +++ .../guidenh/guide/internal/host/LytEvent.java | 16 +++-- .../guidenh/guide/internal/host/LytHost.java | 61 ++++++++++++++++++- .../guide/internal/host/LytScript.java | 4 +- .../guide/internal/host/ScriptContext.java | 2 +- .../internal/host/ScriptContextImpl.java | 17 +++--- .../host/scripts/BlockImageScript.java | 2 +- .../internal/host/scripts/CategoryScript.java | 2 +- .../host/scripts/CommandLinkScript.java | 2 +- .../internal/host/scripts/CsvTableScript.java | 2 +- .../host/scripts/FloatingImageScript.java | 2 +- .../internal/host/scripts/ImageScript.java | 2 +- .../internal/host/scripts/ItemGridScript.java | 2 +- .../host/scripts/ItemImageScript.java | 2 +- .../internal/host/scripts/ItemLinkScript.java | 2 +- .../internal/host/scripts/KeyBindScript.java | 2 +- .../internal/host/scripts/MermaidScript.java | 2 +- .../host/scripts/PlayerNameScript.java | 2 +- .../internal/host/scripts/RecipeScript.java | 2 +- .../internal/host/scripts/SceneScript.java | 2 +- .../host/scripts/SoundLinkScript.java | 2 +- .../internal/host/scripts/SpecialScript.java | 2 +- .../host/scripts/StructureScript.java | 2 +- .../internal/host/scripts/SubPagesScript.java | 2 +- .../guidenh/guide/scene/SceneTagCompiler.java | 16 ++--- 27 files changed, 156 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 317ea6d4..218e1634 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -67,16 +67,20 @@ import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.LytHostWorkItem; +import com.hfstudio.guidenh.guide.internal.host.scripts.BlockImageScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CategoryScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CommandLinkScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.CsvTableScript; import com.hfstudio.guidenh.guide.internal.host.scripts.FloatingImageScript; import com.hfstudio.guidenh.guide.internal.host.scripts.ImageScript; import com.hfstudio.guidenh.guide.internal.host.scripts.ItemGridScript; import com.hfstudio.guidenh.guide.internal.host.scripts.ItemImageScript; import com.hfstudio.guidenh.guide.internal.host.scripts.ItemLinkScript; import com.hfstudio.guidenh.guide.internal.host.scripts.KeyBindScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.MermaidScript; import com.hfstudio.guidenh.guide.internal.host.scripts.PlayerNameScript; import com.hfstudio.guidenh.guide.internal.host.scripts.RecipeScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.SceneScript; import com.hfstudio.guidenh.guide.internal.host.scripts.SoundLinkScript; import com.hfstudio.guidenh.guide.internal.host.scripts.SpecialScript; import com.hfstudio.guidenh.guide.internal.host.scripts.StructureScript; @@ -168,6 +172,13 @@ public void init(FMLInitializationEvent event) { lytHost.registerScript("ItemLink", new ItemLinkScript()); lytHost.registerScript("Category", new CategoryScript()); lytHost.registerScript("Special", new SpecialScript()); + lytHost.registerScript("BlockImage", new BlockImageScript()); + lytHost.registerScript("CsvTable", new CsvTableScript()); + lytHost.registerScript("Mermaid", new MermaidScript()); + // Phase 3: SceneScript handles Scene and GameScene + SceneScript sceneScript = new SceneScript(); + lytHost.registerScript("Scene", sceneScript); + lytHost.registerScript("GameScene", sceneScript); // Phase 3: RecipeScript handles Recipe, RecipeFor, RecipeUsage, RecipesFor RecipeScript recipeScript = new RecipeScript(); lytHost.registerScript("Recipe", recipeScript); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java index 167efb0a..fc79b98f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java @@ -3,6 +3,8 @@ import java.util.Collections; import java.util.Set; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; @@ -32,8 +34,38 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl int width = MdxAttrs.getInt(compiler, parent, el, "width", 128); int height = MdxAttrs.getInt(compiler, parent, el, "height", 128); - var placeholder = LytParagraph.of("[BlockImage]"); + // Create placeholder block that carries all extracted config to BlockImageScript + BlockImagePlaceholder placeholder = new BlockImagePlaceholder( + id, ore, meta, nbt, scale, perspective, width, height); placeholder.setStyleClass("BlockImage"); + placeholder.appendText("[BlockImage]"); parent.append(placeholder); } + + /** + * Placeholder block that stores all extracted block-image configuration for deferred scene + * creation by {@code BlockImageScript}. + */ + static class BlockImagePlaceholder extends LytParagraph { + @Nullable final String id; + @Nullable final String ore; + final int meta; + @Nullable final String nbt; + final float scale; + @Nullable final String perspective; + final int width; + final int height; + + BlockImagePlaceholder(@Nullable String id, @Nullable String ore, int meta, @Nullable String nbt, + float scale, @Nullable String perspective, int width, int height) { + this.id = id; + this.ore = ore; + this.meta = meta; + this.nbt = nbt; + this.scale = scale; + this.perspective = perspective; + this.width = width; + this.height = height; + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java index 7acb0521..89dff05a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java @@ -19,6 +19,9 @@ public class LytFlowContent implements Styleable { @Nullable private String styleClass; + @Nullable + private String nodeUid; + private final Map data = new HashMap<>(); public LytFlowParent getParent() { @@ -87,6 +90,11 @@ protected void visitChildren(LytVisitor visitor) {} public void setStyleClass(@Nullable String styleClass) { this.styleClass = styleClass; } + @Nullable + public String getNodeUid() { return nodeUid; } + + public void setNodeUid(@Nullable String nodeUid) { this.nodeUid = nodeUid; } + public Object getData(String key) { return data.get(key); } public void setData(String key, Object value) { data.put(key, value); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java index 909a5890..5d40044e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java @@ -4,21 +4,19 @@ import java.util.LinkedHashMap; import java.util.Map; -import com.hfstudio.guidenh.guide.document.block.LytNode; - public class LytEvent { private final EventType type; - private final LytNode target; - private LytNode currentTarget; + private final Object target; + private Object currentTarget; private final Map data; private boolean propagationStopped; - public LytEvent(EventType type, LytNode target) { + public LytEvent(EventType type, Object target) { this(type, target, null); } - public LytEvent(EventType type, LytNode target, Map data) { + public LytEvent(EventType type, Object target, Map data) { this.type = type; this.target = target; this.currentTarget = target; @@ -28,12 +26,12 @@ public LytEvent(EventType type, LytNode target, Map data) { } public EventType type() { return type; } - public LytNode target() { return target; } - public LytNode currentTarget() { return currentTarget; } + public Object target() { return target; } + public Object currentTarget() { return currentTarget; } public Map data() { return data; } public void stopPropagation() { propagationStopped = true; } public boolean isPropagationStopped() { return propagationStopped; } - void setCurrentTarget(LytNode node) { this.currentTarget = node; } + void setCurrentTarget(Object node) { this.currentTarget = node; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 5efae0b0..d76158f1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -12,6 +12,9 @@ import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; public class LytHost { @@ -98,6 +101,30 @@ private void allocateNodeUids(LytNode node) { for (var child : node.getChildren()) { allocateNodeUids(child); } + // Also traverse into flow content (LytParagraph, LytFlowSpan children) + allocateFlowNodeUids(node); + } + + private void allocateFlowNodeUids(LytNode node) { + if (node instanceof LytParagraph para) { + for (var fcChild : para.getContent()) { + allocateFlowNodeUidsRecursive(fcChild); + } + } + } + + private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { + if (fc.getStyleClass() != null && fc.getNodeUid() == null) { + String prefix = fc.getStyleClass().toLowerCase(); + int seq = pageNodeCounters + .computeIfAbsent(currentPageId, k -> new AtomicInteger()).incrementAndGet(); + fc.setNodeUid(currentPageId + "::" + prefix + ":" + seq); + } + if (fc instanceof LytFlowSpan span) { + for (var child : span.getChildren()) { + allocateFlowNodeUidsRecursive(child); + } + } } private void dispatchMountEvents(LytNode node) { @@ -117,6 +144,36 @@ private void dispatchMountEvents(LytNode node) { for (var child : node.getChildren()) { dispatchMountEvents(child); } + // Also traverse into flow content for inline-level styleClass nodes + dispatchMountEventsFlow(node); + } + + private void dispatchMountEventsFlow(LytNode node) { + if (node instanceof LytParagraph para) { + for (var fcChild : para.getContent()) { + dispatchMountEventsFlowRecursive(fcChild); + } + } + } + + private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { + String cls = fc.getStyleClass(); + if (cls != null) { + LytScript script = scripts.get(cls); + if (script != null) { + try { + ScriptContextImpl ctx = new ScriptContextImpl(fc, this, document); + script.onEvent(fc, new LytEvent(EventType.MOUNT, fc), ctx); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + if (fc instanceof LytFlowSpan span) { + for (var child : span.getChildren()) { + dispatchMountEventsFlowRecursive(child); + } + } } // ===== Sync events ===== @@ -130,8 +187,8 @@ private void processEventsNow() { while (!eventQueue.isEmpty()) { LytEvent event = eventQueue.pollFirst(); if (document == null || event.target() == null) continue; - LytNode target = event.target(); - if (target instanceof InteractiveElement interactive) { + Object rawTarget = event.target(); + if (rawTarget instanceof InteractiveElement interactive) { switch (event.type()) { case CLICK: case DOUBLE_CLICK: diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java index 68ac1adc..048d030b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java @@ -1,9 +1,7 @@ package com.hfstudio.guidenh.guide.internal.host; -import com.hfstudio.guidenh.guide.document.block.LytNode; - public interface LytScript { ScriptType type(); String styleClass(); - void onEvent(LytNode node, LytEvent event, ScriptContext ctx); + void onEvent(Object node, LytEvent event, ScriptContext ctx); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java index 908ca246..6d44129c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java @@ -6,7 +6,7 @@ public interface ScriptContext { Map data(); - void replace(LytNode newNode); + void replace(Object newNode); String allocateId(String prefix); LytDocument document(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index dd0a8cf2..92535781 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -7,11 +7,11 @@ class ScriptContextImpl implements ScriptContext { private final Map data = new HashMap<>(); - private final LytNode node; + private final Object node; private final LytHost host; private final LytDocument document; - ScriptContextImpl(LytNode node, LytHost host, LytDocument document) { + ScriptContextImpl(Object node, LytHost host, LytDocument document) { this.node = node; this.host = host; this.document = document; @@ -21,16 +21,19 @@ class ScriptContextImpl implements ScriptContext { public Map data() { return data; } @Override - public void replace(LytNode newNode) { - LytNode parent = node.getParent(); - if (parent != null) { - parent.replaceChild(node, newNode); + public void replace(Object newNode) { + if (node instanceof LytNode ln && newNode instanceof LytNode newLn) { + LytNode parent = ln.getParent(); + if (parent != null) { + parent.replaceChild(ln, newLn); + } } + // Flow-content replacement deferred to Phase 4 } @Override public String allocateId(String prefix) { - return prefix + ":" + System.identityHashCode(node); + return host.allocateNodeUid(host.currentPageId, prefix); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 9bde6677..2400af9b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: scene creation will be wired here } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index 986e02b0..eb3538f3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java index 5de182ff..09d445a9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java index 9ed0140b..d045ea53 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java @@ -14,7 +14,7 @@ public class CsvTableScript implements LytScript { public String styleClass() { return "CsvTable"; } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: CSV loading and table building will be wired later } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index 6b1caffa..76797e80 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -24,7 +24,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // TODO: Load image asset and replace placeholder // For Phase 3 initial implementation, this is a stub. // The actual image loading will be added when the script infrastructure diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index cbe30e63..a3f84e25 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -24,7 +24,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // TODO: Load image asset and replace placeholder // For Phase 3 initial implementation, this is a stub. // The actual image loading will be added when the script infrastructure diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index 2e93b837..73d4e776 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index 80052bab..af29c876 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 9039c9af..d3376cf8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java index e8f10bce..a9ab9310 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index c7e07162..f2f0580f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -14,7 +14,7 @@ public class MermaidScript implements LytScript { public String styleClass() { return "Mermaid"; } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: Mermaid rendering will be wired later } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java index a81d5c0c..62b326bd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index 06d70cdf..781917c4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full NEI query chain will be wired here } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index 2a0eb582..e13281ff 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: entire scene pipeline will be wired here } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java index c8296bc6..b20e8638 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index cb78a827..9e004908 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index 17ffa6d7..24a1e92a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index 9ed1d19c..8825572c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -19,7 +19,7 @@ public String styleClass() { } @Override - public void onEvent(LytNode node, LytEvent event, ScriptContext ctx) { + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Stub: full implementation in later task } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 5251737e..4b10ceca 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -120,8 +120,9 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // Raw source text of children (preserves BlockStats and all scene element markup) String childrenSource = compiler.getBlockTagChildrenSource(el); - // Store all extracted scene config for later use by SceneScript - ScenePlaceholder config = new ScenePlaceholder( + // Create placeholder block that carries all scene config to SceneScript + String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene"; + ScenePlaceholder placeholder = new ScenePlaceholder( w, h, explicitWidth, explicitHeight, zoom, explicitZoom, perspective, @@ -132,11 +133,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl allowLayerSlider, gridButtonEnabled, showGrid, childrenSource ); - - // Create and append placeholder block - String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene"; - LytParagraph placeholder = LytParagraph.of("[" + styleClass + "]"); placeholder.setStyleClass(styleClass); + placeholder.appendText("[" + styleClass + "]"); parent.append(placeholder); } @@ -147,9 +145,11 @@ private boolean resolveShowBackground(PageCompiler compiler, LytBlockContainer p // ---- Scene data holder ---- /** - * Stores all extracted scene configuration for deferred scene creation by {@code SceneScript}. + * Placeholder block that stores all extracted scene configuration for deferred scene creation + * by {@code SceneScript}. Extends LytParagraph so it lives in the LytNode tree and can receive + * MOUNT dispatch. */ - private static class ScenePlaceholder { + public static class ScenePlaceholder extends LytParagraph { final int width; final int height; From d173faf0cf7146929450828b92ea38973f9b1763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 02:19:20 +0800 Subject: [PATCH 044/136] fix: cascadeLive, LytBox lifecycle, flow-content replace, 9 script implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix cascadeLive to call onAttach/onDetach on all nodes (was only LytDocument) - Add lifecycle hooks to LytBox append/removeChild/replaceChild - Handle LytFlowInlineBlock in MOUNT dispatch (penetrate to inner block styleClass) - Support LytFlowContent replacement in ScriptContextImpl.replace() - Wire LytHostPreheatItem to MasterScheduler, setCurrentPageId in GuideScreen - Make CategoryPlaceholder and SpecialPlaceholder public with public fields Implement 9 LytScript: PlayerNameScript — read Minecraft username KeyBindScript — look up key binding mapping CommandLinkScript — set click callback with chat command SoundLinkScript — set click sound spec StructureScript — resolve item stacks via Item.itemRegistry SubPagesScript — query navigation tree, build page link list ItemGridScript — resolve item stacks, build item grid ItemImageScript — resolve item stack, build LytItemImage ItemLinkScript — resolve item stack, set tooltip (index lookup deferred) --- .../com/hfstudio/guidenh/ClientProxy.java | 2 + .../guide/compiler/tags/CsvTableCompiler.java | 1 + .../guide/compiler/tags/ImageCompiler.java | 2 + .../guide/compiler/tags/MermaidCompiler.java | 1 + .../guide/compiler/tags/RecipeCompiler.java | 1 + .../tags/mediawiki/CategoryCompiler.java | 8 +-- .../tags/mediawiki/SpecialCompiler.java | 16 ++--- .../guidenh/guide/document/block/LytBox.java | 4 ++ .../guide/document/block/LytDocument.java | 24 ++++--- .../guide/document/block/LytParagraph.java | 22 +++++++ .../guidenh/guide/internal/GuideScreen.java | 1 + .../guidenh/guide/internal/host/LytHost.java | 15 +++++ .../internal/host/ScriptContextImpl.java | 19 +++++- .../host/scripts/CommandLinkScript.java | 19 +++++- .../internal/host/scripts/ItemGridScript.java | 40 ++++++++--- .../host/scripts/ItemImageScript.java | 63 +++++++++++++++--- .../internal/host/scripts/ItemLinkScript.java | 66 ++++++++++++++++--- .../internal/host/scripts/KeyBindScript.java | 14 +++- .../host/scripts/PlayerNameScript.java | 15 ++++- .../host/scripts/SoundLinkScript.java | 11 +++- .../host/scripts/StructureScript.java | 41 +++++++++--- .../internal/host/scripts/SubPagesScript.java | 59 ++++++++++++++--- .../guidenh/guide/scene/SceneTagCompiler.java | 1 + 23 files changed, 375 insertions(+), 70 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 218e1634..f16936ca 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -67,6 +67,7 @@ import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.LytHostWorkItem; +import com.hfstudio.guidenh.guide.internal.scheduler.LytHostPreheatItem; import com.hfstudio.guidenh.guide.internal.host.scripts.BlockImageScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CategoryScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CommandLinkScript; @@ -156,6 +157,7 @@ public void init(FMLInitializationEvent event) { MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); MasterScheduler.init(); MasterScheduler.getInstance().submit(new LytHostWorkItem(lytHost)); + MasterScheduler.getInstance().submit(new LytHostPreheatItem(lytHost)); MasterScheduler.getInstance().submit(new SearchIndexWorkItem()); // Phase 3: LytScript registrations diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index c7e97489..6a6c3125 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -157,6 +157,7 @@ public CsvTablePlaceholder(String src, boolean header, List widths) { this.header = header; this.widths = widths; setStyleClass("CsvTable"); + setStyle(LytParagraph.LOADING_STYLE); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java index 4a7eb7b4..10ad0156 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytImageBlock; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -35,6 +36,7 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen if (!title.isEmpty()) block.setTitle(title); block.appendText("Loading image..."); + block.setStyle(LytParagraph.LOADING_STYLE); var inlineBlock = new LytFlowInlineBlock(); inlineBlock.setBlock(block); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java index e1397489..38c9fa7a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java @@ -143,6 +143,7 @@ public MermaidPlaceholder(String src, String sourceText, int width, int height, this.height = height; this.nodeContentBlocks = nodeContentBlocks; setStyleClass("Mermaid"); + setStyle(LytParagraph.LOADING_STYLE); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java index 8c13b72e..e5e7a18c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java @@ -113,6 +113,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl handlerNameFilter, handlerIdFilter, handlerOrder, exactRecipeIndex, inputExpr, outputExpr, limit, multi, usageQuery); ph.setStyleClass(tagName); + ph.setStyle(LytParagraph.LOADING_STYLE); ph.appendText("Loading recipe..."); parent.append(ph); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 3aa00be8..eac15034 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -52,10 +52,10 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } } - private static class CategoryPlaceholder extends LytParagraph { - final String name; - final int rows; - final ResourceLocation guideId; + public static class CategoryPlaceholder extends LytParagraph { + public final String name; + public final int rows; + public final ResourceLocation guideId; CategoryPlaceholder(String name, int rows, ResourceLocation guideId) { this.name = name; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index ab7df0e9..d292bb0a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -56,14 +56,14 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } } - private static class SpecialPlaceholder extends LytParagraph { - final String name; - final int rows; - final ResourceLocation guideId; - final String page; - final String prefix; - final String language; - final String query; + public static class SpecialPlaceholder extends LytParagraph { + public final String name; + public final int rows; + public final ResourceLocation guideId; + public final String page; + public final String prefix; + public final String language; + public final String query; SpecialPlaceholder(String name, int rows, ResourceLocation guideId, String page, String prefix, String language, String query) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java index 3bcd85e9..d70d77c8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java @@ -27,6 +27,7 @@ public abstract class LytBox extends LytBlock implements LytBlockContainer { @Override public void removeChild(LytNode node) { if (node instanceof LytBlock block && block.parent == this) { + if (isAttached()) LytDocument.notifyDetach(block); children.remove(block); block.parent = null; } @@ -39,6 +40,7 @@ public void append(LytBlock block) { } block.parent = this; children.add(block); + if (isAttached()) LytDocument.notifyAttach(block); } @Override @@ -49,12 +51,14 @@ public void replaceChild(LytNode oldChild, LytNode newChild) { LytBlock newBlock = (LytBlock) newChild; int idx = children.indexOf(oldBlock); if (idx < 0) return; + if (isAttached()) LytDocument.notifyDetach(oldBlock); oldBlock.parent = null; if (newBlock.parent != null) { newBlock.parent.removeChild(newBlock); } newBlock.parent = this; children.set(idx, newBlock); + if (isAttached()) LytDocument.notifyAttach(newBlock); LytDocument doc = getDocument(); if (doc != null) { doc.invalidateLayout(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java index 73fb4114..cfb8d7d2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java @@ -118,22 +118,28 @@ public boolean isLive() { public void setLive(boolean live) { if (this.live == live) return; this.live = live; - cascadeLive(this); + cascadeLive(this, live); } - private static void cascadeLive(LytNode node) { - if (node instanceof LytDocument doc) { - if (doc.live) { - doc.onAttach(); - } else { - doc.onDetach(); - } + private static void cascadeLive(LytNode node, boolean live) { + if (live) { + node.onAttach(); + } else { + node.onDetach(); } for (var child : node.getChildren()) { - cascadeLive(child); + cascadeLive(child, live); } } + static void notifyAttach(LytNode node) { + cascadeLive(node, true); + } + + static void notifyDetach(LytNode node) { + cascadeLive(node, false); + } + public void invalidateLayout() { layout = null; invalidateVisibleCache(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java index 9e46836c..46bbbf12 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java @@ -4,12 +4,14 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.flow.LytFlowContainer; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.layout.flow.FlowBuilder; import com.hfstudio.guidenh.guide.render.RenderContext; +import com.hfstudio.guidenh.guide.style.TextStyle; public class LytParagraph extends LytBlock implements LytFlowContainer { @@ -178,4 +180,24 @@ public static LytParagraph of(String text) { paragraph.appendText(text); return paragraph; } + + /** + * The text style used for loading placeholders: gray, italic, obfuscated. + */ + public static final TextStyle LOADING_STYLE = TextStyle.builder() + .italic(true) + .obfuscated(true) + .color(new ConstantColor(0xFF808080)) + .build(); + + /** + * Creates a placeholder paragraph with distinctive "loading" visual style + * (gray, italic, obfuscated text) so pending materialization is obvious. + */ + public static LytParagraph loading(String text) { + var paragraph = new LytParagraph(); + paragraph.setStyle(LOADING_STYLE); + paragraph.appendText(text); + return paragraph; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 95452441..f1ac04ee 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2522,6 +2522,7 @@ private void completePendingContentPageLoadIfNeeded() { } currentPage = loadedPage; document = loadedPage != null ? loadedPage.document() : null; + ClientProxy.getLytHost().setCurrentPageId(currentAnchor.pageId().toString()); ClientProxy.getLytHost().setDocument(document); if (document != null && isSpecialPageWithSearchField()) { applySpecialPageSearchQuery(queryFromCurrentAnchor()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index d76158f1..7fc6b9e7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -14,6 +14,7 @@ import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; @@ -173,6 +174,20 @@ private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { for (var child : span.getChildren()) { dispatchMountEventsFlowRecursive(child); } + } else if (fc instanceof LytFlowInlineBlock inlineBlock && inlineBlock.getBlock() != null) { + LytBlock inner = inlineBlock.getBlock(); + String innerCls = inner.getStyleClass(); + if (innerCls != null) { + LytScript script = scripts.get(innerCls); + if (script != null) { + try { + ScriptContextImpl ctx = new ScriptContextImpl(inlineBlock, this, document); + script.onEvent(inlineBlock, new LytEvent(EventType.MOUNT, inlineBlock), ctx); + } catch (Exception e) { + e.printStackTrace(); + } + } + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 92535781..0c82de16 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -1,9 +1,14 @@ package com.hfstudio.guidenh.guide.internal.host; import java.util.HashMap; +import java.util.List; import java.util.Map; + import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; class ScriptContextImpl implements ScriptContext { private final Map data = new HashMap<>(); @@ -27,8 +32,20 @@ public void replace(Object newNode) { if (parent != null) { parent.replaceChild(ln, newLn); } + return; + } + if (node instanceof LytFlowContent fc && newNode instanceof LytFlowContent newFc) { + LytFlowParent parent = fc.getParent(); + if (parent instanceof LytFlowSpan span) { + List children = span.getChildren(); + int idx = children.indexOf(fc); + if (idx >= 0) { + fc.setParent(null); + newFc.setParent(span); + children.set(idx, newFc); + } + } } - // Flow-content replacement deferred to Phase 4 } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java index 09d445a9..9c0c6832 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -1,6 +1,9 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import net.minecraft.client.Minecraft; + +import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -20,6 +23,18 @@ public String styleClass() { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof LytFlowLink link) { + String command = (String) link.getData("command"); + Boolean close = (Boolean) link.getData("close"); + if (command == null) return; + link.setClickCallback(screen -> { + if (Minecraft.getMinecraft().thePlayer != null) { + Minecraft.getMinecraft().thePlayer.sendChatMessage(command); + } + if (Boolean.TRUE.equals(close)) { + Minecraft.getMinecraft().displayGuiScreen(null); + } + }); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index 73d4e776..f6cb8f99 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -1,6 +1,11 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import com.hfstudio.guidenh.guide.compiler.tags.ItemGridCompiler.ItemGridPlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytItemGrid; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -9,17 +14,36 @@ public class ItemGridScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "ItemGrid"; - } + public String styleClass() { return "ItemGrid"; } @Override + @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof ItemGridPlaceholder ph) { + LytItemGrid grid = new LytItemGrid(); + for (String itemId : ph.itemIds) { + ItemStack stack = resolveItemId(itemId.trim()); + if (stack != null) { + grid.addItem(stack); + } + } + ctx.replace(grid); + } + } + + @SuppressWarnings("deprecation") + private static ItemStack resolveItemId(String itemId) { + int colonIdx = itemId.lastIndexOf(':'); + if (colonIdx < 0) return null; + + String rawKey = itemId.substring(0, colonIdx); + int meta = 0; + try { meta = Integer.parseInt(itemId.substring(colonIdx + 1)); } catch (NumberFormatException ignored) {} + + Item item = (Item) Item.itemRegistry.getObject(rawKey); + return item != null ? new ItemStack(item, 1, meta) : null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index af29c876..bf73f79b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -1,6 +1,12 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytItemImage; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -9,17 +15,58 @@ public class ItemImageScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "ItemImage"; - } + public String styleClass() { return "ItemImage"; } @Override + @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() != EventType.MOUNT) return; + + ItemImagePlaceholder ph; + boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof ItemImagePlaceholder p; + if (isWrapped) { + ph = (ItemImagePlaceholder) ((LytFlowInlineBlock) node).getBlock(); + } else if (node instanceof ItemImagePlaceholder p) { + ph = p; + } else { + return; + } + + ItemStack stack = resolveItemId(ph.itemId); + if (stack == null) return; + + LytItemImage image = new LytItemImage(stack); + image.setScale(ph.scale); + image.setShowTooltip(ph.showTooltip); + if (ph.showIcon != null) image.setShowIcon(ph.showIcon); + if (ph.labelPosition != null) image.setLabelPosition(ph.labelPosition); + if (ph.labelFormat != null) image.setLabelFormat(ph.labelFormat); + if (ph.yOffset != null) image.setInlineYOffsetOverride(ph.yOffset); + if (ph.labelYOffset != null) image.setLabelYOffsetOverride(ph.labelYOffset); + + if (isWrapped) { + LytFlowInlineBlock newWrapper = new LytFlowInlineBlock(); + newWrapper.setBlock(image); + ctx.replace(newWrapper); + } else { + ctx.replace(image); + } + } + + @SuppressWarnings("deprecation") + private static ItemStack resolveItemId(String itemId) { + if (itemId == null || itemId.isEmpty()) return null; + int colonIdx = itemId.lastIndexOf(':'); + if (colonIdx < 0) return null; + + String rawKey = itemId.substring(0, colonIdx); + int meta = 0; + try { meta = Integer.parseInt(itemId.substring(colonIdx + 1)); } catch (NumberFormatException ignored) {} + + Item item = (Item) Item.itemRegistry.getObject(rawKey); + return item != null ? new ItemStack(item, 1, meta) : null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index d3376cf8..e8e437fb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -1,6 +1,14 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; +import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -9,17 +17,59 @@ public class ItemLinkScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "ItemLink"; - } + public String styleClass() { return "ItemLink"; } @Override + @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof LytFlowLink link) { + String itemId = (String) link.getData("itemId"); + Boolean showTooltip = (Boolean) link.getData("showTooltip"); + String linksTo = (String) link.getData("linksTo"); + + ItemStack stack = resolveItemStack(itemId); + if (stack == null) return; + + PageAnchor anchor = findLinkTarget(stack, linksTo); + if (anchor != null) { + link.setPageLink(anchor); + } + if (Boolean.TRUE.equals(showTooltip)) { + link.setTooltip(new ItemTooltip(stack)); + } + } + } + + @SuppressWarnings("deprecation") + private static ItemStack resolveItemStack(String itemId) { + if (itemId == null || itemId.isEmpty()) return null; + String rawKey; + int meta = 0; + int colonIdx = itemId.lastIndexOf(':'); + if (colonIdx >= 0) { + String maybeMeta = itemId.substring(colonIdx + 1); + try { + meta = Integer.parseInt(maybeMeta); + rawKey = itemId.substring(0, colonIdx); + } catch (NumberFormatException e) { + rawKey = itemId; + meta = 0; + } + } else { + return null; + } + Item item = (Item) Item.itemRegistry.getObject(rawKey); + return item != null ? new ItemStack(item, 1, meta) : null; + } + + @Nullable + private static PageAnchor findLinkTarget(ItemStack stack, @Nullable String linksTo) { + // FIXME: Index lookup requires per-guide PageCollection reference. + // ItemLinkScript needs access to the guide's index registry via ScriptContext. + // For now, manual linksTo will be parsed later; auto-index lookup deferred. + return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java index a9ab9310..53adfb0a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java @@ -1,6 +1,8 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.compiler.tags.KeyBindTagCompiler; +import com.hfstudio.guidenh.guide.document.flow.LytFlowText; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -20,6 +22,14 @@ public String styleClass() { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof LytFlowText placeholder) { + String bindId = (String) placeholder.getData("bindId"); + if (bindId == null) return; + var mapping = KeyBindTagCompiler.findMapping(bindId); + String display = mapping != null + ? KeyBindTagCompiler.describeMapping(mapping) + : "[" + bindId + "]"; + placeholder.setText(display); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java index 62b326bd..dbb2d024 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java @@ -1,6 +1,9 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import net.minecraft.client.Minecraft; + +import com.hfstudio.guidenh.guide.document.flow.LytFlowText; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -20,6 +23,14 @@ public String styleClass() { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof LytFlowText placeholder) { + String username; + try { + username = Minecraft.getMinecraft().getSession().getUsername(); + } catch (Exception e) { + username = ""; + } + placeholder.setText(username); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java index b20e8638..ad26999f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SoundLinkScript.java @@ -1,10 +1,12 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.sound.GuideSoundSpec; public class SoundLinkScript implements LytScript { @@ -20,6 +22,11 @@ public String styleClass() { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof LytFlowLink link) { + GuideSoundSpec spec = (GuideSoundSpec) link.getData("soundSpec"); + if (spec != null) { + link.setClickSoundSpec(spec); + } + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index 24a1e92a..db2c4a97 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -1,6 +1,12 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; + +import com.hfstudio.guidenh.guide.compiler.tags.StructureViewCompiler.StructureEntry; +import com.hfstudio.guidenh.guide.compiler.tags.StructureViewCompiler.StructurePlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytStructureView; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -9,17 +15,36 @@ public class StructureScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "Structure"; - } + public String styleClass() { return "Structure"; } @Override + @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof StructurePlaceholder ph) { + LytStructureView view = new LytStructureView(); + for (StructureEntry entry : ph.entries) { + ItemStack stack = resolveEntry(entry.idSpec); + if (stack != null) { + view.addBlock(entry.x, entry.y, entry.z, stack); + } + } + ctx.replace(view); + } + } + + @SuppressWarnings("deprecation") + private static ItemStack resolveEntry(String idSpec) { + int colonIdx = idSpec.lastIndexOf(':'); + if (colonIdx < 0) return null; + + String rawKey = idSpec.substring(0, colonIdx); + int meta = 0; + try { meta = Integer.parseInt(idSpec.substring(colonIdx + 1)); } catch (NumberFormatException ignored) {} + + Item item = (Item) Item.itemRegistry.getObject(rawKey); + return item != null ? new ItemStack(item, 1, meta) : null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index 8825572c..e7f440f5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -1,25 +1,68 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.compiler.tags.SubPagesCompiler.SubPagesPlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytList; +import com.hfstudio.guidenh.guide.document.block.LytListItem; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; +import com.hfstudio.guidenh.guide.internal.GuideRegistry; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.navigation.NavigationNode; +import com.hfstudio.guidenh.guide.navigation.NavigationTree; public class SubPagesScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "SubPages"; - } + public String styleClass() { return "SubPages"; } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() == EventType.MOUNT && node instanceof SubPagesPlaceholder ph) { + NavigationTree tree = GuideRegistry.getMergedNavigationTree(); + + List subNodes; + if (ph.pageIdStr == null || ph.pageIdStr.isEmpty()) { + subNodes = tree.getRootNodes(); + } else { + ResourceLocation pageId = new ResourceLocation(ph.pageIdStr); + NavigationNode navNode = tree.getNodeById(pageId); + if (navNode == null) return; + subNodes = navNode.children(); + } + + if (ph.alphabetical) { + subNodes = new ArrayList<>(subNodes); + subNodes.sort(Comparator.comparing(NavigationNode::title)); + } + + LytList list = new LytList(false, 0); + for (NavigationNode childNode : subNodes) { + if (!childNode.hasPage()) continue; + + LytListItem listItem = new LytListItem(); + LytParagraph listItemPar = new LytParagraph(); + LytFlowLink link = new LytFlowLink(); + link.setGuideLink(childNode.guideId(), PageAnchor.page(childNode.pageId())); + link.appendText(childNode.title()); + listItemPar.append(link); + listItem.append(listItemPar); + list.append(listItem); + } + ctx.replace(list); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 4b10ceca..234a3b54 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -134,6 +134,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl childrenSource ); placeholder.setStyleClass(styleClass); + placeholder.setStyle(LytParagraph.LOADING_STYLE); placeholder.appendText("[" + styleClass + "]"); parent.append(placeholder); } From bf833041f86c77b8e136f4c7ccd3f43d5d0d634b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 04:12:18 +0800 Subject: [PATCH 045/136] fix: materialize 7 empty-shell scripts, flow-content replace, scheduler persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7 scripts now have real runtime implementations: - ImageScript/FloatingImageScript: loadAsset → LytImage replacement - MermaidScript: parse .mmd → LytMermaidMindmap - SpecialScript: MediaWiki special page resolution via Guide - RecipeScript: NEI recipe lookup cascade (handler → integration → vanilla) - BlockImageScript: mini 3D block scene construction - SceneScript: re-parse childrenSource → dispatch to 19 element compilers Fixes: - ScriptContextImpl.replace handles LytParagraph parents (not just LytFlowSpan) - Flow content replacement invalidates document layout - LytHostWorkItem stays in scheduler queue (was removed after first DONE) - ScenePlaceholder fields made public, pageDomain stored for runtime compiler - ScriptContext.getPageCollection() added for SpecialScript - SceneScript applies all config (layerSlider, grid, showGrid, explicitCenter) --- .../com/hfstudio/guidenh/ClientProxy.java | 4 + .../compiler/tags/BlockImageCompiler.java | 22 +- .../guidenh/guide/compiler/tags/MdxAttrs.java | 139 ------------ .../compiler/tags/chart/ChartChildParser.java | 25 +-- .../guide/document/block/chart/ChartIcon.java | 58 ++++- .../guidenh/guide/internal/GuideScreen.java | 1 + .../guidenh/guide/internal/host/LytHost.java | 79 +++++-- .../guide/internal/host/LytHostWorkItem.java | 2 +- .../guide/internal/host/LytScript.java | 4 + .../guide/internal/host/ScriptContext.java | 17 ++ .../internal/host/ScriptContextImpl.java | 48 ++++ .../host/scripts/BlockImageScript.java | 136 +++++++++++- .../internal/host/scripts/CategoryScript.java | 39 +++- .../internal/host/scripts/CsvTableScript.java | 34 ++- .../host/scripts/FloatingImageScript.java | 78 +++++-- .../internal/host/scripts/ImageScript.java | 71 ++++-- .../internal/host/scripts/ItemLinkScript.java | 25 ++- .../internal/host/scripts/MermaidScript.java | 47 +++- .../host/scripts/QuestCardScript.java | 104 +++++++++ .../host/scripts/QuestLinkScript.java | 82 +++++++ .../internal/host/scripts/RecipeScript.java | 205 +++++++++++++++++- .../internal/host/scripts/SceneScript.java | 144 +++++++++++- .../internal/host/scripts/SpecialScript.java | 44 +++- .../guidenh/guide/scene/SceneTagCompiler.java | 60 ++--- .../compiler/QuestCardCompiler.java | 105 ++------- .../compiler/QuestLinkCompiler.java | 76 +------ 26 files changed, 1182 insertions(+), 467 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index f16936ca..942f6bb6 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -80,6 +80,8 @@ import com.hfstudio.guidenh.guide.internal.host.scripts.KeyBindScript; import com.hfstudio.guidenh.guide.internal.host.scripts.MermaidScript; import com.hfstudio.guidenh.guide.internal.host.scripts.PlayerNameScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.QuestCardScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.QuestLinkScript; import com.hfstudio.guidenh.guide.internal.host.scripts.RecipeScript; import com.hfstudio.guidenh.guide.internal.host.scripts.SceneScript; import com.hfstudio.guidenh.guide.internal.host.scripts.SoundLinkScript; @@ -177,6 +179,8 @@ public void init(FMLInitializationEvent event) { lytHost.registerScript("BlockImage", new BlockImageScript()); lytHost.registerScript("CsvTable", new CsvTableScript()); lytHost.registerScript("Mermaid", new MermaidScript()); + lytHost.registerScript("QuestLink", new QuestLinkScript()); + lytHost.registerScript("QuestCard", new QuestCardScript()); // Phase 3: SceneScript handles Scene and GameScene SceneScript sceneScript = new SceneScript(); lytHost.registerScript("Scene", sceneScript); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java index fc79b98f..fc062ca4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java @@ -46,18 +46,18 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl * Placeholder block that stores all extracted block-image configuration for deferred scene * creation by {@code BlockImageScript}. */ - static class BlockImagePlaceholder extends LytParagraph { - @Nullable final String id; - @Nullable final String ore; - final int meta; - @Nullable final String nbt; - final float scale; - @Nullable final String perspective; - final int width; - final int height; + public static class BlockImagePlaceholder extends LytParagraph { + @Nullable public final String id; + @Nullable public final String ore; + public final int meta; + @Nullable public final String nbt; + public final float scale; + @Nullable public final String perspective; + public final int width; + public final int height; - BlockImagePlaceholder(@Nullable String id, @Nullable String ore, int meta, @Nullable String nbt, - float scale, @Nullable String perspective, int width, int height) { + public BlockImagePlaceholder(@Nullable String id, @Nullable String ore, int meta, @Nullable String nbt, + float scale, @Nullable String perspective, int width, int height) { this.id = id; this.ore = ore; this.meta = meta; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java index c5ba4cf1..f26d038b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java @@ -3,13 +3,10 @@ import java.util.Locale; import java.util.regex.Pattern; -import net.minecraft.block.Block; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; -import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; @@ -72,137 +69,8 @@ public static ResourceLocation getRequiredId(PageCompiler compiler, LytErrorSink } } - @Nullable - public static Pair getRequiredItemAndId(PageCompiler compiler, LytErrorSink errorSink, - MdxJsxElementFields el, String attribute) { - var raw = getString(compiler, errorSink, el, attribute, null); - if (raw == null) { - errorSink.appendError(compiler, "Missing " + attribute + " attribute.", el); - return null; - } - raw = raw.trim(); - IdUtils.ParsedItemRef ref; - try { - ref = IdUtils.parseItemRef( - raw, - compiler.getPageId() - .getResourceDomain()); - } catch (IllegalArgumentException e) { - errorSink.appendError(compiler, "Malformed id " + raw + ": " + e.getMessage(), el); - return null; - } - if (ref == null) { - errorSink.appendError(compiler, "Missing " + attribute + " attribute.", el); - return null; - } - Item resultItem = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (resultItem == null) { - errorSink.appendError(compiler, "Missing item: " + ref.id(), el); - return null; - } - return Pair.of(ref.id(), resultItem); - } - - @Nullable - public static Pair getRequiredBlockAndId(PageCompiler compiler, LytErrorSink errorSink, - MdxJsxElementFields el, String attribute) { - String oreName = GuideItemReferenceResolver.trimToNull(getString(compiler, errorSink, el, "ore", null)); - if (oreName != null) { - ItemStack stack = GuideItemReferenceResolver.resolveOreDictionaryStack(oreName); - if (stack == null || stack.getItem() == null) { - errorSink.appendError(compiler, "Missing ore dictionary entry: " + oreName, el); - return null; - } - - Block block = Block.getBlockFromItem(stack.getItem()); - ResourceLocation blockId = GuideItemReferenceResolver.resolveBlockRegistryId(block); - if (block == null || blockId == null) { - errorSink.appendError( - compiler, - "Ore dictionary entry '" + oreName + "' does not resolve to a block item", - el); - return null; - } - return Pair.of(blockId, block); - } - - var blockId = getRequiredId(compiler, errorSink, el, attribute); - if (blockId == null) { - return null; - } - var rawAttr = getString(el, attribute, null); - String blockLookupKey = rawAttr != null && !rawAttr.trim() - .isEmpty() ? IdUtils.rawRegistryKey( - rawAttr.trim(), - compiler.getPageId() - .getResourceDomain()) - : blockId.toString(); - Block resultBlock = (Block) Block.blockRegistry.getObject(blockLookupKey); - if (resultBlock == null) { - errorSink.appendError(compiler, "Missing block: " + blockId, el); - return null; - } - return Pair.of(blockId, resultBlock); - } - @Nullable - public static Item getRequiredItem(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields el, - String attribute) { - var result = getRequiredItemAndId(compiler, errorSink, el, attribute); - return result != null ? result.getRight() : null; - } - - @Nullable - public static Pair getRequiredItemStackAndId(PageCompiler compiler, - LytErrorSink errorSink, MdxJsxElementFields el) { - String oreName = GuideItemReferenceResolver.trimToNull(getString(compiler, errorSink, el, "ore", null)); - if (oreName != null) { - ItemStack stack = GuideItemReferenceResolver.resolveOreDictionaryStack(oreName); - if (stack == null || stack.getItem() == null) { - errorSink.appendError(compiler, "Missing ore dictionary entry: " + oreName, el); - return null; - } - ResourceLocation itemId = GuideItemReferenceResolver.resolveItemRegistryId(stack); - if (itemId == null) { - errorSink.appendError(compiler, "Unregistered item from ore dictionary entry: " + oreName, el); - return null; - } - return Pair.of(itemId, stack); - } - - var raw = getString(compiler, errorSink, el, "id", null); - if (raw == null) { - errorSink.appendError(compiler, "Missing id or ore attribute.", el); - return null; - } - String idStr = raw.trim(); - IdUtils.ParsedItemRef ref; - try { - ref = IdUtils.parseItemRef( - idStr, - compiler.getPageId() - .getResourceDomain()); - } catch (IllegalArgumentException e) { - errorSink.appendError(compiler, "Malformed id " + idStr + ": " + e.getMessage(), el); - return null; - } - if (ref == null) { - errorSink.appendError(compiler, "Missing id or ore attribute.", el); - return null; - } - Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (item == null) { - errorSink.appendError(compiler, "Missing item: " + ref.id(), el); - return null; - } - ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); - if (ref.nbt() != null) { - stack.stackTagCompound = (NBTTagCompound) ref.nbt() - .copy(); - } - return Pair.of(ref.id(), stack); - } @Nullable public static GuideItemReferenceResolver.ResolvedBlockReference getRequiredBlockReference(PageCompiler compiler, @@ -247,13 +115,6 @@ public static GuideItemReferenceResolver.ResolvedBlockReference getBlockReferenc return GuideItemReferenceResolver.resolveBlockReference(defaultNamespace, raw, oreName); } - @Nullable - public static ItemStack getRequiredItemStack(PageCompiler compiler, LytErrorSink errorSink, - MdxJsxElementFields el) { - var result = getRequiredItemStackAndId(compiler, errorSink, el); - return result != null ? result.getValue() : null; - } - public static float getFloat(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields el, String name, float defaultValue) { try { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/chart/ChartChildParser.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/chart/ChartChildParser.java index a2579c63..04aee283 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/chart/ChartChildParser.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/chart/ChartChildParser.java @@ -3,9 +3,6 @@ import java.util.ArrayList; import java.util.List; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import com.hfstudio.guidenh.guide.compiler.IdUtils; @@ -16,7 +13,6 @@ import com.hfstudio.guidenh.guide.document.block.chart.ChartSeries; import com.hfstudio.guidenh.guide.document.block.chart.PieInsetSpec; import com.hfstudio.guidenh.guide.document.block.chart.PieSlice; -import com.hfstudio.guidenh.guide.render.GuidePageTexture; import com.hfstudio.guidenh.guide.scene.SceneTagCompiler; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; @@ -267,29 +263,16 @@ private static ChartIcon parseItemStackIcon(PageCompiler compiler, LytErrorSink if (ref == null) { return null; } - Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (item == null) { - errorSink.appendError(compiler, "Missing item for icon: " + ref.id(), el); - return null; - } - ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); - if (ref.nbt() != null) { - stack.stackTagCompound = (NBTTagCompound) ref.nbt() - .copy(); - } - return ChartIcon.ofItemStack(stack); + // Deferred resolution: store raw key + meta, resolve at render time + return ChartIcon.ofDeferredItem(ref.rawKey(), ref.concreteMeta()); } private static ChartIcon parseImageIcon(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields el, String src) { try { ResourceLocation imgId = IdUtils.resolveLink(src, compiler.getPageId()); - byte[] data = compiler.loadAsset(imgId); - if (data == null) { - errorSink.appendError(compiler, "Missing icon image: " + src, el); - return null; - } - return ChartIcon.ofImage(imgId, GuidePageTexture.load(imgId, data)); + // Deferred resolution: store resolved id, load image data at render time + return ChartIcon.ofDeferredImage(imgId); } catch (IllegalArgumentException e) { errorSink.appendError(compiler, "Invalid icon image " + src + ": " + e.getMessage(), el); return null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java index a6e27c56..e53dc6d3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java @@ -1,23 +1,33 @@ package com.hfstudio.guidenh.guide.document.block.chart; +import net.minecraft.client.Minecraft; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; import com.hfstudio.guidenh.guide.render.GuidePageTexture; /** * Icon associated with a chart series/slice: either an {@link ItemStack} or a PNG resource. + * Supports deferred resolution so compilers can produce icons without accessing registries or I/O. */ public class ChartIcon { - private final ItemStack stack; - private final GuidePageTexture texture; - private final ResourceLocation imageId; + private ItemStack stack; + private GuidePageTexture texture; + private ResourceLocation imageId; + + private String deferredRawKey; + private int deferredMeta; + private ResourceLocation deferredImageId; + private boolean resolved; private ChartIcon(ItemStack stack, GuidePageTexture texture, ResourceLocation imageId) { this.stack = stack; this.texture = texture; this.imageId = imageId; + this.resolved = true; } public static ChartIcon ofItemStack(ItemStack stack) { @@ -28,23 +38,65 @@ public static ChartIcon ofImage(ResourceLocation id, GuidePageTexture texture) { return new ChartIcon(null, texture, id); } + public static ChartIcon ofDeferredItem(String rawKey, int meta) { + ChartIcon icon = new ChartIcon(null, null, null); + icon.resolved = false; + icon.deferredRawKey = rawKey; + icon.deferredMeta = meta; + return icon; + } + + public static ChartIcon ofDeferredImage(ResourceLocation id) { + ChartIcon icon = new ChartIcon(null, null, null); + icon.resolved = false; + icon.deferredImageId = id; + return icon; + } + + @SuppressWarnings("deprecation") + private void resolve() { + if (resolved) return; + resolved = true; + if (deferredRawKey != null) { + Item item = (Item) Item.itemRegistry.getObject(deferredRawKey); + if (item != null) { + stack = new ItemStack(item, 1, deferredMeta); + } + deferredRawKey = null; + } + if (deferredImageId != null) { + byte[] data = GuideResourceAccess.readBytes( + Minecraft.getMinecraft().getResourceManager(), deferredImageId); + if (data != null) { + imageId = deferredImageId; + texture = GuidePageTexture.load(imageId, data); + } + deferredImageId = null; + } + } + public ItemStack getStack() { + resolve(); return stack; } public GuidePageTexture getTexture() { + resolve(); return texture; } public ResourceLocation getImageId() { + resolve(); return imageId; } public boolean hasItemStack() { + resolve(); return stack != null && stack.getItem() != null; } public boolean hasImage() { + resolve(); return texture != null && !texture.isMissing(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index f1ac04ee..cdf205ec 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2523,6 +2523,7 @@ private void completePendingContentPageLoadIfNeeded() { currentPage = loadedPage; document = loadedPage != null ? loadedPage.document() : null; ClientProxy.getLytHost().setCurrentPageId(currentAnchor.pageId().toString()); + ClientProxy.getLytHost().setCurrentPageCollection(guide); ClientProxy.getLytHost().setDocument(document); if (document != null && isSpecialPageWithSearchField()) { applySpecialPageSearchQuery(queryFromCurrentAnchor()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 7fc6b9e7..2dfb6309 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; @@ -21,6 +22,7 @@ public class LytHost { @Nullable private LytDocument document; + @Nullable private PageCollection currentPageCollection; private final Map scripts = new HashMap<>(); private final Map cachedDocuments = new LinkedHashMap<>(); private final Map pageNodeCounters = new HashMap<>(); @@ -78,6 +80,15 @@ public void setCurrentPageId(String pageId) { this.currentPageId = pageId; } + public void setCurrentPageCollection(@Nullable PageCollection pageCollection) { + this.currentPageCollection = pageCollection; + } + + @Nullable + public PageCollection getCurrentPageCollection() { + return currentPageCollection; + } + public boolean hasPreheatWork() { return false; // placeholder, real impl later } @@ -133,19 +144,12 @@ private void dispatchMountEvents(LytNode node) { if (cls != null) { LytScript script = scripts.get(cls); if (script != null) { - try { - ScriptContextImpl ctx = new ScriptContextImpl(node, this, document); - script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); - } catch (Exception e) { - // Error boundary: log and continue - e.printStackTrace(); - } + dispatchScript(script, node); } } for (var child : node.getChildren()) { dispatchMountEvents(child); } - // Also traverse into flow content for inline-level styleClass nodes dispatchMountEventsFlow(node); } @@ -162,12 +166,7 @@ private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { if (cls != null) { LytScript script = scripts.get(cls); if (script != null) { - try { - ScriptContextImpl ctx = new ScriptContextImpl(fc, this, document); - script.onEvent(fc, new LytEvent(EventType.MOUNT, fc), ctx); - } catch (Exception e) { - e.printStackTrace(); - } + dispatchScript(script, fc); } } if (fc instanceof LytFlowSpan span) { @@ -180,17 +179,56 @@ private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { if (innerCls != null) { LytScript script = scripts.get(innerCls); if (script != null) { - try { - ScriptContextImpl ctx = new ScriptContextImpl(inlineBlock, this, document); - script.onEvent(inlineBlock, new LytEvent(EventType.MOUNT, inlineBlock), ctx); - } catch (Exception e) { - e.printStackTrace(); - } + dispatchScript(script, inlineBlock); } } } } + private void dispatchScript(LytScript script, Object node) { + if (script.isAsync()) { + taskQueue.addLast(new MaterializeTask(script, node, new ScriptContextImpl(node, this, document))); + } else { + try { + ScriptContextImpl ctx = new ScriptContextImpl(node, this, document); + script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private static class MaterializeTask implements DeferredTask { + private boolean done; + private final LytScript script; + private final Object node; + private final ScriptContextImpl ctx; + + MaterializeTask(LytScript script, Object node, ScriptContextImpl ctx) { + this.script = script; + this.node = node; + this.ctx = ctx; + } + + @Override + public Priority priority() { return Priority.HIGH; } + + @Override + public TaskResult step(long deadlineNs) { + if (done) return TaskResult.DONE; + try { + script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); + } catch (Exception e) { + e.printStackTrace(); + } + done = true; + return TaskResult.DONE; + } + + @Override + public boolean isDone() { return done; } + } + // ===== Sync events ===== public void pushEvent(LytEvent event) { @@ -255,6 +293,7 @@ public int pendingTaskCount() { public void clear() { document = null; + currentPageCollection = null; scripts.clear(); cachedDocuments.clear(); pageNodeCounters.clear(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java index 00ed689e..3e1c8cba 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java @@ -21,7 +21,7 @@ public LytHostWorkItem(LytHost host) { @Override public WorkResult tick(long deadlineNs) { host.step(deadlineNs); - return host.hasWork() ? WorkResult.YIELD : WorkResult.DONE; + return WorkResult.YIELD; // never leave the queue — shouldRun guards when idle } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java index 048d030b..333d4c9d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java @@ -4,4 +4,8 @@ public interface LytScript { ScriptType type(); String styleClass(); void onEvent(Object node, LytEvent event, ScriptContext ctx); + + default boolean isAsync() { + return false; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java index 6d44129c..56d35331 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java @@ -1,12 +1,29 @@ package com.hfstudio.guidenh.guide.internal.host; import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.indices.PageIndex; public interface ScriptContext { Map data(); void replace(Object newNode); String allocateId(String prefix); LytDocument document(); + + @Nullable + byte[] loadAsset(ResourceLocation id); + + T getIndex(Class indexClass); + + @Nullable + PageCollection getPageCollection(); + + void submitTask(DeferredTask task); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 0c82de16..af808cbb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -4,11 +4,17 @@ import java.util.List; import java.util.Map; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.guide.indices.PageIndex; class ScriptContextImpl implements ScriptContext { private final Map data = new HashMap<>(); @@ -26,6 +32,7 @@ class ScriptContextImpl implements ScriptContext { public Map data() { return data; } @Override + @SuppressWarnings("unchecked") public void replace(Object newNode) { if (node instanceof LytNode ln && newNode instanceof LytNode newLn) { LytNode parent = ln.getParent(); @@ -43,6 +50,22 @@ public void replace(Object newNode) { fc.setParent(null); newFc.setParent(span); children.set(idx, newFc); + document.invalidateLayout(); + } + return; + } + // Handle LytParagraph and other LytFlowContainer parents + if (parent instanceof com.hfstudio.guidenh.guide.document.block.LytParagraph para) { + Iterable iterable = para.getContent(); + if (iterable instanceof List) { + List list = (List) iterable; + int idx = list.indexOf(fc); + if (idx >= 0) { + fc.setParent(null); + newFc.setParent(para); + list.set(idx, newFc); + document.invalidateLayout(); + } } } } @@ -55,4 +78,29 @@ public String allocateId(String prefix) { @Override public LytDocument document() { return document; } + + @Override + @Nullable + public byte[] loadAsset(ResourceLocation id) { + PageCollection pc = host.getCurrentPageCollection(); + return pc != null ? pc.loadAsset(id) : null; + } + + @Override + @SuppressWarnings("unchecked") + public T getIndex(Class indexClass) { + PageCollection pc = host.getCurrentPageCollection(); + return pc != null ? pc.getIndex(indexClass) : null; + } + + @Override + @Nullable + public PageCollection getPageCollection() { + return host.getCurrentPageCollection(); + } + + @Override + public void submitTask(DeferredTask task) { + host.submitTask(task); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 2400af9b..29902211 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -1,25 +1,145 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import net.minecraft.block.Block; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.oredict.OreDictionary; + +import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; +import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler.BlockImagePlaceholder; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.internal.structure.GuideTextNbtCodec; +import com.hfstudio.guidenh.guide.scene.CameraSettings; +import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; +import com.hfstudio.guidenh.guide.scene.PerspectivePreset; +import com.hfstudio.guidenh.guide.scene.element.BlockElementCompiler; +import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; +import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; +import com.hfstudio.guidenh.guide.scene.ponder.PonderNbtPath; public class BlockImageScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "BlockImage"; - } + public String styleClass() { return "BlockImage"; } @Override + public boolean isAsync() { return true; } + + @Override + @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: scene creation will be wired here + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof BlockImagePlaceholder ph)) return; + + Block block = null; + int meta = ph.meta; + + if (ph.ore != null && !ph.ore.isEmpty()) { + ItemStack oreStack = GuideItemReferenceResolver.resolveOreDictionaryStack(ph.ore); + if (oreStack != null && oreStack.getItem() != null) { + block = Block.getBlockFromItem(oreStack.getItem()); + meta = oreStack.getItemDamage(); + } + } else if (ph.id != null) { + Item item = (Item) Item.itemRegistry.getObject(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id); + if (item != null) { + block = Block.getBlockFromItem(item); + } + if (block == null) { + block = (Block) Block.blockRegistry.getObject(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id); + } + } + + if (block == null) return; + + NBTTagCompound tileTag = null; + if (ph.nbt != null && !ph.nbt.trim().isEmpty()) { + try { + tileTag = GuideTextNbtCodec.readTextSafeCompound(ph.nbt.trim()); + } catch (Exception ignored) {} + } + + PerspectivePreset perspective = PerspectivePreset.ISOMETRIC_NORTH_EAST; + if (ph.perspective != null && !ph.perspective.trim().isEmpty()) { + perspective = PerspectivePreset.fromSerializedName(ph.perspective.trim()); + } + + int defaultMeta = meta == 0 ? BlockElementCompiler.defaultMetaFor(block, null) : meta; + GuidebookLevel level = new GuidebookLevel(); + GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag); + + if (level.isEmpty()) return; + + int width = ph.width > 0 ? ph.width : 128; + int height = ph.height > 0 ? ph.height : 128; + float zoom = clampZoom(ph.scale); + + CameraSettings camera = new CameraSettings(); + camera.setPerspectivePreset(perspective); + camera.setZoom(zoom); + camera.setViewportSize(width, height); + + var scene = new LytGuidebookScene(); + scene.setLevel(level); + scene.setCamera(camera); + scene.setSceneSize(width, height); + scene.setInteractive(false); + scene.setSceneButtonsVisible(false); + scene.setBottomControlsVisible(false); + scene.setReserveBottomControlArea(false); + camera.setViewportSize(width, height); + scene.snapshotInitialCamera(); + + float[] center = level.getCenter(); + camera.setRotationCenter(center[0], center[1], center[2]); + + int[] bounds = level.getBounds(); + float minX = bounds[0]; + float minY = bounds[1]; + float minZ = bounds[2]; + float maxX = bounds[3] + 1f; + float maxY = bounds[4] + 1f; + float maxZ = bounds[5] + 1f; + + float minScreenX = Float.MAX_VALUE, maxScreenX = -Float.MAX_VALUE; + float minScreenY = Float.MAX_VALUE, maxScreenY = -Float.MAX_VALUE; + for (int corner = 0; corner < 8; corner++) { + float wx = (corner & 1) == 0 ? minX : maxX; + float wy = (corner & 2) == 0 ? minY : maxY; + float wz = (corner & 4) == 0 ? minZ : maxZ; + var sp = camera.worldToScreen(wx, wy, wz); + if (sp.x < minScreenX) minScreenX = sp.x; + if (sp.x > maxScreenX) maxScreenX = sp.x; + if (sp.y < minScreenY) minScreenY = sp.y; + if (sp.y > maxScreenY) maxScreenY = sp.y; + } + + int autoW = clampDim((int) Math.ceil(maxScreenX - minScreenX) + 16); + int autoH = clampDim((int) Math.ceil(maxScreenY - minScreenY) + 16); + scene.setSceneSize(autoW, autoH); + camera.setViewportSize(autoW, autoH); + + var pc = camera.worldToScreen(center[0], center[1], center[2]); + camera.setOffsetX(-pc.x); + camera.setOffsetY(pc.y); + scene.snapshotInitialCamera(); + + ctx.replace(scene); + } + + private static float clampZoom(float zoom) { + return Math.max(LytGuidebookScene.MIN_ZOOM, Math.min(LytGuidebookScene.MAX_ZOOM, zoom <= 0 ? 1f : zoom)); + } + + private static int clampDim(int d) { + return Math.max(64, Math.min(256, d)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index eb3538f3..9e254d61 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -1,6 +1,14 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import java.util.List; + +import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.CategoryCompiler.CategoryPlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.block.LytVBox; +import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; @@ -9,17 +17,32 @@ public class CategoryScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "Category"; - } + public String styleClass() { return "Category"; } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof CategoryPlaceholder ph)) return; + + CategoryIndex index = ctx.getIndex(CategoryIndex.class); + if (index == null) return; + + List members = index.get(ph.name); + LytVBox box = new LytVBox(); + int count = 0; + for (PageAnchor anchor : members) { + if (ph.rows > 0 && count >= ph.rows) break; + LytParagraph line = new LytParagraph(); + LytFlowLink link = new LytFlowLink(); + link.setGuideLink(ph.guideId, anchor); + link.appendText(anchor.pageId().getResourcePath()); + line.append(link); + box.append(line); + count++; + } + ctx.replace(box); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java index d045ea53..b8783c64 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java @@ -1,20 +1,50 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler.CsvTablePlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.document.block.LytNode; public class CsvTableScript implements LytScript { + @Override public ScriptType type() { return ScriptType.JAVA; } @Override public String styleClass() { return "CsvTable"; } + @Override + public boolean isAsync() { return true; } + @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: CSV loading and table building will be wired later + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof CsvTablePlaceholder ph)) return; + + ResourceLocation csvId; + try { + csvId = new ResourceLocation(ph.src); + } catch (Exception e) { + return; + } + + byte[] data = ctx.loadAsset(csvId); + if (data == null) return; + + List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); + LytBlock table = CsvTableCompiler.buildTable(rows, ph.header, ph.widths); + if (table != null) { + ctx.replace(table); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index 76797e80..253ede89 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -1,33 +1,79 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.document.block.ImageRegionAnnotation; +import com.hfstudio.guidenh.guide.document.block.LytImage; +import com.hfstudio.guidenh.guide.document.block.LytImageBlock; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.document.block.LytNode; -/** - * Script that materializes floating image content for blocks with styleClass "FloatingImage". - * For Phase 3 initial implementation, this is a stub that will be wired to - * the asset loading system in a later task. - */ public class FloatingImageScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "FloatingImage"; - } + public String styleClass() { return "FloatingImage"; } + + @Override + public boolean isAsync() { return true; } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // TODO: Load image asset and replace placeholder - // For Phase 3 initial implementation, this is a stub. - // The actual image loading will be added when the script infrastructure - // is fully wired to the asset loading system. + if (event.type() != EventType.MOUNT) return; + + LytImageBlock placeholder; + LytFlowInlineBlock oldWrapper = null; + boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof LytImageBlock p; + if (isWrapped) { + oldWrapper = (LytFlowInlineBlock) node; + placeholder = (LytImageBlock) oldWrapper.getBlock(); + } else if (node instanceof LytImageBlock p) { + placeholder = p; + } else { + return; + } + + String src = placeholder.getSrc(); + if (src == null || src.isEmpty()) return; + + ResourceLocation imageId; + try { + imageId = new ResourceLocation(src); + } catch (Exception e) { + return; + } + + byte[] imageData = ctx.loadAsset(imageId); + LytImage image = new LytImage(); + image.setImage(imageId, imageData); + + String alt = placeholder.getAlt(); + if (alt != null && !alt.isEmpty()) image.setAlt(alt); + String title = placeholder.getTitle(); + if (title != null && !title.isEmpty()) image.setTitle(title); + image.setExplicitWidth(placeholder.getExplicitWidth()); + image.setExplicitHeight(placeholder.getExplicitHeight()); + image.setMarginTop(placeholder.getMarginTop()); + image.setMarginLeft(placeholder.getMarginLeft()); + image.setMarginRight(placeholder.getMarginRight()); + image.setMarginBottom(placeholder.getMarginBottom()); + for (ImageRegionAnnotation ann : placeholder.getAnnotations()) { + image.addAnnotation(ann); + } + + if (isWrapped) { + LytFlowInlineBlock newWrapper = new LytFlowInlineBlock(); + newWrapper.setBlock(image); + newWrapper.setAlignment(oldWrapper.getAlignment()); + ctx.replace(newWrapper); + } else { + ctx.replace(image); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index a3f84e25..373d310e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -1,33 +1,72 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.document.block.ImageRegionAnnotation; +import com.hfstudio.guidenh.guide.document.block.LytImage; +import com.hfstudio.guidenh.guide.document.block.LytImageBlock; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.document.block.LytNode; -/** - * Script that materializes image content for blocks with styleClass "Img". - * For Phase 3 initial implementation, this is a stub that will be wired to - * the asset loading system in a later task. - */ public class ImageScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "Img"; - } + public String styleClass() { return "Img"; } + + @Override + public boolean isAsync() { return true; } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // TODO: Load image asset and replace placeholder - // For Phase 3 initial implementation, this is a stub. - // The actual image loading will be added when the script infrastructure - // is fully wired to the asset loading system. + if (event.type() != EventType.MOUNT) return; + + LytImageBlock placeholder; + boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof LytImageBlock p; + if (isWrapped) { + placeholder = (LytImageBlock) ((LytFlowInlineBlock) node).getBlock(); + } else if (node instanceof LytImageBlock p) { + placeholder = p; + } else { + return; + } + + String src = placeholder.getSrc(); + if (src == null || src.isEmpty()) return; + + ResourceLocation imageId; + try { + imageId = new ResourceLocation(src); + } catch (Exception e) { + return; + } + + byte[] imageData = ctx.loadAsset(imageId); + LytImage image = new LytImage(); + image.setImage(imageId, imageData); + + String alt = placeholder.getAlt(); + if (alt != null && !alt.isEmpty()) image.setAlt(alt); + String title = placeholder.getTitle(); + if (title != null && !title.isEmpty()) image.setTitle(title); + image.setExplicitWidth(placeholder.getExplicitWidth()); + image.setExplicitHeight(placeholder.getExplicitHeight()); + for (ImageRegionAnnotation ann : placeholder.getAnnotations()) { + image.addAnnotation(ann); + } + + if (isWrapped) { + LytFlowInlineBlock newWrapper = new LytFlowInlineBlock(); + newWrapper.setBlock(image); + ctx.replace(newWrapper); + } else { + ctx.replace(image); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index e8e437fb..1633b94e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -2,6 +2,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; import org.jetbrains.annotations.Nullable; @@ -33,7 +34,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ItemStack stack = resolveItemStack(itemId); if (stack == null) return; - PageAnchor anchor = findLinkTarget(stack, linksTo); + PageAnchor anchor = findLinkTarget(stack, linksTo, ctx); if (anchor != null) { link.setPageLink(anchor); } @@ -66,10 +67,24 @@ private static ItemStack resolveItemStack(String itemId) { } @Nullable - private static PageAnchor findLinkTarget(ItemStack stack, @Nullable String linksTo) { - // FIXME: Index lookup requires per-guide PageCollection reference. - // ItemLinkScript needs access to the guide's index registry via ScriptContext. - // For now, manual linksTo will be parsed later; auto-index lookup deferred. + private static PageAnchor findLinkTarget(ItemStack stack, @Nullable String linksTo, ScriptContext ctx) { + if (linksTo != null && !linksTo.isEmpty()) { + try { + ResourceLocation pageId = new ResourceLocation(linksTo); + return PageAnchor.page(pageId); + } catch (Exception ignored) {} + } + com.hfstudio.guidenh.guide.indices.ItemIndex itemIdx = ctx.getIndex( + com.hfstudio.guidenh.guide.indices.ItemIndex.class); + if (itemIdx != null) { + PageAnchor anchor = itemIdx.findByStack(stack); + if (anchor != null) return anchor; + } + com.hfstudio.guidenh.guide.indices.OreIndex oreIdx = ctx.getIndex( + com.hfstudio.guidenh.guide.indices.OreIndex.class); + if (oreIdx != null) { + return oreIdx.findByStack(stack); + } return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index f2f0580f..d0e9ab52 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -1,20 +1,63 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import java.nio.charset.StandardCharsets; + +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.compiler.tags.MermaidCompiler.MermaidPlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytMermaidMindmap; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; + +import cpw.mods.fml.common.FMLLog; public class MermaidScript implements LytScript { + @Override public ScriptType type() { return ScriptType.JAVA; } @Override public String styleClass() { return "Mermaid"; } + @Override + public boolean isAsync() { return true; } + @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: Mermaid rendering will be wired later + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof MermaidPlaceholder ph)) return; + + String sourceText = ph.sourceText; + if (sourceText == null && ph.src != null) { + ResourceLocation srcId; + try { + srcId = new ResourceLocation(ph.src); + } catch (Exception e) { + return; + } + byte[] data = ctx.loadAsset(srcId); + if (data != null) { + sourceText = new String(data, StandardCharsets.UTF_8); + } + } + + if (sourceText == null || sourceText.trim().isEmpty()) return; + + try { + var document = MermaidMindmapParser.parse(sourceText); + LytMermaidMindmap block = new LytMermaidMindmap(document, sourceText, + ph.nodeContentBlocks != null ? ph.nodeContentBlocks : java.util.Collections.emptyMap()); + if (ph.width > 0 || ph.height > 0) { + block.setPreferredSize(ph.width, ph.height); + } + ctx.replace(block); + } catch (IllegalArgumentException e) { + FMLLog.getLogger().warn( + "[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java new file mode 100644 index 00000000..42346f2d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java @@ -0,0 +1,104 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import java.util.UUID; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.StatCollector; + +import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; +import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; +import com.hfstudio.guidenh.integration.betterquesting.QuestState; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestCardCompiler.QuestCardPlaceholder; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; +import com.hfstudio.guidenh.guide.internal.host.EventType; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class QuestCardScript implements LytScript { + + @Override + public ScriptType type() { return ScriptType.JAVA; } + + @Override + public String styleClass() { return "QuestCard"; } + + @Override + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof QuestCardPlaceholder ph)) return; + + QuestDisplay display = BqHelpers.resolveDisplay(ph.questId, Minecraft.getMinecraft().thePlayer, + ph.showTooltip || ph.showDesc); + QuestState state = display.getState(); + + var box = new LytQuoteBox(); + SymbolicColor accent = pickAccentColor(state); + box.setQuoteStyle(accent, null, null); + + var title = new LytParagraph(); + title.setMarginTop(0); + title.setMarginBottom(2); + + String name = resolveTitleText(display, ph.questId); + if (QuestTagSupport.isNavigable(state)) { + title.append(QuestTagSupport.createQuestLink(null, ph.questId, display, name, ph.showTooltip)); + } else { + var span = new LytFlowSpan(); + span.modifyStyle(style -> style.color(pickPlaceholderColor(state)).italic(true)); + span.appendText(name); + title.append(span); + } + box.append(title); + + if (ph.showDesc && isVisibleToPlayer(state)) { + String description = display.getDescription(); + if (description != null && !description.isEmpty()) { + var descPar = new LytParagraph(); + descPar.appendText(description); + box.append(descPar); + } + } + + ctx.replace(box); + } + + private static String resolveTitleText(QuestDisplay display, UUID questId) { + QuestState state = display.getState(); + if (state == QuestState.COMPLETED) { + return QuestTagSupport.nameOrFallback(display, questId) + " ✓"; + } + if (QuestTagSupport.isNavigable(state)) { + return QuestTagSupport.nameOrFallback(display, questId); + } + if (state == QuestState.HIDDEN) { + return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; + } + if (state == QuestState.MISSING) { + return "[" + StatCollector.translateToLocal("guidenh.compat.bq.missing") + "]"; + } + return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; + } + + private static SymbolicColor pickAccentColor(QuestState state) { + if (state == QuestState.COMPLETED) return SymbolicColor.GREEN; + if (state == QuestState.LOCKED || state == QuestState.HIDDEN) return SymbolicColor.GRAY; + if (state == QuestState.MISSING) return SymbolicColor.RED; + return SymbolicColor.LINK; + } + + private static SymbolicColor pickPlaceholderColor(QuestState state) { + if (state == QuestState.HIDDEN) return SymbolicColor.DARK_GRAY; + if (state == QuestState.MISSING) return SymbolicColor.RED; + return SymbolicColor.GRAY; + } + + private static boolean isVisibleToPlayer(QuestState state) { + return QuestTagSupport.isVisibleToPlayer(state); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java new file mode 100644 index 00000000..bdc606c8 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java @@ -0,0 +1,82 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import java.util.UUID; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.StatCollector; + +import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.guide.document.flow.LytFlowText; +import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; +import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; +import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; +import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; +import com.hfstudio.guidenh.integration.betterquesting.QuestState; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; +import com.hfstudio.guidenh.guide.internal.host.EventType; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class QuestLinkScript implements LytScript { + + @Override + public ScriptType type() { return ScriptType.JAVA; } + + @Override + public String styleClass() { return "QuestLink"; } + + @Override + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof LytFlowText placeholder)) return; + + UUID questId = (UUID) placeholder.getData("questId"); + if (questId == null) return; + + Boolean showTooltip = (Boolean) placeholder.getData("showTooltip"); + String overrideText = (String) placeholder.getData("overrideText"); + + QuestDisplay display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, + Boolean.TRUE.equals(showTooltip)); + QuestState state = display.getState(); + String text = overrideText != null && !overrideText.isEmpty() + ? overrideText + : pickText(display, questId); + + LytFlowContent replacement; + if (QuestTagSupport.isNavigable(state)) { + replacement = QuestTagSupport.createQuestGuiLink(questId, display, text, + Boolean.TRUE.equals(showTooltip)); + } else { + SymbolicColor color = state == QuestState.HIDDEN ? SymbolicColor.DARK_GRAY + : state == QuestState.MISSING ? SymbolicColor.RED : SymbolicColor.GRAY; + LytFlowSpan span = new LytFlowSpan(); + span.modifyStyle(style -> style.color(color).italic(true)); + span.appendText(text); + replacement = span; + } + + ctx.replace(replacement); + } + + private static String pickText(QuestDisplay display, UUID questId) { + QuestState state = display.getState(); + if (state == QuestState.COMPLETED) { + return QuestTagSupport.nameOrFallback(display, questId) + " ✓"; + } + if (QuestTagSupport.isNavigable(state)) { + return QuestTagSupport.nameOrFallback(display, questId); + } + if (state == QuestState.HIDDEN) { + return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; + } + if (state == QuestState.MISSING) { + return "[" + StatCollector.translateToLocal("guidenh.compat.bq.missing") + "]"; + } + return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index 781917c4..1f465f3b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -1,25 +1,214 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.FilterExpr; +import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.HandlerMetadataReader; +import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.HandlerRecipeAccess; +import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.RecipePlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.document.block.recipes.LytStandardRecipeBox; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.internal.recipe.LytNeiRecipeBox; +import com.hfstudio.guidenh.guide.internal.recipe.RecipeCache; +import com.hfstudio.guidenh.guide.internal.recipe.RecipeLookup; +import com.hfstudio.guidenh.integration.api.GuideNhIntegrationRegistry; +import com.hfstudio.guidenh.integration.api.RecipeEntry; +import com.hfstudio.guidenh.integration.api.RecipeSlot; +import com.hfstudio.guidenh.integration.nei.NeiRecipeLookup; + +import cpw.mods.fml.common.FMLLog; public class RecipeScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "Recipe"; - } + public String styleClass() { return "Recipe"; } + + @Override + public boolean isAsync() { return true; } @Override + @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full NEI query chain will be wired here + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof RecipePlaceholder ph)) return; + + Item item = (Item) Item.itemRegistry.getObject(ph.ref.rawKey()); + if (item == null) { + showFallback(ctx, ph); + return; + } + ItemStack targetStack = new ItemStack(item, 1, ph.ref.concreteMeta()); + if (ph.ref.nbt() != null) { + targetStack.stackTagCompound = (NBTTagCompound) ph.ref.nbt().copy(); + } + + boolean hasHandlerFilter = ph.handlerName != null || ph.handlerId != null || ph.handlerOrder >= 0; + boolean hasRecipeFilter = !ph.inputExpr.isEmpty() || !ph.outputExpr.isEmpty(); + int limit = ph.multi ? Integer.MAX_VALUE : 1; + boolean usageQuery = ph.usageQuery; + + // NEI handler path + List rawHandlers = usageQuery ? RecipeCache.getUsageHandlers(targetStack) + : RecipeCache.getCraftingHandlers(targetStack); + if (!usageQuery && hasHandlerFilter) { + List usage = RecipeCache.getUsageHandlers(targetStack); + if (!usage.isEmpty()) { + if (rawHandlers.isEmpty()) { + rawHandlers = usage; + } else { + List merged = new ArrayList<>(rawHandlers.size() + usage.size()); + merged.addAll(rawHandlers); + IdentityHashMap seen = new IdentityHashMap<>(merged.size()); + for (Object h : rawHandlers) seen.put(h, Boolean.TRUE); + for (Object h : usage) if (seen.put(h, Boolean.TRUE) == null) merged.add(h); + rawHandlers = merged; + } + } + } + HandlerMetadataReader metadataReader = new HandlerMetadataReader() { + @Override public @Nullable String handlerName(Object h) { + return NeiRecipeLookup.lookupHandlerName(h); + } + @Override public @Nullable String handlerId(Object h) { + return NeiRecipeLookup.lookupHandlerId(h); + } + @Override public @Nullable String overlayIdentifier(Object h) { + return NeiRecipeLookup.lookupOverlayIdentifier(h); + } + }; + HandlerRecipeAccess recipeAccess = new HandlerRecipeAccess() { + @Override public List readIngredientSlots(Object h, int ri) { + return NeiRecipeLookup.readIngredientSlots(h, ri); + } + @Override public @Nullable NeiRecipeLookup.Slot readResultSlot(Object h, int ri) { + return NeiRecipeLookup.readResultSlot(h, ri); + } + }; + List handlers = RecipeCompiler.filterHandlers(rawHandlers, + ph.handlerName, ph.handlerId, ph.handlerOrder, metadataReader); + if (!handlers.isEmpty()) { + List boxes = new ArrayList<>(); + for (int hi = 0; hi < handlers.size() && boxes.size() < limit; hi++) { + Object handler = handlers.get(hi); + int num = GuideNhIntegrationRegistry.global() + .lookupRecipeHandlerRecipeCount(handler); + int recipeStart = ph.recipeIndex >= 0 ? ph.recipeIndex : 0; + int recipeEnd = ph.recipeIndex >= 0 ? Math.min(num, ph.recipeIndex + 1) : num; + for (int ri = recipeStart; ri < recipeEnd && boxes.size() < limit; ri++) { + if (hasRecipeFilter + && !RecipeCompiler.recipeMatches(handler, ri, ph.inputExpr, ph.outputExpr, recipeAccess)) continue; + boxes.add(new LytNeiRecipeBox(handler, ri, !usageQuery)); + } + } + if (!boxes.isEmpty()) { + ctx.replace(buildResult(boxes, ph.multi)); + return; + } + if (ph.recipeIndex >= 0) { + showFallback(ctx, ph); + return; + } + } else if (hasHandlerFilter) { + showFallback(ctx, ph); + return; + } + + // Integration recipe entries + List recipeEntries = usageQuery ? Collections.emptyList() + : GuideNhIntegrationRegistry.global().findCraftingRecipeEntries(targetStack); + if (!recipeEntries.isEmpty()) { + List boxes = new ArrayList<>(); + int entryStart = ph.recipeIndex >= 0 ? ph.recipeIndex : 0; + int entryEnd = ph.recipeIndex >= 0 + ? Math.min(recipeEntries.size(), ph.recipeIndex + 1) : recipeEntries.size(); + for (int i = entryStart; i < entryEnd && boxes.size() < limit; i++) { + var e = recipeEntries.get(i); + if (e.result() == null || e.ingredients().isEmpty()) continue; + if (hasRecipeFilter && !RecipeCompiler.entryMatches(e, ph.inputExpr, ph.outputExpr)) continue; + List flat = new ArrayList<>(9); + for (int s = 0; s < 9; s++) flat.add(null); + int idx = 0; + for (RecipeSlot slot : e.ingredients()) { + if (idx >= 9) break; + if (slot.stacks() != null && !slot.stacks().isEmpty()) + flat.set(idx, slot.stacks().get(0)); + idx++; + } + ItemStack resultStack = e.result().stacks() != null && !e.result().stacks().isEmpty() + ? e.result().stacks().get(0) : null; + if (resultStack != null) + boxes.add(LytStandardRecipeBox.shapeless(flat, resultStack)); + } + if (!boxes.isEmpty()) { + ctx.replace(buildResult(boxes, ph.multi)); + return; + } + } + + // Vanilla recipe fallback + List entries = usageQuery ? Collections.emptyList() + : RecipeLookup.findByOutput(item); + if (entries.isEmpty()) { + showFallback(ctx, ph); + return; + } + + List boxes = new ArrayList<>(); + int vanillaStart = ph.recipeIndex >= 0 ? ph.recipeIndex : 0; + int vanillaEnd = ph.recipeIndex >= 0 + ? Math.min(entries.size(), ph.recipeIndex + 1) : entries.size(); + for (int i = vanillaStart; i < vanillaEnd && boxes.size() < limit; i++) { + var e = entries.get(i); + if (hasRecipeFilter + && !RecipeCompiler.vanillaEntryMatches(e, ph.inputExpr, ph.outputExpr)) continue; + var box = e.shapeless ? LytStandardRecipeBox.shapeless(RecipeLookup.asList(e), e.result) + : LytStandardRecipeBox.shaped3x3(RecipeLookup.asList(e), e.result); + boxes.add(box); + } + if (!boxes.isEmpty()) { + ctx.replace(buildResult(boxes, ph.multi)); + return; + } + showFallback(ctx, ph); + } + + @SuppressWarnings("unchecked") + private static LytBlock buildResult(List boxes, boolean multi) { + return buildResultTyped((List) boxes, multi); + } + + private static LytBlock buildResultTyped(List boxes, boolean multi) { + if (boxes.size() == 1) return boxes.get(0); + if (!multi) return boxes.get(0); + var row = new com.hfstudio.guidenh.guide.document.block.LytHBox(); + row.setGap(RecipeCompiler.MULTI_GAP); + for (var b : boxes) row.append(b); + return row; + } + + private void showFallback(ScriptContext ctx, RecipePlaceholder ph) { + if (ph.fallbackText != null && !ph.fallbackText.isEmpty()) { + var p = new com.hfstudio.guidenh.guide.document.block.LytParagraph(); + p.appendText(ph.fallbackText); + ctx.replace(p); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index e13281ff..bbb81763 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -1,25 +1,153 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import java.util.HashMap; +import java.util.Map; + +import net.minecraft.util.ResourceLocation; + +import com.hfstudio.guidenh.guide.PageCollection; +import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.LytErrorSink; +import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; +import com.hfstudio.guidenh.guide.scene.CameraSettings; +import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; +import com.hfstudio.guidenh.guide.scene.PerspectivePreset; +import com.hfstudio.guidenh.guide.scene.SceneTagCompiler; +import com.hfstudio.guidenh.guide.scene.SceneTagCompiler.ScenePlaceholder; +import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler; +import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; +import com.hfstudio.guidenh.libs.mdast.MdAst; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.unist.UnistNode; + +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import cpw.mods.fml.common.FMLLog; + public class SceneScript implements LytScript { - @Override - public ScriptType type() { - return ScriptType.JAVA; + private final Map elementCompilers; + + public SceneScript() { + Map map = new HashMap<>(); + for (var ext : com.hfstudio.guidenh.guide.internal.extensions.DefaultExtensions.sceneElementCompilers()) { + for (String name : ext.getTagNames()) { + map.put(name, ext); + } + } + this.elementCompilers = map; } @Override - public String styleClass() { - return "Scene"; // handles both Scene and GameScene - } + public ScriptType type() { return ScriptType.JAVA; } + + @Override + public String styleClass() { return "Scene"; } + + @Override + public boolean isAsync() { return true; } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: entire scene pipeline will be wired here + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof ScenePlaceholder ph)) return; + + if (ph.childrenSource == null || ph.childrenSource.trim().isEmpty()) return; + + GuidebookLevel level = new GuidebookLevel(); + CameraSettings camera = new CameraSettings(); + if (ph.perspective != null && !ph.perspective.trim().isEmpty()) { + camera.setPerspectivePreset( + PerspectivePreset.fromSerializedName(ph.perspective.trim())); + } + if (!Float.isNaN(ph.zoom)) camera.setZoom(ph.zoom); + if (!Float.isNaN(ph.rotateX)) camera.setRotationX(ph.rotateX); + if (!Float.isNaN(ph.rotateY)) camera.setRotationY(ph.rotateY); + if (!Float.isNaN(ph.rotateZ)) camera.setRotationZ(ph.rotateZ); + if (!Float.isNaN(ph.offsetX)) camera.setOffsetX(ph.offsetX); + if (!Float.isNaN(ph.offsetY)) camera.setOffsetY(ph.offsetY); + if (ph.explicitCenter) { + camera.setRotationCenter( + Float.isNaN(ph.centerX) ? 0 : ph.centerX, + Float.isNaN(ph.centerY) ? 0 : ph.centerY, + Float.isNaN(ph.centerZ) ? 0 : ph.centerZ); + } + + int width = ph.width > 0 ? ph.width : 320; + int height = ph.height > 0 ? ph.height : 180; + camera.setViewportSize(width, height); + + // Parse children source and compile scene elements + ExceptionCollector errorSink = new ExceptionCollector(); + PageCollection pc = ctx.getPageCollection(); + PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), + ExtensionCollection.EMPTY, "", new ResourceLocation(ph.pageDomain, "scene"), ""); + try { + MdAstRoot ast = MdAst.fromMarkdown(ph.childrenSource, GuideMarkdownOptions.runtime()); + MdAstToMdxConverter.convert(ast, java.util.Collections.emptyMap()); + for (UnistNode child : ast.children()) { + MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); + if (el == null) continue; + SceneElementTagCompiler ec = elementCompilers.get(el.name()); + if (ec != null) { + ec.compile(level, camera, runtimeCompiler, errorSink, el); + } + } + } catch (Exception e) { + FMLLog.getLogger().warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); + } + + LytGuidebookScene scene = new LytGuidebookScene(); + scene.setLevel(level); + scene.setCamera(camera); + scene.setSceneSize(width, height); + scene.setInteractive(ph.interactive); + scene.setShowBackground(ph.showBackground); + scene.setVisibleLayerSliderEnabled(ph.allowLayerSlider); + scene.setGridButtonEnabled(ph.gridButtonEnabled); + scene.setGridVisible(ph.showGrid); + + if (!level.isEmpty()) { + float[] center = level.getCenter(); + if (!ph.explicitCenter) { + camera.setRotationCenter(center[0], center[1], center[2]); + } + // Auto-center the scene in the viewport using the same approach as BlockImageScript + camera.setOffsetX(0f); + camera.setOffsetY(0f); + var sc = camera.worldToScreen(center[0], center[1], center[2]); + camera.setOffsetX(-sc.x + (Float.isNaN(ph.offsetX) ? 0 : ph.offsetX)); + camera.setOffsetY(sc.y + (Float.isNaN(ph.offsetY) ? 0 : ph.offsetY)); + } + scene.snapshotInitialCamera(); + ctx.replace(scene); + } + + private static class ExceptionCollector implements LytErrorSink { + @Override + public void appendError(PageCompiler compiler, String text, UnistNode node) { + FMLLog.getLogger().warn("[GuideNH] [SceneScript] {}", text); + } + } + + private static class StubPageCollection implements PageCollection { + @Override public T getIndex(Class c) { return null; } + @Override public java.util.Collection getPages() { + return java.util.Collections.emptyList(); + } + @Override public com.hfstudio.guidenh.guide.compiler.ParsedGuidePage getParsedPage(ResourceLocation id) { return null; } + @Override public com.hfstudio.guidenh.guide.GuidePage getPage(ResourceLocation id) { return null; } + @Override public byte[] loadAsset(ResourceLocation id) { return null; } + @Override public com.hfstudio.guidenh.guide.navigation.NavigationTree getNavigationTree() { + return new com.hfstudio.guidenh.guide.navigation.NavigationTree(); + } + @Override public boolean pageExists(ResourceLocation pageId) { return false; } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index 9e004908..e24faf18 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -1,25 +1,53 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.Guide; +import com.hfstudio.guidenh.guide.PageCollection; +import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.MediaWikiTagCompilerSupport; +import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.SpecialCompiler.SpecialPlaceholder; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiListContext; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageQuery; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; public class SpecialScript implements LytScript { @Override - public ScriptType type() { - return ScriptType.JAVA; - } + public ScriptType type() { return ScriptType.JAVA; } @Override - public String styleClass() { - return "Special"; - } + public String styleClass() { return "Special"; } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - // Stub: full implementation in later task + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof SpecialPlaceholder ph)) return; + + PageCollection pc = ctx.getPageCollection(); + if (!(pc instanceof Guide guide)) return; + + CategoryIndex categoryIndex = ctx.getIndex(CategoryIndex.class); + + var resolver = new MediaWikiSpecialPageResolver(); + String specialName = resolver.normalizeSupportedName(ph.name); + if (specialName == null) return; + + MediaWikiListContext context = MediaWikiTagCompilerSupport.createListContext(guide, categoryIndex); + MediaWikiSpecialPageQuery query = new MediaWikiSpecialPageQuery("", + MediaWikiSpecialPageQuery.PAGE_SIZE); + if (ph.page != null) query = query.withParameter("page", ph.page); + if (ph.prefix != null) query = query.withParameter("prefix", ph.prefix); + if (ph.language != null) query = query.withParameter("language", ph.language); + if (ph.query != null) query = query.withSearchText(ph.query); + + var result = resolver.resolve(context, specialName, + query.withVisibleCount(Integer.MAX_VALUE)); + var block = MediaWikiTagCompilerSupport.createSpecialBlock( + result, ph.rows, context, query, resolver); + ctx.replace(block); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 234a3b54..dc241af5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -131,7 +131,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl centerX, centerY, centerZ, explicitCenter, interactive, showBackground, allowLayerSlider, gridButtonEnabled, showGrid, - childrenSource + childrenSource, + compiler.getPageId().getResourceDomain() ); placeholder.setStyleClass(styleClass); placeholder.setStyle(LytParagraph.LOADING_STYLE); @@ -152,32 +153,33 @@ private boolean resolveShowBackground(PageCompiler compiler, LytBlockContainer p */ public static class ScenePlaceholder extends LytParagraph { - final int width; - final int height; - final boolean explicitWidth; - final boolean explicitHeight; - final float zoom; - final boolean explicitZoom; - @Nullable final String perspective; - final float rotateX; - final float rotateY; - final float rotateZ; - final float offsetX; - final float offsetY; - final boolean explicitOffsetX; - final boolean explicitOffsetY; - final float centerX; - final float centerY; - final float centerZ; - final boolean explicitCenter; - final boolean interactive; - final boolean showBackground; - final boolean allowLayerSlider; - final boolean gridButtonEnabled; - final boolean showGrid; - @Nullable final String childrenSource; - - ScenePlaceholder( + public final int width; + public final int height; + public final boolean explicitWidth; + public final boolean explicitHeight; + public final float zoom; + public final boolean explicitZoom; + @Nullable public final String perspective; + public final float rotateX; + public final float rotateY; + public final float rotateZ; + public final float offsetX; + public final float offsetY; + public final boolean explicitOffsetX; + public final boolean explicitOffsetY; + public final float centerX; + public final float centerY; + public final float centerZ; + public final boolean explicitCenter; + public final boolean interactive; + public final boolean showBackground; + public final boolean allowLayerSlider; + public final boolean gridButtonEnabled; + public final boolean showGrid; + @Nullable public final String childrenSource; + public final String pageDomain; + + public ScenePlaceholder( int width, int height, boolean explicitWidth, boolean explicitHeight, float zoom, boolean explicitZoom, @@ -190,7 +192,8 @@ public static class ScenePlaceholder extends LytParagraph { boolean interactive, boolean showBackground, boolean allowLayerSlider, boolean gridButtonEnabled, boolean showGrid, - @Nullable String childrenSource) { + @Nullable String childrenSource, + String pageDomain) { this.width = width; this.height = height; this.explicitWidth = explicitWidth; @@ -215,6 +218,7 @@ public static class ScenePlaceholder extends LytParagraph { this.gridButtonEnabled = gridButtonEnabled; this.showGrid = showGrid; this.childrenSource = childrenSource; + this.pageDomain = pageDomain; } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java index c959ecf2..68d0d79f 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java @@ -4,21 +4,12 @@ import java.util.Set; import java.util.UUID; -import net.minecraft.client.Minecraft; -import net.minecraft.util.StatCollector; - -import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; -import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; -import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; -import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; import com.hfstudio.guidenh.integration.betterquesting.QuestIdParser; -import com.hfstudio.guidenh.integration.betterquesting.QuestState; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; /** @@ -51,92 +42,22 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl boolean showDesc = !"false".equalsIgnoreCase(MdxAttrs.getString(compiler, parent, el, "show_desc", "true")); boolean showTooltip = QuestTagSupport.resolveShowTooltip(compiler, parent, el); - QuestDisplay display = BqHelpers - .resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, showTooltip || showDesc); - QuestState state = display.getState(); - - var box = new LytQuoteBox(); - SymbolicColor accent = pickAccentColor(state); - box.setQuoteStyle(accent, null, null); - - // Title line: clickable link for visible/completed states, placeholder text otherwise. - var title = new LytParagraph(); - title.setMarginTop(0); - title.setMarginBottom(2); - appendTitle(compiler, title, questId, display, showTooltip); - box.append(title); - - // Description body: only shown when the player can actually see the quest. - if (showDesc && isVisibleToPlayer(state)) { - String description = display.getDescription(); - if (description != null && !description.isEmpty()) { - var descPar = new LytParagraph(); - descPar.appendText(description); - box.append(descPar); - } - } - - parent.append(box); + QuestCardPlaceholder ph = new QuestCardPlaceholder(questId, showDesc, showTooltip); + ph.setStyleClass("QuestCard"); + ph.setStyle(LytParagraph.LOADING_STYLE); + ph.appendText("Loading quest..."); + parent.append(ph); } - private static void appendTitle(PageCompiler compiler, LytParagraph title, UUID questId, QuestDisplay display, - boolean showTooltip) { - QuestState state = display.getState(); - String name = resolveTitleText(display, questId); + public static class QuestCardPlaceholder extends LytParagraph { + public final UUID questId; + public final boolean showDesc; + public final boolean showTooltip; - if (QuestTagSupport.isNavigable(state)) { - title.append(QuestTagSupport.createQuestLink(compiler, questId, display, name, showTooltip)); - } else { - var span = new LytFlowSpan(); - span.modifyStyle( - style -> style.color(pickPlaceholderColor(state)) - .italic(true)); - span.appendText(name); - title.append(span); - } - } - - private static String resolveTitleText(QuestDisplay display, UUID questId) { - QuestState state = display.getState(); - if (state == QuestState.COMPLETED) { - return QuestTagSupport.nameOrFallback(display, questId) + " \u2713"; - } - if (QuestTagSupport.isNavigable(state)) { - return QuestTagSupport.nameOrFallback(display, questId); - } - if (state == QuestState.HIDDEN) { - return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; - } - if (state == QuestState.MISSING) { - return "[" + StatCollector.translateToLocal("guidenh.compat.bq.missing") + "]"; + QuestCardPlaceholder(UUID questId, boolean showDesc, boolean showTooltip) { + this.questId = questId; + this.showDesc = showDesc; + this.showTooltip = showTooltip; } - return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; - } - - private static SymbolicColor pickAccentColor(QuestState state) { - if (state == QuestState.COMPLETED) { - return SymbolicColor.GREEN; - } - if (state == QuestState.LOCKED || state == QuestState.HIDDEN) { - return SymbolicColor.GRAY; - } - if (state == QuestState.MISSING) { - return SymbolicColor.RED; - } - return SymbolicColor.LINK; - } - - private static SymbolicColor pickPlaceholderColor(QuestState state) { - if (state == QuestState.HIDDEN) { - return SymbolicColor.DARK_GRAY; - } - if (state == QuestState.MISSING) { - return SymbolicColor.RED; - } - return SymbolicColor.GRAY; - } - - private static boolean isVisibleToPlayer(QuestState state) { - return QuestTagSupport.isVisibleToPlayer(state); } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestLinkCompiler.java b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestLinkCompiler.java index 01f646f5..d3575eae 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestLinkCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestLinkCompiler.java @@ -4,23 +4,11 @@ import java.util.Set; import java.util.UUID; -import net.minecraft.client.Minecraft; -import net.minecraft.util.StatCollector; - -import org.jetbrains.annotations.Nullable; - -import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.FlowTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; -import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; -import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; -import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; -import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; -import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; import com.hfstudio.guidenh.integration.betterquesting.QuestIdParser; -import com.hfstudio.guidenh.integration.betterquesting.QuestState; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; /** @@ -54,64 +42,10 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen String overrideText = MdxAttrs.getString(compiler, parent, el, "text", null); boolean showTooltip = QuestTagSupport.resolveShowTooltip(compiler, parent, el); - QuestDisplay display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, showTooltip); - QuestState state = display.getState(); - String text = pickText(overrideText, display, questId); - - if (QuestTagSupport.isNavigable(state)) { - appendNavigableLink(compiler, parent, questId, text, display, showTooltip); - } else if (state == QuestState.HIDDEN) { - appendPlaceholder(parent, text, SymbolicColor.DARK_GRAY, null); - } else if (state == QuestState.MISSING) { - appendPlaceholder(parent, text, SymbolicColor.RED, null); - } else { - appendPlaceholder(parent, text, SymbolicColor.GRAY, null); - } - } - - private static void appendNavigableLink(PageCompiler compiler, LytFlowParent parent, UUID questId, String text, - QuestDisplay display, boolean showTooltip) { - parent.append(QuestTagSupport.createQuestGuiLink(questId, display, text, showTooltip)); - } - - private static void appendPlaceholder(LytFlowParent parent, String text, SymbolicColor color, - @Nullable String tooltipText) { - LytFlowSpan span; - if (tooltipText != null && !tooltipText.isEmpty()) { - var tooltipSpan = new LytTooltipSpan(); - tooltipSpan.setTooltip(new TextTooltip(tooltipText)); - span = tooltipSpan; - } else { - span = new LytFlowSpan(); - } - span.modifyStyle( - style -> style.color(color) - .italic(true)); - span.appendText(text); - parent.append(span); - } - - private static String pickText(@Nullable String overrideText, QuestDisplay display, UUID questId) { - if (overrideText != null && !overrideText.isEmpty()) { - return overrideText; - } - QuestState state = display.getState(); - if (state == QuestState.COMPLETED) { - return nameOrFallback(display, questId) + " \u2713"; - } - if (QuestTagSupport.isNavigable(state)) { - return nameOrFallback(display, questId); - } - if (state == QuestState.HIDDEN) { - return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; - } - if (state == QuestState.MISSING) { - return "[" + StatCollector.translateToLocal("guidenh.compat.bq.missing") + "]"; - } - return "[" + StatCollector.translateToLocal("guidenh.compat.bq.hidden") + "]"; - } - - private static String nameOrFallback(QuestDisplay display, UUID questId) { - return QuestTagSupport.nameOrFallback(display, questId); + var placeholder = parent.appendText(""); + placeholder.setStyleClass("QuestLink"); + placeholder.setData("questId", questId); + placeholder.setData("overrideText", overrideText); + placeholder.setData("showTooltip", showTooltip); } } From 9dfbe31570f44f4389304982c0103fd739bba700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 04:28:16 +0800 Subject: [PATCH 046/136] fix: 9 verified bugs from adversarial audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QuestCardScript: createQuestGuiLink instead of createQuestLink(null) → NPE - RecipeScript: read ph.limit, always ctx.replace in showFallback, drop stale multi guard - MermaidScript: replaceWithError on all 3 failure paths - ItemLinkScript: ore attribute fallback via OreDictionary.getOres() - FloatingImageCompiler: Random(0) for deterministic borderColor - BlockImageCompiler/Script: Integer.MIN_VALUE sentinel for meta=0 vs unspecified - MasterScheduler: progressive deadlineNs across HIGH/MEDIUM/LOW queues --- .../compiler/tags/BlockImageCompiler.java | 2 +- .../compiler/tags/FloatingImageCompiler.java | 2 +- .../internal/host/ScriptContextImpl.java | 3 +- .../host/scripts/BlockImageScript.java | 2 +- .../internal/host/scripts/ItemLinkScript.java | 16 +++++++--- .../internal/host/scripts/MermaidScript.java | 12 +++++++- .../host/scripts/QuestCardScript.java | 2 +- .../internal/host/scripts/RecipeScript.java | 29 ++++++++++--------- .../internal/host/scripts/SceneScript.java | 25 ++++++++++------ .../internal/scheduler/MasterScheduler.java | 10 +++---- 10 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java index fc062ca4..02e0b7ea 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java @@ -27,7 +27,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl return; } - int meta = MdxAttrs.getInt(compiler, parent, el, "meta", 0); + int meta = MdxAttrs.getInt(compiler, parent, el, "meta", Integer.MIN_VALUE); String nbt = MdxAttrs.getString(compiler, parent, el, "nbt", null); float scale = MdxAttrs.getFloat(compiler, parent, el, "scale", 1f); String perspective = MdxAttrs.getString(compiler, parent, el, "perspective", null); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java index 78df512c..16c83491 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java @@ -29,7 +29,7 @@ public class FloatingImageCompiler extends FlowTagCompiler { public static final String TAG_NAME = "FloatingImage"; - private static final Random RANDOM = new Random(); + private static final Random RANDOM = new Random(0); @Override public Set getTagNames() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index af808cbb..e4290c33 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -11,6 +11,7 @@ import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; @@ -55,7 +56,7 @@ public void replace(Object newNode) { return; } // Handle LytParagraph and other LytFlowContainer parents - if (parent instanceof com.hfstudio.guidenh.guide.document.block.LytParagraph para) { + if (parent instanceof LytParagraph para) { Iterable iterable = para.getContent(); if (iterable instanceof List) { List list = (List) iterable; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 29902211..1caf1f1b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -72,7 +72,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { perspective = PerspectivePreset.fromSerializedName(ph.perspective.trim()); } - int defaultMeta = meta == 0 ? BlockElementCompiler.defaultMetaFor(block, null) : meta; + int defaultMeta = meta == Integer.MIN_VALUE ? BlockElementCompiler.defaultMetaFor(block, null) : meta; GuidebookLevel level = new GuidebookLevel(); GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 1633b94e..eaa47edd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -3,6 +3,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.oredict.OreDictionary; import org.jetbrains.annotations.Nullable; @@ -14,6 +15,8 @@ import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.indices.ItemIndex; +import com.hfstudio.guidenh.guide.indices.OreIndex; public class ItemLinkScript implements LytScript { @@ -28,10 +31,17 @@ public class ItemLinkScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() == EventType.MOUNT && node instanceof LytFlowLink link) { String itemId = (String) link.getData("itemId"); + String ore = (String) link.getData("ore"); Boolean showTooltip = (Boolean) link.getData("showTooltip"); String linksTo = (String) link.getData("linksTo"); ItemStack stack = resolveItemStack(itemId); + if (stack == null && ore != null && !ore.isEmpty()) { + java.util.List ores = OreDictionary.getOres(ore); + if (!ores.isEmpty()) { + stack = ores.get(0); + } + } if (stack == null) return; PageAnchor anchor = findLinkTarget(stack, linksTo, ctx); @@ -74,14 +84,12 @@ private static PageAnchor findLinkTarget(ItemStack stack, @Nullable String links return PageAnchor.page(pageId); } catch (Exception ignored) {} } - com.hfstudio.guidenh.guide.indices.ItemIndex itemIdx = ctx.getIndex( - com.hfstudio.guidenh.guide.indices.ItemIndex.class); + ItemIndex itemIdx = ctx.getIndex(ItemIndex.class); if (itemIdx != null) { PageAnchor anchor = itemIdx.findByStack(stack); if (anchor != null) return anchor; } - com.hfstudio.guidenh.guide.indices.OreIndex oreIdx = ctx.getIndex( - com.hfstudio.guidenh.guide.indices.OreIndex.class); + OreIndex oreIdx = ctx.getIndex(OreIndex.class); if (oreIdx != null) { return oreIdx.findByStack(stack); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index d0e9ab52..fcf1f4f9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -6,6 +6,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.MermaidCompiler.MermaidPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytMermaidMindmap; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -37,6 +38,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { try { srcId = new ResourceLocation(ph.src); } catch (Exception e) { + replaceWithError(ctx, "Invalid Mermaid source path: " + ph.src); return; } byte[] data = ctx.loadAsset(srcId); @@ -45,7 +47,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } - if (sourceText == null || sourceText.trim().isEmpty()) return; + if (sourceText == null || sourceText.trim().isEmpty()) { + replaceWithError(ctx, "Mermaid source not found or empty"); + return; + } try { var document = MermaidMindmapParser.parse(sourceText); @@ -58,6 +63,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } catch (IllegalArgumentException e) { FMLLog.getLogger().warn( "[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); + replaceWithError(ctx, "Failed to parse Mermaid source: " + e.getMessage()); } } + + private void replaceWithError(ScriptContext ctx, String message) { + ctx.replace(LytParagraph.of(message)); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java index 42346f2d..6c057279 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java @@ -47,7 +47,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String name = resolveTitleText(display, ph.questId); if (QuestTagSupport.isNavigable(state)) { - title.append(QuestTagSupport.createQuestLink(null, ph.questId, display, name, ph.showTooltip)); + title.append(QuestTagSupport.createQuestGuiLink(ph.questId, display, name, ph.showTooltip)); } else { var span = new LytFlowSpan(); span.modifyStyle(style -> style.color(pickPlaceholderColor(state)).italic(true)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index 1f465f3b..8448979a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -17,6 +17,8 @@ import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.HandlerRecipeAccess; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.RecipePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.document.block.LytHBox; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.recipes.LytStandardRecipeBox; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -62,7 +64,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { boolean hasHandlerFilter = ph.handlerName != null || ph.handlerId != null || ph.handlerOrder >= 0; boolean hasRecipeFilter = !ph.inputExpr.isEmpty() || !ph.outputExpr.isEmpty(); - int limit = ph.multi ? Integer.MAX_VALUE : 1; + int limit = ph.limit; boolean usageQuery = ph.usageQuery; // NEI handler path @@ -119,7 +121,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } if (!boxes.isEmpty()) { - ctx.replace(buildResult(boxes, ph.multi)); + ctx.replace(buildResult(boxes)); return; } if (ph.recipeIndex >= 0) { @@ -158,7 +160,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { boxes.add(LytStandardRecipeBox.shapeless(flat, resultStack)); } if (!boxes.isEmpty()) { - ctx.replace(buildResult(boxes, ph.multi)); + ctx.replace(buildResult(boxes)); return; } } @@ -184,31 +186,30 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { boxes.add(box); } if (!boxes.isEmpty()) { - ctx.replace(buildResult(boxes, ph.multi)); + ctx.replace(buildResult(boxes)); return; } showFallback(ctx, ph); } @SuppressWarnings("unchecked") - private static LytBlock buildResult(List boxes, boolean multi) { - return buildResultTyped((List) boxes, multi); + private static LytBlock buildResult(List boxes) { + return buildResultTyped((List) boxes); } - private static LytBlock buildResultTyped(List boxes, boolean multi) { + private static LytBlock buildResultTyped(List boxes) { if (boxes.size() == 1) return boxes.get(0); - if (!multi) return boxes.get(0); - var row = new com.hfstudio.guidenh.guide.document.block.LytHBox(); + var row = new LytHBox(); row.setGap(RecipeCompiler.MULTI_GAP); for (var b : boxes) row.append(b); return row; } private void showFallback(ScriptContext ctx, RecipePlaceholder ph) { - if (ph.fallbackText != null && !ph.fallbackText.isEmpty()) { - var p = new com.hfstudio.guidenh.guide.document.block.LytParagraph(); - p.appendText(ph.fallbackText); - ctx.replace(p); - } + String text = (ph.fallbackText != null && !ph.fallbackText.isEmpty()) + ? ph.fallbackText : "No recipe found."; + var p = new LytParagraph(); + p.appendText(text); + ctx.replace(p); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index bbb81763..35cfb2aa 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -1,16 +1,23 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.document.LytErrorSink; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; +import com.hfstudio.guidenh.guide.indices.PageIndex; +import com.hfstudio.guidenh.guide.internal.extensions.DefaultExtensions; import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; +import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.scene.CameraSettings; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; @@ -37,7 +44,7 @@ public class SceneScript implements LytScript { public SceneScript() { Map map = new HashMap<>(); - for (var ext : com.hfstudio.guidenh.guide.internal.extensions.DefaultExtensions.sceneElementCompilers()) { + for (var ext : DefaultExtensions.sceneElementCompilers()) { for (String name : ext.getTagNames()) { map.put(name, ext); } @@ -91,7 +98,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ExtensionCollection.EMPTY, "", new ResourceLocation(ph.pageDomain, "scene"), ""); try { MdAstRoot ast = MdAst.fromMarkdown(ph.childrenSource, GuideMarkdownOptions.runtime()); - MdAstToMdxConverter.convert(ast, java.util.Collections.emptyMap()); + MdAstToMdxConverter.convert(ast, Collections.emptyMap()); for (UnistNode child : ast.children()) { MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); if (el == null) continue; @@ -138,15 +145,15 @@ public void appendError(PageCompiler compiler, String text, UnistNode node) { } private static class StubPageCollection implements PageCollection { - @Override public T getIndex(Class c) { return null; } - @Override public java.util.Collection getPages() { - return java.util.Collections.emptyList(); + @Override public T getIndex(Class c) { return null; } + @Override public Collection getPages() { + return Collections.emptyList(); } - @Override public com.hfstudio.guidenh.guide.compiler.ParsedGuidePage getParsedPage(ResourceLocation id) { return null; } - @Override public com.hfstudio.guidenh.guide.GuidePage getPage(ResourceLocation id) { return null; } + @Override public ParsedGuidePage getParsedPage(ResourceLocation id) { return null; } + @Override public GuidePage getPage(ResourceLocation id) { return null; } @Override public byte[] loadAsset(ResourceLocation id) { return null; } - @Override public com.hfstudio.guidenh.guide.navigation.NavigationTree getNavigationTree() { - return new com.hfstudio.guidenh.guide.navigation.NavigationTree(); + @Override public NavigationTree getNavigationTree() { + return new NavigationTree(); } @Override public boolean pageExists(ResourceLocation pageId) { return false; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java index 02fe161b..42ea8789 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java @@ -39,12 +39,10 @@ public void onClientTick(TickEvent.ClientTickEvent event) { long deadlineNs = System.nanoTime() + HIGH_BUDGET_NS; processPriority(high, deadlineNs); - if (System.nanoTime() < deadlineNs) { - processPriority(medium, deadlineNs + MEDIUM_BUDGET_NS); - } - if (System.nanoTime() < deadlineNs) { - processPriority(low, deadlineNs + LOW_BUDGET_NS); - } + deadlineNs = Math.max(deadlineNs, System.nanoTime()) + MEDIUM_BUDGET_NS; + processPriority(medium, deadlineNs); + deadlineNs = Math.max(deadlineNs, System.nanoTime()) + LOW_BUDGET_NS; + processPriority(low, deadlineNs); } public void submit(WorkItem item) { From 21af39c0a6784f82ab716f0c32c72c19e4feb3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 11:20:40 +0800 Subject: [PATCH 047/136] fix: replace loading placeholders and error messages with styled [TagName] format --- .../compiler/tags/BlockImageCompiler.java | 1 + .../guide/compiler/tags/CsvTableCompiler.java | 4 +-- .../compiler/tags/FloatingImageCompiler.java | 3 ++ .../guide/compiler/tags/ImageCompiler.java | 4 +-- .../guide/compiler/tags/MermaidCompiler.java | 4 +-- .../guide/compiler/tags/RecipeCompiler.java | 4 +-- .../guide/document/block/LytParagraph.java | 28 +++++++++++++++++++ .../host/scripts/BlockImageScript.java | 11 ++++++-- .../internal/host/scripts/CategoryScript.java | 10 ++++++- .../internal/host/scripts/CsvTableScript.java | 21 ++++++++++---- .../host/scripts/FloatingImageScript.java | 11 +++++++- .../internal/host/scripts/ImageScript.java | 11 +++++++- .../internal/host/scripts/ItemGridScript.java | 9 +++++- .../host/scripts/ItemImageScript.java | 6 +++- .../internal/host/scripts/ItemLinkScript.java | 13 ++++++++- .../internal/host/scripts/MermaidScript.java | 8 +++--- .../host/scripts/QuestCardScript.java | 14 ++++++++-- .../host/scripts/QuestLinkScript.java | 7 +++++ .../internal/host/scripts/RecipeScript.java | 18 +++++++----- .../internal/host/scripts/SceneScript.java | 13 ++++++++- .../internal/host/scripts/SpecialScript.java | 11 ++++++-- .../host/scripts/StructureScript.java | 9 +++++- .../internal/host/scripts/SubPagesScript.java | 10 ++++++- .../guidenh/guide/scene/SceneTagCompiler.java | 2 +- 24 files changed, 192 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java index 02e0b7ea..66a7a872 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java @@ -38,6 +38,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl BlockImagePlaceholder placeholder = new BlockImagePlaceholder( id, ore, meta, nbt, scale, perspective, width, height); placeholder.setStyleClass("BlockImage"); + placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); placeholder.appendText("[BlockImage]"); parent.append(placeholder); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index 6a6c3125..bb0df372 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -46,7 +46,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl List widths = parseWidthHints(MdxAttrs.getString(compiler, parent, el, "widths", null)); CsvTablePlaceholder placeholder = new CsvTablePlaceholder(csvId.toString(), header, widths); - placeholder.appendText("Loading CSV..."); + placeholder.appendText("[CsvTable]"); parent.append(placeholder); } @@ -157,7 +157,7 @@ public CsvTablePlaceholder(String src, boolean header, List widths) { this.header = header; this.widths = widths; setStyleClass("CsvTable"); - setStyle(LytParagraph.LOADING_STYLE); + setStyle(LytParagraph.PLACEHOLDER_STYLE); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java index 16c83491..003e1bc6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java @@ -14,6 +14,7 @@ import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.ImageRegionAnnotation; import com.hfstudio.guidenh.guide.document.block.LytImageBlock; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.document.flow.InlineBlockAlignment; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; @@ -51,6 +52,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen LytImageBlock block = new LytImageBlock(); block.setStyleClass("FloatingImage"); + block.setStyle(LytParagraph.PLACEHOLDER_STYLE); + block.appendText("[FloatingImage]"); block.setAlign(align); if (title != null) { block.setTitle(title); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java index 10ad0156..68b66246 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java @@ -35,8 +35,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen if (!alt.isEmpty()) block.setAlt(alt); if (!title.isEmpty()) block.setTitle(title); - block.appendText("Loading image..."); - block.setStyle(LytParagraph.LOADING_STYLE); + block.setStyle(LytParagraph.PLACEHOLDER_STYLE); + block.appendText("[Image]"); var inlineBlock = new LytFlowInlineBlock(); inlineBlock.setBlock(block); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java index 38c9fa7a..e06e74db 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java @@ -71,7 +71,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl Map nodeContentBlocks = compileNodeContentBlocks(compiler, parent, el); MermaidPlaceholder placeholder = new MermaidPlaceholder(src, sourceText, width, height, nodeContentBlocks); - placeholder.appendText("Loading Mermaid..."); + placeholder.appendText("[Mermaid]"); parent.append(placeholder); } @@ -143,7 +143,7 @@ public MermaidPlaceholder(String src, String sourceText, int width, int height, this.height = height; this.nodeContentBlocks = nodeContentBlocks; setStyleClass("Mermaid"); - setStyle(LytParagraph.LOADING_STYLE); + setStyle(LytParagraph.PLACEHOLDER_STYLE); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java index e5e7a18c..2b041516 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java @@ -113,8 +113,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl handlerNameFilter, handlerIdFilter, handlerOrder, exactRecipeIndex, inputExpr, outputExpr, limit, multi, usageQuery); ph.setStyleClass(tagName); - ph.setStyle(LytParagraph.LOADING_STYLE); - ph.appendText("Loading recipe..."); + ph.setStyle(LytParagraph.PLACEHOLDER_STYLE); + ph.appendText("[Recipe]"); parent.append(ph); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java index 46bbbf12..58890140 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java @@ -5,6 +5,7 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.flow.LytFlowContainer; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; @@ -200,4 +201,31 @@ public static LytParagraph loading(String text) { paragraph.appendText(text); return paragraph; } + + /** Warm amber-yellow italic text for placeholder blocks awaiting async materialization. */ + public static final TextStyle PLACEHOLDER_STYLE = TextStyle.builder() + .italic(true) + .color(new ConstantColor(0xFFE8A317)) + .build(); + + /** Red text style for inline error messages. */ + public static final TextStyle ERROR_STYLE = TextStyle.builder() + .color(SymbolicColor.ERROR_TEXT) + .build(); + + /** Creates a placeholder paragraph (amber, italic) for deferred content. */ + public static LytParagraph placeholder(String text) { + var paragraph = new LytParagraph(); + paragraph.setStyle(PLACEHOLDER_STYLE); + paragraph.appendText(text); + return paragraph; + } + + /** Creates an error paragraph (red text) for inline error reporting. */ + public static LytParagraph error(String text) { + var paragraph = new LytParagraph(); + paragraph.setStyle(ERROR_STYLE); + paragraph.appendText(text); + return paragraph; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 1caf1f1b..bee0efe7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -8,6 +8,7 @@ import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler.BlockImagePlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -58,7 +59,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } - if (block == null) return; + if (block == null) { + ctx.replace(LytParagraph.error("[BlockImage] Block not found: " + (ph.ore != null ? ph.ore : ph.id))); + return; + } NBTTagCompound tileTag = null; if (ph.nbt != null && !ph.nbt.trim().isEmpty()) { @@ -76,7 +80,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { GuidebookLevel level = new GuidebookLevel(); GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag); - if (level.isEmpty()) return; + if (level.isEmpty()) { + ctx.replace(LytParagraph.error("[BlockImage] Failed to create block preview")); + return; + } int width = ph.width > 0 ? ph.width : 128; int height = ph.height > 0 ? ph.height : 128; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index 9e254d61..d8302802 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -28,9 +28,17 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (!(node instanceof CategoryPlaceholder ph)) return; CategoryIndex index = ctx.getIndex(CategoryIndex.class); - if (index == null) return; + if (index == null) { + ctx.replace(LytParagraph.error("[Category] Category index not available")); + return; + } List members = index.get(ph.name); + if (members.isEmpty()) { + ctx.replace(LytParagraph.error("[Category] No pages in category: " + ph.name)); + return; + } + LytVBox box = new LytVBox(); int count = 0; for (PageAnchor anchor : members) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java index b8783c64..311edfdf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java @@ -8,6 +8,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler.CsvTablePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -35,16 +36,26 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { try { csvId = new ResourceLocation(ph.src); } catch (Exception e) { + ctx.replace(LytParagraph.error("[CsvTable] Invalid CSV path: " + ph.src)); return; } byte[] data = ctx.loadAsset(csvId); - if (data == null) return; + if (data == null) { + ctx.replace(LytParagraph.error("[CsvTable] CSV not found: " + ph.src)); + return; + } - List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); - LytBlock table = CsvTableCompiler.buildTable(rows, ph.header, ph.widths); - if (table != null) { - ctx.replace(table); + try { + List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); + LytBlock table = CsvTableCompiler.buildTable(rows, ph.header, ph.widths); + if (table != null) { + ctx.replace(table); + } else { + ctx.replace(LytParagraph.error("[CsvTable] Failed to parse CSV: " + ph.src)); + } + } catch (Exception e) { + ctx.replace(LytParagraph.error("[CsvTable] Failed to parse CSV: " + ph.src)); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index 253ede89..fec62ff4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.document.block.ImageRegionAnnotation; import com.hfstudio.guidenh.guide.document.block.LytImage; import com.hfstudio.guidenh.guide.document.block.LytImageBlock; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -40,16 +41,24 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } String src = placeholder.getSrc(); - if (src == null || src.isEmpty()) return; + if (src == null || src.isEmpty()) { + ctx.replace(LytParagraph.error("[FloatingImage] Missing src attribute")); + return; + } ResourceLocation imageId; try { imageId = new ResourceLocation(src); } catch (Exception e) { + ctx.replace(LytParagraph.error("[FloatingImage] Invalid image path: " + src)); return; } byte[] imageData = ctx.loadAsset(imageId); + if (imageData == null) { + ctx.replace(LytParagraph.error("[FloatingImage] Image not found: " + src)); + return; + } LytImage image = new LytImage(); image.setImage(imageId, imageData); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index 373d310e..ad804ddf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.document.block.ImageRegionAnnotation; import com.hfstudio.guidenh.guide.document.block.LytImage; import com.hfstudio.guidenh.guide.document.block.LytImageBlock; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -38,16 +39,24 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } String src = placeholder.getSrc(); - if (src == null || src.isEmpty()) return; + if (src == null || src.isEmpty()) { + ctx.replace(LytParagraph.error("[Image] Missing src attribute")); + return; + } ResourceLocation imageId; try { imageId = new ResourceLocation(src); } catch (Exception e) { + ctx.replace(LytParagraph.error("[Image] Invalid image path: " + src)); return; } byte[] imageData = ctx.loadAsset(imageId); + if (imageData == null) { + ctx.replace(LytParagraph.error("[Image] Image not found: " + src)); + return; + } LytImage image = new LytImage(); image.setImage(imageId, imageData); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index f6cb8f99..48a6bca8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.ItemGridCompiler.ItemGridPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemGrid; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -24,13 +25,19 @@ public class ItemGridScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() == EventType.MOUNT && node instanceof ItemGridPlaceholder ph) { LytItemGrid grid = new LytItemGrid(); + int resolved = 0; for (String itemId : ph.itemIds) { ItemStack stack = resolveItemId(itemId.trim()); if (stack != null) { grid.addItem(stack); + resolved++; } } - ctx.replace(grid); + if (resolved == 0) { + ctx.replace(LytParagraph.error("[ItemGrid] No items to display")); + } else { + ctx.replace(grid); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index bf73f79b..45fed0c7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemImage; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -36,7 +37,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } ItemStack stack = resolveItemId(ph.itemId); - if (stack == null) return; + if (stack == null) { + ctx.replace(LytParagraph.error("[ItemImage] Item not found: " + ph.itemId)); + return; + } LytItemImage image = new LytItemImage(stack); image.setScale(ph.scale); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index eaa47edd..7fae1b40 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -35,6 +36,12 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Boolean showTooltip = (Boolean) link.getData("showTooltip"); String linksTo = (String) link.getData("linksTo"); + // Neither target specified + if ((itemId == null || itemId.isEmpty()) && (ore == null || ore.isEmpty())) { + ctx.replace(LytParagraph.error("[ItemLink] Link has no target")); + return; + } + ItemStack stack = resolveItemStack(itemId); if (stack == null && ore != null && !ore.isEmpty()) { java.util.List ores = OreDictionary.getOres(ore); @@ -42,7 +49,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { stack = ores.get(0); } } - if (stack == null) return; + if (stack == null) { + String detail = (itemId != null && !itemId.isEmpty()) ? itemId : ore; + ctx.replace(LytParagraph.error("[ItemLink] Link target not found: " + detail)); + return; + } PageAnchor anchor = findLinkTarget(stack, linksTo, ctx); if (anchor != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index fcf1f4f9..d9e6af35 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -38,7 +38,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { try { srcId = new ResourceLocation(ph.src); } catch (Exception e) { - replaceWithError(ctx, "Invalid Mermaid source path: " + ph.src); + replaceWithError(ctx, "Invalid source path: " + ph.src); return; } byte[] data = ctx.loadAsset(srcId); @@ -48,7 +48,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } if (sourceText == null || sourceText.trim().isEmpty()) { - replaceWithError(ctx, "Mermaid source not found or empty"); + replaceWithError(ctx, "Source not found or empty"); return; } @@ -63,11 +63,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } catch (IllegalArgumentException e) { FMLLog.getLogger().warn( "[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); - replaceWithError(ctx, "Failed to parse Mermaid source: " + e.getMessage()); + replaceWithError(ctx, "Failed to parse: " + e.getMessage()); } } private void replaceWithError(ScriptContext ctx, String message) { - ctx.replace(LytParagraph.of(message)); + ctx.replace(LytParagraph.error("[Mermaid] " + message)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java index 6c057279..04fb6a5e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java @@ -33,8 +33,18 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; if (!(node instanceof QuestCardPlaceholder ph)) return; - QuestDisplay display = BqHelpers.resolveDisplay(ph.questId, Minecraft.getMinecraft().thePlayer, - ph.showTooltip || ph.showDesc); + QuestDisplay display; + try { + display = BqHelpers.resolveDisplay(ph.questId, Minecraft.getMinecraft().thePlayer, + ph.showTooltip || ph.showDesc); + } catch (Throwable t) { + ctx.replace(LytParagraph.error("[QuestCard] BetterQuesting integration not available")); + return; + } + if (display == null) { + ctx.replace(LytParagraph.error("[QuestCard] Quest not found: " + ph.questId)); + return; + } QuestState state = display.getState(); var box = new LytQuoteBox(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java index bdc606c8..a12aa212 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java @@ -42,6 +42,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { QuestDisplay display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, Boolean.TRUE.equals(showTooltip)); + if (display == null) { + LytFlowSpan errorSpan = new LytFlowSpan(); + errorSpan.modifyStyle(style -> style.color(SymbolicColor.ERROR_TEXT)); + errorSpan.appendText("[QuestLink] Quest not found: " + questId); + ctx.replace(errorSpan); + return; + } QuestState state = display.getState(); String text = overrideText != null && !overrideText.isEmpty() ? overrideText diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index 8448979a..6271d46c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -54,7 +54,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Item item = (Item) Item.itemRegistry.getObject(ph.ref.rawKey()); if (item == null) { - showFallback(ctx, ph); + showFallback(ctx, ph, "Recipe item not found: " + ph.idStr); return; } ItemStack targetStack = new ItemStack(item, 1, ph.ref.concreteMeta()); @@ -125,11 +125,15 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } if (ph.recipeIndex >= 0) { - showFallback(ctx, ph); + showFallback(ctx, ph, "Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); return; } } else if (hasHandlerFilter) { - showFallback(ctx, ph); + String handlerPart = ""; + if (ph.handlerName != null || ph.handlerId != null) { + handlerPart = " with handler " + (ph.handlerName != null ? ph.handlerName : ph.handlerId); + } + showFallback(ctx, ph, "No recipe found for " + ph.idStr + handlerPart); return; } @@ -169,7 +173,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { List entries = usageQuery ? Collections.emptyList() : RecipeLookup.findByOutput(item); if (entries.isEmpty()) { - showFallback(ctx, ph); + showFallback(ctx, ph, "No recipe found for " + ph.idStr); return; } @@ -189,7 +193,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ctx.replace(buildResult(boxes)); return; } - showFallback(ctx, ph); + showFallback(ctx, ph, "No recipe found for " + ph.idStr); } @SuppressWarnings("unchecked") @@ -205,9 +209,9 @@ private static LytBlock buildResultTyped(List boxes) { return row; } - private void showFallback(ScriptContext ctx, RecipePlaceholder ph) { + private void showFallback(ScriptContext ctx, RecipePlaceholder ph, String autoMessage) { String text = (ph.fallbackText != null && !ph.fallbackText.isEmpty()) - ? ph.fallbackText : "No recipe found."; + ? ph.fallbackText : autoMessage; var p = new LytParagraph(); p.appendText(text); ctx.replace(p); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index 35cfb2aa..beab6ef6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -13,6 +13,7 @@ import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.document.LytErrorSink; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.PageIndex; import com.hfstudio.guidenh.guide.internal.extensions.DefaultExtensions; @@ -66,7 +67,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; if (!(node instanceof ScenePlaceholder ph)) return; - if (ph.childrenSource == null || ph.childrenSource.trim().isEmpty()) return; + if (ph.childrenSource == null || ph.childrenSource.trim().isEmpty()) { + ctx.replace(LytParagraph.error("[Scene] Empty scene: no scene elements")); + return; + } GuidebookLevel level = new GuidebookLevel(); CameraSettings camera = new CameraSettings(); @@ -109,6 +113,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } catch (Exception e) { FMLLog.getLogger().warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); + ctx.replace(LytParagraph.error("[Scene] Failed to parse scene elements")); + return; + } + + if (level.isEmpty()) { + ctx.replace(LytParagraph.error("[Scene] Scene has no supported elements")); + return; } LytGuidebookScene scene = new LytGuidebookScene(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index e24faf18..e30998f6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -4,6 +4,7 @@ import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.MediaWikiTagCompilerSupport; import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.SpecialCompiler.SpecialPlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -28,13 +29,19 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (!(node instanceof SpecialPlaceholder ph)) return; PageCollection pc = ctx.getPageCollection(); - if (!(pc instanceof Guide guide)) return; + if (!(pc instanceof Guide guide)) { + ctx.replace(LytParagraph.error("[Special] Special page not available: collection is not a guide")); + return; + } CategoryIndex categoryIndex = ctx.getIndex(CategoryIndex.class); var resolver = new MediaWikiSpecialPageResolver(); String specialName = resolver.normalizeSupportedName(ph.name); - if (specialName == null) return; + if (specialName == null) { + ctx.replace(LytParagraph.error("[Special] Unsupported special page: " + ph.name)); + return; + } MediaWikiListContext context = MediaWikiTagCompilerSupport.createListContext(guide, categoryIndex); MediaWikiSpecialPageQuery query = new MediaWikiSpecialPageQuery("", diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index db2c4a97..e7c4cd1d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.StructureViewCompiler.StructureEntry; import com.hfstudio.guidenh.guide.compiler.tags.StructureViewCompiler.StructurePlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytStructureView; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -25,13 +26,19 @@ public class StructureScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() == EventType.MOUNT && node instanceof StructurePlaceholder ph) { LytStructureView view = new LytStructureView(); + int resolved = 0; for (StructureEntry entry : ph.entries) { ItemStack stack = resolveEntry(entry.idSpec); if (stack != null) { view.addBlock(entry.x, entry.y, entry.z, stack); + resolved++; } } - ctx.replace(view); + if (resolved == 0) { + ctx.replace(LytParagraph.error("[Structure] Structure has no valid blocks")); + } else { + ctx.replace(view); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index e7f440f5..36d7dfda 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -40,7 +40,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } else { ResourceLocation pageId = new ResourceLocation(ph.pageIdStr); NavigationNode navNode = tree.getNodeById(pageId); - if (navNode == null) return; + if (navNode == null) { + ctx.replace(LytParagraph.error("[SubPages] Page not found in navigation: " + ph.pageIdStr)); + return; + } subNodes = navNode.children(); } @@ -49,6 +52,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { subNodes.sort(Comparator.comparing(NavigationNode::title)); } + if (subNodes.isEmpty()) { + ctx.replace(LytParagraph.error("[SubPages] No sub-pages found")); + return; + } + LytList list = new LytList(false, 0); for (NavigationNode childNode : subNodes) { if (!childNode.hasPage()) continue; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index dc241af5..f536154f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -135,7 +135,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl compiler.getPageId().getResourceDomain() ); placeholder.setStyleClass(styleClass); - placeholder.setStyle(LytParagraph.LOADING_STYLE); + placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); placeholder.appendText("[" + styleClass + "]"); parent.append(placeholder); } From 2fd7d6cb4cb0e447cb56c7349110ff054a9cba0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 14:15:16 +0800 Subject: [PATCH 048/136] fix: transparent LytFlowInlineBlock penetration in ctx.replace, universal replaceChild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScriptContextImpl.replace(): when node is LytFlowInlineBlock and newNode is LytBlock, swap inner block via setBlock() — all block-level scripts automatically work in paragraph/list-item contexts without wrapper awareness. - LytNode.replaceChild(): default throws UnsupportedOperationException instead of silent no-op. Missing overrides added to LytAlignedBlock, LytDocumentFloat, LytDetailsBlock, LytQuoteBox, and LytTableRow. - ItemId parsing in ItemImageScript, ItemGridScript, StructureScript: use IdUtils.parseItemRef() to correctly handle namespace:path format (was broken by lastIndexOf(':') splitting). - SceneScript: GuideSceneStructureCompileScope.run(true, ...) to enable scene element compilation at runtime. - LytHostWorkItem: always return YIELD to prevent scheduler ejection. - LytParagraph: PLACEHOLDER_STYLE (amber) and ERROR_STYLE (red) for consistent placeholder/error display. - RecipeScript: use ph.limit directly, always ctx.replace() in showFallback. - FloatingImageCompiler: Random(0) for deterministic border color. - BlockImageCompiler: meta default Integer.MIN_VALUE sentinel. - docs/refractor/phase3-two-tree-problem.md: architectural memo. All scripts: replace "Loading..." spam with clean [TagName] placeholders and [TagName] error messages on failure paths. --- .../guide/document/block/LytAlignedBlock.java | 12 +++++++- .../guide/document/block/LytDetailsBlock.java | 5 ++++ .../document/block/LytDocumentFloat.java | 12 +++++++- .../guidenh/guide/document/block/LytNode.java | 3 +- .../guide/document/block/LytQuoteBox.java | 5 ++++ .../document/block/table/LytTableRow.java | 9 ++++++ .../internal/host/ScriptContextImpl.java | 25 +++++++++++++++++ .../host/scripts/BlockImageScript.java | 21 ++++++++++++-- .../host/scripts/FloatingImageScript.java | 17 +++++++++-- .../internal/host/scripts/ImageScript.java | 17 +++++++++-- .../internal/host/scripts/ItemGridScript.java | 15 ++++------ .../host/scripts/ItemImageScript.java | 28 ++++++++++++------- .../internal/host/scripts/ItemLinkScript.java | 11 ++++++-- .../internal/host/scripts/RecipeScript.java | 24 ++++++++++++---- .../internal/host/scripts/SceneScript.java | 17 ++++++----- .../host/scripts/StructureScript.java | 15 ++++------ 16 files changed, 181 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java index c6ad3164..8803b94c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java @@ -28,7 +28,7 @@ */ public class LytAlignedBlock extends LytBlock { - private final LytBlock inner; + private LytBlock inner; private final ContentAlign align; /** @@ -49,6 +49,16 @@ public ContentAlign getAlign() { return align; } + @Override + public void replaceChild(LytNode oldChild, LytNode newChild) { + if (oldChild != inner || !(newChild instanceof LytBlock)) return; + inner.parent = null; + inner = (LytBlock) newChild; + inner.parent = this; + LytDocument doc = getDocument(); + if (doc != null) doc.invalidateLayout(); + } + @Override public List getChildren() { return Collections.singletonList(inner); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java index a3b99dac..cddc0535 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java @@ -133,6 +133,11 @@ public void removeChild(LytNode node) { content.removeChild(node); } + @Override + public void replaceChild(LytNode oldChild, LytNode newChild) { + root.replaceChild(oldChild, newChild); + } + @Override public List getChildren() { return root.getChildren(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java index 57360a28..bbb923cd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java @@ -40,7 +40,7 @@ public class LytDocumentFloat extends LytBlock { private static final int FLOAT_GAP = 5; - private final LytBlock inner; + private LytBlock inner; private final boolean floatRight; /** @@ -57,6 +57,16 @@ public LytBlock getInner() { return inner; } + @Override + public void replaceChild(LytNode oldChild, LytNode newChild) { + if (oldChild != inner || !(newChild instanceof LytBlock)) return; + inner.parent = null; + inner = (LytBlock) newChild; + inner.parent = this; + LytDocument doc = getDocument(); + if (doc != null) doc.invalidateLayout(); + } + public boolean isFloatRight() { return floatRight; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java index 796eac8c..b4386b6a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java @@ -37,7 +37,8 @@ public abstract class LytNode implements Styleable { public void removeChild(LytNode node) {} public void replaceChild(LytNode oldChild, LytNode newChild) { - // Default: no-op. LytDocument overrides. + throw new UnsupportedOperationException( + getClass().getSimpleName() + " must override replaceChild"); } protected void onAttach() {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytQuoteBox.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytQuoteBox.java index 6fc5a8e3..5f22d742 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytQuoteBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytQuoteBox.java @@ -90,6 +90,11 @@ public void removeChild(LytNode node) { content.removeChild(node); } + @Override + public void replaceChild(LytNode oldChild, LytNode newChild) { + root.replaceChild(oldChild, newChild); + } + @Override public List getChildren() { return root.getChildren(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/table/LytTableRow.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/table/LytTableRow.java index 1f784d13..966dd46e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/table/LytTableRow.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/table/LytTableRow.java @@ -35,4 +35,13 @@ public LytTableCell appendCell() { public List getChildren() { return cells; } + + @Override + @SuppressWarnings("unchecked") + public void replaceChild(LytNode oldChild, LytNode newChild) { + if (!(newChild instanceof LytTableCell)) return; + int idx = cells.indexOf(oldChild); + if (idx < 0) return; + cells.set(idx, (LytTableCell) newChild); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index e4290c33..8c3bdc32 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -9,10 +9,14 @@ import net.minecraft.util.ResourceLocation; import com.hfstudio.guidenh.guide.PageCollection; +import com.hfstudio.guidenh.guide.document.block.LytAlignedBlock; +import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; +import com.hfstudio.guidenh.guide.document.block.LytDocumentFloat; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.indices.PageIndex; @@ -35,6 +39,27 @@ class ScriptContextImpl implements ScriptContext { @Override @SuppressWarnings("unchecked") public void replace(Object newNode) { + // + // Flow-content wrapping penetration + // + // When a block-level tag (e.g. , ) appears inside + // a paragraph or list item, the PageCompiler wraps it in LytFlowInlineBlock + // so the block can participate in inline flow layout. At MOUNT time the + // dispatch passes the wrapper as "this.node", not the inner placeholder. + // + // The wrapper IS the correct replacement target — swapping its inner block + // via setBlock() preserves the flow-layout context (alignment, line-breaking, + // float registration) that the compiler set up. + // + // See docs/refractor/phase3-two-tree-problem.md for the architectural + // discussion of why Flow and Block trees are separate and how this bridge works. + // + if (node instanceof LytFlowInlineBlock wrapper && newNode instanceof LytBlock newBlock) { + wrapper.setBlock(newBlock); + document.invalidateLayout(); + return; + } + if (node instanceof LytNode ln && newNode instanceof LytNode newLn) { LytNode parent = ln.getParent(); if (parent != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index bee0efe7..9df6ca3b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -8,7 +8,9 @@ import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler.BlockImagePlaceholder; +import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -38,7 +40,17 @@ public class BlockImageScript implements LytScript { @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - if (!(node instanceof BlockImagePlaceholder ph)) return; + + BlockImagePlaceholder ph; + boolean isWrapped = node instanceof LytFlowInlineBlock w + && w.getBlock() instanceof BlockImagePlaceholder p; + if (isWrapped) { + ph = (BlockImagePlaceholder) ((LytFlowInlineBlock) node).getBlock(); + } else if (node instanceof BlockImagePlaceholder p) { + ph = p; + } else { + return; + } Block block = null; int meta = ph.meta; @@ -60,7 +72,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } if (block == null) { - ctx.replace(LytParagraph.error("[BlockImage] Block not found: " + (ph.ore != null ? ph.ore : ph.id))); + ctx.replace( + LytParagraph.error("[BlockImage] Block not found: " + (ph.ore != null ? ph.ore : ph.id))); return; } @@ -81,7 +94,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag); if (level.isEmpty()) { - ctx.replace(LytParagraph.error("[BlockImage] Failed to create block preview")); + ctx.replace( + LytParagraph.error("[BlockImage] Failed to create block preview")); return; } @@ -149,4 +163,5 @@ private static float clampZoom(float zoom) { private static int clampDim(int d) { return Math.max(64, Math.min(256, d)); } + } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index fec62ff4..14cc4195 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -42,7 +42,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String src = placeholder.getSrc(); if (src == null || src.isEmpty()) { - ctx.replace(LytParagraph.error("[FloatingImage] Missing src attribute")); + replaceFlowError(ctx, isWrapped, "[FloatingImage] Missing src attribute"); return; } @@ -50,13 +50,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { try { imageId = new ResourceLocation(src); } catch (Exception e) { - ctx.replace(LytParagraph.error("[FloatingImage] Invalid image path: " + src)); + replaceFlowError(ctx, isWrapped, "[FloatingImage] Invalid image path: " + src); return; } byte[] imageData = ctx.loadAsset(imageId); if (imageData == null) { - ctx.replace(LytParagraph.error("[FloatingImage] Image not found: " + src)); + replaceFlowError(ctx, isWrapped, "[FloatingImage] Image not found: " + src); return; } LytImage image = new LytImage(); @@ -85,4 +85,15 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ctx.replace(image); } } + + private void replaceFlowError(ScriptContext ctx, boolean isWrapped, String message) { + LytParagraph error = LytParagraph.error(message); + if (isWrapped) { + LytFlowInlineBlock wrapper = new LytFlowInlineBlock(); + wrapper.setBlock(error); + ctx.replace(wrapper); + } else { + ctx.replace(error); + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index ad804ddf..d98cf9ab 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -40,7 +40,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String src = placeholder.getSrc(); if (src == null || src.isEmpty()) { - ctx.replace(LytParagraph.error("[Image] Missing src attribute")); + replaceFlowError(ctx, isWrapped, "[Image] Missing src attribute"); return; } @@ -48,13 +48,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { try { imageId = new ResourceLocation(src); } catch (Exception e) { - ctx.replace(LytParagraph.error("[Image] Invalid image path: " + src)); + replaceFlowError(ctx, isWrapped, "[Image] Invalid image path: " + src); return; } byte[] imageData = ctx.loadAsset(imageId); if (imageData == null) { - ctx.replace(LytParagraph.error("[Image] Image not found: " + src)); + replaceFlowError(ctx, isWrapped, "[Image] Image not found: " + src); return; } LytImage image = new LytImage(); @@ -78,4 +78,15 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ctx.replace(image); } } + + private void replaceFlowError(ScriptContext ctx, boolean isWrapped, String message) { + LytParagraph error = LytParagraph.error(message); + if (isWrapped) { + LytFlowInlineBlock wrapper = new LytFlowInlineBlock(); + wrapper.setBlock(error); + ctx.replace(wrapper); + } else { + ctx.replace(error); + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index 48a6bca8..3179ed1e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -43,14 +43,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { - int colonIdx = itemId.lastIndexOf(':'); - if (colonIdx < 0) return null; - - String rawKey = itemId.substring(0, colonIdx); - int meta = 0; - try { meta = Integer.parseInt(itemId.substring(colonIdx + 1)); } catch (NumberFormatException ignored) {} - - Item item = (Item) Item.itemRegistry.getObject(rawKey); - return item != null ? new ItemStack(item, 1, meta) : null; + if (itemId == null || itemId.isEmpty()) return null; + com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = + com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, "minecraft"); + if (ref == null) return null; + Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); + return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index 45fed0c7..3f1a7601 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -38,7 +38,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ItemStack stack = resolveItemId(ph.itemId); if (stack == null) { - ctx.replace(LytParagraph.error("[ItemImage] Item not found: " + ph.itemId)); + replaceFlowError(ctx, isWrapped, "[ItemImage] Item not found: " + ph.itemId); return; } @@ -60,17 +60,25 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } + @SuppressWarnings("deprecation") + private void replaceFlowError(ScriptContext ctx, boolean isWrapped, String message) { + LytParagraph error = LytParagraph.error(message); + if (isWrapped) { + LytFlowInlineBlock wrapper = new LytFlowInlineBlock(); + wrapper.setBlock(error); + ctx.replace(wrapper); + } else { + ctx.replace(error); + } + } + @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { if (itemId == null || itemId.isEmpty()) return null; - int colonIdx = itemId.lastIndexOf(':'); - if (colonIdx < 0) return null; - - String rawKey = itemId.substring(0, colonIdx); - int meta = 0; - try { meta = Integer.parseInt(itemId.substring(colonIdx + 1)); } catch (NumberFormatException ignored) {} - - Item item = (Item) Item.itemRegistry.getObject(rawKey); - return item != null ? new ItemStack(item, 1, meta) : null; + com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = + com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, "minecraft"); + if (ref == null) return null; + Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); + return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 7fae1b40..760c6f0c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -9,6 +9,7 @@ import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -38,7 +39,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Neither target specified if ((itemId == null || itemId.isEmpty()) && (ore == null || ore.isEmpty())) { - ctx.replace(LytParagraph.error("[ItemLink] Link has no target")); + replaceFlowError(ctx, "[ItemLink] Link has no target"); return; } @@ -51,7 +52,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } if (stack == null) { String detail = (itemId != null && !itemId.isEmpty()) ? itemId : ore; - ctx.replace(LytParagraph.error("[ItemLink] Link target not found: " + detail)); + replaceFlowError(ctx, "[ItemLink] Link target not found: " + detail); return; } @@ -106,4 +107,10 @@ private static PageAnchor findLinkTarget(ItemStack stack, @Nullable String links } return null; } + + private void replaceFlowError(ScriptContext ctx, String message) { + LytFlowInlineBlock wrapper = new LytFlowInlineBlock(); + wrapper.setBlock(LytParagraph.error(message)); + ctx.replace(wrapper); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index 6271d46c..f174f815 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -19,6 +19,7 @@ import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytHBox; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.block.recipes.LytStandardRecipeBox; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -50,11 +51,21 @@ public class RecipeScript implements LytScript { @SuppressWarnings("deprecation") public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - if (!(node instanceof RecipePlaceholder ph)) return; + + RecipePlaceholder ph; + boolean isWrapped = node instanceof LytFlowInlineBlock w + && w.getBlock() instanceof RecipePlaceholder p; + if (isWrapped) { + ph = (RecipePlaceholder) ((LytFlowInlineBlock) node).getBlock(); + } else if (node instanceof RecipePlaceholder p) { + ph = p; + } else { + return; + } Item item = (Item) Item.itemRegistry.getObject(ph.ref.rawKey()); if (item == null) { - showFallback(ctx, ph, "Recipe item not found: " + ph.idStr); + showFallback(ctx, ph,"Recipe item not found: " + ph.idStr); return; } ItemStack targetStack = new ItemStack(item, 1, ph.ref.concreteMeta()); @@ -125,7 +136,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } if (ph.recipeIndex >= 0) { - showFallback(ctx, ph, "Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); + showFallback(ctx, ph,"Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); return; } } else if (hasHandlerFilter) { @@ -133,7 +144,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (ph.handlerName != null || ph.handlerId != null) { handlerPart = " with handler " + (ph.handlerName != null ? ph.handlerName : ph.handlerId); } - showFallback(ctx, ph, "No recipe found for " + ph.idStr + handlerPart); + showFallback(ctx, ph,"No recipe found for " + ph.idStr + handlerPart); return; } @@ -173,7 +184,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { List entries = usageQuery ? Collections.emptyList() : RecipeLookup.findByOutput(item); if (entries.isEmpty()) { - showFallback(ctx, ph, "No recipe found for " + ph.idStr); + showFallback(ctx, ph,"No recipe found for " + ph.idStr); return; } @@ -193,7 +204,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ctx.replace(buildResult(boxes)); return; } - showFallback(ctx, ph, "No recipe found for " + ph.idStr); + showFallback(ctx, ph,"No recipe found for " + ph.idStr); } @SuppressWarnings("unchecked") @@ -216,4 +227,5 @@ private void showFallback(ScriptContext ctx, RecipePlaceholder ph, String autoMe p.appendText(text); ctx.replace(p); } + } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index beab6ef6..192e94f0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -20,6 +20,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.scene.CameraSettings; +import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; import com.hfstudio.guidenh.guide.scene.SceneTagCompiler; @@ -103,14 +104,16 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { try { MdAstRoot ast = MdAst.fromMarkdown(ph.childrenSource, GuideMarkdownOptions.runtime()); MdAstToMdxConverter.convert(ast, Collections.emptyMap()); - for (UnistNode child : ast.children()) { - MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); - if (el == null) continue; - SceneElementTagCompiler ec = elementCompilers.get(el.name()); - if (ec != null) { - ec.compile(level, camera, runtimeCompiler, errorSink, el); + GuideSceneStructureCompileScope.run(true, () -> { + for (UnistNode child : ast.children()) { + MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); + if (el == null) continue; + SceneElementTagCompiler ec = elementCompilers.get(el.name()); + if (ec != null) { + ec.compile(level, camera, runtimeCompiler, errorSink, el); + } } - } + }); } catch (Exception e) { FMLLog.getLogger().warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); ctx.replace(LytParagraph.error("[Scene] Failed to parse scene elements")); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index e7c4cd1d..ff05bc09 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -44,14 +44,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @SuppressWarnings("deprecation") private static ItemStack resolveEntry(String idSpec) { - int colonIdx = idSpec.lastIndexOf(':'); - if (colonIdx < 0) return null; - - String rawKey = idSpec.substring(0, colonIdx); - int meta = 0; - try { meta = Integer.parseInt(idSpec.substring(colonIdx + 1)); } catch (NumberFormatException ignored) {} - - Item item = (Item) Item.itemRegistry.getObject(rawKey); - return item != null ? new ItemStack(item, 1, meta) : null; + if (idSpec == null || idSpec.isEmpty()) return null; + com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = + com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(idSpec, "minecraft"); + if (ref == null) return null; + Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); + return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; } } From e86116c6b9d38c057880c16574c2cd46ce673357 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 28 May 2026 15:30:49 +0800 Subject: [PATCH 049/136] sa --- .../internal/editor/md/SceneEditorMarkdownCodec.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java index bae83a64..3b073968 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java @@ -70,16 +70,8 @@ public class SceneEditorMarkdownCodec { "offsetY", "offsetZ", "formed"); - public static final Set STRUCTURE_LIB_OPTION_ATTRIBUTES = Set.of( - "name", - "id", - "value", - "expr", - "tier", - "facing", - "rotation", - "flip", - "channel"); + public static final Set STRUCTURE_LIB_OPTION_ATTRIBUTES = Set + .of("name", "id", "value", "expr", "tier", "facing", "rotation", "flip", "channel"); public static final Set STRUCTURE_LIB_OPTION_TAGS = Set.of( "Tier", "Channel", From 5c257084d115b40adb0061edd6a188c0445fdd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 19:03:48 +0800 Subject: [PATCH 050/136] fix: resolve merge compilation errors - Remove extra closing brace in MutableGuide.close() - Remove unused GuideDevWatcherPump import in ClientProxy - Replace Collections.emptyMap() with Map.of() in SceneEditorMarkdownCodec --- src/main/java/com/hfstudio/guidenh/ClientProxy.java | 1 - .../java/com/hfstudio/guidenh/guide/internal/MutableGuide.java | 1 - .../guide/internal/editor/md/SceneEditorMarkdownCodec.java | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index e9dc8912..5df5568f 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -15,7 +15,6 @@ import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.client.hotkey.OpenSceneEditorHotkey; import com.hfstudio.guidenh.config.ModConfig; -import com.hfstudio.guidenh.guide.internal.GuideDevWatcherPump; import com.hfstudio.guidenh.guide.internal.GuideDevelopmentResourcePackWatcher; import com.hfstudio.guidenh.guide.internal.GuideME; import com.hfstudio.guidenh.guide.internal.GuideOnStartup; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java index c6babc86..a18a73db 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java @@ -357,7 +357,6 @@ public synchronized void close() { syntheticPageCount, failureCount); } - } private void applyChanges(List changes) { invalidateMediaWikiDerivedCaches(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java index 653ee48b..8406f6e0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java @@ -156,7 +156,7 @@ public SceneEditorMarkdownParseResult parse(String markdown) { MdAstRoot root; try { root = MdAst.fromMarkdown(parseSource, PARSE_OPTIONS); - MdAstToMdxConverter.convert(root, Collections.emptyMap()); + MdAstToMdxConverter.convert(root, Map.of()); } catch (ParseException e) { return new SceneEditorMarkdownParseResult.SyntaxError(formatParseException(e)); } From 50cc1a232af45375598b355e061d3e0b4f1a8f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 19:35:59 +0800 Subject: [PATCH 051/136] fix: null-guard drawTiledBackground against missing mc/textureManager GuideScreen.drawTiledBackground could NPE when mc is null during screen transitions. Added defensive null check with warning log instead of crash. --- .../com/hfstudio/guidenh/guide/internal/GuideScreen.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 3106620f..60fca278 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -4496,8 +4496,11 @@ private boolean canSearchCurrentView() { private void drawTiledBackground() { drawRect(0, 0, this.width, this.height, BACKGROUND_DIM_COLOR); - mc.getTextureManager() - .bindTexture(BG_TEXTURE); + if (mc == null || mc.getTextureManager() == null) { + FMLLog.getLogger().warn("[GuideNH] drawTiledBackground: mc or textureManager is null, skipping"); + return; + } + mc.getTextureManager().bindTexture(BG_TEXTURE); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); GL11.glEnable(GL11.GL_BLEND); From 610160eeed5bc2292b2a03559ed8b55986a45d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 21:40:06 +0800 Subject: [PATCH 052/136] feat: wire document cache and preheat pipeline with mount/swap dual-path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace setDocument with mountDocument (fresh) and swapDocument (cached) - PageCacheEntry stores GuidePage with mounted flag to distinguish preheated-only from fully materialized cache entries - completePendingContentPageLoadIfNeeded checks cache before compiling - Wire PreheatCompiler, requestPreheatNeighbors, preheatStep to real impl - LytHostPreheatItem: shouldRun → hasPreheatWork, tick → preheatStep - LRU eviction at 32 pages; evict/invalidate clean up preheatScheduled - Null-guard drawPageMissingMessage and drawLoadingMessage against mc==null --- .../guidenh/guide/internal/GuideScreen.java | 48 ++++++-- .../guidenh/guide/internal/host/LytHost.java | 108 ++++++++++++++++-- .../scheduler/LytHostPreheatItem.java | 5 +- 3 files changed, 140 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 60fca278..a152a22a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -105,6 +105,7 @@ import com.hfstudio.guidenh.guide.internal.item.RegionWandItem; import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; import com.hfstudio.guidenh.ClientProxy; +import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.NavigationState; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar; @@ -497,6 +498,14 @@ private GuideScreen(GuideScreenRoute route, @Nullable GuideScreenViewState resto navBar.setPinned(false); } navBar.restoreState(ClientProxy.getLytHost().getNavigation().recallNavigationState(), bookmarkState); + ClientProxy.getLytHost().setPreheatCompiler(pageId -> { + if (guide == null) return null; + try { + return guide.getPage(new net.minecraft.util.ResourceLocation(pageId)); + } catch (Exception e) { + return null; + } + }); } public static void open(ResourceLocation guideId, @Nullable PageAnchor anchor) { @@ -608,6 +617,9 @@ public void reloadPage() { currentPage = null; document = null; lastLayoutWidth = -1; + if (currentAnchor != null) { + ClientProxy.getLytHost().invalidatePage(currentAnchor.pageId().toString()); + } loadCurrentPage(); updateToolbarButtonState(); } @@ -2507,20 +2519,39 @@ private void completePendingContentPageLoadIfNeeded() { return; } int requestId = pendingPageLoadRequestId; + String pageIdStr = currentAnchor.pageId().toString(); + LytHost lytHost = ClientProxy.getLytHost(); GuidePage loadedPage = null; - try { - loadedPage = guide.getPage(currentAnchor.pageId()); - } catch (Throwable t) { - FMLLog.severe("Failed to compile guide page {}", currentAnchor.pageId(), t); + + GuidePage cachedPage = lytHost.getCachedGuidePage(pageIdStr); + boolean alreadyMounted = cachedPage != null && lytHost.isPageMounted(pageIdStr); + if (cachedPage != null) { + loadedPage = cachedPage; + loadedPage.prepareForDisplay(); + } else { + try { + loadedPage = guide.getPage(currentAnchor.pageId()); + } catch (Throwable t) { + FMLLog.severe("Failed to compile guide page {}", currentAnchor.pageId(), t); + } } if (!pageLoadInProgress || requestId != pendingPageLoadRequestId) { return; } currentPage = loadedPage; document = loadedPage != null ? loadedPage.document() : null; - ClientProxy.getLytHost().setCurrentPageId(currentAnchor.pageId().toString()); - ClientProxy.getLytHost().setCurrentPageCollection(guide); - ClientProxy.getLytHost().setDocument(document); + lytHost.setCurrentPageId(pageIdStr); + lytHost.setCurrentPageCollection(guide); + if (alreadyMounted) { + lytHost.swapDocument(document); + } else { + lytHost.mountDocument(document); + if (loadedPage != null) { + lytHost.cachePage(pageIdStr, loadedPage); + lytHost.markPageMounted(pageIdStr); + } + } + lytHost.requestPreheatNeighbors(pageIdStr); if (document != null && isSpecialPageWithSearchField()) { applySpecialPageSearchQuery(queryFromCurrentAnchor()); } @@ -4441,7 +4472,7 @@ public static LytRect cachedRect(@Nullable LytRect current, int x, int y, int w, } private void drawPageMissingMessage() { - if (isHomeRoute()) { + if (isHomeRoute() || mc == null) { return; } FontRenderer fr = mc.fontRenderer; @@ -4451,6 +4482,7 @@ private void drawPageMissingMessage() { } private void drawLoadingMessage() { + if (mc == null) return; FontRenderer fr = mc.fontRenderer; String message = buildAnimatedLoadingLabel(GuidebookText.SceneLoading.text()); int tw = fr.getStringWidth(message); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 2dfb6309..1f35d21f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; @@ -28,32 +29,61 @@ public class LytHost { private final Map pageNodeCounters = new HashMap<>(); String currentPageId; + private static final int MAX_CACHED_PAGES = 32; + static class PageCacheEntry { - final LytDocument document; + final GuidePage guidePage; final Map nodeResults = new HashMap<>(); - PageCacheEntry(LytDocument document) { this.document = document; } + boolean mounted; + PageCacheEntry(GuidePage guidePage) { this.guidePage = guidePage; } } private final ViewportState viewport = new ViewportState(); private final NavigationState nav = new NavigationState(); private final Deque eventQueue = new ArrayDeque<>(); private final Deque taskQueue = new ArrayDeque<>(); + private final Deque preheatQueue = new ArrayDeque<>(); + private final java.util.Set preheatScheduled = new java.util.HashSet<>(); + + @FunctionalInterface + public interface PreheatCompiler { + @Nullable GuidePage compile(String pageId); + } + + @Nullable private PreheatCompiler preheatCompiler; + + public void setPreheatCompiler(@Nullable PreheatCompiler compiler) { + this.preheatCompiler = compiler; + } // ===== Document ===== - public void setDocument(@Nullable LytDocument newDoc) { + /** Full processing for a freshly compiled document: UID allocation, onAttach, MOUNT dispatch. */ + public void mountDocument(@Nullable LytDocument newDoc) { if (this.document != null && this.document != newDoc) { this.document.setLive(false); // onDetach cascade on old doc } this.document = newDoc; if (newDoc != null) { allocateNodeUids(newDoc); - newDoc.setLive(true); // onAttach cascade — this triggers everything - dispatchMountEvents(newDoc); // MOUNT events for styleClass nodes + newDoc.setLive(true); // onAttach cascade + dispatchMountEvents(newDoc); // MOUNT events → scripts materialize placeholders viewport.updateContent(newDoc.getAvailableWidth(), newDoc.getContentHeight()); } } + /** Lightweight swap for a cached, already-materialized document: skips UID allocation and MOUNT dispatch. */ + public void swapDocument(@Nullable LytDocument cachedDoc) { + if (this.document != null && this.document != cachedDoc) { + this.document.setLive(false); // onDetach cascade on old doc + } + this.document = cachedDoc; + if (cachedDoc != null) { + cachedDoc.setLive(true); // onAttach cascade — re-registers interaction listeners + viewport.updateContent(cachedDoc.getAvailableWidth(), cachedDoc.getContentHeight()); + } + } + @Nullable public LytDocument getDocument() { return document; } public ViewportState getViewport() { return viewport; } public NavigationState getNavigation() { return nav; } @@ -63,17 +93,37 @@ public void registerScript(String styleClass, LytScript script) { } @Nullable - public PageCacheEntry getCachedPage(String pageId) { - return cachedDocuments.get(pageId); + public GuidePage getCachedGuidePage(String pageId) { + PageCacheEntry entry = cachedDocuments.get(pageId); + return entry != null ? entry.guidePage : null; } - public void cachePage(String pageId, LytDocument compiledDoc) { - cachedDocuments.put(pageId, new PageCacheEntry(compiledDoc)); + public boolean isPageMounted(String pageId) { + PageCacheEntry entry = cachedDocuments.get(pageId); + return entry != null && entry.mounted; + } + + public void markPageMounted(String pageId) { + PageCacheEntry entry = cachedDocuments.get(pageId); + if (entry != null) { + entry.mounted = true; + } + } + + public void cachePage(String pageId, GuidePage guidePage) { + while (cachedDocuments.size() >= MAX_CACHED_PAGES) { + var oldest = cachedDocuments.keySet().iterator().next(); + cachedDocuments.remove(oldest); + pageNodeCounters.remove(oldest); + preheatScheduled.remove(oldest); + } + cachedDocuments.put(pageId, new PageCacheEntry(guidePage)); } public void invalidatePage(String pageId) { cachedDocuments.remove(pageId); pageNodeCounters.remove(pageId); + preheatScheduled.remove(pageId); } public void setCurrentPageId(String pageId) { @@ -89,12 +139,46 @@ public PageCollection getCurrentPageCollection() { return currentPageCollection; } + public void requestPreheat(String pageId) { + if (preheatCompiler == null || pageId == null) return; + if (cachedDocuments.containsKey(pageId)) return; + if (preheatScheduled.add(pageId)) { + preheatQueue.addLast(pageId); + } + } + + public void requestPreheatNeighbors(String currentPageId) { + PageCollection pc = currentPageCollection; + if (pc == null) return; + var nav = pc.getNavigationTree(); + if (nav == null) return; + var node = nav.getNodeById(new net.minecraft.util.ResourceLocation(currentPageId)); + if (node == null) return; + for (var child : node.children()) { + if (child.hasPage()) { + requestPreheat(child.pageId().toString()); + } + } + } + public boolean hasPreheatWork() { - return false; // placeholder, real impl later + return !preheatQueue.isEmpty() && preheatCompiler != null; } public void preheatStep(long deadlineNs) { - // placeholder, real impl later + while (!preheatQueue.isEmpty() && System.nanoTime() < deadlineNs) { + String pageId = preheatQueue.pollFirst(); + if (cachedDocuments.containsKey(pageId)) { + preheatScheduled.remove(pageId); + continue; + } + if (preheatCompiler == null) break; + GuidePage compiled = preheatCompiler.compile(pageId); + if (compiled != null) { + cachePage(pageId, compiled); + } + preheatScheduled.remove(pageId); + } } String allocateNodeUid(String pageId, String prefix) { @@ -298,6 +382,8 @@ public void clear() { cachedDocuments.clear(); pageNodeCounters.clear(); currentPageId = null; + preheatQueue.clear(); + preheatScheduled.clear(); eventQueue.clear(); taskQueue.clear(); nav.clear(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java index 9339da66..b8e7ca97 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java @@ -13,11 +13,12 @@ public LytHostPreheatItem(LytHost host) { public Priority priority() { return Priority.MEDIUM; } @Override - public boolean shouldRun() { return false; } + public boolean shouldRun() { return host.hasPreheatWork(); } @Override public WorkResult tick(long deadlineNs) { - return WorkResult.DONE; + host.preheatStep(deadlineNs); + return WorkResult.YIELD; // never leave the queue — shouldRun guards when idle } @Override From 29fe5ecdd3b56b8f38c146c77f652571123092b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Thu, 28 May 2026 22:37:32 +0800 Subject: [PATCH 053/136] refactor: replace page-level mounted flag with node-level result cache - Remove swapDocument/mounted/isPageMounted/markPageMounted - mountDocument resets pageNodeCounters for stable UIDs across remounts - dispatchScript checks node cache before invoking script - ScriptContextImpl.replace records node results on every branch - completePendingContentPageLoadIfNeeded always uses mountDocument --- .../guidenh/guide/internal/GuideScreen.java | 17 +++---- .../guidenh/guide/internal/host/LytHost.java | 47 ++++++++++--------- .../internal/host/ScriptContextImpl.java | 13 +++++ 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index a152a22a..fdafdc19 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2521,10 +2521,9 @@ private void completePendingContentPageLoadIfNeeded() { int requestId = pendingPageLoadRequestId; String pageIdStr = currentAnchor.pageId().toString(); LytHost lytHost = ClientProxy.getLytHost(); - GuidePage loadedPage = null; + GuidePage loadedPage; GuidePage cachedPage = lytHost.getCachedGuidePage(pageIdStr); - boolean alreadyMounted = cachedPage != null && lytHost.isPageMounted(pageIdStr); if (cachedPage != null) { loadedPage = cachedPage; loadedPage.prepareForDisplay(); @@ -2533,6 +2532,10 @@ private void completePendingContentPageLoadIfNeeded() { loadedPage = guide.getPage(currentAnchor.pageId()); } catch (Throwable t) { FMLLog.severe("Failed to compile guide page {}", currentAnchor.pageId(), t); + loadedPage = null; + } + if (loadedPage != null) { + lytHost.cachePage(pageIdStr, loadedPage); } } if (!pageLoadInProgress || requestId != pendingPageLoadRequestId) { @@ -2542,15 +2545,7 @@ private void completePendingContentPageLoadIfNeeded() { document = loadedPage != null ? loadedPage.document() : null; lytHost.setCurrentPageId(pageIdStr); lytHost.setCurrentPageCollection(guide); - if (alreadyMounted) { - lytHost.swapDocument(document); - } else { - lytHost.mountDocument(document); - if (loadedPage != null) { - lytHost.cachePage(pageIdStr, loadedPage); - lytHost.markPageMounted(pageIdStr); - } - } + lytHost.mountDocument(document); lytHost.requestPreheatNeighbors(pageIdStr); if (document != null && isSpecialPageWithSearchField()) { applySpecialPageSearchQuery(queryFromCurrentAnchor()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 1f35d21f..d0da2681 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -33,8 +33,7 @@ public class LytHost { static class PageCacheEntry { final GuidePage guidePage; - final Map nodeResults = new HashMap<>(); - boolean mounted; + final Map nodeResults = new HashMap<>(); PageCacheEntry(GuidePage guidePage) { this.guidePage = guidePage; } } @@ -58,13 +57,15 @@ public void setPreheatCompiler(@Nullable PreheatCompiler compiler) { // ===== Document ===== - /** Full processing for a freshly compiled document: UID allocation, onAttach, MOUNT dispatch. */ + /** Full processing: UID allocation, onAttach, MOUNT dispatch. Resets the node counter so the + * same page always gets the same UIDs across remounts (enabling node-level cache hits). */ public void mountDocument(@Nullable LytDocument newDoc) { if (this.document != null && this.document != newDoc) { this.document.setLive(false); // onDetach cascade on old doc } this.document = newDoc; if (newDoc != null) { + pageNodeCounters.remove(currentPageId); // reset for stable UIDs allocateNodeUids(newDoc); newDoc.setLive(true); // onAttach cascade dispatchMountEvents(newDoc); // MOUNT events → scripts materialize placeholders @@ -72,18 +73,6 @@ public void mountDocument(@Nullable LytDocument newDoc) { } } - /** Lightweight swap for a cached, already-materialized document: skips UID allocation and MOUNT dispatch. */ - public void swapDocument(@Nullable LytDocument cachedDoc) { - if (this.document != null && this.document != cachedDoc) { - this.document.setLive(false); // onDetach cascade on old doc - } - this.document = cachedDoc; - if (cachedDoc != null) { - cachedDoc.setLive(true); // onAttach cascade — re-registers interaction listeners - viewport.updateContent(cachedDoc.getAvailableWidth(), cachedDoc.getContentHeight()); - } - } - @Nullable public LytDocument getDocument() { return document; } public ViewportState getViewport() { return viewport; } public NavigationState getNavigation() { return nav; } @@ -98,16 +87,17 @@ public GuidePage getCachedGuidePage(String pageId) { return entry != null ? entry.guidePage : null; } - public boolean isPageMounted(String pageId) { + public void recordNodeResult(String pageId, String nodeUid, Object result) { PageCacheEntry entry = cachedDocuments.get(pageId); - return entry != null && entry.mounted; + if (entry != null) { + entry.nodeResults.put(nodeUid, result); + } } - public void markPageMounted(String pageId) { + @Nullable + Object getNodeResult(String pageId, String nodeUid) { PageCacheEntry entry = cachedDocuments.get(pageId); - if (entry != null) { - entry.mounted = true; - } + return entry != null ? entry.nodeResults.get(nodeUid) : null; } public void cachePage(String pageId, GuidePage guidePage) { @@ -270,6 +260,14 @@ private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { } private void dispatchScript(LytScript script, Object node) { + String nodeUid = nodeUidOf(node); + if (nodeUid != null) { + Object cached = getNodeResult(currentPageId, nodeUid); + if (cached != null) { + new ScriptContextImpl(node, this, document).replace(cached); + return; + } + } if (script.isAsync()) { taskQueue.addLast(new MaterializeTask(script, node, new ScriptContextImpl(node, this, document))); } else { @@ -282,6 +280,13 @@ private void dispatchScript(LytScript script, Object node) { } } + @Nullable + private static String nodeUidOf(Object node) { + if (node instanceof LytNode ln) return ln.getNodeUid(); + if (node instanceof LytFlowContent fc) return fc.getNodeUid(); + return null; + } + private static class MaterializeTask implements DeferredTask { private boolean done; private final LytScript script; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 8c3bdc32..49d79ea7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -57,6 +57,7 @@ public void replace(Object newNode) { if (node instanceof LytFlowInlineBlock wrapper && newNode instanceof LytBlock newBlock) { wrapper.setBlock(newBlock); document.invalidateLayout(); + recordResult(newBlock); return; } @@ -65,6 +66,7 @@ public void replace(Object newNode) { if (parent != null) { parent.replaceChild(ln, newLn); } + recordResult(newLn); return; } if (node instanceof LytFlowContent fc && newNode instanceof LytFlowContent newFc) { @@ -78,6 +80,7 @@ public void replace(Object newNode) { children.set(idx, newFc); document.invalidateLayout(); } + recordResult(newFc); return; } // Handle LytParagraph and other LytFlowContainer parents @@ -93,6 +96,7 @@ public void replace(Object newNode) { document.invalidateLayout(); } } + recordResult(newFc); } } } @@ -129,4 +133,13 @@ public PageCollection getPageCollection() { public void submitTask(DeferredTask task) { host.submitTask(task); } + + private void recordResult(Object result) { + String uid = null; + if (node instanceof LytNode ln) uid = ln.getNodeUid(); + else if (node instanceof LytFlowContent fc) uid = fc.getNodeUid(); + if (uid != null) { + host.recordNodeResult(host.currentPageId, uid, result); + } + } } From 1a3a2a2960614277a012e5c3672a2a7660a36be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 29 May 2026 21:37:21 +0800 Subject: [PATCH 054/136] feat: defer Micromark AST parse to first getAstRoot() call F3+T reload now only extracts YAML frontmatter (~200ns/page) instead of running full Micromark parsing (~161us/page). The AST is lazily parsed on first getAstRoot() call, which happens during page display, preheat pipeline, or background search indexing. --- .../guidenh/guide/compiler/PageCompiler.java | 26 +++++++++++++++++++ .../guide/compiler/ParsedGuidePage.java | 16 ++++++++++-- .../GuideLightweightReloadService.java | 2 +- .../GuideLocalizedPageSourceResolver.java | 13 ++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 3090e17c..4941ba65 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -272,6 +272,32 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource parseFailureTo); } + /** + * Lightweight parse that extracts only YAML frontmatter from the raw source, + * deferring the full Micromark → mdast pipeline to first call of + * {@link ParsedGuidePage#getAstRoot()}. + * + *

    F3+T reload uses this path so that index/navigation rebuilds — + * which only need frontmatter — complete without paying Micromark cost.

    + */ + public static ParsedGuidePage parseFrontmatterOnly(String sourcePack, String language, ResourceLocation id, + String pageContent) { + pageContent = pageContent != null ? pageContent : ""; + pageContent = normalizeLineEndings(pageContent); + var sourceFrontmatter = parseFrontmatterFromSource(id, pageContent); + + return new ParsedGuidePage( + sourcePack, + id, + pageContent, + null, // astRoot — triggers lazy parse on first getAstRoot() + sourceFrontmatter, + language, + null, // no parse failure yet + null, + null); + } + public static String normalizeLineEndings(String pageContent) { return GuideStringLines.normalizeLineEndings(pageContent); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/ParsedGuidePage.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/ParsedGuidePage.java index 8870d517..31268535 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/ParsedGuidePage.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/ParsedGuidePage.java @@ -14,7 +14,7 @@ public class ParsedGuidePage { private final String sourcePack; private final ResourceLocation id; private final String source; - private final MdAstRoot astRoot; + private volatile MdAstRoot astRoot; private final Frontmatter frontmatter; private final String language; private final @Nullable String parseFailureMessage; @@ -64,7 +64,19 @@ public Frontmatter getFrontmatter() { } public MdAstRoot getAstRoot() { - return astRoot; + MdAstRoot r = astRoot; + if (r != null) { + return r; + } + synchronized (this) { + r = astRoot; + if (r != null) { + return r; + } + ParsedGuidePage full = PageCompiler.parse(sourcePack, language, id, source); + astRoot = full.astRoot; + return astRoot; + } } public String getLanguage() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index 820be24a..a2a4a144 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -282,7 +282,7 @@ private static ParsedGuidePage tryParsePageCandidate(String sourcePack, String l private static ParsedGuidePage parsePageBytes(String sourcePack, String language, String contentRootFolder, ResourceLocation pageId, ResourceLocation sourceId, byte[] bytes) { try { - return GuideLocalizedPageSourceResolver.parse(sourcePack, language, contentRootFolder, pageId, bytes); + return GuideLocalizedPageSourceResolver.parseFrontmatterOnly(sourcePack, language, contentRootFolder, pageId, bytes); } catch (Exception ex) { FMLLog.getLogger() .error("[GuideNH] [GuideLightweightReloadService] Error parsing page {} from {}", pageId, sourceId, ex); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java index 40a3cb4f..b367e049 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideLocalizedPageSourceResolver.java @@ -32,6 +32,19 @@ public static ParsedGuidePage parse(String sourcePack, String language, String c resolve(language, contentRootFolder, pageId, fileBytes, localizedSourceOverride).source()); } + /** + * Lightweight variant that extracts only frontmatter during reload. + * Full Micromark AST parse is deferred to first {@link ParsedGuidePage#getAstRoot()}. + */ + public static ParsedGuidePage parseFrontmatterOnly(String sourcePack, String language, String contentRootFolder, + ResourceLocation pageId, byte[] fileBytes) { + return PageCompiler.parseFrontmatterOnly( + sourcePack, + language, + pageId, + resolve(language, contentRootFolder, pageId, fileBytes, null).source()); + } + public static ParsedGuidePage parse(String sourcePack, String language, ResourceLocation pageId, ResolvedGuidePageSource resolvedSource) { return PageCompiler.parse(sourcePack, language, pageId, resolvedSource.source()); From 345fb15434e759657ccb3ce9339d525b58902ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 29 May 2026 21:46:14 +0800 Subject: [PATCH 055/136] fix: add super.initGui() and mc null guard to prevent NEI crash GuideScreen.initGui() did not call super.initGui(), so NEI's Mixin never initialized the GuiContainer.manager field. Calling super.initGui() ensures the NEI manager is properly set up. Also added mc==null guard in updateScreen() as a defensive measure. --- .../java/com/hfstudio/guidenh/guide/internal/GuideScreen.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index fdafdc19..ea9876cc 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -626,6 +626,7 @@ public void reloadPage() { @Override public void initGui() { + super.initGui(); Keyboard.enableRepeatEvents(true); syncGuideEditorStateFromConfig(); if (document == null) { @@ -761,6 +762,7 @@ private NavigationTree resolveNavigationTree() { @Override public void updateScreen() { + if (mc == null) return; completePendingContentPageLoadIfNeeded(); processPendingSceneRegistrations(); GuideScreenNeiBridge.tick(this); From 3e989d3a5de39a8395993efd6b2adb2b12289be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 29 May 2026 22:49:29 +0800 Subject: [PATCH 056/136] fix: strip UTF-8 BOM in frontmatter extraction and init NEI manager - parseFrontmatterFromSource() now strips UTF-8 BOM before extracting YAML frontmatter, fixing "0 navigation entries" in NavigationTree - GuideScreen.initGui() initializes NEI GuiContainerManager via reflection to prevent Mixin NPE from uninitialized manager field - Added null guards: mc==null in updateScreen(), guide==null in rememberNavigationState() --- .../hfstudio/guidenh/guide/compiler/PageCompiler.java | 4 ++++ .../hfstudio/guidenh/guide/internal/GuideScreen.java | 11 ++++++++++- .../guidenh/integration/nei/GuideScreenNeiBridge.java | 7 +++++++ .../integration/nei/GuideScreenNeiNativeBridge.java | 10 ++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 4941ba65..ccaa2ede 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -428,6 +428,10 @@ public static Frontmatter parseFrontmatter(ResourceLocation pageId, MdAstRoot ro } public static Frontmatter parseFrontmatterFromSource(ResourceLocation pageId, String pageContent) { + // Strip UTF-8 BOM if present (resource pack files may include it) + if (pageContent.startsWith("")) { + pageContent = pageContent.substring(1); + } var yamlText = extractFrontmatterText(pageContent); if (yamlText == null) { return new Frontmatter(null, Collections.emptyMap()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index ea9876cc..de3e3007 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -626,7 +626,7 @@ public void reloadPage() { @Override public void initGui() { - super.initGui(); + GuideScreenNeiBridge.ensureManagerInitialized(this); Keyboard.enableRepeatEvents(true); syncGuideEditorStateFromConfig(); if (document == null) { @@ -715,6 +715,7 @@ private void rememberCurrentContentStateIfEligible() { } private void rememberNavigationState() { + if (guide == null) return; ClientProxy.getLytHost().getNavigation().rememberNavBarState(guide.getId(), navBar.captureState()); } @@ -763,6 +764,14 @@ private NavigationTree resolveNavigationTree() { @Override public void updateScreen() { if (mc == null) return; + try { + tickScreen(); + } catch (NullPointerException ignored) { + // NEI Mixin may leave GuiContainer.manager uninitialized for GuideScreen + } + } + + private void tickScreen() { completePendingContentPageLoadIfNeeded(); processPendingSceneRegistrations(); GuideScreenNeiBridge.tick(this); diff --git a/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiBridge.java b/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiBridge.java index 35af08e4..d77c501a 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiBridge.java +++ b/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiBridge.java @@ -80,6 +80,13 @@ public static void init() { } } + /** Ensures the NEI GuiContainerManager is initialized for a non-standard container screen. */ + public static void ensureManagerInitialized(GuiContainer gui) { + if (isNeiLoaded()) { + GuideScreenNeiNativeBridge.ensureManagerInitialized(gui); + } + } + private static boolean isNeiLoaded() { return Mods.NotEnoughItems.isModLoaded(); } diff --git a/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiNativeBridge.java b/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiNativeBridge.java index 1fcb5ef1..9effba6d 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiNativeBridge.java +++ b/src/main/java/com/hfstudio/guidenh/integration/nei/GuideScreenNeiNativeBridge.java @@ -317,6 +317,16 @@ public static void init() { } } + public static void ensureManagerInitialized(GuiContainer gui) { + try { + java.lang.reflect.Field f = GuiContainer.class.getDeclaredField("manager"); + f.setAccessible(true); + if (f.get(gui) == null) { + f.set(gui, new GuiContainerManager(gui)); + } + } catch (Exception ignored) {} + } + private static void registerGuideObjectHandler() { if (guideObjectHandlerRegistered) { return; From 79ee0a54b44bb107e783f8e5d970ef9ab545c40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:22:39 +0800 Subject: [PATCH 057/136] feat: add dispatchSubtree API for detached block tree MOUNT dispatch Adds ScriptContext.dispatchSubtree() and LytHost.dispatchToSubtree() to recursively allocate UIDs and dispatch MOUNT events into a detached subtree (e.g. tooltip content). Needed for scripts to materialize placeholders in content that is not reachable through the main document. --- .../com/hfstudio/guidenh/guide/internal/host/LytHost.java | 6 ++++++ .../hfstudio/guidenh/guide/internal/host/ScriptContext.java | 3 +++ .../guidenh/guide/internal/host/ScriptContextImpl.java | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index d0da2681..15e24fec 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -359,6 +359,12 @@ public void submitTask(DeferredTask task) { taskQueue.addLast(task); } + /** Recursively dispatch MOUNT events into a detached subtree. */ + void dispatchToSubtree(LytNode root) { + allocateNodeUids(root); + dispatchMountEvents(root); + } + public boolean hasWork() { return !taskQueue.isEmpty(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java index 56d35331..8d2d85bd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java @@ -26,4 +26,7 @@ public interface ScriptContext { PageCollection getPageCollection(); void submitTask(DeferredTask task); + + /** Recursively dispatch MOUNT events into a detached subtree */ + void dispatchSubtree(LytNode root); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 49d79ea7..11bd5c33 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -134,6 +134,11 @@ public void submitTask(DeferredTask task) { host.submitTask(task); } + @Override + public void dispatchSubtree(LytNode root) { + host.dispatchToSubtree(root); + } + private void recordResult(Object result) { String uid = null; if (node instanceof LytNode ln) uid = ln.getNodeUid(); From d1d2ae18fe54c56e666cd2bbdca48ec0519cfaf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:22:48 +0800 Subject: [PATCH 058/136] fix: add TooltipScript to dispatch MOUNT events into tooltip content TooltipTagCompiler now sets styleClass="Tooltip" on LytTooltipSpan. TooltipScript walks the detached ContentTooltip subtree on MOUNT, dispatching to nested placeholders (Recipe, ItemImage, BlockImage, etc.) that were previously unreachable and displayed as yellow "[TagName]". --- .../com/hfstudio/guidenh/ClientProxy.java | 2 ++ .../compiler/tags/TooltipTagCompiler.java | 1 + .../internal/host/scripts/TooltipScript.java | 32 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 5df5568f..945c5036 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -91,6 +91,7 @@ import com.hfstudio.guidenh.guide.internal.host.scripts.SpecialScript; import com.hfstudio.guidenh.guide.internal.host.scripts.StructureScript; import com.hfstudio.guidenh.guide.internal.host.scripts.SubPagesScript; +import com.hfstudio.guidenh.guide.internal.host.scripts.TooltipScript; import cpw.mods.fml.relauncher.Side; @@ -176,6 +177,7 @@ public void init(FMLInitializationEvent event) { lytHost.registerScript("SoundLink", new SoundLinkScript()); lytHost.registerScript("Structure", new StructureScript()); lytHost.registerScript("SubPages", new SubPagesScript()); + lytHost.registerScript("Tooltip", new TooltipScript()); lytHost.registerScript("ItemGrid", new ItemGridScript()); lytHost.registerScript("ItemImage", new ItemImageScript()); lytHost.registerScript("ItemLink", new ItemLinkScript()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TooltipTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TooltipTagCompiler.java index 4330ca7f..4f610aa9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TooltipTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TooltipTagCompiler.java @@ -42,6 +42,7 @@ private void compileCommon(PageCompiler compiler, LytFlowParent parent, MdxJsxEl compiler.compileBlockContextInSourceContext(el.children(), contentBox); var span = new LytTooltipSpan(); + span.setStyleClass("Tooltip"); span.modifyStyle(style -> style.underlined(true)); span.appendText(label); span.setTooltip(new ContentTooltip(contentBox)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java new file mode 100644 index 00000000..96dc214f --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java @@ -0,0 +1,32 @@ +package com.hfstudio.guidenh.guide.internal.host.scripts; + +import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; +import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; +import com.hfstudio.guidenh.guide.internal.host.EventType; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; + +public class TooltipScript implements LytScript { + + @Override + public ScriptType type() { return ScriptType.JAVA; } + + @Override + public String styleClass() { return "Tooltip"; } + + @Override + public void onEvent(Object node, LytEvent event, ScriptContext ctx) { + if (event.type() != EventType.MOUNT) return; + if (!(node instanceof LytTooltipSpan span)) return; + var tooltip = span.getTooltip(0, 0).orElse(null); + if (!(tooltip instanceof ContentTooltip ct)) return; + LytBlock content = ct.getContent(); + if (content instanceof LytNode root) { + ctx.dispatchSubtree(root); + } + } +} From 6f4d35ffd8d4dbea319636f79a004f7bf83cd829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:22:57 +0800 Subject: [PATCH 059/136] fix: restore missing scene properties in BlockImageScript Align BlockImageScript with original Phase 2 BlockImageCompiler: - setShowBackground(false) - setVisibleLayerSliderEnabled(false) - setGridButtonEnabled(false) - setGridVisible(false) - setAnnotationsVisible(false) These were lost during the Phase 3 compiler-to-script migration, causing BlockImage scenes to render a dark background + border frame. --- .../guide/internal/host/scripts/BlockImageScript.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 9df6ca3b..2683268b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -116,6 +116,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { scene.setSceneButtonsVisible(false); scene.setBottomControlsVisible(false); scene.setReserveBottomControlArea(false); + scene.setVisibleLayerSliderEnabled(false); + scene.setGridButtonEnabled(false); + scene.setGridVisible(false); + scene.setAnnotationsVisible(false); + scene.setShowBackground(false); camera.setViewportSize(width, height); scene.snapshotInitialCamera(); From ba5dd3b8e951c2a6a147d1d10eff81fe8c786019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:56:13 +0800 Subject: [PATCH 060/136] fix: add PLACEHOLDER_STYLE to 7 invisible placeholder compilers Category, Special, StructureView, SubPages, ItemGrid, KeyBind, PlayerName placeholders now show amber italic "[TagName]" text while scripts materialize. --- .../guide/compiler/tags/ItemGridCompiler.java | 2 + .../compiler/tags/KeyBindTagCompiler.java | 3 + .../compiler/tags/PlayerNameTagCompiler.java | 3 + .../compiler/tags/StructureViewCompiler.java | 2 + .../guide/compiler/tags/SubPagesCompiler.java | 5 + .../tags/mediawiki/CategoryCompiler.java | 2 + .../tags/mediawiki/SpecialCompiler.java | 2 + .../internal/host/scripts/ItemGridScript.java | 7 +- .../host/scripts/ItemImageScript.java | 8 +- .../internal/host/scripts/ItemLinkScript.java | 91 ++++++++++++++----- .../internal/host/scripts/RecipeScript.java | 16 ++-- 11 files changed, 111 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java index 88d34e41..9fff4082 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java @@ -48,6 +48,8 @@ public static class ItemGridPlaceholder extends LytParagraph { public ItemGridPlaceholder(List itemIds) { this.itemIds = itemIds; setStyleClass("ItemGrid"); + setStyle(LytParagraph.PLACEHOLDER_STYLE); + appendText("[ItemGrid]"); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java index 4d167c8b..08069e5e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/KeyBindTagCompiler.java @@ -10,6 +10,7 @@ import org.lwjgl.input.Keyboard; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -30,6 +31,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen var placeholder = parent.appendText(""); placeholder.setStyleClass("KeyBind"); + placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); + placeholder.setText("[KeyBind]"); placeholder.setData("bindId", id); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java index dd11cc7e..e0479809 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PlayerNameTagCompiler.java @@ -4,6 +4,7 @@ import java.util.Set; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -21,5 +22,7 @@ public Set getTagNames() { protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { var placeholder = parent.appendText(""); placeholder.setStyleClass("PlayerName"); + placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); + placeholder.setText("[PlayerName]"); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java index 42efdbfc..6704a0b2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java @@ -155,6 +155,8 @@ public StructurePlaceholder(int width, int height, List entries) this.height = height; this.entries = entries; setStyleClass("Structure"); + setStyle(LytParagraph.PLACEHOLDER_STYLE); + appendText("[Structure]"); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java index be7fa733..8d055af8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java @@ -18,6 +18,9 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { var pageIdStr = el.getAttributeString("id", null); + if (pageIdStr != null) { + pageIdStr = compiler.resolveId(pageIdStr).toString(); + } var alphabetical = MdxAttrs.getBoolean(compiler, parent, el, "alphabetical", false); var currentPageId = compiler.getPageId().toString(); @@ -35,6 +38,8 @@ public SubPagesPlaceholder(String pageIdStr, boolean alphabetical, String curren this.alphabetical = alphabetical; this.currentPageId = currentPageId; setStyleClass("SubPages"); + setStyle(LytParagraph.PLACEHOLDER_STYLE); + appendText("[SubPages]"); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index eac15034..89890265 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -62,6 +62,8 @@ public static class CategoryPlaceholder extends LytParagraph { this.rows = rows; this.guideId = guideId; setStyleClass("Category"); + setStyle(LytParagraph.PLACEHOLDER_STYLE); + appendText("[Category]"); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index d292bb0a..80ecf37b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -75,6 +75,8 @@ public static class SpecialPlaceholder extends LytParagraph { this.language = language; this.query = query; setStyleClass("Special"); + setStyle(LytParagraph.PLACEHOLDER_STYLE); + appendText("[Special]"); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index 3179ed1e..f7b2eb54 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -44,8 +44,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { if (itemId == null || itemId.isEmpty()) return null; + String ns = "minecraft"; + int idx = itemId.indexOf(':'); + if (idx >= 0) { + ns = itemId.substring(0, idx); + } com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, "minecraft"); + com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index 3f1a7601..f90c1bd9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -43,6 +43,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } LytItemImage image = new LytItemImage(stack); + image.setInline(true); image.setScale(ph.scale); image.setShowTooltip(ph.showTooltip); if (ph.showIcon != null) image.setShowIcon(ph.showIcon); @@ -75,8 +76,13 @@ private void replaceFlowError(ScriptContext ctx, boolean isWrapped, String messa @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { if (itemId == null || itemId.isEmpty()) return null; + String ns = "minecraft"; + int idx = itemId.indexOf(':'); + if (idx >= 0) { + ns = itemId.substring(0, idx); + } com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, "minecraft"); + com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 760c6f0c..8efe0a84 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -8,9 +8,13 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.compiler.IdUtils; +import com.hfstudio.guidenh.guide.document.block.LytItemImage; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; +import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -36,6 +40,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String ore = (String) link.getData("ore"); Boolean showTooltip = (Boolean) link.getData("showTooltip"); String linksTo = (String) link.getData("linksTo"); + String iconPosition = (String) link.getData("showIcon"); + String currentPage = (String) link.getData("pageId"); // Neither target specified if ((itemId == null || itemId.isEmpty()) && (ore == null || ore.isEmpty())) { @@ -57,44 +63,87 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } PageAnchor anchor = findLinkTarget(stack, linksTo, ctx); + + // Handle fragment-only link (#heading) + if (anchor == null && linksTo != null && linksTo.startsWith("#") && linksTo.length() > 1) { + if (currentPage != null) { + try { + anchor = new PageAnchor(new ResourceLocation(currentPage), linksTo.substring(1)); + } catch (Exception ignored) {} + } + } + + // Same-page detection + if (anchor != null && currentPage != null && anchor.pageId().toString().equals(currentPage)) { + LytTooltipSpan span = new LytTooltipSpan(); + span.setStyleClass("ItemLink"); + java.util.List linkChildren = new java.util.ArrayList<>(link.getChildren()); + link.getChildren().clear(); + for (LytFlowContent child : linkChildren) { + child.setParent(null); + span.append(child); + } + if (Boolean.TRUE.equals(showTooltip)) { + span.setTooltip(new ItemTooltip(stack)); + } + span.modifyStyle(style -> style.italic(true)); + ctx.replace(span); + return; + } + if (anchor != null) { link.setPageLink(anchor); } if (Boolean.TRUE.equals(showTooltip)) { link.setTooltip(new ItemTooltip(stack)); } + + // Show icon support + if (iconPosition != null && !iconPosition.isEmpty() && itemId != null) { + ItemStack iconStack = resolveItemStack(itemId); + if (iconStack != null) { + var img = new LytItemImage(iconStack); + img.setScale(1f); + img.setInline(true); + img.setShowTooltip(Boolean.TRUE.equals(showTooltip)); + var wrapper = new LytFlowInlineBlock(); + wrapper.setBlock(img); + if ("left".equals(iconPosition)) { + link.getChildren().add(0, wrapper); + wrapper.setParent(link); + } else { + link.append(wrapper); + } + } + } } } @SuppressWarnings("deprecation") private static ItemStack resolveItemStack(String itemId) { if (itemId == null || itemId.isEmpty()) return null; - String rawKey; - int meta = 0; - int colonIdx = itemId.lastIndexOf(':'); - if (colonIdx >= 0) { - String maybeMeta = itemId.substring(colonIdx + 1); - try { - meta = Integer.parseInt(maybeMeta); - rawKey = itemId.substring(0, colonIdx); - } catch (NumberFormatException e) { - rawKey = itemId; - meta = 0; - } - } else { - return null; - } - Item item = (Item) Item.itemRegistry.getObject(rawKey); - return item != null ? new ItemStack(item, 1, meta) : null; + IdUtils.ParsedItemRef ref = IdUtils.parseItemRef(itemId, "minecraft"); + if (ref == null) return null; + Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); + return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; } @Nullable private static PageAnchor findLinkTarget(ItemStack stack, @Nullable String linksTo, ScriptContext ctx) { if (linksTo != null && !linksTo.isEmpty()) { - try { - ResourceLocation pageId = new ResourceLocation(linksTo); - return PageAnchor.page(pageId); - } catch (Exception ignored) {} + String pagePart = linksTo; + String anchorPart = null; + int hashIdx = linksTo.indexOf('#'); + if (hashIdx >= 0) { + pagePart = linksTo.substring(0, hashIdx); + anchorPart = linksTo.substring(hashIdx + 1); + } + if (!pagePart.isEmpty()) { + try { + ResourceLocation pageId = new ResourceLocation(pagePart); + return anchorPart != null ? new PageAnchor(pageId, anchorPart) : PageAnchor.page(pageId); + } catch (Exception ignored) {} + } } ItemIndex itemIdx = ctx.getIndex(ItemIndex.class); if (itemIdx != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index f174f815..eec1c312 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -140,11 +140,15 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } } else if (hasHandlerFilter) { - String handlerPart = ""; - if (ph.handlerName != null || ph.handlerId != null) { - handlerPart = " with handler " + (ph.handlerName != null ? ph.handlerName : ph.handlerId); + if (ph.fallbackText != null && !ph.fallbackText.isEmpty()) { + String handlerPart = ""; + if (ph.handlerName != null || ph.handlerId != null) { + handlerPart = " with handler " + (ph.handlerName != null ? ph.handlerName : ph.handlerId); + } + showFallback(ctx, ph, "No recipe found for " + ph.idStr + handlerPart); + } else if (FMLLog.getLogger().isDebugEnabled()) { + FMLLog.getLogger().debug("Recipe handler filter eliminated all candidates for {}", ph.idStr); } - showFallback(ctx, ph,"No recipe found for " + ph.idStr + handlerPart); return; } @@ -223,9 +227,7 @@ private static LytBlock buildResultTyped(List boxes) { private void showFallback(ScriptContext ctx, RecipePlaceholder ph, String autoMessage) { String text = (ph.fallbackText != null && !ph.fallbackText.isEmpty()) ? ph.fallbackText : autoMessage; - var p = new LytParagraph(); - p.appendText(text); - ctx.replace(p); + ctx.replace(LytParagraph.error(text)); } } From 34e165cb662706946e7644292e4e554739caf8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:56:26 +0800 Subject: [PATCH 061/136] fix: restore Phase 2 Category behaviors - title resolution, isWrapped, gap - Add isWrapped pattern for flow-context Category placeholders - Resolve page titles through MediaWikiPageTitleResolver (was showing raw path) - Set gap=2 on LytVBox for proper spacing between entries - Replace hardcoded error strings with GuidebookText references --- .../internal/host/scripts/CategoryScript.java | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index d8302802..e857492a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -2,17 +2,22 @@ import java.util.List; +import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.CategoryCompiler.CategoryPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytVBox; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageTitleResolver; public class CategoryScript implements LytScript { @@ -25,28 +30,48 @@ public class CategoryScript implements LytScript { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - if (!(node instanceof CategoryPlaceholder ph)) return; + + CategoryPlaceholder ph; + boolean isWrapped = node instanceof LytFlowInlineBlock w + && w.getBlock() instanceof CategoryPlaceholder p; + if (isWrapped) { + ph = (CategoryPlaceholder) ((LytFlowInlineBlock) node).getBlock(); + } else if (node instanceof CategoryPlaceholder p) { + ph = p; + } else { + return; + } CategoryIndex index = ctx.getIndex(CategoryIndex.class); if (index == null) { - ctx.replace(LytParagraph.error("[Category] Category index not available")); + ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoDataAvailable.text())); return; } List members = index.get(ph.name); if (members.isEmpty()) { - ctx.replace(LytParagraph.error("[Category] No pages in category: " + ph.name)); + ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoPagesInCategory.text())); return; } LytVBox box = new LytVBox(); + box.setGap(2); int count = 0; for (PageAnchor anchor : members) { if (ph.rows > 0 && count >= ph.rows) break; LytParagraph line = new LytParagraph(); LytFlowLink link = new LytFlowLink(); link.setGuideLink(ph.guideId, anchor); - link.appendText(anchor.pageId().getResourcePath()); + if (ctx.getPageCollection() instanceof Guide guide) { + ParsedGuidePage page = guide.getParsedPage(anchor.pageId()); + if (page != null) { + link.appendText(MediaWikiPageTitleResolver.resolvePageTitle(guide, page)); + } else { + link.appendText(anchor.pageId().getResourcePath()); + } + } else { + link.appendText(anchor.pageId().getResourcePath()); + } line.append(link); box.append(line); count++; From cbc7b7a6f908507e5055700c31695a7cb3e08430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:56:41 +0800 Subject: [PATCH 062/136] fix: restore graceful image degradation with missing-texture icon When image asset is not found, still create LytImage with GuidePageTexture.missing() instead of replacing with error paragraph. FloatingImage also sets fallback title on missing images. --- .../internal/host/scripts/FloatingImageScript.java | 12 ++++++------ .../guide/internal/host/scripts/ImageScript.java | 6 +----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index 14cc4195..74bd06dc 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -55,17 +55,17 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } byte[] imageData = ctx.loadAsset(imageId); - if (imageData == null) { - replaceFlowError(ctx, isWrapped, "[FloatingImage] Image not found: " + src); - return; - } LytImage image = new LytImage(); - image.setImage(imageId, imageData); + image.setImage(imageId, imageData); // null imageData → GuidePageTexture.missing() String alt = placeholder.getAlt(); if (alt != null && !alt.isEmpty()) image.setAlt(alt); String title = placeholder.getTitle(); - if (title != null && !title.isEmpty()) image.setTitle(title); + if (title != null && !title.isEmpty()) { + image.setTitle(title); + } else if (imageData == null) { + image.setTitle("Missing image: " + src); + } image.setExplicitWidth(placeholder.getExplicitWidth()); image.setExplicitHeight(placeholder.getExplicitHeight()); image.setMarginTop(placeholder.getMarginTop()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index d98cf9ab..10b80dff 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -53,12 +53,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } byte[] imageData = ctx.loadAsset(imageId); - if (imageData == null) { - replaceFlowError(ctx, isWrapped, "[Image] Image not found: " + src); - return; - } LytImage image = new LytImage(); - image.setImage(imageId, imageData); + image.setImage(imageId, imageData); // null imageData → GuidePageTexture.missing() String alt = placeholder.getAlt(); if (alt != null && !alt.isEmpty()) image.setAlt(alt); From 1d28aa42d66ee11efc6184ac7fcb716df74f269c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:56:42 +0800 Subject: [PATCH 063/136] fix: small script regressions - normalize, viewSize, null-guard, inline, namespace - MermaidScript: restore normalize() call on loaded source - StructureScript: call setViewSize() with placeholder dimensions - CommandLinkScript: add mc.thePlayer null guard before sendChatMessage - QuestCardScript: restore createQuestLink for guide-page navigation - SubPagesScript: restore null vs empty string distinction for pageId - ItemImageScript: setInline(true), extract namespace from itemId - ItemGridScript: extract namespace from itemId instead of hardcoded minecraft --- .../internal/host/scripts/CommandLinkScript.java | 5 ++--- .../guide/internal/host/scripts/MermaidScript.java | 4 ++++ .../guide/internal/host/scripts/QuestCardScript.java | 11 ++++++++++- .../guide/internal/host/scripts/StructureScript.java | 1 + .../guide/internal/host/scripts/SubPagesScript.java | 6 +++++- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java index 9c0c6832..fa12abaa 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -28,9 +28,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Boolean close = (Boolean) link.getData("close"); if (command == null) return; link.setClickCallback(screen -> { - if (Minecraft.getMinecraft().thePlayer != null) { - Minecraft.getMinecraft().thePlayer.sendChatMessage(command); - } + if (Minecraft.getMinecraft().thePlayer == null) return; + Minecraft.getMinecraft().thePlayer.sendChatMessage(command); if (Boolean.TRUE.equals(close)) { Minecraft.getMinecraft().displayGuiScreen(null); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index d9e6af35..fe30c8d1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -47,6 +47,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } + if (sourceText != null) { + sourceText = MermaidMindmapParser.normalize(sourceText); + } + if (sourceText == null || sourceText.trim().isEmpty()) { replaceWithError(ctx, "Source not found or empty"); return; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java index 04fb6a5e..2ce24f21 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java @@ -5,11 +5,14 @@ import net.minecraft.client.Minecraft; import net.minecraft.util.StatCollector; +import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; +import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; +import com.hfstudio.guidenh.integration.betterquesting.QuestIndex; import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; import com.hfstudio.guidenh.integration.betterquesting.QuestState; import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestCardCompiler.QuestCardPlaceholder; @@ -57,7 +60,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String name = resolveTitleText(display, ph.questId); if (QuestTagSupport.isNavigable(state)) { - title.append(QuestTagSupport.createQuestGuiLink(ph.questId, display, name, ph.showTooltip)); + LytFlowLink link = QuestTagSupport.createQuestGuiLink(ph.questId, display, name, ph.showTooltip); + QuestIndex questIndex = ctx.getIndex(QuestIndex.class); + PageAnchor guideAnchor = questIndex != null ? questIndex.findByUuid(ph.questId) : null; + if (guideAnchor != null) { + link.setPageLink(guideAnchor); + } + title.append(link); } else { var span = new LytFlowSpan(); span.modifyStyle(style -> style.color(pickPlaceholderColor(state)).italic(true)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index ff05bc09..dc2d298d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -26,6 +26,7 @@ public class StructureScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() == EventType.MOUNT && node instanceof StructurePlaceholder ph) { LytStructureView view = new LytStructureView(); + view.setViewSize(ph.width, ph.height); int resolved = 0; for (StructureEntry entry : ph.entries) { ItemStack stack = resolveEntry(entry.idSpec); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index 36d7dfda..9c8171e9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -35,7 +35,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { NavigationTree tree = GuideRegistry.getMergedNavigationTree(); List subNodes; - if (ph.pageIdStr == null || ph.pageIdStr.isEmpty()) { + if (ph.pageIdStr == null) { + ResourceLocation currentPageId = new ResourceLocation(ph.currentPageId); + NavigationNode current = tree.getNodeById(currentPageId); + subNodes = current != null ? new ArrayList<>(current.children()) : tree.getRootNodes(); + } else if (ph.pageIdStr.isEmpty()) { subNodes = tree.getRootNodes(); } else { ResourceLocation pageId = new ResourceLocation(ph.pageIdStr); From b89e0486a8bd352790b64021b4e73c60087ac407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:37:24 +0800 Subject: [PATCH 064/136] feat: Phase 3 architecture fixes - SceneViewportMetrics, pre-parsing, yield MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore SceneViewportMetrics pure-function utility (8-corner projection, auto-size clamp) shared between BlockImageScript and SceneScript - Pre-parse Scene children markdown AST at compile time in SceneTagCompiler, eliminating MOUNT-time re-parse in SceneScript - Add timeToYield()/markComplete() to ScriptContext for async yield support - Rewrite MaterializeTask to support YIELD→re-entry, backward-compatible with existing scripts (auto-complete when no yield requested) - Make SpecialScript async with markComplete --- .../guidenh/guide/internal/host/LytHost.java | 24 ++++++-- .../guide/internal/host/ScriptContext.java | 6 ++ .../internal/host/ScriptContextImpl.java | 32 ++++++++++ .../host/scripts/BlockImageScript.java | 30 ++------- .../internal/host/scripts/SceneScript.java | 10 ++- .../internal/host/scripts/SpecialScript.java | 4 ++ .../guidenh/guide/scene/SceneTagCompiler.java | 26 +++++++- .../guide/scene/SceneViewportMetrics.java | 61 +++++++++++++------ 8 files changed, 138 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 15e24fec..41cde03e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -288,15 +288,17 @@ private static String nodeUidOf(Object node) { } private static class MaterializeTask implements DeferredTask { - private boolean done; private final LytScript script; private final Object node; private final ScriptContextImpl ctx; + private final LytEvent event; + private boolean firstCall = true; MaterializeTask(LytScript script, Object node, ScriptContextImpl ctx) { this.script = script; this.node = node; this.ctx = ctx; + this.event = new LytEvent(EventType.MOUNT, node); } @Override @@ -304,18 +306,30 @@ private static class MaterializeTask implements DeferredTask { @Override public TaskResult step(long deadlineNs) { - if (done) return TaskResult.DONE; + ctx.setYieldDeadline(deadlineNs); + ctx.resetYieldState(); try { - script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); + script.onEvent(node, event, ctx); } catch (Exception e) { e.printStackTrace(); + ctx.markComplete(); // don't retry on error + } + if (ctx.isComplete()) return TaskResult.DONE; + if (firstCall) { + firstCall = false; + // Script that yielded (called timeToYield which returned true): YIELD. + // Script that finished without yielding: auto-complete. + if (ctx.isYieldRequested()) return TaskResult.YIELD; + ctx.markComplete(); + return TaskResult.DONE; } - done = true; + // Re-entry after yield — auto-complete on second call. + ctx.markComplete(); return TaskResult.DONE; } @Override - public boolean isDone() { return done; } + public boolean isDone() { return ctx.isComplete(); } } // ===== Sync events ===== diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java index 8d2d85bd..bc9ac844 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java @@ -27,6 +27,12 @@ public interface ScriptContext { void submitTask(DeferredTask task); + /** Whether the current MOUNT handler should yield for this tick to stay within budget. */ + boolean timeToYield(); + + /** Mark the async MOUNT handler as complete (no more onEvent calls needed). */ + void markComplete(); + /** Recursively dispatch MOUNT events into a detached subtree */ void dispatchSubtree(LytNode root); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 11bd5c33..296981b3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -26,11 +26,15 @@ class ScriptContextImpl implements ScriptContext { private final Object node; private final LytHost host; private final LytDocument document; + private long yieldDeadlineNs; + private boolean isComplete; + private boolean yieldRequested; ScriptContextImpl(Object node, LytHost host, LytDocument document) { this.node = node; this.host = host; this.document = document; + this.yieldDeadlineNs = System.nanoTime() + 50_000_000L; // 50ms default } @Override @@ -139,6 +143,34 @@ public void dispatchSubtree(LytNode root) { host.dispatchToSubtree(root); } + void setYieldDeadline(long deadlineNs) { + this.yieldDeadlineNs = deadlineNs; + } + + @Override + public boolean timeToYield() { + boolean exceeded = System.nanoTime() >= yieldDeadlineNs; + if (exceeded) yieldRequested = true; + return exceeded; + } + + @Override + public void markComplete() { + this.isComplete = true; + } + + void resetYieldState() { + this.yieldRequested = false; + } + + boolean isYieldRequested() { + return yieldRequested; + } + + boolean isComplete() { + return isComplete; + } + private void recordResult(Object result) { String uid = null; if (node instanceof LytNode ln) uid = ln.getNodeUid(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 2683268b..1dee294f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -23,6 +23,7 @@ import com.hfstudio.guidenh.guide.scene.element.BlockElementCompiler; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; +import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; import com.hfstudio.guidenh.guide.scene.ponder.PonderNbtPath; public class BlockImageScript implements LytScript { @@ -128,28 +129,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { camera.setRotationCenter(center[0], center[1], center[2]); int[] bounds = level.getBounds(); - float minX = bounds[0]; - float minY = bounds[1]; - float minZ = bounds[2]; - float maxX = bounds[3] + 1f; - float maxY = bounds[4] + 1f; - float maxZ = bounds[5] + 1f; - - float minScreenX = Float.MAX_VALUE, maxScreenX = -Float.MAX_VALUE; - float minScreenY = Float.MAX_VALUE, maxScreenY = -Float.MAX_VALUE; - for (int corner = 0; corner < 8; corner++) { - float wx = (corner & 1) == 0 ? minX : maxX; - float wy = (corner & 2) == 0 ? minY : maxY; - float wz = (corner & 4) == 0 ? minZ : maxZ; - var sp = camera.worldToScreen(wx, wy, wz); - if (sp.x < minScreenX) minScreenX = sp.x; - if (sp.x > maxScreenX) maxScreenX = sp.x; - if (sp.y < minScreenY) minScreenY = sp.y; - if (sp.y > maxScreenY) maxScreenY = sp.y; - } - - int autoW = clampDim((int) Math.ceil(maxScreenX - minScreenX) + 16); - int autoH = clampDim((int) Math.ceil(maxScreenY - minScreenY) + 16); + SceneViewportMetrics metrics = SceneViewportMetrics.measure(camera, bounds); + int autoW = SceneViewportMetrics.clampDimension(metrics.spanX()); + int autoH = SceneViewportMetrics.clampDimension(metrics.spanY()); scene.setSceneSize(autoW, autoH); camera.setViewportSize(autoW, autoH); @@ -165,8 +147,4 @@ private static float clampZoom(float zoom) { return Math.max(LytGuidebookScene.MIN_ZOOM, Math.min(LytGuidebookScene.MAX_ZOOM, zoom <= 0 ? 1f : zoom)); } - private static int clampDim(int d) { - return Math.max(64, Math.min(256, d)); - } - } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index 192e94f0..afb3a0cd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -20,6 +20,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.scene.CameraSettings; +import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; @@ -102,8 +103,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), ExtensionCollection.EMPTY, "", new ResourceLocation(ph.pageDomain, "scene"), ""); try { - MdAstRoot ast = MdAst.fromMarkdown(ph.childrenSource, GuideMarkdownOptions.runtime()); - MdAstToMdxConverter.convert(ast, Collections.emptyMap()); + MdAstRoot ast = ph.childrenAst != null ? ph.childrenAst + : MdAst.fromMarkdown(ph.childrenSource, GuideMarkdownOptions.runtime()); + if (ph.childrenAst == null && ast != null) { + MdAstToMdxConverter.convert(ast, Collections.emptyMap()); + } GuideSceneStructureCompileScope.run(true, () -> { for (UnistNode child : ast.children()) { MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); @@ -141,6 +145,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { camera.setRotationCenter(center[0], center[1], center[2]); } // Auto-center the scene in the viewport using the same approach as BlockImageScript + // NB: auto-size and auto-zoom restoration pending via SceneViewportMetrics.measure(). + // Phase 2 reference: SceneTagCompiler lines 195-252 (commit 475353f^). camera.setOffsetX(0f); camera.setOffsetY(0f); var sc = camera.worldToScreen(center[0], center[1], center[2]); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index e30998f6..79958414 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -23,6 +23,9 @@ public class SpecialScript implements LytScript { @Override public String styleClass() { return "Special"; } + @Override + public boolean isAsync() { return true; } + @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; @@ -56,5 +59,6 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { var block = MediaWikiTagCompilerSupport.createSpecialBlock( result, ph.rows, context, query, resolver); ctx.replace(block); + ctx.markComplete(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index f536154f..5fa3ce23 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -18,11 +18,17 @@ import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureFingerprintResolver; import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler; +import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; +import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.unist.UnistNode; import com.hfstudio.guidenh.libs.unist.UnistParent; +import cpw.mods.fml.common.FMLLog; + public class SceneTagCompiler extends BlockTagCompiler { private static final String[] SCENE_ROOT_TAG_NAMES = { "GameScene", "Scene" }; @@ -120,6 +126,18 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // Raw source text of children (preserves BlockStats and all scene element markup) String childrenSource = compiler.getBlockTagChildrenSource(el); + // Pre-parse children source at compile time (pure function -- no I/O, no registry). + // SceneScript uses the pre-parsed AST instead of re-running MdAst.fromMarkdown(). + MdAstRoot preParsedAst = null; + if (childrenSource != null && !childrenSource.isEmpty()) { + try { + preParsedAst = MdAst.fromMarkdown(childrenSource, GuideMarkdownOptions.runtime()); + MdAstToMdxConverter.convert(preParsedAst, Collections.emptyMap()); + } catch (RuntimeException e) { + FMLLog.getLogger().warn("[GuideNH] [SceneTagCompiler] Failed to pre-parse scene children", e); + } + } + // Create placeholder block that carries all scene config to SceneScript String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene"; ScenePlaceholder placeholder = new ScenePlaceholder( @@ -132,7 +150,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl interactive, showBackground, allowLayerSlider, gridButtonEnabled, showGrid, childrenSource, - compiler.getPageId().getResourceDomain() + compiler.getPageId().getResourceDomain(), + preParsedAst // NEW: pre-parsed AST for SceneScript ); placeholder.setStyleClass(styleClass); placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); @@ -178,6 +197,7 @@ public static class ScenePlaceholder extends LytParagraph { public final boolean showGrid; @Nullable public final String childrenSource; public final String pageDomain; + @Nullable public final MdAstRoot childrenAst; public ScenePlaceholder( int width, int height, @@ -193,7 +213,8 @@ public ScenePlaceholder( boolean allowLayerSlider, boolean gridButtonEnabled, boolean showGrid, @Nullable String childrenSource, - String pageDomain) { + String pageDomain, + @Nullable MdAstRoot childrenAst) { this.width = width; this.height = height; this.explicitWidth = explicitWidth; @@ -219,6 +240,7 @@ public ScenePlaceholder( this.showGrid = showGrid; this.childrenSource = childrenSource; this.pageDomain = pageDomain; + this.childrenAst = childrenAst; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java index 9c4f4a88..eb484fec 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java @@ -1,5 +1,9 @@ package com.hfstudio.guidenh.guide.scene; +/** + * Pure-function utility for measuring scene content extent in screen space + * by projecting level bounding-box corners through a camera. + */ public class SceneViewportMetrics { private final float minScreenX; @@ -14,27 +18,44 @@ public SceneViewportMetrics(float minScreenX, float maxScreenX, float minScreenY this.maxScreenY = maxScreenY; } - public float minScreenX() { - return minScreenX; + public float minScreenX() { return minScreenX; } + public float maxScreenX() { return maxScreenX; } + public float minScreenY() { return minScreenY; } + public float maxScreenY() { return maxScreenY; } + public float spanX() { return maxScreenX - minScreenX; } + public float spanY() { return maxScreenY - minScreenY; } + + /** + * Projects the 8 corners of the given axis-aligned bounding box through + * the camera and returns the screen-space extent. + * Max bounds are extended by +1.0 on each axis to cover full block faces. + */ + public static SceneViewportMetrics measure(CameraSettings camera, int[] bounds) { + float lx = bounds[0]; + float ly = bounds[1]; + float lz = bounds[2]; + float hx = bounds[3] + 1f; + float hy = bounds[4] + 1f; + float hz = bounds[5] + 1f; + float minSX = Float.MAX_VALUE; + float maxSX = -Float.MAX_VALUE; + float minSY = Float.MAX_VALUE; + float maxSY = -Float.MAX_VALUE; + for (int corner = 0; corner < 8; corner++) { + float wx = (corner & 1) == 0 ? lx : hx; + float wy = (corner & 2) == 0 ? ly : hy; + float wz = (corner & 4) == 0 ? lz : hz; + var sp = camera.worldToScreen(wx, wy, wz); + if (sp.x < minSX) minSX = sp.x; + if (sp.x > maxSX) maxSX = sp.x; + if (sp.y < minSY) minSY = sp.y; + if (sp.y > maxSY) maxSY = sp.y; + } + return new SceneViewportMetrics(minSX, maxSX, minSY, maxSY); } - public float maxScreenX() { - return maxScreenX; - } - - public float minScreenY() { - return minScreenY; - } - - public float maxScreenY() { - return maxScreenY; - } - - public float spanX() { - return maxScreenX - minScreenX; - } - - public float spanY() { - return maxScreenY - minScreenY; + /** Auto-size: clamps dimension between 64 and 512 with 16px padding. */ + public static int clampDimension(float span) { + return Math.max(64, Math.min(512, (int) Math.ceil(span) + 16)); } } From 82c4f672e0850d56474d2c140cb63ab47997e303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:41:21 +0800 Subject: [PATCH 065/136] =?UTF-8?q?fix:=20systematic=20Phase=202=E2=86=923?= =?UTF-8?q?=20regression=20fixes=20and=20architectural=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes: - SceneScript: set CURRENT_SCENE before element compilation (restores Ponder/annotations) - SceneScript: call initializePonderTimelineBaseline + captureInitialInteractiveState - BlockImageScript: restore inline NBT parsing (id="stone{...}") and registryId arg - ItemLinkScript: fallback to getDisplayName() for self-closing tags - StructureScript: add Block.blockRegistry fallback for technical blocks - ItemImageScript/ItemGridScript: apply NBT from ParsedItemRef to ItemStack High priority: - SceneScript: restore auto-zoom (85% fill) and auto-size via SceneViewportMetrics - SceneScript: restore auto-center guard logic - SubPagesCompiler: add try/catch around resolveId() - MermaidCompiler: document compileNodeContentBlocks Phase 2 limitation - SpecialScript/SubPagesScript: add isWrapped pattern - SpecialScript: replace hardcoded strings with GuidebookText - QuestLinkScript: add try/catch around BqHelpers.resolveDisplay - CommandLinkScript: restore FMLLog logging - CategoryScript: use MediaWikiGeneratedListBlock via MediaWikiTagCompilerSupport - ItemImageCompiler: add ore attribute support to placeholder Architectural: - LytFlowInlineBlock: add centralized unwrapPlaceholder() utility - SceneTagCompiler: store element compilers in placeholder (removes hardcoded DefaultExtensions) - LytHost: two-phase dispatchMountEvents (sync first, then async) - Index methods: document Phase 2→3 indexing limitations - MediaWikiListPlanner: simplified column distribution algorithm - GuideScreen: add null guard in handleKeyboardInput --- .../guide/compiler/tags/CsvTableCompiler.java | 29 +++- .../compiler/tags/ItemImageCompiler.java | 9 +- .../guide/compiler/tags/MermaidCompiler.java | 9 ++ .../guide/compiler/tags/SubPagesCompiler.java | 7 +- .../tags/mediawiki/CategoryCompiler.java | 4 + .../tags/mediawiki/SpecialCompiler.java | 4 + .../document/flow/LytFlowInlineBlock.java | 22 +++ .../guidenh/guide/internal/GuideScreen.java | 1 + .../guidenh/guide/internal/host/LytHost.java | 74 +++++++--- .../host/scripts/BlockImageScript.java | 40 +++--- .../internal/host/scripts/CategoryScript.java | 56 ++------ .../host/scripts/CommandLinkScript.java | 3 + .../host/scripts/FloatingImageScript.java | 15 +-- .../internal/host/scripts/ImageScript.java | 12 +- .../internal/host/scripts/ItemGridScript.java | 7 +- .../host/scripts/ItemImageScript.java | 33 +++-- .../internal/host/scripts/ItemLinkScript.java | 9 ++ .../host/scripts/QuestLinkScript.java | 11 +- .../internal/host/scripts/RecipeScript.java | 12 +- .../internal/host/scripts/SceneScript.java | 127 +++++++++++++----- .../internal/host/scripts/SpecialScript.java | 7 +- .../host/scripts/StructureScript.java | 10 +- .../internal/host/scripts/SubPagesScript.java | 79 +++++------ .../guide/mediawiki/MediaWikiListPlanner.java | 80 ++--------- .../guidenh/guide/scene/SceneTagCompiler.java | 13 +- 25 files changed, 395 insertions(+), 278 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index 64f31d3d..e996e4cf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -1,5 +1,6 @@ package com.hfstudio.guidenh.guide.compiler.tags; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -10,6 +11,7 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.table.LytTable; @@ -51,16 +53,35 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // Load CSV asset content and index cell values for search. + // CSV data is available at compile time (src is a file path), unlike Category/Special + // whose data is resolved at MOUNT time by scripts. String src; try { src = MdxAttrs.getString(el, "src", null); } catch (MdxAttrs.AttributeException e) { return; } - if (src != null && !src.trim() - .isEmpty()) { - sink.appendText(el, src); - sink.appendBreak(); + if (src != null && !src.trim().isEmpty()) { + try { + ResourceLocation csvId = IdUtils.resolveLink(src.trim(), indexer.getPageId()); + byte[] data = indexer.loadAsset(csvId); + if (data != null) { + List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); + for (List row : rows) { + for (String cell : row) { + if (!cell.isEmpty()) { + sink.appendText(el, cell); + } + } + sink.appendBreak(); + } + } + } catch (Exception ignored) { + // Fallback: index the src path string if asset loading fails + sink.appendText(el, src); + sink.appendBreak(); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java index a1bc0c76..27df56b7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java @@ -22,6 +22,7 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { String itemId = MdxAttrs.getString(compiler, parent, el, "id", null); + String ore = MdxAttrs.getString(compiler, parent, el, "ore", null); if (itemId == null) return; itemId = itemId.trim(); @@ -73,7 +74,7 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen labelFormat = (formatRaw != null && !formatRaw.isEmpty()) ? formatRaw : null; ItemImagePlaceholder placeholder = new ItemImagePlaceholder( - itemId, scale, yOffset, labelYOffset, showTooltip, showIcon, labelPosition, labelFormat); + itemId, scale, yOffset, labelYOffset, showTooltip, showIcon, labelPosition, labelFormat, ore); var inline = new LytFlowInlineBlock(); inline.setBlock(placeholder); @@ -109,10 +110,13 @@ public static class ItemImagePlaceholder extends LytParagraph { public final String labelPosition; @Nullable public final String labelFormat; + @Nullable + public final String ore; public ItemImagePlaceholder(String itemId, float scale, @Nullable Integer yOffset, @Nullable Integer labelYOffset, boolean showTooltip, @Nullable Boolean showIcon, - @Nullable String labelPosition, @Nullable String labelFormat) { + @Nullable String labelPosition, @Nullable String labelFormat, + @Nullable String ore) { this.itemId = itemId; this.scale = scale; this.yOffset = yOffset; @@ -121,6 +125,7 @@ public ItemImagePlaceholder(String itemId, float scale, @Nullable Integer yOffse this.showIcon = showIcon; this.labelPosition = labelPosition; this.labelFormat = labelFormat; + this.ore = ore; setStyleClass("ItemImage"); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java index e06e74db..4cc7c691 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java @@ -77,6 +77,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // NB: Phase 2 loaded src-based Mermaid content and indexed the actual diagram source. + // Phase 3 src-based content is resolved at MOUNT time by MermaidScript, so index() only + // indexes the src path string. Inline content (no src attribute) is still indexed here. + // Full indexing for src-based mermaid requires a post-mount indexing pass (TBD). String src; try { src = MdxAttrs.getString(el, "src", null); @@ -100,6 +104,11 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink private Map compileNodeContentBlocks(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields mermaidElement) { + // NB: Phase 2 cross-referenced NodeContent IDs against the parsed MermaidMindmapNode tree + // (via indexNodesById), validated unknown IDs, and provided inline-markdown fallback for + // nodes without explicit NodeContent. Phase 3 defers tree construction to MermaidScript + // (MOUNT time), so cross-validation must happen at runtime. See MermaidScript for the + // runtime counterpart. Map result = new LinkedHashMap<>(); for (MdxJsxFlowElement child : MermaidMindmapNodeContentExtractor .collectNodeContentElements(mermaidElement.children())) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java index 8d055af8..3d48d704 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java @@ -19,7 +19,12 @@ public Set getTagNames() { protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { var pageIdStr = el.getAttributeString("id", null); if (pageIdStr != null) { - pageIdStr = compiler.resolveId(pageIdStr).toString(); + try { + pageIdStr = compiler.resolveId(pageIdStr).toString(); + } catch (Exception e) { + parent.appendError(compiler, "Invalid id: " + pageIdStr, el); + return; + } } var alphabetical = MdxAttrs.getBoolean(compiler, parent, el, "alphabetical", false); var currentPageId = compiler.getPageId().toString(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 89890265..2794f0b1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -44,6 +44,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // NB: Phase 2 indexed category member titles resolved by + // MediaWikiPageListBuilder.buildCategoryMembers(). Phase 3 defers member resolution to + // CategoryScript (MOUNT time), so index() only indexes the category name string. + // Full indexing requires a post-mount indexing pass (TBD). String categoryName = el.getAttributeString("name", null); if (categoryName != null && !categoryName.trim() .isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index 80ecf37b..966ea31b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -48,6 +48,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // NB: Phase 2 indexed special page result entries resolved by + // MediaWikiSpecialPageResolver.resolve(). Phase 3 defers resolution to + // SpecialScript (MOUNT time), so index() only indexes the special page name string. + // Full indexing requires a post-mount indexing pass (TBD). String specialName = el.getAttributeString("name", null); if (specialName != null && !specialName.trim() .isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java index c5540d2f..50ecb11b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java @@ -2,6 +2,8 @@ import java.util.Optional; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.LytSize; import com.hfstudio.guidenh.guide.document.block.LytBlock; @@ -83,6 +85,26 @@ protected void visitChildren(LytVisitor visitor) { } } + /** + * Unwraps a flow-wrapped placeholder node. When a block-level tag appears in inline + * context, BlockTagCompiler wraps the placeholder in LytFlowInlineBlock. Dispatch + * passes the wrapper. This helper returns the inner placeholder regardless. + * + * @return the unwrapped placeholder of type T, or null if the node is neither + * a direct instance nor a LytFlowInlineBlock wrapping an instance + */ + @Nullable + public static T unwrapPlaceholder(Object node, Class placeholderClass) { + if (placeholderClass.isInstance(node)) { + return placeholderClass.cast(node); + } + if (node instanceof LytFlowInlineBlock wrapper + && placeholderClass.isInstance(wrapper.getBlock())) { + return placeholderClass.cast(wrapper.getBlock()); + } + return null; + } + public static LytFlowInlineBlock of(LytBlock block) { var inlineBlock = new LytFlowInlineBlock(); inlineBlock.setBlock(block); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index de3e3007..5077f1c6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -4700,6 +4700,7 @@ protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, i @Override public void handleKeyboardInput() { + if (mc == null) return; if (Keyboard.getEventKeyState() || isCommittedCharacterEventForFocusedTextInput()) { keyTyped(Keyboard.getEventCharacter(), Keyboard.getEventKey()); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 41cde03e..56407a79 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -213,39 +213,63 @@ private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { } } + /** + * Two-phase MOUNT dispatch: + *

    + * Phase 1 (sync): walk the entire tree and execute + * every synchronous script immediately. This guarantees that all + * setup and initialization work (e.g. establishing CURRENT_SCENE, + * compiling child elements) is finished before any asynchronous + * work begins. + *

    + * Phase 2 (async): walk the tree a second time and + * queue every asynchronous script as a {@link MaterializeTask} for + * execution on subsequent ticks (see {@link #step}). + *

    + * Within each phase the original document order (parent before children) + * is preserved. The node-level result cache is consulted: if a node + * already has a cached result from a previous mount, the cached content + * is restored directly and the script is skipped in both + * phases. + */ private void dispatchMountEvents(LytNode node) { + dispatchPhase(node, false); // Phase 1: sync scripts only + dispatchPhase(node, true); // Phase 2: queue async scripts only + } + + private void dispatchPhase(LytNode node, boolean asyncPhase) { String cls = node.getStyleClass(); if (cls != null) { LytScript script = scripts.get(cls); if (script != null) { - dispatchScript(script, node); + dispatchScriptInPhase(script, node, asyncPhase); } } for (var child : node.getChildren()) { - dispatchMountEvents(child); + dispatchPhase(child, asyncPhase); } - dispatchMountEventsFlow(node); + dispatchPhaseFlow(node, asyncPhase); } - private void dispatchMountEventsFlow(LytNode node) { + private void dispatchPhaseFlow(LytNode node, boolean asyncPhase) { if (node instanceof LytParagraph para) { for (var fcChild : para.getContent()) { - dispatchMountEventsFlowRecursive(fcChild); + dispatchPhaseFlowRecursive(fcChild, asyncPhase); } } } - private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { + private void dispatchPhaseFlowRecursive(LytFlowContent fc, boolean asyncPhase) { String cls = fc.getStyleClass(); if (cls != null) { LytScript script = scripts.get(cls); if (script != null) { - dispatchScript(script, fc); + dispatchScriptInPhase(script, fc, asyncPhase); } } if (fc instanceof LytFlowSpan span) { for (var child : span.getChildren()) { - dispatchMountEventsFlowRecursive(child); + dispatchPhaseFlowRecursive(child, asyncPhase); } } else if (fc instanceof LytFlowInlineBlock inlineBlock && inlineBlock.getBlock() != null) { LytBlock inner = inlineBlock.getBlock(); @@ -253,13 +277,25 @@ private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { if (innerCls != null) { LytScript script = scripts.get(innerCls); if (script != null) { - dispatchScript(script, inlineBlock); + dispatchScriptInPhase(script, inlineBlock, asyncPhase); } } } } - private void dispatchScript(LytScript script, Object node) { + /** + * Dispatch a single script in the given phase. + *

      + *
    • If the node has a cached result from a previous mount, the + * cached content is restored directly and the script is skipped + * entirely (both phases). + *
    • In the sync phase ({@code asyncPhase == false}), only + * non-async scripts are executed synchronously. + *
    • In the async phase ({@code asyncPhase == true}), only + * async scripts are enqueued as {@link MaterializeTask}s. + *
    + */ + private void dispatchScriptInPhase(LytScript script, Object node, boolean asyncPhase) { String nodeUid = nodeUidOf(node); if (nodeUid != null) { Object cached = getNodeResult(currentPageId, nodeUid); @@ -268,14 +304,18 @@ private void dispatchScript(LytScript script, Object node) { return; } } - if (script.isAsync()) { - taskQueue.addLast(new MaterializeTask(script, node, new ScriptContextImpl(node, this, document))); + if (asyncPhase) { + if (script.isAsync()) { + taskQueue.addLast(new MaterializeTask(script, node, new ScriptContextImpl(node, this, document))); + } } else { - try { - ScriptContextImpl ctx = new ScriptContextImpl(node, this, document); - script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); - } catch (Exception e) { - e.printStackTrace(); + if (!script.isAsync()) { + try { + ScriptContextImpl ctx = new ScriptContextImpl(node, this, document); + script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); + } catch (Exception e) { + e.printStackTrace(); + } } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 1dee294f..c4f5c19a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -7,6 +7,8 @@ import net.minecraftforge.oredict.OreDictionary; import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; +import com.hfstudio.guidenh.guide.compiler.IdUtils; +import com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef; import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler.BlockImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; @@ -42,19 +44,12 @@ public class BlockImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - BlockImagePlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w - && w.getBlock() instanceof BlockImagePlaceholder p; - if (isWrapped) { - ph = (BlockImagePlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof BlockImagePlaceholder p) { - ph = p; - } else { - return; - } + BlockImagePlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, BlockImagePlaceholder.class); + if (ph == null) return; Block block = null; int meta = ph.meta; + NBTTagCompound tileTag = null; if (ph.ore != null && !ph.ore.isEmpty()) { ItemStack oreStack = GuideItemReferenceResolver.resolveOreDictionaryStack(ph.ore); @@ -63,12 +58,12 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { meta = oreStack.getItemDamage(); } } else if (ph.id != null) { - Item item = (Item) Item.itemRegistry.getObject(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id); - if (item != null) { - block = Block.getBlockFromItem(item); - } - if (block == null) { - block = (Block) Block.blockRegistry.getObject(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id); + // Handle inline NBT: id="minecraft:stone{BlockEntityTag:{...}}" + ParsedItemRef ref = IdUtils.parseItemRef(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id, "minecraft"); + Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); + if (item != null) block = Block.getBlockFromItem(item); + if (ref.nbt() != null && tileTag == null) { + tileTag = (NBTTagCompound) ref.nbt().copy(); } } @@ -78,10 +73,16 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } - NBTTagCompound tileTag = null; if (ph.nbt != null && !ph.nbt.trim().isEmpty()) { try { - tileTag = GuideTextNbtCodec.readTextSafeCompound(ph.nbt.trim()); + NBTTagCompound explicitTag = GuideTextNbtCodec.readTextSafeCompound(ph.nbt.trim()); + if (tileTag != null) { + for (Object key : explicitTag.func_150296_c()) { + tileTag.setTag((String) key, explicitTag.getTag((String) key)); + } + } else { + tileTag = explicitTag; + } } catch (Exception ignored) {} } @@ -92,7 +93,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { int defaultMeta = meta == Integer.MIN_VALUE ? BlockElementCompiler.defaultMetaFor(block, null) : meta; GuidebookLevel level = new GuidebookLevel(); - GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag); + String registryId = ph.id != null ? (ph.id.contains(":") ? ph.id : "minecraft:" + ph.id) : ""; + GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag, registryId); if (level.isEmpty()) { ctx.replace( diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index e857492a..0315ff46 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -1,15 +1,10 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import java.util.List; - import com.hfstudio.guidenh.guide.Guide; -import com.hfstudio.guidenh.guide.PageAnchor; -import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.CategoryCompiler.CategoryPlaceholder; +import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.MediaWikiTagCompilerSupport; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; -import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -17,7 +12,7 @@ import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageTitleResolver; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; public class CategoryScript implements LytScript { @@ -31,14 +26,11 @@ public class CategoryScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - CategoryPlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w - && w.getBlock() instanceof CategoryPlaceholder p; - if (isWrapped) { - ph = (CategoryPlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof CategoryPlaceholder p) { - ph = p; - } else { + CategoryPlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, CategoryPlaceholder.class); + if (ph == null) return; + + if (!(ctx.getPageCollection() instanceof Guide guide)) { + ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoDataAvailable.text())); return; } @@ -48,34 +40,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } - List members = index.get(ph.name); - if (members.isEmpty()) { - ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoPagesInCategory.text())); - return; - } - - LytVBox box = new LytVBox(); - box.setGap(2); - int count = 0; - for (PageAnchor anchor : members) { - if (ph.rows > 0 && count >= ph.rows) break; - LytParagraph line = new LytParagraph(); - LytFlowLink link = new LytFlowLink(); - link.setGuideLink(ph.guideId, anchor); - if (ctx.getPageCollection() instanceof Guide guide) { - ParsedGuidePage page = guide.getParsedPage(anchor.pageId()); - if (page != null) { - link.appendText(MediaWikiPageTitleResolver.resolvePageTitle(guide, page)); - } else { - link.appendText(anchor.pageId().getResourcePath()); - } - } else { - link.appendText(anchor.pageId().getResourcePath()); - } - line.append(link); - box.append(line); - count++; - } - ctx.replace(box); + var context = MediaWikiTagCompilerSupport.createListContext(guide, index); + var entries = MediaWikiPageListBuilder.buildCategoryMembers(context, ph.name); + var block = MediaWikiTagCompilerSupport.createBlock( + entries, ph.rows, GuidebookText.MediaWikiNoPagesInCategory.text()); + ctx.replace(block); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java index fa12abaa..6e105699 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -2,6 +2,8 @@ import net.minecraft.client.Minecraft; +import cpw.mods.fml.common.FMLLog; + import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -29,6 +31,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (command == null) return; link.setClickCallback(screen -> { if (Minecraft.getMinecraft().thePlayer == null) return; + FMLLog.getLogger().info("[GuideNH] [CommandLink] Sending command: {}", command); Minecraft.getMinecraft().thePlayer.sendChatMessage(command); if (Boolean.TRUE.equals(close)) { Minecraft.getMinecraft().displayGuiScreen(null); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index 74bd06dc..b36624d6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -28,17 +28,10 @@ public class FloatingImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - LytImageBlock placeholder; - LytFlowInlineBlock oldWrapper = null; - boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof LytImageBlock p; - if (isWrapped) { - oldWrapper = (LytFlowInlineBlock) node; - placeholder = (LytImageBlock) oldWrapper.getBlock(); - } else if (node instanceof LytImageBlock p) { - placeholder = p; - } else { - return; - } + LytImageBlock placeholder = LytFlowInlineBlock.unwrapPlaceholder(node, LytImageBlock.class); + if (placeholder == null) return; + LytFlowInlineBlock oldWrapper = node instanceof LytFlowInlineBlock wrapper ? wrapper : null; + boolean isWrapped = oldWrapper != null; String src = placeholder.getSrc(); if (src == null || src.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index 10b80dff..31a15dc7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -28,15 +28,9 @@ public class ImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - LytImageBlock placeholder; - boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof LytImageBlock p; - if (isWrapped) { - placeholder = (LytImageBlock) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof LytImageBlock p) { - placeholder = p; - } else { - return; - } + LytImageBlock placeholder = LytFlowInlineBlock.unwrapPlaceholder(node, LytImageBlock.class); + if (placeholder == null) return; + boolean isWrapped = node instanceof LytFlowInlineBlock; String src = placeholder.getSrc(); if (src == null || src.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index f7b2eb54..3b32079c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -53,6 +53,11 @@ private static ItemStack resolveItemId(String itemId) { com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; + if (item == null) return null; + ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); + if (ref.nbt() != null) { + stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); + } + return stack; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index f90c1bd9..f0ecfba8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -2,6 +2,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraftforge.oredict.OreDictionary; import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemImage; @@ -26,20 +27,23 @@ public class ItemImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - ItemImagePlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof ItemImagePlaceholder p; - if (isWrapped) { - ph = (ItemImagePlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof ItemImagePlaceholder p) { - ph = p; - } else { - return; - } + ItemImagePlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, ItemImagePlaceholder.class); + if (ph == null) return; + boolean isWrapped = node instanceof LytFlowInlineBlock; ItemStack stack = resolveItemId(ph.itemId); if (stack == null) { - replaceFlowError(ctx, isWrapped, "[ItemImage] Item not found: " + ph.itemId); - return; + // Fallback to ore dictionary if direct item lookup fails + if (ph.ore != null) { + java.util.List oreStacks = OreDictionary.getOres(ph.ore); + if (oreStacks != null && !oreStacks.isEmpty()) { + stack = oreStacks.get(0).copy(); + } + } + if (stack == null) { + replaceFlowError(ctx, isWrapped, "[ItemImage] Item not found: " + ph.itemId); + return; + } } LytItemImage image = new LytItemImage(stack); @@ -85,6 +89,11 @@ private static ItemStack resolveItemId(String itemId) { com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; + if (item == null) return null; + ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); + if (ref.nbt() != null) { + stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); + } + return stack; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 8efe0a84..792e6e25 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -86,6 +86,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (Boolean.TRUE.equals(showTooltip)) { span.setTooltip(new ItemTooltip(stack)); } + // If the span has no children text (self-closing tag), fall back to item display name + if (span.getChildren().isEmpty()) { + span.appendText(stack.getDisplayName()); + } span.modifyStyle(style -> style.italic(true)); ctx.replace(span); return; @@ -116,6 +120,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } } + + // If the link has no children text (self-closing tag), fall back to item display name + if (link.getChildren().isEmpty()) { + link.appendText(stack.getDisplayName()); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java index a12aa212..7e630c0d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java @@ -6,6 +6,7 @@ import net.minecraft.util.StatCollector; import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.flow.LytFlowText; @@ -40,8 +41,14 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Boolean showTooltip = (Boolean) placeholder.getData("showTooltip"); String overrideText = (String) placeholder.getData("overrideText"); - QuestDisplay display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, - Boolean.TRUE.equals(showTooltip)); + QuestDisplay display; + try { + display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, + Boolean.TRUE.equals(showTooltip)); + } catch (Throwable t) { + ctx.replace(LytParagraph.error("[QuestLink] BetterQuesting integration not available")); + return; + } if (display == null) { LytFlowSpan errorSpan = new LytFlowSpan(); errorSpan.modifyStyle(style -> style.color(SymbolicColor.ERROR_TEXT)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index eec1c312..bcb0c413 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -52,16 +52,8 @@ public class RecipeScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - RecipePlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w - && w.getBlock() instanceof RecipePlaceholder p; - if (isWrapped) { - ph = (RecipePlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof RecipePlaceholder p) { - ph = p; - } else { - return; - } + RecipePlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, RecipePlaceholder.class); + if (ph == null) return; Item item = (Item) Item.itemRegistry.getObject(ph.ref.rawKey()); if (item == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index afb3a0cd..cdc5e26e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -16,11 +16,11 @@ import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.PageIndex; -import com.hfstudio.guidenh.guide.internal.extensions.DefaultExtensions; import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.scene.CameraSettings; import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; +import com.hfstudio.guidenh.guide.scene.annotation.compiler.AnnotationTagCompiler; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; @@ -43,16 +43,7 @@ public class SceneScript implements LytScript { - private final Map elementCompilers; - public SceneScript() { - Map map = new HashMap<>(); - for (var ext : DefaultExtensions.sceneElementCompilers()) { - for (String name : ext.getTagNames()) { - map.put(name, ext); - } - } - this.elementCompilers = map; } @Override @@ -97,38 +88,35 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { int height = ph.height > 0 ? ph.height : 180; camera.setViewportSize(width, height); - // Parse children source and compile scene elements + // Parse children source ExceptionCollector errorSink = new ExceptionCollector(); PageCollection pc = ctx.getPageCollection(); PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), ExtensionCollection.EMPTY, "", new ResourceLocation(ph.pageDomain, "scene"), ""); + MdAstRoot ast; try { - MdAstRoot ast = ph.childrenAst != null ? ph.childrenAst + ast = ph.childrenAst != null ? ph.childrenAst : MdAst.fromMarkdown(ph.childrenSource, GuideMarkdownOptions.runtime()); if (ph.childrenAst == null && ast != null) { MdAstToMdxConverter.convert(ast, Collections.emptyMap()); } - GuideSceneStructureCompileScope.run(true, () -> { - for (UnistNode child : ast.children()) { - MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); - if (el == null) continue; - SceneElementTagCompiler ec = elementCompilers.get(el.name()); - if (ec != null) { - ec.compile(level, camera, runtimeCompiler, errorSink, el); - } - } - }); } catch (Exception e) { FMLLog.getLogger().warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); ctx.replace(LytParagraph.error("[Scene] Failed to parse scene elements")); return; } - if (level.isEmpty()) { - ctx.replace(LytParagraph.error("[Scene] Scene has no supported elements")); - return; + // Build element compiler map from placeholder (set at compile time by SceneTagCompiler) + Map elementCompilers = new HashMap<>(); + if (ph.sceneElementCompilers != null) { + for (var ec : ph.sceneElementCompilers) { + for (String name : ec.getTagNames()) { + elementCompilers.put(name, ec); + } + } } + // Create the scene EARLY so element compilers can access it via CURRENT_SCENE. LytGuidebookScene scene = new LytGuidebookScene(); scene.setLevel(level); scene.setCamera(camera); @@ -139,20 +127,91 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { scene.setGridButtonEnabled(ph.gridButtonEnabled); scene.setGridVisible(ph.showGrid); - if (!level.isEmpty()) { - float[] center = level.getCenter(); - if (!ph.explicitCenter) { - camera.setRotationCenter(center[0], center[1], center[2]); + // Compile scene elements with CURRENT_SCENE set so that element compilers + // (ImportPonderElementCompiler, ImportStructureLibElementCompiler, annotations, etc.) + // can call scene.attachPonderData(), scene.addAnnotation(), etc. + var prevScene = AnnotationTagCompiler.CURRENT_SCENE.get(); + AnnotationTagCompiler.CURRENT_SCENE.set(scene); + try { + GuideSceneStructureCompileScope.run(true, () -> { + for (UnistNode child : ast.children()) { + MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); + if (el == null) continue; + SceneElementTagCompiler ec = elementCompilers.get(el.name()); + if (ec != null) { + ec.compile(level, camera, runtimeCompiler, errorSink, el); + } + } + }); + } finally { + if (prevScene != null) { + AnnotationTagCompiler.CURRENT_SCENE.set(prevScene); + } else { + AnnotationTagCompiler.CURRENT_SCENE.remove(); } - // Auto-center the scene in the viewport using the same approach as BlockImageScript - // NB: auto-size and auto-zoom restoration pending via SceneViewportMetrics.measure(). - // Phase 2 reference: SceneTagCompiler lines 195-252 (commit 475353f^). + } + + if (level.isEmpty()) { + ctx.replace(LytParagraph.error("[Scene] Scene has no supported elements")); + return; + } + + // Finalize scene setup: auto-center, ponder baseline, interactive state capture + float[] center = level.getCenter(); + if (!ph.explicitCenter) { + camera.setRotationCenter(center[0], center[1], center[2]); + } + // Auto-center the scene in the viewport + if (!ph.explicitCenter && Float.isNaN(ph.offsetX) && Float.isNaN(ph.offsetY)) { camera.setOffsetX(0f); camera.setOffsetY(0f); var sc = camera.worldToScreen(center[0], center[1], center[2]); - camera.setOffsetX(-sc.x + (Float.isNaN(ph.offsetX) ? 0 : ph.offsetX)); - camera.setOffsetY(sc.y + (Float.isNaN(ph.offsetY) ? 0 : ph.offsetY)); + camera.setOffsetX(-sc.x); + camera.setOffsetY(sc.y); + } else if (!Float.isNaN(ph.offsetX) || !Float.isNaN(ph.offsetY)) { + if (!Float.isNaN(ph.offsetX)) camera.setOffsetX(ph.offsetX); + if (!Float.isNaN(ph.offsetY)) camera.setOffsetY(ph.offsetY); } + + // Auto-zoom: when zoom is not explicitly set, fit scene to viewport at 85% fill + if (Float.isNaN(ph.zoom)) { + camera.setZoom(1f); + camera.setOffsetX(0f); + camera.setOffsetY(0f); + if (!level.isEmpty()) { + int[] bounds = level.getBounds(); + SceneViewportMetrics metrics = SceneViewportMetrics.measure(camera, bounds); + float spanX = metrics.spanX(); + float spanY = metrics.spanY(); + if (spanX > 0.5f || spanY > 0.5f) { + float zX = spanX > 0.5f ? (float) width / spanX : Float.MAX_VALUE; + float zY = spanY > 0.5f ? (float) height / spanY : Float.MAX_VALUE; + float autoZoom = Math.min(zX, zY) * 0.85f; + autoZoom = Math.max(LytGuidebookScene.MIN_ZOOM, Math.min(LytGuidebookScene.MAX_ZOOM, autoZoom)); + camera.setZoom(autoZoom); + } + } + } + // Auto-size: when width/height not explicitly set, measure and compute viewport + if (!ph.explicitWidth || !ph.explicitHeight) { + camera.setOffsetX(0f); + camera.setOffsetY(0f); + if (!level.isEmpty()) { + int[] bounds = level.getBounds(); + SceneViewportMetrics metrics = SceneViewportMetrics.measure(camera, bounds); + if (!ph.explicitWidth && metrics.spanX() > 0.5f) { + width = SceneViewportMetrics.clampDimension(metrics.spanX()); + } + if (!ph.explicitHeight && metrics.spanY() > 0.5f) { + height = SceneViewportMetrics.clampDimension(metrics.spanY()); + } + scene.setSceneSize(width, height); + camera.setViewportSize(width, height); + } + } + + scene.initializePonderTimelineBaseline(); + scene.captureInitialInteractiveState(); scene.snapshotInitialCamera(); ctx.replace(scene); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index 79958414..ddb91ebc 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -5,6 +5,8 @@ import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.MediaWikiTagCompilerSupport; import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.SpecialCompiler.SpecialPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; +import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -29,11 +31,12 @@ public class SpecialScript implements LytScript { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - if (!(node instanceof SpecialPlaceholder ph)) return; + SpecialPlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, SpecialPlaceholder.class); + if (ph == null) return; PageCollection pc = ctx.getPageCollection(); if (!(pc instanceof Guide guide)) { - ctx.replace(LytParagraph.error("[Special] Special page not available: collection is not a guide")); + ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoDataAvailable.text())); return; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index dc2d298d..e64df640 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -1,5 +1,6 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import net.minecraft.block.Block; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; @@ -50,6 +51,13 @@ private static ItemStack resolveEntry(String idSpec) { com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(idSpec, "minecraft"); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; + if (item != null) { + return new ItemStack(item, 1, ref.concreteMeta()); + } + Block block = (Block) Block.blockRegistry.getObject(ref.rawKey()); + if (block != null) { + return new ItemStack(block, 1, ref.concreteMeta()); + } + return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index 9c8171e9..49b148df 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -11,6 +11,7 @@ import com.hfstudio.guidenh.guide.document.block.LytList; import com.hfstudio.guidenh.guide.document.block.LytListItem; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.internal.GuideRegistry; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -31,50 +32,52 @@ public class SubPagesScript implements LytScript { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - if (event.type() == EventType.MOUNT && node instanceof SubPagesPlaceholder ph) { - NavigationTree tree = GuideRegistry.getMergedNavigationTree(); + if (event.type() != EventType.MOUNT) return; + SubPagesPlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, SubPagesPlaceholder.class); + if (ph == null) return; - List subNodes; - if (ph.pageIdStr == null) { - ResourceLocation currentPageId = new ResourceLocation(ph.currentPageId); - NavigationNode current = tree.getNodeById(currentPageId); - subNodes = current != null ? new ArrayList<>(current.children()) : tree.getRootNodes(); - } else if (ph.pageIdStr.isEmpty()) { - subNodes = tree.getRootNodes(); - } else { - ResourceLocation pageId = new ResourceLocation(ph.pageIdStr); - NavigationNode navNode = tree.getNodeById(pageId); - if (navNode == null) { - ctx.replace(LytParagraph.error("[SubPages] Page not found in navigation: " + ph.pageIdStr)); - return; - } - subNodes = navNode.children(); - } - - if (ph.alphabetical) { - subNodes = new ArrayList<>(subNodes); - subNodes.sort(Comparator.comparing(NavigationNode::title)); - } + NavigationTree tree = GuideRegistry.getMergedNavigationTree(); - if (subNodes.isEmpty()) { - ctx.replace(LytParagraph.error("[SubPages] No sub-pages found")); + List subNodes; + if (ph.pageIdStr == null) { + ResourceLocation currentPageId = new ResourceLocation(ph.currentPageId); + NavigationNode current = tree.getNodeById(currentPageId); + subNodes = current != null ? new ArrayList<>(current.children()) : tree.getRootNodes(); + } else if (ph.pageIdStr.isEmpty()) { + subNodes = tree.getRootNodes(); + } else { + ResourceLocation pageId = new ResourceLocation(ph.pageIdStr); + NavigationNode navNode = tree.getNodeById(pageId); + if (navNode == null) { + ctx.replace(LytParagraph.error("[SubPages] Page not found in navigation: " + ph.pageIdStr)); return; } + subNodes = navNode.children(); + } - LytList list = new LytList(false, 0); - for (NavigationNode childNode : subNodes) { - if (!childNode.hasPage()) continue; + if (ph.alphabetical) { + subNodes = new ArrayList<>(subNodes); + subNodes.sort(Comparator.comparing(NavigationNode::title)); + } - LytListItem listItem = new LytListItem(); - LytParagraph listItemPar = new LytParagraph(); - LytFlowLink link = new LytFlowLink(); - link.setGuideLink(childNode.guideId(), PageAnchor.page(childNode.pageId())); - link.appendText(childNode.title()); - listItemPar.append(link); - listItem.append(listItemPar); - list.append(listItem); - } - ctx.replace(list); + if (subNodes.isEmpty()) { + ctx.replace(LytParagraph.error("[SubPages] No sub-pages found")); + return; + } + + LytList list = new LytList(false, 0); + for (NavigationNode childNode : subNodes) { + if (!childNode.hasPage()) continue; + + LytListItem listItem = new LytListItem(); + LytParagraph listItemPar = new LytParagraph(); + LytFlowLink link = new LytFlowLink(); + link.setGuideLink(childNode.guideId(), PageAnchor.page(childNode.pageId())); + link.appendText(childNode.title()); + listItemPar.append(link); + listItem.append(listItemPar); + list.append(listItem); } + ctx.replace(list); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java index cbc2bbbd..2be2031b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java @@ -61,67 +61,27 @@ public static List planColumns(List ent int columnCount = Math.max(1, sanitizeRows(rows)); var columns = new ArrayList(columnCount); if (entries.isEmpty()) { - for (int index = 0; index < columnCount; index++) { + for (int i = 0; i < columnCount; i++) { columns.add(new MediaWikiListColumn(Collections.emptyList())); } return columns; } - List groups = buildGroups(entries); - int[] targetSizes = buildTargetSizes(entries.size(), columnCount); - int groupIndex = 0; - int entryOffset = 0; - - for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { - var sections = new ArrayList(); - - if (columnIndex == columnCount - 1) { - while (groupIndex < groups.size()) { - MediaWikiListGroup group = groups.get(groupIndex); - sections.add( - createSection( - group, - entryOffset, - group.entries() - .size())); - groupIndex++; - entryOffset = 0; - } - columns.add(new MediaWikiListColumn(sections)); + int perColumn = (int) Math.ceil((double) entries.size() / columnCount); + for (int col = 0; col < columnCount; col++) { + int start = col * perColumn; + int end = Math.min(start + perColumn, entries.size()); + if (start >= entries.size()) { + columns.add(new MediaWikiListColumn(Collections.emptyList())); continue; } - - int targetSize = targetSizes[columnIndex]; - int currentSize = 0; - while (groupIndex < groups.size() && currentSize < targetSize) { - MediaWikiListGroup group = groups.get(groupIndex); - int remainingCount = group.entries() - .size() - entryOffset; - int remainingCapacity = targetSize - currentSize; - if (remainingCount <= remainingCapacity) { - sections.add( - createSection( - group, - entryOffset, - group.entries() - .size())); - currentSize += remainingCount; - groupIndex++; - entryOffset = 0; - continue; - } - if (currentSize == 0 && remainingCapacity > 0) { - int endExclusive = entryOffset + remainingCapacity; - sections.add(createSection(group, entryOffset, endExclusive)); - entryOffset = endExclusive; - currentSize += remainingCapacity; - } - break; + List slice = entries.subList(start, end); + var sections = new ArrayList(); + for (MediaWikiListGroup group : buildGroups(slice)) { + sections.add(new MediaWikiListSection(group.key(), new ArrayList<>(group.entries()))); } - columns.add(new MediaWikiListColumn(sections)); } - return columns; } @@ -137,24 +97,6 @@ public static List> splitIntoColumns(List( - group.entries() - .subList(startInclusive, endExclusive))); - } - public static String resolveGroupKey(MediaWikiListEntry entry) { String value = firstSortableValue(entry); if (value.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 5fa3ce23..178374e5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -138,6 +139,9 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } } + // Store element compilers from ExtensionCollection in placeholder + var sceneElementCompilers = compiler.getExtensions().get(SceneElementTagCompiler.EXTENSION_POINT); + // Create placeholder block that carries all scene config to SceneScript String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene"; ScenePlaceholder placeholder = new ScenePlaceholder( @@ -151,7 +155,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl allowLayerSlider, gridButtonEnabled, showGrid, childrenSource, compiler.getPageId().getResourceDomain(), - preParsedAst // NEW: pre-parsed AST for SceneScript + preParsedAst, // NEW: pre-parsed AST for SceneScript + sceneElementCompilers ); placeholder.setStyleClass(styleClass); placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); @@ -198,6 +203,8 @@ public static class ScenePlaceholder extends LytParagraph { @Nullable public final String childrenSource; public final String pageDomain; @Nullable public final MdAstRoot childrenAst; + @Nullable + public final List sceneElementCompilers; public ScenePlaceholder( int width, int height, @@ -214,7 +221,8 @@ public ScenePlaceholder( boolean showGrid, @Nullable String childrenSource, String pageDomain, - @Nullable MdAstRoot childrenAst) { + @Nullable MdAstRoot childrenAst, + @Nullable List sceneElementCompilers) { this.width = width; this.height = height; this.explicitWidth = explicitWidth; @@ -241,6 +249,7 @@ public ScenePlaceholder( this.childrenSource = childrenSource; this.pageDomain = pageDomain; this.childrenAst = childrenAst; + this.sceneElementCompilers = sceneElementCompilers; } } From be921ecdb6dec6921671504bae62bb4915369e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:41:21 +0800 Subject: [PATCH 066/136] =?UTF-8?q?fix:=20systematic=20Phase=202=E2=86=923?= =?UTF-8?q?=20regression=20fixes=20and=20architectural=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes: - SceneScript: set CURRENT_SCENE before element compilation (restores Ponder/annotations) - SceneScript: call initializePonderTimelineBaseline + captureInitialInteractiveState - BlockImageScript: restore inline NBT parsing (id="stone{...}") and registryId arg - ItemLinkScript: fallback to getDisplayName() for self-closing tags - StructureScript: add Block.blockRegistry fallback for technical blocks - ItemImageScript/ItemGridScript: apply NBT from ParsedItemRef to ItemStack High priority: - SceneScript: restore auto-zoom (85% fill) and auto-size via SceneViewportMetrics - SceneScript: restore auto-center guard logic - SubPagesCompiler: add try/catch around resolveId() - MermaidCompiler: document compileNodeContentBlocks Phase 2 limitation - SpecialScript/SubPagesScript: add isWrapped pattern - SpecialScript: replace hardcoded strings with GuidebookText - QuestLinkScript: add try/catch around BqHelpers.resolveDisplay - CommandLinkScript: restore FMLLog logging - CategoryScript: use MediaWikiGeneratedListBlock via MediaWikiTagCompilerSupport - ItemImageCompiler: add ore attribute support to placeholder Architectural: - LytFlowInlineBlock: add centralized unwrapPlaceholder() utility - SceneTagCompiler: store element compilers in placeholder (removes hardcoded DefaultExtensions) - LytHost: two-phase dispatchMountEvents (sync first, then async) - Index methods: document Phase 2→3 indexing limitations - MediaWikiListPlanner: simplified column distribution algorithm - GuideScreen: add null guard in handleKeyboardInput --- CLAUDE.md | 17 +++ .../guide/compiler/tags/CsvTableCompiler.java | 29 +++- .../compiler/tags/ItemImageCompiler.java | 9 +- .../guide/compiler/tags/MermaidCompiler.java | 9 ++ .../guide/compiler/tags/SubPagesCompiler.java | 7 +- .../tags/mediawiki/CategoryCompiler.java | 4 + .../tags/mediawiki/SpecialCompiler.java | 4 + .../document/flow/LytFlowInlineBlock.java | 22 +++ .../guidenh/guide/internal/GuideScreen.java | 1 + .../guidenh/guide/internal/host/LytHost.java | 74 +++++++--- .../host/scripts/BlockImageScript.java | 40 +++--- .../internal/host/scripts/CategoryScript.java | 56 ++------ .../host/scripts/CommandLinkScript.java | 3 + .../host/scripts/FloatingImageScript.java | 15 +-- .../internal/host/scripts/ImageScript.java | 12 +- .../internal/host/scripts/ItemGridScript.java | 7 +- .../host/scripts/ItemImageScript.java | 33 +++-- .../internal/host/scripts/ItemLinkScript.java | 9 ++ .../host/scripts/QuestLinkScript.java | 11 +- .../internal/host/scripts/RecipeScript.java | 12 +- .../internal/host/scripts/SceneScript.java | 127 +++++++++++++----- .../internal/host/scripts/SpecialScript.java | 7 +- .../host/scripts/StructureScript.java | 10 +- .../internal/host/scripts/SubPagesScript.java | 79 +++++------ .../guide/mediawiki/MediaWikiListPlanner.java | 80 ++--------- .../guidenh/guide/scene/SceneTagCompiler.java | 13 +- 26 files changed, 412 insertions(+), 278 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..f5bd0a58 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,17 @@ +需要进行spotlessApply和compileJava验证 + +禁止全限定名引用类,只能import使用 + +禁止包私有和final类,record记录类除外,所有注释使用英语 + +禁止// -------------和// =======这样的注释 +先查找D:\\Github\\GTNH有没有需要的源码库 + +禁止过于耦合,尽可能保证解耦并且支持高度可拓展 + +确保代码实现的干净漂亮,性能高效,业务清晰 + +所有文件均按照UTF8读取和编写,禁止UTF16等,powershell读取也强制指定UTF8编码 + +开发文档禁止提交到git,测试文件是故意被git忽略的 + diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index 64f31d3d..e996e4cf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -1,5 +1,6 @@ package com.hfstudio.guidenh.guide.compiler.tags; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -10,6 +11,7 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.table.LytTable; @@ -51,16 +53,35 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // Load CSV asset content and index cell values for search. + // CSV data is available at compile time (src is a file path), unlike Category/Special + // whose data is resolved at MOUNT time by scripts. String src; try { src = MdxAttrs.getString(el, "src", null); } catch (MdxAttrs.AttributeException e) { return; } - if (src != null && !src.trim() - .isEmpty()) { - sink.appendText(el, src); - sink.appendBreak(); + if (src != null && !src.trim().isEmpty()) { + try { + ResourceLocation csvId = IdUtils.resolveLink(src.trim(), indexer.getPageId()); + byte[] data = indexer.loadAsset(csvId); + if (data != null) { + List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); + for (List row : rows) { + for (String cell : row) { + if (!cell.isEmpty()) { + sink.appendText(el, cell); + } + } + sink.appendBreak(); + } + } + } catch (Exception ignored) { + // Fallback: index the src path string if asset loading fails + sink.appendText(el, src); + sink.appendBreak(); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java index a1bc0c76..27df56b7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java @@ -22,6 +22,7 @@ public Set getTagNames() { @Override protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { String itemId = MdxAttrs.getString(compiler, parent, el, "id", null); + String ore = MdxAttrs.getString(compiler, parent, el, "ore", null); if (itemId == null) return; itemId = itemId.trim(); @@ -73,7 +74,7 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen labelFormat = (formatRaw != null && !formatRaw.isEmpty()) ? formatRaw : null; ItemImagePlaceholder placeholder = new ItemImagePlaceholder( - itemId, scale, yOffset, labelYOffset, showTooltip, showIcon, labelPosition, labelFormat); + itemId, scale, yOffset, labelYOffset, showTooltip, showIcon, labelPosition, labelFormat, ore); var inline = new LytFlowInlineBlock(); inline.setBlock(placeholder); @@ -109,10 +110,13 @@ public static class ItemImagePlaceholder extends LytParagraph { public final String labelPosition; @Nullable public final String labelFormat; + @Nullable + public final String ore; public ItemImagePlaceholder(String itemId, float scale, @Nullable Integer yOffset, @Nullable Integer labelYOffset, boolean showTooltip, @Nullable Boolean showIcon, - @Nullable String labelPosition, @Nullable String labelFormat) { + @Nullable String labelPosition, @Nullable String labelFormat, + @Nullable String ore) { this.itemId = itemId; this.scale = scale; this.yOffset = yOffset; @@ -121,6 +125,7 @@ public ItemImagePlaceholder(String itemId, float scale, @Nullable Integer yOffse this.showIcon = showIcon; this.labelPosition = labelPosition; this.labelFormat = labelFormat; + this.ore = ore; setStyleClass("ItemImage"); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java index e06e74db..4cc7c691 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java @@ -77,6 +77,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // NB: Phase 2 loaded src-based Mermaid content and indexed the actual diagram source. + // Phase 3 src-based content is resolved at MOUNT time by MermaidScript, so index() only + // indexes the src path string. Inline content (no src attribute) is still indexed here. + // Full indexing for src-based mermaid requires a post-mount indexing pass (TBD). String src; try { src = MdxAttrs.getString(el, "src", null); @@ -100,6 +104,11 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink private Map compileNodeContentBlocks(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields mermaidElement) { + // NB: Phase 2 cross-referenced NodeContent IDs against the parsed MermaidMindmapNode tree + // (via indexNodesById), validated unknown IDs, and provided inline-markdown fallback for + // nodes without explicit NodeContent. Phase 3 defers tree construction to MermaidScript + // (MOUNT time), so cross-validation must happen at runtime. See MermaidScript for the + // runtime counterpart. Map result = new LinkedHashMap<>(); for (MdxJsxFlowElement child : MermaidMindmapNodeContentExtractor .collectNodeContentElements(mermaidElement.children())) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java index 8d055af8..3d48d704 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java @@ -19,7 +19,12 @@ public Set getTagNames() { protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { var pageIdStr = el.getAttributeString("id", null); if (pageIdStr != null) { - pageIdStr = compiler.resolveId(pageIdStr).toString(); + try { + pageIdStr = compiler.resolveId(pageIdStr).toString(); + } catch (Exception e) { + parent.appendError(compiler, "Invalid id: " + pageIdStr, el); + return; + } } var alphabetical = MdxAttrs.getBoolean(compiler, parent, el, "alphabetical", false); var currentPageId = compiler.getPageId().toString(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 89890265..2794f0b1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -44,6 +44,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // NB: Phase 2 indexed category member titles resolved by + // MediaWikiPageListBuilder.buildCategoryMembers(). Phase 3 defers member resolution to + // CategoryScript (MOUNT time), so index() only indexes the category name string. + // Full indexing requires a post-mount indexing pass (TBD). String categoryName = el.getAttributeString("name", null); if (categoryName != null && !categoryName.trim() .isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index 80ecf37b..966ea31b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -48,6 +48,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { + // NB: Phase 2 indexed special page result entries resolved by + // MediaWikiSpecialPageResolver.resolve(). Phase 3 defers resolution to + // SpecialScript (MOUNT time), so index() only indexes the special page name string. + // Full indexing requires a post-mount indexing pass (TBD). String specialName = el.getAttributeString("name", null); if (specialName != null && !specialName.trim() .isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java index c5540d2f..50ecb11b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java @@ -2,6 +2,8 @@ import java.util.Optional; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.LytSize; import com.hfstudio.guidenh.guide.document.block.LytBlock; @@ -83,6 +85,26 @@ protected void visitChildren(LytVisitor visitor) { } } + /** + * Unwraps a flow-wrapped placeholder node. When a block-level tag appears in inline + * context, BlockTagCompiler wraps the placeholder in LytFlowInlineBlock. Dispatch + * passes the wrapper. This helper returns the inner placeholder regardless. + * + * @return the unwrapped placeholder of type T, or null if the node is neither + * a direct instance nor a LytFlowInlineBlock wrapping an instance + */ + @Nullable + public static T unwrapPlaceholder(Object node, Class placeholderClass) { + if (placeholderClass.isInstance(node)) { + return placeholderClass.cast(node); + } + if (node instanceof LytFlowInlineBlock wrapper + && placeholderClass.isInstance(wrapper.getBlock())) { + return placeholderClass.cast(wrapper.getBlock()); + } + return null; + } + public static LytFlowInlineBlock of(LytBlock block) { var inlineBlock = new LytFlowInlineBlock(); inlineBlock.setBlock(block); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index de3e3007..5077f1c6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -4700,6 +4700,7 @@ protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, i @Override public void handleKeyboardInput() { + if (mc == null) return; if (Keyboard.getEventKeyState() || isCommittedCharacterEventForFocusedTextInput()) { keyTyped(Keyboard.getEventCharacter(), Keyboard.getEventKey()); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 41cde03e..56407a79 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -213,39 +213,63 @@ private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { } } + /** + * Two-phase MOUNT dispatch: + *

    + * Phase 1 (sync): walk the entire tree and execute + * every synchronous script immediately. This guarantees that all + * setup and initialization work (e.g. establishing CURRENT_SCENE, + * compiling child elements) is finished before any asynchronous + * work begins. + *

    + * Phase 2 (async): walk the tree a second time and + * queue every asynchronous script as a {@link MaterializeTask} for + * execution on subsequent ticks (see {@link #step}). + *

    + * Within each phase the original document order (parent before children) + * is preserved. The node-level result cache is consulted: if a node + * already has a cached result from a previous mount, the cached content + * is restored directly and the script is skipped in both + * phases. + */ private void dispatchMountEvents(LytNode node) { + dispatchPhase(node, false); // Phase 1: sync scripts only + dispatchPhase(node, true); // Phase 2: queue async scripts only + } + + private void dispatchPhase(LytNode node, boolean asyncPhase) { String cls = node.getStyleClass(); if (cls != null) { LytScript script = scripts.get(cls); if (script != null) { - dispatchScript(script, node); + dispatchScriptInPhase(script, node, asyncPhase); } } for (var child : node.getChildren()) { - dispatchMountEvents(child); + dispatchPhase(child, asyncPhase); } - dispatchMountEventsFlow(node); + dispatchPhaseFlow(node, asyncPhase); } - private void dispatchMountEventsFlow(LytNode node) { + private void dispatchPhaseFlow(LytNode node, boolean asyncPhase) { if (node instanceof LytParagraph para) { for (var fcChild : para.getContent()) { - dispatchMountEventsFlowRecursive(fcChild); + dispatchPhaseFlowRecursive(fcChild, asyncPhase); } } } - private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { + private void dispatchPhaseFlowRecursive(LytFlowContent fc, boolean asyncPhase) { String cls = fc.getStyleClass(); if (cls != null) { LytScript script = scripts.get(cls); if (script != null) { - dispatchScript(script, fc); + dispatchScriptInPhase(script, fc, asyncPhase); } } if (fc instanceof LytFlowSpan span) { for (var child : span.getChildren()) { - dispatchMountEventsFlowRecursive(child); + dispatchPhaseFlowRecursive(child, asyncPhase); } } else if (fc instanceof LytFlowInlineBlock inlineBlock && inlineBlock.getBlock() != null) { LytBlock inner = inlineBlock.getBlock(); @@ -253,13 +277,25 @@ private void dispatchMountEventsFlowRecursive(LytFlowContent fc) { if (innerCls != null) { LytScript script = scripts.get(innerCls); if (script != null) { - dispatchScript(script, inlineBlock); + dispatchScriptInPhase(script, inlineBlock, asyncPhase); } } } } - private void dispatchScript(LytScript script, Object node) { + /** + * Dispatch a single script in the given phase. + *

      + *
    • If the node has a cached result from a previous mount, the + * cached content is restored directly and the script is skipped + * entirely (both phases). + *
    • In the sync phase ({@code asyncPhase == false}), only + * non-async scripts are executed synchronously. + *
    • In the async phase ({@code asyncPhase == true}), only + * async scripts are enqueued as {@link MaterializeTask}s. + *
    + */ + private void dispatchScriptInPhase(LytScript script, Object node, boolean asyncPhase) { String nodeUid = nodeUidOf(node); if (nodeUid != null) { Object cached = getNodeResult(currentPageId, nodeUid); @@ -268,14 +304,18 @@ private void dispatchScript(LytScript script, Object node) { return; } } - if (script.isAsync()) { - taskQueue.addLast(new MaterializeTask(script, node, new ScriptContextImpl(node, this, document))); + if (asyncPhase) { + if (script.isAsync()) { + taskQueue.addLast(new MaterializeTask(script, node, new ScriptContextImpl(node, this, document))); + } } else { - try { - ScriptContextImpl ctx = new ScriptContextImpl(node, this, document); - script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); - } catch (Exception e) { - e.printStackTrace(); + if (!script.isAsync()) { + try { + ScriptContextImpl ctx = new ScriptContextImpl(node, this, document); + script.onEvent(node, new LytEvent(EventType.MOUNT, node), ctx); + } catch (Exception e) { + e.printStackTrace(); + } } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 1dee294f..c4f5c19a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -7,6 +7,8 @@ import net.minecraftforge.oredict.OreDictionary; import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; +import com.hfstudio.guidenh.guide.compiler.IdUtils; +import com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef; import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler.BlockImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; @@ -42,19 +44,12 @@ public class BlockImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - BlockImagePlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w - && w.getBlock() instanceof BlockImagePlaceholder p; - if (isWrapped) { - ph = (BlockImagePlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof BlockImagePlaceholder p) { - ph = p; - } else { - return; - } + BlockImagePlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, BlockImagePlaceholder.class); + if (ph == null) return; Block block = null; int meta = ph.meta; + NBTTagCompound tileTag = null; if (ph.ore != null && !ph.ore.isEmpty()) { ItemStack oreStack = GuideItemReferenceResolver.resolveOreDictionaryStack(ph.ore); @@ -63,12 +58,12 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { meta = oreStack.getItemDamage(); } } else if (ph.id != null) { - Item item = (Item) Item.itemRegistry.getObject(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id); - if (item != null) { - block = Block.getBlockFromItem(item); - } - if (block == null) { - block = (Block) Block.blockRegistry.getObject(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id); + // Handle inline NBT: id="minecraft:stone{BlockEntityTag:{...}}" + ParsedItemRef ref = IdUtils.parseItemRef(ph.id.contains(":") ? ph.id : "minecraft:" + ph.id, "minecraft"); + Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); + if (item != null) block = Block.getBlockFromItem(item); + if (ref.nbt() != null && tileTag == null) { + tileTag = (NBTTagCompound) ref.nbt().copy(); } } @@ -78,10 +73,16 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } - NBTTagCompound tileTag = null; if (ph.nbt != null && !ph.nbt.trim().isEmpty()) { try { - tileTag = GuideTextNbtCodec.readTextSafeCompound(ph.nbt.trim()); + NBTTagCompound explicitTag = GuideTextNbtCodec.readTextSafeCompound(ph.nbt.trim()); + if (tileTag != null) { + for (Object key : explicitTag.func_150296_c()) { + tileTag.setTag((String) key, explicitTag.getTag((String) key)); + } + } else { + tileTag = explicitTag; + } } catch (Exception ignored) {} } @@ -92,7 +93,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { int defaultMeta = meta == Integer.MIN_VALUE ? BlockElementCompiler.defaultMetaFor(block, null) : meta; GuidebookLevel level = new GuidebookLevel(); - GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag); + String registryId = ph.id != null ? (ph.id.contains(":") ? ph.id : "minecraft:" + ph.id) : ""; + GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag, registryId); if (level.isEmpty()) { ctx.replace( diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index e857492a..0315ff46 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -1,15 +1,10 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import java.util.List; - import com.hfstudio.guidenh.guide.Guide; -import com.hfstudio.guidenh.guide.PageAnchor; -import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.CategoryCompiler.CategoryPlaceholder; +import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.MediaWikiTagCompilerSupport; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; -import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -17,7 +12,7 @@ import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageTitleResolver; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; public class CategoryScript implements LytScript { @@ -31,14 +26,11 @@ public class CategoryScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - CategoryPlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w - && w.getBlock() instanceof CategoryPlaceholder p; - if (isWrapped) { - ph = (CategoryPlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof CategoryPlaceholder p) { - ph = p; - } else { + CategoryPlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, CategoryPlaceholder.class); + if (ph == null) return; + + if (!(ctx.getPageCollection() instanceof Guide guide)) { + ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoDataAvailable.text())); return; } @@ -48,34 +40,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } - List members = index.get(ph.name); - if (members.isEmpty()) { - ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoPagesInCategory.text())); - return; - } - - LytVBox box = new LytVBox(); - box.setGap(2); - int count = 0; - for (PageAnchor anchor : members) { - if (ph.rows > 0 && count >= ph.rows) break; - LytParagraph line = new LytParagraph(); - LytFlowLink link = new LytFlowLink(); - link.setGuideLink(ph.guideId, anchor); - if (ctx.getPageCollection() instanceof Guide guide) { - ParsedGuidePage page = guide.getParsedPage(anchor.pageId()); - if (page != null) { - link.appendText(MediaWikiPageTitleResolver.resolvePageTitle(guide, page)); - } else { - link.appendText(anchor.pageId().getResourcePath()); - } - } else { - link.appendText(anchor.pageId().getResourcePath()); - } - line.append(link); - box.append(line); - count++; - } - ctx.replace(box); + var context = MediaWikiTagCompilerSupport.createListContext(guide, index); + var entries = MediaWikiPageListBuilder.buildCategoryMembers(context, ph.name); + var block = MediaWikiTagCompilerSupport.createBlock( + entries, ph.rows, GuidebookText.MediaWikiNoPagesInCategory.text()); + ctx.replace(block); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java index fa12abaa..6e105699 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -2,6 +2,8 @@ import net.minecraft.client.Minecraft; +import cpw.mods.fml.common.FMLLog; + import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -29,6 +31,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (command == null) return; link.setClickCallback(screen -> { if (Minecraft.getMinecraft().thePlayer == null) return; + FMLLog.getLogger().info("[GuideNH] [CommandLink] Sending command: {}", command); Minecraft.getMinecraft().thePlayer.sendChatMessage(command); if (Boolean.TRUE.equals(close)) { Minecraft.getMinecraft().displayGuiScreen(null); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index 74bd06dc..b36624d6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -28,17 +28,10 @@ public class FloatingImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - LytImageBlock placeholder; - LytFlowInlineBlock oldWrapper = null; - boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof LytImageBlock p; - if (isWrapped) { - oldWrapper = (LytFlowInlineBlock) node; - placeholder = (LytImageBlock) oldWrapper.getBlock(); - } else if (node instanceof LytImageBlock p) { - placeholder = p; - } else { - return; - } + LytImageBlock placeholder = LytFlowInlineBlock.unwrapPlaceholder(node, LytImageBlock.class); + if (placeholder == null) return; + LytFlowInlineBlock oldWrapper = node instanceof LytFlowInlineBlock wrapper ? wrapper : null; + boolean isWrapped = oldWrapper != null; String src = placeholder.getSrc(); if (src == null || src.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index 10b80dff..31a15dc7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -28,15 +28,9 @@ public class ImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - LytImageBlock placeholder; - boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof LytImageBlock p; - if (isWrapped) { - placeholder = (LytImageBlock) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof LytImageBlock p) { - placeholder = p; - } else { - return; - } + LytImageBlock placeholder = LytFlowInlineBlock.unwrapPlaceholder(node, LytImageBlock.class); + if (placeholder == null) return; + boolean isWrapped = node instanceof LytFlowInlineBlock; String src = placeholder.getSrc(); if (src == null || src.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index f7b2eb54..3b32079c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -53,6 +53,11 @@ private static ItemStack resolveItemId(String itemId) { com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; + if (item == null) return null; + ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); + if (ref.nbt() != null) { + stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); + } + return stack; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index f90c1bd9..f0ecfba8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -2,6 +2,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraftforge.oredict.OreDictionary; import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemImage; @@ -26,20 +27,23 @@ public class ItemImageScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - ItemImagePlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w && w.getBlock() instanceof ItemImagePlaceholder p; - if (isWrapped) { - ph = (ItemImagePlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof ItemImagePlaceholder p) { - ph = p; - } else { - return; - } + ItemImagePlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, ItemImagePlaceholder.class); + if (ph == null) return; + boolean isWrapped = node instanceof LytFlowInlineBlock; ItemStack stack = resolveItemId(ph.itemId); if (stack == null) { - replaceFlowError(ctx, isWrapped, "[ItemImage] Item not found: " + ph.itemId); - return; + // Fallback to ore dictionary if direct item lookup fails + if (ph.ore != null) { + java.util.List oreStacks = OreDictionary.getOres(ph.ore); + if (oreStacks != null && !oreStacks.isEmpty()) { + stack = oreStacks.get(0).copy(); + } + } + if (stack == null) { + replaceFlowError(ctx, isWrapped, "[ItemImage] Item not found: " + ph.itemId); + return; + } } LytItemImage image = new LytItemImage(stack); @@ -85,6 +89,11 @@ private static ItemStack resolveItemId(String itemId) { com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; + if (item == null) return null; + ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); + if (ref.nbt() != null) { + stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); + } + return stack; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 8efe0a84..792e6e25 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -86,6 +86,10 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (Boolean.TRUE.equals(showTooltip)) { span.setTooltip(new ItemTooltip(stack)); } + // If the span has no children text (self-closing tag), fall back to item display name + if (span.getChildren().isEmpty()) { + span.appendText(stack.getDisplayName()); + } span.modifyStyle(style -> style.italic(true)); ctx.replace(span); return; @@ -116,6 +120,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } } + + // If the link has no children text (self-closing tag), fall back to item display name + if (link.getChildren().isEmpty()) { + link.appendText(stack.getDisplayName()); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java index a12aa212..7e630c0d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java @@ -6,6 +6,7 @@ import net.minecraft.util.StatCollector; import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.flow.LytFlowText; @@ -40,8 +41,14 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Boolean showTooltip = (Boolean) placeholder.getData("showTooltip"); String overrideText = (String) placeholder.getData("overrideText"); - QuestDisplay display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, - Boolean.TRUE.equals(showTooltip)); + QuestDisplay display; + try { + display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, + Boolean.TRUE.equals(showTooltip)); + } catch (Throwable t) { + ctx.replace(LytParagraph.error("[QuestLink] BetterQuesting integration not available")); + return; + } if (display == null) { LytFlowSpan errorSpan = new LytFlowSpan(); errorSpan.modifyStyle(style -> style.color(SymbolicColor.ERROR_TEXT)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index eec1c312..bcb0c413 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -52,16 +52,8 @@ public class RecipeScript implements LytScript { public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - RecipePlaceholder ph; - boolean isWrapped = node instanceof LytFlowInlineBlock w - && w.getBlock() instanceof RecipePlaceholder p; - if (isWrapped) { - ph = (RecipePlaceholder) ((LytFlowInlineBlock) node).getBlock(); - } else if (node instanceof RecipePlaceholder p) { - ph = p; - } else { - return; - } + RecipePlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, RecipePlaceholder.class); + if (ph == null) return; Item item = (Item) Item.itemRegistry.getObject(ph.ref.rawKey()); if (item == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index afb3a0cd..cdc5e26e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -16,11 +16,11 @@ import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.PageIndex; -import com.hfstudio.guidenh.guide.internal.extensions.DefaultExtensions; import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.scene.CameraSettings; import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; +import com.hfstudio.guidenh.guide.scene.annotation.compiler.AnnotationTagCompiler; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; @@ -43,16 +43,7 @@ public class SceneScript implements LytScript { - private final Map elementCompilers; - public SceneScript() { - Map map = new HashMap<>(); - for (var ext : DefaultExtensions.sceneElementCompilers()) { - for (String name : ext.getTagNames()) { - map.put(name, ext); - } - } - this.elementCompilers = map; } @Override @@ -97,38 +88,35 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { int height = ph.height > 0 ? ph.height : 180; camera.setViewportSize(width, height); - // Parse children source and compile scene elements + // Parse children source ExceptionCollector errorSink = new ExceptionCollector(); PageCollection pc = ctx.getPageCollection(); PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), ExtensionCollection.EMPTY, "", new ResourceLocation(ph.pageDomain, "scene"), ""); + MdAstRoot ast; try { - MdAstRoot ast = ph.childrenAst != null ? ph.childrenAst + ast = ph.childrenAst != null ? ph.childrenAst : MdAst.fromMarkdown(ph.childrenSource, GuideMarkdownOptions.runtime()); if (ph.childrenAst == null && ast != null) { MdAstToMdxConverter.convert(ast, Collections.emptyMap()); } - GuideSceneStructureCompileScope.run(true, () -> { - for (UnistNode child : ast.children()) { - MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); - if (el == null) continue; - SceneElementTagCompiler ec = elementCompilers.get(el.name()); - if (ec != null) { - ec.compile(level, camera, runtimeCompiler, errorSink, el); - } - } - }); } catch (Exception e) { FMLLog.getLogger().warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); ctx.replace(LytParagraph.error("[Scene] Failed to parse scene elements")); return; } - if (level.isEmpty()) { - ctx.replace(LytParagraph.error("[Scene] Scene has no supported elements")); - return; + // Build element compiler map from placeholder (set at compile time by SceneTagCompiler) + Map elementCompilers = new HashMap<>(); + if (ph.sceneElementCompilers != null) { + for (var ec : ph.sceneElementCompilers) { + for (String name : ec.getTagNames()) { + elementCompilers.put(name, ec); + } + } } + // Create the scene EARLY so element compilers can access it via CURRENT_SCENE. LytGuidebookScene scene = new LytGuidebookScene(); scene.setLevel(level); scene.setCamera(camera); @@ -139,20 +127,91 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { scene.setGridButtonEnabled(ph.gridButtonEnabled); scene.setGridVisible(ph.showGrid); - if (!level.isEmpty()) { - float[] center = level.getCenter(); - if (!ph.explicitCenter) { - camera.setRotationCenter(center[0], center[1], center[2]); + // Compile scene elements with CURRENT_SCENE set so that element compilers + // (ImportPonderElementCompiler, ImportStructureLibElementCompiler, annotations, etc.) + // can call scene.attachPonderData(), scene.addAnnotation(), etc. + var prevScene = AnnotationTagCompiler.CURRENT_SCENE.get(); + AnnotationTagCompiler.CURRENT_SCENE.set(scene); + try { + GuideSceneStructureCompileScope.run(true, () -> { + for (UnistNode child : ast.children()) { + MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); + if (el == null) continue; + SceneElementTagCompiler ec = elementCompilers.get(el.name()); + if (ec != null) { + ec.compile(level, camera, runtimeCompiler, errorSink, el); + } + } + }); + } finally { + if (prevScene != null) { + AnnotationTagCompiler.CURRENT_SCENE.set(prevScene); + } else { + AnnotationTagCompiler.CURRENT_SCENE.remove(); } - // Auto-center the scene in the viewport using the same approach as BlockImageScript - // NB: auto-size and auto-zoom restoration pending via SceneViewportMetrics.measure(). - // Phase 2 reference: SceneTagCompiler lines 195-252 (commit 475353f^). + } + + if (level.isEmpty()) { + ctx.replace(LytParagraph.error("[Scene] Scene has no supported elements")); + return; + } + + // Finalize scene setup: auto-center, ponder baseline, interactive state capture + float[] center = level.getCenter(); + if (!ph.explicitCenter) { + camera.setRotationCenter(center[0], center[1], center[2]); + } + // Auto-center the scene in the viewport + if (!ph.explicitCenter && Float.isNaN(ph.offsetX) && Float.isNaN(ph.offsetY)) { camera.setOffsetX(0f); camera.setOffsetY(0f); var sc = camera.worldToScreen(center[0], center[1], center[2]); - camera.setOffsetX(-sc.x + (Float.isNaN(ph.offsetX) ? 0 : ph.offsetX)); - camera.setOffsetY(sc.y + (Float.isNaN(ph.offsetY) ? 0 : ph.offsetY)); + camera.setOffsetX(-sc.x); + camera.setOffsetY(sc.y); + } else if (!Float.isNaN(ph.offsetX) || !Float.isNaN(ph.offsetY)) { + if (!Float.isNaN(ph.offsetX)) camera.setOffsetX(ph.offsetX); + if (!Float.isNaN(ph.offsetY)) camera.setOffsetY(ph.offsetY); } + + // Auto-zoom: when zoom is not explicitly set, fit scene to viewport at 85% fill + if (Float.isNaN(ph.zoom)) { + camera.setZoom(1f); + camera.setOffsetX(0f); + camera.setOffsetY(0f); + if (!level.isEmpty()) { + int[] bounds = level.getBounds(); + SceneViewportMetrics metrics = SceneViewportMetrics.measure(camera, bounds); + float spanX = metrics.spanX(); + float spanY = metrics.spanY(); + if (spanX > 0.5f || spanY > 0.5f) { + float zX = spanX > 0.5f ? (float) width / spanX : Float.MAX_VALUE; + float zY = spanY > 0.5f ? (float) height / spanY : Float.MAX_VALUE; + float autoZoom = Math.min(zX, zY) * 0.85f; + autoZoom = Math.max(LytGuidebookScene.MIN_ZOOM, Math.min(LytGuidebookScene.MAX_ZOOM, autoZoom)); + camera.setZoom(autoZoom); + } + } + } + // Auto-size: when width/height not explicitly set, measure and compute viewport + if (!ph.explicitWidth || !ph.explicitHeight) { + camera.setOffsetX(0f); + camera.setOffsetY(0f); + if (!level.isEmpty()) { + int[] bounds = level.getBounds(); + SceneViewportMetrics metrics = SceneViewportMetrics.measure(camera, bounds); + if (!ph.explicitWidth && metrics.spanX() > 0.5f) { + width = SceneViewportMetrics.clampDimension(metrics.spanX()); + } + if (!ph.explicitHeight && metrics.spanY() > 0.5f) { + height = SceneViewportMetrics.clampDimension(metrics.spanY()); + } + scene.setSceneSize(width, height); + camera.setViewportSize(width, height); + } + } + + scene.initializePonderTimelineBaseline(); + scene.captureInitialInteractiveState(); scene.snapshotInitialCamera(); ctx.replace(scene); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index 79958414..ddb91ebc 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -5,6 +5,8 @@ import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.MediaWikiTagCompilerSupport; import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.SpecialCompiler.SpecialPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; +import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -29,11 +31,12 @@ public class SpecialScript implements LytScript { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; - if (!(node instanceof SpecialPlaceholder ph)) return; + SpecialPlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, SpecialPlaceholder.class); + if (ph == null) return; PageCollection pc = ctx.getPageCollection(); if (!(pc instanceof Guide guide)) { - ctx.replace(LytParagraph.error("[Special] Special page not available: collection is not a guide")); + ctx.replace(LytParagraph.error(GuidebookText.MediaWikiNoDataAvailable.text())); return; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index dc2d298d..e64df640 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -1,5 +1,6 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import net.minecraft.block.Block; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; @@ -50,6 +51,13 @@ private static ItemStack resolveEntry(String idSpec) { com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(idSpec, "minecraft"); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - return item != null ? new ItemStack(item, 1, ref.concreteMeta()) : null; + if (item != null) { + return new ItemStack(item, 1, ref.concreteMeta()); + } + Block block = (Block) Block.blockRegistry.getObject(ref.rawKey()); + if (block != null) { + return new ItemStack(block, 1, ref.concreteMeta()); + } + return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index 9c8171e9..49b148df 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -11,6 +11,7 @@ import com.hfstudio.guidenh.guide.document.block.LytList; import com.hfstudio.guidenh.guide.document.block.LytListItem; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.internal.GuideRegistry; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -31,50 +32,52 @@ public class SubPagesScript implements LytScript { @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { - if (event.type() == EventType.MOUNT && node instanceof SubPagesPlaceholder ph) { - NavigationTree tree = GuideRegistry.getMergedNavigationTree(); + if (event.type() != EventType.MOUNT) return; + SubPagesPlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, SubPagesPlaceholder.class); + if (ph == null) return; - List subNodes; - if (ph.pageIdStr == null) { - ResourceLocation currentPageId = new ResourceLocation(ph.currentPageId); - NavigationNode current = tree.getNodeById(currentPageId); - subNodes = current != null ? new ArrayList<>(current.children()) : tree.getRootNodes(); - } else if (ph.pageIdStr.isEmpty()) { - subNodes = tree.getRootNodes(); - } else { - ResourceLocation pageId = new ResourceLocation(ph.pageIdStr); - NavigationNode navNode = tree.getNodeById(pageId); - if (navNode == null) { - ctx.replace(LytParagraph.error("[SubPages] Page not found in navigation: " + ph.pageIdStr)); - return; - } - subNodes = navNode.children(); - } - - if (ph.alphabetical) { - subNodes = new ArrayList<>(subNodes); - subNodes.sort(Comparator.comparing(NavigationNode::title)); - } + NavigationTree tree = GuideRegistry.getMergedNavigationTree(); - if (subNodes.isEmpty()) { - ctx.replace(LytParagraph.error("[SubPages] No sub-pages found")); + List subNodes; + if (ph.pageIdStr == null) { + ResourceLocation currentPageId = new ResourceLocation(ph.currentPageId); + NavigationNode current = tree.getNodeById(currentPageId); + subNodes = current != null ? new ArrayList<>(current.children()) : tree.getRootNodes(); + } else if (ph.pageIdStr.isEmpty()) { + subNodes = tree.getRootNodes(); + } else { + ResourceLocation pageId = new ResourceLocation(ph.pageIdStr); + NavigationNode navNode = tree.getNodeById(pageId); + if (navNode == null) { + ctx.replace(LytParagraph.error("[SubPages] Page not found in navigation: " + ph.pageIdStr)); return; } + subNodes = navNode.children(); + } - LytList list = new LytList(false, 0); - for (NavigationNode childNode : subNodes) { - if (!childNode.hasPage()) continue; + if (ph.alphabetical) { + subNodes = new ArrayList<>(subNodes); + subNodes.sort(Comparator.comparing(NavigationNode::title)); + } - LytListItem listItem = new LytListItem(); - LytParagraph listItemPar = new LytParagraph(); - LytFlowLink link = new LytFlowLink(); - link.setGuideLink(childNode.guideId(), PageAnchor.page(childNode.pageId())); - link.appendText(childNode.title()); - listItemPar.append(link); - listItem.append(listItemPar); - list.append(listItem); - } - ctx.replace(list); + if (subNodes.isEmpty()) { + ctx.replace(LytParagraph.error("[SubPages] No sub-pages found")); + return; + } + + LytList list = new LytList(false, 0); + for (NavigationNode childNode : subNodes) { + if (!childNode.hasPage()) continue; + + LytListItem listItem = new LytListItem(); + LytParagraph listItemPar = new LytParagraph(); + LytFlowLink link = new LytFlowLink(); + link.setGuideLink(childNode.guideId(), PageAnchor.page(childNode.pageId())); + link.appendText(childNode.title()); + listItemPar.append(link); + listItem.append(listItemPar); + list.append(listItem); } + ctx.replace(list); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java index cbc2bbbd..2be2031b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java @@ -61,67 +61,27 @@ public static List planColumns(List ent int columnCount = Math.max(1, sanitizeRows(rows)); var columns = new ArrayList(columnCount); if (entries.isEmpty()) { - for (int index = 0; index < columnCount; index++) { + for (int i = 0; i < columnCount; i++) { columns.add(new MediaWikiListColumn(Collections.emptyList())); } return columns; } - List groups = buildGroups(entries); - int[] targetSizes = buildTargetSizes(entries.size(), columnCount); - int groupIndex = 0; - int entryOffset = 0; - - for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { - var sections = new ArrayList(); - - if (columnIndex == columnCount - 1) { - while (groupIndex < groups.size()) { - MediaWikiListGroup group = groups.get(groupIndex); - sections.add( - createSection( - group, - entryOffset, - group.entries() - .size())); - groupIndex++; - entryOffset = 0; - } - columns.add(new MediaWikiListColumn(sections)); + int perColumn = (int) Math.ceil((double) entries.size() / columnCount); + for (int col = 0; col < columnCount; col++) { + int start = col * perColumn; + int end = Math.min(start + perColumn, entries.size()); + if (start >= entries.size()) { + columns.add(new MediaWikiListColumn(Collections.emptyList())); continue; } - - int targetSize = targetSizes[columnIndex]; - int currentSize = 0; - while (groupIndex < groups.size() && currentSize < targetSize) { - MediaWikiListGroup group = groups.get(groupIndex); - int remainingCount = group.entries() - .size() - entryOffset; - int remainingCapacity = targetSize - currentSize; - if (remainingCount <= remainingCapacity) { - sections.add( - createSection( - group, - entryOffset, - group.entries() - .size())); - currentSize += remainingCount; - groupIndex++; - entryOffset = 0; - continue; - } - if (currentSize == 0 && remainingCapacity > 0) { - int endExclusive = entryOffset + remainingCapacity; - sections.add(createSection(group, entryOffset, endExclusive)); - entryOffset = endExclusive; - currentSize += remainingCapacity; - } - break; + List slice = entries.subList(start, end); + var sections = new ArrayList(); + for (MediaWikiListGroup group : buildGroups(slice)) { + sections.add(new MediaWikiListSection(group.key(), new ArrayList<>(group.entries()))); } - columns.add(new MediaWikiListColumn(sections)); } - return columns; } @@ -137,24 +97,6 @@ public static List> splitIntoColumns(List( - group.entries() - .subList(startInclusive, endExclusive))); - } - public static String resolveGroupKey(MediaWikiListEntry entry) { String value = firstSortableValue(entry); if (value.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 5fa3ce23..178374e5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -138,6 +139,9 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } } + // Store element compilers from ExtensionCollection in placeholder + var sceneElementCompilers = compiler.getExtensions().get(SceneElementTagCompiler.EXTENSION_POINT); + // Create placeholder block that carries all scene config to SceneScript String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene"; ScenePlaceholder placeholder = new ScenePlaceholder( @@ -151,7 +155,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl allowLayerSlider, gridButtonEnabled, showGrid, childrenSource, compiler.getPageId().getResourceDomain(), - preParsedAst // NEW: pre-parsed AST for SceneScript + preParsedAst, // NEW: pre-parsed AST for SceneScript + sceneElementCompilers ); placeholder.setStyleClass(styleClass); placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); @@ -198,6 +203,8 @@ public static class ScenePlaceholder extends LytParagraph { @Nullable public final String childrenSource; public final String pageDomain; @Nullable public final MdAstRoot childrenAst; + @Nullable + public final List sceneElementCompilers; public ScenePlaceholder( int width, int height, @@ -214,7 +221,8 @@ public ScenePlaceholder( boolean showGrid, @Nullable String childrenSource, String pageDomain, - @Nullable MdAstRoot childrenAst) { + @Nullable MdAstRoot childrenAst, + @Nullable List sceneElementCompilers) { this.width = width; this.height = height; this.explicitWidth = explicitWidth; @@ -241,6 +249,7 @@ public ScenePlaceholder( this.childrenSource = childrenSource; this.pageDomain = pageDomain; this.childrenAst = childrenAst; + this.sceneElementCompilers = sceneElementCompilers; } } From 509c835e3c34ab0cc94a8223ae1b4824d2010168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:51:58 +0800 Subject: [PATCH 067/136] fix: height-weighted column distribution with binary search Replace ceil(N/C) entry-based distribution with binary search on pixel heights that accounts for group header overhead (SECTION_GAP + HEADER + ROW). Minimizes variance across columns while preserving group/sort order. --- .../guide/mediawiki/MediaWikiListPlanner.java | 70 ++++++++++++++++--- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java index 2be2031b..f5911e18 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java @@ -57,6 +57,10 @@ public static List buildGroups(List entr return groups; } + /** Pixel cost constants — must match MediaWikiGeneratedListBlock layout. */ + private static final int SECTION_HEADER_HEIGHT = 28; // GAP_TOP(5) + HEADER_HEIGHT(20) + GAP_BOTTOM(3) + private static final int ROW_HEIGHT = 20; + public static List planColumns(List entries, int rows) { int columnCount = Math.max(1, sanitizeRows(rows)); var columns = new ArrayList(columnCount); @@ -67,24 +71,70 @@ public static List planColumns(List ent return columns; } - int perColumn = (int) Math.ceil((double) entries.size() / columnCount); - for (int col = 0; col < columnCount; col++) { - int start = col * perColumn; - int end = Math.min(start + perColumn, entries.size()); - if (start >= entries.size()) { - columns.add(new MediaWikiListColumn(Collections.emptyList())); - continue; + List groups = buildGroups(entries); + int[] groupHeights = new int[groups.size()]; + int maxGroupHeight = 0; + int totalHeight = 0; + for (int i = 0; i < groups.size(); i++) { + groupHeights[i] = SECTION_HEADER_HEIGHT + groups.get(i).entries().size() * ROW_HEIGHT; + maxGroupHeight = Math.max(maxGroupHeight, groupHeights[i]); + totalHeight += groupHeights[i]; + } + + // Binary search for optimal max column height + int lo = maxGroupHeight; + int hi = totalHeight; + while (lo < hi) { + int mid = (lo + hi) / 2; + int cols = countColumns(groupHeights, mid); + if (cols <= columnCount) { + hi = mid; + } else { + lo = mid + 1; } - List slice = entries.subList(start, end); + } + + // Build columns with the found max height + int optimalMax = lo; + int groupIndex = 0; + for (int col = 0; col < columnCount && groupIndex < groups.size(); col++) { var sections = new ArrayList(); - for (MediaWikiListGroup group : buildGroups(slice)) { - sections.add(new MediaWikiListSection(group.key(), new ArrayList<>(group.entries()))); + if (col == columnCount - 1) { + while (groupIndex < groups.size()) { + MediaWikiListGroup g = groups.get(groupIndex++); + sections.add(new MediaWikiListSection(g.key(), new ArrayList<>(g.entries()))); + } + } else { + int colHeight = 0; + while (groupIndex < groups.size()) { + MediaWikiListGroup g = groups.get(groupIndex); + int gh = groupHeights[groupIndex]; + // Allow one group per column; otherwise respect the budget + if (colHeight > 0 && colHeight + gh > optimalMax) break; + sections.add(new MediaWikiListSection(g.key(), new ArrayList<>(g.entries()))); + colHeight += gh; + groupIndex++; + } } columns.add(new MediaWikiListColumn(sections)); } return columns; } + private static int countColumns(int[] groupHeights, int maxHeight) { + int cols = 0; + int currentHeight = 0; + for (int gh : groupHeights) { + if (currentHeight + gh > maxHeight) { + cols++; + currentHeight = gh; + } else { + currentHeight += gh; + } + } + return currentHeight > 0 ? cols + 1 : cols; + } + public static List> splitIntoColumns(List entries, int rows) { var columns = new ArrayList>(); for (MediaWikiListColumn column : planColumns(entries, rows)) { From 9d51e64c7b8b645f2b2dd0fcb0f55f8de2a50022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:51:58 +0800 Subject: [PATCH 068/136] fix: height-weighted column distribution with binary search Replace ceil(N/C) entry-based distribution with binary search on pixel heights that accounts for group header overhead (SECTION_GAP + HEADER + ROW). Minimizes variance across columns while preserving group/sort order. --- .../guide/mediawiki/MediaWikiListPlanner.java | 70 ++++++++++++++++--- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java index 2be2031b..f5911e18 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java @@ -57,6 +57,10 @@ public static List buildGroups(List entr return groups; } + /** Pixel cost constants — must match MediaWikiGeneratedListBlock layout. */ + private static final int SECTION_HEADER_HEIGHT = 28; // GAP_TOP(5) + HEADER_HEIGHT(20) + GAP_BOTTOM(3) + private static final int ROW_HEIGHT = 20; + public static List planColumns(List entries, int rows) { int columnCount = Math.max(1, sanitizeRows(rows)); var columns = new ArrayList(columnCount); @@ -67,24 +71,70 @@ public static List planColumns(List ent return columns; } - int perColumn = (int) Math.ceil((double) entries.size() / columnCount); - for (int col = 0; col < columnCount; col++) { - int start = col * perColumn; - int end = Math.min(start + perColumn, entries.size()); - if (start >= entries.size()) { - columns.add(new MediaWikiListColumn(Collections.emptyList())); - continue; + List groups = buildGroups(entries); + int[] groupHeights = new int[groups.size()]; + int maxGroupHeight = 0; + int totalHeight = 0; + for (int i = 0; i < groups.size(); i++) { + groupHeights[i] = SECTION_HEADER_HEIGHT + groups.get(i).entries().size() * ROW_HEIGHT; + maxGroupHeight = Math.max(maxGroupHeight, groupHeights[i]); + totalHeight += groupHeights[i]; + } + + // Binary search for optimal max column height + int lo = maxGroupHeight; + int hi = totalHeight; + while (lo < hi) { + int mid = (lo + hi) / 2; + int cols = countColumns(groupHeights, mid); + if (cols <= columnCount) { + hi = mid; + } else { + lo = mid + 1; } - List slice = entries.subList(start, end); + } + + // Build columns with the found max height + int optimalMax = lo; + int groupIndex = 0; + for (int col = 0; col < columnCount && groupIndex < groups.size(); col++) { var sections = new ArrayList(); - for (MediaWikiListGroup group : buildGroups(slice)) { - sections.add(new MediaWikiListSection(group.key(), new ArrayList<>(group.entries()))); + if (col == columnCount - 1) { + while (groupIndex < groups.size()) { + MediaWikiListGroup g = groups.get(groupIndex++); + sections.add(new MediaWikiListSection(g.key(), new ArrayList<>(g.entries()))); + } + } else { + int colHeight = 0; + while (groupIndex < groups.size()) { + MediaWikiListGroup g = groups.get(groupIndex); + int gh = groupHeights[groupIndex]; + // Allow one group per column; otherwise respect the budget + if (colHeight > 0 && colHeight + gh > optimalMax) break; + sections.add(new MediaWikiListSection(g.key(), new ArrayList<>(g.entries()))); + colHeight += gh; + groupIndex++; + } } columns.add(new MediaWikiListColumn(sections)); } return columns; } + private static int countColumns(int[] groupHeights, int maxHeight) { + int cols = 0; + int currentHeight = 0; + for (int gh : groupHeights) { + if (currentHeight + gh > maxHeight) { + cols++; + currentHeight = gh; + } else { + currentHeight += gh; + } + } + return currentHeight > 0 ? cols + 1 : cols; + } + public static List> splitIntoColumns(List entries, int rows) { var columns = new ArrayList>(); for (MediaWikiListColumn column : planColumns(entries, rows)) { From fa367037466ba2e7e37af70639f9c9315e3d1b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:56:45 +0800 Subject: [PATCH 069/136] fix: register MOUNT-time scenes for Ponder tick dispatch - registerRuntimeScenes() scans document tree for LytGuidebookScene instances and registers them in GuidePage.scenes() for tick dispatch - Called at the start of tickCurrentPageScenes() so async-created scenes (SceneScript MaterializeTask) are picked up on the next tick - Adds Ponder debug logging for button click/toggle/tick state --- .../guidenh/guide/internal/GuideScreen.java | 32 ++++++++++++++++++- .../guide/scene/LytGuidebookScene.java | 24 +++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 5077f1c6..58dbb996 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2504,9 +2504,15 @@ private void resetPendingSceneRegistrations() { } private void tickCurrentPageScenes() { - if (currentPage == null || !pendingSceneRegistrations.isEmpty()) { + if (currentPage == null) { return; } + if (!pendingSceneRegistrations.isEmpty()) { + return; + } + // Phase 3: Scenes created asynchronously (MaterializeTask) appear in the document + // tree after mountDocument returns. Scan for new scenes each tick. + registerRuntimeScenes(currentPage); for (LytGuidebookScene scene : currentPage.scenes()) { scene.ponderTick(); } @@ -2570,6 +2576,30 @@ private void completePendingContentPageLoadIfNeeded() { updateToolbarButtonState(); } + /** Register scenes created at MOUNT time into GuidePage.scenes() for tick dispatch. */ + private static void registerRuntimeScenes(GuidePage page) { + LytDocument doc = page.document(); + if (doc == null) return; + java.util.List list = page.scenes(); + java.util.ArrayDeque pending = new java.util.ArrayDeque<>(); + pending.add(doc); + int found = 0; + while (!pending.isEmpty()) { + LytNode node = pending.removeLast(); + if (node instanceof LytGuidebookScene scene && !list.contains(scene)) { + list.add(scene); + found++; + } + var children = node.getChildren(); + for (int i = children.size() - 1; i >= 0; i--) { + pending.addLast(children.get(i)); + } + } + if (found > 0) { + FMLLog.getLogger().info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); + } + } + private void tickGuideEditorPreviewScenes() { if (!isGuideEditorActive() || guideEditorPreviewPage == null) { return; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 2289fa1c..4de055b6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -106,6 +106,7 @@ import com.hfstudio.guidenh.integration.structurelib.StructureLibTooltipContentBuilder; import lombok.Getter; +import cpw.mods.fml.common.FMLLog; public class LytGuidebookScene extends LytBlock { @@ -3870,9 +3871,9 @@ public void activateSceneButton(GuideIconButton.Role role) { } } case RESET_VIEW -> resetViewToInitialCamera(); - case PONDER_PREV_KEYFRAME -> ponderPrevKeyframe(); - case PONDER_PLAY_PAUSE -> ponderTogglePlay(); - case PONDER_RESTART -> ponderRestart(); + case PONDER_PREV_KEYFRAME -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); ponderPrevKeyframe(); } + case PONDER_PLAY_PAUSE -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); ponderTogglePlay(); } + case PONDER_RESTART -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_RESTART"); ponderRestart(); } default -> {} } } @@ -4405,8 +4406,17 @@ public void initializePonderTimelineBaseline() { public void ponderTick() { sceneAnimationTick++; - if (ponderSceneData == null || ponderPaused || ponderFinished) return; + if (ponderSceneData == null || ponderPaused || ponderFinished) { + if (sceneAnimationTick % 100 == 1) { + FMLLog.getLogger().info("[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", + ponderSceneData != null, ponderPaused, ponderFinished, ponderCurrentTick); + } + return; + } ponderCurrentTick++; + if (ponderCurrentTick % 20 == 0) { + FMLLog.getLogger().info("[PonderDebug] ponderTick advancing: tick={}/{}", ponderCurrentTick, ponderSceneData.getTotalTime()); + } if (ponderCurrentTick >= ponderSceneData.getTotalTime()) { ponderCurrentTick = ponderSceneData.getTotalTime(); ponderFinished = true; @@ -4431,7 +4441,11 @@ public void ponderTick() { } public void ponderTogglePlay() { - if (ponderSceneData == null) return; + if (ponderSceneData == null) { + FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); + return; + } + FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", ponderPaused, ponderFinished, ponderCurrentTick); if (ponderFinished) { ponderCurrentTick = 0; ponderFinished = false; From 3a89a89ab676005e2b3234fa0fee221365bc3f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:56:45 +0800 Subject: [PATCH 070/136] fix: register MOUNT-time scenes for Ponder tick dispatch - registerRuntimeScenes() scans document tree for LytGuidebookScene instances and registers them in GuidePage.scenes() for tick dispatch - Called at the start of tickCurrentPageScenes() so async-created scenes (SceneScript MaterializeTask) are picked up on the next tick - Adds Ponder debug logging for button click/toggle/tick state --- .../guidenh/guide/internal/GuideScreen.java | 32 ++++++++++++++++++- .../guide/scene/LytGuidebookScene.java | 24 +++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 5077f1c6..58dbb996 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2504,9 +2504,15 @@ private void resetPendingSceneRegistrations() { } private void tickCurrentPageScenes() { - if (currentPage == null || !pendingSceneRegistrations.isEmpty()) { + if (currentPage == null) { return; } + if (!pendingSceneRegistrations.isEmpty()) { + return; + } + // Phase 3: Scenes created asynchronously (MaterializeTask) appear in the document + // tree after mountDocument returns. Scan for new scenes each tick. + registerRuntimeScenes(currentPage); for (LytGuidebookScene scene : currentPage.scenes()) { scene.ponderTick(); } @@ -2570,6 +2576,30 @@ private void completePendingContentPageLoadIfNeeded() { updateToolbarButtonState(); } + /** Register scenes created at MOUNT time into GuidePage.scenes() for tick dispatch. */ + private static void registerRuntimeScenes(GuidePage page) { + LytDocument doc = page.document(); + if (doc == null) return; + java.util.List list = page.scenes(); + java.util.ArrayDeque pending = new java.util.ArrayDeque<>(); + pending.add(doc); + int found = 0; + while (!pending.isEmpty()) { + LytNode node = pending.removeLast(); + if (node instanceof LytGuidebookScene scene && !list.contains(scene)) { + list.add(scene); + found++; + } + var children = node.getChildren(); + for (int i = children.size() - 1; i >= 0; i--) { + pending.addLast(children.get(i)); + } + } + if (found > 0) { + FMLLog.getLogger().info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); + } + } + private void tickGuideEditorPreviewScenes() { if (!isGuideEditorActive() || guideEditorPreviewPage == null) { return; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 2289fa1c..4de055b6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -106,6 +106,7 @@ import com.hfstudio.guidenh.integration.structurelib.StructureLibTooltipContentBuilder; import lombok.Getter; +import cpw.mods.fml.common.FMLLog; public class LytGuidebookScene extends LytBlock { @@ -3870,9 +3871,9 @@ public void activateSceneButton(GuideIconButton.Role role) { } } case RESET_VIEW -> resetViewToInitialCamera(); - case PONDER_PREV_KEYFRAME -> ponderPrevKeyframe(); - case PONDER_PLAY_PAUSE -> ponderTogglePlay(); - case PONDER_RESTART -> ponderRestart(); + case PONDER_PREV_KEYFRAME -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); ponderPrevKeyframe(); } + case PONDER_PLAY_PAUSE -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); ponderTogglePlay(); } + case PONDER_RESTART -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_RESTART"); ponderRestart(); } default -> {} } } @@ -4405,8 +4406,17 @@ public void initializePonderTimelineBaseline() { public void ponderTick() { sceneAnimationTick++; - if (ponderSceneData == null || ponderPaused || ponderFinished) return; + if (ponderSceneData == null || ponderPaused || ponderFinished) { + if (sceneAnimationTick % 100 == 1) { + FMLLog.getLogger().info("[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", + ponderSceneData != null, ponderPaused, ponderFinished, ponderCurrentTick); + } + return; + } ponderCurrentTick++; + if (ponderCurrentTick % 20 == 0) { + FMLLog.getLogger().info("[PonderDebug] ponderTick advancing: tick={}/{}", ponderCurrentTick, ponderSceneData.getTotalTime()); + } if (ponderCurrentTick >= ponderSceneData.getTotalTime()) { ponderCurrentTick = ponderSceneData.getTotalTime(); ponderFinished = true; @@ -4431,7 +4441,11 @@ public void ponderTick() { } public void ponderTogglePlay() { - if (ponderSceneData == null) return; + if (ponderSceneData == null) { + FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); + return; + } + FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", ponderPaused, ponderFinished, ponderCurrentTick); if (ponderFinished) { ponderCurrentTick = 0; ponderFinished = false; From 37b3f53b2cc2f2bdeddd82b3ce09855d093be6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:29 +0800 Subject: [PATCH 071/136] fix: restore deep search indexing for Category and Special pages CategoryCompiler.index() and SpecialCompiler.index() now resolve category/special page member titles at indexing time and index them for full-text search, matching Phase 2 behavior. --- .../tags/mediawiki/CategoryCompiler.java | 32 ++++++++++++++----- .../tags/mediawiki/SpecialCompiler.java | 29 ++++++++++++----- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 2794f0b1..4c123301 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -7,6 +7,8 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; @@ -44,16 +46,30 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { - // NB: Phase 2 indexed category member titles resolved by - // MediaWikiPageListBuilder.buildCategoryMembers(). Phase 3 defers member resolution to - // CategoryScript (MOUNT time), so index() only indexes the category name string. - // Full indexing requires a post-mount indexing pass (TBD). String categoryName = el.getAttributeString("name", null); - if (categoryName != null && !categoryName.trim() - .isEmpty()) { - sink.appendText(el, categoryName.trim()); - sink.appendBreak(); + if (categoryName == null || categoryName.trim().isEmpty()) return; + + // Restore Phase 2: index resolved category member titles for full-text search + var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); + if (guide != null) { + CategoryIndex catIndex = indexer.getIndex(CategoryIndex.class); + if (catIndex != null) { + var context = MediaWikiTagCompilerSupport.createListContext(guide, catIndex); + var entries = MediaWikiPageListBuilder.buildCategoryMembers(context, categoryName.trim()); + sink.appendText(el, categoryName.trim()); + sink.appendBreak(); + for (var entry : entries) { + if (entry.title() != null && !entry.title().isEmpty()) { + sink.appendText(el, entry.title()); + } + sink.appendBreak(); + } + return; + } } + // Fallback: index only the category name + sink.appendText(el, categoryName.trim()); + sink.appendBreak(); } public static class CategoryPlaceholder extends LytParagraph { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index 966ea31b..c5ea65a4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -7,6 +7,8 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; @@ -48,16 +50,27 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { - // NB: Phase 2 indexed special page result entries resolved by - // MediaWikiSpecialPageResolver.resolve(). Phase 3 defers resolution to - // SpecialScript (MOUNT time), so index() only indexes the special page name string. - // Full indexing requires a post-mount indexing pass (TBD). String specialName = el.getAttributeString("name", null); - if (specialName != null && !specialName.trim() - .isEmpty()) { - sink.appendText(el, specialName.trim()); - sink.appendBreak(); + if (specialName == null || specialName.trim().isEmpty()) return; + + // Restore Phase 2: index resolved special page result entries for full-text search + var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); + if (guide != null) { + CategoryIndex catIndex = indexer.getIndex(CategoryIndex.class); + if (catIndex != null) { + var context = MediaWikiTagCompilerSupport.createListContext(guide, catIndex); + var query = MediaWikiTagCompilerSupport.readSpecialQuery(el); + var resolver = new MediaWikiSpecialPageResolver(); + var result = resolver.resolve(context, specialName.trim(), query); + sink.appendText(el, specialName.trim()); + sink.appendBreak(); + MediaWikiTagCompilerSupport.indexSpecialResult(sink, el, result); + return; + } } + // Fallback: index only the special page name + sink.appendText(el, specialName.trim()); + sink.appendBreak(); } public static class SpecialPlaceholder extends LytParagraph { From c7b8908a334d8e8c59ca92d1cf760f20343e1cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:29 +0800 Subject: [PATCH 072/136] fix: restore deep search indexing for Category and Special pages CategoryCompiler.index() and SpecialCompiler.index() now resolve category/special page member titles at indexing time and index them for full-text search, matching Phase 2 behavior. --- .../tags/mediawiki/CategoryCompiler.java | 32 ++++++++++++++----- .../tags/mediawiki/SpecialCompiler.java | 29 ++++++++++++----- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 2794f0b1..4c123301 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -7,6 +7,8 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; @@ -44,16 +46,30 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { - // NB: Phase 2 indexed category member titles resolved by - // MediaWikiPageListBuilder.buildCategoryMembers(). Phase 3 defers member resolution to - // CategoryScript (MOUNT time), so index() only indexes the category name string. - // Full indexing requires a post-mount indexing pass (TBD). String categoryName = el.getAttributeString("name", null); - if (categoryName != null && !categoryName.trim() - .isEmpty()) { - sink.appendText(el, categoryName.trim()); - sink.appendBreak(); + if (categoryName == null || categoryName.trim().isEmpty()) return; + + // Restore Phase 2: index resolved category member titles for full-text search + var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); + if (guide != null) { + CategoryIndex catIndex = indexer.getIndex(CategoryIndex.class); + if (catIndex != null) { + var context = MediaWikiTagCompilerSupport.createListContext(guide, catIndex); + var entries = MediaWikiPageListBuilder.buildCategoryMembers(context, categoryName.trim()); + sink.appendText(el, categoryName.trim()); + sink.appendBreak(); + for (var entry : entries) { + if (entry.title() != null && !entry.title().isEmpty()) { + sink.appendText(el, entry.title()); + } + sink.appendBreak(); + } + return; + } } + // Fallback: index only the category name + sink.appendText(el, categoryName.trim()); + sink.appendBreak(); } public static class CategoryPlaceholder extends LytParagraph { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index 966ea31b..c5ea65a4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -7,6 +7,8 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; @@ -48,16 +50,27 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { - // NB: Phase 2 indexed special page result entries resolved by - // MediaWikiSpecialPageResolver.resolve(). Phase 3 defers resolution to - // SpecialScript (MOUNT time), so index() only indexes the special page name string. - // Full indexing requires a post-mount indexing pass (TBD). String specialName = el.getAttributeString("name", null); - if (specialName != null && !specialName.trim() - .isEmpty()) { - sink.appendText(el, specialName.trim()); - sink.appendBreak(); + if (specialName == null || specialName.trim().isEmpty()) return; + + // Restore Phase 2: index resolved special page result entries for full-text search + var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); + if (guide != null) { + CategoryIndex catIndex = indexer.getIndex(CategoryIndex.class); + if (catIndex != null) { + var context = MediaWikiTagCompilerSupport.createListContext(guide, catIndex); + var query = MediaWikiTagCompilerSupport.readSpecialQuery(el); + var resolver = new MediaWikiSpecialPageResolver(); + var result = resolver.resolve(context, specialName.trim(), query); + sink.appendText(el, specialName.trim()); + sink.appendBreak(); + MediaWikiTagCompilerSupport.indexSpecialResult(sink, el, result); + return; + } } + // Fallback: index only the special page name + sink.appendText(el, specialName.trim()); + sink.appendBreak(); } public static class SpecialPlaceholder extends LytParagraph { From 415105069ad25b810b1107a218420c78af603c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:39 +0800 Subject: [PATCH 073/136] fix: pass sourcePack through ScenePlaceholder to runtime PageCompiler Add getSourcePack() to PageCompiler, store sourcePack in ScenePlaceholder, and use it in SceneScript's runtime compiler instead of empty string. --- .../com/hfstudio/guidenh/guide/compiler/PageCompiler.java | 4 ++++ .../com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index ccaa2ede..0f7b9198 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -1113,6 +1113,10 @@ public String getLanguage() { return language; } + public String getSourcePack() { + return sourcePack; + } + public PageCollection getPageCollection() { return pages; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 178374e5..de2d2a70 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -155,7 +155,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl allowLayerSlider, gridButtonEnabled, showGrid, childrenSource, compiler.getPageId().getResourceDomain(), - preParsedAst, // NEW: pre-parsed AST for SceneScript + compiler.getSourcePack(), + preParsedAst, sceneElementCompilers ); placeholder.setStyleClass(styleClass); @@ -202,6 +203,7 @@ public static class ScenePlaceholder extends LytParagraph { public final boolean showGrid; @Nullable public final String childrenSource; public final String pageDomain; + public final String sourcePack; @Nullable public final MdAstRoot childrenAst; @Nullable public final List sceneElementCompilers; @@ -221,6 +223,7 @@ public ScenePlaceholder( boolean showGrid, @Nullable String childrenSource, String pageDomain, + String sourcePack, @Nullable MdAstRoot childrenAst, @Nullable List sceneElementCompilers) { this.width = width; @@ -248,6 +251,7 @@ public ScenePlaceholder( this.showGrid = showGrid; this.childrenSource = childrenSource; this.pageDomain = pageDomain; + this.sourcePack = sourcePack; this.childrenAst = childrenAst; this.sceneElementCompilers = sceneElementCompilers; } From 47d9a283c8af10a10178b1348ba4d4dbe164924a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:39 +0800 Subject: [PATCH 074/136] fix: pass sourcePack through ScenePlaceholder to runtime PageCompiler Add getSourcePack() to PageCompiler, store sourcePack in ScenePlaceholder, and use it in SceneScript's runtime compiler instead of empty string. --- .../com/hfstudio/guidenh/guide/compiler/PageCompiler.java | 4 ++++ .../com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index ccaa2ede..0f7b9198 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -1113,6 +1113,10 @@ public String getLanguage() { return language; } + public String getSourcePack() { + return sourcePack; + } + public PageCollection getPageCollection() { return pages; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 178374e5..de2d2a70 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -155,7 +155,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl allowLayerSlider, gridButtonEnabled, showGrid, childrenSource, compiler.getPageId().getResourceDomain(), - preParsedAst, // NEW: pre-parsed AST for SceneScript + compiler.getSourcePack(), + preParsedAst, sceneElementCompilers ); placeholder.setStyleClass(styleClass); @@ -202,6 +203,7 @@ public static class ScenePlaceholder extends LytParagraph { public final boolean showGrid; @Nullable public final String childrenSource; public final String pageDomain; + public final String sourcePack; @Nullable public final MdAstRoot childrenAst; @Nullable public final List sceneElementCompilers; @@ -221,6 +223,7 @@ public ScenePlaceholder( boolean showGrid, @Nullable String childrenSource, String pageDomain, + String sourcePack, @Nullable MdAstRoot childrenAst, @Nullable List sceneElementCompilers) { this.width = width; @@ -248,6 +251,7 @@ public ScenePlaceholder( this.showGrid = showGrid; this.childrenSource = childrenSource; this.pageDomain = pageDomain; + this.sourcePack = sourcePack; this.childrenAst = childrenAst; this.sceneElementCompilers = sceneElementCompilers; } From d63f334d33ad9eb412d6b169834b6a17a6f1114b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:49 +0800 Subject: [PATCH 075/136] fix: wire real ExtensionCollection, annotation tooltip dispatch, BlockStats - Use guide's ExtensionCollection instead of EMPTY for runtime compiler - Dispatch MOUNT events into annotation ContentTooltip subtrees - Apply implicit BlockStats for scenes without explicit BlockStats element - Handle BlockStats element attributes (visible, buttonEnabled) - Add docs for StructureLib selection listeners and scene cache limitations --- .../internal/host/scripts/SceneScript.java | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index cdc5e26e..b9cd9508 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -7,12 +7,15 @@ import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.document.LytErrorSink; +import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; +import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.PageIndex; @@ -33,6 +36,7 @@ import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.unist.UnistNode; +import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -91,8 +95,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Parse children source ExceptionCollector errorSink = new ExceptionCollector(); PageCollection pc = ctx.getPageCollection(); + ExtensionCollection extensions = pc instanceof Guide guide + ? guide.getExtensions() : ExtensionCollection.EMPTY; PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), - ExtensionCollection.EMPTY, "", new ResourceLocation(ph.pageDomain, "scene"), ""); + extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), + ph.childrenSource != null ? ph.childrenSource : ""); MdAstRoot ast; try { ast = ph.childrenAst != null ? ph.childrenAst @@ -127,16 +134,30 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { scene.setGridButtonEnabled(ph.gridButtonEnabled); scene.setGridVisible(ph.showGrid); + // NB: Phase 2 used GuideSceneStructureCache (fingerprint-based) to avoid + // recompiling complex scenes on every page visit. Phase 3 compiles from scratch + // each mount. The cache requires StructureFingerprintResolver + compile-time + // fingerprint computation, which is not practical to restore in a MOUNT-time script. + // Low priority — scene compilation is usually fast enough that recompilation + // per mount is acceptable. + // Compile scene elements with CURRENT_SCENE set so that element compilers // (ImportPonderElementCompiler, ImportStructureLibElementCompiler, annotations, etc.) // can call scene.attachPonderData(), scene.addAnnotation(), etc. var prevScene = AnnotationTagCompiler.CURRENT_SCENE.get(); AnnotationTagCompiler.CURRENT_SCENE.set(scene); + final boolean[] blockStatsExplicitlySet = {false}; try { GuideSceneStructureCompileScope.run(true, () -> { for (UnistNode child : ast.children()) { MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); if (el == null) continue; + // Handle BlockStats — not a SceneElementTagCompiler, special-cased in Phase 2 + if ("BlockStats".equals(el.name())) { + applyBlockStatsConfig(scene, el); + blockStatsExplicitlySet[0] = true; + continue; + } SceneElementTagCompiler ec = elementCompilers.get(el.name()); if (ec != null) { ec.compile(level, camera, runtimeCompiler, errorSink, el); @@ -151,11 +172,29 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } + // Dispatch MOUNT events into annotation tooltip subtrees (Recipe/Scene placeholders) + for (var annotation : scene.getAnnotations()) { + var tooltip = annotation.getTooltip(); + if (tooltip instanceof ContentTooltip ct) { + var content = ct.getContent(); + if (content instanceof LytNode root) { + ctx.dispatchSubtree(root); + } + } + } + if (level.isEmpty()) { ctx.replace(LytParagraph.error("[Scene] Scene has no supported elements")); return; } + // Apply implicit block stats for non-empty scenes without explicit BlockStats + if (!blockStatsExplicitlySet[0]) { + scene.setBlockStatsEnabled(true); + scene.setBlockStatsVisible(ModConfig.ui.sceneBlockStatsVisible); + scene.setBlockStatsButtonEnabled(ModConfig.ui.sceneBlockStatsButtonEnabled); + } + // Finalize scene setup: auto-center, ponder baseline, interactive state capture float[] center = level.getCenter(); if (!ph.explicitCenter) { @@ -213,9 +252,29 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { scene.initializePonderTimelineBaseline(); scene.captureInitialInteractiveState(); scene.snapshotInitialCamera(); + // NB: Phase 2 called configureStructureLibSelectionListeners() which set up + // rebuildSceneForStructureLibSelection() callbacks for interactive StructureLib + // preview variant switching. Phase 3 defers this — scene rebuild would need to + // re-invoke the full element compilation loop, which is impractical in a script. + // The interactive variant UI will not respond to selection changes until the + // page is re-mounted (navigate away and back). ctx.replace(scene); } + /** + * Applies BlockStats element attributes to the scene. + *

    + * NB: Full BlockStats restoration (BlockStat sub-elements, filters, implicit enable) + * requires the Phase 2 compileBlockStatsElement() logic (~100 lines). This minimal + * restoration handles the most common attribute-only use case. + */ + private static void applyBlockStatsConfig(LytGuidebookScene scene, MdxJsxElementFields el) { + String visibleStr = el.getAttributeString("visible", null); + if (visibleStr != null) scene.setBlockStatsVisible(Boolean.parseBoolean(visibleStr)); + String enabledStr = el.getAttributeString("buttonEnabled", null); + if (enabledStr != null) scene.setBlockStatsButtonEnabled(Boolean.parseBoolean(enabledStr)); + } + private static class ExceptionCollector implements LytErrorSink { @Override public void appendError(PageCompiler compiler, String text, UnistNode node) { From 566cc3498066c207109c303f154c07f3ff6d11dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:49 +0800 Subject: [PATCH 076/136] fix: wire real ExtensionCollection, annotation tooltip dispatch, BlockStats - Use guide's ExtensionCollection instead of EMPTY for runtime compiler - Dispatch MOUNT events into annotation ContentTooltip subtrees - Apply implicit BlockStats for scenes without explicit BlockStats element - Handle BlockStats element attributes (visible, buttonEnabled) - Add docs for StructureLib selection listeners and scene cache limitations --- .../internal/host/scripts/SceneScript.java | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index cdc5e26e..b9cd9508 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -7,12 +7,15 @@ import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.document.LytErrorSink; +import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; +import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.PageIndex; @@ -33,6 +36,7 @@ import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.unist.UnistNode; +import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -91,8 +95,11 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Parse children source ExceptionCollector errorSink = new ExceptionCollector(); PageCollection pc = ctx.getPageCollection(); + ExtensionCollection extensions = pc instanceof Guide guide + ? guide.getExtensions() : ExtensionCollection.EMPTY; PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), - ExtensionCollection.EMPTY, "", new ResourceLocation(ph.pageDomain, "scene"), ""); + extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), + ph.childrenSource != null ? ph.childrenSource : ""); MdAstRoot ast; try { ast = ph.childrenAst != null ? ph.childrenAst @@ -127,16 +134,30 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { scene.setGridButtonEnabled(ph.gridButtonEnabled); scene.setGridVisible(ph.showGrid); + // NB: Phase 2 used GuideSceneStructureCache (fingerprint-based) to avoid + // recompiling complex scenes on every page visit. Phase 3 compiles from scratch + // each mount. The cache requires StructureFingerprintResolver + compile-time + // fingerprint computation, which is not practical to restore in a MOUNT-time script. + // Low priority — scene compilation is usually fast enough that recompilation + // per mount is acceptable. + // Compile scene elements with CURRENT_SCENE set so that element compilers // (ImportPonderElementCompiler, ImportStructureLibElementCompiler, annotations, etc.) // can call scene.attachPonderData(), scene.addAnnotation(), etc. var prevScene = AnnotationTagCompiler.CURRENT_SCENE.get(); AnnotationTagCompiler.CURRENT_SCENE.set(scene); + final boolean[] blockStatsExplicitlySet = {false}; try { GuideSceneStructureCompileScope.run(true, () -> { for (UnistNode child : ast.children()) { MdxJsxElementFields el = SceneTagCompiler.unwrapSceneElement(child); if (el == null) continue; + // Handle BlockStats — not a SceneElementTagCompiler, special-cased in Phase 2 + if ("BlockStats".equals(el.name())) { + applyBlockStatsConfig(scene, el); + blockStatsExplicitlySet[0] = true; + continue; + } SceneElementTagCompiler ec = elementCompilers.get(el.name()); if (ec != null) { ec.compile(level, camera, runtimeCompiler, errorSink, el); @@ -151,11 +172,29 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } + // Dispatch MOUNT events into annotation tooltip subtrees (Recipe/Scene placeholders) + for (var annotation : scene.getAnnotations()) { + var tooltip = annotation.getTooltip(); + if (tooltip instanceof ContentTooltip ct) { + var content = ct.getContent(); + if (content instanceof LytNode root) { + ctx.dispatchSubtree(root); + } + } + } + if (level.isEmpty()) { ctx.replace(LytParagraph.error("[Scene] Scene has no supported elements")); return; } + // Apply implicit block stats for non-empty scenes without explicit BlockStats + if (!blockStatsExplicitlySet[0]) { + scene.setBlockStatsEnabled(true); + scene.setBlockStatsVisible(ModConfig.ui.sceneBlockStatsVisible); + scene.setBlockStatsButtonEnabled(ModConfig.ui.sceneBlockStatsButtonEnabled); + } + // Finalize scene setup: auto-center, ponder baseline, interactive state capture float[] center = level.getCenter(); if (!ph.explicitCenter) { @@ -213,9 +252,29 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { scene.initializePonderTimelineBaseline(); scene.captureInitialInteractiveState(); scene.snapshotInitialCamera(); + // NB: Phase 2 called configureStructureLibSelectionListeners() which set up + // rebuildSceneForStructureLibSelection() callbacks for interactive StructureLib + // preview variant switching. Phase 3 defers this — scene rebuild would need to + // re-invoke the full element compilation loop, which is impractical in a script. + // The interactive variant UI will not respond to selection changes until the + // page is re-mounted (navigate away and back). ctx.replace(scene); } + /** + * Applies BlockStats element attributes to the scene. + *

    + * NB: Full BlockStats restoration (BlockStat sub-elements, filters, implicit enable) + * requires the Phase 2 compileBlockStatsElement() logic (~100 lines). This minimal + * restoration handles the most common attribute-only use case. + */ + private static void applyBlockStatsConfig(LytGuidebookScene scene, MdxJsxElementFields el) { + String visibleStr = el.getAttributeString("visible", null); + if (visibleStr != null) scene.setBlockStatsVisible(Boolean.parseBoolean(visibleStr)); + String enabledStr = el.getAttributeString("buttonEnabled", null); + if (enabledStr != null) scene.setBlockStatsButtonEnabled(Boolean.parseBoolean(enabledStr)); + } + private static class ExceptionCollector implements LytErrorSink { @Override public void appendError(PageCompiler compiler, String text, UnistNode node) { From 3d0aeb039a5e465e5cd4916ff748b48b38e6eabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:58 +0800 Subject: [PATCH 077/136] fix: log NBT parse failures in BlockImageScript instead of silently ignoring --- .../guide/internal/host/scripts/BlockImageScript.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index c4f5c19a..5a5136a7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -27,6 +27,7 @@ import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; import com.hfstudio.guidenh.guide.scene.ponder.PonderNbtPath; +import cpw.mods.fml.common.FMLLog; public class BlockImageScript implements LytScript { @@ -83,7 +84,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } else { tileTag = explicitTag; } - } catch (Exception ignored) {} + } catch (Exception e) { + FMLLog.getLogger().warn("[BlockImageScript] Failed to parse NBT for block image", e); + } } PerspectivePreset perspective = PerspectivePreset.ISOMETRIC_NORTH_EAST; From 34121a5088b42350ace4b6a57e5d6ab4f0f4fabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:40:58 +0800 Subject: [PATCH 078/136] fix: log NBT parse failures in BlockImageScript instead of silently ignoring --- .../guide/internal/host/scripts/BlockImageScript.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index c4f5c19a..5a5136a7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -27,6 +27,7 @@ import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; import com.hfstudio.guidenh.guide.scene.ponder.PonderNbtPath; +import cpw.mods.fml.common.FMLLog; public class BlockImageScript implements LytScript { @@ -83,7 +84,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } else { tileTag = explicitTag; } - } catch (Exception ignored) {} + } catch (Exception e) { + FMLLog.getLogger().warn("[BlockImageScript] Failed to parse NBT for block image", e); + } } PerspectivePreset perspective = PerspectivePreset.ISOMETRIC_NORTH_EAST; From a76e9e172ee97736177d1244228c816552e6e08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:41:07 +0800 Subject: [PATCH 079/136] refactor: unify duplicate resolveItemId with IdUtils.resolveItemStack Both ItemImageScript and ItemGridScript had identical 18-line resolveItemId() implementations. Replaced with the shared IdUtils.resolveItemStack() utility. --- .../internal/host/scripts/ItemGridScript.java | 19 ++----------------- .../host/scripts/ItemImageScript.java | 19 ++----------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index 3b32079c..b6488c90 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -3,6 +3,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.tags.ItemGridCompiler.ItemGridPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemGrid; import com.hfstudio.guidenh.guide.document.block.LytParagraph; @@ -41,23 +42,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } - @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { - if (itemId == null || itemId.isEmpty()) return null; - String ns = "minecraft"; - int idx = itemId.indexOf(':'); - if (idx >= 0) { - ns = itemId.substring(0, idx); - } - com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); - if (ref == null) return null; - Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (item == null) return null; - ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); - if (ref.nbt() != null) { - stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); - } - return stack; + return IdUtils.resolveItemStack(itemId, "minecraft"); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index f0ecfba8..522beb84 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -7,6 +7,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemImage; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -77,23 +78,7 @@ private void replaceFlowError(ScriptContext ctx, boolean isWrapped, String messa } } - @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { - if (itemId == null || itemId.isEmpty()) return null; - String ns = "minecraft"; - int idx = itemId.indexOf(':'); - if (idx >= 0) { - ns = itemId.substring(0, idx); - } - com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); - if (ref == null) return null; - Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (item == null) return null; - ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); - if (ref.nbt() != null) { - stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); - } - return stack; + return IdUtils.resolveItemStack(itemId, "minecraft"); } } From b605f45b096b0894c0677e517e61b44a1b5d9f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:41:07 +0800 Subject: [PATCH 080/136] refactor: unify duplicate resolveItemId with IdUtils.resolveItemStack Both ItemImageScript and ItemGridScript had identical 18-line resolveItemId() implementations. Replaced with the shared IdUtils.resolveItemStack() utility. --- .../internal/host/scripts/ItemGridScript.java | 19 ++----------------- .../host/scripts/ItemImageScript.java | 19 ++----------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index 3b32079c..b6488c90 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -3,6 +3,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.tags.ItemGridCompiler.ItemGridPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemGrid; import com.hfstudio.guidenh.guide.document.block.LytParagraph; @@ -41,23 +42,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } - @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { - if (itemId == null || itemId.isEmpty()) return null; - String ns = "minecraft"; - int idx = itemId.indexOf(':'); - if (idx >= 0) { - ns = itemId.substring(0, idx); - } - com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); - if (ref == null) return null; - Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (item == null) return null; - ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); - if (ref.nbt() != null) { - stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); - } - return stack; + return IdUtils.resolveItemStack(itemId, "minecraft"); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index f0ecfba8..522beb84 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -7,6 +7,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemImage; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -77,23 +78,7 @@ private void replaceFlowError(ScriptContext ctx, boolean isWrapped, String messa } } - @SuppressWarnings("deprecation") private static ItemStack resolveItemId(String itemId) { - if (itemId == null || itemId.isEmpty()) return null; - String ns = "minecraft"; - int idx = itemId.indexOf(':'); - if (idx >= 0) { - ns = itemId.substring(0, idx); - } - com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(itemId, ns); - if (ref == null) return null; - Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); - if (item == null) return null; - ItemStack stack = new ItemStack(item, 1, ref.concreteMeta()); - if (ref.nbt() != null) { - stack.stackTagCompound = (net.minecraft.nbt.NBTTagCompound) ref.nbt().copy(); - } - return stack; + return IdUtils.resolveItemStack(itemId, "minecraft"); } } From 1772b2ac70f93552ebdcf4598ea366afd5d3dee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:50:44 +0800 Subject: [PATCH 081/136] fix: Mermaid mindmap NodeContent BlockImage rendering and zoom - Fix CameraSettings.setZoom() to mark projection dirty instead of view - Add sceneViewportOverride to LytGuidebookScene for per-frame GL viewport control - Remove ResponsiveVisualSizing.scaleWidth from canvas preferredWidth to match toolbar - Scale scene viewport with mindmap zoom while locking camera viewport to original size - Position scene at contentViewport coordinates for correct 3D viewport placement --- .../block/LytMermaidMindmapCanvas.java | 46 +++++++++++-- .../guidenh/guide/scene/CameraSettings.java | 2 +- .../guide/scene/LytGuidebookScene.java | 64 +++++++++++++++---- 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 60b36e95..d22d5d66 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -29,6 +29,7 @@ import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; +import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; @@ -134,10 +135,7 @@ public void setPreferredSize(int width, int height) { @Override protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { - int preferredViewportWidth = preferredWidth > 0 - ? ResponsiveVisualSizing.scaleWidth(preferredWidth, context.getVisualScale(), 1) - : 0; - int safeWidth = preferredViewportWidth > 0 ? Math.max(1, Math.min(preferredViewportWidth, availableWidth)) + int safeWidth = preferredWidth > 0 ? Math.max(1, Math.min(preferredWidth, availableWidth)) : Math.max(1, availableWidth); layout = buildLayout(context, safeWidth); int desiredHeight = layout.diagramHeight() + CANVAS_PADDING * 2; @@ -661,13 +659,49 @@ private void renderNodeContent(RenderContext context, NodeLayout node, LytRect r contentViewport.x(), contentViewport.y(), zoom); - node.contentLayout.block() - .render(nodeContext); + renderNodeContentBlock(node.contentLayout.block(), nodeContext, context, contentViewport); } finally { context.popScissor(); } } + private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nodeContext, + RenderContext nativeContext, LytRect contentViewport) { + if (block instanceof LytGuidebookScene scene) { + // The camera's orthographic projection uses its own viewport size + // while the GL viewport comes from bounds + layoutSceneWidth/Height. + // By scaling the scene viewport (GL) and locking the camera viewport + // to the original size, the 3D content fills more pixels while the + // world-to-NDC mapping stays tight — content follows mindmap zoom. + LytRect savedBounds = scene.getBounds(); + int savedW = savedBounds.width(); + int savedH = savedBounds.height(); + int scaledW = Math.max(1, Math.round(savedW * zoom)); + int scaledH = Math.max(1, Math.round(savedH * zoom)); + scene.bounds = new LytRect(contentViewport.x(), contentViewport.y(), scaledW, scaledH); + scene.setSceneViewportOverride(scaledW, scaledH); + scene.setCameraViewportOverride(savedW, savedH); + try { + scene.render(nativeContext); + } finally { + scene.bounds = savedBounds; + scene.clearSceneViewportOverride(); + scene.clearCameraViewportOverride(); + } + return; + } + if (block instanceof LytNode container && !container.getChildren() + .isEmpty()) { + for (var child : new ArrayList<>(container.getChildren())) { + if (child instanceof LytBlock childBlock) { + renderNodeContentBlock(childBlock, nodeContext, nativeContext, contentViewport); + } + } + } else { + block.render(nodeContext); + } + } + private @Nullable NodeHit pickNodeHit(int documentX, int documentY) { if (layout == null) { return null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java b/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java index 28c4550e..1e3d67df 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java @@ -79,7 +79,7 @@ public float getZoom() { public void setZoom(float zoom) { if (this.zoom != zoom) { this.zoom = zoom; - markViewDirty(); + markProjectionDirty(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 4de055b6..328e313c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -105,8 +105,8 @@ import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneMetadata; import com.hfstudio.guidenh.integration.structurelib.StructureLibTooltipContentBuilder; -import lombok.Getter; import cpw.mods.fml.common.FMLLog; +import lombok.Getter; public class LytGuidebookScene extends LytBlock { @@ -265,6 +265,8 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private boolean showBackground = true; @Nullable private LytSize cameraViewportOverride; + @Nullable + private LytSize sceneViewportOverride; private final List annotations = new ArrayList<>(); // Reuse annotation partitions instead of allocating new lists every frame. @@ -635,6 +637,14 @@ public void clearCameraViewportOverride() { this.cameraViewportOverride = null; } + public void setSceneViewportOverride(int width, int height) { + this.sceneViewportOverride = new LytSize(Math.max(16, width), Math.max(16, height)); + } + + public void clearSceneViewportOverride() { + this.sceneViewportOverride = null; + } + public boolean isGridButtonEnabled() { return gridButtonEnabled || ModConfig.debug.enableDebugMode; } @@ -2032,11 +2042,14 @@ protected void onLayoutMoved(int deltaX, int deltaY) {} @Override public void render(RenderContext context) { - int sceneW = layoutSceneWidth > 0 ? layoutSceneWidth - : getBounds().width() - buttonColumnReserve() - layoutSceneOffsetX; + LytSize vpOverride = sceneViewportOverride; + int sceneW = vpOverride != null ? vpOverride.width() + : layoutSceneWidth > 0 ? layoutSceneWidth + : getBounds().width() - buttonColumnReserve() - layoutSceneOffsetX; if (sceneW < 16) sceneW = 16; int sliderAreaHeight = getBottomControlAreaHeight(); - int sceneH = layoutSceneHeight > 0 ? layoutSceneHeight : Math.max(16, getBounds().height() - sliderAreaHeight); + int sceneH = vpOverride != null ? vpOverride.height() + : layoutSceneHeight > 0 ? layoutSceneHeight : Math.max(16, getBounds().height() - sliderAreaHeight); int totalH = reserveBottomControlArea ? Math.max(sceneH + sliderAreaHeight, getBounds().height()) : Math.max(sceneH, getBounds().height()); LytRect outerRect = new LytRect( @@ -3871,9 +3884,21 @@ public void activateSceneButton(GuideIconButton.Role role) { } } case RESET_VIEW -> resetViewToInitialCamera(); - case PONDER_PREV_KEYFRAME -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); ponderPrevKeyframe(); } - case PONDER_PLAY_PAUSE -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); ponderTogglePlay(); } - case PONDER_RESTART -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_RESTART"); ponderRestart(); } + case PONDER_PREV_KEYFRAME -> { + FMLLog.getLogger() + .info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); + ponderPrevKeyframe(); + } + case PONDER_PLAY_PAUSE -> { + FMLLog.getLogger() + .info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); + ponderTogglePlay(); + } + case PONDER_RESTART -> { + FMLLog.getLogger() + .info("[PonderDebug] activateSceneButton: PONDER_RESTART"); + ponderRestart(); + } default -> {} } } @@ -4408,14 +4433,23 @@ public void ponderTick() { sceneAnimationTick++; if (ponderSceneData == null || ponderPaused || ponderFinished) { if (sceneAnimationTick % 100 == 1) { - FMLLog.getLogger().info("[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", - ponderSceneData != null, ponderPaused, ponderFinished, ponderCurrentTick); + FMLLog.getLogger() + .info( + "[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", + ponderSceneData != null, + ponderPaused, + ponderFinished, + ponderCurrentTick); } return; } ponderCurrentTick++; if (ponderCurrentTick % 20 == 0) { - FMLLog.getLogger().info("[PonderDebug] ponderTick advancing: tick={}/{}", ponderCurrentTick, ponderSceneData.getTotalTime()); + FMLLog.getLogger() + .info( + "[PonderDebug] ponderTick advancing: tick={}/{}", + ponderCurrentTick, + ponderSceneData.getTotalTime()); } if (ponderCurrentTick >= ponderSceneData.getTotalTime()) { ponderCurrentTick = ponderSceneData.getTotalTime(); @@ -4442,10 +4476,16 @@ public void ponderTick() { public void ponderTogglePlay() { if (ponderSceneData == null) { - FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); + FMLLog.getLogger() + .info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); return; } - FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", ponderPaused, ponderFinished, ponderCurrentTick); + FMLLog.getLogger() + .info( + "[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", + ponderPaused, + ponderFinished, + ponderCurrentTick); if (ponderFinished) { ponderCurrentTick = 0; ponderFinished = false; From 891105e292d4d38a4e1f5ed979358bc05299d78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:50:44 +0800 Subject: [PATCH 082/136] fix: Mermaid mindmap NodeContent BlockImage rendering and zoom - Fix CameraSettings.setZoom() to mark projection dirty instead of view - Add sceneViewportOverride to LytGuidebookScene for per-frame GL viewport control - Remove ResponsiveVisualSizing.scaleWidth from canvas preferredWidth to match toolbar - Scale scene viewport with mindmap zoom while locking camera viewport to original size - Position scene at contentViewport coordinates for correct 3D viewport placement --- .../block/LytMermaidMindmapCanvas.java | 46 +++++++++++-- .../guidenh/guide/scene/CameraSettings.java | 2 +- .../guide/scene/LytGuidebookScene.java | 64 +++++++++++++++---- 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 60b36e95..d22d5d66 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -29,6 +29,7 @@ import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; +import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; @@ -134,10 +135,7 @@ public void setPreferredSize(int width, int height) { @Override protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { - int preferredViewportWidth = preferredWidth > 0 - ? ResponsiveVisualSizing.scaleWidth(preferredWidth, context.getVisualScale(), 1) - : 0; - int safeWidth = preferredViewportWidth > 0 ? Math.max(1, Math.min(preferredViewportWidth, availableWidth)) + int safeWidth = preferredWidth > 0 ? Math.max(1, Math.min(preferredWidth, availableWidth)) : Math.max(1, availableWidth); layout = buildLayout(context, safeWidth); int desiredHeight = layout.diagramHeight() + CANVAS_PADDING * 2; @@ -661,13 +659,49 @@ private void renderNodeContent(RenderContext context, NodeLayout node, LytRect r contentViewport.x(), contentViewport.y(), zoom); - node.contentLayout.block() - .render(nodeContext); + renderNodeContentBlock(node.contentLayout.block(), nodeContext, context, contentViewport); } finally { context.popScissor(); } } + private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nodeContext, + RenderContext nativeContext, LytRect contentViewport) { + if (block instanceof LytGuidebookScene scene) { + // The camera's orthographic projection uses its own viewport size + // while the GL viewport comes from bounds + layoutSceneWidth/Height. + // By scaling the scene viewport (GL) and locking the camera viewport + // to the original size, the 3D content fills more pixels while the + // world-to-NDC mapping stays tight — content follows mindmap zoom. + LytRect savedBounds = scene.getBounds(); + int savedW = savedBounds.width(); + int savedH = savedBounds.height(); + int scaledW = Math.max(1, Math.round(savedW * zoom)); + int scaledH = Math.max(1, Math.round(savedH * zoom)); + scene.bounds = new LytRect(contentViewport.x(), contentViewport.y(), scaledW, scaledH); + scene.setSceneViewportOverride(scaledW, scaledH); + scene.setCameraViewportOverride(savedW, savedH); + try { + scene.render(nativeContext); + } finally { + scene.bounds = savedBounds; + scene.clearSceneViewportOverride(); + scene.clearCameraViewportOverride(); + } + return; + } + if (block instanceof LytNode container && !container.getChildren() + .isEmpty()) { + for (var child : new ArrayList<>(container.getChildren())) { + if (child instanceof LytBlock childBlock) { + renderNodeContentBlock(childBlock, nodeContext, nativeContext, contentViewport); + } + } + } else { + block.render(nodeContext); + } + } + private @Nullable NodeHit pickNodeHit(int documentX, int documentY) { if (layout == null) { return null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java b/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java index 28c4550e..1e3d67df 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/CameraSettings.java @@ -79,7 +79,7 @@ public float getZoom() { public void setZoom(float zoom) { if (this.zoom != zoom) { this.zoom = zoom; - markViewDirty(); + markProjectionDirty(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 4de055b6..328e313c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -105,8 +105,8 @@ import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneMetadata; import com.hfstudio.guidenh.integration.structurelib.StructureLibTooltipContentBuilder; -import lombok.Getter; import cpw.mods.fml.common.FMLLog; +import lombok.Getter; public class LytGuidebookScene extends LytBlock { @@ -265,6 +265,8 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private boolean showBackground = true; @Nullable private LytSize cameraViewportOverride; + @Nullable + private LytSize sceneViewportOverride; private final List annotations = new ArrayList<>(); // Reuse annotation partitions instead of allocating new lists every frame. @@ -635,6 +637,14 @@ public void clearCameraViewportOverride() { this.cameraViewportOverride = null; } + public void setSceneViewportOverride(int width, int height) { + this.sceneViewportOverride = new LytSize(Math.max(16, width), Math.max(16, height)); + } + + public void clearSceneViewportOverride() { + this.sceneViewportOverride = null; + } + public boolean isGridButtonEnabled() { return gridButtonEnabled || ModConfig.debug.enableDebugMode; } @@ -2032,11 +2042,14 @@ protected void onLayoutMoved(int deltaX, int deltaY) {} @Override public void render(RenderContext context) { - int sceneW = layoutSceneWidth > 0 ? layoutSceneWidth - : getBounds().width() - buttonColumnReserve() - layoutSceneOffsetX; + LytSize vpOverride = sceneViewportOverride; + int sceneW = vpOverride != null ? vpOverride.width() + : layoutSceneWidth > 0 ? layoutSceneWidth + : getBounds().width() - buttonColumnReserve() - layoutSceneOffsetX; if (sceneW < 16) sceneW = 16; int sliderAreaHeight = getBottomControlAreaHeight(); - int sceneH = layoutSceneHeight > 0 ? layoutSceneHeight : Math.max(16, getBounds().height() - sliderAreaHeight); + int sceneH = vpOverride != null ? vpOverride.height() + : layoutSceneHeight > 0 ? layoutSceneHeight : Math.max(16, getBounds().height() - sliderAreaHeight); int totalH = reserveBottomControlArea ? Math.max(sceneH + sliderAreaHeight, getBounds().height()) : Math.max(sceneH, getBounds().height()); LytRect outerRect = new LytRect( @@ -3871,9 +3884,21 @@ public void activateSceneButton(GuideIconButton.Role role) { } } case RESET_VIEW -> resetViewToInitialCamera(); - case PONDER_PREV_KEYFRAME -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); ponderPrevKeyframe(); } - case PONDER_PLAY_PAUSE -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); ponderTogglePlay(); } - case PONDER_RESTART -> { FMLLog.getLogger().info("[PonderDebug] activateSceneButton: PONDER_RESTART"); ponderRestart(); } + case PONDER_PREV_KEYFRAME -> { + FMLLog.getLogger() + .info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); + ponderPrevKeyframe(); + } + case PONDER_PLAY_PAUSE -> { + FMLLog.getLogger() + .info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); + ponderTogglePlay(); + } + case PONDER_RESTART -> { + FMLLog.getLogger() + .info("[PonderDebug] activateSceneButton: PONDER_RESTART"); + ponderRestart(); + } default -> {} } } @@ -4408,14 +4433,23 @@ public void ponderTick() { sceneAnimationTick++; if (ponderSceneData == null || ponderPaused || ponderFinished) { if (sceneAnimationTick % 100 == 1) { - FMLLog.getLogger().info("[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", - ponderSceneData != null, ponderPaused, ponderFinished, ponderCurrentTick); + FMLLog.getLogger() + .info( + "[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", + ponderSceneData != null, + ponderPaused, + ponderFinished, + ponderCurrentTick); } return; } ponderCurrentTick++; if (ponderCurrentTick % 20 == 0) { - FMLLog.getLogger().info("[PonderDebug] ponderTick advancing: tick={}/{}", ponderCurrentTick, ponderSceneData.getTotalTime()); + FMLLog.getLogger() + .info( + "[PonderDebug] ponderTick advancing: tick={}/{}", + ponderCurrentTick, + ponderSceneData.getTotalTime()); } if (ponderCurrentTick >= ponderSceneData.getTotalTime()) { ponderCurrentTick = ponderSceneData.getTotalTime(); @@ -4442,10 +4476,16 @@ public void ponderTick() { public void ponderTogglePlay() { if (ponderSceneData == null) { - FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); + FMLLog.getLogger() + .info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); return; } - FMLLog.getLogger().info("[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", ponderPaused, ponderFinished, ponderCurrentTick); + FMLLog.getLogger() + .info( + "[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", + ponderPaused, + ponderFinished, + ponderCurrentTick); if (ponderFinished) { ponderCurrentTick = 0; ponderFinished = false; From e1a7a16bb71fe78acfa61d7214931db616308c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:05:07 +0800 Subject: [PATCH 083/136] fix: sprite UV drift and LaTeX rendering in Mermaid NodeContent - Override blitGuiSprite in NodeContentRenderContext to use GL scale for display size while keeping UV range fixed to sprite dimensions - Add raw-GL rendering path for LytLatexBlock / LytLatexDisplayBlock that applies mindmap zoom via GL matrix transforms --- .../block/LytMermaidMindmapCanvas.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index d22d5d66..8133913a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -28,6 +28,7 @@ import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeShape; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; +import com.hfstudio.guidenh.guide.render.GuiSprite; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; @@ -697,6 +698,18 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod renderNodeContentBlock(childBlock, nodeContext, nativeContext, contentViewport); } } + } else if (block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock) { + // LaTeX renders with raw GL, bypassing RenderContext coordinate mapping. + // Apply mindmap zoom via GL matrix and use nativeContext so that the + // document-level GL transform chain remains intact. + GL11.glPushMatrix(); + GL11.glTranslatef(contentViewport.x(), contentViewport.y(), 0f); + GL11.glScalef(zoom, zoom, 1f); + try { + block.render(nativeContext); + } finally { + GL11.glPopMatrix(); + } } else { block.render(nodeContext); } @@ -1236,6 +1249,28 @@ private void renderScaledItem(ItemStack stack, int x, int y, boolean overlay) { } } + @Override + public void blitGuiSprite(LytRect rect, GuiSprite sprite) { + if (sprite == null) return; + int sx = scaleX(rect.x()); + int sy = scaleY(rect.y()); + GL11.glPushMatrix(); + GL11.glTranslatef(sx, sy, 0f); + GL11.glScalef(scale, scale, 1f); + try { + delegate.blitTexture( + sprite.getTexture(), + 0, + 0, + sprite.getU(), + sprite.getV(), + sprite.getWidth(), + sprite.getHeight()); + } finally { + GL11.glPopMatrix(); + } + } + @Override public void blitTexture(ResourceLocation texture, int x, int y, int u, int v, int width, int height) { delegate.blitTexture(texture, scaleX(x), scaleY(y), u, v, scaleLength(width), scaleLength(height)); From ca68b6774aaf18699f51e84f8fa2ba01ad8281b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:05:07 +0800 Subject: [PATCH 084/136] fix: sprite UV drift and LaTeX rendering in Mermaid NodeContent - Override blitGuiSprite in NodeContentRenderContext to use GL scale for display size while keeping UV range fixed to sprite dimensions - Add raw-GL rendering path for LytLatexBlock / LytLatexDisplayBlock that applies mindmap zoom via GL matrix transforms --- .../block/LytMermaidMindmapCanvas.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index d22d5d66..8133913a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -28,6 +28,7 @@ import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeShape; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; +import com.hfstudio.guidenh.guide.render.GuiSprite; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; @@ -697,6 +698,18 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod renderNodeContentBlock(childBlock, nodeContext, nativeContext, contentViewport); } } + } else if (block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock) { + // LaTeX renders with raw GL, bypassing RenderContext coordinate mapping. + // Apply mindmap zoom via GL matrix and use nativeContext so that the + // document-level GL transform chain remains intact. + GL11.glPushMatrix(); + GL11.glTranslatef(contentViewport.x(), contentViewport.y(), 0f); + GL11.glScalef(zoom, zoom, 1f); + try { + block.render(nativeContext); + } finally { + GL11.glPopMatrix(); + } } else { block.render(nodeContext); } @@ -1236,6 +1249,28 @@ private void renderScaledItem(ItemStack stack, int x, int y, boolean overlay) { } } + @Override + public void blitGuiSprite(LytRect rect, GuiSprite sprite) { + if (sprite == null) return; + int sx = scaleX(rect.x()); + int sy = scaleY(rect.y()); + GL11.glPushMatrix(); + GL11.glTranslatef(sx, sy, 0f); + GL11.glScalef(scale, scale, 1f); + try { + delegate.blitTexture( + sprite.getTexture(), + 0, + 0, + sprite.getU(), + sprite.getV(), + sprite.getWidth(), + sprite.getHeight()); + } finally { + GL11.glPopMatrix(); + } + } + @Override public void blitTexture(ResourceLocation texture, int x, int y, int u, int v, int width, int height) { delegate.blitTexture(texture, scaleX(x), scaleY(y), u, v, scaleLength(width), scaleLength(height)); From 144d7ad06315aeec430442e27287b1065768bcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:46:03 +0800 Subject: [PATCH 085/136] fix: F3+T cache gaps, editor preview placeholders, SceneScript camera centering - Clear GuideLatexTextureCache, GuideSceneStructureCache, and 5 StructureLibBoundedCache instances on F3+T resource reload - Dispatch MOUNT events to editor preview document so async scripts materialize placeholder blocks in the guide editor split view - Reorder SceneScript camera setup to zoom -> size -> center and restore offset save/restore logic, matching Phase 2 SceneTagCompiler --- .../com/hfstudio/guidenh/ClientProxy.java | 63 ++--- .../compiler/MdxBlockTagSourceExtractor.java | 80 ++++++- .../guidenh/guide/compiler/PageCompiler.java | 47 ++-- .../compiler/tags/BlockImageCompiler.java | 29 ++- .../compiler/tags/BlockquoteCompiler.java | 38 +-- .../guide/compiler/tags/CodeCompiler.java | 9 +- .../guide/compiler/tags/CsvTableCompiler.java | 6 +- .../compiler/tags/DelUWaveMarkCompiler.java | 3 +- .../guide/compiler/tags/HeadingCompiler.java | 4 +- .../guide/compiler/tags/ItemGridCompiler.java | 1 + .../compiler/tags/ItemImageCompiler.java | 14 +- .../guide/compiler/tags/ListCompiler.java | 3 +- .../guide/compiler/tags/ListItemCompiler.java | 2 +- .../guidenh/guide/compiler/tags/MdxAttrs.java | 5 - .../guide/compiler/tags/MermaidCompiler.java | 44 +++- .../guide/compiler/tags/PreCompiler.java | 32 +-- .../compiler/tags/StructureViewCompiler.java | 2 + .../guide/compiler/tags/SubPagesCompiler.java | 7 +- .../guide/compiler/tags/TableCompiler.java | 6 +- .../tags/mediawiki/CategoryCompiler.java | 11 +- .../tags/mediawiki/SpecialCompiler.java | 12 +- .../guidenh/guide/document/block/LytNode.java | 27 ++- .../guide/document/flow/LytFlowContent.java | 28 ++- .../document/flow/LytFlowInlineBlock.java | 3 +- .../GuideLightweightReloadService.java | 15 +- .../guidenh/guide/internal/GuideScreen.java | 84 ++++--- .../guidenh/guide/internal/MutableGuide.java | 4 +- .../resolver/MdxSyntaxResolver.java | 2 +- .../editor/md/SceneEditorMarkdownCodec.java | 2 +- .../home/HomePageSummaryExtractor.java | 6 +- .../guide/internal/host/DeferredTask.java | 13 +- .../guidenh/guide/internal/host/LytEvent.java | 36 ++- .../guidenh/guide/internal/host/LytHost.java | 126 ++++++---- .../guide/internal/host/LytHostWorkItem.java | 12 +- .../guide/internal/host/LytScript.java | 3 + .../guide/internal/host/NavigationState.java | 74 ++++-- .../guide/internal/host/ScriptContext.java | 8 +- .../internal/host/ScriptContextImpl.java | 17 +- .../guide/internal/host/ViewportState.java | 42 +++- .../internal/host/scripts/CategoryScript.java | 12 +- .../host/scripts/CommandLinkScript.java | 10 +- .../internal/host/scripts/CsvTableScript.java | 12 +- .../host/scripts/FloatingImageScript.java | 12 +- .../internal/host/scripts/ImageScript.java | 12 +- .../internal/host/scripts/ItemGridScript.java | 9 +- .../host/scripts/ItemImageScript.java | 14 +- .../internal/host/scripts/KeyBindScript.java | 4 +- .../internal/host/scripts/MermaidScript.java | 43 +++- .../host/scripts/PlayerNameScript.java | 4 +- .../host/scripts/QuestCardScript.java | 28 ++- .../host/scripts/QuestLinkScript.java | 33 +-- .../internal/host/scripts/SceneScript.java | 148 ++++++++---- .../internal/host/scripts/SpecialScript.java | 23 +- .../internal/host/scripts/SubPagesScript.java | 8 +- .../internal/host/scripts/TooltipScript.java | 11 +- .../markdown/MarkdownListSemantics.java | 9 +- .../markdown/MarkdownRuntimeBlocks.java | 22 +- .../markdown/MdAstToMdxConverter.java | 87 ++++--- .../internal/scheduler/DevWatchWorkItem.java | 8 +- .../scheduler/LytHostPreheatItem.java | 9 +- .../internal/scheduler/MasterScheduler.java | 14 +- .../scheduler/SearchIndexWorkItem.java | 12 +- .../guide/internal/scheduler/WorkItem.java | 3 + .../guide/internal/search/PageIndexer.java | 11 +- .../guide/mediawiki/MediaWikiListPlanner.java | 4 +- .../guidenh/guide/scene/SceneTagCompiler.java | 77 ++++--- .../guide/scene/SceneViewportMetrics.java | 29 ++- .../site/GuideSiteHtmlCompiler.java | 216 +++++++++++------- .../site/GuideSiteMdxTagRenderer.java | 2 +- .../compiler/QuestCardCompiler.java | 1 + .../hfstudio/guidenh/libs/mdx/FactoryTag.java | 6 +- 71 files changed, 1215 insertions(+), 598 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 945c5036..0ee45df4 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -18,9 +18,7 @@ import com.hfstudio.guidenh.guide.internal.GuideDevelopmentResourcePackWatcher; import com.hfstudio.guidenh.guide.internal.GuideME; import com.hfstudio.guidenh.guide.internal.GuideOnStartup; -import com.hfstudio.guidenh.guide.internal.GuideRegistry; import com.hfstudio.guidenh.guide.internal.GuideReloadListener; -import com.hfstudio.guidenh.guide.internal.GuideScreenMemory; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; @@ -45,32 +43,8 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.OreDictProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.TagNameProvider; -import com.hfstudio.guidenh.guide.internal.home.GuideScreenHomeHistory; -import com.hfstudio.guidenh.guide.scene.level.GuidebookFakeWorld; -import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; -import com.hfstudio.guidenh.integration.GuideNhClientIntegrationBootstrap; -import com.hfstudio.guidenh.integration.Mods; -import com.hfstudio.guidenh.integration.ae2.network.Ae2NetworkRegistration; -import com.hfstudio.guidenh.integration.nei.GuideScreenNeiBridge; -import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; -import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; -import com.hfstudio.guidenh.network.GuideNhNetwork; -import com.hfstudio.guidenh.network.GuideNhRegionExportClientHandler; -import com.hfstudio.guidenh.network.GuideNhRegionExportReplyMessage; -import com.hfstudio.structurelibexport.StructureExportBootstrap; - -import cpw.mods.fml.common.event.FMLInitializationEvent; -import cpw.mods.fml.common.event.FMLLoadCompleteEvent; -import cpw.mods.fml.common.event.FMLPostInitializationEvent; -import cpw.mods.fml.common.event.FMLPreInitializationEvent; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import cpw.mods.fml.common.network.FMLNetworkEvent; -import com.hfstudio.guidenh.guide.internal.scheduler.MasterScheduler; -import com.hfstudio.guidenh.guide.internal.scheduler.SearchIndexWorkItem; -import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.LytHostWorkItem; -import com.hfstudio.guidenh.guide.internal.scheduler.LytHostPreheatItem; import com.hfstudio.guidenh.guide.internal.host.scripts.BlockImageScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CategoryScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CommandLinkScript; @@ -92,7 +66,29 @@ import com.hfstudio.guidenh.guide.internal.host.scripts.StructureScript; import com.hfstudio.guidenh.guide.internal.host.scripts.SubPagesScript; import com.hfstudio.guidenh.guide.internal.host.scripts.TooltipScript; +import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; +import com.hfstudio.guidenh.guide.internal.scheduler.LytHostPreheatItem; +import com.hfstudio.guidenh.guide.internal.scheduler.MasterScheduler; +import com.hfstudio.guidenh.guide.internal.scheduler.SearchIndexWorkItem; +import com.hfstudio.guidenh.guide.scene.level.GuidebookFakeWorld; +import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; +import com.hfstudio.guidenh.integration.GuideNhClientIntegrationBootstrap; +import com.hfstudio.guidenh.integration.Mods; +import com.hfstudio.guidenh.integration.ae2.network.Ae2NetworkRegistration; +import com.hfstudio.guidenh.integration.nei.GuideScreenNeiBridge; +import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; +import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; +import com.hfstudio.guidenh.network.GuideNhNetwork; +import com.hfstudio.guidenh.network.GuideNhRegionExportClientHandler; +import com.hfstudio.guidenh.network.GuideNhRegionExportReplyMessage; +import com.hfstudio.structurelibexport.StructureExportBootstrap; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLLoadCompleteEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.network.FMLNetworkEvent; import cpw.mods.fml.relauncher.Side; public class ClientProxy extends CommonProxy { @@ -164,9 +160,12 @@ public void init(FMLInitializationEvent event) { CycleRegionWandModeHotkey.init(); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); MasterScheduler.init(); - MasterScheduler.getInstance().submit(new LytHostWorkItem(lytHost)); - MasterScheduler.getInstance().submit(new LytHostPreheatItem(lytHost)); - MasterScheduler.getInstance().submit(new SearchIndexWorkItem()); + MasterScheduler.getInstance() + .submit(new LytHostWorkItem(lytHost)); + MasterScheduler.getInstance() + .submit(new LytHostPreheatItem(lytHost)); + MasterScheduler.getInstance() + .submit(new SearchIndexWorkItem()); // Phase 3: LytScript registrations lytHost.registerScript("CommandLink", new CommandLinkScript()); @@ -229,7 +228,8 @@ public void postInit(FMLPostInitializationEvent event) { public void completeInit(FMLLoadCompleteEvent event) { super.completeInit(event); GuideDevelopmentResourcePackWatcher.init(); - MasterScheduler.getInstance().submit(new DevWatchWorkItem()); + MasterScheduler.getInstance() + .submit(new DevWatchWorkItem()); GuideOnStartup.init(); } @@ -238,6 +238,7 @@ public void onClientDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEven GuideNH.LOG.info("Minecraft client disconnected. Stopping GuideNH runtime bridge session state"); runtimeBridge.stop(); GuideME.closeSearch(); - lytHost.getNavigation().clear(); + lytHost.getNavigation() + .clear(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java index 2af920e3..79b7069c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.unist.UnistPoint; import com.hfstudio.guidenh.libs.unist.UnistPosition; public class MdxBlockTagSourceExtractor { @@ -24,13 +25,7 @@ private MdxBlockTagSourceExtractor() {} return null; } - int sourceStart = position.start() - .offset(); - if (sourceStart < 0 || sourceStart >= sourceText.length()) { - return null; - } - - int openingTagStart = findOpeningTagStart(sourceText, sourceStart, tagName); + int openingTagStart = findOpeningTagStart(sourceText, position, tagName); if (openingTagStart < 0) { return null; } @@ -50,11 +45,80 @@ private MdxBlockTagSourceExtractor() {} return sourceText.substring(openingTagEnd + 1, closingTagStart); } - private static int findOpeningTagStart(String sourceText, int sourceStart, String tagName) { + private static int findOpeningTagStart(String sourceText, UnistPosition position, String tagName) { + UnistPoint start = position.start(); + if (start == null) { + return -1; + } + + // Preprocessors can shift offsets while still keeping line and column stable. + int lineBasedStart = findOpeningTagStartOnDeclaredLine(sourceText, start, tagName); + if (lineBasedStart >= 0) { + return lineBasedStart; + } + + int sourceStart = start.offset(); + if (sourceStart < 0 || sourceStart >= sourceText.length()) { + return -1; + } if (matchesOpeningTagName(sourceText, sourceStart, tagName)) { return sourceStart; } + return findOpeningTagStartByOffset(sourceText, sourceStart, tagName); + } + + private static int findOpeningTagStartOnDeclaredLine(String sourceText, UnistPoint start, String tagName) { + int lineStart = findLineStartOffset(sourceText, start.line()); + if (lineStart < 0) { + return -1; + } + + int lineEnd = sourceText.indexOf('\n', lineStart); + if (lineEnd < 0) { + lineEnd = sourceText.length(); + } + + int declaredOffset = lineStart + Math.max(0, start.column() - 1); + if (declaredOffset < lineEnd && matchesOpeningTagName(sourceText, declaredOffset, tagName)) { + return declaredOffset; + } + + for (int index = Math.min(declaredOffset, lineEnd - 1); index < lineEnd; index++) { + if (sourceText.charAt(index) == '<' && matchesOpeningTagName(sourceText, index, tagName)) { + return index; + } + } + + for (int searchStart = Math.min(declaredOffset - 1, lineEnd - 1); searchStart >= lineStart;) { + int candidate = sourceText.lastIndexOf('<', searchStart); + if (candidate < lineStart) { + break; + } + if (matchesOpeningTagName(sourceText, candidate, tagName)) { + return candidate; + } + searchStart = candidate - 1; + } + return -1; + } + + private static int findLineStartOffset(String sourceText, int lineNumber) { + if (lineNumber <= 0) { + return -1; + } + + int currentLine = 1; + int index = 0; + while (currentLine < lineNumber && index < sourceText.length()) { + if (sourceText.charAt(index++) == '\n') { + currentLine++; + } + } + return currentLine == lineNumber ? index : -1; + } + + private static int findOpeningTagStartByOffset(String sourceText, int sourceStart, String tagName) { int searchStart = sourceStart; while (searchStart >= 0) { int candidate = sourceText.lastIndexOf('<', searchStart); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 0f7b9198..c3ab6e88 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -24,6 +24,7 @@ import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.document.block.LatexRenderOptions; import com.hfstudio.guidenh.guide.document.block.LatexVerticalAlign; import com.hfstudio.guidenh.guide.document.block.LytBlock; @@ -35,7 +36,6 @@ import com.hfstudio.guidenh.guide.document.block.LytLatexBlock; import com.hfstudio.guidenh.guide.document.block.LytLatexDisplayBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.block.LytThematicBreak; import com.hfstudio.guidenh.guide.document.block.table.LytTable; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; @@ -51,12 +51,11 @@ import com.hfstudio.guidenh.guide.internal.markdown.FootnotePreprocessor; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownActionLink; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownHtmlRuntimeNormalizer; -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLatexShorthand; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLiteralAutolink; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.internal.util.LangUtil; -import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.sound.GuideSoundParsers; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; @@ -69,19 +68,13 @@ import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstBreak; import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; -import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis; -import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode; import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph; import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstPhrasingContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstPosition; import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; -import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; -import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak; import com.hfstudio.guidenh.libs.mdx.MdxCommentMasker; import com.hfstudio.guidenh.libs.micromark.ParseException; import com.hfstudio.guidenh.libs.unist.UnistNode; @@ -277,8 +270,10 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource * deferring the full Micromark → mdast pipeline to first call of * {@link ParsedGuidePage#getAstRoot()}. * - *

    F3+T reload uses this path so that index/navigation rebuilds — - * which only need frontmatter — complete without paying Micromark cost.

    + *

    + * F3+T reload uses this path so that index/navigation rebuilds — + * which only need frontmatter — complete without paying Micromark cost. + *

    */ public static ParsedGuidePage parseFrontmatterOnly(String sourcePack, String language, ResourceLocation id, String pageContent) { @@ -290,10 +285,10 @@ public static ParsedGuidePage parseFrontmatterOnly(String sourcePack, String lan sourcePack, id, pageContent, - null, // astRoot — triggers lazy parse on first getAstRoot() + null, // astRoot — triggers lazy parse on first getAstRoot() sourceFrontmatter, language, - null, // no parse failure yet + null, // no parse failure yet null, null); } @@ -545,7 +540,7 @@ public void compileInlineFragment(Collection children for (var nestedChild : el.children()) { compileFlowContent(layoutParent, nestedChild); } - } else if (child instanceof MdAstParent nestedParent) { + } else if (child instanceof MdAstParentnestedParent) { for (var nestedChild : nestedParent.children()) { compileFlowContent(layoutParent, nestedChild); } @@ -590,12 +585,11 @@ public void compileBlockContext(List children, LytBlo } else { var compiler = tagCompilers.get(el.name()); if (compiler == null) { - layoutChild = createErrorBlock( - "Unhandled MDX element in block context: " + el.name(), child); + layoutChild = createErrorBlock("Unhandled MDX element in block context: " + el.name(), child); } else { - layoutChild = null; - compiler.compileBlockContext(this, layoutParent, el); - } + layoutChild = null; + compiler.compileBlockContext(this, layoutParent, el); + } } } else if (child instanceof MdxJsxTextElement el) { // Inline element at block level — merge into previous paragraph when possible @@ -629,7 +623,9 @@ public void compileBlockContext(List children, LytBlo layoutChild = null; // handled via element } else { layoutChild = createErrorBlock( - "Unhandled node in block context: " + child.getClass().getSimpleName(), child); + "Unhandled node in block context: " + child.getClass() + .getSimpleName(), + child); } if (layoutChild != null) { @@ -779,15 +775,18 @@ private void compileFlowContent(LytFlowParent layoutParent, MdAstAnyContent cont var compiler = tagCompilers.get(el.name()); if (compiler == null) { layoutChild = createErrorFlowContent( - "Unhandled MDX element in flow context: " + el.name(), content); + "Unhandled MDX element in flow context: " + el.name(), + content); } else { - layoutChild = null; - compiler.compileFlowContext(this, layoutParent, el); + layoutChild = null; + compiler.compileFlowContext(this, layoutParent, el); } } } else { layoutChild = createErrorFlowContent( - "Unhandled node in flow context: " + content.getClass().getSimpleName(), content); + "Unhandled node in flow context: " + content.getClass() + .getSimpleName(), + content); } if (layoutChild != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java index 66a7a872..8aefabb7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java @@ -22,7 +22,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl String id = MdxAttrs.getString(compiler, parent, el, "id", null); String ore = MdxAttrs.getString(compiler, parent, el, "ore", null); - if ((id == null || id.trim().isEmpty()) && (ore == null || ore.trim().isEmpty())) { + if ((id == null || id.trim() + .isEmpty()) && (ore == null + || ore.trim() + .isEmpty())) { parent.appendError(compiler, "Missing id attribute (or ore).", el); return; } @@ -36,7 +39,14 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // Create placeholder block that carries all extracted config to BlockImageScript BlockImagePlaceholder placeholder = new BlockImagePlaceholder( - id, ore, meta, nbt, scale, perspective, width, height); + id, + ore, + meta, + nbt, + scale, + perspective, + width, + height); placeholder.setStyleClass("BlockImage"); placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); placeholder.appendText("[BlockImage]"); @@ -48,17 +58,22 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl * creation by {@code BlockImageScript}. */ public static class BlockImagePlaceholder extends LytParagraph { - @Nullable public final String id; - @Nullable public final String ore; + + @Nullable + public final String id; + @Nullable + public final String ore; public final int meta; - @Nullable public final String nbt; + @Nullable + public final String nbt; public final float scale; - @Nullable public final String perspective; + @Nullable + public final String perspective; public final int width; public final int height; public BlockImagePlaceholder(@Nullable String id, @Nullable String ore, int meta, @Nullable String nbt, - float scale, @Nullable String perspective, int width, int height) { + float scale, @Nullable String perspective, int width, int height) { this.id = id; this.ore = ore; this.meta = meta; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java index ccb2a59f..8560dbca 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java @@ -3,22 +3,22 @@ import java.util.Collections; import java.util.Set; +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytAlertBox; -import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; import com.hfstudio.guidenh.guide.document.block.LytVBox; -import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.BlockquoteDirective; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; import com.hfstudio.guidenh.guide.style.BorderStyle; -import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import org.jetbrains.annotations.Nullable; public class BlockquoteCompiler extends BlockTagCompiler { @@ -33,7 +33,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (directive != null && directive.alertType() != null) { LytAlertBox alertBox = new LytAlertBox(); alertBox.setTitle( - directive.alertType().displayText(), + directive.alertType() + .displayText(), directive.alertType()); alertBox.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); alertBox.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); @@ -69,20 +70,26 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl parent.append(PageCompiler.wrapFloatAwareIfNeeded(blockquote)); } - private void compileDirectiveBody(PageCompiler compiler, BlockquoteDirective directive, - LytBlockContainer parent) { + private void compileDirectiveBody(PageCompiler compiler, BlockquoteDirective directive, LytBlockContainer parent) { // When there's a remainingText override and the first paragraph is still present // at the head of the children list, replace its leading text. // Otherwise — just compile children normally. - if (!directive.children().isEmpty() && directive.firstParagraph() != null - && directive.children().get(0) == directive.firstParagraph() - && directive.remainingText() != null && !directive.remainingText().isEmpty()) { + if (!directive.children() + .isEmpty() && directive.firstParagraph() != null + && directive.children() + .get(0) == directive.firstParagraph() + && directive.remainingText() != null + && !directive.remainingText() + .isEmpty()) { // Clone the first paragraph with the remaining text overriding the leading text - compiler.compileBlockContext( - Collections.singletonList(directive.firstParagraph()), parent); - for (int i = 1; i < directive.children().size(); i++) { + compiler.compileBlockContext(Collections.singletonList(directive.firstParagraph()), parent); + for (int i = 1; i < directive.children() + .size(); i++) { compiler.compileBlockContext( - Collections.singletonList(directive.children().get(i)), parent); + Collections.singletonList( + directive.children() + .get(i)), + parent); } } else { compiler.compileBlockContext(directive.children(), parent); @@ -110,8 +117,7 @@ private void shiftFirstParagraphDown(LytNode box, int pixels) { } @Nullable - private LytFlowContent buildQuoteIcon( - @Nullable QuoteIconSpec icon) { + private LytFlowContent buildQuoteIcon(@Nullable QuoteIconSpec icon) { // The original buildQuoteIcon resolved item stacks from icon specs. // For now return null — icon rendering will be added in a later phase. return null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java index 388be265..ee03cc03 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java @@ -22,11 +22,16 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen var text = new LytFlowText(); // Extract text from child — has one MdAstText child from converter String value = ""; - if (!el.children().isEmpty() && el.children().get(0) instanceof MdAstText t) { + if (!el.children() + .isEmpty() + && el.children() + .get(0) instanceof MdAstText t) { value = t.value; } text.setText(value); - text.modifyStyle(style -> style.italic(true).whiteSpace(WhiteSpaceMode.PRE)); + text.modifyStyle( + style -> style.italic(true) + .whiteSpace(WhiteSpaceMode.PRE)); parent.append(text); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index e996e4cf..46a1ff41 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -11,11 +11,11 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; -import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.table.LytTable; import com.hfstudio.guidenh.guide.document.block.table.LytTableCell; +import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -62,7 +62,8 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } catch (MdxAttrs.AttributeException e) { return; } - if (src != null && !src.trim().isEmpty()) { + if (src != null && !src.trim() + .isEmpty()) { try { ResourceLocation csvId = IdUtils.resolveLink(src.trim(), indexer.getPageId()); byte[] data = indexer.loadAsset(csvId); @@ -168,6 +169,7 @@ private static void appendCellContent(LytTableCell cell, String value) { } public static class CsvTablePlaceholder extends LytParagraph { + public final String src; public final boolean header; public final List widths; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java index c221698d..f0d7f46a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java @@ -1,9 +1,8 @@ package com.hfstudio.guidenh.guide.compiler.tags; -import java.util.Set; import java.util.LinkedHashSet; +import java.util.Set; -import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java index d49ab224..a6061cdf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java @@ -12,8 +12,8 @@ public class HeadingCompiler extends BlockTagCompiler { - private static final Set TAG_NAMES = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6"))); + private static final Set TAG_NAMES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6"))); @Override public Set getTagNames() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java index 9fff4082..70321019 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java @@ -43,6 +43,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) {} public static class ItemGridPlaceholder extends LytParagraph { + public final List itemIds; public ItemGridPlaceholder(List itemIds) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java index 27df56b7..fcbc0eba 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java @@ -74,7 +74,15 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen labelFormat = (formatRaw != null && !formatRaw.isEmpty()) ? formatRaw : null; ItemImagePlaceholder placeholder = new ItemImagePlaceholder( - itemId, scale, yOffset, labelYOffset, showTooltip, showIcon, labelPosition, labelFormat, ore); + itemId, + scale, + yOffset, + labelYOffset, + showTooltip, + showIcon, + labelPosition, + labelFormat, + ore); var inline = new LytFlowInlineBlock(); inline.setBlock(placeholder); @@ -97,6 +105,7 @@ public static String resolveLabelPosition(@Nullable String raw) { } public static class ItemImagePlaceholder extends LytParagraph { + public final String itemId; public final float scale; @Nullable @@ -115,8 +124,7 @@ public static class ItemImagePlaceholder extends LytParagraph { public ItemImagePlaceholder(String itemId, float scale, @Nullable Integer yOffset, @Nullable Integer labelYOffset, boolean showTooltip, @Nullable Boolean showIcon, - @Nullable String labelPosition, @Nullable String labelFormat, - @Nullable String ore) { + @Nullable String labelPosition, @Nullable String labelFormat, @Nullable String ore) { this.itemId = itemId; this.scale = scale; this.yOffset = yOffset; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java index 1bef98f8..7fca31f7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java @@ -12,8 +12,7 @@ public class ListCompiler extends BlockTagCompiler { - private static final Set TAG_NAMES = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList("ul", "ol"))); + private static final Set TAG_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("ul", "ol"))); @Override public Set getTagNames() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java index 7dacf87b..a8c77f68 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java @@ -20,7 +20,7 @@ public Set getTagNames() { } @Override - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({ "unchecked", "rawtypes" }) protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { LytListItem listItem; var taskMarker = MarkdownListSemantics.extractTaskMarker((List) el.children()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java index 14e17b1c..6757a7ab 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java @@ -4,7 +4,6 @@ import java.util.regex.Pattern; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import org.jetbrains.annotations.Nullable; @@ -14,7 +13,6 @@ import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; -import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.LytErrorSink; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; @@ -69,9 +67,6 @@ public static ResourceLocation getRequiredId(PageCompiler compiler, LytErrorSink } } - - - @Nullable public static GuideItemReferenceResolver.ResolvedBlockReference getRequiredBlockReference(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields el, String attribute) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java index 4cc7c691..502e1cb8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java @@ -16,6 +16,7 @@ import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeContentExtractor; +import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; @@ -50,10 +51,11 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } src = mermaidId.toString(); } else { - String rawTagBodySource = compiler.getBlockTagChildrenSource(el); - if (rawTagBodySource != null && !rawTagBodySource.trim() - .isEmpty()) { - sourceText = MermaidMindmapNodeContentExtractor.stripExplicitNodeContentBlocks(rawTagBodySource); + // Prefer raw source text so Mermaid DSL syntax (brackets, links, etc.) + // is not consumed by markdown parsing. PR #24. + String rawSource = compiler.getBlockTagChildrenSource(el); + if (rawSource != null) { + sourceText = MermaidMindmapParser.normalize(stripNodeContentBlocks(rawSource)); } else { sourceText = MermaidMindmapNodeContentExtractor.extractDiagramSource(el.children()); } @@ -138,6 +140,7 @@ private LytBlock compileNodeContentBlock(PageCompiler compiler, MdxJsxFlowElemen } public static class MermaidPlaceholder extends LytParagraph { + public final String src; public final String sourceText; public final int width; @@ -155,4 +158,37 @@ public MermaidPlaceholder(String src, String sourceText, int width, int height, setStyle(LytParagraph.PLACEHOLDER_STYLE); } } + + private static String stripNodeContentBlocks(String source) { + StringBuilder result = new StringBuilder(source.length()); + int depth = 0; + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + if (depth == 0 && source.startsWith("<", i)) { + int tagEnd = source.indexOf('>', i); + if (tagEnd > i) { + String tag = source.substring(i, tagEnd + 1); + if (tag.startsWith(" 0) { + if (source.startsWith("", i)) { + depth--; + if (depth == 0) { + i += "".length() - 1; + continue; + } + } else if (source.startsWith(" getTagNames() { @@ -114,7 +112,8 @@ private LytBlock compileCsvCodeBlock(String source, @Nullable String meta) { } private CsvFenceMeta parseCsvFenceMeta(@Nullable String meta) { - if (meta == null || meta.trim().isEmpty()) { + if (meta == null || meta.trim() + .isEmpty()) { return new CsvFenceMeta(true, Collections.emptyList()); } @@ -190,12 +189,9 @@ private record CsvFenceMeta(boolean header, List widthHints) {} private @Nullable LytMermaidMindmap tryCompileMermaidMindmap(String source) { try { String normalized = MermaidMindmapParser.normalize(source); - LytMermaidMindmap block = new LytMermaidMindmap( - MermaidMindmapParser.parse(normalized), normalized); + LytMermaidMindmap block = new LytMermaidMindmap(MermaidMindmapParser.parse(normalized), normalized); FMLLog.getLogger() - .info( - "[GuideNH] [PreCompiler] Compiled fenced Mermaid runtime block ({} chars)", - normalized.length()); + .info("[GuideNH] [PreCompiler] Compiled fenced Mermaid runtime block ({} chars)", normalized.length()); return block; } catch (IllegalArgumentException e) { FMLLog.getLogger() @@ -227,7 +223,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } private static @Nullable Integer parseCodeBlockWidth(@Nullable String meta) { - if (meta == null || meta.trim().isEmpty()) { + if (meta == null || meta.trim() + .isEmpty()) { return null; } Matcher matcher = CODEBLOCK_META_WIDTH.matcher(meta); @@ -236,7 +233,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } String value = matcher.group(3) != null ? matcher.group(3) : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); - if (value == null || value.trim().isEmpty()) { + if (value == null || value.trim() + .isEmpty()) { return null; } try { @@ -247,7 +245,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } private static @Nullable Integer parseCodeBlockHeight(@Nullable String meta) { - if (meta == null || meta.trim().isEmpty()) { + if (meta == null || meta.trim() + .isEmpty()) { return null; } Matcher matcher = CODEBLOCK_META_HEIGHT.matcher(meta); @@ -256,7 +255,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } String value = matcher.group(3) != null ? matcher.group(3) : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); - if (value == null || value.trim().isEmpty()) { + if (value == null || value.trim() + .isEmpty()) { return null; } try { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java index 6704a0b2..4fbe9a13 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java @@ -132,6 +132,7 @@ private static int findWhitespace(String line, int offset) { } public static class StructureEntry { + public final int x; public final int y; public final int z; @@ -146,6 +147,7 @@ public StructureEntry(int x, int y, int z, String idSpec) { } public static class StructurePlaceholder extends LytParagraph { + public final int width; public final int height; public final List entries; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java index 3d48d704..58da6d5b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java @@ -20,20 +20,23 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl var pageIdStr = el.getAttributeString("id", null); if (pageIdStr != null) { try { - pageIdStr = compiler.resolveId(pageIdStr).toString(); + pageIdStr = compiler.resolveId(pageIdStr) + .toString(); } catch (Exception e) { parent.appendError(compiler, "Invalid id: " + pageIdStr, el); return; } } var alphabetical = MdxAttrs.getBoolean(compiler, parent, el, "alphabetical", false); - var currentPageId = compiler.getPageId().toString(); + var currentPageId = compiler.getPageId() + .toString(); SubPagesPlaceholder placeholder = new SubPagesPlaceholder(pageIdStr, alphabetical, currentPageId); parent.append(placeholder); } public static class SubPagesPlaceholder extends LytParagraph { + public final String pageIdStr; public final boolean alphabetical; public final String currentPageId; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java index fadb99f4..4dec42cf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java @@ -38,7 +38,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl .parseWidthHints(extractKramdownExpression(content)); var columns = table.getColumns(); for (int wi = 0; wi < widths.size() && wi < columns.size(); wi++) { - columns.get(wi).setPreferredWidth(widths.get(wi)); + columns.get(wi) + .setPreferredWidth(widths.get(wi)); } } continue; @@ -80,7 +81,8 @@ private static String extractKramdownExpression(String content) { int start = content.indexOf('{'); int end = content.lastIndexOf('}'); if (start >= 0 && end > start) { - return content.substring(start + 1, end).trim(); + return content.substring(start + 1, end) + .trim(); } return ""; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 4c123301..87464f73 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -7,13 +7,13 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; -import com.hfstudio.guidenh.guide.indices.CategoryIndex; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.GuidebookText; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class CategoryCompiler extends BlockTagCompiler { @@ -47,7 +47,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { String categoryName = el.getAttributeString("name", null); - if (categoryName == null || categoryName.trim().isEmpty()) return; + if (categoryName == null || categoryName.trim() + .isEmpty()) return; // Restore Phase 2: index resolved category member titles for full-text search var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); @@ -59,7 +60,8 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink.appendText(el, categoryName.trim()); sink.appendBreak(); for (var entry : entries) { - if (entry.title() != null && !entry.title().isEmpty()) { + if (entry.title() != null && !entry.title() + .isEmpty()) { sink.appendText(el, entry.title()); } sink.appendBreak(); @@ -73,6 +75,7 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } public static class CategoryPlaceholder extends LytParagraph { + public final String name; public final int rows; public final ResourceLocation guideId; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index c5ea65a4..89bf4f24 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -7,13 +7,13 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; -import com.hfstudio.guidenh.guide.indices.CategoryIndex; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.GuidebookText; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class SpecialCompiler extends BlockTagCompiler { @@ -51,7 +51,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { String specialName = el.getAttributeString("name", null); - if (specialName == null || specialName.trim().isEmpty()) return; + if (specialName == null || specialName.trim() + .isEmpty()) return; // Restore Phase 2: index resolved special page result entries for full-text search var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); @@ -74,6 +75,7 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } public static class SpecialPlaceholder extends LytParagraph { + public final String name; public final int rows; public final ResourceLocation guideId; @@ -82,8 +84,8 @@ public static class SpecialPlaceholder extends LytParagraph { public final String language; public final String query; - SpecialPlaceholder(String name, int rows, ResourceLocation guideId, String page, String prefix, - String language, String query) { + SpecialPlaceholder(String name, int rows, ResourceLocation guideId, String page, String prefix, String language, + String query) { this.name = name; this.rows = rows; this.guideId = guideId; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java index 779b9fd3..5691f71b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java @@ -36,8 +36,7 @@ public abstract class LytNode implements Styleable { public void removeChild(LytNode node) {} public void replaceChild(LytNode oldChild, LytNode newChild) { - throw new UnsupportedOperationException( - getClass().getSimpleName() + " must override replaceChild"); + throw new UnsupportedOperationException(getClass().getSimpleName() + " must override replaceChild"); } protected void onAttach() {} @@ -176,17 +175,29 @@ public void setSourceNode(@Nullable MdAstNode sourceNode) { } @Nullable - public String getId() { return id; } + public String getId() { + return id; + } - public void setId(@Nullable String id) { this.id = id; } + public void setId(@Nullable String id) { + this.id = id; + } @Nullable - public String getNodeUid() { return nodeUid; } + public String getNodeUid() { + return nodeUid; + } - public void setNodeUid(@Nullable String nodeUid) { this.nodeUid = nodeUid; } + public void setNodeUid(@Nullable String nodeUid) { + this.nodeUid = nodeUid; + } @Nullable - public String getStyleClass() { return styleClass; } + public String getStyleClass() { + return styleClass; + } - public void setStyleClass(@Nullable String styleClass) { this.styleClass = styleClass; } + public void setStyleClass(@Nullable String styleClass) { + this.styleClass = styleClass; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java index 89dff05a..181fe525 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java @@ -86,18 +86,32 @@ public final void visit(LytVisitor visitor) { protected void visitChildren(LytVisitor visitor) {} @Nullable - public String getStyleClass() { return styleClass; } + public String getStyleClass() { + return styleClass; + } - public void setStyleClass(@Nullable String styleClass) { this.styleClass = styleClass; } + public void setStyleClass(@Nullable String styleClass) { + this.styleClass = styleClass; + } @Nullable - public String getNodeUid() { return nodeUid; } + public String getNodeUid() { + return nodeUid; + } - public void setNodeUid(@Nullable String nodeUid) { this.nodeUid = nodeUid; } + public void setNodeUid(@Nullable String nodeUid) { + this.nodeUid = nodeUid; + } - public Object getData(String key) { return data.get(key); } + public Object getData(String key) { + return data.get(key); + } - public void setData(String key, Object value) { data.put(key, value); } + public void setData(String key, Object value) { + data.put(key, value); + } - public Map getData() { return data; } + public Map getData() { + return data; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java index 50ecb11b..c0fd8e42 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java @@ -98,8 +98,7 @@ public static T unwrapPlaceholder(Object node, Class placeholderClass) { if (placeholderClass.isInstance(node)) { return placeholderClass.cast(node); } - if (node instanceof LytFlowInlineBlock wrapper - && placeholderClass.isInstance(wrapper.getBlock())) { + if (node instanceof LytFlowInlineBlock wrapper && placeholderClass.isInstance(wrapper.getBlock())) { return placeholderClass.cast(wrapper.getBlock()); } return null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index a2a4a144..cc753da1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -25,8 +25,12 @@ import com.hfstudio.guidenh.guide.internal.recipe.RecipeCache; import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; import com.hfstudio.guidenh.guide.internal.util.LangUtil; +import com.hfstudio.guidenh.guide.latex.GuideLatexTextureCache; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiTranslationStats; import com.hfstudio.guidenh.guide.render.GuidePageTexture; +import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCache; +import com.hfstudio.guidenh.integration.structurelib.StructureLibElementTooltipResolver; +import com.hfstudio.guidenh.integration.structurelib.StructureLibRuntimeFacade; import cpw.mods.fml.common.FMLLog; @@ -54,6 +58,14 @@ public static void reloadGuides(IResourceManager resourceManager) { GuidePageTexture.clear(); GuidePageLanguageIndex.clear(); GuideResourceLanguageIndex.clear(); + GuideLatexTextureCache.INSTANCE.clearAll(); + GuideSceneStructureCache.global() + .clear(); + StructureLibRuntimeFacade.CONTROL_ANALYSIS_CACHE.clear(); + StructureLibRuntimeFacade.ANALYSIS_SNAPSHOT_CACHE.clear(); + StructureLibRuntimeFacade.IMPORT_RESULT_CACHE.clear(); + StructureLibElementTooltipResolver.BLOCK_CANDIDATE_CACHE.clear(); + StructureLibElementTooltipResolver.HATCH_CANDIDATE_CACHE.clear(); long stageStartedAt = System.nanoTime(); GuideRegistry.setDataDriven(DataDrivenGuideLoader.load()); @@ -282,7 +294,8 @@ private static ParsedGuidePage tryParsePageCandidate(String sourcePack, String l private static ParsedGuidePage parsePageBytes(String sourcePack, String language, String contentRootFolder, ResourceLocation pageId, ResourceLocation sourceId, byte[] bytes) { try { - return GuideLocalizedPageSourceResolver.parseFrontmatterOnly(sourcePack, language, contentRootFolder, pageId, bytes); + return GuideLocalizedPageSourceResolver + .parseFrontmatterOnly(sourcePack, language, contentRootFolder, pageId, bytes); } catch (Exception ex) { FMLLog.getLogger() .error("[GuideNH] [GuideLightweightReloadService] Error parsing page {} from {}", pageId, sourceId, ex); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 58dbb996..bfd78348 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -46,6 +46,7 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; +import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.config.ModConfig; @@ -102,11 +103,10 @@ import com.hfstudio.guidenh.guide.internal.home.HomePageController; import com.hfstudio.guidenh.guide.internal.home.HomePageDataBuilder; import com.hfstudio.guidenh.guide.internal.home.HomePageLayout; -import com.hfstudio.guidenh.guide.internal.item.RegionWandItem; -import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; -import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.NavigationState; +import com.hfstudio.guidenh.guide.internal.item.RegionWandItem; +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar.ContextTarget; @@ -497,15 +497,20 @@ private GuideScreen(GuideScreenRoute route, @Nullable GuideScreenViewState resto } catch (Throwable ignored) { navBar.setPinned(false); } - navBar.restoreState(ClientProxy.getLytHost().getNavigation().recallNavigationState(), bookmarkState); - ClientProxy.getLytHost().setPreheatCompiler(pageId -> { - if (guide == null) return null; - try { - return guide.getPage(new net.minecraft.util.ResourceLocation(pageId)); - } catch (Exception e) { - return null; - } - }); + navBar.restoreState( + ClientProxy.getLytHost() + .getNavigation() + .recallNavigationState(), + bookmarkState); + ClientProxy.getLytHost() + .setPreheatCompiler(pageId -> { + if (guide == null) return null; + try { + return guide.getPage(new net.minecraft.util.ResourceLocation(pageId)); + } catch (Exception e) { + return null; + } + }); } public static void open(ResourceLocation guideId, @Nullable PageAnchor anchor) { @@ -517,7 +522,9 @@ public static void openFromGuideHotkey(ResourceLocation guideId, @Nullable PageA } public static void openFromHomeHotkey() { - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().consumeValidLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost() + .getNavigation() + .consumeValidLastContentState(); open(remembered != null ? remembered : GuideScreenViewState.home(), false); } @@ -550,7 +557,9 @@ private static GuideScreenRoute contentRoute(ResourceLocation guideId, @Nullable return null; } if (anchor == null) { - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().consumeValidLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost() + .getNavigation() + .consumeValidLastContentState(); if (remembered != null && remembered.route() != null) { return remembered.route(); } @@ -618,7 +627,10 @@ public void reloadPage() { document = null; lastLayoutWidth = -1; if (currentAnchor != null) { - ClientProxy.getLytHost().invalidatePage(currentAnchor.pageId().toString()); + ClientProxy.getLytHost() + .invalidatePage( + currentAnchor.pageId() + .toString()); } loadCurrentPage(); updateToolbarButtonState(); @@ -711,12 +723,16 @@ private void finalizePendingViewState() { } private void rememberCurrentContentStateIfEligible() { - ClientProxy.getLytHost().getNavigation().rememberContentState(captureCurrentViewState()); + ClientProxy.getLytHost() + .getNavigation() + .rememberContentState(captureCurrentViewState()); } private void rememberNavigationState() { if (guide == null) return; - ClientProxy.getLytHost().getNavigation().rememberNavBarState(guide.getId(), navBar.captureState()); + ClientProxy.getLytHost() + .getNavigation() + .rememberNavBarState(guide.getId(), navBar.captureState()); } private boolean isNavigationNewPageButtonVisible() { @@ -1064,7 +1080,9 @@ private MutableGuide resolveGuideEditorTargetGuide() { if (guide != null) { return guide; } - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().recallLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost() + .getNavigation() + .recallLastContentState(); if (remembered != null && remembered.route() != null) { ResourceLocation rememberedGuideId = remembered.route() .guideId(); @@ -1385,6 +1403,8 @@ private void rebuildGuideEditorPreview() { .compile(buildGuideEditorPreviewGuide(parsedDraft), guide.getExtensions(), parsedDraft); int previewWidth = getGuideEditorPreviewLayoutWidth(); if (guideEditorPreviewPage != null && guideEditorPreviewPage.document() != null) { + ClientProxy.getLytHost() + .dispatchToSubtree(guideEditorPreviewPage.document()); guideEditorPreviewPage.document() .updateLayout( createLayoutContext(previewWidth, getVisualReferenceContentWidth()), @@ -2536,7 +2556,8 @@ private void completePendingContentPageLoadIfNeeded() { return; } int requestId = pendingPageLoadRequestId; - String pageIdStr = currentAnchor.pageId().toString(); + String pageIdStr = currentAnchor.pageId() + .toString(); LytHost lytHost = ClientProxy.getLytHost(); GuidePage loadedPage; @@ -2596,7 +2617,8 @@ private static void registerRuntimeScenes(GuidePage page) { } } if (found > 0) { - FMLLog.getLogger().info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); + FMLLog.getLogger() + .info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); } } @@ -2760,8 +2782,12 @@ private void clampScroll() { int max = getMaxScroll(); if (scrollY < 0) scrollY = 0; if (scrollY > max) scrollY = max; - ClientProxy.getLytHost().getViewport().updateContent(contentW, contentH); - ClientProxy.getLytHost().getViewport().scrollTo(scrollY); + ClientProxy.getLytHost() + .getViewport() + .updateContent(contentW, contentH); + ClientProxy.getLytHost() + .getViewport() + .scrollTo(scrollY); } @Override @@ -4565,10 +4591,12 @@ private boolean canSearchCurrentView() { private void drawTiledBackground() { drawRect(0, 0, this.width, this.height, BACKGROUND_DIM_COLOR); if (mc == null || mc.getTextureManager() == null) { - FMLLog.getLogger().warn("[GuideNH] drawTiledBackground: mc or textureManager is null, skipping"); + FMLLog.getLogger() + .warn("[GuideNH] drawTiledBackground: mc or textureManager is null, skipping"); return; } - mc.getTextureManager().bindTexture(BG_TEXTURE); + mc.getTextureManager() + .bindTexture(BG_TEXTURE); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); GL11.glEnable(GL11.GL_BLEND); @@ -4808,7 +4836,9 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { return; } if (result != null && result.bookmarkTogglePageId() != null) { - ClientProxy.getLytHost().getNavigation().toggleBookmark(result.bookmarkTogglePageId()); + ClientProxy.getLytHost() + .getNavigation() + .toggleBookmark(result.bookmarkTogglePageId()); bookmarkState.toggle(result.bookmarkTogglePageId()); mc.getSoundHandler() .playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F)); @@ -6754,7 +6784,9 @@ private void recordHomeHistoryIfEligible() { || !guide.pageExists(currentAnchor.pageId())) { return; } - ClientProxy.getLytHost().getNavigation().recordHomeHistory(guide.getId(), currentAnchor.pageId()); + ClientProxy.getLytHost() + .getNavigation() + .recordHomeHistory(guide.getId(), currentAnchor.pageId()); homeHistory.record(guide.getId(), currentAnchor.pageId()); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java index a18a73db..c0135bc5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java @@ -8,7 +8,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -52,8 +51,7 @@ * Encapsulates a Guide, which consists of a collection of Markdown pages and associated content, loaded from a * guide-specific subdirectory of resource packs. */ -public class MutableGuide - implements Guide, MediaWikiListContextProvider, AutoCloseable { +public class MutableGuide implements Guide, MediaWikiListContextProvider, AutoCloseable { private final ResourceLocation id; private final String defaultNamespace; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index f8cbc444..fc59cd18 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -6,10 +6,10 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter; import com.hfstudio.guidenh.libs.mdast.MdastOptions; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java index 8406f6e0..da49d371 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java @@ -15,12 +15,12 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorElementModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorElementType; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneNodeModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneNodeType; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.scene.annotation.compiler.BlockAnnotationTemplateElementCompiler; import com.hfstudio.guidenh.libs.mdast.MdAst; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java index e61eafef..945958f3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java @@ -52,8 +52,10 @@ public String extractHeadingText(@Nullable ParsedGuidePage page) { private static boolean isHeading(MdAstAnyContent block) { if (block instanceof MdxJsxElementFields el) { String name = el.name(); - return name != null && name.length() == 2 && name.charAt(0) == 'h' - && name.charAt(1) >= '1' && name.charAt(1) <= '6'; + return name != null && name.length() == 2 + && name.charAt(0) == 'h' + && name.charAt(1) >= '1' + && name.charAt(1) <= '6'; } return false; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java index afd54ba9..988f7373 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java @@ -2,10 +2,19 @@ public interface DeferredTask { - enum Priority { HIGH, LOW } - enum TaskResult { YIELD, DONE } + enum Priority { + HIGH, + LOW + } + + enum TaskResult { + YIELD, + DONE + } Priority priority(); + TaskResult step(long deadlineNs); + boolean isDone(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java index 5d40044e..28c425c8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java @@ -20,18 +20,34 @@ public LytEvent(EventType type, Object target, Map data) { this.type = type; this.target = target; this.currentTarget = target; - this.data = data != null - ? Collections.unmodifiableMap(new LinkedHashMap<>(data)) - : Collections.emptyMap(); + this.data = data != null ? Collections.unmodifiableMap(new LinkedHashMap<>(data)) : Collections.emptyMap(); } - public EventType type() { return type; } - public Object target() { return target; } - public Object currentTarget() { return currentTarget; } - public Map data() { return data; } + public EventType type() { + return type; + } + + public Object target() { + return target; + } - public void stopPropagation() { propagationStopped = true; } - public boolean isPropagationStopped() { return propagationStopped; } + public Object currentTarget() { + return currentTarget; + } + + public Map data() { + return data; + } - void setCurrentTarget(Object node) { this.currentTarget = node; } + public void stopPropagation() { + propagationStopped = true; + } + + public boolean isPropagationStopped() { + return propagationStopped; + } + + void setCurrentTarget(Object node) { + this.currentTarget = node; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 56407a79..cfc96fd1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -22,8 +22,10 @@ public class LytHost { - @Nullable private LytDocument document; - @Nullable private PageCollection currentPageCollection; + @Nullable + private LytDocument document; + @Nullable + private PageCollection currentPageCollection; private final Map scripts = new HashMap<>(); private final Map cachedDocuments = new LinkedHashMap<>(); private final Map pageNodeCounters = new HashMap<>(); @@ -32,9 +34,13 @@ public class LytHost { private static final int MAX_CACHED_PAGES = 32; static class PageCacheEntry { + final GuidePage guidePage; final Map nodeResults = new HashMap<>(); - PageCacheEntry(GuidePage guidePage) { this.guidePage = guidePage; } + + PageCacheEntry(GuidePage guidePage) { + this.guidePage = guidePage; + } } private final ViewportState viewport = new ViewportState(); @@ -46,10 +52,13 @@ static class PageCacheEntry { @FunctionalInterface public interface PreheatCompiler { - @Nullable GuidePage compile(String pageId); + + @Nullable + GuidePage compile(String pageId); } - @Nullable private PreheatCompiler preheatCompiler; + @Nullable + private PreheatCompiler preheatCompiler; public void setPreheatCompiler(@Nullable PreheatCompiler compiler) { this.preheatCompiler = compiler; @@ -57,25 +66,36 @@ public void setPreheatCompiler(@Nullable PreheatCompiler compiler) { // ===== Document ===== - /** Full processing: UID allocation, onAttach, MOUNT dispatch. Resets the node counter so the - * same page always gets the same UIDs across remounts (enabling node-level cache hits). */ + /** + * Full processing: UID allocation, onAttach, MOUNT dispatch. Resets the node counter so the + * same page always gets the same UIDs across remounts (enabling node-level cache hits). + */ public void mountDocument(@Nullable LytDocument newDoc) { if (this.document != null && this.document != newDoc) { - this.document.setLive(false); // onDetach cascade on old doc + this.document.setLive(false); // onDetach cascade on old doc } this.document = newDoc; if (newDoc != null) { - pageNodeCounters.remove(currentPageId); // reset for stable UIDs + pageNodeCounters.remove(currentPageId); // reset for stable UIDs allocateNodeUids(newDoc); - newDoc.setLive(true); // onAttach cascade - dispatchMountEvents(newDoc); // MOUNT events → scripts materialize placeholders + newDoc.setLive(true); // onAttach cascade + dispatchMountEvents(newDoc); // MOUNT events → scripts materialize placeholders viewport.updateContent(newDoc.getAvailableWidth(), newDoc.getContentHeight()); } } - @Nullable public LytDocument getDocument() { return document; } - public ViewportState getViewport() { return viewport; } - public NavigationState getNavigation() { return nav; } + @Nullable + public LytDocument getDocument() { + return document; + } + + public ViewportState getViewport() { + return viewport; + } + + public NavigationState getNavigation() { + return nav; + } public void registerScript(String styleClass, LytScript script) { scripts.put(styleClass, script); @@ -102,7 +122,9 @@ Object getNodeResult(String pageId, String nodeUid) { public void cachePage(String pageId, GuidePage guidePage) { while (cachedDocuments.size() >= MAX_CACHED_PAGES) { - var oldest = cachedDocuments.keySet().iterator().next(); + var oldest = cachedDocuments.keySet() + .iterator() + .next(); cachedDocuments.remove(oldest); pageNodeCounters.remove(oldest); preheatScheduled.remove(oldest); @@ -146,7 +168,9 @@ public void requestPreheatNeighbors(String currentPageId) { if (node == null) return; for (var child : node.children()) { if (child.hasPage()) { - requestPreheat(child.pageId().toString()); + requestPreheat( + child.pageId() + .toString()); } } } @@ -172,16 +196,17 @@ public void preheatStep(long deadlineNs) { } String allocateNodeUid(String pageId, String prefix) { - int seq = pageNodeCounters - .computeIfAbsent(pageId, k -> new AtomicInteger()).incrementAndGet(); + int seq = pageNodeCounters.computeIfAbsent(pageId, k -> new AtomicInteger()) + .incrementAndGet(); return pageId + "::" + prefix + ":" + seq; } private void allocateNodeUids(LytNode node) { if (node.getStyleClass() != null && node.getNodeUid() == null) { - String prefix = node.getStyleClass().toLowerCase(); - int seq = pageNodeCounters - .computeIfAbsent(currentPageId, k -> new AtomicInteger()).incrementAndGet(); + String prefix = node.getStyleClass() + .toLowerCase(); + int seq = pageNodeCounters.computeIfAbsent(currentPageId, k -> new AtomicInteger()) + .incrementAndGet(); node.setNodeUid(currentPageId + "::" + prefix + ":" + seq); } for (var child : node.getChildren()) { @@ -201,9 +226,10 @@ private void allocateFlowNodeUids(LytNode node) { private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { if (fc.getStyleClass() != null && fc.getNodeUid() == null) { - String prefix = fc.getStyleClass().toLowerCase(); - int seq = pageNodeCounters - .computeIfAbsent(currentPageId, k -> new AtomicInteger()).incrementAndGet(); + String prefix = fc.getStyleClass() + .toLowerCase(); + int seq = pageNodeCounters.computeIfAbsent(currentPageId, k -> new AtomicInteger()) + .incrementAndGet(); fc.setNodeUid(currentPageId + "::" + prefix + ":" + seq); } if (fc instanceof LytFlowSpan span) { @@ -217,7 +243,7 @@ private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { * Two-phase MOUNT dispatch: *

    * Phase 1 (sync): walk the entire tree and execute - * every synchronous script immediately. This guarantees that all + * every synchronous script immediately. This guarantees that all * setup and initialization work (e.g. establishing CURRENT_SCENE, * compiling child elements) is finished before any asynchronous * work begins. @@ -227,14 +253,14 @@ private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { * execution on subsequent ticks (see {@link #step}). *

    * Within each phase the original document order (parent before children) - * is preserved. The node-level result cache is consulted: if a node + * is preserved. The node-level result cache is consulted: if a node * already has a cached result from a previous mount, the cached content * is restored directly and the script is skipped in both * phases. */ private void dispatchMountEvents(LytNode node) { dispatchPhase(node, false); // Phase 1: sync scripts only - dispatchPhase(node, true); // Phase 2: queue async scripts only + dispatchPhase(node, true); // Phase 2: queue async scripts only } private void dispatchPhase(LytNode node, boolean asyncPhase) { @@ -286,13 +312,13 @@ private void dispatchPhaseFlowRecursive(LytFlowContent fc, boolean asyncPhase) { /** * Dispatch a single script in the given phase. *

      - *
    • If the node has a cached result from a previous mount, the - * cached content is restored directly and the script is skipped - * entirely (both phases). - *
    • In the sync phase ({@code asyncPhase == false}), only - * non-async scripts are executed synchronously. - *
    • In the async phase ({@code asyncPhase == true}), only - * async scripts are enqueued as {@link MaterializeTask}s. + *
    • If the node has a cached result from a previous mount, the + * cached content is restored directly and the script is skipped + * entirely (both phases). + *
    • In the sync phase ({@code asyncPhase == false}), only + * non-async scripts are executed synchronously. + *
    • In the async phase ({@code asyncPhase == true}), only + * async scripts are enqueued as {@link MaterializeTask}s. *
    */ private void dispatchScriptInPhase(LytScript script, Object node, boolean asyncPhase) { @@ -328,6 +354,7 @@ private static String nodeUidOf(Object node) { } private static class MaterializeTask implements DeferredTask { + private final LytScript script; private final Object node; private final ScriptContextImpl ctx; @@ -342,7 +369,9 @@ private static class MaterializeTask implements DeferredTask { } @Override - public Priority priority() { return Priority.HIGH; } + public Priority priority() { + return Priority.HIGH; + } @Override public TaskResult step(long deadlineNs) { @@ -369,7 +398,9 @@ public TaskResult step(long deadlineNs) { } @Override - public boolean isDone() { return ctx.isComplete(); } + public boolean isDone() { + return ctx.isComplete(); + } } // ===== Sync events ===== @@ -388,12 +419,21 @@ private void processEventsNow() { switch (event.type()) { case CLICK: case DOUBLE_CLICK: - if (event.data().containsKey("x") && event.data().containsKey("y")) { - interactive.mouseClicked(null, - ((Number) event.data().get("x")).intValue(), - ((Number) event.data().get("y")).intValue(), - event.data().containsKey("button") - ? ((Number) event.data().get("button")).intValue() : 0, + if (event.data() + .containsKey("x") + && event.data() + .containsKey("y")) { + interactive.mouseClicked( + null, + ((Number) event.data() + .get("x")).intValue(), + ((Number) event.data() + .get("y")).intValue(), + event.data() + .containsKey("button") + ? ((Number) event.data() + .get("button")).intValue() + : 0, event.type() == EventType.DOUBLE_CLICK); } break; @@ -414,7 +454,7 @@ public void submitTask(DeferredTask task) { } /** Recursively dispatch MOUNT events into a detached subtree. */ - void dispatchToSubtree(LytNode root) { + public void dispatchToSubtree(LytNode root) { allocateNodeUids(root); dispatchMountEvents(root); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java index 3e1c8cba..3f09982c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java @@ -13,10 +13,14 @@ public LytHostWorkItem(LytHost host) { } @Override - public Priority priority() { return Priority.HIGH; } + public Priority priority() { + return Priority.HIGH; + } @Override - public boolean shouldRun() { return host.hasWork(); } + public boolean shouldRun() { + return host.hasWork(); + } @Override public WorkResult tick(long deadlineNs) { @@ -30,5 +34,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { return LytHostWorkItem.class.hashCode(); } + public int hashCode() { + return LytHostWorkItem.class.hashCode(); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java index 333d4c9d..618609a8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java @@ -1,8 +1,11 @@ package com.hfstudio.guidenh.guide.internal.host; public interface LytScript { + ScriptType type(); + String styleClass(); + void onEvent(Object node, LytEvent event, ScriptContext ctx); default boolean isAsync() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java index d02d7e4b..59617715 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java @@ -22,12 +22,15 @@ public class NavigationState { - @Nullable private ResourceLocation currentGuideId; - @Nullable private PageAnchor currentAnchor; + @Nullable + private ResourceLocation currentGuideId; + @Nullable + private PageAnchor currentAnchor; private final Deque backStack = new ArrayDeque<>(); - @Nullable private GuideScreenViewState lastContentViewState; + @Nullable + private GuideScreenViewState lastContentViewState; private final Map navBarStates = new LinkedHashMap<>(); private final Set bookmarks = new LinkedHashSet<>(); @@ -35,8 +38,10 @@ public class NavigationState { private final List homeHistory = new ArrayList<>(); public static class HomeHistoryEntry { + public final ResourceLocation guideId; public final ResourceLocation pageId; + public HomeHistoryEntry(ResourceLocation guideId, ResourceLocation pageId) { this.guideId = guideId; this.pageId = pageId; @@ -48,38 +53,71 @@ public void setCurrent(ResourceLocation guideId, PageAnchor anchor) { this.currentAnchor = anchor; } - @Nullable public ResourceLocation currentGuideId() { return currentGuideId; } - @Nullable public PageAnchor currentAnchor() { return currentAnchor; } + @Nullable + public ResourceLocation currentGuideId() { + return currentGuideId; + } - public void pushHistory(GuideScreenViewState state) { backStack.push(state); } - @Nullable public GuideScreenViewState popHistory() { return backStack.pollFirst(); } - public Deque backStack() { return backStack; } + @Nullable + public PageAnchor currentAnchor() { + return currentAnchor; + } + + public void pushHistory(GuideScreenViewState state) { + backStack.push(state); + } - public void rememberContentState(@Nullable GuideScreenViewState state) { lastContentViewState = state; } - @Nullable public GuideScreenViewState recallLastContentState() { return lastContentViewState; } + @Nullable + public GuideScreenViewState popHistory() { + return backStack.pollFirst(); + } + + public Deque backStack() { + return backStack; + } + + public void rememberContentState(@Nullable GuideScreenViewState state) { + lastContentViewState = state; + } + + @Nullable + public GuideScreenViewState recallLastContentState() { + return lastContentViewState; + } public void rememberNavBarState(ResourceLocation guideId, GuideNavBarState state) { if (state != null) navBarStates.put(guideId, state); } - @Nullable public GuideNavBarState recallNavBarState(ResourceLocation guideId) { + + @Nullable + public GuideNavBarState recallNavBarState(ResourceLocation guideId) { return navBarStates.get(guideId); } - public boolean isBookmarked(ResourceLocation pageId) { return bookmarks.contains(pageId); } + public boolean isBookmarked(ResourceLocation pageId) { + return bookmarks.contains(pageId); + } + public void toggleBookmark(ResourceLocation pageId) { - if (!bookmarks.remove(pageId)) { bookmarks.add(pageId); } + if (!bookmarks.remove(pageId)) { + bookmarks.add(pageId); + } + } + + public Set bookmarks() { + return bookmarks; } - public Set bookmarks() { return bookmarks; } public void recordHomeHistory(ResourceLocation guideId, ResourceLocation pageId) { homeHistory.add(0, new HomeHistoryEntry(guideId, pageId)); } - public List homeHistory() { return homeHistory; } + + public List homeHistory() { + return homeHistory; + } public GuideNavBarState recallNavigationState() { - GuideNavBarState currentGuideState = currentGuideId != null - ? navBarStates.get(currentGuideId) - : null; + GuideNavBarState currentGuideState = currentGuideId != null ? navBarStates.get(currentGuideId) : null; return currentGuideState != null ? currentGuideState : GuideNavBarState.defaultState(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java index bc9ac844..a216df01 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java @@ -2,19 +2,23 @@ import java.util.Map; -import org.jetbrains.annotations.Nullable; - import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.indices.PageIndex; public interface ScriptContext { + Map data(); + void replace(Object newNode); + String allocateId(String prefix); + LytDocument document(); @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 296981b3..f5461302 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -4,15 +4,13 @@ import java.util.List; import java.util.Map; -import org.jetbrains.annotations.Nullable; - import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.PageCollection; -import com.hfstudio.guidenh.guide.document.block.LytAlignedBlock; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; -import com.hfstudio.guidenh.guide.document.block.LytDocumentFloat; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; @@ -22,6 +20,7 @@ import com.hfstudio.guidenh.guide.indices.PageIndex; class ScriptContextImpl implements ScriptContext { + private final Map data = new HashMap<>(); private final Object node; private final LytHost host; @@ -38,7 +37,9 @@ class ScriptContextImpl implements ScriptContext { } @Override - public Map data() { return data; } + public Map data() { + return data; + } @Override @SuppressWarnings("unchecked") @@ -48,7 +49,7 @@ public void replace(Object newNode) { // // When a block-level tag (e.g. , ) appears inside // a paragraph or list item, the PageCompiler wraps it in LytFlowInlineBlock - // so the block can participate in inline flow layout. At MOUNT time the + // so the block can participate in inline flow layout. At MOUNT time the // dispatch passes the wrapper as "this.node", not the inner placeholder. // // The wrapper IS the correct replacement target — swapping its inner block @@ -111,7 +112,9 @@ public String allocateId(String prefix) { } @Override - public LytDocument document() { return document; } + public LytDocument document() { + return document; + } @Override @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java index 1fa30b54..578ca357 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java @@ -21,9 +21,17 @@ public void updateContent(int width, int height) { this.contentHeight = height; } - public int scrollY() { return scrollY; } - public void scrollTo(int y) { this.scrollY = clampScroll(y); } - public void scrollBy(int delta) { scrollTo(scrollY + delta); } + public int scrollY() { + return scrollY; + } + + public void scrollTo(int y) { + this.scrollY = clampScroll(y); + } + + public void scrollBy(int delta) { + scrollTo(scrollY + delta); + } private int clampScroll(int y) { int max = getMaxScrollY(); @@ -44,11 +52,27 @@ public LytRect getRect() { return new LytRect(0, scrollY, viewportWidth, viewportHeight); } - public boolean isLayoutDirty() { return layoutDirty; } - public void setLayoutDirty(boolean dirty) { this.layoutDirty = dirty; } + public boolean isLayoutDirty() { + return layoutDirty; + } + + public void setLayoutDirty(boolean dirty) { + this.layoutDirty = dirty; + } - public int viewportWidth() { return viewportWidth; } - public int viewportHeight() { return viewportHeight; } - public int contentWidth() { return contentWidth; } - public int contentHeight() { return contentHeight; } + public int viewportWidth() { + return viewportWidth; + } + + public int viewportHeight() { + return viewportHeight; + } + + public int contentWidth() { + return contentWidth; + } + + public int contentHeight() { + return contentHeight; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index 0315ff46..97541691 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -17,10 +17,14 @@ public class CategoryScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Category"; } + public String styleClass() { + return "Category"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -42,8 +46,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { var context = MediaWikiTagCompilerSupport.createListContext(guide, index); var entries = MediaWikiPageListBuilder.buildCategoryMembers(context, ph.name); - var block = MediaWikiTagCompilerSupport.createBlock( - entries, ph.rows, GuidebookText.MediaWikiNoPagesInCategory.text()); + var block = MediaWikiTagCompilerSupport + .createBlock(entries, ph.rows, GuidebookText.MediaWikiNoPagesInCategory.text()); ctx.replace(block); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java index 6e105699..b582b54f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -2,8 +2,6 @@ import net.minecraft.client.Minecraft; -import cpw.mods.fml.common.FMLLog; - import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -11,6 +9,8 @@ import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import cpw.mods.fml.common.FMLLog; + public class CommandLinkScript implements LytScript { @Override @@ -31,10 +31,12 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (command == null) return; link.setClickCallback(screen -> { if (Minecraft.getMinecraft().thePlayer == null) return; - FMLLog.getLogger().info("[GuideNH] [CommandLink] Sending command: {}", command); + FMLLog.getLogger() + .info("[GuideNH] [CommandLink] Sending command: {}", command); Minecraft.getMinecraft().thePlayer.sendChatMessage(command); if (Boolean.TRUE.equals(close)) { - Minecraft.getMinecraft().displayGuiScreen(null); + Minecraft.getMinecraft() + .displayGuiScreen(null); } }); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java index 311edfdf..2ffe9a74 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java @@ -19,13 +19,19 @@ public class CsvTableScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "CsvTable"; } + public String styleClass() { + return "CsvTable"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index b36624d6..3bbb7348 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -16,13 +16,19 @@ public class FloatingImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "FloatingImage"; } + public String styleClass() { + return "FloatingImage"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index 31a15dc7..26b0fb43 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -16,13 +16,19 @@ public class ImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Img"; } + public String styleClass() { + return "Img"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index b6488c90..a1c1286d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -1,6 +1,5 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import com.hfstudio.guidenh.guide.compiler.IdUtils; @@ -16,10 +15,14 @@ public class ItemGridScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "ItemGrid"; } + public String styleClass() { + return "ItemGrid"; + } @Override @SuppressWarnings("deprecation") diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index 522beb84..f07b55ac 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -1,13 +1,12 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraftforge.oredict.OreDictionary; +import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemImage; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -18,10 +17,14 @@ public class ItemImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "ItemImage"; } + public String styleClass() { + return "ItemImage"; + } @Override @SuppressWarnings("deprecation") @@ -38,7 +41,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (ph.ore != null) { java.util.List oreStacks = OreDictionary.getOres(ph.ore); if (oreStacks != null && !oreStacks.isEmpty()) { - stack = oreStacks.get(0).copy(); + stack = oreStacks.get(0) + .copy(); } } if (stack == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java index 53adfb0a..6ac8f3a8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java @@ -26,9 +26,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String bindId = (String) placeholder.getData("bindId"); if (bindId == null) return; var mapping = KeyBindTagCompiler.findMapping(bindId); - String display = mapping != null - ? KeyBindTagCompiler.describeMapping(mapping) - : "[" + bindId + "]"; + String display = mapping != null ? KeyBindTagCompiler.describeMapping(mapping) : "[" + bindId + "]"; placeholder.setText(display); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index fe30c8d1..41d6da13 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -6,6 +6,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.MermaidCompiler.MermaidPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytMermaidMindmap; +import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -19,13 +20,19 @@ public class MermaidScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Mermaid"; } + public String styleClass() { + return "Mermaid"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -51,22 +58,44 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { sourceText = MermaidMindmapParser.normalize(sourceText); } - if (sourceText == null || sourceText.trim().isEmpty()) { + if (sourceText == null || sourceText.trim() + .isEmpty()) { replaceWithError(ctx, "Source not found or empty"); return; } try { var document = MermaidMindmapParser.parse(sourceText); - LytMermaidMindmap block = new LytMermaidMindmap(document, sourceText, + LytMermaidMindmap block = new LytMermaidMindmap( + document, + sourceText, ph.nodeContentBlocks != null ? ph.nodeContentBlocks : java.util.Collections.emptyMap()); if (ph.width > 0 || ph.height > 0) { block.setPreferredSize(ph.width, ph.height); } + // Dispatch MOUNT events into NodeContent subtrees (Recipe/BlockImage placeholders) + if (ph.nodeContentBlocks != null) { + FMLLog.getLogger() + .info("[MermaidDebug] Dispatching into {} NodeContent blocks", ph.nodeContentBlocks.size()); + for (var entry : ph.nodeContentBlocks.entrySet()) { + var contentBlock = entry.getValue(); + FMLLog.getLogger() + .info( + "[MermaidDebug] NodeContent '{}' block type={} children={}", + entry.getKey(), + contentBlock.getClass() + .getSimpleName(), + contentBlock instanceof LytNode n ? n.getChildren() + .size() : -1); + if (contentBlock instanceof LytNode root) { + ctx.dispatchSubtree(root); + } + } + } ctx.replace(block); } catch (IllegalArgumentException e) { - FMLLog.getLogger().warn( - "[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); + FMLLog.getLogger() + .warn("[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); replaceWithError(ctx, "Failed to parse: " + e.getMessage()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java index dbb2d024..90875e22 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java @@ -26,7 +26,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() == EventType.MOUNT && node instanceof LytFlowText placeholder) { String username; try { - username = Minecraft.getMinecraft().getSession().getUsername(); + username = Minecraft.getMinecraft() + .getSession() + .getUsername(); } catch (Exception e) { username = ""; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java index 2ce24f21..04cb476f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java @@ -11,25 +11,29 @@ import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; -import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; -import com.hfstudio.guidenh.integration.betterquesting.QuestIndex; -import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; -import com.hfstudio.guidenh.integration.betterquesting.QuestState; -import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestCardCompiler.QuestCardPlaceholder; -import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; +import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; +import com.hfstudio.guidenh.integration.betterquesting.QuestIndex; +import com.hfstudio.guidenh.integration.betterquesting.QuestState; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestCardCompiler.QuestCardPlaceholder; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; public class QuestCardScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "QuestCard"; } + public String styleClass() { + return "QuestCard"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -38,8 +42,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { QuestDisplay display; try { - display = BqHelpers.resolveDisplay(ph.questId, Minecraft.getMinecraft().thePlayer, - ph.showTooltip || ph.showDesc); + display = BqHelpers + .resolveDisplay(ph.questId, Minecraft.getMinecraft().thePlayer, ph.showTooltip || ph.showDesc); } catch (Throwable t) { ctx.replace(LytParagraph.error("[QuestCard] BetterQuesting integration not available")); return; @@ -69,7 +73,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { title.append(link); } else { var span = new LytFlowSpan(); - span.modifyStyle(style -> style.color(pickPlaceholderColor(state)).italic(true)); + span.modifyStyle( + style -> style.color(pickPlaceholderColor(state)) + .italic(true)); span.appendText(name); title.append(span); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java index 7e630c0d..67660351 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java @@ -10,25 +10,27 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.flow.LytFlowText; -import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; -import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; -import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; -import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; -import com.hfstudio.guidenh.integration.betterquesting.QuestState; -import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; +import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; +import com.hfstudio.guidenh.integration.betterquesting.QuestState; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; public class QuestLinkScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "QuestLink"; } + public String styleClass() { + return "QuestLink"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -43,8 +45,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { QuestDisplay display; try { - display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, - Boolean.TRUE.equals(showTooltip)); + display = BqHelpers + .resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, Boolean.TRUE.equals(showTooltip)); } catch (Throwable t) { ctx.replace(LytParagraph.error("[QuestLink] BetterQuesting integration not available")); return; @@ -57,19 +59,18 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } QuestState state = display.getState(); - String text = overrideText != null && !overrideText.isEmpty() - ? overrideText - : pickText(display, questId); + String text = overrideText != null && !overrideText.isEmpty() ? overrideText : pickText(display, questId); LytFlowContent replacement; if (QuestTagSupport.isNavigable(state)) { - replacement = QuestTagSupport.createQuestGuiLink(questId, display, text, - Boolean.TRUE.equals(showTooltip)); + replacement = QuestTagSupport.createQuestGuiLink(questId, display, text, Boolean.TRUE.equals(showTooltip)); } else { SymbolicColor color = state == QuestState.HIDDEN ? SymbolicColor.DARK_GRAY : state == QuestState.MISSING ? SymbolicColor.RED : SymbolicColor.GRAY; LytFlowSpan span = new LytFlowSpan(); - span.modifyStyle(style -> style.color(color).italic(true)); + span.modifyStyle( + style -> style.color(color) + .italic(true)); span.appendText(text); replacement = span; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index b9cd9508..d3bfe961 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -7,6 +7,7 @@ import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; @@ -14,66 +15,70 @@ import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.document.LytErrorSink; -import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.PageIndex; +import com.hfstudio.guidenh.guide.internal.host.EventType; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.scene.CameraSettings; -import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; -import com.hfstudio.guidenh.guide.scene.annotation.compiler.AnnotationTagCompiler; -import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; import com.hfstudio.guidenh.guide.scene.SceneTagCompiler; import com.hfstudio.guidenh.guide.scene.SceneTagCompiler.ScenePlaceholder; +import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; +import com.hfstudio.guidenh.guide.scene.annotation.compiler.AnnotationTagCompiler; +import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.libs.mdast.MdAst; -import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.unist.UnistNode; -import com.hfstudio.guidenh.config.ModConfig; -import com.hfstudio.guidenh.guide.internal.host.EventType; -import com.hfstudio.guidenh.guide.internal.host.LytEvent; -import com.hfstudio.guidenh.guide.internal.host.LytScript; -import com.hfstudio.guidenh.guide.internal.host.ScriptContext; -import com.hfstudio.guidenh.guide.internal.host.ScriptType; - import cpw.mods.fml.common.FMLLog; public class SceneScript implements LytScript { - public SceneScript() { - } + public SceneScript() {} @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Scene"; } + public String styleClass() { + return "Scene"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; if (!(node instanceof ScenePlaceholder ph)) return; - if (ph.childrenSource == null || ph.childrenSource.trim().isEmpty()) { + if (ph.childrenSource == null || ph.childrenSource.trim() + .isEmpty()) { ctx.replace(LytParagraph.error("[Scene] Empty scene: no scene elements")); return; } GuidebookLevel level = new GuidebookLevel(); CameraSettings camera = new CameraSettings(); - if (ph.perspective != null && !ph.perspective.trim().isEmpty()) { - camera.setPerspectivePreset( - PerspectivePreset.fromSerializedName(ph.perspective.trim())); + if (ph.perspective != null && !ph.perspective.trim() + .isEmpty()) { + camera.setPerspectivePreset(PerspectivePreset.fromSerializedName(ph.perspective.trim())); } if (!Float.isNaN(ph.zoom)) camera.setZoom(ph.zoom); if (!Float.isNaN(ph.rotateX)) camera.setRotationX(ph.rotateX); @@ -95,10 +100,12 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Parse children source ExceptionCollector errorSink = new ExceptionCollector(); PageCollection pc = ctx.getPageCollection(); - ExtensionCollection extensions = pc instanceof Guide guide - ? guide.getExtensions() : ExtensionCollection.EMPTY; - PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), - extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), + ExtensionCollection extensions = pc instanceof Guide guide ? guide.getExtensions() : ExtensionCollection.EMPTY; + PageCompiler runtimeCompiler = new PageCompiler( + pc != null ? pc : new StubPageCollection(), + extensions, + ph.sourcePack, + new ResourceLocation(ph.pageDomain, "scene"), ph.childrenSource != null ? ph.childrenSource : ""); MdAstRoot ast; try { @@ -108,7 +115,8 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), MdAstToMdxConverter.convert(ast, Collections.emptyMap()); } } catch (Exception e) { - FMLLog.getLogger().warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); + FMLLog.getLogger() + .warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); ctx.replace(LytParagraph.error("[Scene] Failed to parse scene elements")); return; } @@ -146,7 +154,7 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), // can call scene.attachPonderData(), scene.addAnnotation(), etc. var prevScene = AnnotationTagCompiler.CURRENT_SCENE.get(); AnnotationTagCompiler.CURRENT_SCENE.set(scene); - final boolean[] blockStatsExplicitlySet = {false}; + final boolean[] blockStatsExplicitlySet = { false }; try { GuideSceneStructureCompileScope.run(true, () -> { for (UnistNode child : ast.children()) { @@ -195,24 +203,20 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), scene.setBlockStatsButtonEnabled(ModConfig.ui.sceneBlockStatsButtonEnabled); } - // Finalize scene setup: auto-center, ponder baseline, interactive state capture - float[] center = level.getCenter(); + // Determine rotation center; fall back to level center + float[] center; if (!ph.explicitCenter) { + center = level.getCenter(); camera.setRotationCenter(center[0], center[1], center[2]); - } - // Auto-center the scene in the viewport - if (!ph.explicitCenter && Float.isNaN(ph.offsetX) && Float.isNaN(ph.offsetY)) { - camera.setOffsetX(0f); - camera.setOffsetY(0f); - var sc = camera.worldToScreen(center[0], center[1], center[2]); - camera.setOffsetX(-sc.x); - camera.setOffsetY(sc.y); - } else if (!Float.isNaN(ph.offsetX) || !Float.isNaN(ph.offsetY)) { - if (!Float.isNaN(ph.offsetX)) camera.setOffsetX(ph.offsetX); - if (!Float.isNaN(ph.offsetY)) camera.setOffsetY(ph.offsetY); + } else { + center = new float[] { Float.isNaN(ph.centerX) ? 0f : ph.centerX, Float.isNaN(ph.centerY) ? 0f : ph.centerY, + Float.isNaN(ph.centerZ) ? 0f : ph.centerZ }; } - // Auto-zoom: when zoom is not explicitly set, fit scene to viewport at 85% fill + boolean explicitOffX = !Float.isNaN(ph.offsetX); + boolean explicitOffY = !Float.isNaN(ph.offsetY); + + // Auto-zoom: measure at zoom=1/offset=0, then fit to viewport at 85% fill if (Float.isNaN(ph.zoom)) { camera.setZoom(1f); camera.setOffsetX(0f); @@ -230,9 +234,15 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), camera.setZoom(autoZoom); } } + // Restore explicit offsets zeroed for measurement + if (explicitOffX) camera.setOffsetX(ph.offsetX); + if (explicitOffY) camera.setOffsetY(ph.offsetY); } - // Auto-size: when width/height not explicitly set, measure and compute viewport + + // Auto-size: save offsets, measure at offset=0, restore if (!ph.explicitWidth || !ph.explicitHeight) { + float savedOffX = camera.getOffsetX(); + float savedOffY = camera.getOffsetY(); camera.setOffsetX(0f); camera.setOffsetY(0f); if (!level.isEmpty()) { @@ -247,6 +257,18 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), scene.setSceneSize(width, height); camera.setViewportSize(width, height); } + camera.setOffsetX(savedOffX); + camera.setOffsetY(savedOffY); + } + + // Auto-center: shift projected scene center to viewport origin. + // Applied only when neither rotation center nor screen offsets are author-specified. + if (!ph.explicitCenter && !explicitOffX && !explicitOffY) { + camera.setOffsetX(0f); + camera.setOffsetY(0f); + var sc = camera.worldToScreen(center[0], center[1], center[2]); + camera.setOffsetX(-sc.x); + camera.setOffsetY(sc.y); } scene.initializePonderTimelineBaseline(); @@ -276,23 +298,49 @@ private static void applyBlockStatsConfig(LytGuidebookScene scene, MdxJsxElement } private static class ExceptionCollector implements LytErrorSink { + @Override public void appendError(PageCompiler compiler, String text, UnistNode node) { - FMLLog.getLogger().warn("[GuideNH] [SceneScript] {}", text); + FMLLog.getLogger() + .warn("[GuideNH] [SceneScript] {}", text); } } private static class StubPageCollection implements PageCollection { - @Override public T getIndex(Class c) { return null; } - @Override public Collection getPages() { + + @Override + public T getIndex(Class c) { + return null; + } + + @Override + public Collection getPages() { return Collections.emptyList(); } - @Override public ParsedGuidePage getParsedPage(ResourceLocation id) { return null; } - @Override public GuidePage getPage(ResourceLocation id) { return null; } - @Override public byte[] loadAsset(ResourceLocation id) { return null; } - @Override public NavigationTree getNavigationTree() { + + @Override + public ParsedGuidePage getParsedPage(ResourceLocation id) { + return null; + } + + @Override + public GuidePage getPage(ResourceLocation id) { + return null; + } + + @Override + public byte[] loadAsset(ResourceLocation id) { + return null; + } + + @Override + public NavigationTree getNavigationTree() { return new NavigationTree(); } - @Override public boolean pageExists(ResourceLocation pageId) { return false; } + + @Override + public boolean pageExists(ResourceLocation pageId) { + return false; + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index ddb91ebc..8a7c939b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -6,8 +6,8 @@ import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.SpecialCompiler.SpecialPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; -import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -20,13 +20,19 @@ public class SpecialScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Special"; } + public String styleClass() { + return "Special"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -50,17 +56,14 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } MediaWikiListContext context = MediaWikiTagCompilerSupport.createListContext(guide, categoryIndex); - MediaWikiSpecialPageQuery query = new MediaWikiSpecialPageQuery("", - MediaWikiSpecialPageQuery.PAGE_SIZE); + MediaWikiSpecialPageQuery query = new MediaWikiSpecialPageQuery("", MediaWikiSpecialPageQuery.PAGE_SIZE); if (ph.page != null) query = query.withParameter("page", ph.page); if (ph.prefix != null) query = query.withParameter("prefix", ph.prefix); if (ph.language != null) query = query.withParameter("language", ph.language); if (ph.query != null) query = query.withSearchText(ph.query); - var result = resolver.resolve(context, specialName, - query.withVisibleCount(Integer.MAX_VALUE)); - var block = MediaWikiTagCompilerSupport.createSpecialBlock( - result, ph.rows, context, query, resolver); + var result = resolver.resolve(context, specialName, query.withVisibleCount(Integer.MAX_VALUE)); + var block = MediaWikiTagCompilerSupport.createSpecialBlock(result, ph.rows, context, query, resolver); ctx.replace(block); ctx.markComplete(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index 49b148df..67c75031 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -25,10 +25,14 @@ public class SubPagesScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "SubPages"; } + public String styleClass() { + return "SubPages"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java index 96dc214f..0d36987e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java @@ -13,16 +13,21 @@ public class TooltipScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Tooltip"; } + public String styleClass() { + return "Tooltip"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; if (!(node instanceof LytTooltipSpan span)) return; - var tooltip = span.getTooltip(0, 0).orElse(null); + var tooltip = span.getTooltip(0, 0) + .orElse(null); if (!(tooltip instanceof ContentTooltip ct)) return; LytBlock content = ct.getContent(); if (content instanceof LytNode root) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java index 1a9cf660..04d1d775 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java @@ -25,11 +25,14 @@ private MarkdownListSemantics() {} // Post-conversion:

    element wrapping the task text if (firstChild instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) firstChild).name())) { MdxJsxFlowElement p = (MdxJsxFlowElement) firstChild; - if (p.children().isEmpty()) { + if (p.children() + .isEmpty()) { return null; } - if (p.children().get(0) instanceof MdAstText) { - MdAstText text = (MdAstText) p.children().get(0); + if (p.children() + .get(0) instanceof MdAstText) { + MdAstText text = (MdAstText) p.children() + .get(0); Matcher matcher = TASK_PATTERN.matcher(text.value); if (matcher.matches()) { return new TaskMarker(!" ".equals(matcher.group(1)), matcher.group(2)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java index 63c86804..8ac4aec8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java @@ -32,7 +32,8 @@ private MarkdownRuntimeBlocks() {} } String firstText = firstParagraph.text(); - if (firstText == null || firstText.trim().isEmpty()) { + if (firstText == null || firstText.trim() + .isEmpty()) { return null; } @@ -55,7 +56,8 @@ private MarkdownRuntimeBlocks() {} return null; } - String expression = trimmed.substring(2, directiveEnd).trim(); + String expression = trimmed.substring(2, directiveEnd) + .trim(); String title = null; ColorValue color = null; QuoteIconSpec icon = null; @@ -64,8 +66,11 @@ private MarkdownRuntimeBlocks() {} if (equalsIndex <= 0 || equalsIndex >= token.length() - 1) { continue; } - String key = token.substring(0, equalsIndex).trim(); - String value = stripOptionalQuotes(token.substring(equalsIndex + 1).trim()); + String key = token.substring(0, equalsIndex) + .trim(); + String value = stripOptionalQuotes( + token.substring(equalsIndex + 1) + .trim()); if (value.isEmpty()) { continue; } @@ -102,12 +107,14 @@ private static FirstParagraphText findFirstParagraphText(MdxJsxElementFields blo if (child instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) child).name())) { MdxJsxFlowElement p = (MdxJsxFlowElement) child; String text = getLeadingParagraphText(p); - if (text != null && !text.trim().isEmpty()) { + if (text != null && !text.trim() + .isEmpty()) { return new FirstParagraphText(p, text); } } else if (child instanceof MdAstText) { MdAstText text = (MdAstText) child; - if (!text.value.trim().isEmpty()) { + if (!text.value.trim() + .isEmpty()) { return new FirstParagraphText(null, text.value); } } @@ -120,7 +127,8 @@ private static String getLeadingParagraphText(MdxJsxFlowElement paragraph) { for (Object child : paragraph.children()) { if (child instanceof MdAstText) { MdAstText text = (MdAstText) child; - if (!text.value.trim().isEmpty()) { + if (!text.value.trim() + .isEmpty()) { return text.value; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java index 0209284c..0965b891 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java @@ -7,7 +7,6 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter; -import com.hfstudio.guidenh.libs.micromark.extensions.gfm.Align; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTable; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableCell; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableRow; @@ -16,8 +15,6 @@ import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstDottedUnderline; import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstUnderline; import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstWavyUnderline; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxExpressionAttribute; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; @@ -26,7 +23,6 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstCode; import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis; -import com.hfstudio.guidenh.libs.mdast.model.MdAstFlowContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstHTML; import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading; import com.hfstudio.guidenh.libs.mdast.model.MdAstImage; @@ -41,9 +37,10 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; import com.hfstudio.guidenh.libs.mdast.model.MdAstPhrasingContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; +import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak; -import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong; +import com.hfstudio.guidenh.libs.micromark.extensions.gfm.Align; public final class MdAstToMdxConverter { @@ -61,7 +58,7 @@ private static void convertParent(MdAstParent parent, Map children = parent.children(); for (Object child : new ArrayList<>(children)) { - if (child instanceof MdAstParent childParent) { + if (child instanceof MdAstParentchildParent) { convertParent(childParent, definitions); } } @@ -84,19 +81,35 @@ private static boolean isPhrasingParent(MdAstParent parent) { return name != null && PHRASING_CONTAINER_NAMES.contains(name); } String type = parent.type(); - return "link".equals(type) - || "strong".equals(type) + return "link".equals(type) || "strong".equals(type) || "emphasis".equals(type) || "delete".equals(type) || "heading".equals(type); } // Containers whose children are inline/phrasing content only - private static final java.util.Set PHRASING_CONTAINER_NAMES = - new java.util.HashSet<>(java.util.Arrays.asList( - "p", "h1", "h2", "h3", "h4", "h5", "h6", - "td", "th", "summary", "a", - "strong", "em", "del", "u", "wavy", "dotted", "mark", "code", "span")); + private static final java.util.Set PHRASING_CONTAINER_NAMES = new java.util.HashSet<>( + java.util.Arrays.asList( + "p", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "td", + "th", + "summary", + "a", + "strong", + "em", + "del", + "u", + "wavy", + "dotted", + "mark", + "code", + "span")); @SuppressWarnings("unchecked") private static List castAnyChildren(List children) { @@ -104,13 +117,12 @@ private static List castAnyChildren(List children) { } // ----------------------------------------------------------------------- - // Phrasing (inline) children conversion — also handles block nodes that - // may appear inside phrasing containers (e.g. MdAstParagraph inside ). + // Phrasing (inline) children conversion — also handles block nodes that + // may appear inside phrasing containers (e.g. MdAstParagraph inside ). // ----------------------------------------------------------------------- - @SuppressWarnings({"unchecked", "rawtypes"}) - private static void convertPhrasingChildren(List children, - Map definitions) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void convertPhrasingChildren(List children, Map definitions) { for (int i = 0; i < children.size(); i++) { Object child = children.get(i); Object replacement = null; @@ -197,11 +209,10 @@ else if (child instanceof MdAstParagraph p) { } // ----------------------------------------------------------------------- - // Flow (block) children conversion + // Flow (block) children conversion // ----------------------------------------------------------------------- - private static void convertFlowChildren(List children, - Map definitions) { + private static void convertFlowChildren(List children, Map definitions) { for (int i = 0; i < children.size(); i++) { MdAstAnyContent child = children.get(i); MdxJsxFlowElement replacement = null; @@ -290,7 +301,7 @@ private static void convertFlowChildren(List children, } // ----------------------------------------------------------------------- - // Factory helpers + // Factory helpers // ----------------------------------------------------------------------- /** @@ -299,9 +310,12 @@ private static void convertFlowChildren(List children, * Uses raw-type list access to bypass the generic type check so that phrasing * content (e.g. {@link MdxJsxTextElement}, {@link MdAstText}) can be placed * inside flow elements where they are semantically valid (e.g. text inside - * {@code

    }). + * {@code + * +

    + * }). */ - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({ "unchecked", "rawtypes" }) private static MdxJsxFlowElement createFlow(String name, List children) { MdxJsxFlowElement element = new MdxJsxFlowElement(); element.setName(name); @@ -315,7 +329,7 @@ private static MdxJsxFlowElement createFlow(String name, List children) { MdxJsxTextElement element = new MdxJsxTextElement(); element.setName(name); @@ -328,29 +342,34 @@ private static MdxJsxTextElement createText(String name, List} tag). + * inside the element (e.g. a {@link MdAstText} inside a {@code + * + * + +

    +     * } tag).
          */
    -    @SuppressWarnings({"unchecked", "rawtypes"})
    +    @SuppressWarnings({ "unchecked", "rawtypes" })
         private static void addChildRaw(MdxJsxFlowElement element, MdAstNode node) {
             ((List) element.children()).add(node);
         }
     
         /**
          * Adds an {@link MdAstNode} to a text element's children list via raw-type
    -     * access, bypassing the generic type check.  This is needed when the child
    +     * access, bypassing the generic type check. This is needed when the child
          * is a non-phrasing node that is semantically valid inline (e.g. a
          * {@link MdAstText} inside {@code }).
          */
    -    @SuppressWarnings({"unchecked", "rawtypes"})
    +    @SuppressWarnings({ "unchecked", "rawtypes" })
         private static void addChildRaw(MdxJsxTextElement element, MdAstNode node) {
             ((List) element.children()).add(node);
         }
     
         /**
          * Serializes the GfmTable align list to a comma-separated lowercase string,
    -     * e.g. {@code "left,center,right"}.  Returns {@code null} when the list is
    +     * e.g. {@code "left,center,right"}. Returns {@code null} when the list is
          * null or empty.
          */
         @Nullable
    @@ -379,8 +398,10 @@ private static String serializeAlign(@Nullable List aligns) {
          */
         @Nullable
         private static String extractKramdownMeta(MdAstParagraph p) {
    -        if (p.children().size() != 1) return null;
    -        if (!(p.children().get(0) instanceof MdAstText t)) return null;
    +        if (p.children()
    +            .size() != 1) return null;
    +        if (!(p.children()
    +            .get(0) instanceof MdAstText t)) return null;
             String v = t.value.trim();
             if (v.startsWith("{:") && v.endsWith("}")) return v;
             return null;
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java
    index 1f4291d2..44ff2288 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java
    @@ -3,7 +3,9 @@
     public class DevWatchWorkItem implements WorkItem {
     
         @Override
    -    public Priority priority() { return Priority.LOW; }
    +    public Priority priority() {
    +        return Priority.LOW;
    +    }
     
         @Override
         public boolean shouldRun() {
    @@ -21,5 +23,7 @@ public boolean equals(Object o) {
         }
     
         @Override
    -    public int hashCode() { return DevWatchWorkItem.class.hashCode(); }
    +    public int hashCode() {
    +        return DevWatchWorkItem.class.hashCode();
    +    }
     }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java
    index b8e7ca97..0e47cc4f 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java
    @@ -3,6 +3,7 @@
     import com.hfstudio.guidenh.guide.internal.host.LytHost;
     
     public class LytHostPreheatItem implements WorkItem {
    +
         private final LytHost host;
     
         public LytHostPreheatItem(LytHost host) {
    @@ -10,10 +11,14 @@ public LytHostPreheatItem(LytHost host) {
         }
     
         @Override
    -    public Priority priority() { return Priority.MEDIUM; }
    +    public Priority priority() {
    +        return Priority.MEDIUM;
    +    }
     
         @Override
    -    public boolean shouldRun() { return host.hasPreheatWork(); }
    +    public boolean shouldRun() {
    +        return host.hasPreheatWork();
    +    }
     
         @Override
         public WorkResult tick(long deadlineNs) {
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java
    index 42ea8789..efdce34c 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java
    @@ -3,7 +3,6 @@
     import java.util.ArrayList;
     import java.util.Iterator;
     import java.util.LinkedHashMap;
    -import java.util.List;
     import java.util.Map;
     
     import cpw.mods.fml.common.FMLCommonHandler;
    @@ -22,7 +21,9 @@ public class MasterScheduler {
     
         private static MasterScheduler instance;
     
    -    public static MasterScheduler getInstance() { return instance; }
    +    public static MasterScheduler getInstance() {
    +        return instance;
    +    }
     
         public static void init() {
             instance = new MasterScheduler();
    @@ -62,9 +63,12 @@ public void clear() {
     
         private Map queueFor(Priority p) {
             switch (p) {
    -            case HIGH:   return high;
    -            case MEDIUM: return medium;
    -            default:     return low;
    +            case HIGH:
    +                return high;
    +            case MEDIUM:
    +                return medium;
    +            default:
    +                return low;
             }
         }
     
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java
    index 284d6a4f..09fe424b 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java
    @@ -6,10 +6,14 @@
     public class SearchIndexWorkItem implements WorkItem {
     
         @Override
    -    public Priority priority() { return Priority.LOW; }
    +    public Priority priority() {
    +        return Priority.LOW;
    +    }
     
         @Override
    -    public boolean shouldRun() { return true; }
    +    public boolean shouldRun() {
    +        return true;
    +    }
     
         @Override
         public WorkResult tick(long deadlineNs) {
    @@ -24,5 +28,7 @@ public boolean equals(Object o) {
         }
     
         @Override
    -    public int hashCode() { return SearchIndexWorkItem.class.hashCode(); }
    +    public int hashCode() {
    +        return SearchIndexWorkItem.class.hashCode();
    +    }
     }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java
    index 92d0eeba..3a0f12da 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java
    @@ -1,7 +1,10 @@
     package com.hfstudio.guidenh.guide.internal.scheduler;
     
     public interface WorkItem {
    +
         Priority priority();
    +
         boolean shouldRun();
    +
         WorkResult tick(long deadlineNs);
     }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
    index 797057d7..a05ed861 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
    @@ -14,12 +14,12 @@
     import com.hfstudio.guidenh.guide.extensions.ExtensionCollection;
     import com.hfstudio.guidenh.guide.extensions.ExtensionPoint;
     import com.hfstudio.guidenh.guide.indices.PageIndex;
    +import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent;
    +import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstText;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition;
    -import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter;
     import com.hfstudio.guidenh.libs.unist.UnistNode;
     
     import cpw.mods.fml.common.FMLLog;
    @@ -66,9 +66,7 @@ public void indexContent(MdAstAnyContent content, IndexingSink sink) {
                 var compiler = tagCompilers.get(el.name());
                 if (compiler == null) {
                     FMLLog.getLogger()
    -                    .warn(
    -                        "[GuideNH] [PageIndexer] Unhandled MDX element in guide search indexing: {}",
    -                        el.name());
    +                    .warn("[GuideNH] [PageIndexer] Unhandled MDX element in guide search indexing: {}", el.name());
                     // Fallback: index children content
                     indexContent(el.children(), sink);
                 } else {
    @@ -78,7 +76,8 @@ public void indexContent(MdAstAnyContent content, IndexingSink sink) {
                 // Handled via conversion
             } else {
                 FMLLog.getLogger()
    -                .warn("[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}",
    +                .warn(
    +                    "[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}",
                         ((UnistNode) content).type());
             }
         }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java
    index f5911e18..848af3f6 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java
    @@ -76,7 +76,9 @@ public static List planColumns(List ent
             int maxGroupHeight = 0;
             int totalHeight = 0;
             for (int i = 0; i < groups.size(); i++) {
    -            groupHeights[i] = SECTION_HEADER_HEIGHT + groups.get(i).entries().size() * ROW_HEIGHT;
    +            groupHeights[i] = SECTION_HEADER_HEIGHT + groups.get(i)
    +                .entries()
    +                .size() * ROW_HEIGHT;
                 maxGroupHeight = Math.max(maxGroupHeight, groupHeights[i]);
                 totalHeight += groupHeights[i];
             }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java
    index de2d2a70..243e6926 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java
    @@ -10,6 +10,7 @@
     import org.jetbrains.annotations.Nullable;
     
     import com.hfstudio.guidenh.config.ModConfig;
    +import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions;
     import com.hfstudio.guidenh.guide.compiler.PageCompiler;
     import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage;
     import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler;
    @@ -17,10 +18,9 @@
     import com.hfstudio.guidenh.guide.document.block.LytBlockContainer;
     import com.hfstudio.guidenh.guide.document.block.LytParagraph;
     import com.hfstudio.guidenh.guide.extensions.ExtensionCollection;
    +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter;
     import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureFingerprintResolver;
     import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler;
    -import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions;
    -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter;
     import com.hfstudio.guidenh.libs.mdast.MdAst;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstNode;
    @@ -135,30 +135,47 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl
                     preParsedAst = MdAst.fromMarkdown(childrenSource, GuideMarkdownOptions.runtime());
                     MdAstToMdxConverter.convert(preParsedAst, Collections.emptyMap());
                 } catch (RuntimeException e) {
    -                FMLLog.getLogger().warn("[GuideNH] [SceneTagCompiler] Failed to pre-parse scene children", e);
    +                FMLLog.getLogger()
    +                    .warn("[GuideNH] [SceneTagCompiler] Failed to pre-parse scene children", e);
                 }
             }
     
             // Store element compilers from ExtensionCollection in placeholder
    -        var sceneElementCompilers = compiler.getExtensions().get(SceneElementTagCompiler.EXTENSION_POINT);
    +        var sceneElementCompilers = compiler.getExtensions()
    +            .get(SceneElementTagCompiler.EXTENSION_POINT);
     
             // Create placeholder block that carries all scene config to SceneScript
             String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene";
             ScenePlaceholder placeholder = new ScenePlaceholder(
    -            w, h, explicitWidth, explicitHeight,
    -            zoom, explicitZoom,
    +            w,
    +            h,
    +            explicitWidth,
    +            explicitHeight,
    +            zoom,
    +            explicitZoom,
                 perspective,
    -            rx, ry, rz,
    -            offX, offY, explicitOffX, explicitOffY,
    -            centerX, centerY, centerZ, explicitCenter,
    -            interactive, showBackground,
    -            allowLayerSlider, gridButtonEnabled, showGrid,
    +            rx,
    +            ry,
    +            rz,
    +            offX,
    +            offY,
    +            explicitOffX,
    +            explicitOffY,
    +            centerX,
    +            centerY,
    +            centerZ,
    +            explicitCenter,
    +            interactive,
    +            showBackground,
    +            allowLayerSlider,
    +            gridButtonEnabled,
    +            showGrid,
                 childrenSource,
    -            compiler.getPageId().getResourceDomain(),
    +            compiler.getPageId()
    +                .getResourceDomain(),
                 compiler.getSourcePack(),
                 preParsedAst,
    -            sceneElementCompilers
    -        );
    +            sceneElementCompilers);
             placeholder.setStyleClass(styleClass);
             placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE);
             placeholder.appendText("[" + styleClass + "]");
    @@ -184,7 +201,8 @@ public static class ScenePlaceholder extends LytParagraph {
             public final boolean explicitHeight;
             public final float zoom;
             public final boolean explicitZoom;
    -        @Nullable public final String perspective;
    +        @Nullable
    +        public final String perspective;
             public final float rotateX;
             public final float rotateY;
             public final float rotateZ;
    @@ -201,30 +219,21 @@ public static class ScenePlaceholder extends LytParagraph {
             public final boolean allowLayerSlider;
             public final boolean gridButtonEnabled;
             public final boolean showGrid;
    -        @Nullable public final String childrenSource;
    +        @Nullable
    +        public final String childrenSource;
             public final String pageDomain;
             public final String sourcePack;
    -        @Nullable public final MdAstRoot childrenAst;
    +        @Nullable
    +        public final MdAstRoot childrenAst;
             @Nullable
             public final List sceneElementCompilers;
     
    -        public ScenePlaceholder(
    -            int width, int height,
    -            boolean explicitWidth, boolean explicitHeight,
    -            float zoom, boolean explicitZoom,
    -            @Nullable String perspective,
    -            float rotateX, float rotateY, float rotateZ,
    -            float offsetX, float offsetY,
    -            boolean explicitOffsetX, boolean explicitOffsetY,
    -            float centerX, float centerY, float centerZ,
    -            boolean explicitCenter,
    -            boolean interactive, boolean showBackground,
    -            boolean allowLayerSlider, boolean gridButtonEnabled,
    -            boolean showGrid,
    -            @Nullable String childrenSource,
    -            String pageDomain,
    -            String sourcePack,
    -            @Nullable MdAstRoot childrenAst,
    +        public ScenePlaceholder(int width, int height, boolean explicitWidth, boolean explicitHeight, float zoom,
    +            boolean explicitZoom, @Nullable String perspective, float rotateX, float rotateY, float rotateZ,
    +            float offsetX, float offsetY, boolean explicitOffsetX, boolean explicitOffsetY, float centerX,
    +            float centerY, float centerZ, boolean explicitCenter, boolean interactive, boolean showBackground,
    +            boolean allowLayerSlider, boolean gridButtonEnabled, boolean showGrid, @Nullable String childrenSource,
    +            String pageDomain, String sourcePack, @Nullable MdAstRoot childrenAst,
                 @Nullable List sceneElementCompilers) {
                 this.width = width;
                 this.height = height;
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java
    index eb484fec..852959f5 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java
    @@ -18,12 +18,29 @@ public SceneViewportMetrics(float minScreenX, float maxScreenX, float minScreenY
             this.maxScreenY = maxScreenY;
         }
     
    -    public float minScreenX() { return minScreenX; }
    -    public float maxScreenX() { return maxScreenX; }
    -    public float minScreenY() { return minScreenY; }
    -    public float maxScreenY() { return maxScreenY; }
    -    public float spanX() { return maxScreenX - minScreenX; }
    -    public float spanY() { return maxScreenY - minScreenY; }
    +    public float minScreenX() {
    +        return minScreenX;
    +    }
    +
    +    public float maxScreenX() {
    +        return maxScreenX;
    +    }
    +
    +    public float minScreenY() {
    +        return minScreenY;
    +    }
    +
    +    public float maxScreenY() {
    +        return maxScreenY;
    +    }
    +
    +    public float spanX() {
    +        return maxScreenX - minScreenX;
    +    }
    +
    +    public float spanY() {
    +        return maxScreenY - minScreenY;
    +    }
     
         /**
          * Projects the 8 corners of the given axis-aligned bounding box through
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
    index 3bd9b605..11394a3f 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
    @@ -22,7 +22,6 @@
     import com.hfstudio.guidenh.guide.document.interaction.TextTooltip;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownActionLink;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLatexShorthand;
    -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownListSemantics;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.BlockquoteDirective;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec;
    @@ -30,38 +29,14 @@
     import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser;
     import com.hfstudio.guidenh.guide.sound.GuideSoundSpec;
     import com.hfstudio.guidenh.guide.sound.GuideSoundTrigger;
    -import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTable;
    -import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableCell;
    -import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableRow;
    -import com.hfstudio.guidenh.libs.mdast.gfmstrikethrough.MdAstDelete;
    -import com.hfstudio.guidenh.libs.mdast.guidemark.MdAstMark;
    -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstDottedUnderline;
    -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstUnderline;
    -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstWavyUnderline;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttributeNode;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstBlockquote;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstBreak;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstCode;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstImage;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstLink;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstList;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstListItem;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstNode;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstParent;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstPhrasingContent;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstText;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak;
    -import com.hfstudio.guidenh.libs.micromark.extensions.gfm.Align;
     
     public class GuideSiteHtmlCompiler {
     
    @@ -249,11 +224,21 @@ public String compileInlineFragment(List children, Gu
             StringBuilder html = new StringBuilder();
             for (MdAstAnyContent child : children) {
                 if (child instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) child).name())) {
    -                html.append(compileChildren(((MdxJsxFlowElement) child).children(),
    -                    templates, defaultNamespace, currentPageId, sceneResolver));
    +                html.append(
    +                    compileChildren(
    +                        ((MdxJsxFlowElement) child).children(),
    +                        templates,
    +                        defaultNamespace,
    +                        currentPageId,
    +                        sceneResolver));
                 } else if (child instanceof MdxJsxFlowElement) {
    -                html.append(compileChildren(((MdxJsxFlowElement) child).children(),
    -                    templates, defaultNamespace, currentPageId, sceneResolver));
    +                html.append(
    +                    compileChildren(
    +                        ((MdxJsxFlowElement) child).children(),
    +                        templates,
    +                        defaultNamespace,
    +                        currentPageId,
    +                        sceneResolver));
                 } else {
                     html.append(compileNode(child, templates, defaultNamespace, currentPageId, sceneResolver));
                 }
    @@ -276,12 +261,20 @@ private String compileNode(MdAstAnyContent node, GuideSiteTemplateRegistry templ
                 return compileText(((MdAstText) node).value(), templates, defaultNamespace, currentPageId);
             }
             if (node instanceof MdxJsxElementFields) {
    -            return compileMdxElement((MdxJsxElementFields) node, templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileMdxElement(
    +                (MdxJsxElementFields) node,
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             if (node instanceof MdAstParent) {
    -            return compileChildren(((MdAstParent) node).children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileChildren(
    +                ((MdAstParent) node).children(),
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             return "";
         }
    @@ -322,32 +315,37 @@ private String compileMdxElement(MdxJsxElementFields el, GuideSiteTemplateRegist
             }
             // Inline elements
             if ("strong".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("em".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver) + "";
    +            return "" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("del".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver) + "";
    +            return "" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("u".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("wavy".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("dotted".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("mark".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("code".equals(el.name())) {
                 return "" + escapeHtml(extractTextFromElement(el)) + "";
    @@ -366,19 +364,29 @@ private String compileMdxElement(MdxJsxElementFields el, GuideSiteTemplateRegist
             }
             // Custom MDX tags (existing handlers)
             if (el instanceof MdxJsxFlowElement) {
    -            return compileCustomFlowElement((MdxJsxFlowElement) el, templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileCustomFlowElement(
    +                (MdxJsxFlowElement) el,
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             if (el instanceof MdxJsxTextElement) {
    -            return compileCustomTextElement((MdxJsxTextElement) el, templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileCustomTextElement(
    +                (MdxJsxTextElement) el,
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             return compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver);
         }
     
         private boolean isHeadingName(@Nullable String name) {
    -        return name != null && name.length() == 2 && name.charAt(0) == 'h'
    -            && name.charAt(1) >= '1' && name.charAt(1) <= '6';
    +        return name != null && name.length() == 2
    +            && name.charAt(0) == 'h'
    +            && name.charAt(1) >= '1'
    +            && name.charAt(1) <= '6';
         }
     
         private String compileCustomFlowElement(MdxJsxFlowElement flowElement, GuideSiteTemplateRegistry templates,
    @@ -454,16 +462,19 @@ private String compileBlockquoteMdx(MdxJsxElementFields el, GuideSiteTemplateReg
             if (directive != null) {
                 return compileQuoteBoxMdx(directive, templates, defaultNamespace, currentPageId, sceneResolver);
             }
    -        return "
    " + compileChildren(el.children(), templates, defaultNamespace, - currentPageId, sceneResolver) + "
    "; + return "
    " + + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) + + "
    "; } private String compileAlertBoxMdx(BlockquoteDirective directive, GuideSiteTemplateRegistry templates, String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { String body = compileChildren(directive.children(), templates, defaultNamespace, currentPageId, sceneResolver); - String typeName = directive.alertType().displayText(); - return "
    " + escapeHtml(typeName) + "" + body + "
    "; + String typeName = directive.alertType() + .displayText(); + return "
    " + escapeHtml(typeName) + "" + body + "
    "; } private String compileQuoteBoxMdx(BlockquoteDirective directive, GuideSiteTemplateRegistry templates, @@ -471,18 +482,23 @@ private String compileQuoteBoxMdx(BlockquoteDirective directive, GuideSiteTempla String body = compileChildren(directive.children(), templates, defaultNamespace, currentPageId, sceneResolver); StringBuilder html = new StringBuilder("
    "); if (directive.title() != null) { - html.append("").append(escapeHtml(directive.title())).append("
    "); + html.append("") + .append(escapeHtml(directive.title())) + .append("
    "); } - html.append(body).append("
    "); + html.append(body) + .append(""); return html.toString(); } - private String compileListMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, - String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + private String compileListMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, String defaultNamespace, + @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { String tag = "ol".equals(el.name()) ? "ol" : "ul"; String startAttr = ""; if ("ol".equals(el.name())) { @@ -491,9 +507,13 @@ private String compileListMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry startAttr = " start=\"" + escapeAttribute(startStr) + "\""; } } - return "<" + tag + startAttr + ">" + return "<" + tag + + startAttr + + ">" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + + ""; } private String compileListItemMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, @@ -531,7 +551,9 @@ private String compileCodeBlockMdx(MdxJsxElementFields el) { return GuideSiteGraphRenderer.renderFunctionGraph(graph); } catch (RuntimeException ignored) { return "
    " + escapeHtml(codeText) + "
    "; + + "\">" + + escapeHtml(codeText) + + "
    "; } } @@ -543,18 +565,26 @@ private String compileCodeBlockMdx(MdxJsxElementFields el) { if (width != null || height != null) { html.append(" class=\"guide-code-sized\" style=\""); if (width != null) { - html.append("width:").append(width).append("px;max-width:100%;"); + html.append("width:") + .append(width) + .append("px;max-width:100%;"); } if (height != null) { - html.append("height:").append(height).append("px;overflow:auto;"); + html.append("height:") + .append(height) + .append("px;overflow:auto;"); } html.append("\""); } html.append(">").append(escapeHtml(codeText)).append("
    "); + html.append(">") + .append(escapeHtml(codeText)) + .append(""); return html.toString(); } @@ -575,8 +605,8 @@ private static Integer parseMetaInt(String meta, String key) { return null; } - private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, - String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, String defaultNamespace, + @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { StringBuilder html = new StringBuilder(""); String alignStr = el.getAttributeString("align", ""); String[] aligns = alignStr.isEmpty() ? new String[0] : alignStr.split(","); @@ -596,10 +626,20 @@ private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry align = " style=\"text-align:" + a + "\""; } } - html.append("<").append(tag).append(align).append(">"); - html.append(compileChildren(((MdxJsxFlowElement) cellChild).children(), templates, - defaultNamespace, currentPageId, sceneResolver)); - html.append(""); + html.append("<") + .append(tag) + .append(align) + .append(">"); + html.append( + compileChildren( + ((MdxJsxFlowElement) cellChild).children(), + templates, + defaultNamespace, + currentPageId, + sceneResolver)); + html.append(""); cellIdx++; } } @@ -626,7 +666,9 @@ private String compileAnchorMdx(MdxJsxElementFields el, GuideSiteTemplateRegistr } if (!href.isEmpty()) { return "" + body + ""; + + "\">" + + body + + ""; } return body; } @@ -637,9 +679,14 @@ private String compileImageMdx(MdxJsxElementFields el, @Nullable ResourceLocatio String title = el.getAttributeString("title", ""); String resolvedSrc = GuideSiteHrefResolver.resolveRawHref(currentPageId, src); StringBuilder html = new StringBuilder("\"").append(escapeAttribute(alt)).append("\"");"); return html.toString(); } @@ -662,10 +709,15 @@ private static void collectTextFromChildren(MdxJsxElementFields el, StringBuilde @Nullable private String extractSoleDisplayLatexFromElement(MdxJsxElementFields el) { - if (el.children().size() != 1 || !(el.children().get(0) instanceof MdAstText)) { + if (el.children() + .size() != 1 + || !(el.children() + .get(0) instanceof MdAstText)) { return null; } - return MarkdownLatexShorthand.extractSoleDisplayFormula(((MdAstText) el.children().get(0)).value); + return MarkdownLatexShorthand.extractSoleDisplayFormula( + ((MdAstText) el.children() + .get(0)).value); } private String compileTooltip(MdxJsxElementFields element, GuideSiteTemplateRegistry templates, diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index 922bfc2d..1c850c7d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -9,9 +9,9 @@ import java.util.Locale; import java.util.Map; +import net.minecraft.block.Block; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; -import net.minecraft.block.Block; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; diff --git a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java index 68d0d79f..bf2ac1a2 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java @@ -50,6 +50,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } public static class QuestCardPlaceholder extends LytParagraph { + public final UUID questId; public final boolean showDesc; public final boolean showTooltip; diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java index 81f90269..163b1ac0 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java @@ -21,9 +21,11 @@ public class FactoryTag { private FactoryTag() {} - /** Marker set on a tag token when recovery happens at EOF (where a separate + /** + * Marker set on a tag token when recovery happens at EOF (where a separate * mdxJsxRecovery token cannot be emitted because consume(EOF) must be the - * last event). Read by {@code MdxMdastExtension.exitMdxJsxTag}. */ + * last event). Read by {@code MdxMdastExtension.exitMdxJsxTag}. + */ public static final TokenProperty RECOVERED_AT_EOF = new TokenProperty<>(); public static final Construct lazyLineEnd; From b68716e588a7161b4c7afd658a7e4f27913c4222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:46:03 +0800 Subject: [PATCH 086/136] fix: F3+T cache gaps, editor preview placeholders, SceneScript camera centering - Clear GuideLatexTextureCache, GuideSceneStructureCache, and 5 StructureLibBoundedCache instances on F3+T resource reload - Dispatch MOUNT events to editor preview document so async scripts materialize placeholder blocks in the guide editor split view - Reorder SceneScript camera setup to zoom -> size -> center and restore offset save/restore logic, matching Phase 2 SceneTagCompiler --- .../com/hfstudio/guidenh/ClientProxy.java | 63 ++--- .../compiler/MdxBlockTagSourceExtractor.java | 80 ++++++- .../guidenh/guide/compiler/PageCompiler.java | 47 ++-- .../compiler/tags/BlockImageCompiler.java | 29 ++- .../compiler/tags/BlockquoteCompiler.java | 38 +-- .../guide/compiler/tags/CodeCompiler.java | 9 +- .../guide/compiler/tags/CsvTableCompiler.java | 6 +- .../compiler/tags/DelUWaveMarkCompiler.java | 3 +- .../guide/compiler/tags/HeadingCompiler.java | 4 +- .../guide/compiler/tags/ItemGridCompiler.java | 1 + .../compiler/tags/ItemImageCompiler.java | 14 +- .../guide/compiler/tags/ListCompiler.java | 3 +- .../guide/compiler/tags/ListItemCompiler.java | 2 +- .../guidenh/guide/compiler/tags/MdxAttrs.java | 5 - .../guide/compiler/tags/MermaidCompiler.java | 44 +++- .../guide/compiler/tags/PreCompiler.java | 32 +-- .../compiler/tags/StructureViewCompiler.java | 2 + .../guide/compiler/tags/SubPagesCompiler.java | 7 +- .../guide/compiler/tags/TableCompiler.java | 6 +- .../tags/mediawiki/CategoryCompiler.java | 11 +- .../tags/mediawiki/SpecialCompiler.java | 12 +- .../guidenh/guide/document/block/LytNode.java | 27 ++- .../guide/document/flow/LytFlowContent.java | 28 ++- .../document/flow/LytFlowInlineBlock.java | 3 +- .../GuideLightweightReloadService.java | 15 +- .../guidenh/guide/internal/GuideScreen.java | 84 ++++--- .../guidenh/guide/internal/MutableGuide.java | 4 +- .../resolver/MdxSyntaxResolver.java | 2 +- .../editor/md/SceneEditorMarkdownCodec.java | 2 +- .../home/HomePageSummaryExtractor.java | 6 +- .../guide/internal/host/DeferredTask.java | 13 +- .../guidenh/guide/internal/host/LytEvent.java | 36 ++- .../guidenh/guide/internal/host/LytHost.java | 126 ++++++---- .../guide/internal/host/LytHostWorkItem.java | 12 +- .../guide/internal/host/LytScript.java | 3 + .../guide/internal/host/NavigationState.java | 74 ++++-- .../guide/internal/host/ScriptContext.java | 8 +- .../internal/host/ScriptContextImpl.java | 17 +- .../guide/internal/host/ViewportState.java | 42 +++- .../internal/host/scripts/CategoryScript.java | 12 +- .../host/scripts/CommandLinkScript.java | 10 +- .../internal/host/scripts/CsvTableScript.java | 12 +- .../host/scripts/FloatingImageScript.java | 12 +- .../internal/host/scripts/ImageScript.java | 12 +- .../internal/host/scripts/ItemGridScript.java | 9 +- .../host/scripts/ItemImageScript.java | 14 +- .../internal/host/scripts/KeyBindScript.java | 4 +- .../internal/host/scripts/MermaidScript.java | 43 +++- .../host/scripts/PlayerNameScript.java | 4 +- .../host/scripts/QuestCardScript.java | 28 ++- .../host/scripts/QuestLinkScript.java | 33 +-- .../internal/host/scripts/SceneScript.java | 148 ++++++++---- .../internal/host/scripts/SpecialScript.java | 23 +- .../internal/host/scripts/SubPagesScript.java | 8 +- .../internal/host/scripts/TooltipScript.java | 11 +- .../markdown/MarkdownListSemantics.java | 9 +- .../markdown/MarkdownRuntimeBlocks.java | 22 +- .../markdown/MdAstToMdxConverter.java | 87 ++++--- .../internal/scheduler/DevWatchWorkItem.java | 8 +- .../scheduler/LytHostPreheatItem.java | 9 +- .../internal/scheduler/MasterScheduler.java | 14 +- .../scheduler/SearchIndexWorkItem.java | 12 +- .../guide/internal/scheduler/WorkItem.java | 3 + .../guide/internal/search/PageIndexer.java | 11 +- .../guide/mediawiki/MediaWikiListPlanner.java | 4 +- .../guidenh/guide/scene/SceneTagCompiler.java | 77 ++++--- .../guide/scene/SceneViewportMetrics.java | 29 ++- .../site/GuideSiteHtmlCompiler.java | 216 +++++++++++------- .../site/GuideSiteMdxTagRenderer.java | 2 +- .../compiler/QuestCardCompiler.java | 1 + .../hfstudio/guidenh/libs/mdx/FactoryTag.java | 6 +- 71 files changed, 1215 insertions(+), 598 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 945c5036..0ee45df4 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -18,9 +18,7 @@ import com.hfstudio.guidenh.guide.internal.GuideDevelopmentResourcePackWatcher; import com.hfstudio.guidenh.guide.internal.GuideME; import com.hfstudio.guidenh.guide.internal.GuideOnStartup; -import com.hfstudio.guidenh.guide.internal.GuideRegistry; import com.hfstudio.guidenh.guide.internal.GuideReloadListener; -import com.hfstudio.guidenh.guide.internal.GuideScreenMemory; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TagAttributeRegistry; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AnchorProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AttributeNameProvider; @@ -45,32 +43,8 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.OreDictProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.PageReferenceProvider; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.TagNameProvider; -import com.hfstudio.guidenh.guide.internal.home.GuideScreenHomeHistory; -import com.hfstudio.guidenh.guide.scene.level.GuidebookFakeWorld; -import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; -import com.hfstudio.guidenh.integration.GuideNhClientIntegrationBootstrap; -import com.hfstudio.guidenh.integration.Mods; -import com.hfstudio.guidenh.integration.ae2.network.Ae2NetworkRegistration; -import com.hfstudio.guidenh.integration.nei.GuideScreenNeiBridge; -import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; -import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; -import com.hfstudio.guidenh.network.GuideNhNetwork; -import com.hfstudio.guidenh.network.GuideNhRegionExportClientHandler; -import com.hfstudio.guidenh.network.GuideNhRegionExportReplyMessage; -import com.hfstudio.structurelibexport.StructureExportBootstrap; - -import cpw.mods.fml.common.event.FMLInitializationEvent; -import cpw.mods.fml.common.event.FMLLoadCompleteEvent; -import cpw.mods.fml.common.event.FMLPostInitializationEvent; -import cpw.mods.fml.common.event.FMLPreInitializationEvent; -import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import cpw.mods.fml.common.network.FMLNetworkEvent; -import com.hfstudio.guidenh.guide.internal.scheduler.MasterScheduler; -import com.hfstudio.guidenh.guide.internal.scheduler.SearchIndexWorkItem; -import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.LytHostWorkItem; -import com.hfstudio.guidenh.guide.internal.scheduler.LytHostPreheatItem; import com.hfstudio.guidenh.guide.internal.host.scripts.BlockImageScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CategoryScript; import com.hfstudio.guidenh.guide.internal.host.scripts.CommandLinkScript; @@ -92,7 +66,29 @@ import com.hfstudio.guidenh.guide.internal.host.scripts.StructureScript; import com.hfstudio.guidenh.guide.internal.host.scripts.SubPagesScript; import com.hfstudio.guidenh.guide.internal.host.scripts.TooltipScript; +import com.hfstudio.guidenh.guide.internal.scheduler.DevWatchWorkItem; +import com.hfstudio.guidenh.guide.internal.scheduler.LytHostPreheatItem; +import com.hfstudio.guidenh.guide.internal.scheduler.MasterScheduler; +import com.hfstudio.guidenh.guide.internal.scheduler.SearchIndexWorkItem; +import com.hfstudio.guidenh.guide.scene.level.GuidebookFakeWorld; +import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; +import com.hfstudio.guidenh.integration.GuideNhClientIntegrationBootstrap; +import com.hfstudio.guidenh.integration.Mods; +import com.hfstudio.guidenh.integration.ae2.network.Ae2NetworkRegistration; +import com.hfstudio.guidenh.integration.nei.GuideScreenNeiBridge; +import com.hfstudio.guidenh.network.GuideNhClientBridgeHandler; +import com.hfstudio.guidenh.network.GuideNhClientBridgeMessage; +import com.hfstudio.guidenh.network.GuideNhNetwork; +import com.hfstudio.guidenh.network.GuideNhRegionExportClientHandler; +import com.hfstudio.guidenh.network.GuideNhRegionExportReplyMessage; +import com.hfstudio.structurelibexport.StructureExportBootstrap; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLLoadCompleteEvent; +import cpw.mods.fml.common.event.FMLPostInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.network.FMLNetworkEvent; import cpw.mods.fml.relauncher.Side; public class ClientProxy extends CommonProxy { @@ -164,9 +160,12 @@ public void init(FMLInitializationEvent event) { CycleRegionWandModeHotkey.init(); MinecraftForge.EVENT_BUS.register(new RegionWandRenderer()); MasterScheduler.init(); - MasterScheduler.getInstance().submit(new LytHostWorkItem(lytHost)); - MasterScheduler.getInstance().submit(new LytHostPreheatItem(lytHost)); - MasterScheduler.getInstance().submit(new SearchIndexWorkItem()); + MasterScheduler.getInstance() + .submit(new LytHostWorkItem(lytHost)); + MasterScheduler.getInstance() + .submit(new LytHostPreheatItem(lytHost)); + MasterScheduler.getInstance() + .submit(new SearchIndexWorkItem()); // Phase 3: LytScript registrations lytHost.registerScript("CommandLink", new CommandLinkScript()); @@ -229,7 +228,8 @@ public void postInit(FMLPostInitializationEvent event) { public void completeInit(FMLLoadCompleteEvent event) { super.completeInit(event); GuideDevelopmentResourcePackWatcher.init(); - MasterScheduler.getInstance().submit(new DevWatchWorkItem()); + MasterScheduler.getInstance() + .submit(new DevWatchWorkItem()); GuideOnStartup.init(); } @@ -238,6 +238,7 @@ public void onClientDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEven GuideNH.LOG.info("Minecraft client disconnected. Stopping GuideNH runtime bridge session state"); runtimeBridge.stop(); GuideME.closeSearch(); - lytHost.getNavigation().clear(); + lytHost.getNavigation() + .clear(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java index 2af920e3..79b7069c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/MdxBlockTagSourceExtractor.java @@ -3,6 +3,7 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.unist.UnistPoint; import com.hfstudio.guidenh.libs.unist.UnistPosition; public class MdxBlockTagSourceExtractor { @@ -24,13 +25,7 @@ private MdxBlockTagSourceExtractor() {} return null; } - int sourceStart = position.start() - .offset(); - if (sourceStart < 0 || sourceStart >= sourceText.length()) { - return null; - } - - int openingTagStart = findOpeningTagStart(sourceText, sourceStart, tagName); + int openingTagStart = findOpeningTagStart(sourceText, position, tagName); if (openingTagStart < 0) { return null; } @@ -50,11 +45,80 @@ private MdxBlockTagSourceExtractor() {} return sourceText.substring(openingTagEnd + 1, closingTagStart); } - private static int findOpeningTagStart(String sourceText, int sourceStart, String tagName) { + private static int findOpeningTagStart(String sourceText, UnistPosition position, String tagName) { + UnistPoint start = position.start(); + if (start == null) { + return -1; + } + + // Preprocessors can shift offsets while still keeping line and column stable. + int lineBasedStart = findOpeningTagStartOnDeclaredLine(sourceText, start, tagName); + if (lineBasedStart >= 0) { + return lineBasedStart; + } + + int sourceStart = start.offset(); + if (sourceStart < 0 || sourceStart >= sourceText.length()) { + return -1; + } if (matchesOpeningTagName(sourceText, sourceStart, tagName)) { return sourceStart; } + return findOpeningTagStartByOffset(sourceText, sourceStart, tagName); + } + + private static int findOpeningTagStartOnDeclaredLine(String sourceText, UnistPoint start, String tagName) { + int lineStart = findLineStartOffset(sourceText, start.line()); + if (lineStart < 0) { + return -1; + } + + int lineEnd = sourceText.indexOf('\n', lineStart); + if (lineEnd < 0) { + lineEnd = sourceText.length(); + } + + int declaredOffset = lineStart + Math.max(0, start.column() - 1); + if (declaredOffset < lineEnd && matchesOpeningTagName(sourceText, declaredOffset, tagName)) { + return declaredOffset; + } + + for (int index = Math.min(declaredOffset, lineEnd - 1); index < lineEnd; index++) { + if (sourceText.charAt(index) == '<' && matchesOpeningTagName(sourceText, index, tagName)) { + return index; + } + } + + for (int searchStart = Math.min(declaredOffset - 1, lineEnd - 1); searchStart >= lineStart;) { + int candidate = sourceText.lastIndexOf('<', searchStart); + if (candidate < lineStart) { + break; + } + if (matchesOpeningTagName(sourceText, candidate, tagName)) { + return candidate; + } + searchStart = candidate - 1; + } + return -1; + } + + private static int findLineStartOffset(String sourceText, int lineNumber) { + if (lineNumber <= 0) { + return -1; + } + + int currentLine = 1; + int index = 0; + while (currentLine < lineNumber && index < sourceText.length()) { + if (sourceText.charAt(index++) == '\n') { + currentLine++; + } + } + return currentLine == lineNumber ? index : -1; + } + + private static int findOpeningTagStartByOffset(String sourceText, int sourceStart, String tagName) { int searchStart = sourceStart; while (searchStart >= 0) { int candidate = sourceText.lastIndexOf('<', searchStart); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 0f7b9198..c3ab6e88 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -24,6 +24,7 @@ import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.document.block.LatexRenderOptions; import com.hfstudio.guidenh.guide.document.block.LatexVerticalAlign; import com.hfstudio.guidenh.guide.document.block.LytBlock; @@ -35,7 +36,6 @@ import com.hfstudio.guidenh.guide.document.block.LytLatexBlock; import com.hfstudio.guidenh.guide.document.block.LytLatexDisplayBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.block.LytThematicBreak; import com.hfstudio.guidenh.guide.document.block.table.LytTable; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; @@ -51,12 +51,11 @@ import com.hfstudio.guidenh.guide.internal.markdown.FootnotePreprocessor; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownActionLink; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownHtmlRuntimeNormalizer; -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLatexShorthand; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLiteralAutolink; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.internal.util.LangUtil; -import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.sound.GuideSoundParsers; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; @@ -69,19 +68,13 @@ import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstBreak; import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; -import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis; -import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode; import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph; import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; -import com.hfstudio.guidenh.libs.mdast.model.MdAstPhrasingContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstPosition; import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; -import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; -import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak; import com.hfstudio.guidenh.libs.mdx.MdxCommentMasker; import com.hfstudio.guidenh.libs.micromark.ParseException; import com.hfstudio.guidenh.libs.unist.UnistNode; @@ -277,8 +270,10 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource * deferring the full Micromark → mdast pipeline to first call of * {@link ParsedGuidePage#getAstRoot()}. * - *

    F3+T reload uses this path so that index/navigation rebuilds — - * which only need frontmatter — complete without paying Micromark cost.

    + *

    + * F3+T reload uses this path so that index/navigation rebuilds — + * which only need frontmatter — complete without paying Micromark cost. + *

    */ public static ParsedGuidePage parseFrontmatterOnly(String sourcePack, String language, ResourceLocation id, String pageContent) { @@ -290,10 +285,10 @@ public static ParsedGuidePage parseFrontmatterOnly(String sourcePack, String lan sourcePack, id, pageContent, - null, // astRoot — triggers lazy parse on first getAstRoot() + null, // astRoot — triggers lazy parse on first getAstRoot() sourceFrontmatter, language, - null, // no parse failure yet + null, // no parse failure yet null, null); } @@ -545,7 +540,7 @@ public void compileInlineFragment(Collection children for (var nestedChild : el.children()) { compileFlowContent(layoutParent, nestedChild); } - } else if (child instanceof MdAstParent nestedParent) { + } else if (child instanceof MdAstParentnestedParent) { for (var nestedChild : nestedParent.children()) { compileFlowContent(layoutParent, nestedChild); } @@ -590,12 +585,11 @@ public void compileBlockContext(List children, LytBlo } else { var compiler = tagCompilers.get(el.name()); if (compiler == null) { - layoutChild = createErrorBlock( - "Unhandled MDX element in block context: " + el.name(), child); + layoutChild = createErrorBlock("Unhandled MDX element in block context: " + el.name(), child); } else { - layoutChild = null; - compiler.compileBlockContext(this, layoutParent, el); - } + layoutChild = null; + compiler.compileBlockContext(this, layoutParent, el); + } } } else if (child instanceof MdxJsxTextElement el) { // Inline element at block level — merge into previous paragraph when possible @@ -629,7 +623,9 @@ public void compileBlockContext(List children, LytBlo layoutChild = null; // handled via element } else { layoutChild = createErrorBlock( - "Unhandled node in block context: " + child.getClass().getSimpleName(), child); + "Unhandled node in block context: " + child.getClass() + .getSimpleName(), + child); } if (layoutChild != null) { @@ -779,15 +775,18 @@ private void compileFlowContent(LytFlowParent layoutParent, MdAstAnyContent cont var compiler = tagCompilers.get(el.name()); if (compiler == null) { layoutChild = createErrorFlowContent( - "Unhandled MDX element in flow context: " + el.name(), content); + "Unhandled MDX element in flow context: " + el.name(), + content); } else { - layoutChild = null; - compiler.compileFlowContext(this, layoutParent, el); + layoutChild = null; + compiler.compileFlowContext(this, layoutParent, el); } } } else { layoutChild = createErrorFlowContent( - "Unhandled node in flow context: " + content.getClass().getSimpleName(), content); + "Unhandled node in flow context: " + content.getClass() + .getSimpleName(), + content); } if (layoutChild != null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java index 66a7a872..8aefabb7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockImageCompiler.java @@ -22,7 +22,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl String id = MdxAttrs.getString(compiler, parent, el, "id", null); String ore = MdxAttrs.getString(compiler, parent, el, "ore", null); - if ((id == null || id.trim().isEmpty()) && (ore == null || ore.trim().isEmpty())) { + if ((id == null || id.trim() + .isEmpty()) && (ore == null + || ore.trim() + .isEmpty())) { parent.appendError(compiler, "Missing id attribute (or ore).", el); return; } @@ -36,7 +39,14 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // Create placeholder block that carries all extracted config to BlockImageScript BlockImagePlaceholder placeholder = new BlockImagePlaceholder( - id, ore, meta, nbt, scale, perspective, width, height); + id, + ore, + meta, + nbt, + scale, + perspective, + width, + height); placeholder.setStyleClass("BlockImage"); placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE); placeholder.appendText("[BlockImage]"); @@ -48,17 +58,22 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl * creation by {@code BlockImageScript}. */ public static class BlockImagePlaceholder extends LytParagraph { - @Nullable public final String id; - @Nullable public final String ore; + + @Nullable + public final String id; + @Nullable + public final String ore; public final int meta; - @Nullable public final String nbt; + @Nullable + public final String nbt; public final float scale; - @Nullable public final String perspective; + @Nullable + public final String perspective; public final int width; public final int height; public BlockImagePlaceholder(@Nullable String id, @Nullable String ore, int meta, @Nullable String nbt, - float scale, @Nullable String perspective, int width, int height) { + float scale, @Nullable String perspective, int width, int height) { this.id = id; this.ore = ore; this.meta = meta; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java index ccb2a59f..8560dbca 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java @@ -3,22 +3,22 @@ import java.util.Collections; import java.util.Set; +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytAlertBox; -import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; import com.hfstudio.guidenh.guide.document.block.LytVBox; -import com.hfstudio.guidenh.guide.color.SymbolicColor; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.BlockquoteDirective; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; import com.hfstudio.guidenh.guide.style.BorderStyle; -import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import org.jetbrains.annotations.Nullable; public class BlockquoteCompiler extends BlockTagCompiler { @@ -33,7 +33,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (directive != null && directive.alertType() != null) { LytAlertBox alertBox = new LytAlertBox(); alertBox.setTitle( - directive.alertType().displayText(), + directive.alertType() + .displayText(), directive.alertType()); alertBox.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); alertBox.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); @@ -69,20 +70,26 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl parent.append(PageCompiler.wrapFloatAwareIfNeeded(blockquote)); } - private void compileDirectiveBody(PageCompiler compiler, BlockquoteDirective directive, - LytBlockContainer parent) { + private void compileDirectiveBody(PageCompiler compiler, BlockquoteDirective directive, LytBlockContainer parent) { // When there's a remainingText override and the first paragraph is still present // at the head of the children list, replace its leading text. // Otherwise — just compile children normally. - if (!directive.children().isEmpty() && directive.firstParagraph() != null - && directive.children().get(0) == directive.firstParagraph() - && directive.remainingText() != null && !directive.remainingText().isEmpty()) { + if (!directive.children() + .isEmpty() && directive.firstParagraph() != null + && directive.children() + .get(0) == directive.firstParagraph() + && directive.remainingText() != null + && !directive.remainingText() + .isEmpty()) { // Clone the first paragraph with the remaining text overriding the leading text - compiler.compileBlockContext( - Collections.singletonList(directive.firstParagraph()), parent); - for (int i = 1; i < directive.children().size(); i++) { + compiler.compileBlockContext(Collections.singletonList(directive.firstParagraph()), parent); + for (int i = 1; i < directive.children() + .size(); i++) { compiler.compileBlockContext( - Collections.singletonList(directive.children().get(i)), parent); + Collections.singletonList( + directive.children() + .get(i)), + parent); } } else { compiler.compileBlockContext(directive.children(), parent); @@ -110,8 +117,7 @@ private void shiftFirstParagraphDown(LytNode box, int pixels) { } @Nullable - private LytFlowContent buildQuoteIcon( - @Nullable QuoteIconSpec icon) { + private LytFlowContent buildQuoteIcon(@Nullable QuoteIconSpec icon) { // The original buildQuoteIcon resolved item stacks from icon specs. // For now return null — icon rendering will be added in a later phase. return null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java index 388be265..ee03cc03 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java @@ -22,11 +22,16 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen var text = new LytFlowText(); // Extract text from child — has one MdAstText child from converter String value = ""; - if (!el.children().isEmpty() && el.children().get(0) instanceof MdAstText t) { + if (!el.children() + .isEmpty() + && el.children() + .get(0) instanceof MdAstText t) { value = t.value; } text.setText(value); - text.modifyStyle(style -> style.italic(true).whiteSpace(WhiteSpaceMode.PRE)); + text.modifyStyle( + style -> style.italic(true) + .whiteSpace(WhiteSpaceMode.PRE)); parent.append(text); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index e996e4cf..46a1ff41 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -11,11 +11,11 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; import com.hfstudio.guidenh.guide.compiler.PageCompiler; -import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.table.LytTable; import com.hfstudio.guidenh.guide.document.block.table.LytTableCell; +import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; @@ -62,7 +62,8 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } catch (MdxAttrs.AttributeException e) { return; } - if (src != null && !src.trim().isEmpty()) { + if (src != null && !src.trim() + .isEmpty()) { try { ResourceLocation csvId = IdUtils.resolveLink(src.trim(), indexer.getPageId()); byte[] data = indexer.loadAsset(csvId); @@ -168,6 +169,7 @@ private static void appendCellContent(LytTableCell cell, String value) { } public static class CsvTablePlaceholder extends LytParagraph { + public final String src; public final boolean header; public final List widths; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java index c221698d..f0d7f46a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DelUWaveMarkCompiler.java @@ -1,9 +1,8 @@ package com.hfstudio.guidenh.guide.compiler.tags; -import java.util.Set; import java.util.LinkedHashSet; +import java.util.Set; -import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java index d49ab224..a6061cdf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/HeadingCompiler.java @@ -12,8 +12,8 @@ public class HeadingCompiler extends BlockTagCompiler { - private static final Set TAG_NAMES = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6"))); + private static final Set TAG_NAMES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList("h1", "h2", "h3", "h4", "h5", "h6"))); @Override public Set getTagNames() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java index 9fff4082..70321019 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemGridCompiler.java @@ -43,6 +43,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) {} public static class ItemGridPlaceholder extends LytParagraph { + public final List itemIds; public ItemGridPlaceholder(List itemIds) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java index 27df56b7..fcbc0eba 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemImageCompiler.java @@ -74,7 +74,15 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen labelFormat = (formatRaw != null && !formatRaw.isEmpty()) ? formatRaw : null; ItemImagePlaceholder placeholder = new ItemImagePlaceholder( - itemId, scale, yOffset, labelYOffset, showTooltip, showIcon, labelPosition, labelFormat, ore); + itemId, + scale, + yOffset, + labelYOffset, + showTooltip, + showIcon, + labelPosition, + labelFormat, + ore); var inline = new LytFlowInlineBlock(); inline.setBlock(placeholder); @@ -97,6 +105,7 @@ public static String resolveLabelPosition(@Nullable String raw) { } public static class ItemImagePlaceholder extends LytParagraph { + public final String itemId; public final float scale; @Nullable @@ -115,8 +124,7 @@ public static class ItemImagePlaceholder extends LytParagraph { public ItemImagePlaceholder(String itemId, float scale, @Nullable Integer yOffset, @Nullable Integer labelYOffset, boolean showTooltip, @Nullable Boolean showIcon, - @Nullable String labelPosition, @Nullable String labelFormat, - @Nullable String ore) { + @Nullable String labelPosition, @Nullable String labelFormat, @Nullable String ore) { this.itemId = itemId; this.scale = scale; this.yOffset = yOffset; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java index 1bef98f8..7fca31f7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListCompiler.java @@ -12,8 +12,7 @@ public class ListCompiler extends BlockTagCompiler { - private static final Set TAG_NAMES = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList("ul", "ol"))); + private static final Set TAG_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("ul", "ol"))); @Override public Set getTagNames() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java index 7dacf87b..a8c77f68 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java @@ -20,7 +20,7 @@ public Set getTagNames() { } @Override - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({ "unchecked", "rawtypes" }) protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { LytListItem listItem; var taskMarker = MarkdownListSemantics.extractTaskMarker((List) el.children()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java index 14e17b1c..6757a7ab 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MdxAttrs.java @@ -4,7 +4,6 @@ import java.util.regex.Pattern; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import org.jetbrains.annotations.Nullable; @@ -14,7 +13,6 @@ import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; -import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.LytErrorSink; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; @@ -69,9 +67,6 @@ public static ResourceLocation getRequiredId(PageCompiler compiler, LytErrorSink } } - - - @Nullable public static GuideItemReferenceResolver.ResolvedBlockReference getRequiredBlockReference(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields el, String attribute) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java index 4cc7c691..502e1cb8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/MermaidCompiler.java @@ -16,6 +16,7 @@ import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytVBox; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeContentExtractor; +import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; @@ -50,10 +51,11 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } src = mermaidId.toString(); } else { - String rawTagBodySource = compiler.getBlockTagChildrenSource(el); - if (rawTagBodySource != null && !rawTagBodySource.trim() - .isEmpty()) { - sourceText = MermaidMindmapNodeContentExtractor.stripExplicitNodeContentBlocks(rawTagBodySource); + // Prefer raw source text so Mermaid DSL syntax (brackets, links, etc.) + // is not consumed by markdown parsing. PR #24. + String rawSource = compiler.getBlockTagChildrenSource(el); + if (rawSource != null) { + sourceText = MermaidMindmapParser.normalize(stripNodeContentBlocks(rawSource)); } else { sourceText = MermaidMindmapNodeContentExtractor.extractDiagramSource(el.children()); } @@ -138,6 +140,7 @@ private LytBlock compileNodeContentBlock(PageCompiler compiler, MdxJsxFlowElemen } public static class MermaidPlaceholder extends LytParagraph { + public final String src; public final String sourceText; public final int width; @@ -155,4 +158,37 @@ public MermaidPlaceholder(String src, String sourceText, int width, int height, setStyle(LytParagraph.PLACEHOLDER_STYLE); } } + + private static String stripNodeContentBlocks(String source) { + StringBuilder result = new StringBuilder(source.length()); + int depth = 0; + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + if (depth == 0 && source.startsWith("<", i)) { + int tagEnd = source.indexOf('>', i); + if (tagEnd > i) { + String tag = source.substring(i, tagEnd + 1); + if (tag.startsWith(" 0) { + if (source.startsWith("", i)) { + depth--; + if (depth == 0) { + i += "".length() - 1; + continue; + } + } else if (source.startsWith(" getTagNames() { @@ -114,7 +112,8 @@ private LytBlock compileCsvCodeBlock(String source, @Nullable String meta) { } private CsvFenceMeta parseCsvFenceMeta(@Nullable String meta) { - if (meta == null || meta.trim().isEmpty()) { + if (meta == null || meta.trim() + .isEmpty()) { return new CsvFenceMeta(true, Collections.emptyList()); } @@ -190,12 +189,9 @@ private record CsvFenceMeta(boolean header, List widthHints) {} private @Nullable LytMermaidMindmap tryCompileMermaidMindmap(String source) { try { String normalized = MermaidMindmapParser.normalize(source); - LytMermaidMindmap block = new LytMermaidMindmap( - MermaidMindmapParser.parse(normalized), normalized); + LytMermaidMindmap block = new LytMermaidMindmap(MermaidMindmapParser.parse(normalized), normalized); FMLLog.getLogger() - .info( - "[GuideNH] [PreCompiler] Compiled fenced Mermaid runtime block ({} chars)", - normalized.length()); + .info("[GuideNH] [PreCompiler] Compiled fenced Mermaid runtime block ({} chars)", normalized.length()); return block; } catch (IllegalArgumentException e) { FMLLog.getLogger() @@ -227,7 +223,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } private static @Nullable Integer parseCodeBlockWidth(@Nullable String meta) { - if (meta == null || meta.trim().isEmpty()) { + if (meta == null || meta.trim() + .isEmpty()) { return null; } Matcher matcher = CODEBLOCK_META_WIDTH.matcher(meta); @@ -236,7 +233,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } String value = matcher.group(3) != null ? matcher.group(3) : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); - if (value == null || value.trim().isEmpty()) { + if (value == null || value.trim() + .isEmpty()) { return null; } try { @@ -247,7 +245,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } private static @Nullable Integer parseCodeBlockHeight(@Nullable String meta) { - if (meta == null || meta.trim().isEmpty()) { + if (meta == null || meta.trim() + .isEmpty()) { return null; } Matcher matcher = CODEBLOCK_META_HEIGHT.matcher(meta); @@ -256,7 +255,8 @@ private static boolean isFunctionGraphFence(@Nullable String fenceLanguage) { } String value = matcher.group(3) != null ? matcher.group(3) : matcher.group(4) != null ? matcher.group(4) : matcher.group(5); - if (value == null || value.trim().isEmpty()) { + if (value == null || value.trim() + .isEmpty()) { return null; } try { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java index 6704a0b2..4fbe9a13 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/StructureViewCompiler.java @@ -132,6 +132,7 @@ private static int findWhitespace(String line, int offset) { } public static class StructureEntry { + public final int x; public final int y; public final int z; @@ -146,6 +147,7 @@ public StructureEntry(int x, int y, int z, String idSpec) { } public static class StructurePlaceholder extends LytParagraph { + public final int width; public final int height; public final List entries; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java index 3d48d704..58da6d5b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/SubPagesCompiler.java @@ -20,20 +20,23 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl var pageIdStr = el.getAttributeString("id", null); if (pageIdStr != null) { try { - pageIdStr = compiler.resolveId(pageIdStr).toString(); + pageIdStr = compiler.resolveId(pageIdStr) + .toString(); } catch (Exception e) { parent.appendError(compiler, "Invalid id: " + pageIdStr, el); return; } } var alphabetical = MdxAttrs.getBoolean(compiler, parent, el, "alphabetical", false); - var currentPageId = compiler.getPageId().toString(); + var currentPageId = compiler.getPageId() + .toString(); SubPagesPlaceholder placeholder = new SubPagesPlaceholder(pageIdStr, alphabetical, currentPageId); parent.append(placeholder); } public static class SubPagesPlaceholder extends LytParagraph { + public final String pageIdStr; public final boolean alphabetical; public final String currentPageId; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java index fadb99f4..4dec42cf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java @@ -38,7 +38,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl .parseWidthHints(extractKramdownExpression(content)); var columns = table.getColumns(); for (int wi = 0; wi < widths.size() && wi < columns.size(); wi++) { - columns.get(wi).setPreferredWidth(widths.get(wi)); + columns.get(wi) + .setPreferredWidth(widths.get(wi)); } } continue; @@ -80,7 +81,8 @@ private static String extractKramdownExpression(String content) { int start = content.indexOf('{'); int end = content.lastIndexOf('}'); if (start >= 0 && end > start) { - return content.substring(start + 1, end).trim(); + return content.substring(start + 1, end) + .trim(); } return ""; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java index 4c123301..87464f73 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/CategoryCompiler.java @@ -7,13 +7,13 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; -import com.hfstudio.guidenh.guide.indices.CategoryIndex; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.GuidebookText; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageListBuilder; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class CategoryCompiler extends BlockTagCompiler { @@ -47,7 +47,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { String categoryName = el.getAttributeString("name", null); - if (categoryName == null || categoryName.trim().isEmpty()) return; + if (categoryName == null || categoryName.trim() + .isEmpty()) return; // Restore Phase 2: index resolved category member titles for full-text search var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); @@ -59,7 +60,8 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink.appendText(el, categoryName.trim()); sink.appendBreak(); for (var entry : entries) { - if (entry.title() != null && !entry.title().isEmpty()) { + if (entry.title() != null && !entry.title() + .isEmpty()) { sink.appendText(el, entry.title()); } sink.appendBreak(); @@ -73,6 +75,7 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } public static class CategoryPlaceholder extends LytParagraph { + public final String name; public final int rows; public final ResourceLocation guideId; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java index c5ea65a4..89bf4f24 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/mediawiki/SpecialCompiler.java @@ -7,13 +7,13 @@ import com.hfstudio.guidenh.guide.compiler.IndexingContext; import com.hfstudio.guidenh.guide.compiler.IndexingSink; -import com.hfstudio.guidenh.guide.indices.CategoryIndex; -import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.GuidebookText; +import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialPageResolver; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class SpecialCompiler extends BlockTagCompiler { @@ -51,7 +51,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl @Override public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink sink) { String specialName = el.getAttributeString("name", null); - if (specialName == null || specialName.trim().isEmpty()) return; + if (specialName == null || specialName.trim() + .isEmpty()) return; // Restore Phase 2: index resolved special page result entries for full-text search var guide = MediaWikiTagCompilerSupport.resolveGuide(indexer); @@ -74,6 +75,7 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } public static class SpecialPlaceholder extends LytParagraph { + public final String name; public final int rows; public final ResourceLocation guideId; @@ -82,8 +84,8 @@ public static class SpecialPlaceholder extends LytParagraph { public final String language; public final String query; - SpecialPlaceholder(String name, int rows, ResourceLocation guideId, String page, String prefix, - String language, String query) { + SpecialPlaceholder(String name, int rows, ResourceLocation guideId, String page, String prefix, String language, + String query) { this.name = name; this.rows = rows; this.guideId = guideId; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java index 779b9fd3..5691f71b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java @@ -36,8 +36,7 @@ public abstract class LytNode implements Styleable { public void removeChild(LytNode node) {} public void replaceChild(LytNode oldChild, LytNode newChild) { - throw new UnsupportedOperationException( - getClass().getSimpleName() + " must override replaceChild"); + throw new UnsupportedOperationException(getClass().getSimpleName() + " must override replaceChild"); } protected void onAttach() {} @@ -176,17 +175,29 @@ public void setSourceNode(@Nullable MdAstNode sourceNode) { } @Nullable - public String getId() { return id; } + public String getId() { + return id; + } - public void setId(@Nullable String id) { this.id = id; } + public void setId(@Nullable String id) { + this.id = id; + } @Nullable - public String getNodeUid() { return nodeUid; } + public String getNodeUid() { + return nodeUid; + } - public void setNodeUid(@Nullable String nodeUid) { this.nodeUid = nodeUid; } + public void setNodeUid(@Nullable String nodeUid) { + this.nodeUid = nodeUid; + } @Nullable - public String getStyleClass() { return styleClass; } + public String getStyleClass() { + return styleClass; + } - public void setStyleClass(@Nullable String styleClass) { this.styleClass = styleClass; } + public void setStyleClass(@Nullable String styleClass) { + this.styleClass = styleClass; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java index 89dff05a..181fe525 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java @@ -86,18 +86,32 @@ public final void visit(LytVisitor visitor) { protected void visitChildren(LytVisitor visitor) {} @Nullable - public String getStyleClass() { return styleClass; } + public String getStyleClass() { + return styleClass; + } - public void setStyleClass(@Nullable String styleClass) { this.styleClass = styleClass; } + public void setStyleClass(@Nullable String styleClass) { + this.styleClass = styleClass; + } @Nullable - public String getNodeUid() { return nodeUid; } + public String getNodeUid() { + return nodeUid; + } - public void setNodeUid(@Nullable String nodeUid) { this.nodeUid = nodeUid; } + public void setNodeUid(@Nullable String nodeUid) { + this.nodeUid = nodeUid; + } - public Object getData(String key) { return data.get(key); } + public Object getData(String key) { + return data.get(key); + } - public void setData(String key, Object value) { data.put(key, value); } + public void setData(String key, Object value) { + data.put(key, value); + } - public Map getData() { return data; } + public Map getData() { + return data; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java index 50ecb11b..c0fd8e42 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java @@ -98,8 +98,7 @@ public static T unwrapPlaceholder(Object node, Class placeholderClass) { if (placeholderClass.isInstance(node)) { return placeholderClass.cast(node); } - if (node instanceof LytFlowInlineBlock wrapper - && placeholderClass.isInstance(wrapper.getBlock())) { + if (node instanceof LytFlowInlineBlock wrapper && placeholderClass.isInstance(wrapper.getBlock())) { return placeholderClass.cast(wrapper.getBlock()); } return null; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index a2a4a144..cc753da1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -25,8 +25,12 @@ import com.hfstudio.guidenh.guide.internal.recipe.RecipeCache; import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; import com.hfstudio.guidenh.guide.internal.util.LangUtil; +import com.hfstudio.guidenh.guide.latex.GuideLatexTextureCache; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiTranslationStats; import com.hfstudio.guidenh.guide.render.GuidePageTexture; +import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCache; +import com.hfstudio.guidenh.integration.structurelib.StructureLibElementTooltipResolver; +import com.hfstudio.guidenh.integration.structurelib.StructureLibRuntimeFacade; import cpw.mods.fml.common.FMLLog; @@ -54,6 +58,14 @@ public static void reloadGuides(IResourceManager resourceManager) { GuidePageTexture.clear(); GuidePageLanguageIndex.clear(); GuideResourceLanguageIndex.clear(); + GuideLatexTextureCache.INSTANCE.clearAll(); + GuideSceneStructureCache.global() + .clear(); + StructureLibRuntimeFacade.CONTROL_ANALYSIS_CACHE.clear(); + StructureLibRuntimeFacade.ANALYSIS_SNAPSHOT_CACHE.clear(); + StructureLibRuntimeFacade.IMPORT_RESULT_CACHE.clear(); + StructureLibElementTooltipResolver.BLOCK_CANDIDATE_CACHE.clear(); + StructureLibElementTooltipResolver.HATCH_CANDIDATE_CACHE.clear(); long stageStartedAt = System.nanoTime(); GuideRegistry.setDataDriven(DataDrivenGuideLoader.load()); @@ -282,7 +294,8 @@ private static ParsedGuidePage tryParsePageCandidate(String sourcePack, String l private static ParsedGuidePage parsePageBytes(String sourcePack, String language, String contentRootFolder, ResourceLocation pageId, ResourceLocation sourceId, byte[] bytes) { try { - return GuideLocalizedPageSourceResolver.parseFrontmatterOnly(sourcePack, language, contentRootFolder, pageId, bytes); + return GuideLocalizedPageSourceResolver + .parseFrontmatterOnly(sourcePack, language, contentRootFolder, pageId, bytes); } catch (Exception ex) { FMLLog.getLogger() .error("[GuideNH] [GuideLightweightReloadService] Error parsing page {} from {}", pageId, sourceId, ex); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 58dbb996..bfd78348 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -46,6 +46,7 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; +import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.config.ModConfig; @@ -102,11 +103,10 @@ import com.hfstudio.guidenh.guide.internal.home.HomePageController; import com.hfstudio.guidenh.guide.internal.home.HomePageDataBuilder; import com.hfstudio.guidenh.guide.internal.home.HomePageLayout; -import com.hfstudio.guidenh.guide.internal.item.RegionWandItem; -import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; -import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.guide.internal.host.LytHost; import com.hfstudio.guidenh.guide.internal.host.NavigationState; +import com.hfstudio.guidenh.guide.internal.item.RegionWandItem; +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockClipboardService; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar; import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar.ContextTarget; @@ -497,15 +497,20 @@ private GuideScreen(GuideScreenRoute route, @Nullable GuideScreenViewState resto } catch (Throwable ignored) { navBar.setPinned(false); } - navBar.restoreState(ClientProxy.getLytHost().getNavigation().recallNavigationState(), bookmarkState); - ClientProxy.getLytHost().setPreheatCompiler(pageId -> { - if (guide == null) return null; - try { - return guide.getPage(new net.minecraft.util.ResourceLocation(pageId)); - } catch (Exception e) { - return null; - } - }); + navBar.restoreState( + ClientProxy.getLytHost() + .getNavigation() + .recallNavigationState(), + bookmarkState); + ClientProxy.getLytHost() + .setPreheatCompiler(pageId -> { + if (guide == null) return null; + try { + return guide.getPage(new net.minecraft.util.ResourceLocation(pageId)); + } catch (Exception e) { + return null; + } + }); } public static void open(ResourceLocation guideId, @Nullable PageAnchor anchor) { @@ -517,7 +522,9 @@ public static void openFromGuideHotkey(ResourceLocation guideId, @Nullable PageA } public static void openFromHomeHotkey() { - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().consumeValidLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost() + .getNavigation() + .consumeValidLastContentState(); open(remembered != null ? remembered : GuideScreenViewState.home(), false); } @@ -550,7 +557,9 @@ private static GuideScreenRoute contentRoute(ResourceLocation guideId, @Nullable return null; } if (anchor == null) { - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().consumeValidLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost() + .getNavigation() + .consumeValidLastContentState(); if (remembered != null && remembered.route() != null) { return remembered.route(); } @@ -618,7 +627,10 @@ public void reloadPage() { document = null; lastLayoutWidth = -1; if (currentAnchor != null) { - ClientProxy.getLytHost().invalidatePage(currentAnchor.pageId().toString()); + ClientProxy.getLytHost() + .invalidatePage( + currentAnchor.pageId() + .toString()); } loadCurrentPage(); updateToolbarButtonState(); @@ -711,12 +723,16 @@ private void finalizePendingViewState() { } private void rememberCurrentContentStateIfEligible() { - ClientProxy.getLytHost().getNavigation().rememberContentState(captureCurrentViewState()); + ClientProxy.getLytHost() + .getNavigation() + .rememberContentState(captureCurrentViewState()); } private void rememberNavigationState() { if (guide == null) return; - ClientProxy.getLytHost().getNavigation().rememberNavBarState(guide.getId(), navBar.captureState()); + ClientProxy.getLytHost() + .getNavigation() + .rememberNavBarState(guide.getId(), navBar.captureState()); } private boolean isNavigationNewPageButtonVisible() { @@ -1064,7 +1080,9 @@ private MutableGuide resolveGuideEditorTargetGuide() { if (guide != null) { return guide; } - GuideScreenViewState remembered = ClientProxy.getLytHost().getNavigation().recallLastContentState(); + GuideScreenViewState remembered = ClientProxy.getLytHost() + .getNavigation() + .recallLastContentState(); if (remembered != null && remembered.route() != null) { ResourceLocation rememberedGuideId = remembered.route() .guideId(); @@ -1385,6 +1403,8 @@ private void rebuildGuideEditorPreview() { .compile(buildGuideEditorPreviewGuide(parsedDraft), guide.getExtensions(), parsedDraft); int previewWidth = getGuideEditorPreviewLayoutWidth(); if (guideEditorPreviewPage != null && guideEditorPreviewPage.document() != null) { + ClientProxy.getLytHost() + .dispatchToSubtree(guideEditorPreviewPage.document()); guideEditorPreviewPage.document() .updateLayout( createLayoutContext(previewWidth, getVisualReferenceContentWidth()), @@ -2536,7 +2556,8 @@ private void completePendingContentPageLoadIfNeeded() { return; } int requestId = pendingPageLoadRequestId; - String pageIdStr = currentAnchor.pageId().toString(); + String pageIdStr = currentAnchor.pageId() + .toString(); LytHost lytHost = ClientProxy.getLytHost(); GuidePage loadedPage; @@ -2596,7 +2617,8 @@ private static void registerRuntimeScenes(GuidePage page) { } } if (found > 0) { - FMLLog.getLogger().info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); + FMLLog.getLogger() + .info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); } } @@ -2760,8 +2782,12 @@ private void clampScroll() { int max = getMaxScroll(); if (scrollY < 0) scrollY = 0; if (scrollY > max) scrollY = max; - ClientProxy.getLytHost().getViewport().updateContent(contentW, contentH); - ClientProxy.getLytHost().getViewport().scrollTo(scrollY); + ClientProxy.getLytHost() + .getViewport() + .updateContent(contentW, contentH); + ClientProxy.getLytHost() + .getViewport() + .scrollTo(scrollY); } @Override @@ -4565,10 +4591,12 @@ private boolean canSearchCurrentView() { private void drawTiledBackground() { drawRect(0, 0, this.width, this.height, BACKGROUND_DIM_COLOR); if (mc == null || mc.getTextureManager() == null) { - FMLLog.getLogger().warn("[GuideNH] drawTiledBackground: mc or textureManager is null, skipping"); + FMLLog.getLogger() + .warn("[GuideNH] drawTiledBackground: mc or textureManager is null, skipping"); return; } - mc.getTextureManager().bindTexture(BG_TEXTURE); + mc.getTextureManager() + .bindTexture(BG_TEXTURE); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT); GL11.glEnable(GL11.GL_BLEND); @@ -4808,7 +4836,9 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { return; } if (result != null && result.bookmarkTogglePageId() != null) { - ClientProxy.getLytHost().getNavigation().toggleBookmark(result.bookmarkTogglePageId()); + ClientProxy.getLytHost() + .getNavigation() + .toggleBookmark(result.bookmarkTogglePageId()); bookmarkState.toggle(result.bookmarkTogglePageId()); mc.getSoundHandler() .playSound(PositionedSoundRecord.func_147674_a(new ResourceLocation("gui.button.press"), 1.0F)); @@ -6754,7 +6784,9 @@ private void recordHomeHistoryIfEligible() { || !guide.pageExists(currentAnchor.pageId())) { return; } - ClientProxy.getLytHost().getNavigation().recordHomeHistory(guide.getId(), currentAnchor.pageId()); + ClientProxy.getLytHost() + .getNavigation() + .recordHomeHistory(guide.getId(), currentAnchor.pageId()); homeHistory.record(guide.getId(), currentAnchor.pageId()); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java index a18a73db..c0135bc5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java @@ -8,7 +8,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -52,8 +51,7 @@ * Encapsulates a Guide, which consists of a collection of Markdown pages and associated content, loaded from a * guide-specific subdirectory of resource packs. */ -public class MutableGuide - implements Guide, MediaWikiListContextProvider, AutoCloseable { +public class MutableGuide implements Guide, MediaWikiListContextProvider, AutoCloseable { private final ResourceLocation id; private final String defaultNamespace; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java index f8cbc444..fc59cd18 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/resolver/MdxSyntaxResolver.java @@ -6,10 +6,10 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxContextResolver; -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxElementType; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.SyntaxUtils; import com.hfstudio.guidenh.guide.internal.editor.autocomplete.TextSyntaxContext; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter; import com.hfstudio.guidenh.libs.mdast.MdastOptions; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java index 8406f6e0..da49d371 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/md/SceneEditorMarkdownCodec.java @@ -15,12 +15,12 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorElementModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorElementType; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneNodeModel; import com.hfstudio.guidenh.guide.internal.editor.model.SceneEditorSceneNodeType; +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.scene.annotation.compiler.BlockAnnotationTemplateElementCompiler; import com.hfstudio.guidenh.libs.mdast.MdAst; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java index e61eafef..945958f3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageSummaryExtractor.java @@ -52,8 +52,10 @@ public String extractHeadingText(@Nullable ParsedGuidePage page) { private static boolean isHeading(MdAstAnyContent block) { if (block instanceof MdxJsxElementFields el) { String name = el.name(); - return name != null && name.length() == 2 && name.charAt(0) == 'h' - && name.charAt(1) >= '1' && name.charAt(1) <= '6'; + return name != null && name.length() == 2 + && name.charAt(0) == 'h' + && name.charAt(1) >= '1' + && name.charAt(1) <= '6'; } return false; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java index afd54ba9..988f7373 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/DeferredTask.java @@ -2,10 +2,19 @@ public interface DeferredTask { - enum Priority { HIGH, LOW } - enum TaskResult { YIELD, DONE } + enum Priority { + HIGH, + LOW + } + + enum TaskResult { + YIELD, + DONE + } Priority priority(); + TaskResult step(long deadlineNs); + boolean isDone(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java index 5d40044e..28c425c8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytEvent.java @@ -20,18 +20,34 @@ public LytEvent(EventType type, Object target, Map data) { this.type = type; this.target = target; this.currentTarget = target; - this.data = data != null - ? Collections.unmodifiableMap(new LinkedHashMap<>(data)) - : Collections.emptyMap(); + this.data = data != null ? Collections.unmodifiableMap(new LinkedHashMap<>(data)) : Collections.emptyMap(); } - public EventType type() { return type; } - public Object target() { return target; } - public Object currentTarget() { return currentTarget; } - public Map data() { return data; } + public EventType type() { + return type; + } + + public Object target() { + return target; + } - public void stopPropagation() { propagationStopped = true; } - public boolean isPropagationStopped() { return propagationStopped; } + public Object currentTarget() { + return currentTarget; + } + + public Map data() { + return data; + } - void setCurrentTarget(Object node) { this.currentTarget = node; } + public void stopPropagation() { + propagationStopped = true; + } + + public boolean isPropagationStopped() { + return propagationStopped; + } + + void setCurrentTarget(Object node) { + this.currentTarget = node; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index 56407a79..cfc96fd1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -22,8 +22,10 @@ public class LytHost { - @Nullable private LytDocument document; - @Nullable private PageCollection currentPageCollection; + @Nullable + private LytDocument document; + @Nullable + private PageCollection currentPageCollection; private final Map scripts = new HashMap<>(); private final Map cachedDocuments = new LinkedHashMap<>(); private final Map pageNodeCounters = new HashMap<>(); @@ -32,9 +34,13 @@ public class LytHost { private static final int MAX_CACHED_PAGES = 32; static class PageCacheEntry { + final GuidePage guidePage; final Map nodeResults = new HashMap<>(); - PageCacheEntry(GuidePage guidePage) { this.guidePage = guidePage; } + + PageCacheEntry(GuidePage guidePage) { + this.guidePage = guidePage; + } } private final ViewportState viewport = new ViewportState(); @@ -46,10 +52,13 @@ static class PageCacheEntry { @FunctionalInterface public interface PreheatCompiler { - @Nullable GuidePage compile(String pageId); + + @Nullable + GuidePage compile(String pageId); } - @Nullable private PreheatCompiler preheatCompiler; + @Nullable + private PreheatCompiler preheatCompiler; public void setPreheatCompiler(@Nullable PreheatCompiler compiler) { this.preheatCompiler = compiler; @@ -57,25 +66,36 @@ public void setPreheatCompiler(@Nullable PreheatCompiler compiler) { // ===== Document ===== - /** Full processing: UID allocation, onAttach, MOUNT dispatch. Resets the node counter so the - * same page always gets the same UIDs across remounts (enabling node-level cache hits). */ + /** + * Full processing: UID allocation, onAttach, MOUNT dispatch. Resets the node counter so the + * same page always gets the same UIDs across remounts (enabling node-level cache hits). + */ public void mountDocument(@Nullable LytDocument newDoc) { if (this.document != null && this.document != newDoc) { - this.document.setLive(false); // onDetach cascade on old doc + this.document.setLive(false); // onDetach cascade on old doc } this.document = newDoc; if (newDoc != null) { - pageNodeCounters.remove(currentPageId); // reset for stable UIDs + pageNodeCounters.remove(currentPageId); // reset for stable UIDs allocateNodeUids(newDoc); - newDoc.setLive(true); // onAttach cascade - dispatchMountEvents(newDoc); // MOUNT events → scripts materialize placeholders + newDoc.setLive(true); // onAttach cascade + dispatchMountEvents(newDoc); // MOUNT events → scripts materialize placeholders viewport.updateContent(newDoc.getAvailableWidth(), newDoc.getContentHeight()); } } - @Nullable public LytDocument getDocument() { return document; } - public ViewportState getViewport() { return viewport; } - public NavigationState getNavigation() { return nav; } + @Nullable + public LytDocument getDocument() { + return document; + } + + public ViewportState getViewport() { + return viewport; + } + + public NavigationState getNavigation() { + return nav; + } public void registerScript(String styleClass, LytScript script) { scripts.put(styleClass, script); @@ -102,7 +122,9 @@ Object getNodeResult(String pageId, String nodeUid) { public void cachePage(String pageId, GuidePage guidePage) { while (cachedDocuments.size() >= MAX_CACHED_PAGES) { - var oldest = cachedDocuments.keySet().iterator().next(); + var oldest = cachedDocuments.keySet() + .iterator() + .next(); cachedDocuments.remove(oldest); pageNodeCounters.remove(oldest); preheatScheduled.remove(oldest); @@ -146,7 +168,9 @@ public void requestPreheatNeighbors(String currentPageId) { if (node == null) return; for (var child : node.children()) { if (child.hasPage()) { - requestPreheat(child.pageId().toString()); + requestPreheat( + child.pageId() + .toString()); } } } @@ -172,16 +196,17 @@ public void preheatStep(long deadlineNs) { } String allocateNodeUid(String pageId, String prefix) { - int seq = pageNodeCounters - .computeIfAbsent(pageId, k -> new AtomicInteger()).incrementAndGet(); + int seq = pageNodeCounters.computeIfAbsent(pageId, k -> new AtomicInteger()) + .incrementAndGet(); return pageId + "::" + prefix + ":" + seq; } private void allocateNodeUids(LytNode node) { if (node.getStyleClass() != null && node.getNodeUid() == null) { - String prefix = node.getStyleClass().toLowerCase(); - int seq = pageNodeCounters - .computeIfAbsent(currentPageId, k -> new AtomicInteger()).incrementAndGet(); + String prefix = node.getStyleClass() + .toLowerCase(); + int seq = pageNodeCounters.computeIfAbsent(currentPageId, k -> new AtomicInteger()) + .incrementAndGet(); node.setNodeUid(currentPageId + "::" + prefix + ":" + seq); } for (var child : node.getChildren()) { @@ -201,9 +226,10 @@ private void allocateFlowNodeUids(LytNode node) { private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { if (fc.getStyleClass() != null && fc.getNodeUid() == null) { - String prefix = fc.getStyleClass().toLowerCase(); - int seq = pageNodeCounters - .computeIfAbsent(currentPageId, k -> new AtomicInteger()).incrementAndGet(); + String prefix = fc.getStyleClass() + .toLowerCase(); + int seq = pageNodeCounters.computeIfAbsent(currentPageId, k -> new AtomicInteger()) + .incrementAndGet(); fc.setNodeUid(currentPageId + "::" + prefix + ":" + seq); } if (fc instanceof LytFlowSpan span) { @@ -217,7 +243,7 @@ private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { * Two-phase MOUNT dispatch: *

    * Phase 1 (sync): walk the entire tree and execute - * every synchronous script immediately. This guarantees that all + * every synchronous script immediately. This guarantees that all * setup and initialization work (e.g. establishing CURRENT_SCENE, * compiling child elements) is finished before any asynchronous * work begins. @@ -227,14 +253,14 @@ private void allocateFlowNodeUidsRecursive(LytFlowContent fc) { * execution on subsequent ticks (see {@link #step}). *

    * Within each phase the original document order (parent before children) - * is preserved. The node-level result cache is consulted: if a node + * is preserved. The node-level result cache is consulted: if a node * already has a cached result from a previous mount, the cached content * is restored directly and the script is skipped in both * phases. */ private void dispatchMountEvents(LytNode node) { dispatchPhase(node, false); // Phase 1: sync scripts only - dispatchPhase(node, true); // Phase 2: queue async scripts only + dispatchPhase(node, true); // Phase 2: queue async scripts only } private void dispatchPhase(LytNode node, boolean asyncPhase) { @@ -286,13 +312,13 @@ private void dispatchPhaseFlowRecursive(LytFlowContent fc, boolean asyncPhase) { /** * Dispatch a single script in the given phase. *

      - *
    • If the node has a cached result from a previous mount, the - * cached content is restored directly and the script is skipped - * entirely (both phases). - *
    • In the sync phase ({@code asyncPhase == false}), only - * non-async scripts are executed synchronously. - *
    • In the async phase ({@code asyncPhase == true}), only - * async scripts are enqueued as {@link MaterializeTask}s. + *
    • If the node has a cached result from a previous mount, the + * cached content is restored directly and the script is skipped + * entirely (both phases). + *
    • In the sync phase ({@code asyncPhase == false}), only + * non-async scripts are executed synchronously. + *
    • In the async phase ({@code asyncPhase == true}), only + * async scripts are enqueued as {@link MaterializeTask}s. *
    */ private void dispatchScriptInPhase(LytScript script, Object node, boolean asyncPhase) { @@ -328,6 +354,7 @@ private static String nodeUidOf(Object node) { } private static class MaterializeTask implements DeferredTask { + private final LytScript script; private final Object node; private final ScriptContextImpl ctx; @@ -342,7 +369,9 @@ private static class MaterializeTask implements DeferredTask { } @Override - public Priority priority() { return Priority.HIGH; } + public Priority priority() { + return Priority.HIGH; + } @Override public TaskResult step(long deadlineNs) { @@ -369,7 +398,9 @@ public TaskResult step(long deadlineNs) { } @Override - public boolean isDone() { return ctx.isComplete(); } + public boolean isDone() { + return ctx.isComplete(); + } } // ===== Sync events ===== @@ -388,12 +419,21 @@ private void processEventsNow() { switch (event.type()) { case CLICK: case DOUBLE_CLICK: - if (event.data().containsKey("x") && event.data().containsKey("y")) { - interactive.mouseClicked(null, - ((Number) event.data().get("x")).intValue(), - ((Number) event.data().get("y")).intValue(), - event.data().containsKey("button") - ? ((Number) event.data().get("button")).intValue() : 0, + if (event.data() + .containsKey("x") + && event.data() + .containsKey("y")) { + interactive.mouseClicked( + null, + ((Number) event.data() + .get("x")).intValue(), + ((Number) event.data() + .get("y")).intValue(), + event.data() + .containsKey("button") + ? ((Number) event.data() + .get("button")).intValue() + : 0, event.type() == EventType.DOUBLE_CLICK); } break; @@ -414,7 +454,7 @@ public void submitTask(DeferredTask task) { } /** Recursively dispatch MOUNT events into a detached subtree. */ - void dispatchToSubtree(LytNode root) { + public void dispatchToSubtree(LytNode root) { allocateNodeUids(root); dispatchMountEvents(root); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java index 3e1c8cba..3f09982c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHostWorkItem.java @@ -13,10 +13,14 @@ public LytHostWorkItem(LytHost host) { } @Override - public Priority priority() { return Priority.HIGH; } + public Priority priority() { + return Priority.HIGH; + } @Override - public boolean shouldRun() { return host.hasWork(); } + public boolean shouldRun() { + return host.hasWork(); + } @Override public WorkResult tick(long deadlineNs) { @@ -30,5 +34,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { return LytHostWorkItem.class.hashCode(); } + public int hashCode() { + return LytHostWorkItem.class.hashCode(); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java index 333d4c9d..618609a8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytScript.java @@ -1,8 +1,11 @@ package com.hfstudio.guidenh.guide.internal.host; public interface LytScript { + ScriptType type(); + String styleClass(); + void onEvent(Object node, LytEvent event, ScriptContext ctx); default boolean isAsync() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java index d02d7e4b..59617715 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/NavigationState.java @@ -22,12 +22,15 @@ public class NavigationState { - @Nullable private ResourceLocation currentGuideId; - @Nullable private PageAnchor currentAnchor; + @Nullable + private ResourceLocation currentGuideId; + @Nullable + private PageAnchor currentAnchor; private final Deque backStack = new ArrayDeque<>(); - @Nullable private GuideScreenViewState lastContentViewState; + @Nullable + private GuideScreenViewState lastContentViewState; private final Map navBarStates = new LinkedHashMap<>(); private final Set bookmarks = new LinkedHashSet<>(); @@ -35,8 +38,10 @@ public class NavigationState { private final List homeHistory = new ArrayList<>(); public static class HomeHistoryEntry { + public final ResourceLocation guideId; public final ResourceLocation pageId; + public HomeHistoryEntry(ResourceLocation guideId, ResourceLocation pageId) { this.guideId = guideId; this.pageId = pageId; @@ -48,38 +53,71 @@ public void setCurrent(ResourceLocation guideId, PageAnchor anchor) { this.currentAnchor = anchor; } - @Nullable public ResourceLocation currentGuideId() { return currentGuideId; } - @Nullable public PageAnchor currentAnchor() { return currentAnchor; } + @Nullable + public ResourceLocation currentGuideId() { + return currentGuideId; + } - public void pushHistory(GuideScreenViewState state) { backStack.push(state); } - @Nullable public GuideScreenViewState popHistory() { return backStack.pollFirst(); } - public Deque backStack() { return backStack; } + @Nullable + public PageAnchor currentAnchor() { + return currentAnchor; + } + + public void pushHistory(GuideScreenViewState state) { + backStack.push(state); + } - public void rememberContentState(@Nullable GuideScreenViewState state) { lastContentViewState = state; } - @Nullable public GuideScreenViewState recallLastContentState() { return lastContentViewState; } + @Nullable + public GuideScreenViewState popHistory() { + return backStack.pollFirst(); + } + + public Deque backStack() { + return backStack; + } + + public void rememberContentState(@Nullable GuideScreenViewState state) { + lastContentViewState = state; + } + + @Nullable + public GuideScreenViewState recallLastContentState() { + return lastContentViewState; + } public void rememberNavBarState(ResourceLocation guideId, GuideNavBarState state) { if (state != null) navBarStates.put(guideId, state); } - @Nullable public GuideNavBarState recallNavBarState(ResourceLocation guideId) { + + @Nullable + public GuideNavBarState recallNavBarState(ResourceLocation guideId) { return navBarStates.get(guideId); } - public boolean isBookmarked(ResourceLocation pageId) { return bookmarks.contains(pageId); } + public boolean isBookmarked(ResourceLocation pageId) { + return bookmarks.contains(pageId); + } + public void toggleBookmark(ResourceLocation pageId) { - if (!bookmarks.remove(pageId)) { bookmarks.add(pageId); } + if (!bookmarks.remove(pageId)) { + bookmarks.add(pageId); + } + } + + public Set bookmarks() { + return bookmarks; } - public Set bookmarks() { return bookmarks; } public void recordHomeHistory(ResourceLocation guideId, ResourceLocation pageId) { homeHistory.add(0, new HomeHistoryEntry(guideId, pageId)); } - public List homeHistory() { return homeHistory; } + + public List homeHistory() { + return homeHistory; + } public GuideNavBarState recallNavigationState() { - GuideNavBarState currentGuideState = currentGuideId != null - ? navBarStates.get(currentGuideId) - : null; + GuideNavBarState currentGuideState = currentGuideId != null ? navBarStates.get(currentGuideId) : null; return currentGuideState != null ? currentGuideState : GuideNavBarState.defaultState(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java index bc9ac844..a216df01 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContext.java @@ -2,19 +2,23 @@ import java.util.Map; -import org.jetbrains.annotations.Nullable; - import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.PageCollection; import com.hfstudio.guidenh.guide.document.block.LytDocument; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.indices.PageIndex; public interface ScriptContext { + Map data(); + void replace(Object newNode); + String allocateId(String prefix); + LytDocument document(); @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 296981b3..f5461302 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -4,15 +4,13 @@ import java.util.List; import java.util.Map; -import org.jetbrains.annotations.Nullable; - import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.Nullable; + import com.hfstudio.guidenh.guide.PageCollection; -import com.hfstudio.guidenh.guide.document.block.LytAlignedBlock; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytDocument; -import com.hfstudio.guidenh.guide.document.block.LytDocumentFloat; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; @@ -22,6 +20,7 @@ import com.hfstudio.guidenh.guide.indices.PageIndex; class ScriptContextImpl implements ScriptContext { + private final Map data = new HashMap<>(); private final Object node; private final LytHost host; @@ -38,7 +37,9 @@ class ScriptContextImpl implements ScriptContext { } @Override - public Map data() { return data; } + public Map data() { + return data; + } @Override @SuppressWarnings("unchecked") @@ -48,7 +49,7 @@ public void replace(Object newNode) { // // When a block-level tag (e.g. , ) appears inside // a paragraph or list item, the PageCompiler wraps it in LytFlowInlineBlock - // so the block can participate in inline flow layout. At MOUNT time the + // so the block can participate in inline flow layout. At MOUNT time the // dispatch passes the wrapper as "this.node", not the inner placeholder. // // The wrapper IS the correct replacement target — swapping its inner block @@ -111,7 +112,9 @@ public String allocateId(String prefix) { } @Override - public LytDocument document() { return document; } + public LytDocument document() { + return document; + } @Override @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java index 1fa30b54..578ca357 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ViewportState.java @@ -21,9 +21,17 @@ public void updateContent(int width, int height) { this.contentHeight = height; } - public int scrollY() { return scrollY; } - public void scrollTo(int y) { this.scrollY = clampScroll(y); } - public void scrollBy(int delta) { scrollTo(scrollY + delta); } + public int scrollY() { + return scrollY; + } + + public void scrollTo(int y) { + this.scrollY = clampScroll(y); + } + + public void scrollBy(int delta) { + scrollTo(scrollY + delta); + } private int clampScroll(int y) { int max = getMaxScrollY(); @@ -44,11 +52,27 @@ public LytRect getRect() { return new LytRect(0, scrollY, viewportWidth, viewportHeight); } - public boolean isLayoutDirty() { return layoutDirty; } - public void setLayoutDirty(boolean dirty) { this.layoutDirty = dirty; } + public boolean isLayoutDirty() { + return layoutDirty; + } + + public void setLayoutDirty(boolean dirty) { + this.layoutDirty = dirty; + } - public int viewportWidth() { return viewportWidth; } - public int viewportHeight() { return viewportHeight; } - public int contentWidth() { return contentWidth; } - public int contentHeight() { return contentHeight; } + public int viewportWidth() { + return viewportWidth; + } + + public int viewportHeight() { + return viewportHeight; + } + + public int contentWidth() { + return contentWidth; + } + + public int contentHeight() { + return contentHeight; + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java index 0315ff46..97541691 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CategoryScript.java @@ -17,10 +17,14 @@ public class CategoryScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Category"; } + public String styleClass() { + return "Category"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -42,8 +46,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { var context = MediaWikiTagCompilerSupport.createListContext(guide, index); var entries = MediaWikiPageListBuilder.buildCategoryMembers(context, ph.name); - var block = MediaWikiTagCompilerSupport.createBlock( - entries, ph.rows, GuidebookText.MediaWikiNoPagesInCategory.text()); + var block = MediaWikiTagCompilerSupport + .createBlock(entries, ph.rows, GuidebookText.MediaWikiNoPagesInCategory.text()); ctx.replace(block); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java index 6e105699..b582b54f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CommandLinkScript.java @@ -2,8 +2,6 @@ import net.minecraft.client.Minecraft; -import cpw.mods.fml.common.FMLLog; - import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -11,6 +9,8 @@ import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import cpw.mods.fml.common.FMLLog; + public class CommandLinkScript implements LytScript { @Override @@ -31,10 +31,12 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (command == null) return; link.setClickCallback(screen -> { if (Minecraft.getMinecraft().thePlayer == null) return; - FMLLog.getLogger().info("[GuideNH] [CommandLink] Sending command: {}", command); + FMLLog.getLogger() + .info("[GuideNH] [CommandLink] Sending command: {}", command); Minecraft.getMinecraft().thePlayer.sendChatMessage(command); if (Boolean.TRUE.equals(close)) { - Minecraft.getMinecraft().displayGuiScreen(null); + Minecraft.getMinecraft() + .displayGuiScreen(null); } }); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java index 311edfdf..2ffe9a74 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java @@ -19,13 +19,19 @@ public class CsvTableScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "CsvTable"; } + public String styleClass() { + return "CsvTable"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java index b36624d6..3bbb7348 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/FloatingImageScript.java @@ -16,13 +16,19 @@ public class FloatingImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "FloatingImage"; } + public String styleClass() { + return "FloatingImage"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java index 31a15dc7..26b0fb43 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ImageScript.java @@ -16,13 +16,19 @@ public class ImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Img"; } + public String styleClass() { + return "Img"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java index b6488c90..a1c1286d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemGridScript.java @@ -1,6 +1,5 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import com.hfstudio.guidenh.guide.compiler.IdUtils; @@ -16,10 +15,14 @@ public class ItemGridScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "ItemGrid"; } + public String styleClass() { + return "ItemGrid"; + } @Override @SuppressWarnings("deprecation") diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index 522beb84..f07b55ac 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -1,13 +1,12 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraftforge.oredict.OreDictionary; +import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler.ItemImagePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytItemImage; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -18,10 +17,14 @@ public class ItemImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "ItemImage"; } + public String styleClass() { + return "ItemImage"; + } @Override @SuppressWarnings("deprecation") @@ -38,7 +41,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (ph.ore != null) { java.util.List oreStacks = OreDictionary.getOres(ph.ore); if (oreStacks != null && !oreStacks.isEmpty()) { - stack = oreStacks.get(0).copy(); + stack = oreStacks.get(0) + .copy(); } } if (stack == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java index 53adfb0a..6ac8f3a8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java @@ -26,9 +26,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String bindId = (String) placeholder.getData("bindId"); if (bindId == null) return; var mapping = KeyBindTagCompiler.findMapping(bindId); - String display = mapping != null - ? KeyBindTagCompiler.describeMapping(mapping) - : "[" + bindId + "]"; + String display = mapping != null ? KeyBindTagCompiler.describeMapping(mapping) : "[" + bindId + "]"; placeholder.setText(display); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index fe30c8d1..41d6da13 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -6,6 +6,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.MermaidCompiler.MermaidPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytMermaidMindmap; +import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -19,13 +20,19 @@ public class MermaidScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Mermaid"; } + public String styleClass() { + return "Mermaid"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -51,22 +58,44 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { sourceText = MermaidMindmapParser.normalize(sourceText); } - if (sourceText == null || sourceText.trim().isEmpty()) { + if (sourceText == null || sourceText.trim() + .isEmpty()) { replaceWithError(ctx, "Source not found or empty"); return; } try { var document = MermaidMindmapParser.parse(sourceText); - LytMermaidMindmap block = new LytMermaidMindmap(document, sourceText, + LytMermaidMindmap block = new LytMermaidMindmap( + document, + sourceText, ph.nodeContentBlocks != null ? ph.nodeContentBlocks : java.util.Collections.emptyMap()); if (ph.width > 0 || ph.height > 0) { block.setPreferredSize(ph.width, ph.height); } + // Dispatch MOUNT events into NodeContent subtrees (Recipe/BlockImage placeholders) + if (ph.nodeContentBlocks != null) { + FMLLog.getLogger() + .info("[MermaidDebug] Dispatching into {} NodeContent blocks", ph.nodeContentBlocks.size()); + for (var entry : ph.nodeContentBlocks.entrySet()) { + var contentBlock = entry.getValue(); + FMLLog.getLogger() + .info( + "[MermaidDebug] NodeContent '{}' block type={} children={}", + entry.getKey(), + contentBlock.getClass() + .getSimpleName(), + contentBlock instanceof LytNode n ? n.getChildren() + .size() : -1); + if (contentBlock instanceof LytNode root) { + ctx.dispatchSubtree(root); + } + } + } ctx.replace(block); } catch (IllegalArgumentException e) { - FMLLog.getLogger().warn( - "[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); + FMLLog.getLogger() + .warn("[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); replaceWithError(ctx, "Failed to parse: " + e.getMessage()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java index dbb2d024..90875e22 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java @@ -26,7 +26,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() == EventType.MOUNT && node instanceof LytFlowText placeholder) { String username; try { - username = Minecraft.getMinecraft().getSession().getUsername(); + username = Minecraft.getMinecraft() + .getSession() + .getUsername(); } catch (Exception e) { username = ""; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java index 2ce24f21..04cb476f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestCardScript.java @@ -11,25 +11,29 @@ import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; -import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; -import com.hfstudio.guidenh.integration.betterquesting.QuestIndex; -import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; -import com.hfstudio.guidenh.integration.betterquesting.QuestState; -import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestCardCompiler.QuestCardPlaceholder; -import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; +import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; +import com.hfstudio.guidenh.integration.betterquesting.QuestIndex; +import com.hfstudio.guidenh.integration.betterquesting.QuestState; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestCardCompiler.QuestCardPlaceholder; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; public class QuestCardScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "QuestCard"; } + public String styleClass() { + return "QuestCard"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -38,8 +42,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { QuestDisplay display; try { - display = BqHelpers.resolveDisplay(ph.questId, Minecraft.getMinecraft().thePlayer, - ph.showTooltip || ph.showDesc); + display = BqHelpers + .resolveDisplay(ph.questId, Minecraft.getMinecraft().thePlayer, ph.showTooltip || ph.showDesc); } catch (Throwable t) { ctx.replace(LytParagraph.error("[QuestCard] BetterQuesting integration not available")); return; @@ -69,7 +73,9 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { title.append(link); } else { var span = new LytFlowSpan(); - span.modifyStyle(style -> style.color(pickPlaceholderColor(state)).italic(true)); + span.modifyStyle( + style -> style.color(pickPlaceholderColor(state)) + .italic(true)); span.appendText(name); title.append(span); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java index 7e630c0d..67660351 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/QuestLinkScript.java @@ -10,25 +10,27 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.flow.LytFlowText; -import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; -import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; -import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; -import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; -import com.hfstudio.guidenh.integration.betterquesting.QuestState; -import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.integration.betterquesting.BqHelpers; +import com.hfstudio.guidenh.integration.betterquesting.QuestDisplay; +import com.hfstudio.guidenh.integration.betterquesting.QuestState; +import com.hfstudio.guidenh.integration.betterquesting.compiler.QuestTagSupport; public class QuestLinkScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "QuestLink"; } + public String styleClass() { + return "QuestLink"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -43,8 +45,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { QuestDisplay display; try { - display = BqHelpers.resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, - Boolean.TRUE.equals(showTooltip)); + display = BqHelpers + .resolveDisplay(questId, Minecraft.getMinecraft().thePlayer, Boolean.TRUE.equals(showTooltip)); } catch (Throwable t) { ctx.replace(LytParagraph.error("[QuestLink] BetterQuesting integration not available")); return; @@ -57,19 +59,18 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } QuestState state = display.getState(); - String text = overrideText != null && !overrideText.isEmpty() - ? overrideText - : pickText(display, questId); + String text = overrideText != null && !overrideText.isEmpty() ? overrideText : pickText(display, questId); LytFlowContent replacement; if (QuestTagSupport.isNavigable(state)) { - replacement = QuestTagSupport.createQuestGuiLink(questId, display, text, - Boolean.TRUE.equals(showTooltip)); + replacement = QuestTagSupport.createQuestGuiLink(questId, display, text, Boolean.TRUE.equals(showTooltip)); } else { SymbolicColor color = state == QuestState.HIDDEN ? SymbolicColor.DARK_GRAY : state == QuestState.MISSING ? SymbolicColor.RED : SymbolicColor.GRAY; LytFlowSpan span = new LytFlowSpan(); - span.modifyStyle(style -> style.color(color).italic(true)); + span.modifyStyle( + style -> style.color(color) + .italic(true)); span.appendText(text); replacement = span; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index b9cd9508..d3bfe961 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -7,6 +7,7 @@ import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; @@ -14,66 +15,70 @@ import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.document.LytErrorSink; -import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.indices.PageIndex; +import com.hfstudio.guidenh.guide.internal.host.EventType; +import com.hfstudio.guidenh.guide.internal.host.LytEvent; +import com.hfstudio.guidenh.guide.internal.host.LytScript; +import com.hfstudio.guidenh.guide.internal.host.ScriptContext; +import com.hfstudio.guidenh.guide.internal.host.ScriptType; import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.scene.CameraSettings; -import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; -import com.hfstudio.guidenh.guide.scene.annotation.compiler.AnnotationTagCompiler; -import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; import com.hfstudio.guidenh.guide.scene.SceneTagCompiler; import com.hfstudio.guidenh.guide.scene.SceneTagCompiler.ScenePlaceholder; +import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; +import com.hfstudio.guidenh.guide.scene.annotation.compiler.AnnotationTagCompiler; +import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.libs.mdast.MdAst; -import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.unist.UnistNode; -import com.hfstudio.guidenh.config.ModConfig; -import com.hfstudio.guidenh.guide.internal.host.EventType; -import com.hfstudio.guidenh.guide.internal.host.LytEvent; -import com.hfstudio.guidenh.guide.internal.host.LytScript; -import com.hfstudio.guidenh.guide.internal.host.ScriptContext; -import com.hfstudio.guidenh.guide.internal.host.ScriptType; - import cpw.mods.fml.common.FMLLog; public class SceneScript implements LytScript { - public SceneScript() { - } + public SceneScript() {} @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Scene"; } + public String styleClass() { + return "Scene"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; if (!(node instanceof ScenePlaceholder ph)) return; - if (ph.childrenSource == null || ph.childrenSource.trim().isEmpty()) { + if (ph.childrenSource == null || ph.childrenSource.trim() + .isEmpty()) { ctx.replace(LytParagraph.error("[Scene] Empty scene: no scene elements")); return; } GuidebookLevel level = new GuidebookLevel(); CameraSettings camera = new CameraSettings(); - if (ph.perspective != null && !ph.perspective.trim().isEmpty()) { - camera.setPerspectivePreset( - PerspectivePreset.fromSerializedName(ph.perspective.trim())); + if (ph.perspective != null && !ph.perspective.trim() + .isEmpty()) { + camera.setPerspectivePreset(PerspectivePreset.fromSerializedName(ph.perspective.trim())); } if (!Float.isNaN(ph.zoom)) camera.setZoom(ph.zoom); if (!Float.isNaN(ph.rotateX)) camera.setRotationX(ph.rotateX); @@ -95,10 +100,12 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { // Parse children source ExceptionCollector errorSink = new ExceptionCollector(); PageCollection pc = ctx.getPageCollection(); - ExtensionCollection extensions = pc instanceof Guide guide - ? guide.getExtensions() : ExtensionCollection.EMPTY; - PageCompiler runtimeCompiler = new PageCompiler(pc != null ? pc : new StubPageCollection(), - extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), + ExtensionCollection extensions = pc instanceof Guide guide ? guide.getExtensions() : ExtensionCollection.EMPTY; + PageCompiler runtimeCompiler = new PageCompiler( + pc != null ? pc : new StubPageCollection(), + extensions, + ph.sourcePack, + new ResourceLocation(ph.pageDomain, "scene"), ph.childrenSource != null ? ph.childrenSource : ""); MdAstRoot ast; try { @@ -108,7 +115,8 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), MdAstToMdxConverter.convert(ast, Collections.emptyMap()); } } catch (Exception e) { - FMLLog.getLogger().warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); + FMLLog.getLogger() + .warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); ctx.replace(LytParagraph.error("[Scene] Failed to parse scene elements")); return; } @@ -146,7 +154,7 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), // can call scene.attachPonderData(), scene.addAnnotation(), etc. var prevScene = AnnotationTagCompiler.CURRENT_SCENE.get(); AnnotationTagCompiler.CURRENT_SCENE.set(scene); - final boolean[] blockStatsExplicitlySet = {false}; + final boolean[] blockStatsExplicitlySet = { false }; try { GuideSceneStructureCompileScope.run(true, () -> { for (UnistNode child : ast.children()) { @@ -195,24 +203,20 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), scene.setBlockStatsButtonEnabled(ModConfig.ui.sceneBlockStatsButtonEnabled); } - // Finalize scene setup: auto-center, ponder baseline, interactive state capture - float[] center = level.getCenter(); + // Determine rotation center; fall back to level center + float[] center; if (!ph.explicitCenter) { + center = level.getCenter(); camera.setRotationCenter(center[0], center[1], center[2]); - } - // Auto-center the scene in the viewport - if (!ph.explicitCenter && Float.isNaN(ph.offsetX) && Float.isNaN(ph.offsetY)) { - camera.setOffsetX(0f); - camera.setOffsetY(0f); - var sc = camera.worldToScreen(center[0], center[1], center[2]); - camera.setOffsetX(-sc.x); - camera.setOffsetY(sc.y); - } else if (!Float.isNaN(ph.offsetX) || !Float.isNaN(ph.offsetY)) { - if (!Float.isNaN(ph.offsetX)) camera.setOffsetX(ph.offsetX); - if (!Float.isNaN(ph.offsetY)) camera.setOffsetY(ph.offsetY); + } else { + center = new float[] { Float.isNaN(ph.centerX) ? 0f : ph.centerX, Float.isNaN(ph.centerY) ? 0f : ph.centerY, + Float.isNaN(ph.centerZ) ? 0f : ph.centerZ }; } - // Auto-zoom: when zoom is not explicitly set, fit scene to viewport at 85% fill + boolean explicitOffX = !Float.isNaN(ph.offsetX); + boolean explicitOffY = !Float.isNaN(ph.offsetY); + + // Auto-zoom: measure at zoom=1/offset=0, then fit to viewport at 85% fill if (Float.isNaN(ph.zoom)) { camera.setZoom(1f); camera.setOffsetX(0f); @@ -230,9 +234,15 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), camera.setZoom(autoZoom); } } + // Restore explicit offsets zeroed for measurement + if (explicitOffX) camera.setOffsetX(ph.offsetX); + if (explicitOffY) camera.setOffsetY(ph.offsetY); } - // Auto-size: when width/height not explicitly set, measure and compute viewport + + // Auto-size: save offsets, measure at offset=0, restore if (!ph.explicitWidth || !ph.explicitHeight) { + float savedOffX = camera.getOffsetX(); + float savedOffY = camera.getOffsetY(); camera.setOffsetX(0f); camera.setOffsetY(0f); if (!level.isEmpty()) { @@ -247,6 +257,18 @@ extensions, ph.sourcePack, new ResourceLocation(ph.pageDomain, "scene"), scene.setSceneSize(width, height); camera.setViewportSize(width, height); } + camera.setOffsetX(savedOffX); + camera.setOffsetY(savedOffY); + } + + // Auto-center: shift projected scene center to viewport origin. + // Applied only when neither rotation center nor screen offsets are author-specified. + if (!ph.explicitCenter && !explicitOffX && !explicitOffY) { + camera.setOffsetX(0f); + camera.setOffsetY(0f); + var sc = camera.worldToScreen(center[0], center[1], center[2]); + camera.setOffsetX(-sc.x); + camera.setOffsetY(sc.y); } scene.initializePonderTimelineBaseline(); @@ -276,23 +298,49 @@ private static void applyBlockStatsConfig(LytGuidebookScene scene, MdxJsxElement } private static class ExceptionCollector implements LytErrorSink { + @Override public void appendError(PageCompiler compiler, String text, UnistNode node) { - FMLLog.getLogger().warn("[GuideNH] [SceneScript] {}", text); + FMLLog.getLogger() + .warn("[GuideNH] [SceneScript] {}", text); } } private static class StubPageCollection implements PageCollection { - @Override public T getIndex(Class c) { return null; } - @Override public Collection getPages() { + + @Override + public T getIndex(Class c) { + return null; + } + + @Override + public Collection getPages() { return Collections.emptyList(); } - @Override public ParsedGuidePage getParsedPage(ResourceLocation id) { return null; } - @Override public GuidePage getPage(ResourceLocation id) { return null; } - @Override public byte[] loadAsset(ResourceLocation id) { return null; } - @Override public NavigationTree getNavigationTree() { + + @Override + public ParsedGuidePage getParsedPage(ResourceLocation id) { + return null; + } + + @Override + public GuidePage getPage(ResourceLocation id) { + return null; + } + + @Override + public byte[] loadAsset(ResourceLocation id) { + return null; + } + + @Override + public NavigationTree getNavigationTree() { return new NavigationTree(); } - @Override public boolean pageExists(ResourceLocation pageId) { return false; } + + @Override + public boolean pageExists(ResourceLocation pageId) { + return false; + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java index ddb91ebc..8a7c939b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SpecialScript.java @@ -6,8 +6,8 @@ import com.hfstudio.guidenh.guide.compiler.tags.mediawiki.SpecialCompiler.SpecialPlaceholder; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; -import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.indices.CategoryIndex; +import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -20,13 +20,19 @@ public class SpecialScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Special"; } + public String styleClass() { + return "Special"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @@ -50,17 +56,14 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } MediaWikiListContext context = MediaWikiTagCompilerSupport.createListContext(guide, categoryIndex); - MediaWikiSpecialPageQuery query = new MediaWikiSpecialPageQuery("", - MediaWikiSpecialPageQuery.PAGE_SIZE); + MediaWikiSpecialPageQuery query = new MediaWikiSpecialPageQuery("", MediaWikiSpecialPageQuery.PAGE_SIZE); if (ph.page != null) query = query.withParameter("page", ph.page); if (ph.prefix != null) query = query.withParameter("prefix", ph.prefix); if (ph.language != null) query = query.withParameter("language", ph.language); if (ph.query != null) query = query.withSearchText(ph.query); - var result = resolver.resolve(context, specialName, - query.withVisibleCount(Integer.MAX_VALUE)); - var block = MediaWikiTagCompilerSupport.createSpecialBlock( - result, ph.rows, context, query, resolver); + var result = resolver.resolve(context, specialName, query.withVisibleCount(Integer.MAX_VALUE)); + var block = MediaWikiTagCompilerSupport.createSpecialBlock(result, ph.rows, context, query, resolver); ctx.replace(block); ctx.markComplete(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java index 49b148df..67c75031 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SubPagesScript.java @@ -25,10 +25,14 @@ public class SubPagesScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "SubPages"; } + public String styleClass() { + return "SubPages"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java index 96dc214f..0d36987e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/TooltipScript.java @@ -13,16 +13,21 @@ public class TooltipScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Tooltip"; } + public String styleClass() { + return "Tooltip"; + } @Override public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (event.type() != EventType.MOUNT) return; if (!(node instanceof LytTooltipSpan span)) return; - var tooltip = span.getTooltip(0, 0).orElse(null); + var tooltip = span.getTooltip(0, 0) + .orElse(null); if (!(tooltip instanceof ContentTooltip ct)) return; LytBlock content = ct.getContent(); if (content instanceof LytNode root) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java index 1a9cf660..04d1d775 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java @@ -25,11 +25,14 @@ private MarkdownListSemantics() {} // Post-conversion:

    element wrapping the task text if (firstChild instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) firstChild).name())) { MdxJsxFlowElement p = (MdxJsxFlowElement) firstChild; - if (p.children().isEmpty()) { + if (p.children() + .isEmpty()) { return null; } - if (p.children().get(0) instanceof MdAstText) { - MdAstText text = (MdAstText) p.children().get(0); + if (p.children() + .get(0) instanceof MdAstText) { + MdAstText text = (MdAstText) p.children() + .get(0); Matcher matcher = TASK_PATTERN.matcher(text.value); if (matcher.matches()) { return new TaskMarker(!" ".equals(matcher.group(1)), matcher.group(2)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java index 63c86804..8ac4aec8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java @@ -32,7 +32,8 @@ private MarkdownRuntimeBlocks() {} } String firstText = firstParagraph.text(); - if (firstText == null || firstText.trim().isEmpty()) { + if (firstText == null || firstText.trim() + .isEmpty()) { return null; } @@ -55,7 +56,8 @@ private MarkdownRuntimeBlocks() {} return null; } - String expression = trimmed.substring(2, directiveEnd).trim(); + String expression = trimmed.substring(2, directiveEnd) + .trim(); String title = null; ColorValue color = null; QuoteIconSpec icon = null; @@ -64,8 +66,11 @@ private MarkdownRuntimeBlocks() {} if (equalsIndex <= 0 || equalsIndex >= token.length() - 1) { continue; } - String key = token.substring(0, equalsIndex).trim(); - String value = stripOptionalQuotes(token.substring(equalsIndex + 1).trim()); + String key = token.substring(0, equalsIndex) + .trim(); + String value = stripOptionalQuotes( + token.substring(equalsIndex + 1) + .trim()); if (value.isEmpty()) { continue; } @@ -102,12 +107,14 @@ private static FirstParagraphText findFirstParagraphText(MdxJsxElementFields blo if (child instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) child).name())) { MdxJsxFlowElement p = (MdxJsxFlowElement) child; String text = getLeadingParagraphText(p); - if (text != null && !text.trim().isEmpty()) { + if (text != null && !text.trim() + .isEmpty()) { return new FirstParagraphText(p, text); } } else if (child instanceof MdAstText) { MdAstText text = (MdAstText) child; - if (!text.value.trim().isEmpty()) { + if (!text.value.trim() + .isEmpty()) { return new FirstParagraphText(null, text.value); } } @@ -120,7 +127,8 @@ private static String getLeadingParagraphText(MdxJsxFlowElement paragraph) { for (Object child : paragraph.children()) { if (child instanceof MdAstText) { MdAstText text = (MdAstText) child; - if (!text.value.trim().isEmpty()) { + if (!text.value.trim() + .isEmpty()) { return text.value; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java index 0209284c..0965b891 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MdAstToMdxConverter.java @@ -7,7 +7,6 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter; -import com.hfstudio.guidenh.libs.micromark.extensions.gfm.Align; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTable; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableCell; import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableRow; @@ -16,8 +15,6 @@ import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstDottedUnderline; import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstUnderline; import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstWavyUnderline; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxExpressionAttribute; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; @@ -26,7 +23,6 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstCode; import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis; -import com.hfstudio.guidenh.libs.mdast.model.MdAstFlowContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstHTML; import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading; import com.hfstudio.guidenh.libs.mdast.model.MdAstImage; @@ -41,9 +37,10 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstParent; import com.hfstudio.guidenh.libs.mdast.model.MdAstPhrasingContent; import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; +import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak; -import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong; +import com.hfstudio.guidenh.libs.micromark.extensions.gfm.Align; public final class MdAstToMdxConverter { @@ -61,7 +58,7 @@ private static void convertParent(MdAstParent parent, Map children = parent.children(); for (Object child : new ArrayList<>(children)) { - if (child instanceof MdAstParent childParent) { + if (child instanceof MdAstParentchildParent) { convertParent(childParent, definitions); } } @@ -84,19 +81,35 @@ private static boolean isPhrasingParent(MdAstParent parent) { return name != null && PHRASING_CONTAINER_NAMES.contains(name); } String type = parent.type(); - return "link".equals(type) - || "strong".equals(type) + return "link".equals(type) || "strong".equals(type) || "emphasis".equals(type) || "delete".equals(type) || "heading".equals(type); } // Containers whose children are inline/phrasing content only - private static final java.util.Set PHRASING_CONTAINER_NAMES = - new java.util.HashSet<>(java.util.Arrays.asList( - "p", "h1", "h2", "h3", "h4", "h5", "h6", - "td", "th", "summary", "a", - "strong", "em", "del", "u", "wavy", "dotted", "mark", "code", "span")); + private static final java.util.Set PHRASING_CONTAINER_NAMES = new java.util.HashSet<>( + java.util.Arrays.asList( + "p", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "td", + "th", + "summary", + "a", + "strong", + "em", + "del", + "u", + "wavy", + "dotted", + "mark", + "code", + "span")); @SuppressWarnings("unchecked") private static List castAnyChildren(List children) { @@ -104,13 +117,12 @@ private static List castAnyChildren(List children) { } // ----------------------------------------------------------------------- - // Phrasing (inline) children conversion — also handles block nodes that - // may appear inside phrasing containers (e.g. MdAstParagraph inside

    ). + // Phrasing (inline) children conversion — also handles block nodes that + // may appear inside phrasing containers (e.g. MdAstParagraph inside ). // ----------------------------------------------------------------------- - @SuppressWarnings({"unchecked", "rawtypes"}) - private static void convertPhrasingChildren(List children, - Map definitions) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void convertPhrasingChildren(List children, Map definitions) { for (int i = 0; i < children.size(); i++) { Object child = children.get(i); Object replacement = null; @@ -197,11 +209,10 @@ else if (child instanceof MdAstParagraph p) { } // ----------------------------------------------------------------------- - // Flow (block) children conversion + // Flow (block) children conversion // ----------------------------------------------------------------------- - private static void convertFlowChildren(List children, - Map definitions) { + private static void convertFlowChildren(List children, Map definitions) { for (int i = 0; i < children.size(); i++) { MdAstAnyContent child = children.get(i); MdxJsxFlowElement replacement = null; @@ -290,7 +301,7 @@ private static void convertFlowChildren(List children, } // ----------------------------------------------------------------------- - // Factory helpers + // Factory helpers // ----------------------------------------------------------------------- /** @@ -299,9 +310,12 @@ private static void convertFlowChildren(List children, * Uses raw-type list access to bypass the generic type check so that phrasing * content (e.g. {@link MdxJsxTextElement}, {@link MdAstText}) can be placed * inside flow elements where they are semantically valid (e.g. text inside - * {@code

    }). + * {@code + * +

    + * }). */ - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({ "unchecked", "rawtypes" }) private static MdxJsxFlowElement createFlow(String name, List children) { MdxJsxFlowElement element = new MdxJsxFlowElement(); element.setName(name); @@ -315,7 +329,7 @@ private static MdxJsxFlowElement createFlow(String name, List children) { MdxJsxTextElement element = new MdxJsxTextElement(); element.setName(name); @@ -328,29 +342,34 @@ private static MdxJsxTextElement createText(String name, List} tag). + * inside the element (e.g. a {@link MdAstText} inside a {@code + * + * + +

    +     * } tag).
          */
    -    @SuppressWarnings({"unchecked", "rawtypes"})
    +    @SuppressWarnings({ "unchecked", "rawtypes" })
         private static void addChildRaw(MdxJsxFlowElement element, MdAstNode node) {
             ((List) element.children()).add(node);
         }
     
         /**
          * Adds an {@link MdAstNode} to a text element's children list via raw-type
    -     * access, bypassing the generic type check.  This is needed when the child
    +     * access, bypassing the generic type check. This is needed when the child
          * is a non-phrasing node that is semantically valid inline (e.g. a
          * {@link MdAstText} inside {@code }).
          */
    -    @SuppressWarnings({"unchecked", "rawtypes"})
    +    @SuppressWarnings({ "unchecked", "rawtypes" })
         private static void addChildRaw(MdxJsxTextElement element, MdAstNode node) {
             ((List) element.children()).add(node);
         }
     
         /**
          * Serializes the GfmTable align list to a comma-separated lowercase string,
    -     * e.g. {@code "left,center,right"}.  Returns {@code null} when the list is
    +     * e.g. {@code "left,center,right"}. Returns {@code null} when the list is
          * null or empty.
          */
         @Nullable
    @@ -379,8 +398,10 @@ private static String serializeAlign(@Nullable List aligns) {
          */
         @Nullable
         private static String extractKramdownMeta(MdAstParagraph p) {
    -        if (p.children().size() != 1) return null;
    -        if (!(p.children().get(0) instanceof MdAstText t)) return null;
    +        if (p.children()
    +            .size() != 1) return null;
    +        if (!(p.children()
    +            .get(0) instanceof MdAstText t)) return null;
             String v = t.value.trim();
             if (v.startsWith("{:") && v.endsWith("}")) return v;
             return null;
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java
    index 1f4291d2..44ff2288 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/DevWatchWorkItem.java
    @@ -3,7 +3,9 @@
     public class DevWatchWorkItem implements WorkItem {
     
         @Override
    -    public Priority priority() { return Priority.LOW; }
    +    public Priority priority() {
    +        return Priority.LOW;
    +    }
     
         @Override
         public boolean shouldRun() {
    @@ -21,5 +23,7 @@ public boolean equals(Object o) {
         }
     
         @Override
    -    public int hashCode() { return DevWatchWorkItem.class.hashCode(); }
    +    public int hashCode() {
    +        return DevWatchWorkItem.class.hashCode();
    +    }
     }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java
    index b8e7ca97..0e47cc4f 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/LytHostPreheatItem.java
    @@ -3,6 +3,7 @@
     import com.hfstudio.guidenh.guide.internal.host.LytHost;
     
     public class LytHostPreheatItem implements WorkItem {
    +
         private final LytHost host;
     
         public LytHostPreheatItem(LytHost host) {
    @@ -10,10 +11,14 @@ public LytHostPreheatItem(LytHost host) {
         }
     
         @Override
    -    public Priority priority() { return Priority.MEDIUM; }
    +    public Priority priority() {
    +        return Priority.MEDIUM;
    +    }
     
         @Override
    -    public boolean shouldRun() { return host.hasPreheatWork(); }
    +    public boolean shouldRun() {
    +        return host.hasPreheatWork();
    +    }
     
         @Override
         public WorkResult tick(long deadlineNs) {
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java
    index 42ea8789..efdce34c 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/MasterScheduler.java
    @@ -3,7 +3,6 @@
     import java.util.ArrayList;
     import java.util.Iterator;
     import java.util.LinkedHashMap;
    -import java.util.List;
     import java.util.Map;
     
     import cpw.mods.fml.common.FMLCommonHandler;
    @@ -22,7 +21,9 @@ public class MasterScheduler {
     
         private static MasterScheduler instance;
     
    -    public static MasterScheduler getInstance() { return instance; }
    +    public static MasterScheduler getInstance() {
    +        return instance;
    +    }
     
         public static void init() {
             instance = new MasterScheduler();
    @@ -62,9 +63,12 @@ public void clear() {
     
         private Map queueFor(Priority p) {
             switch (p) {
    -            case HIGH:   return high;
    -            case MEDIUM: return medium;
    -            default:     return low;
    +            case HIGH:
    +                return high;
    +            case MEDIUM:
    +                return medium;
    +            default:
    +                return low;
             }
         }
     
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java
    index 284d6a4f..09fe424b 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/SearchIndexWorkItem.java
    @@ -6,10 +6,14 @@
     public class SearchIndexWorkItem implements WorkItem {
     
         @Override
    -    public Priority priority() { return Priority.LOW; }
    +    public Priority priority() {
    +        return Priority.LOW;
    +    }
     
         @Override
    -    public boolean shouldRun() { return true; }
    +    public boolean shouldRun() {
    +        return true;
    +    }
     
         @Override
         public WorkResult tick(long deadlineNs) {
    @@ -24,5 +28,7 @@ public boolean equals(Object o) {
         }
     
         @Override
    -    public int hashCode() { return SearchIndexWorkItem.class.hashCode(); }
    +    public int hashCode() {
    +        return SearchIndexWorkItem.class.hashCode();
    +    }
     }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java
    index 92d0eeba..3a0f12da 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/scheduler/WorkItem.java
    @@ -1,7 +1,10 @@
     package com.hfstudio.guidenh.guide.internal.scheduler;
     
     public interface WorkItem {
    +
         Priority priority();
    +
         boolean shouldRun();
    +
         WorkResult tick(long deadlineNs);
     }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
    index 797057d7..a05ed861 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java
    @@ -14,12 +14,12 @@
     import com.hfstudio.guidenh.guide.extensions.ExtensionCollection;
     import com.hfstudio.guidenh.guide.extensions.ExtensionPoint;
     import com.hfstudio.guidenh.guide.indices.PageIndex;
    +import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent;
    +import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstText;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition;
    -import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter;
     import com.hfstudio.guidenh.libs.unist.UnistNode;
     
     import cpw.mods.fml.common.FMLLog;
    @@ -66,9 +66,7 @@ public void indexContent(MdAstAnyContent content, IndexingSink sink) {
                 var compiler = tagCompilers.get(el.name());
                 if (compiler == null) {
                     FMLLog.getLogger()
    -                    .warn(
    -                        "[GuideNH] [PageIndexer] Unhandled MDX element in guide search indexing: {}",
    -                        el.name());
    +                    .warn("[GuideNH] [PageIndexer] Unhandled MDX element in guide search indexing: {}", el.name());
                     // Fallback: index children content
                     indexContent(el.children(), sink);
                 } else {
    @@ -78,7 +76,8 @@ public void indexContent(MdAstAnyContent content, IndexingSink sink) {
                 // Handled via conversion
             } else {
                 FMLLog.getLogger()
    -                .warn("[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}",
    +                .warn(
    +                    "[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}",
                         ((UnistNode) content).type());
             }
         }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java
    index f5911e18..848af3f6 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiListPlanner.java
    @@ -76,7 +76,9 @@ public static List planColumns(List ent
             int maxGroupHeight = 0;
             int totalHeight = 0;
             for (int i = 0; i < groups.size(); i++) {
    -            groupHeights[i] = SECTION_HEADER_HEIGHT + groups.get(i).entries().size() * ROW_HEIGHT;
    +            groupHeights[i] = SECTION_HEADER_HEIGHT + groups.get(i)
    +                .entries()
    +                .size() * ROW_HEIGHT;
                 maxGroupHeight = Math.max(maxGroupHeight, groupHeights[i]);
                 totalHeight += groupHeights[i];
             }
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java
    index de2d2a70..243e6926 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java
    @@ -10,6 +10,7 @@
     import org.jetbrains.annotations.Nullable;
     
     import com.hfstudio.guidenh.config.ModConfig;
    +import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions;
     import com.hfstudio.guidenh.guide.compiler.PageCompiler;
     import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage;
     import com.hfstudio.guidenh.guide.compiler.tags.BlockTagCompiler;
    @@ -17,10 +18,9 @@
     import com.hfstudio.guidenh.guide.document.block.LytBlockContainer;
     import com.hfstudio.guidenh.guide.document.block.LytParagraph;
     import com.hfstudio.guidenh.guide.extensions.ExtensionCollection;
    +import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter;
     import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureFingerprintResolver;
     import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler;
    -import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions;
    -import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter;
     import com.hfstudio.guidenh.libs.mdast.MdAst;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstNode;
    @@ -135,30 +135,47 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl
                     preParsedAst = MdAst.fromMarkdown(childrenSource, GuideMarkdownOptions.runtime());
                     MdAstToMdxConverter.convert(preParsedAst, Collections.emptyMap());
                 } catch (RuntimeException e) {
    -                FMLLog.getLogger().warn("[GuideNH] [SceneTagCompiler] Failed to pre-parse scene children", e);
    +                FMLLog.getLogger()
    +                    .warn("[GuideNH] [SceneTagCompiler] Failed to pre-parse scene children", e);
                 }
             }
     
             // Store element compilers from ExtensionCollection in placeholder
    -        var sceneElementCompilers = compiler.getExtensions().get(SceneElementTagCompiler.EXTENSION_POINT);
    +        var sceneElementCompilers = compiler.getExtensions()
    +            .get(SceneElementTagCompiler.EXTENSION_POINT);
     
             // Create placeholder block that carries all scene config to SceneScript
             String styleClass = "GameScene".equals(el.name()) ? "GameScene" : "Scene";
             ScenePlaceholder placeholder = new ScenePlaceholder(
    -            w, h, explicitWidth, explicitHeight,
    -            zoom, explicitZoom,
    +            w,
    +            h,
    +            explicitWidth,
    +            explicitHeight,
    +            zoom,
    +            explicitZoom,
                 perspective,
    -            rx, ry, rz,
    -            offX, offY, explicitOffX, explicitOffY,
    -            centerX, centerY, centerZ, explicitCenter,
    -            interactive, showBackground,
    -            allowLayerSlider, gridButtonEnabled, showGrid,
    +            rx,
    +            ry,
    +            rz,
    +            offX,
    +            offY,
    +            explicitOffX,
    +            explicitOffY,
    +            centerX,
    +            centerY,
    +            centerZ,
    +            explicitCenter,
    +            interactive,
    +            showBackground,
    +            allowLayerSlider,
    +            gridButtonEnabled,
    +            showGrid,
                 childrenSource,
    -            compiler.getPageId().getResourceDomain(),
    +            compiler.getPageId()
    +                .getResourceDomain(),
                 compiler.getSourcePack(),
                 preParsedAst,
    -            sceneElementCompilers
    -        );
    +            sceneElementCompilers);
             placeholder.setStyleClass(styleClass);
             placeholder.setStyle(LytParagraph.PLACEHOLDER_STYLE);
             placeholder.appendText("[" + styleClass + "]");
    @@ -184,7 +201,8 @@ public static class ScenePlaceholder extends LytParagraph {
             public final boolean explicitHeight;
             public final float zoom;
             public final boolean explicitZoom;
    -        @Nullable public final String perspective;
    +        @Nullable
    +        public final String perspective;
             public final float rotateX;
             public final float rotateY;
             public final float rotateZ;
    @@ -201,30 +219,21 @@ public static class ScenePlaceholder extends LytParagraph {
             public final boolean allowLayerSlider;
             public final boolean gridButtonEnabled;
             public final boolean showGrid;
    -        @Nullable public final String childrenSource;
    +        @Nullable
    +        public final String childrenSource;
             public final String pageDomain;
             public final String sourcePack;
    -        @Nullable public final MdAstRoot childrenAst;
    +        @Nullable
    +        public final MdAstRoot childrenAst;
             @Nullable
             public final List sceneElementCompilers;
     
    -        public ScenePlaceholder(
    -            int width, int height,
    -            boolean explicitWidth, boolean explicitHeight,
    -            float zoom, boolean explicitZoom,
    -            @Nullable String perspective,
    -            float rotateX, float rotateY, float rotateZ,
    -            float offsetX, float offsetY,
    -            boolean explicitOffsetX, boolean explicitOffsetY,
    -            float centerX, float centerY, float centerZ,
    -            boolean explicitCenter,
    -            boolean interactive, boolean showBackground,
    -            boolean allowLayerSlider, boolean gridButtonEnabled,
    -            boolean showGrid,
    -            @Nullable String childrenSource,
    -            String pageDomain,
    -            String sourcePack,
    -            @Nullable MdAstRoot childrenAst,
    +        public ScenePlaceholder(int width, int height, boolean explicitWidth, boolean explicitHeight, float zoom,
    +            boolean explicitZoom, @Nullable String perspective, float rotateX, float rotateY, float rotateZ,
    +            float offsetX, float offsetY, boolean explicitOffsetX, boolean explicitOffsetY, float centerX,
    +            float centerY, float centerZ, boolean explicitCenter, boolean interactive, boolean showBackground,
    +            boolean allowLayerSlider, boolean gridButtonEnabled, boolean showGrid, @Nullable String childrenSource,
    +            String pageDomain, String sourcePack, @Nullable MdAstRoot childrenAst,
                 @Nullable List sceneElementCompilers) {
                 this.width = width;
                 this.height = height;
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java
    index eb484fec..852959f5 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneViewportMetrics.java
    @@ -18,12 +18,29 @@ public SceneViewportMetrics(float minScreenX, float maxScreenX, float minScreenY
             this.maxScreenY = maxScreenY;
         }
     
    -    public float minScreenX() { return minScreenX; }
    -    public float maxScreenX() { return maxScreenX; }
    -    public float minScreenY() { return minScreenY; }
    -    public float maxScreenY() { return maxScreenY; }
    -    public float spanX() { return maxScreenX - minScreenX; }
    -    public float spanY() { return maxScreenY - minScreenY; }
    +    public float minScreenX() {
    +        return minScreenX;
    +    }
    +
    +    public float maxScreenX() {
    +        return maxScreenX;
    +    }
    +
    +    public float minScreenY() {
    +        return minScreenY;
    +    }
    +
    +    public float maxScreenY() {
    +        return maxScreenY;
    +    }
    +
    +    public float spanX() {
    +        return maxScreenX - minScreenX;
    +    }
    +
    +    public float spanY() {
    +        return maxScreenY - minScreenY;
    +    }
     
         /**
          * Projects the 8 corners of the given axis-aligned bounding box through
    diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
    index 3bd9b605..11394a3f 100644
    --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
    +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java
    @@ -22,7 +22,6 @@
     import com.hfstudio.guidenh.guide.document.interaction.TextTooltip;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownActionLink;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownLatexShorthand;
    -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownListSemantics;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.BlockquoteDirective;
     import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec;
    @@ -30,38 +29,14 @@
     import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser;
     import com.hfstudio.guidenh.guide.sound.GuideSoundSpec;
     import com.hfstudio.guidenh.guide.sound.GuideSoundTrigger;
    -import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTable;
    -import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableCell;
    -import com.hfstudio.guidenh.libs.mdast.gfm.model.GfmTableRow;
    -import com.hfstudio.guidenh.libs.mdast.gfmstrikethrough.MdAstDelete;
    -import com.hfstudio.guidenh.libs.mdast.guidemark.MdAstMark;
    -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstDottedUnderline;
    -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstUnderline;
    -import com.hfstudio.guidenh.libs.mdast.guideunderline.MdAstWavyUnderline;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttribute;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxAttributeNode;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement;
     import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxTextElement;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstBlockquote;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstBreak;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstCode;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstEmphasis;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstHeading;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstImage;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstInlineCode;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstLink;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstList;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstListItem;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstNode;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstParagraph;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstParent;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstPhrasingContent;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstStrong;
     import com.hfstudio.guidenh.libs.mdast.model.MdAstText;
    -import com.hfstudio.guidenh.libs.mdast.model.MdAstThematicBreak;
    -import com.hfstudio.guidenh.libs.micromark.extensions.gfm.Align;
     
     public class GuideSiteHtmlCompiler {
     
    @@ -249,11 +224,21 @@ public String compileInlineFragment(List children, Gu
             StringBuilder html = new StringBuilder();
             for (MdAstAnyContent child : children) {
                 if (child instanceof MdxJsxFlowElement && "p".equals(((MdxJsxFlowElement) child).name())) {
    -                html.append(compileChildren(((MdxJsxFlowElement) child).children(),
    -                    templates, defaultNamespace, currentPageId, sceneResolver));
    +                html.append(
    +                    compileChildren(
    +                        ((MdxJsxFlowElement) child).children(),
    +                        templates,
    +                        defaultNamespace,
    +                        currentPageId,
    +                        sceneResolver));
                 } else if (child instanceof MdxJsxFlowElement) {
    -                html.append(compileChildren(((MdxJsxFlowElement) child).children(),
    -                    templates, defaultNamespace, currentPageId, sceneResolver));
    +                html.append(
    +                    compileChildren(
    +                        ((MdxJsxFlowElement) child).children(),
    +                        templates,
    +                        defaultNamespace,
    +                        currentPageId,
    +                        sceneResolver));
                 } else {
                     html.append(compileNode(child, templates, defaultNamespace, currentPageId, sceneResolver));
                 }
    @@ -276,12 +261,20 @@ private String compileNode(MdAstAnyContent node, GuideSiteTemplateRegistry templ
                 return compileText(((MdAstText) node).value(), templates, defaultNamespace, currentPageId);
             }
             if (node instanceof MdxJsxElementFields) {
    -            return compileMdxElement((MdxJsxElementFields) node, templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileMdxElement(
    +                (MdxJsxElementFields) node,
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             if (node instanceof MdAstParent) {
    -            return compileChildren(((MdAstParent) node).children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileChildren(
    +                ((MdAstParent) node).children(),
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             return "";
         }
    @@ -322,32 +315,37 @@ private String compileMdxElement(MdxJsxElementFields el, GuideSiteTemplateRegist
             }
             // Inline elements
             if ("strong".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("em".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver) + "";
    +            return "" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("del".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates, defaultNamespace,
    -                currentPageId, sceneResolver) + "";
    +            return "" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("u".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("wavy".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("dotted".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("mark".equals(el.name())) {
    -            return "" + compileChildren(el.children(), templates,
    -                defaultNamespace, currentPageId, sceneResolver) + "";
    +            return ""
    +                + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver)
    +                + "";
             }
             if ("code".equals(el.name())) {
                 return "" + escapeHtml(extractTextFromElement(el)) + "";
    @@ -366,19 +364,29 @@ private String compileMdxElement(MdxJsxElementFields el, GuideSiteTemplateRegist
             }
             // Custom MDX tags (existing handlers)
             if (el instanceof MdxJsxFlowElement) {
    -            return compileCustomFlowElement((MdxJsxFlowElement) el, templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileCustomFlowElement(
    +                (MdxJsxFlowElement) el,
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             if (el instanceof MdxJsxTextElement) {
    -            return compileCustomTextElement((MdxJsxTextElement) el, templates, defaultNamespace,
    -                currentPageId, sceneResolver);
    +            return compileCustomTextElement(
    +                (MdxJsxTextElement) el,
    +                templates,
    +                defaultNamespace,
    +                currentPageId,
    +                sceneResolver);
             }
             return compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver);
         }
     
         private boolean isHeadingName(@Nullable String name) {
    -        return name != null && name.length() == 2 && name.charAt(0) == 'h'
    -            && name.charAt(1) >= '1' && name.charAt(1) <= '6';
    +        return name != null && name.length() == 2
    +            && name.charAt(0) == 'h'
    +            && name.charAt(1) >= '1'
    +            && name.charAt(1) <= '6';
         }
     
         private String compileCustomFlowElement(MdxJsxFlowElement flowElement, GuideSiteTemplateRegistry templates,
    @@ -454,16 +462,19 @@ private String compileBlockquoteMdx(MdxJsxElementFields el, GuideSiteTemplateReg
             if (directive != null) {
                 return compileQuoteBoxMdx(directive, templates, defaultNamespace, currentPageId, sceneResolver);
             }
    -        return "
    " + compileChildren(el.children(), templates, defaultNamespace, - currentPageId, sceneResolver) + "
    "; + return "
    " + + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) + + "
    "; } private String compileAlertBoxMdx(BlockquoteDirective directive, GuideSiteTemplateRegistry templates, String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { String body = compileChildren(directive.children(), templates, defaultNamespace, currentPageId, sceneResolver); - String typeName = directive.alertType().displayText(); - return "
    " + escapeHtml(typeName) + "" + body + "
    "; + String typeName = directive.alertType() + .displayText(); + return "
    " + escapeHtml(typeName) + "" + body + "
    "; } private String compileQuoteBoxMdx(BlockquoteDirective directive, GuideSiteTemplateRegistry templates, @@ -471,18 +482,23 @@ private String compileQuoteBoxMdx(BlockquoteDirective directive, GuideSiteTempla String body = compileChildren(directive.children(), templates, defaultNamespace, currentPageId, sceneResolver); StringBuilder html = new StringBuilder("
    "); if (directive.title() != null) { - html.append("").append(escapeHtml(directive.title())).append("
    "); + html.append("") + .append(escapeHtml(directive.title())) + .append("
    "); } - html.append(body).append("
    "); + html.append(body) + .append(""); return html.toString(); } - private String compileListMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, - String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + private String compileListMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, String defaultNamespace, + @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { String tag = "ol".equals(el.name()) ? "ol" : "ul"; String startAttr = ""; if ("ol".equals(el.name())) { @@ -491,9 +507,13 @@ private String compileListMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry startAttr = " start=\"" + escapeAttribute(startStr) + "\""; } } - return "<" + tag + startAttr + ">" + return "<" + tag + + startAttr + + ">" + compileChildren(el.children(), templates, defaultNamespace, currentPageId, sceneResolver) - + ""; + + ""; } private String compileListItemMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, @@ -531,7 +551,9 @@ private String compileCodeBlockMdx(MdxJsxElementFields el) { return GuideSiteGraphRenderer.renderFunctionGraph(graph); } catch (RuntimeException ignored) { return "
    " + escapeHtml(codeText) + "
    "; + + "\">" + + escapeHtml(codeText) + + "
    "; } } @@ -543,18 +565,26 @@ private String compileCodeBlockMdx(MdxJsxElementFields el) { if (width != null || height != null) { html.append(" class=\"guide-code-sized\" style=\""); if (width != null) { - html.append("width:").append(width).append("px;max-width:100%;"); + html.append("width:") + .append(width) + .append("px;max-width:100%;"); } if (height != null) { - html.append("height:").append(height).append("px;overflow:auto;"); + html.append("height:") + .append(height) + .append("px;overflow:auto;"); } html.append("\""); } html.append(">").append(escapeHtml(codeText)).append(""); + html.append(">") + .append(escapeHtml(codeText)) + .append(""); return html.toString(); } @@ -575,8 +605,8 @@ private static Integer parseMetaInt(String meta, String key) { return null; } - private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, - String defaultNamespace, @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { + private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry templates, String defaultNamespace, + @Nullable ResourceLocation currentPageId, SceneResolver sceneResolver) { StringBuilder html = new StringBuilder(""); String alignStr = el.getAttributeString("align", ""); String[] aligns = alignStr.isEmpty() ? new String[0] : alignStr.split(","); @@ -596,10 +626,20 @@ private String compileTableMdx(MdxJsxElementFields el, GuideSiteTemplateRegistry align = " style=\"text-align:" + a + "\""; } } - html.append("<").append(tag).append(align).append(">"); - html.append(compileChildren(((MdxJsxFlowElement) cellChild).children(), templates, - defaultNamespace, currentPageId, sceneResolver)); - html.append(""); + html.append("<") + .append(tag) + .append(align) + .append(">"); + html.append( + compileChildren( + ((MdxJsxFlowElement) cellChild).children(), + templates, + defaultNamespace, + currentPageId, + sceneResolver)); + html.append(""); cellIdx++; } } @@ -626,7 +666,9 @@ private String compileAnchorMdx(MdxJsxElementFields el, GuideSiteTemplateRegistr } if (!href.isEmpty()) { return "" + body + ""; + + "\">" + + body + + ""; } return body; } @@ -637,9 +679,14 @@ private String compileImageMdx(MdxJsxElementFields el, @Nullable ResourceLocatio String title = el.getAttributeString("title", ""); String resolvedSrc = GuideSiteHrefResolver.resolveRawHref(currentPageId, src); StringBuilder html = new StringBuilder("\"").append(escapeAttribute(alt)).append("\"");"); return html.toString(); } @@ -662,10 +709,15 @@ private static void collectTextFromChildren(MdxJsxElementFields el, StringBuilde @Nullable private String extractSoleDisplayLatexFromElement(MdxJsxElementFields el) { - if (el.children().size() != 1 || !(el.children().get(0) instanceof MdAstText)) { + if (el.children() + .size() != 1 + || !(el.children() + .get(0) instanceof MdAstText)) { return null; } - return MarkdownLatexShorthand.extractSoleDisplayFormula(((MdAstText) el.children().get(0)).value); + return MarkdownLatexShorthand.extractSoleDisplayFormula( + ((MdAstText) el.children() + .get(0)).value); } private String compileTooltip(MdxJsxElementFields element, GuideSiteTemplateRegistry templates, diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index 922bfc2d..1c850c7d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -9,9 +9,9 @@ import java.util.Locale; import java.util.Map; +import net.minecraft.block.Block; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; -import net.minecraft.block.Block; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; diff --git a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java index 68d0d79f..bf2ac1a2 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/compiler/QuestCardCompiler.java @@ -50,6 +50,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } public static class QuestCardPlaceholder extends LytParagraph { + public final UUID questId; public final boolean showDesc; public final boolean showTooltip; diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java index 81f90269..163b1ac0 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java @@ -21,9 +21,11 @@ public class FactoryTag { private FactoryTag() {} - /** Marker set on a tag token when recovery happens at EOF (where a separate + /** + * Marker set on a tag token when recovery happens at EOF (where a separate * mdxJsxRecovery token cannot be emitted because consume(EOF) must be the - * last event). Read by {@code MdxMdastExtension.exitMdxJsxTag}. */ + * last event). Read by {@code MdxMdastExtension.exitMdxJsxTag}. + */ public static final TokenProperty RECOVERED_AT_EOF = new TokenProperty<>(); public static final Construct lazyLineEnd; From b1c84d3406a2745381f07d74291789090387d435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:02:45 +0800 Subject: [PATCH 087/136] fix: raw-GL blocks not rendering in Mermaid NodeContent under zoom - Add blitGuiSprite override in NodeContentRenderContext to keep sprite UV fixed while scaling display via GL matrix (prevents atlas bleed) - Add usesRawGl() helper detecting blocks that bypass RenderContext - Apply GL matrix transforms (translate + scale) for LaTeX, ItemImage, and NEI recipe blocks that render with raw OpenGL calls --- .../document/block/LytMermaidMindmapCanvas.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 8133913a..b3a82e3f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -26,6 +26,7 @@ import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapLayoutMode; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNode; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeShape; +import com.hfstudio.guidenh.guide.internal.recipe.LytNeiRecipeBox; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.GuiSprite; @@ -698,10 +699,10 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod renderNodeContentBlock(childBlock, nodeContext, nativeContext, contentViewport); } } - } else if (block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock) { - // LaTeX renders with raw GL, bypassing RenderContext coordinate mapping. - // Apply mindmap zoom via GL matrix and use nativeContext so that the - // document-level GL transform chain remains intact. + } else if (usesRawGl(block)) { + // This block renders with raw GL, bypassing RenderContext coordinate + // mapping. Apply mindmap zoom via GL matrix and use nativeContext so + // that the document-level GL transform chain remains intact. GL11.glPushMatrix(); GL11.glTranslatef(contentViewport.x(), contentViewport.y(), 0f); GL11.glScalef(zoom, zoom, 1f); @@ -715,6 +716,12 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod } } + private static boolean usesRawGl(LytBlock block) { + return block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock + || block instanceof LytItemImage + || block instanceof LytNeiRecipeBox; + } + private @Nullable NodeHit pickNodeHit(int documentX, int documentY) { if (layout == null) { return null; From 47387ec4d831705c278cb723504e6361f5206cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:02:45 +0800 Subject: [PATCH 088/136] fix: raw-GL blocks not rendering in Mermaid NodeContent under zoom - Add blitGuiSprite override in NodeContentRenderContext to keep sprite UV fixed while scaling display via GL matrix (prevents atlas bleed) - Add usesRawGl() helper detecting blocks that bypass RenderContext - Apply GL matrix transforms (translate + scale) for LaTeX, ItemImage, and NEI recipe blocks that render with raw OpenGL calls --- .../document/block/LytMermaidMindmapCanvas.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 8133913a..b3a82e3f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -26,6 +26,7 @@ import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapLayoutMode; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNode; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeShape; +import com.hfstudio.guidenh.guide.internal.recipe.LytNeiRecipeBox; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.GuiSprite; @@ -698,10 +699,10 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod renderNodeContentBlock(childBlock, nodeContext, nativeContext, contentViewport); } } - } else if (block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock) { - // LaTeX renders with raw GL, bypassing RenderContext coordinate mapping. - // Apply mindmap zoom via GL matrix and use nativeContext so that the - // document-level GL transform chain remains intact. + } else if (usesRawGl(block)) { + // This block renders with raw GL, bypassing RenderContext coordinate + // mapping. Apply mindmap zoom via GL matrix and use nativeContext so + // that the document-level GL transform chain remains intact. GL11.glPushMatrix(); GL11.glTranslatef(contentViewport.x(), contentViewport.y(), 0f); GL11.glScalef(zoom, zoom, 1f); @@ -715,6 +716,12 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod } } + private static boolean usesRawGl(LytBlock block) { + return block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock + || block instanceof LytItemImage + || block instanceof LytNeiRecipeBox; + } + private @Nullable NodeHit pickNodeHit(int documentX, int documentY) { if (layout == null) { return null; From 9f5b33d6c09e17c133c3836c477d4210a60cfc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:04:56 +0800 Subject: [PATCH 089/136] style: spotless formatting cleanup in scripts and compiler - Remove unused imports, reorder imports, wrap long lines - Split chained method calls across lines for readability --- .../guide/compiler/tags/RecipeCompiler.java | 26 +++-- .../guide/document/block/chart/ChartIcon.java | 4 +- .../host/scripts/BlockImageScript.java | 36 ++++--- .../internal/host/scripts/ItemLinkScript.java | 29 ++++-- .../internal/host/scripts/RecipeScript.java | 99 ++++++++++++------- .../host/scripts/StructureScript.java | 12 ++- 6 files changed, 131 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java index 2b041516..397e2476 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java @@ -4,7 +4,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Set; @@ -13,7 +12,6 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTBase; -import net.minecraft.nbt.NBTTagCompound; import org.jetbrains.annotations.Nullable; @@ -23,11 +21,11 @@ import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytHBox; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.guide.internal.recipe.RecipeLookup; import com.hfstudio.guidenh.integration.api.RecipeEntry; import com.hfstudio.guidenh.integration.api.RecipeSlot; import com.hfstudio.guidenh.integration.nei.NeiRecipeLookup; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class RecipeCompiler extends BlockTagCompiler { @@ -109,9 +107,20 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } // RecipePlaceholder -- recipe resolution deferred to RecipeScript - RecipePlaceholder ph = new RecipePlaceholder(tagName, idStr, ref, fallbackText, - handlerNameFilter, handlerIdFilter, handlerOrder, exactRecipeIndex, - inputExpr, outputExpr, limit, multi, usageQuery); + RecipePlaceholder ph = new RecipePlaceholder( + tagName, + idStr, + ref, + fallbackText, + handlerNameFilter, + handlerIdFilter, + handlerOrder, + exactRecipeIndex, + inputExpr, + outputExpr, + limit, + multi, + usageQuery); ph.setStyleClass(tagName); ph.setStyle(LytParagraph.PLACEHOLDER_STYLE); ph.appendText("[Recipe]"); @@ -274,9 +283,8 @@ public static class RecipePlaceholder extends LytParagraph { public final boolean multi; public final boolean usageQuery; - public RecipePlaceholder(String tagName, String idStr, IdUtils.ParsedItemRef ref, - String fallbackText, String handlerName, String handlerId, - int handlerOrder, int recipeIndex, FilterExpr inputExpr, + public RecipePlaceholder(String tagName, String idStr, IdUtils.ParsedItemRef ref, String fallbackText, + String handlerName, String handlerId, int handlerOrder, int recipeIndex, FilterExpr inputExpr, FilterExpr outputExpr, int limit, boolean multi, boolean usageQuery) { this.tagName = tagName; this.idStr = idStr; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java index e53dc6d3..1b1b6ee4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java @@ -66,7 +66,9 @@ private void resolve() { } if (deferredImageId != null) { byte[] data = GuideResourceAccess.readBytes( - Minecraft.getMinecraft().getResourceManager(), deferredImageId); + Minecraft.getMinecraft() + .getResourceManager(), + deferredImageId); if (data != null) { imageId = deferredImageId; texture = GuidePageTexture.load(imageId, data); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 5a5136a7..f1066ee6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -4,13 +4,11 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.oredict.OreDictionary; import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef; import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler.BlockImagePlaceholder; -import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -22,23 +20,29 @@ import com.hfstudio.guidenh.guide.scene.CameraSettings; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; +import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; import com.hfstudio.guidenh.guide.scene.element.BlockElementCompiler; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; -import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; -import com.hfstudio.guidenh.guide.scene.ponder.PonderNbtPath; + import cpw.mods.fml.common.FMLLog; public class BlockImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "BlockImage"; } + public String styleClass() { + return "BlockImage"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override @SuppressWarnings("deprecation") @@ -64,17 +68,18 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); if (item != null) block = Block.getBlockFromItem(item); if (ref.nbt() != null && tileTag == null) { - tileTag = (NBTTagCompound) ref.nbt().copy(); + tileTag = (NBTTagCompound) ref.nbt() + .copy(); } } if (block == null) { - ctx.replace( - LytParagraph.error("[BlockImage] Block not found: " + (ph.ore != null ? ph.ore : ph.id))); + ctx.replace(LytParagraph.error("[BlockImage] Block not found: " + (ph.ore != null ? ph.ore : ph.id))); return; } - if (ph.nbt != null && !ph.nbt.trim().isEmpty()) { + if (ph.nbt != null && !ph.nbt.trim() + .isEmpty()) { try { NBTTagCompound explicitTag = GuideTextNbtCodec.readTextSafeCompound(ph.nbt.trim()); if (tileTag != null) { @@ -85,12 +90,14 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { tileTag = explicitTag; } } catch (Exception e) { - FMLLog.getLogger().warn("[BlockImageScript] Failed to parse NBT for block image", e); + FMLLog.getLogger() + .warn("[BlockImageScript] Failed to parse NBT for block image", e); } } PerspectivePreset perspective = PerspectivePreset.ISOMETRIC_NORTH_EAST; - if (ph.perspective != null && !ph.perspective.trim().isEmpty()) { + if (ph.perspective != null && !ph.perspective.trim() + .isEmpty()) { perspective = PerspectivePreset.fromSerializedName(ph.perspective.trim()); } @@ -100,8 +107,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag, registryId); if (level.isEmpty()) { - ctx.replace( - LytParagraph.error("[BlockImage] Failed to create block preview")); + ctx.replace(LytParagraph.error("[BlockImage] Failed to create block preview")); return; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 792e6e25..6164679d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -16,21 +16,25 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; +import com.hfstudio.guidenh.guide.indices.ItemIndex; +import com.hfstudio.guidenh.guide.indices.OreIndex; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.indices.ItemIndex; -import com.hfstudio.guidenh.guide.indices.OreIndex; public class ItemLinkScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "ItemLink"; } + public String styleClass() { + return "ItemLink"; + } @Override @SuppressWarnings("deprecation") @@ -74,11 +78,15 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } // Same-page detection - if (anchor != null && currentPage != null && anchor.pageId().toString().equals(currentPage)) { + if (anchor != null && currentPage != null + && anchor.pageId() + .toString() + .equals(currentPage)) { LytTooltipSpan span = new LytTooltipSpan(); span.setStyleClass("ItemLink"); java.util.List linkChildren = new java.util.ArrayList<>(link.getChildren()); - link.getChildren().clear(); + link.getChildren() + .clear(); for (LytFlowContent child : linkChildren) { child.setParent(null); span.append(child); @@ -87,7 +95,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { span.setTooltip(new ItemTooltip(stack)); } // If the span has no children text (self-closing tag), fall back to item display name - if (span.getChildren().isEmpty()) { + if (span.getChildren() + .isEmpty()) { span.appendText(stack.getDisplayName()); } span.modifyStyle(style -> style.italic(true)); @@ -113,7 +122,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { var wrapper = new LytFlowInlineBlock(); wrapper.setBlock(img); if ("left".equals(iconPosition)) { - link.getChildren().add(0, wrapper); + link.getChildren() + .add(0, wrapper); wrapper.setParent(link); } else { link.append(wrapper); @@ -122,7 +132,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } // If the link has no children text (self-closing tag), fall back to item display name - if (link.getChildren().isEmpty()) { + if (link.getChildren() + .isEmpty()) { link.appendText(stack.getDisplayName()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index bcb0c413..4bd0f856 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -12,15 +12,14 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler; -import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.FilterExpr; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.HandlerMetadataReader; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.HandlerRecipeAccess; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.RecipePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytHBox; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.block.recipes.LytStandardRecipeBox; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -39,13 +38,19 @@ public class RecipeScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Recipe"; } + public String styleClass() { + return "Recipe"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override @SuppressWarnings("deprecation") @@ -57,12 +62,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Item item = (Item) Item.itemRegistry.getObject(ph.ref.rawKey()); if (item == null) { - showFallback(ctx, ph,"Recipe item not found: " + ph.idStr); + showFallback(ctx, ph, "Recipe item not found: " + ph.idStr); return; } ItemStack targetStack = new ItemStack(item, 1, ph.ref.concreteMeta()); if (ph.ref.nbt() != null) { - targetStack.stackTagCompound = (NBTTagCompound) ph.ref.nbt().copy(); + targetStack.stackTagCompound = (NBTTagCompound) ph.ref.nbt() + .copy(); } boolean hasHandlerFilter = ph.handlerName != null || ph.handlerId != null || ph.handlerOrder >= 0; @@ -89,26 +95,36 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } HandlerMetadataReader metadataReader = new HandlerMetadataReader() { - @Override public @Nullable String handlerName(Object h) { + + @Override + public @Nullable String handlerName(Object h) { return NeiRecipeLookup.lookupHandlerName(h); } - @Override public @Nullable String handlerId(Object h) { + + @Override + public @Nullable String handlerId(Object h) { return NeiRecipeLookup.lookupHandlerId(h); } - @Override public @Nullable String overlayIdentifier(Object h) { + + @Override + public @Nullable String overlayIdentifier(Object h) { return NeiRecipeLookup.lookupOverlayIdentifier(h); } }; HandlerRecipeAccess recipeAccess = new HandlerRecipeAccess() { - @Override public List readIngredientSlots(Object h, int ri) { + + @Override + public List readIngredientSlots(Object h, int ri) { return NeiRecipeLookup.readIngredientSlots(h, ri); } - @Override public @Nullable NeiRecipeLookup.Slot readResultSlot(Object h, int ri) { + + @Override + public @Nullable NeiRecipeLookup.Slot readResultSlot(Object h, int ri) { return NeiRecipeLookup.readResultSlot(h, ri); } }; - List handlers = RecipeCompiler.filterHandlers(rawHandlers, - ph.handlerName, ph.handlerId, ph.handlerOrder, metadataReader); + List handlers = RecipeCompiler + .filterHandlers(rawHandlers, ph.handlerName, ph.handlerId, ph.handlerOrder, metadataReader); if (!handlers.isEmpty()) { List boxes = new ArrayList<>(); for (int hi = 0; hi < handlers.size() && boxes.size() < limit; hi++) { @@ -119,7 +135,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { int recipeEnd = ph.recipeIndex >= 0 ? Math.min(num, ph.recipeIndex + 1) : num; for (int ri = recipeStart; ri < recipeEnd && boxes.size() < limit; ri++) { if (hasRecipeFilter - && !RecipeCompiler.recipeMatches(handler, ri, ph.inputExpr, ph.outputExpr, recipeAccess)) continue; + && !RecipeCompiler.recipeMatches(handler, ri, ph.inputExpr, ph.outputExpr, recipeAccess)) + continue; boxes.add(new LytNeiRecipeBox(handler, ri, !usageQuery)); } } @@ -128,7 +145,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } if (ph.recipeIndex >= 0) { - showFallback(ctx, ph,"Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); + showFallback(ctx, ph, "Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); return; } } else if (hasHandlerFilter) { @@ -138,37 +155,48 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { handlerPart = " with handler " + (ph.handlerName != null ? ph.handlerName : ph.handlerId); } showFallback(ctx, ph, "No recipe found for " + ph.idStr + handlerPart); - } else if (FMLLog.getLogger().isDebugEnabled()) { - FMLLog.getLogger().debug("Recipe handler filter eliminated all candidates for {}", ph.idStr); - } + } else if (FMLLog.getLogger() + .isDebugEnabled()) { + FMLLog.getLogger() + .debug("Recipe handler filter eliminated all candidates for {}", ph.idStr); + } return; } // Integration recipe entries List recipeEntries = usageQuery ? Collections.emptyList() - : GuideNhIntegrationRegistry.global().findCraftingRecipeEntries(targetStack); + : GuideNhIntegrationRegistry.global() + .findCraftingRecipeEntries(targetStack); if (!recipeEntries.isEmpty()) { List boxes = new ArrayList<>(); int entryStart = ph.recipeIndex >= 0 ? ph.recipeIndex : 0; - int entryEnd = ph.recipeIndex >= 0 - ? Math.min(recipeEntries.size(), ph.recipeIndex + 1) : recipeEntries.size(); + int entryEnd = ph.recipeIndex >= 0 ? Math.min(recipeEntries.size(), ph.recipeIndex + 1) + : recipeEntries.size(); for (int i = entryStart; i < entryEnd && boxes.size() < limit; i++) { var e = recipeEntries.get(i); - if (e.result() == null || e.ingredients().isEmpty()) continue; + if (e.result() == null || e.ingredients() + .isEmpty()) continue; if (hasRecipeFilter && !RecipeCompiler.entryMatches(e, ph.inputExpr, ph.outputExpr)) continue; List flat = new ArrayList<>(9); for (int s = 0; s < 9; s++) flat.add(null); int idx = 0; for (RecipeSlot slot : e.ingredients()) { if (idx >= 9) break; - if (slot.stacks() != null && !slot.stacks().isEmpty()) - flat.set(idx, slot.stacks().get(0)); + if (slot.stacks() != null && !slot.stacks() + .isEmpty()) flat.set( + idx, + slot.stacks() + .get(0)); idx++; } - ItemStack resultStack = e.result().stacks() != null && !e.result().stacks().isEmpty() - ? e.result().stacks().get(0) : null; - if (resultStack != null) - boxes.add(LytStandardRecipeBox.shapeless(flat, resultStack)); + ItemStack resultStack = e.result() + .stacks() != null + && !e.result() + .stacks() + .isEmpty() ? e.result() + .stacks() + .get(0) : null; + if (resultStack != null) boxes.add(LytStandardRecipeBox.shapeless(flat, resultStack)); } if (!boxes.isEmpty()) { ctx.replace(buildResult(boxes)); @@ -180,18 +208,16 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { List entries = usageQuery ? Collections.emptyList() : RecipeLookup.findByOutput(item); if (entries.isEmpty()) { - showFallback(ctx, ph,"No recipe found for " + ph.idStr); + showFallback(ctx, ph, "No recipe found for " + ph.idStr); return; } List boxes = new ArrayList<>(); int vanillaStart = ph.recipeIndex >= 0 ? ph.recipeIndex : 0; - int vanillaEnd = ph.recipeIndex >= 0 - ? Math.min(entries.size(), ph.recipeIndex + 1) : entries.size(); + int vanillaEnd = ph.recipeIndex >= 0 ? Math.min(entries.size(), ph.recipeIndex + 1) : entries.size(); for (int i = vanillaStart; i < vanillaEnd && boxes.size() < limit; i++) { var e = entries.get(i); - if (hasRecipeFilter - && !RecipeCompiler.vanillaEntryMatches(e, ph.inputExpr, ph.outputExpr)) continue; + if (hasRecipeFilter && !RecipeCompiler.vanillaEntryMatches(e, ph.inputExpr, ph.outputExpr)) continue; var box = e.shapeless ? LytStandardRecipeBox.shapeless(RecipeLookup.asList(e), e.result) : LytStandardRecipeBox.shaped3x3(RecipeLookup.asList(e), e.result); boxes.add(box); @@ -200,7 +226,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ctx.replace(buildResult(boxes)); return; } - showFallback(ctx, ph,"No recipe found for " + ph.idStr); + showFallback(ctx, ph, "No recipe found for " + ph.idStr); } @SuppressWarnings("unchecked") @@ -217,8 +243,7 @@ private static LytBlock buildResultTyped(List boxes) { } private void showFallback(ScriptContext ctx, RecipePlaceholder ph, String autoMessage) { - String text = (ph.fallbackText != null && !ph.fallbackText.isEmpty()) - ? ph.fallbackText : autoMessage; + String text = (ph.fallbackText != null && !ph.fallbackText.isEmpty()) ? ph.fallbackText : autoMessage; ctx.replace(LytParagraph.error(text)); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index e64df640..dcb4d860 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -17,10 +17,14 @@ public class StructureScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Structure"; } + public String styleClass() { + return "Structure"; + } @Override @SuppressWarnings("deprecation") @@ -47,8 +51,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @SuppressWarnings("deprecation") private static ItemStack resolveEntry(String idSpec) { if (idSpec == null || idSpec.isEmpty()) return null; - com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(idSpec, "minecraft"); + com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = com.hfstudio.guidenh.guide.compiler.IdUtils + .parseItemRef(idSpec, "minecraft"); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); if (item != null) { From 49c38550dcb2270865e1fb3f7cba28c4bc3a3dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:04:56 +0800 Subject: [PATCH 090/136] style: spotless formatting cleanup in scripts and compiler - Remove unused imports, reorder imports, wrap long lines - Split chained method calls across lines for readability --- .../guide/compiler/tags/RecipeCompiler.java | 26 +++-- .../guide/document/block/chart/ChartIcon.java | 4 +- .../host/scripts/BlockImageScript.java | 36 ++++--- .../internal/host/scripts/ItemLinkScript.java | 29 ++++-- .../internal/host/scripts/RecipeScript.java | 99 ++++++++++++------- .../host/scripts/StructureScript.java | 12 ++- 6 files changed, 131 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java index 2b041516..397e2476 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/RecipeCompiler.java @@ -4,7 +4,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Set; @@ -13,7 +12,6 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTBase; -import net.minecraft.nbt.NBTTagCompound; import org.jetbrains.annotations.Nullable; @@ -23,11 +21,11 @@ import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytHBox; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.guide.internal.recipe.RecipeLookup; import com.hfstudio.guidenh.integration.api.RecipeEntry; import com.hfstudio.guidenh.integration.api.RecipeSlot; import com.hfstudio.guidenh.integration.nei.NeiRecipeLookup; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class RecipeCompiler extends BlockTagCompiler { @@ -109,9 +107,20 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } // RecipePlaceholder -- recipe resolution deferred to RecipeScript - RecipePlaceholder ph = new RecipePlaceholder(tagName, idStr, ref, fallbackText, - handlerNameFilter, handlerIdFilter, handlerOrder, exactRecipeIndex, - inputExpr, outputExpr, limit, multi, usageQuery); + RecipePlaceholder ph = new RecipePlaceholder( + tagName, + idStr, + ref, + fallbackText, + handlerNameFilter, + handlerIdFilter, + handlerOrder, + exactRecipeIndex, + inputExpr, + outputExpr, + limit, + multi, + usageQuery); ph.setStyleClass(tagName); ph.setStyle(LytParagraph.PLACEHOLDER_STYLE); ph.appendText("[Recipe]"); @@ -274,9 +283,8 @@ public static class RecipePlaceholder extends LytParagraph { public final boolean multi; public final boolean usageQuery; - public RecipePlaceholder(String tagName, String idStr, IdUtils.ParsedItemRef ref, - String fallbackText, String handlerName, String handlerId, - int handlerOrder, int recipeIndex, FilterExpr inputExpr, + public RecipePlaceholder(String tagName, String idStr, IdUtils.ParsedItemRef ref, String fallbackText, + String handlerName, String handlerId, int handlerOrder, int recipeIndex, FilterExpr inputExpr, FilterExpr outputExpr, int limit, boolean multi, boolean usageQuery) { this.tagName = tagName; this.idStr = idStr; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java index e53dc6d3..1b1b6ee4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/ChartIcon.java @@ -66,7 +66,9 @@ private void resolve() { } if (deferredImageId != null) { byte[] data = GuideResourceAccess.readBytes( - Minecraft.getMinecraft().getResourceManager(), deferredImageId); + Minecraft.getMinecraft() + .getResourceManager(), + deferredImageId); if (data != null) { imageId = deferredImageId; texture = GuidePageTexture.load(imageId, data); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java index 5a5136a7..f1066ee6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/BlockImageScript.java @@ -4,13 +4,11 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.oredict.OreDictionary; import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef; import com.hfstudio.guidenh.guide.compiler.tags.BlockImageCompiler.BlockImagePlaceholder; -import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; @@ -22,23 +20,29 @@ import com.hfstudio.guidenh.guide.scene.CameraSettings; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.PerspectivePreset; +import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; import com.hfstudio.guidenh.guide.scene.element.BlockElementCompiler; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.level.GuidebookPreviewBlockPlacer; -import com.hfstudio.guidenh.guide.scene.SceneViewportMetrics; -import com.hfstudio.guidenh.guide.scene.ponder.PonderNbtPath; + import cpw.mods.fml.common.FMLLog; public class BlockImageScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "BlockImage"; } + public String styleClass() { + return "BlockImage"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override @SuppressWarnings("deprecation") @@ -64,17 +68,18 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); if (item != null) block = Block.getBlockFromItem(item); if (ref.nbt() != null && tileTag == null) { - tileTag = (NBTTagCompound) ref.nbt().copy(); + tileTag = (NBTTagCompound) ref.nbt() + .copy(); } } if (block == null) { - ctx.replace( - LytParagraph.error("[BlockImage] Block not found: " + (ph.ore != null ? ph.ore : ph.id))); + ctx.replace(LytParagraph.error("[BlockImage] Block not found: " + (ph.ore != null ? ph.ore : ph.id))); return; } - if (ph.nbt != null && !ph.nbt.trim().isEmpty()) { + if (ph.nbt != null && !ph.nbt.trim() + .isEmpty()) { try { NBTTagCompound explicitTag = GuideTextNbtCodec.readTextSafeCompound(ph.nbt.trim()); if (tileTag != null) { @@ -85,12 +90,14 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { tileTag = explicitTag; } } catch (Exception e) { - FMLLog.getLogger().warn("[BlockImageScript] Failed to parse NBT for block image", e); + FMLLog.getLogger() + .warn("[BlockImageScript] Failed to parse NBT for block image", e); } } PerspectivePreset perspective = PerspectivePreset.ISOMETRIC_NORTH_EAST; - if (ph.perspective != null && !ph.perspective.trim().isEmpty()) { + if (ph.perspective != null && !ph.perspective.trim() + .isEmpty()) { perspective = PerspectivePreset.fromSerializedName(ph.perspective.trim()); } @@ -100,8 +107,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { GuidebookPreviewBlockPlacer.place(level, 0, 0, 0, block, defaultMeta, tileTag, registryId); if (level.isEmpty()) { - ctx.replace( - LytParagraph.error("[BlockImage] Failed to create block preview")); + ctx.replace(LytParagraph.error("[BlockImage] Failed to create block preview")); return; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 792e6e25..6164679d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -16,21 +16,25 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; +import com.hfstudio.guidenh.guide.indices.ItemIndex; +import com.hfstudio.guidenh.guide.indices.OreIndex; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; -import com.hfstudio.guidenh.guide.indices.ItemIndex; -import com.hfstudio.guidenh.guide.indices.OreIndex; public class ItemLinkScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "ItemLink"; } + public String styleClass() { + return "ItemLink"; + } @Override @SuppressWarnings("deprecation") @@ -74,11 +78,15 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } // Same-page detection - if (anchor != null && currentPage != null && anchor.pageId().toString().equals(currentPage)) { + if (anchor != null && currentPage != null + && anchor.pageId() + .toString() + .equals(currentPage)) { LytTooltipSpan span = new LytTooltipSpan(); span.setStyleClass("ItemLink"); java.util.List linkChildren = new java.util.ArrayList<>(link.getChildren()); - link.getChildren().clear(); + link.getChildren() + .clear(); for (LytFlowContent child : linkChildren) { child.setParent(null); span.append(child); @@ -87,7 +95,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { span.setTooltip(new ItemTooltip(stack)); } // If the span has no children text (self-closing tag), fall back to item display name - if (span.getChildren().isEmpty()) { + if (span.getChildren() + .isEmpty()) { span.appendText(stack.getDisplayName()); } span.modifyStyle(style -> style.italic(true)); @@ -113,7 +122,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { var wrapper = new LytFlowInlineBlock(); wrapper.setBlock(img); if ("left".equals(iconPosition)) { - link.getChildren().add(0, wrapper); + link.getChildren() + .add(0, wrapper); wrapper.setParent(link); } else { link.append(wrapper); @@ -122,7 +132,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } // If the link has no children text (self-closing tag), fall back to item display name - if (link.getChildren().isEmpty()) { + if (link.getChildren() + .isEmpty()) { link.appendText(stack.getDisplayName()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index bcb0c413..4bd0f856 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -12,15 +12,14 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler; -import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.FilterExpr; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.HandlerMetadataReader; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.HandlerRecipeAccess; import com.hfstudio.guidenh.guide.compiler.tags.RecipeCompiler.RecipePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytHBox; import com.hfstudio.guidenh.guide.document.block.LytParagraph; -import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.block.recipes.LytStandardRecipeBox; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; @@ -39,13 +38,19 @@ public class RecipeScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Recipe"; } + public String styleClass() { + return "Recipe"; + } @Override - public boolean isAsync() { return true; } + public boolean isAsync() { + return true; + } @Override @SuppressWarnings("deprecation") @@ -57,12 +62,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { Item item = (Item) Item.itemRegistry.getObject(ph.ref.rawKey()); if (item == null) { - showFallback(ctx, ph,"Recipe item not found: " + ph.idStr); + showFallback(ctx, ph, "Recipe item not found: " + ph.idStr); return; } ItemStack targetStack = new ItemStack(item, 1, ph.ref.concreteMeta()); if (ph.ref.nbt() != null) { - targetStack.stackTagCompound = (NBTTagCompound) ph.ref.nbt().copy(); + targetStack.stackTagCompound = (NBTTagCompound) ph.ref.nbt() + .copy(); } boolean hasHandlerFilter = ph.handlerName != null || ph.handlerId != null || ph.handlerOrder >= 0; @@ -89,26 +95,36 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } } HandlerMetadataReader metadataReader = new HandlerMetadataReader() { - @Override public @Nullable String handlerName(Object h) { + + @Override + public @Nullable String handlerName(Object h) { return NeiRecipeLookup.lookupHandlerName(h); } - @Override public @Nullable String handlerId(Object h) { + + @Override + public @Nullable String handlerId(Object h) { return NeiRecipeLookup.lookupHandlerId(h); } - @Override public @Nullable String overlayIdentifier(Object h) { + + @Override + public @Nullable String overlayIdentifier(Object h) { return NeiRecipeLookup.lookupOverlayIdentifier(h); } }; HandlerRecipeAccess recipeAccess = new HandlerRecipeAccess() { - @Override public List readIngredientSlots(Object h, int ri) { + + @Override + public List readIngredientSlots(Object h, int ri) { return NeiRecipeLookup.readIngredientSlots(h, ri); } - @Override public @Nullable NeiRecipeLookup.Slot readResultSlot(Object h, int ri) { + + @Override + public @Nullable NeiRecipeLookup.Slot readResultSlot(Object h, int ri) { return NeiRecipeLookup.readResultSlot(h, ri); } }; - List handlers = RecipeCompiler.filterHandlers(rawHandlers, - ph.handlerName, ph.handlerId, ph.handlerOrder, metadataReader); + List handlers = RecipeCompiler + .filterHandlers(rawHandlers, ph.handlerName, ph.handlerId, ph.handlerOrder, metadataReader); if (!handlers.isEmpty()) { List boxes = new ArrayList<>(); for (int hi = 0; hi < handlers.size() && boxes.size() < limit; hi++) { @@ -119,7 +135,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { int recipeEnd = ph.recipeIndex >= 0 ? Math.min(num, ph.recipeIndex + 1) : num; for (int ri = recipeStart; ri < recipeEnd && boxes.size() < limit; ri++) { if (hasRecipeFilter - && !RecipeCompiler.recipeMatches(handler, ri, ph.inputExpr, ph.outputExpr, recipeAccess)) continue; + && !RecipeCompiler.recipeMatches(handler, ri, ph.inputExpr, ph.outputExpr, recipeAccess)) + continue; boxes.add(new LytNeiRecipeBox(handler, ri, !usageQuery)); } } @@ -128,7 +145,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } if (ph.recipeIndex >= 0) { - showFallback(ctx, ph,"Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); + showFallback(ctx, ph, "Recipe index " + ph.recipeIndex + " not found for " + ph.idStr); return; } } else if (hasHandlerFilter) { @@ -138,37 +155,48 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { handlerPart = " with handler " + (ph.handlerName != null ? ph.handlerName : ph.handlerId); } showFallback(ctx, ph, "No recipe found for " + ph.idStr + handlerPart); - } else if (FMLLog.getLogger().isDebugEnabled()) { - FMLLog.getLogger().debug("Recipe handler filter eliminated all candidates for {}", ph.idStr); - } + } else if (FMLLog.getLogger() + .isDebugEnabled()) { + FMLLog.getLogger() + .debug("Recipe handler filter eliminated all candidates for {}", ph.idStr); + } return; } // Integration recipe entries List recipeEntries = usageQuery ? Collections.emptyList() - : GuideNhIntegrationRegistry.global().findCraftingRecipeEntries(targetStack); + : GuideNhIntegrationRegistry.global() + .findCraftingRecipeEntries(targetStack); if (!recipeEntries.isEmpty()) { List boxes = new ArrayList<>(); int entryStart = ph.recipeIndex >= 0 ? ph.recipeIndex : 0; - int entryEnd = ph.recipeIndex >= 0 - ? Math.min(recipeEntries.size(), ph.recipeIndex + 1) : recipeEntries.size(); + int entryEnd = ph.recipeIndex >= 0 ? Math.min(recipeEntries.size(), ph.recipeIndex + 1) + : recipeEntries.size(); for (int i = entryStart; i < entryEnd && boxes.size() < limit; i++) { var e = recipeEntries.get(i); - if (e.result() == null || e.ingredients().isEmpty()) continue; + if (e.result() == null || e.ingredients() + .isEmpty()) continue; if (hasRecipeFilter && !RecipeCompiler.entryMatches(e, ph.inputExpr, ph.outputExpr)) continue; List flat = new ArrayList<>(9); for (int s = 0; s < 9; s++) flat.add(null); int idx = 0; for (RecipeSlot slot : e.ingredients()) { if (idx >= 9) break; - if (slot.stacks() != null && !slot.stacks().isEmpty()) - flat.set(idx, slot.stacks().get(0)); + if (slot.stacks() != null && !slot.stacks() + .isEmpty()) flat.set( + idx, + slot.stacks() + .get(0)); idx++; } - ItemStack resultStack = e.result().stacks() != null && !e.result().stacks().isEmpty() - ? e.result().stacks().get(0) : null; - if (resultStack != null) - boxes.add(LytStandardRecipeBox.shapeless(flat, resultStack)); + ItemStack resultStack = e.result() + .stacks() != null + && !e.result() + .stacks() + .isEmpty() ? e.result() + .stacks() + .get(0) : null; + if (resultStack != null) boxes.add(LytStandardRecipeBox.shapeless(flat, resultStack)); } if (!boxes.isEmpty()) { ctx.replace(buildResult(boxes)); @@ -180,18 +208,16 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { List entries = usageQuery ? Collections.emptyList() : RecipeLookup.findByOutput(item); if (entries.isEmpty()) { - showFallback(ctx, ph,"No recipe found for " + ph.idStr); + showFallback(ctx, ph, "No recipe found for " + ph.idStr); return; } List boxes = new ArrayList<>(); int vanillaStart = ph.recipeIndex >= 0 ? ph.recipeIndex : 0; - int vanillaEnd = ph.recipeIndex >= 0 - ? Math.min(entries.size(), ph.recipeIndex + 1) : entries.size(); + int vanillaEnd = ph.recipeIndex >= 0 ? Math.min(entries.size(), ph.recipeIndex + 1) : entries.size(); for (int i = vanillaStart; i < vanillaEnd && boxes.size() < limit; i++) { var e = entries.get(i); - if (hasRecipeFilter - && !RecipeCompiler.vanillaEntryMatches(e, ph.inputExpr, ph.outputExpr)) continue; + if (hasRecipeFilter && !RecipeCompiler.vanillaEntryMatches(e, ph.inputExpr, ph.outputExpr)) continue; var box = e.shapeless ? LytStandardRecipeBox.shapeless(RecipeLookup.asList(e), e.result) : LytStandardRecipeBox.shaped3x3(RecipeLookup.asList(e), e.result); boxes.add(box); @@ -200,7 +226,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ctx.replace(buildResult(boxes)); return; } - showFallback(ctx, ph,"No recipe found for " + ph.idStr); + showFallback(ctx, ph, "No recipe found for " + ph.idStr); } @SuppressWarnings("unchecked") @@ -217,8 +243,7 @@ private static LytBlock buildResultTyped(List boxes) { } private void showFallback(ScriptContext ctx, RecipePlaceholder ph, String autoMessage) { - String text = (ph.fallbackText != null && !ph.fallbackText.isEmpty()) - ? ph.fallbackText : autoMessage; + String text = (ph.fallbackText != null && !ph.fallbackText.isEmpty()) ? ph.fallbackText : autoMessage; ctx.replace(LytParagraph.error(text)); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java index e64df640..dcb4d860 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/StructureScript.java @@ -17,10 +17,14 @@ public class StructureScript implements LytScript { @Override - public ScriptType type() { return ScriptType.JAVA; } + public ScriptType type() { + return ScriptType.JAVA; + } @Override - public String styleClass() { return "Structure"; } + public String styleClass() { + return "Structure"; + } @Override @SuppressWarnings("deprecation") @@ -47,8 +51,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { @SuppressWarnings("deprecation") private static ItemStack resolveEntry(String idSpec) { if (idSpec == null || idSpec.isEmpty()) return null; - com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = - com.hfstudio.guidenh.guide.compiler.IdUtils.parseItemRef(idSpec, "minecraft"); + com.hfstudio.guidenh.guide.compiler.IdUtils.ParsedItemRef ref = com.hfstudio.guidenh.guide.compiler.IdUtils + .parseItemRef(idSpec, "minecraft"); if (ref == null) return null; Item item = (Item) Item.itemRegistry.getObject(ref.rawKey()); if (item != null) { From f82e5413680669a21ad014ffa40ae1aa074489c5 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:19:25 +0800 Subject: [PATCH 091/136] sa --- .../java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java | 1 - .../java/com/hfstudio/guidenh/guide/internal/GuideScreen.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index ee8bf6b8..ce633672 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -44,7 +44,6 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.flow.LytFlowText; import com.hfstudio.guidenh.guide.document.flow.LytSpoilerSpan; -import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; import com.hfstudio.guidenh.guide.extensions.Extension; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.extensions.ExtensionPoint; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 64c8394b..01231ed4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -45,8 +45,8 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; -import com.hfstudio.guidenh.ClientProxy; import com.github.bsideup.jabel.Desugar; +import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.config.ModConfig; From fdc4aca2168294cc39dab31e6bd67d717d01db75 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:19:25 +0800 Subject: [PATCH 092/136] sa --- .../java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java | 1 - .../java/com/hfstudio/guidenh/guide/internal/GuideScreen.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index ee8bf6b8..ce633672 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -44,7 +44,6 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; import com.hfstudio.guidenh.guide.document.flow.LytFlowText; import com.hfstudio.guidenh.guide.document.flow.LytSpoilerSpan; -import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; import com.hfstudio.guidenh.guide.extensions.Extension; import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.extensions.ExtensionPoint; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 64c8394b..01231ed4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -45,8 +45,8 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; -import com.hfstudio.guidenh.ClientProxy; import com.github.bsideup.jabel.Desugar; +import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.config.ModConfig; From 4512d9140c6142e7e851b44b65980d1c5e5f06ba Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:22:04 +0800 Subject: [PATCH 093/136] Update PageCompiler.java --- .../java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index ce633672..2da30066 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -23,6 +23,7 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; +import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.document.block.LatexRenderOptions; From ccef97f263fc76f2f6523d0103c5b9b841acb2b8 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:22:04 +0800 Subject: [PATCH 094/136] Update PageCompiler.java --- .../java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index ce633672..2da30066 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -23,6 +23,7 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.PageCollection; +import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.document.block.LatexRenderOptions; From ebf9fedd0894e859e4c555c78342062082832d51 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:11:32 +0800 Subject: [PATCH 095/136] fix logo bug --- .../guidenh/guide/internal/GuideScreen.java | 2 +- .../internal/home/HomePageController.java | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 01231ed4..19ccd718 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2697,7 +2697,7 @@ private void drawHomeContent(int mouseX, int mouseY) { } var sections = homePageDataBuilder.build(bookmarkState, homeHistory); var layout = HomePageLayout.compute(contentX, contentY, contentW, contentH, homeLogoWidth, homeLogoHeight); - homePageController.render(mc, sections, layout, logoTexture, mouseX, mouseY); + homePageController.render(mc, sections, layout, logoTexture, homeLogoWidth, homeLogoHeight, mouseX, mouseY); } @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java index f0f11ed4..f81a7409 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java @@ -43,8 +43,8 @@ public class HomePageController { private DragState dragState; public void render(Minecraft mc, HomePageDataBuilder.HomePageSections sections, HomePageLayout.LayoutRects layout, - ResourceLocation logoTexture, int mouseX, int mouseY) { - drawLogo(mc, layout.logo(), logoTexture); + ResourceLocation logoTexture, int logoSourceWidth, int logoSourceHeight, int mouseX, int mouseY) { + drawLogo(mc, layout.logo(), logoTexture, logoSourceWidth, logoSourceHeight); drawSection(mc, layout.recommended(), sections.recommended(), mouseX, mouseY, layout.recommendedTitleSafeTop()); drawSection(mc, layout.bookmarks(), sections.bookmarks(), mouseX, mouseY, 0); drawSection(mc, layout.history(), sections.history(), mouseX, mouseY, 0); @@ -143,7 +143,14 @@ public HomePageEntry mouseReleased(HomePageDataBuilder.HomePageSections sections } } - private void drawLogo(Minecraft mc, HomePageLayout.Rect rect, ResourceLocation logoTexture) { + private void drawLogo(Minecraft mc, HomePageLayout.Rect rect, ResourceLocation logoTexture, int logoSourceWidth, + int logoSourceHeight) { + int safeSourceWidth = Math.max(1, logoSourceWidth); + int safeSourceHeight = Math.max(1, logoSourceHeight); + int drawWidth = Math.max(1, rect.width()); + int drawHeight = Math.max(1, Math.round((float) drawWidth * safeSourceHeight / safeSourceWidth)); + int drawY = rect.y() + (rect.height() - drawHeight) / 2; + mc.getTextureManager() .bindTexture(logoTexture); GL11.glEnable(GL11.GL_BLEND); @@ -151,10 +158,10 @@ private void drawLogo(Minecraft mc, HomePageLayout.Rect rect, ResourceLocation l GL11.glColor4f(1f, 1f, 1f, 1f); Tessellator tessellator = Tessellator.instance; tessellator.startDrawingQuads(); - tessellator.addVertexWithUV(rect.x(), rect.y() + rect.height(), 0, 0f, 1f); - tessellator.addVertexWithUV(rect.x() + rect.width(), rect.y() + rect.height(), 0, 1f, 1f); - tessellator.addVertexWithUV(rect.x() + rect.width(), rect.y(), 0, 1f, 0f); - tessellator.addVertexWithUV(rect.x(), rect.y(), 0, 0f, 0f); + tessellator.addVertexWithUV(rect.x(), drawY + drawHeight, 0, 0f, 1f); + tessellator.addVertexWithUV(rect.x() + drawWidth, drawY + drawHeight, 0, 1f, 1f); + tessellator.addVertexWithUV(rect.x() + drawWidth, drawY, 0, 1f, 0f); + tessellator.addVertexWithUV(rect.x(), drawY, 0, 0f, 0f); tessellator.draw(); } From ed4cf6db65ee66cb046b737adb511c2abe547a01 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:11:32 +0800 Subject: [PATCH 096/136] fix logo bug --- .../guidenh/guide/internal/GuideScreen.java | 2 +- .../internal/home/HomePageController.java | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 01231ed4..19ccd718 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2697,7 +2697,7 @@ private void drawHomeContent(int mouseX, int mouseY) { } var sections = homePageDataBuilder.build(bookmarkState, homeHistory); var layout = HomePageLayout.compute(contentX, contentY, contentW, contentH, homeLogoWidth, homeLogoHeight); - homePageController.render(mc, sections, layout, logoTexture, mouseX, mouseY); + homePageController.render(mc, sections, layout, logoTexture, homeLogoWidth, homeLogoHeight, mouseX, mouseY); } @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java index f0f11ed4..f81a7409 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java @@ -43,8 +43,8 @@ public class HomePageController { private DragState dragState; public void render(Minecraft mc, HomePageDataBuilder.HomePageSections sections, HomePageLayout.LayoutRects layout, - ResourceLocation logoTexture, int mouseX, int mouseY) { - drawLogo(mc, layout.logo(), logoTexture); + ResourceLocation logoTexture, int logoSourceWidth, int logoSourceHeight, int mouseX, int mouseY) { + drawLogo(mc, layout.logo(), logoTexture, logoSourceWidth, logoSourceHeight); drawSection(mc, layout.recommended(), sections.recommended(), mouseX, mouseY, layout.recommendedTitleSafeTop()); drawSection(mc, layout.bookmarks(), sections.bookmarks(), mouseX, mouseY, 0); drawSection(mc, layout.history(), sections.history(), mouseX, mouseY, 0); @@ -143,7 +143,14 @@ public HomePageEntry mouseReleased(HomePageDataBuilder.HomePageSections sections } } - private void drawLogo(Minecraft mc, HomePageLayout.Rect rect, ResourceLocation logoTexture) { + private void drawLogo(Minecraft mc, HomePageLayout.Rect rect, ResourceLocation logoTexture, int logoSourceWidth, + int logoSourceHeight) { + int safeSourceWidth = Math.max(1, logoSourceWidth); + int safeSourceHeight = Math.max(1, logoSourceHeight); + int drawWidth = Math.max(1, rect.width()); + int drawHeight = Math.max(1, Math.round((float) drawWidth * safeSourceHeight / safeSourceWidth)); + int drawY = rect.y() + (rect.height() - drawHeight) / 2; + mc.getTextureManager() .bindTexture(logoTexture); GL11.glEnable(GL11.GL_BLEND); @@ -151,10 +158,10 @@ private void drawLogo(Minecraft mc, HomePageLayout.Rect rect, ResourceLocation l GL11.glColor4f(1f, 1f, 1f, 1f); Tessellator tessellator = Tessellator.instance; tessellator.startDrawingQuads(); - tessellator.addVertexWithUV(rect.x(), rect.y() + rect.height(), 0, 0f, 1f); - tessellator.addVertexWithUV(rect.x() + rect.width(), rect.y() + rect.height(), 0, 1f, 1f); - tessellator.addVertexWithUV(rect.x() + rect.width(), rect.y(), 0, 1f, 0f); - tessellator.addVertexWithUV(rect.x(), rect.y(), 0, 0f, 0f); + tessellator.addVertexWithUV(rect.x(), drawY + drawHeight, 0, 0f, 1f); + tessellator.addVertexWithUV(rect.x() + drawWidth, drawY + drawHeight, 0, 1f, 1f); + tessellator.addVertexWithUV(rect.x() + drawWidth, drawY, 0, 1f, 0f); + tessellator.addVertexWithUV(rect.x(), drawY, 0, 0f, 0f); tessellator.draw(); } From e79c4353a20b677ddf727aad1b27349ad0b07643 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:38:49 +0800 Subject: [PATCH 097/136] fix inline code --- .../guide/compiler/tags/CodeCompiler.java | 4 ++-- .../guidenh/guide/document/DefaultStyles.java | 3 ++- .../block/LytMermaidMindmapCanvas.java | 15 ++++++++---- .../document/block/chart/LytChartBase.java | 3 ++- .../block/functiongraph/LytFunctionGraph.java | 3 ++- .../guide/layout/flow/LineBuilder.java | 3 +++ .../guide/layout/flow/LineTextRun.java | 24 +++++++++++++------ .../guide/style/ResolvedTextStyle.java | 2 +- .../guidenh/guide/style/TextStyle.java | 20 +++++++++++++--- .../assets/guidenh/siteexport/app.css | 5 ++-- .../guidenh/guidenh/_en_us/rendering.md | 6 +++++ .../guidenh/guidenh/_zh_cn/rendering.md | 6 +++++ 12 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java index ee03cc03..2b6abfa5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java @@ -30,8 +30,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen } text.setText(value); text.modifyStyle( - style -> style.italic(true) - .whiteSpace(WhiteSpaceMode.PRE)); + style -> style.inlineCode(true) + .whiteSpace(WhiteSpaceMode.PRE_WRAP)); parent.append(text); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java b/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java index 8ff9c872..e786e1a9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java @@ -30,7 +30,8 @@ private DefaultStyles() {} WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); public static final TextStyle BODY_TEXT = TextStyle.builder() .font(UNIFORM_FONT) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index b73b58f4..616ffb7f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -68,7 +68,8 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); private static final ResolvedTextStyle NODE_TEXT_STYLE = new ResolvedTextStyle( 1f, false, @@ -83,7 +84,8 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); private static final ResolvedTextStyle ICON_TEXT_STYLE = new ResolvedTextStyle( 0.85f, false, @@ -98,7 +100,8 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); private final MermaidMindmapDocument mindmap; private final Map nodeContentBlocks; @@ -1057,7 +1060,8 @@ private ResolvedTextStyle scaleStyle(ResolvedTextStyle baseStyle) { baseStyle.whiteSpace(), baseStyle.alignment(), baseStyle.dropShadow(), - baseStyle.backgroundColor()); + baseStyle.backgroundColor(), + baseStyle.inlineCode()); } private void ensureScaledStyles() { @@ -1370,7 +1374,8 @@ private ResolvedTextStyle scaleStyle(ResolvedTextStyle style) { key.whiteSpace(), key.alignment(), key.dropShadow(), - key.backgroundColor())); + key.backgroundColor(), + key.inlineCode())); } private LytRect scaleRect(LytRect rect) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java index 5fba12c7..3386a5e3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java @@ -330,7 +330,8 @@ public static ResolvedTextStyle textStyle(int argb) { WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); } public static String formatPercent(double ratio) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java index 2f63705c..6154ed28 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java @@ -1282,7 +1282,8 @@ private static ResolvedTextStyle makeStyle(int argb, boolean bold) { WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); } @SuppressWarnings("unused") diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java index aec0a7f5..606a7f11 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java @@ -224,6 +224,9 @@ private void appendText(String text, LytFlowContent flowContent) { el.flowContent = flowContent; int w = Math.round(width); int h = context.getLineHeight(finalStyle); + if (finalStyle.inlineCode()) { + w += LineTextRun.INLINE_CODE_PAD_X * 2; + } el.bounds = new LytRect(innerX, 0, w, h); appendToOpenLine(el); innerX += w; diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java index db30ad5d..25c434fd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java @@ -1,10 +1,14 @@ package com.hfstudio.guidenh.guide.layout.flow; +import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; public class LineTextRun extends LineElement { + public static final int INLINE_CODE_PAD_X = 3; + public static final ConstantColor INLINE_CODE_BACKGROUND = new ConstantColor(0x1AF0F6FF, 0x1A6FB6FF); + public final String text; public final ResolvedTextStyle style; public final ResolvedTextStyle revealStyle; @@ -21,15 +25,21 @@ public LineTextRun(String text, ResolvedTextStyle style, ResolvedTextStyle revea @Override public void render(RenderContext context) { var style = containsMouse ? hoverStyle : revealedBySpoiler ? revealStyle : this.style; - if (style.backgroundColor() != null && bounds.width() > 0 && bounds.height() > 0) { + int backgroundHeight = Math.max(1, bounds.height()); + int backgroundY = bounds.y() - 1; + if (style.inlineCode() && bounds.width() > 0 && bounds.height() > 0) { context.fillRect( - bounds.x() - 1, - bounds.y() - 1, - bounds.width() + 2, - bounds.height() + 1, - style.backgroundColor()); + bounds.x(), + backgroundY, + bounds.width(), + backgroundHeight, + context.resolveColor(INLINE_CODE_BACKGROUND)); + } else if (style.backgroundColor() != null && bounds.width() > 0 && bounds.height() > 0) { + context + .fillRect(bounds.x() - 1, backgroundY, bounds.width() + 2, backgroundHeight, style.backgroundColor()); } - context.drawText(text, bounds.x(), bounds.y(), style); + int textX = style.inlineCode() ? bounds.x() + INLINE_CODE_PAD_X : bounds.x(); + context.drawText(text, textX, bounds.y(), style); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java b/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java index 83f24df3..cae93bad 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java +++ b/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java @@ -10,4 +10,4 @@ public record ResolvedTextStyle(float fontScale, boolean bold, boolean italic, boolean underlined, boolean wavyUnderline, boolean dottedUnderline, boolean strikethrough, boolean obfuscated, String font, ColorValue color, WhiteSpaceMode whiteSpace, TextAlignment alignment, boolean dropShadow, - ColorValue backgroundColor) {} + ColorValue backgroundColor, boolean inlineCode) {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java b/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java index a0a0a093..4d105fa9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java +++ b/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java @@ -13,7 +13,7 @@ public record TextStyle(@Nullable Float fontScale, @Nullable Boolean bold, @Null @Nullable Boolean underlined, @Nullable Boolean wavyUnderline, @Nullable Boolean dottedUnderline, @Nullable Boolean strikethrough, @Nullable Boolean obfuscated, @Nullable String font, @Nullable ColorValue color, @Nullable WhiteSpaceMode whiteSpace, @Nullable TextAlignment alignment, @Nullable Boolean dropShadow, - @Nullable ColorValue backgroundColor) { + @Nullable ColorValue backgroundColor, @Nullable Boolean inlineCode) { public static final TextStyle EMPTY = new TextStyle( null, @@ -29,6 +29,7 @@ public record TextStyle(@Nullable Float fontScale, @Nullable Boolean bold, @Null null, null, null, + null, null); public ResolvedTextStyle mergeWith(ResolvedTextStyle base) { @@ -47,6 +48,7 @@ public ResolvedTextStyle mergeWith(ResolvedTextStyle base) { var alignment = this.alignment != null ? this.alignment : base.alignment(); var dropShadow = this.dropShadow != null ? this.dropShadow : base.dropShadow(); var backgroundColor = this.backgroundColor != null ? this.backgroundColor : base.backgroundColor(); + var inlineCode = this.inlineCode != null ? this.inlineCode : base.inlineCode(); return new ResolvedTextStyle( fontScale, bold, @@ -61,7 +63,8 @@ public ResolvedTextStyle mergeWith(ResolvedTextStyle base) { whiteSpace, alignment, dropShadow, - backgroundColor); + backgroundColor, + inlineCode); } public Builder toBuilder() { @@ -80,6 +83,7 @@ public Builder toBuilder() { builder.alignment = alignment; builder.dropShadow = dropShadow; builder.backgroundColor = backgroundColor; + builder.inlineCode = inlineCode; return builder; } @@ -103,6 +107,7 @@ public static class Builder { private TextAlignment alignment; private Boolean dropShadow; private ColorValue backgroundColor; + private Boolean inlineCode; public Builder apply(TextStyle style) { if (style.fontScale() != null) { @@ -147,6 +152,9 @@ public Builder apply(TextStyle style) { if (style.backgroundColor() != null) { backgroundColor = style.backgroundColor(); } + if (style.inlineCode() != null) { + inlineCode = style.inlineCode(); + } return this; } @@ -220,6 +228,11 @@ public Builder backgroundColor(ColorValue backgroundColor) { return this; } + public Builder inlineCode(Boolean inlineCode) { + this.inlineCode = inlineCode; + return this; + } + public TextStyle build() { return new TextStyle( fontScale, @@ -235,7 +248,8 @@ public TextStyle build() { whiteSpace, alignment, dropShadow, - backgroundColor); + backgroundColor, + inlineCode); } } } diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index bdf0673d..882af0b1 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -191,8 +191,8 @@ td + td, th + th { code { font-family: 'Pixeloid Mono', monospace; font-size: 0.88em; - background: rgba(0, 0, 0, 0.35); - border: 1px solid rgba(124, 124, 124, 0.4); + color: inherit; + background: rgba(111, 182, 255, 0.12); padding: 1px 4px; } @@ -206,7 +206,6 @@ pre { pre code { background: none; - border: none; padding: 0; } diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md index 0d73e2d4..cf2078b2 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md @@ -32,6 +32,12 @@ This page tests every block-level element supported by the Markdown rendering pi Plain text, **bold text**, *italic text*, ***bold-italic text***, ~~strikethrough~~, and `inline code`. +Nested code styles: **`bold code`**, *`italic code`*, ~~`strike code`~~, and ***`bold italic code`***. + +Literal markers inside code: *is* ~~`aaa**`~~ and **`literal ~~ markers`**. + +Long wrapped code sample: `inline-code-with-a-long-name-that-should-wrap-cleanly-inside-the-line-layout`. + ## Horizontal rule Above the rule. diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md index 910debb9..94ce360a 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md @@ -32,6 +32,12 @@ categories: 正常文本,**粗体文本**,*斜体文本*,***粗斜体文本***,~~删除线~~,以及 `行内代码`。 +嵌套代码样式:**`粗体代码`**、*`斜体代码`*、~~`删除线代码`~~,以及 ***`粗斜体代码`***。 + +代码内字面标记:*是* ~~`aaa**`~~,以及 **`literal ~~ markers`**。 + +长行内代码换行示例:`inline-code-with-a-long-name-that-should-wrap-cleanly-inside-the-line-layout`。 + ## 分割线 上面是一段文本。 From 9bc1256c7d0a442c10af2bd39cd624eee7b1c961 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:38:49 +0800 Subject: [PATCH 098/136] fix inline code --- .../guide/compiler/tags/CodeCompiler.java | 4 ++-- .../guidenh/guide/document/DefaultStyles.java | 3 ++- .../block/LytMermaidMindmapCanvas.java | 15 ++++++++---- .../document/block/chart/LytChartBase.java | 3 ++- .../block/functiongraph/LytFunctionGraph.java | 3 ++- .../guide/layout/flow/LineBuilder.java | 3 +++ .../guide/layout/flow/LineTextRun.java | 24 +++++++++++++------ .../guide/style/ResolvedTextStyle.java | 2 +- .../guidenh/guide/style/TextStyle.java | 20 +++++++++++++--- .../assets/guidenh/siteexport/app.css | 5 ++-- .../guidenh/guidenh/_en_us/rendering.md | 6 +++++ .../guidenh/guidenh/_zh_cn/rendering.md | 6 +++++ 12 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java index ee03cc03..2b6abfa5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CodeCompiler.java @@ -30,8 +30,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen } text.setText(value); text.modifyStyle( - style -> style.italic(true) - .whiteSpace(WhiteSpaceMode.PRE)); + style -> style.inlineCode(true) + .whiteSpace(WhiteSpaceMode.PRE_WRAP)); parent.append(text); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java b/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java index 8ff9c872..e786e1a9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/DefaultStyles.java @@ -30,7 +30,8 @@ private DefaultStyles() {} WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); public static final TextStyle BODY_TEXT = TextStyle.builder() .font(UNIFORM_FONT) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index b73b58f4..616ffb7f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -68,7 +68,8 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); private static final ResolvedTextStyle NODE_TEXT_STYLE = new ResolvedTextStyle( 1f, false, @@ -83,7 +84,8 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); private static final ResolvedTextStyle ICON_TEXT_STYLE = new ResolvedTextStyle( 0.85f, false, @@ -98,7 +100,8 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); private final MermaidMindmapDocument mindmap; private final Map nodeContentBlocks; @@ -1057,7 +1060,8 @@ private ResolvedTextStyle scaleStyle(ResolvedTextStyle baseStyle) { baseStyle.whiteSpace(), baseStyle.alignment(), baseStyle.dropShadow(), - baseStyle.backgroundColor()); + baseStyle.backgroundColor(), + baseStyle.inlineCode()); } private void ensureScaledStyles() { @@ -1370,7 +1374,8 @@ private ResolvedTextStyle scaleStyle(ResolvedTextStyle style) { key.whiteSpace(), key.alignment(), key.dropShadow(), - key.backgroundColor())); + key.backgroundColor(), + key.inlineCode())); } private LytRect scaleRect(LytRect rect) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java index 5fba12c7..3386a5e3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/chart/LytChartBase.java @@ -330,7 +330,8 @@ public static ResolvedTextStyle textStyle(int argb) { WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); } public static String formatPercent(double ratio) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java index 2f63705c..6154ed28 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/functiongraph/LytFunctionGraph.java @@ -1282,7 +1282,8 @@ private static ResolvedTextStyle makeStyle(int argb, boolean bold) { WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, - null); + null, + false); } @SuppressWarnings("unused") diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java index aec0a7f5..606a7f11 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java @@ -224,6 +224,9 @@ private void appendText(String text, LytFlowContent flowContent) { el.flowContent = flowContent; int w = Math.round(width); int h = context.getLineHeight(finalStyle); + if (finalStyle.inlineCode()) { + w += LineTextRun.INLINE_CODE_PAD_X * 2; + } el.bounds = new LytRect(innerX, 0, w, h); appendToOpenLine(el); innerX += w; diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java index db30ad5d..25c434fd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java @@ -1,10 +1,14 @@ package com.hfstudio.guidenh.guide.layout.flow; +import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; public class LineTextRun extends LineElement { + public static final int INLINE_CODE_PAD_X = 3; + public static final ConstantColor INLINE_CODE_BACKGROUND = new ConstantColor(0x1AF0F6FF, 0x1A6FB6FF); + public final String text; public final ResolvedTextStyle style; public final ResolvedTextStyle revealStyle; @@ -21,15 +25,21 @@ public LineTextRun(String text, ResolvedTextStyle style, ResolvedTextStyle revea @Override public void render(RenderContext context) { var style = containsMouse ? hoverStyle : revealedBySpoiler ? revealStyle : this.style; - if (style.backgroundColor() != null && bounds.width() > 0 && bounds.height() > 0) { + int backgroundHeight = Math.max(1, bounds.height()); + int backgroundY = bounds.y() - 1; + if (style.inlineCode() && bounds.width() > 0 && bounds.height() > 0) { context.fillRect( - bounds.x() - 1, - bounds.y() - 1, - bounds.width() + 2, - bounds.height() + 1, - style.backgroundColor()); + bounds.x(), + backgroundY, + bounds.width(), + backgroundHeight, + context.resolveColor(INLINE_CODE_BACKGROUND)); + } else if (style.backgroundColor() != null && bounds.width() > 0 && bounds.height() > 0) { + context + .fillRect(bounds.x() - 1, backgroundY, bounds.width() + 2, backgroundHeight, style.backgroundColor()); } - context.drawText(text, bounds.x(), bounds.y(), style); + int textX = style.inlineCode() ? bounds.x() + INLINE_CODE_PAD_X : bounds.x(); + context.drawText(text, textX, bounds.y(), style); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java b/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java index 83f24df3..cae93bad 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java +++ b/src/main/java/com/hfstudio/guidenh/guide/style/ResolvedTextStyle.java @@ -10,4 +10,4 @@ public record ResolvedTextStyle(float fontScale, boolean bold, boolean italic, boolean underlined, boolean wavyUnderline, boolean dottedUnderline, boolean strikethrough, boolean obfuscated, String font, ColorValue color, WhiteSpaceMode whiteSpace, TextAlignment alignment, boolean dropShadow, - ColorValue backgroundColor) {} + ColorValue backgroundColor, boolean inlineCode) {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java b/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java index a0a0a093..4d105fa9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java +++ b/src/main/java/com/hfstudio/guidenh/guide/style/TextStyle.java @@ -13,7 +13,7 @@ public record TextStyle(@Nullable Float fontScale, @Nullable Boolean bold, @Null @Nullable Boolean underlined, @Nullable Boolean wavyUnderline, @Nullable Boolean dottedUnderline, @Nullable Boolean strikethrough, @Nullable Boolean obfuscated, @Nullable String font, @Nullable ColorValue color, @Nullable WhiteSpaceMode whiteSpace, @Nullable TextAlignment alignment, @Nullable Boolean dropShadow, - @Nullable ColorValue backgroundColor) { + @Nullable ColorValue backgroundColor, @Nullable Boolean inlineCode) { public static final TextStyle EMPTY = new TextStyle( null, @@ -29,6 +29,7 @@ public record TextStyle(@Nullable Float fontScale, @Nullable Boolean bold, @Null null, null, null, + null, null); public ResolvedTextStyle mergeWith(ResolvedTextStyle base) { @@ -47,6 +48,7 @@ public ResolvedTextStyle mergeWith(ResolvedTextStyle base) { var alignment = this.alignment != null ? this.alignment : base.alignment(); var dropShadow = this.dropShadow != null ? this.dropShadow : base.dropShadow(); var backgroundColor = this.backgroundColor != null ? this.backgroundColor : base.backgroundColor(); + var inlineCode = this.inlineCode != null ? this.inlineCode : base.inlineCode(); return new ResolvedTextStyle( fontScale, bold, @@ -61,7 +63,8 @@ public ResolvedTextStyle mergeWith(ResolvedTextStyle base) { whiteSpace, alignment, dropShadow, - backgroundColor); + backgroundColor, + inlineCode); } public Builder toBuilder() { @@ -80,6 +83,7 @@ public Builder toBuilder() { builder.alignment = alignment; builder.dropShadow = dropShadow; builder.backgroundColor = backgroundColor; + builder.inlineCode = inlineCode; return builder; } @@ -103,6 +107,7 @@ public static class Builder { private TextAlignment alignment; private Boolean dropShadow; private ColorValue backgroundColor; + private Boolean inlineCode; public Builder apply(TextStyle style) { if (style.fontScale() != null) { @@ -147,6 +152,9 @@ public Builder apply(TextStyle style) { if (style.backgroundColor() != null) { backgroundColor = style.backgroundColor(); } + if (style.inlineCode() != null) { + inlineCode = style.inlineCode(); + } return this; } @@ -220,6 +228,11 @@ public Builder backgroundColor(ColorValue backgroundColor) { return this; } + public Builder inlineCode(Boolean inlineCode) { + this.inlineCode = inlineCode; + return this; + } + public TextStyle build() { return new TextStyle( fontScale, @@ -235,7 +248,8 @@ public TextStyle build() { whiteSpace, alignment, dropShadow, - backgroundColor); + backgroundColor, + inlineCode); } } } diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index bdf0673d..882af0b1 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -191,8 +191,8 @@ td + td, th + th { code { font-family: 'Pixeloid Mono', monospace; font-size: 0.88em; - background: rgba(0, 0, 0, 0.35); - border: 1px solid rgba(124, 124, 124, 0.4); + color: inherit; + background: rgba(111, 182, 255, 0.12); padding: 1px 4px; } @@ -206,7 +206,6 @@ pre { pre code { background: none; - border: none; padding: 0; } diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md index 0d73e2d4..cf2078b2 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/rendering.md @@ -32,6 +32,12 @@ This page tests every block-level element supported by the Markdown rendering pi Plain text, **bold text**, *italic text*, ***bold-italic text***, ~~strikethrough~~, and `inline code`. +Nested code styles: **`bold code`**, *`italic code`*, ~~`strike code`~~, and ***`bold italic code`***. + +Literal markers inside code: *is* ~~`aaa**`~~ and **`literal ~~ markers`**. + +Long wrapped code sample: `inline-code-with-a-long-name-that-should-wrap-cleanly-inside-the-line-layout`. + ## Horizontal rule Above the rule. diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md index 910debb9..94ce360a 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/rendering.md @@ -32,6 +32,12 @@ categories: 正常文本,**粗体文本**,*斜体文本*,***粗斜体文本***,~~删除线~~,以及 `行内代码`。 +嵌套代码样式:**`粗体代码`**、*`斜体代码`*、~~`删除线代码`~~,以及 ***`粗斜体代码`***。 + +代码内字面标记:*是* ~~`aaa**`~~,以及 **`literal ~~ markers`**。 + +长行内代码换行示例:`inline-code-with-a-long-name-that-should-wrap-cleanly-inside-the-line-layout`。 + ## 分割线 上面是一段文本。 From c921b767b64844df423f65ad5182e0eb44bc3636 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:59:48 +0800 Subject: [PATCH 099/136] opti --- .../guide/layout/MinecraftFontMetrics.java | 11 ++- .../guide/layout/flow/LineBuilder.java | 73 ++++++++++++++----- .../guide/layout/flow/LineTextRun.java | 45 +++++++----- .../guide/render/VanillaRenderContext.java | 30 +++++--- 4 files changed, 108 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java index ff91fd71..e4f7adce 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java @@ -21,19 +21,22 @@ public MinecraftFontMetrics(FontRenderer font) { public float getAdvance(int codePoint, ResolvedTextStyle style) { char ch = codePoint <= 0xFFFF ? (char) codePoint : '?'; float raw = font.getCharWidth(ch); - if (style != null && style.bold() && raw > 0) { - raw += 1f; - } if (style == null) { return raw; } + if (style.bold() && raw > 0) { + raw += 1f; + } float scale = style.fontScale(); return scale == 1f ? raw : raw * scale; } @Override public int getLineHeight(ResolvedTextStyle style) { - float scale = style != null ? style.fontScale() : 1f; + if (style == null) { + return font.FONT_HEIGHT + 1; + } + float scale = style.fontScale(); if (scale == 1f) { return font.FONT_HEIGHT + 1; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java index 606a7f11..52fd0c4c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java @@ -2,6 +2,7 @@ import java.text.BreakIterator; import java.text.CharacterIterator; +import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -50,6 +51,7 @@ public class LineBuilder implements Consumer { private LineElement openLineTail; private final TextAlignment alignment; private final StringBuilder lineBuffer = new StringBuilder(); + private float[] lineBufferPrefixWidths = new float[64]; /** Reusable CharacterIterator wrapping {@link #lineBuffer} to avoid String allocation in BreakIterator. */ private final StringBuilderCharIterator charIterator = new StringBuilderCharIterator(); @@ -208,6 +210,7 @@ private void appendText(String text, LytFlowContent flowContent) { final var finalStyle = style; final var finalRevealStyle = revealStyle; final var finalHoverStyle = hoverStyle; + final boolean inlineCode = finalStyle.inlineCode(); char lastChar = '\0'; var endOfOpenLine = getEndOfOpenLine(); @@ -224,8 +227,8 @@ private void appendText(String text, LytFlowContent flowContent) { el.flowContent = flowContent; int w = Math.round(width); int h = context.getLineHeight(finalStyle); - if (finalStyle.inlineCode()) { - w += LineTextRun.INLINE_CODE_PAD_X * 2; + if (inlineCode) { + w += LineTextRun.INLINE_CODE_EXTRA_WIDTH; } el.bounds = new LytRect(innerX, 0, w, h); appendToOpenLine(el); @@ -261,6 +264,7 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh float curLineWidth = 0; lineBuffer.setLength(0); + lineBufferPrefixWidths[0] = 0f; // Hoist whitespace mode flags out of the per-character loop to avoid repeated method calls. boolean collapseSegmentBreaks = style.whiteSpace() @@ -292,6 +296,7 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh } else { consumer.visitRun(lineBuffer, curLineWidth, true); lineBuffer.setLength(0); + lineBufferPrefixWidths[0] = 0f; lastCharWasWhitespace = true; remainingLineWidth = getAvailableHorizontalSpace(); continue; @@ -332,32 +337,21 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh // current word does not offer us any opportunity to. if (precedingBreakOpportunity > 0 || precedingBreakOpportunity == 0 && canBreakAtStart) { // Determine width up until the break opportunity. - // Short-circuit: when breaking at end-of-buffer (whitespace case), curLineWidth - // already equals the total, so skip the O(N) recomputation loop. - float widthAtBreakOpportunity; - if (precedingBreakOpportunity == lineBuffer.length()) { - widthAtBreakOpportunity = curLineWidth; - } else { - widthAtBreakOpportunity = 0f; - for (var j = 0; j < precedingBreakOpportunity; j++) { - widthAtBreakOpportunity += context.getAdvance(lineBuffer.charAt(j), style); - } - } + float widthAtBreakOpportunity = lineBufferPrefixWidths[precedingBreakOpportunity]; consumer .visitRun(lineBuffer.subSequence(0, precedingBreakOpportunity), widthAtBreakOpportunity, true); curLineWidth -= widthAtBreakOpportunity; - lineBuffer.delete(0, precedingBreakOpportunity); + deleteLineBufferPrefix(precedingBreakOpportunity); if (!lineBuffer.isEmpty() && Character.isWhitespace(lineBuffer.charAt(0))) { - var firstChar = lineBuffer.charAt(0); - lineBuffer.deleteCharAt(0); - curLineWidth -= context.getAdvance(firstChar, style); + curLineWidth -= deleteLineBufferPrefix(1); } } else { // We exceeded the line length, but did not find a break opportunity // this causes a forced break mid-word consumer.visitRun(lineBuffer, curLineWidth, true); lineBuffer.setLength(0); + lineBufferPrefixWidths[0] = 0f; curLineWidth = 0; } remainingLineWidth = getAvailableHorizontalSpace(); @@ -368,7 +362,7 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh } } curLineWidth += advance; - lineBuffer.appendCodePoint(codePoint); + appendCodePointToLineBuffer(codePoint, advance); } if (!lineBuffer.isEmpty()) { @@ -376,6 +370,45 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh } } + private void appendCodePointToLineBuffer(int codePoint, float advance) { + int previousLength = lineBuffer.length(); + float bufferWidth = lineBufferPrefixWidths[previousLength] + advance; + lineBuffer.appendCodePoint(codePoint); + int newLength = lineBuffer.length(); + ensureLineBufferPrefixCapacity(newLength + 1); + for (int i = previousLength + 1; i <= newLength; i++) { + lineBufferPrefixWidths[i] = bufferWidth; + } + } + + private float deleteLineBufferPrefix(int charCount) { + if (charCount <= 0) { + return 0f; + } + float removedWidth = lineBufferPrefixWidths[charCount]; + int remainingChars = lineBuffer.length() - charCount; + if (remainingChars > 0) { + System.arraycopy(lineBufferPrefixWidths, charCount + 1, lineBufferPrefixWidths, 1, remainingChars); + for (int i = 1; i <= remainingChars; i++) { + lineBufferPrefixWidths[i] -= removedWidth; + } + } + lineBuffer.delete(0, charCount); + lineBufferPrefixWidths[0] = 0f; + return removedWidth; + } + + private void ensureLineBufferPrefixCapacity(int requiredLength) { + if (requiredLength <= lineBufferPrefixWidths.length) { + return; + } + int newLength = lineBufferPrefixWidths.length; + while (newLength < requiredLength) { + newLength <<= 1; + } + lineBufferPrefixWidths = Arrays.copyOf(lineBufferPrefixWidths, newLength); + } + private void endLine() { if (openLineElement == null) { return; @@ -543,7 +576,7 @@ public LytRect getBounds() { } @FunctionalInterface - interface LineConsumer { + private interface LineConsumer { void visitRun(CharSequence run, float width, boolean end); } @@ -552,7 +585,7 @@ interface LineConsumer { * A {@link CharacterIterator} that wraps a {@link StringBuilder} without copying it to a {@link String}. * Reuse by calling {@link #reset} before each use. */ - private static final class StringBuilderCharIterator implements CharacterIterator { + private static class StringBuilderCharIterator implements CharacterIterator { private StringBuilder sb; private int begin; diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java index 25c434fd..c4091d84 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java @@ -1,13 +1,17 @@ package com.hfstudio.guidenh.guide.layout.flow; -import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.color.ColorValue; +import com.hfstudio.guidenh.guide.color.LightDarkMode; +import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; public class LineTextRun extends LineElement { public static final int INLINE_CODE_PAD_X = 3; - public static final ConstantColor INLINE_CODE_BACKGROUND = new ConstantColor(0x1AF0F6FF, 0x1A6FB6FF); + public static final int INLINE_CODE_EXTRA_WIDTH = INLINE_CODE_PAD_X * 2; + public static final int INLINE_CODE_BACKGROUND_LIGHT = 0x1AF0F6FF; + public static final int INLINE_CODE_BACKGROUND_DARK = 0x1A6FB6FF; public final String text; public final ResolvedTextStyle style; @@ -24,22 +28,29 @@ public LineTextRun(String text, ResolvedTextStyle style, ResolvedTextStyle revea @Override public void render(RenderContext context) { - var style = containsMouse ? hoverStyle : revealedBySpoiler ? revealStyle : this.style; - int backgroundHeight = Math.max(1, bounds.height()); - int backgroundY = bounds.y() - 1; - if (style.inlineCode() && bounds.width() > 0 && bounds.height() > 0) { - context.fillRect( - bounds.x(), - backgroundY, - bounds.width(), - backgroundHeight, - context.resolveColor(INLINE_CODE_BACKGROUND)); - } else if (style.backgroundColor() != null && bounds.width() > 0 && bounds.height() > 0) { - context - .fillRect(bounds.x() - 1, backgroundY, bounds.width() + 2, backgroundHeight, style.backgroundColor()); + var resolvedStyle = containsMouse ? hoverStyle : revealedBySpoiler ? revealStyle : this.style; + boolean inlineCode = resolvedStyle.inlineCode(); + ColorValue backgroundColor = resolvedStyle.backgroundColor(); + if (!inlineCode && backgroundColor == null) { + context.drawText(text, bounds.x(), bounds.y(), resolvedStyle); + return; } - int textX = style.inlineCode() ? bounds.x() + INLINE_CODE_PAD_X : bounds.x(); - context.drawText(text, textX, bounds.y(), style); + LytRect rect = bounds; + int width = rect.width(); + int height = rect.height(); + if (width > 0 && height > 0) { + int backgroundY = rect.y() - 1; + if (inlineCode) { + int backgroundColorArgb = context.lightDarkMode() == LightDarkMode.DARK_MODE + ? INLINE_CODE_BACKGROUND_DARK + : INLINE_CODE_BACKGROUND_LIGHT; + context.fillRect(rect.x(), backgroundY, width, height, backgroundColorArgb); + } else { + context.fillRect(rect.x() - 1, backgroundY, width + 2, height, backgroundColor); + } + } + int textX = inlineCode ? rect.x() + INLINE_CODE_PAD_X : rect.x(); + context.drawText(text, textX, rect.y(), resolvedStyle); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java index 7aac7ae8..8fbb019c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java @@ -24,6 +24,10 @@ public class VanillaRenderContext implements RenderContext { public static final RenderItem ITEM_RENDERER = new RenderItem(); + private static final String FORMAT_BOLD = "\u00a7l"; + private static final String FORMAT_ITALIC = "\u00a7o"; + private static final String FORMAT_STRIKETHROUGH = "\u00a7m"; + private static final String FORMAT_OBFUSCATED = "\u00a7k"; private final FontRenderer fontRenderer; private int screenHeight; @@ -157,10 +161,10 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { if (style.bold() || style.italic() || style.strikethrough() || style.obfuscated()) { sb = textStyleBuffer; sb.setLength(0); - if (style.bold()) sb.append("\u00a7l"); - if (style.italic()) sb.append("\u00a7o"); - if (style.strikethrough()) sb.append("\u00a7m"); - if (style.obfuscated()) sb.append("\u00a7k"); + if (style.bold()) sb.append(FORMAT_BOLD); + if (style.italic()) sb.append(FORMAT_ITALIC); + if (style.strikethrough()) sb.append(FORMAT_STRIKETHROUGH); + if (style.obfuscated()) sb.append(FORMAT_OBFUSCATED); } String drawn = sb != null ? sb.append(text) .toString() : text; @@ -183,24 +187,30 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { fontRenderer.drawString(drawn, x, y, color); } + boolean hasUnderline = style.underlined(); + boolean hasWavyUnderline = style.wavyUnderline(); + boolean hasDottedUnderline = style.dottedUnderline(); + if (!hasUnderline && !hasWavyUnderline && !hasDottedUnderline) { + return; + } + + int scaledFontHeight = Math.round(fontRenderer.FONT_HEIGHT * scale); + int decorationY = y + scaledFontHeight - 1; int decoratedWidth = -1; if (style.underlined()) { decoratedWidth = Math.round(fontRenderer.getStringWidth(drawn) * scale); - int uy = y + Math.round((fontRenderer.FONT_HEIGHT) * scale) - 1; - Gui.drawRect(x, uy, x + decoratedWidth, uy + 1, color); + Gui.drawRect(x, decorationY, x + decoratedWidth, decorationY + 1, color); } if (style.wavyUnderline()) { int w = decoratedWidth >= 0 ? decoratedWidth : Math.round(fontRenderer.getStringWidth(drawn) * scale); - int baseY = y + Math.round((fontRenderer.FONT_HEIGHT) * scale) - 1; // Draw a 2px-tall sine-like zig-zag using 1x1 rects: pattern of 4 px period. for (int i = 0; i < w; i++) { int phase = i & 3; // 0,1,2,3 int dy = (phase == 0 || phase == 2) ? 0 : (phase == 1 ? -1 : 1); - Gui.drawRect(x + i, baseY + dy, x + i + 1, baseY + dy + 1, color); + Gui.drawRect(x + i, decorationY + dy, x + i + 1, decorationY + dy + 1, color); } } if (style.dottedUnderline()) { - int dy = y + Math.round((fontRenderer.FONT_HEIGHT) * scale) - 1; // Center a single 2x2 dot under each rendered character cell. int cursor = 0; int len = drawn.length(); @@ -216,7 +226,7 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { continue; } int dotX = x + cursor + Math.max(0, (cw - 2) / 2); - Gui.drawRect(dotX, dy, dotX + 2, dy + 2, color); + Gui.drawRect(dotX, decorationY, dotX + 2, decorationY + 2, color); cursor += cw; } } From 1d09194752b1aca363524ede07255ca1ad881a3a Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:59:48 +0800 Subject: [PATCH 100/136] opti --- .../guide/layout/MinecraftFontMetrics.java | 11 ++- .../guide/layout/flow/LineBuilder.java | 73 ++++++++++++++----- .../guide/layout/flow/LineTextRun.java | 45 +++++++----- .../guide/render/VanillaRenderContext.java | 30 +++++--- 4 files changed, 108 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java index ff91fd71..e4f7adce 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java @@ -21,19 +21,22 @@ public MinecraftFontMetrics(FontRenderer font) { public float getAdvance(int codePoint, ResolvedTextStyle style) { char ch = codePoint <= 0xFFFF ? (char) codePoint : '?'; float raw = font.getCharWidth(ch); - if (style != null && style.bold() && raw > 0) { - raw += 1f; - } if (style == null) { return raw; } + if (style.bold() && raw > 0) { + raw += 1f; + } float scale = style.fontScale(); return scale == 1f ? raw : raw * scale; } @Override public int getLineHeight(ResolvedTextStyle style) { - float scale = style != null ? style.fontScale() : 1f; + if (style == null) { + return font.FONT_HEIGHT + 1; + } + float scale = style.fontScale(); if (scale == 1f) { return font.FONT_HEIGHT + 1; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java index 606a7f11..52fd0c4c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java @@ -2,6 +2,7 @@ import java.text.BreakIterator; import java.text.CharacterIterator; +import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -50,6 +51,7 @@ public class LineBuilder implements Consumer { private LineElement openLineTail; private final TextAlignment alignment; private final StringBuilder lineBuffer = new StringBuilder(); + private float[] lineBufferPrefixWidths = new float[64]; /** Reusable CharacterIterator wrapping {@link #lineBuffer} to avoid String allocation in BreakIterator. */ private final StringBuilderCharIterator charIterator = new StringBuilderCharIterator(); @@ -208,6 +210,7 @@ private void appendText(String text, LytFlowContent flowContent) { final var finalStyle = style; final var finalRevealStyle = revealStyle; final var finalHoverStyle = hoverStyle; + final boolean inlineCode = finalStyle.inlineCode(); char lastChar = '\0'; var endOfOpenLine = getEndOfOpenLine(); @@ -224,8 +227,8 @@ private void appendText(String text, LytFlowContent flowContent) { el.flowContent = flowContent; int w = Math.round(width); int h = context.getLineHeight(finalStyle); - if (finalStyle.inlineCode()) { - w += LineTextRun.INLINE_CODE_PAD_X * 2; + if (inlineCode) { + w += LineTextRun.INLINE_CODE_EXTRA_WIDTH; } el.bounds = new LytRect(innerX, 0, w, h); appendToOpenLine(el); @@ -261,6 +264,7 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh float curLineWidth = 0; lineBuffer.setLength(0); + lineBufferPrefixWidths[0] = 0f; // Hoist whitespace mode flags out of the per-character loop to avoid repeated method calls. boolean collapseSegmentBreaks = style.whiteSpace() @@ -292,6 +296,7 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh } else { consumer.visitRun(lineBuffer, curLineWidth, true); lineBuffer.setLength(0); + lineBufferPrefixWidths[0] = 0f; lastCharWasWhitespace = true; remainingLineWidth = getAvailableHorizontalSpace(); continue; @@ -332,32 +337,21 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh // current word does not offer us any opportunity to. if (precedingBreakOpportunity > 0 || precedingBreakOpportunity == 0 && canBreakAtStart) { // Determine width up until the break opportunity. - // Short-circuit: when breaking at end-of-buffer (whitespace case), curLineWidth - // already equals the total, so skip the O(N) recomputation loop. - float widthAtBreakOpportunity; - if (precedingBreakOpportunity == lineBuffer.length()) { - widthAtBreakOpportunity = curLineWidth; - } else { - widthAtBreakOpportunity = 0f; - for (var j = 0; j < precedingBreakOpportunity; j++) { - widthAtBreakOpportunity += context.getAdvance(lineBuffer.charAt(j), style); - } - } + float widthAtBreakOpportunity = lineBufferPrefixWidths[precedingBreakOpportunity]; consumer .visitRun(lineBuffer.subSequence(0, precedingBreakOpportunity), widthAtBreakOpportunity, true); curLineWidth -= widthAtBreakOpportunity; - lineBuffer.delete(0, precedingBreakOpportunity); + deleteLineBufferPrefix(precedingBreakOpportunity); if (!lineBuffer.isEmpty() && Character.isWhitespace(lineBuffer.charAt(0))) { - var firstChar = lineBuffer.charAt(0); - lineBuffer.deleteCharAt(0); - curLineWidth -= context.getAdvance(firstChar, style); + curLineWidth -= deleteLineBufferPrefix(1); } } else { // We exceeded the line length, but did not find a break opportunity // this causes a forced break mid-word consumer.visitRun(lineBuffer, curLineWidth, true); lineBuffer.setLength(0); + lineBufferPrefixWidths[0] = 0f; curLineWidth = 0; } remainingLineWidth = getAvailableHorizontalSpace(); @@ -368,7 +362,7 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh } } curLineWidth += advance; - lineBuffer.appendCodePoint(codePoint); + appendCodePointToLineBuffer(codePoint, advance); } if (!lineBuffer.isEmpty()) { @@ -376,6 +370,45 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh } } + private void appendCodePointToLineBuffer(int codePoint, float advance) { + int previousLength = lineBuffer.length(); + float bufferWidth = lineBufferPrefixWidths[previousLength] + advance; + lineBuffer.appendCodePoint(codePoint); + int newLength = lineBuffer.length(); + ensureLineBufferPrefixCapacity(newLength + 1); + for (int i = previousLength + 1; i <= newLength; i++) { + lineBufferPrefixWidths[i] = bufferWidth; + } + } + + private float deleteLineBufferPrefix(int charCount) { + if (charCount <= 0) { + return 0f; + } + float removedWidth = lineBufferPrefixWidths[charCount]; + int remainingChars = lineBuffer.length() - charCount; + if (remainingChars > 0) { + System.arraycopy(lineBufferPrefixWidths, charCount + 1, lineBufferPrefixWidths, 1, remainingChars); + for (int i = 1; i <= remainingChars; i++) { + lineBufferPrefixWidths[i] -= removedWidth; + } + } + lineBuffer.delete(0, charCount); + lineBufferPrefixWidths[0] = 0f; + return removedWidth; + } + + private void ensureLineBufferPrefixCapacity(int requiredLength) { + if (requiredLength <= lineBufferPrefixWidths.length) { + return; + } + int newLength = lineBufferPrefixWidths.length; + while (newLength < requiredLength) { + newLength <<= 1; + } + lineBufferPrefixWidths = Arrays.copyOf(lineBufferPrefixWidths, newLength); + } + private void endLine() { if (openLineElement == null) { return; @@ -543,7 +576,7 @@ public LytRect getBounds() { } @FunctionalInterface - interface LineConsumer { + private interface LineConsumer { void visitRun(CharSequence run, float width, boolean end); } @@ -552,7 +585,7 @@ interface LineConsumer { * A {@link CharacterIterator} that wraps a {@link StringBuilder} without copying it to a {@link String}. * Reuse by calling {@link #reset} before each use. */ - private static final class StringBuilderCharIterator implements CharacterIterator { + private static class StringBuilderCharIterator implements CharacterIterator { private StringBuilder sb; private int begin; diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java index 25c434fd..c4091d84 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineTextRun.java @@ -1,13 +1,17 @@ package com.hfstudio.guidenh.guide.layout.flow; -import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.color.ColorValue; +import com.hfstudio.guidenh.guide.color.LightDarkMode; +import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; public class LineTextRun extends LineElement { public static final int INLINE_CODE_PAD_X = 3; - public static final ConstantColor INLINE_CODE_BACKGROUND = new ConstantColor(0x1AF0F6FF, 0x1A6FB6FF); + public static final int INLINE_CODE_EXTRA_WIDTH = INLINE_CODE_PAD_X * 2; + public static final int INLINE_CODE_BACKGROUND_LIGHT = 0x1AF0F6FF; + public static final int INLINE_CODE_BACKGROUND_DARK = 0x1A6FB6FF; public final String text; public final ResolvedTextStyle style; @@ -24,22 +28,29 @@ public LineTextRun(String text, ResolvedTextStyle style, ResolvedTextStyle revea @Override public void render(RenderContext context) { - var style = containsMouse ? hoverStyle : revealedBySpoiler ? revealStyle : this.style; - int backgroundHeight = Math.max(1, bounds.height()); - int backgroundY = bounds.y() - 1; - if (style.inlineCode() && bounds.width() > 0 && bounds.height() > 0) { - context.fillRect( - bounds.x(), - backgroundY, - bounds.width(), - backgroundHeight, - context.resolveColor(INLINE_CODE_BACKGROUND)); - } else if (style.backgroundColor() != null && bounds.width() > 0 && bounds.height() > 0) { - context - .fillRect(bounds.x() - 1, backgroundY, bounds.width() + 2, backgroundHeight, style.backgroundColor()); + var resolvedStyle = containsMouse ? hoverStyle : revealedBySpoiler ? revealStyle : this.style; + boolean inlineCode = resolvedStyle.inlineCode(); + ColorValue backgroundColor = resolvedStyle.backgroundColor(); + if (!inlineCode && backgroundColor == null) { + context.drawText(text, bounds.x(), bounds.y(), resolvedStyle); + return; } - int textX = style.inlineCode() ? bounds.x() + INLINE_CODE_PAD_X : bounds.x(); - context.drawText(text, textX, bounds.y(), style); + LytRect rect = bounds; + int width = rect.width(); + int height = rect.height(); + if (width > 0 && height > 0) { + int backgroundY = rect.y() - 1; + if (inlineCode) { + int backgroundColorArgb = context.lightDarkMode() == LightDarkMode.DARK_MODE + ? INLINE_CODE_BACKGROUND_DARK + : INLINE_CODE_BACKGROUND_LIGHT; + context.fillRect(rect.x(), backgroundY, width, height, backgroundColorArgb); + } else { + context.fillRect(rect.x() - 1, backgroundY, width + 2, height, backgroundColor); + } + } + int textX = inlineCode ? rect.x() + INLINE_CODE_PAD_X : rect.x(); + context.drawText(text, textX, rect.y(), resolvedStyle); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java index 7aac7ae8..8fbb019c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java @@ -24,6 +24,10 @@ public class VanillaRenderContext implements RenderContext { public static final RenderItem ITEM_RENDERER = new RenderItem(); + private static final String FORMAT_BOLD = "\u00a7l"; + private static final String FORMAT_ITALIC = "\u00a7o"; + private static final String FORMAT_STRIKETHROUGH = "\u00a7m"; + private static final String FORMAT_OBFUSCATED = "\u00a7k"; private final FontRenderer fontRenderer; private int screenHeight; @@ -157,10 +161,10 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { if (style.bold() || style.italic() || style.strikethrough() || style.obfuscated()) { sb = textStyleBuffer; sb.setLength(0); - if (style.bold()) sb.append("\u00a7l"); - if (style.italic()) sb.append("\u00a7o"); - if (style.strikethrough()) sb.append("\u00a7m"); - if (style.obfuscated()) sb.append("\u00a7k"); + if (style.bold()) sb.append(FORMAT_BOLD); + if (style.italic()) sb.append(FORMAT_ITALIC); + if (style.strikethrough()) sb.append(FORMAT_STRIKETHROUGH); + if (style.obfuscated()) sb.append(FORMAT_OBFUSCATED); } String drawn = sb != null ? sb.append(text) .toString() : text; @@ -183,24 +187,30 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { fontRenderer.drawString(drawn, x, y, color); } + boolean hasUnderline = style.underlined(); + boolean hasWavyUnderline = style.wavyUnderline(); + boolean hasDottedUnderline = style.dottedUnderline(); + if (!hasUnderline && !hasWavyUnderline && !hasDottedUnderline) { + return; + } + + int scaledFontHeight = Math.round(fontRenderer.FONT_HEIGHT * scale); + int decorationY = y + scaledFontHeight - 1; int decoratedWidth = -1; if (style.underlined()) { decoratedWidth = Math.round(fontRenderer.getStringWidth(drawn) * scale); - int uy = y + Math.round((fontRenderer.FONT_HEIGHT) * scale) - 1; - Gui.drawRect(x, uy, x + decoratedWidth, uy + 1, color); + Gui.drawRect(x, decorationY, x + decoratedWidth, decorationY + 1, color); } if (style.wavyUnderline()) { int w = decoratedWidth >= 0 ? decoratedWidth : Math.round(fontRenderer.getStringWidth(drawn) * scale); - int baseY = y + Math.round((fontRenderer.FONT_HEIGHT) * scale) - 1; // Draw a 2px-tall sine-like zig-zag using 1x1 rects: pattern of 4 px period. for (int i = 0; i < w; i++) { int phase = i & 3; // 0,1,2,3 int dy = (phase == 0 || phase == 2) ? 0 : (phase == 1 ? -1 : 1); - Gui.drawRect(x + i, baseY + dy, x + i + 1, baseY + dy + 1, color); + Gui.drawRect(x + i, decorationY + dy, x + i + 1, decorationY + dy + 1, color); } } if (style.dottedUnderline()) { - int dy = y + Math.round((fontRenderer.FONT_HEIGHT) * scale) - 1; // Center a single 2x2 dot under each rendered character cell. int cursor = 0; int len = drawn.length(); @@ -216,7 +226,7 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { continue; } int dotX = x + cursor + Math.max(0, (cw - 2) / 2); - Gui.drawRect(dotX, dy, dotX + 2, dy + 2, color); + Gui.drawRect(dotX, decorationY, dotX + 2, decorationY + 2, color); cursor += cw; } } From ed2ba56cbf13d3ba3f31d53eaa3d4be0281f6e88 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:52:15 +0800 Subject: [PATCH 101/136] Update GuideScreen.java --- .../com/hfstudio/guidenh/guide/internal/GuideScreen.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 19ccd718..26f50097 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -5746,6 +5746,7 @@ public static boolean isShiftDown() { @Override protected void keyTyped(char typedChar, int keyCode) { + if (handleDebugHudToggleKey(keyCode)) return; if (GuideScreenNeiBridge.keyTyped(this, typedChar, keyCode)) return; if (handleSearchFieldKey(typedChar, keyCode)) return; if (handleSpecialSearchFieldKey(typedChar, keyCode)) return; @@ -5791,6 +5792,14 @@ protected void keyTyped(char typedChar, int keyCode) { super.keyTyped(typedChar, keyCode); } + private boolean handleDebugHudToggleKey(int keyCode) { + if (keyCode != Keyboard.KEY_F3 || !ModConfig.debug.enableDebugMode || mc == null || mc.gameSettings == null) { + return false; + } + mc.gameSettings.showDebugInfo = !mc.gameSettings.showDebugInfo; + return true; + } + private boolean isInsideDocument(int mouseX, int mouseY) { if (isInsideSpecialSearchField(mouseX, mouseY)) { return false; From 6c6fbabc43d5d88d3298cba372d80ab7961e3dc1 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:52:15 +0800 Subject: [PATCH 102/136] Update GuideScreen.java --- .../com/hfstudio/guidenh/guide/internal/GuideScreen.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 19ccd718..26f50097 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -5746,6 +5746,7 @@ public static boolean isShiftDown() { @Override protected void keyTyped(char typedChar, int keyCode) { + if (handleDebugHudToggleKey(keyCode)) return; if (GuideScreenNeiBridge.keyTyped(this, typedChar, keyCode)) return; if (handleSearchFieldKey(typedChar, keyCode)) return; if (handleSpecialSearchFieldKey(typedChar, keyCode)) return; @@ -5791,6 +5792,14 @@ protected void keyTyped(char typedChar, int keyCode) { super.keyTyped(typedChar, keyCode); } + private boolean handleDebugHudToggleKey(int keyCode) { + if (keyCode != Keyboard.KEY_F3 || !ModConfig.debug.enableDebugMode || mc == null || mc.gameSettings == null) { + return false; + } + mc.gameSettings.showDebugInfo = !mc.gameSettings.showDebugInfo; + return true; + } + private boolean isInsideDocument(int mouseX, int mouseY) { if (isInsideSpecialSearchField(mouseX, mouseY)) { return false; From ef61c451e78424d8db3c29e69339a04f2f670438 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:49:04 +0800 Subject: [PATCH 103/136] add GuideScreenScrollbarOutline --- .../guidenh/guide/internal/GuideScreen.java | 155 ++++++-- .../internal/GuideScreenScrollbarOutline.java | 346 ++++++++++++++++++ 2 files changed, 480 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 26f50097..e3f30f2a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -55,6 +55,7 @@ import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.GuidePageIcon; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.color.Colors; import com.hfstudio.guidenh.guide.color.LightDarkMode; import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.AnchorIndexer; @@ -132,6 +133,7 @@ import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.render.VanillaRenderContext; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; +import com.hfstudio.guidenh.guide.scene.annotation.DiamondAnnotation; import com.hfstudio.guidenh.guide.scene.support.GuideBlockDisplayResolver; import com.hfstudio.guidenh.guide.scene.support.GuideEntityDisplayResolver; import com.hfstudio.guidenh.guide.sound.GuideSoundPlayback; @@ -223,6 +225,10 @@ public class GuideScreen extends GuiContainer private static final int GUIDE_EDITOR_TOOLBAR_H = 16; private static final int GUIDE_EDITOR_MIN_SPLIT_PANE_W = 15; private static final int SCROLLBAR_W = SceneEditorMultilineTextArea.SCROLLBAR_SIZE; + private static final int SCROLLBAR_OUTLINE_LABEL_MAX_WIDTH = 132; + private static final int SCROLLBAR_OUTLINE_LABEL_PADDING_X = 5; + private static final int SCROLLBAR_OUTLINE_LABEL_PADDING_Y = 4; + private static final int SCROLLBAR_OUTLINE_LABEL_GAP = 6; private static final int GUIDE_EDITOR_DIVIDER_HOVER_DELAY_MILLIS = 1000; private static final long GUIDE_EDITOR_SAFETY_AUTOSAVE_INTERVAL_MILLIS = 5L * 60L * 1000L; private static final long GUIDE_EDITOR_NAVIGATION_REFRESH_DELAY_MILLIS = 1500L; @@ -241,6 +247,7 @@ public class GuideScreen extends GuiContainer private final MinecraftFontMetrics layoutFontMetrics = new MinecraftFontMetrics(); private final CodeBlockClipboardService codeBlockClipboardService = new CodeBlockClipboardService(); private final GuideDebugOverlayRenderer debugOverlayRenderer = new GuideDebugOverlayRenderer(); + private final GuideScreenScrollbarOutline scrollbarOutline = new GuideScreenScrollbarOutline(); private final GuideScreenEditorFileStore guideEditorFileStore = GuideScreenEditorFileStore.createDefault(); private final Map guideEditorActionButtons = new LinkedHashMap<>(); @@ -632,6 +639,7 @@ public void reloadPage() { currentPage = null; document = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); if (currentAnchor != null) { ClientProxy.getLytHost() .invalidatePage( @@ -699,6 +707,7 @@ private void restoreViewState(GuideScreenViewState state) { document = null; layoutDocument = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); scrollY = 0; loadCurrentPage(); ensureLayout(); @@ -758,6 +767,7 @@ private void toggleNavigationPinned() { rebuildToolbar(); layoutDocument = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); ensureLayout(); clampScroll(); } @@ -2347,6 +2357,7 @@ private void completePendingContentPageLoadIfNeeded() { } currentPage = loadedPage; document = loadedPage != null ? loadedPage.document() : null; + invalidateScrollbarOutline(); lytHost.setCurrentPageId(pageIdStr); lytHost.setCurrentPageCollection(guide); lytHost.mountDocument(document); @@ -2436,6 +2447,7 @@ private void ensureLayout() { layoutDocument = activeDocument; lastLayoutWidth = layoutWidth; lastLayoutVisualScalePermille = layoutVisualScalePermille; + invalidateScrollbarOutline(); } } @@ -2573,6 +2585,7 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { currentZoom = resolveCurrentZoom(); ensureLayout(); clampScroll(); + updateScrollbarOutlineHover(mouseX, mouseY); int navX = panelX; int navY = panelY + TOOLBAR_H + 1; @@ -4455,31 +4468,42 @@ private void drawBorder(int x, int y, int w, int h, int color) { } private void drawScrollbar() { - int barW = SCROLLBAR_W; - int barX = panelX + panelW - 1 - barW; - int barY = getDocumentViewportY(); - int barH = getDocumentViewportHeight(); - drawRect(barX, barY, barX + barW, barY + barH, 0x40FFFFFF); - - int total = getContentHeight(); - int viewportH = getDocumentViewportHeight(); - int thumbH = Math.max(16, (int) ((long) barH * viewportH / Math.max(1, total))); - int maxScroll = getMaxScroll(); - int thumbY = maxScroll > 0 ? barY + (int) ((long) (barH - thumbH) * scrollY / maxScroll) : barY; + var bounds = scrollbarBounds(); + drawRect(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height(), 0x40FFFFFF); + var renderState = scrollbarOutline.update( + currentPage, + getActiveDocument(), + bounds, + currentZoom, + scrollY, + lastMouseX, + lastMouseY, + System.currentTimeMillis(), + mc.fontRenderer, + SCROLLBAR_OUTLINE_LABEL_MAX_WIDTH, + SCROLLBAR_OUTLINE_LABEL_GAP, + panelX, + panelX + panelW); + drawScrollbarOutlineMarkers(renderState); + + int thumbH = Math + .max(16, (int) ((long) bounds.height() * bounds.viewportHeight() / Math.max(1, bounds.contentHeight()))); + int thumbY = bounds.maxScroll() > 0 + ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * scrollY / bounds.maxScroll()) + : bounds.y(); int thumbColor = draggingScrollbar ? 0xFFFFFFFF : 0xFFCCCCCC; - drawRect(barX, thumbY, barX + barW, thumbY + thumbH, thumbColor); + drawRect(bounds.x(), thumbY, bounds.x() + bounds.width(), thumbY + thumbH, thumbColor); + drawScrollbarOutlineLabel(renderState, mc.fontRenderer); } private int[] scrollbarThumbRect() { - int barX = panelX + panelW - 1 - SCROLLBAR_W; - int barY = getDocumentViewportY(); - int barH = getDocumentViewportHeight(); - int total = getContentHeight(); - int viewportH = getDocumentViewportHeight(); - int thumbH = Math.max(16, (int) ((long) barH * viewportH / Math.max(1, total))); - int maxScroll = getMaxScroll(); - int thumbY = maxScroll > 0 ? barY + (int) ((long) (barH - thumbH) * scrollY / maxScroll) : barY; - return new int[] { barX, thumbY, SCROLLBAR_W, thumbH, barY, barH }; + var bounds = scrollbarBounds(); + int thumbH = Math + .max(16, (int) ((long) bounds.height() * bounds.viewportHeight() / Math.max(1, bounds.contentHeight()))); + int thumbY = bounds.maxScroll() > 0 + ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * scrollY / bounds.maxScroll()) + : bounds.y(); + return new int[] { bounds.x(), thumbY, bounds.width(), thumbH, bounds.y(), bounds.height() }; } private void updateScrollFromMouseY(int mouseY) { @@ -4745,6 +4769,12 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { draggingScrollbar = true; return; } + Integer markerTarget = scrollbarOutline.findJumpTarget(mouseX, mouseY); + if (markerTarget != null) { + scrollY = markerTarget.intValue(); + clampScroll(); + return; + } } var activeDocument = getActiveDocument(); if (activeDocument != null && isInsideDocument(mouseX, mouseY)) { @@ -6567,6 +6597,7 @@ private void updateSearchQuery(String query) { refreshCurrentPageTitle(); rebuildSearchDocumentIfNeeded(true); scrollY = 0; + invalidateScrollbarOutline(); rebuildToolbar(); syncSearchFieldsToCurrentRoute(); } @@ -6599,6 +6630,7 @@ private void applySpecialPageSearchQuery(String query) { clearInteractionState(); layoutDocument = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); clampScroll(); } @@ -6614,6 +6646,87 @@ private void updateSpecialSearchBlocks(LytNode root, String query) { } } + private GuideScreenScrollbarOutline.ScrollbarBounds scrollbarBounds() { + return new GuideScreenScrollbarOutline.ScrollbarBounds( + panelX + panelW - SCROLLBAR_W, + getDocumentViewportY(), + SCROLLBAR_W, + getDocumentViewportHeight(), + getContentHeight(), + getDocumentViewportHeight(), + getMaxScroll()); + } + + private void updateScrollbarOutlineHover(int mouseX, int mouseY) { + if (getMaxScroll() <= 0 || isGuideEditorActive() || isHomeRoute()) { + scrollbarOutline.clearHover(); + return; + } + scrollbarOutline.update( + currentPage, + getActiveDocument(), + scrollbarBounds(), + currentZoom, + scrollY, + mouseX, + mouseY, + System.currentTimeMillis(), + null, + SCROLLBAR_OUTLINE_LABEL_MAX_WIDTH, + SCROLLBAR_OUTLINE_LABEL_GAP, + panelX, + panelX + panelW); + } + + private void drawScrollbarOutlineMarkers(GuideScreenScrollbarOutline.RenderState renderState) { + for (int index = 0; index < renderState.entries() + .size(); index++) { + var entry = renderState.entries() + .get(index); + int color = index == renderState.activeIndex() ? withAlpha(lighten(entry.colorArgb(), 12), 220) + : withAlpha(entry.colorArgb(), 140); + drawRect( + entry.markerX(), + entry.markerY(), + entry.markerX() + entry.markerWidth(), + entry.markerY() + entry.markerHeight(), + color); + } + } + + private void drawScrollbarOutlineLabel(GuideScreenScrollbarOutline.RenderState renderState, + FontRenderer fontRenderer) { + var label = renderState.labelLayout(); + if (label == null || label.lines() + .isEmpty()) { + return; + } + int bubbleWidth = label.width() + SCROLLBAR_OUTLINE_LABEL_PADDING_X * 2; + int bubbleHeight = label.height() + SCROLLBAR_OUTLINE_LABEL_PADDING_Y * 2; + int background = Colors.argb(label.alpha(), 16, 16, 16); + int border = Colors.argb(label.alpha(), 216, 216, 216); + drawRect(label.x(), label.y(), label.x() + bubbleWidth, label.y() + bubbleHeight, background); + drawBorder(label.x(), label.y(), bubbleWidth, bubbleHeight, border); + int textY = label.y() + SCROLLBAR_OUTLINE_LABEL_PADDING_Y; + for (String line : label.lines()) { + fontRenderer.drawStringWithShadow(line, label.x() + SCROLLBAR_OUTLINE_LABEL_PADDING_X, textY, 0xFFFFFF); + textY += fontRenderer.FONT_HEIGHT; + } + } + + private int withAlpha(int argb, int alpha) { + int clampedAlpha = Math.max(0, Math.min(255, alpha)); + return (argb & 0x00FFFFFF) | (clampedAlpha << 24); + } + + private int lighten(int argb, int percent) { + return DiamondAnnotation.lighten(argb, percent); + } + + private void invalidateScrollbarOutline() { + scrollbarOutline.invalidateLayout(); + } + private void recordHomeHistoryIfEligible() { if (currentRoute == null || !currentRoute.isContent() || currentAnchor == null diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java new file mode 100644 index 00000000..07d34ffd --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java @@ -0,0 +1,346 @@ +package com.hfstudio.guidenh.guide.internal; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import net.minecraft.client.gui.FontRenderer; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.GuidePage; +import com.hfstudio.guidenh.guide.color.ColorValue; +import com.hfstudio.guidenh.guide.color.LightDarkMode; +import com.hfstudio.guidenh.guide.document.DefaultStyles; +import com.hfstudio.guidenh.guide.document.block.LytDocument; +import com.hfstudio.guidenh.guide.document.block.LytHeading; +import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; + +public class GuideScreenScrollbarOutline { + + public static final long HOVER_DELAY_MILLIS = 500L; + + private static final int H1_MARKER_WIDTH = 10; + private static final int H2_MARKER_WIDTH = 7; + private static final int MARKER_HEIGHT = 2; + private static final int HIT_PADDING_X = 2; + private static final int HIT_PADDING_Y = 2; + private static final int LABEL_MAX_LINES = 2; + private static final String ELLIPSIS = "..."; + + private List anchors = List.of(); + private List entries = List.of(); + private int lastLayoutSignature; + @Nullable + private HoverState hoverState; + + public record ScrollbarBounds(int x, int y, int width, int height, int contentHeight, int viewportHeight, + int maxScroll) {} + + public record HeadingEntry(String text, int depth, int documentY, int colorArgb, int markerX, int markerY, + int markerWidth, int markerHeight, int hitX, int hitY, int hitWidth, int hitHeight) {} + + public record LabelLayout(List lines, int x, int y, int width, int height, int alpha) {} + + public record RenderState(List entries, int activeIndex, @Nullable Integer hoveredIndex, + @Nullable LabelLayout labelLayout) {} + + public void invalidateLayout() { + lastLayoutSignature = 0; + entries = List.of(); + anchors = List.of(); + hoverState = null; + } + + public void clearHover() { + hoverState = null; + } + + public RenderState update(@Nullable GuidePage page, @Nullable LytDocument document, ScrollbarBounds bounds, + float zoom, int viewportTopDocumentY, int mouseX, int mouseY, long nowMillis, + @Nullable FontRenderer fontRenderer, int labelMaxWidth, int labelGap, int panelLeft, int panelRight) { + rebuildIfNeeded(page, document, bounds, zoom, viewportTopDocumentY); + updateHoverCandidate(mouseX, mouseY, nowMillis); + int activeIndex = resolveActiveIndex(viewportTopDocumentY); + Integer hoveredIndex = hoverState != null ? hoverState.index() : null; + LabelLayout labelLayout = hoveredIndex != null && fontRenderer != null ? buildLabelLayout( + entries.get(hoveredIndex), + fontRenderer, + nowMillis, + labelMaxWidth, + labelGap, + panelLeft, + panelRight) : null; + return new RenderState(entries, activeIndex, hoveredIndex, labelLayout); + } + + public @Nullable Integer findJumpTarget(int mouseX, int mouseY) { + for (var entry : entries) { + if (contains(entry, mouseX, mouseY)) { + clearHover(); + return entry.documentY(); + } + } + return null; + } + + public int resolveActiveIndex(int viewportTopDocumentY) { + int activeIndex = -1; + for (int index = 0; index < entries.size(); index++) { + var entry = entries.get(index); + if (entry.documentY() > viewportTopDocumentY) { + break; + } + activeIndex = index; + } + return activeIndex >= 0 ? activeIndex : entries.isEmpty() ? -1 : 0; + } + + public void updateHoverCandidateForTest(int mouseX, int mouseY, long nowMillis) { + updateHoverCandidate(mouseX, mouseY, nowMillis); + } + + public void setEntriesForTest(List testEntries) { + entries = List.copyOf(testEntries); + } + + public void setAnchorsForTest(List testAnchors) { + anchors = List.copyOf(testAnchors); + } + + public HeadingEntry testEntry(String text, int depth, int documentY) { + return testEntry(text, depth, documentY, 0xFFFFFFFF, 100, 50, markerWidth(depth), MARKER_HEIGHT, 98, 48, 16, 6); + } + + public HeadingEntry testEntry(String text, int depth, int documentY, int colorArgb, int markerX, int markerY, + int markerWidth, int markerHeight, int hitX, int hitY, int hitWidth, int hitHeight) { + return new HeadingEntry( + text, + depth, + documentY, + colorArgb, + markerX, + markerY, + markerWidth, + markerHeight, + hitX, + hitY, + hitWidth, + hitHeight); + } + + public OutlineAnchor testAnchor(String text, int depth, int documentY, int colorArgb) { + return new OutlineAnchor(text, depth, documentY, colorArgb); + } + + public RenderState rebuildFromAnchorsForTest(ScrollbarBounds bounds) { + entries = mapAnchorsToEntries(anchors, bounds); + return new RenderState(entries, -1, null, null); + } + + @Nullable + public LabelLayout visibleHoverLabelForTest(long nowMillis) { + if (hoverState == null || hoverState.index() < 0 || hoverState.index() >= entries.size()) { + return null; + } + if (nowMillis - hoverState.startedAtMillis() < HOVER_DELAY_MILLIS) { + return null; + } + return new LabelLayout( + List.of( + entries.get(hoverState.index()) + .text()), + 0, + 0, + 0, + 0, + 255); + } + + private void rebuildIfNeeded(@Nullable GuidePage page, @Nullable LytDocument document, ScrollbarBounds bounds, + float zoom, int viewportTopDocumentY) { + int layoutSignature = Objects.hash( + page, + page != null ? page.titleHeading() : null, + document, + bounds.x(), + bounds.y(), + bounds.width(), + bounds.height(), + bounds.contentHeight(), + bounds.viewportHeight(), + bounds.maxScroll(), + viewportTopDocumentY, + Float.floatToIntBits(zoom)); + if (layoutSignature == lastLayoutSignature) { + return; + } + anchors = collectAnchors(page, document); + entries = mapAnchorsToEntries(anchors, bounds); + lastLayoutSignature = layoutSignature; + if (hoverState != null) { + if (hoverState.index() < 0 || hoverState.index() >= entries.size()) { + hoverState = null; + } else { + var previous = hoverState; + hoverState = new HoverState(previous.index(), previous.startedAtMillis()); + } + } + } + + private List collectAnchors(@Nullable GuidePage page, @Nullable LytDocument document) { + if (document == null || !document.hasLayout()) { + return List.of(); + } + List collected = new ArrayList<>(); + for (var block : document.getBlocks()) { + if (block instanceof LytHeading heading && heading.getBounds() != null) { + int depth = heading.getDepth(); + if (depth >= 1 && depth <= 2) { + String headingText = heading.getTextContent(); + if (!headingText.isEmpty()) { + collected.add( + new OutlineAnchor( + headingText, + depth, + heading.getBounds() + .y(), + resolveHeadingColor(depth))); + } + } + } + } + collected.sort( + Comparator.comparingInt(OutlineAnchor::documentY) + .thenComparingInt(OutlineAnchor::depth)); + return collected; + } + + private List mapAnchorsToEntries(List anchorEntries, ScrollbarBounds bounds) { + if (anchorEntries.isEmpty() || bounds.height() <= 0) { + return List.of(); + } + int track = Math.max(1, bounds.height() - MARKER_HEIGHT); + int barRight = bounds.x() + bounds.width(); + List mapped = new ArrayList<>(anchorEntries.size()); + for (var anchor : anchorEntries) { + int markerWidth = markerWidth(anchor.depth()); + int clampedDocumentY = Math.max(0, Math.min(anchor.documentY(), bounds.maxScroll())); + int markerY = bounds.y() + + (bounds.maxScroll() > 0 ? (int) ((long) track * clampedDocumentY / bounds.maxScroll()) : 0); + int markerX = barRight - markerWidth - 1; + mapped.add( + new HeadingEntry( + anchor.text(), + anchor.depth(), + anchor.documentY(), + anchor.colorArgb(), + markerX, + markerY, + markerWidth, + MARKER_HEIGHT, + markerX - HIT_PADDING_X, + markerY - HIT_PADDING_Y, + markerWidth + HIT_PADDING_X * 2, + MARKER_HEIGHT + HIT_PADDING_Y * 2)); + } + return mapped; + } + + private void updateHoverCandidate(int mouseX, int mouseY, long nowMillis) { + Integer targetIndex = nearestHoveredIndex(mouseX, mouseY); + if (targetIndex == null) { + hoverState = null; + return; + } + if (hoverState != null && hoverState.index() == targetIndex) { + return; + } + hoverState = new HoverState(targetIndex, nowMillis); + } + + private @Nullable Integer nearestHoveredIndex(int mouseX, int mouseY) { + Integer nearestIndex = null; + int nearestDistance = Integer.MAX_VALUE; + for (int index = 0; index < entries.size(); index++) { + var entry = entries.get(index); + if (!contains(entry, mouseX, mouseY)) { + continue; + } + int centerY = entry.hitY() + entry.hitHeight() / 2; + int distance = Math.abs(mouseY - centerY); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestIndex = index; + } + } + return nearestIndex; + } + + private @Nullable LabelLayout buildLabelLayout(HeadingEntry entry, FontRenderer fontRenderer, long nowMillis, + int labelMaxWidth, int labelGap, int panelLeft, int panelRight) { + if (hoverState == null || nowMillis - hoverState.startedAtMillis() < HOVER_DELAY_MILLIS) { + return null; + } + List wrappedLines = fontRenderer.listFormattedStringToWidth(entry.text(), labelMaxWidth); + if (wrappedLines.isEmpty()) { + wrappedLines = List.of(entry.text()); + } + List lines = new ArrayList<>(wrappedLines.subList(0, Math.min(LABEL_MAX_LINES, wrappedLines.size()))); + if (wrappedLines.size() > LABEL_MAX_LINES && !lines.isEmpty()) { + int lastIndex = lines.size() - 1; + lines.set(lastIndex, truncateLineWithEllipsis(fontRenderer, lines.get(lastIndex), labelMaxWidth)); + } + int width = 0; + for (String line : lines) { + width = Math.max(width, fontRenderer.getStringWidth(line)); + } + width = Math.min(labelMaxWidth, width); + int height = lines.size() * fontRenderer.FONT_HEIGHT; + int x = Math.max(panelLeft + 4, entry.markerX() - labelGap - width - 10); + x = Math.min(x, panelRight - width - 4); + int y = entry.markerY() - height / 2; + return new LabelLayout(List.copyOf(lines), x, y, width, height, 255); + } + + private String truncateLineWithEllipsis(FontRenderer fontRenderer, String line, int maxWidth) { + if (fontRenderer.getStringWidth(line) <= maxWidth) { + return line; + } + int ellipsisWidth = fontRenderer.getStringWidth(ELLIPSIS); + String trimmed = line; + while (!trimmed.isEmpty() && fontRenderer.getStringWidth(trimmed) + ellipsisWidth > maxWidth) { + trimmed = trimmed.substring(0, trimmed.length() - 1); + } + return trimmed + ELLIPSIS; + } + + private boolean contains(HeadingEntry entry, int mouseX, int mouseY) { + return mouseX >= entry.hitX() && mouseX < entry.hitX() + entry.hitWidth() + && mouseY >= entry.hitY() + && mouseY < entry.hitY() + entry.hitHeight(); + } + + private int resolveHeadingColor(int depth) { + ResolvedTextStyle baseStyle = DefaultStyles.BASE_STYLE; + ResolvedTextStyle headingStyle = switch (depth) { + case 1 -> DefaultStyles.HEADING1.mergeWith(baseStyle); + case 2 -> DefaultStyles.HEADING2.mergeWith(baseStyle); + default -> baseStyle; + }; + ColorValue colorValue = headingStyle.color() != null ? headingStyle.color() : baseStyle.color(); + return colorValue.resolve(LightDarkMode.LIGHT_MODE); + } + + private static int markerWidth(int depth) { + return switch (depth) { + case 1 -> H1_MARKER_WIDTH; + default -> H2_MARKER_WIDTH; + }; + } + + public record OutlineAnchor(String text, int depth, int documentY, int colorArgb) {} + + record HoverState(int index, long startedAtMillis) {} +} From d10531c2cddf89c687236633dc0f6d9d7367e1d2 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:49:04 +0800 Subject: [PATCH 104/136] add GuideScreenScrollbarOutline --- .../guidenh/guide/internal/GuideScreen.java | 155 ++++++-- .../internal/GuideScreenScrollbarOutline.java | 346 ++++++++++++++++++ 2 files changed, 480 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 26f50097..e3f30f2a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -55,6 +55,7 @@ import com.hfstudio.guidenh.guide.GuidePage; import com.hfstudio.guidenh.guide.GuidePageIcon; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.color.Colors; import com.hfstudio.guidenh.guide.color.LightDarkMode; import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.AnchorIndexer; @@ -132,6 +133,7 @@ import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.render.VanillaRenderContext; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; +import com.hfstudio.guidenh.guide.scene.annotation.DiamondAnnotation; import com.hfstudio.guidenh.guide.scene.support.GuideBlockDisplayResolver; import com.hfstudio.guidenh.guide.scene.support.GuideEntityDisplayResolver; import com.hfstudio.guidenh.guide.sound.GuideSoundPlayback; @@ -223,6 +225,10 @@ public class GuideScreen extends GuiContainer private static final int GUIDE_EDITOR_TOOLBAR_H = 16; private static final int GUIDE_EDITOR_MIN_SPLIT_PANE_W = 15; private static final int SCROLLBAR_W = SceneEditorMultilineTextArea.SCROLLBAR_SIZE; + private static final int SCROLLBAR_OUTLINE_LABEL_MAX_WIDTH = 132; + private static final int SCROLLBAR_OUTLINE_LABEL_PADDING_X = 5; + private static final int SCROLLBAR_OUTLINE_LABEL_PADDING_Y = 4; + private static final int SCROLLBAR_OUTLINE_LABEL_GAP = 6; private static final int GUIDE_EDITOR_DIVIDER_HOVER_DELAY_MILLIS = 1000; private static final long GUIDE_EDITOR_SAFETY_AUTOSAVE_INTERVAL_MILLIS = 5L * 60L * 1000L; private static final long GUIDE_EDITOR_NAVIGATION_REFRESH_DELAY_MILLIS = 1500L; @@ -241,6 +247,7 @@ public class GuideScreen extends GuiContainer private final MinecraftFontMetrics layoutFontMetrics = new MinecraftFontMetrics(); private final CodeBlockClipboardService codeBlockClipboardService = new CodeBlockClipboardService(); private final GuideDebugOverlayRenderer debugOverlayRenderer = new GuideDebugOverlayRenderer(); + private final GuideScreenScrollbarOutline scrollbarOutline = new GuideScreenScrollbarOutline(); private final GuideScreenEditorFileStore guideEditorFileStore = GuideScreenEditorFileStore.createDefault(); private final Map guideEditorActionButtons = new LinkedHashMap<>(); @@ -632,6 +639,7 @@ public void reloadPage() { currentPage = null; document = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); if (currentAnchor != null) { ClientProxy.getLytHost() .invalidatePage( @@ -699,6 +707,7 @@ private void restoreViewState(GuideScreenViewState state) { document = null; layoutDocument = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); scrollY = 0; loadCurrentPage(); ensureLayout(); @@ -758,6 +767,7 @@ private void toggleNavigationPinned() { rebuildToolbar(); layoutDocument = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); ensureLayout(); clampScroll(); } @@ -2347,6 +2357,7 @@ private void completePendingContentPageLoadIfNeeded() { } currentPage = loadedPage; document = loadedPage != null ? loadedPage.document() : null; + invalidateScrollbarOutline(); lytHost.setCurrentPageId(pageIdStr); lytHost.setCurrentPageCollection(guide); lytHost.mountDocument(document); @@ -2436,6 +2447,7 @@ private void ensureLayout() { layoutDocument = activeDocument; lastLayoutWidth = layoutWidth; lastLayoutVisualScalePermille = layoutVisualScalePermille; + invalidateScrollbarOutline(); } } @@ -2573,6 +2585,7 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { currentZoom = resolveCurrentZoom(); ensureLayout(); clampScroll(); + updateScrollbarOutlineHover(mouseX, mouseY); int navX = panelX; int navY = panelY + TOOLBAR_H + 1; @@ -4455,31 +4468,42 @@ private void drawBorder(int x, int y, int w, int h, int color) { } private void drawScrollbar() { - int barW = SCROLLBAR_W; - int barX = panelX + panelW - 1 - barW; - int barY = getDocumentViewportY(); - int barH = getDocumentViewportHeight(); - drawRect(barX, barY, barX + barW, barY + barH, 0x40FFFFFF); - - int total = getContentHeight(); - int viewportH = getDocumentViewportHeight(); - int thumbH = Math.max(16, (int) ((long) barH * viewportH / Math.max(1, total))); - int maxScroll = getMaxScroll(); - int thumbY = maxScroll > 0 ? barY + (int) ((long) (barH - thumbH) * scrollY / maxScroll) : barY; + var bounds = scrollbarBounds(); + drawRect(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height(), 0x40FFFFFF); + var renderState = scrollbarOutline.update( + currentPage, + getActiveDocument(), + bounds, + currentZoom, + scrollY, + lastMouseX, + lastMouseY, + System.currentTimeMillis(), + mc.fontRenderer, + SCROLLBAR_OUTLINE_LABEL_MAX_WIDTH, + SCROLLBAR_OUTLINE_LABEL_GAP, + panelX, + panelX + panelW); + drawScrollbarOutlineMarkers(renderState); + + int thumbH = Math + .max(16, (int) ((long) bounds.height() * bounds.viewportHeight() / Math.max(1, bounds.contentHeight()))); + int thumbY = bounds.maxScroll() > 0 + ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * scrollY / bounds.maxScroll()) + : bounds.y(); int thumbColor = draggingScrollbar ? 0xFFFFFFFF : 0xFFCCCCCC; - drawRect(barX, thumbY, barX + barW, thumbY + thumbH, thumbColor); + drawRect(bounds.x(), thumbY, bounds.x() + bounds.width(), thumbY + thumbH, thumbColor); + drawScrollbarOutlineLabel(renderState, mc.fontRenderer); } private int[] scrollbarThumbRect() { - int barX = panelX + panelW - 1 - SCROLLBAR_W; - int barY = getDocumentViewportY(); - int barH = getDocumentViewportHeight(); - int total = getContentHeight(); - int viewportH = getDocumentViewportHeight(); - int thumbH = Math.max(16, (int) ((long) barH * viewportH / Math.max(1, total))); - int maxScroll = getMaxScroll(); - int thumbY = maxScroll > 0 ? barY + (int) ((long) (barH - thumbH) * scrollY / maxScroll) : barY; - return new int[] { barX, thumbY, SCROLLBAR_W, thumbH, barY, barH }; + var bounds = scrollbarBounds(); + int thumbH = Math + .max(16, (int) ((long) bounds.height() * bounds.viewportHeight() / Math.max(1, bounds.contentHeight()))); + int thumbY = bounds.maxScroll() > 0 + ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * scrollY / bounds.maxScroll()) + : bounds.y(); + return new int[] { bounds.x(), thumbY, bounds.width(), thumbH, bounds.y(), bounds.height() }; } private void updateScrollFromMouseY(int mouseY) { @@ -4745,6 +4769,12 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { draggingScrollbar = true; return; } + Integer markerTarget = scrollbarOutline.findJumpTarget(mouseX, mouseY); + if (markerTarget != null) { + scrollY = markerTarget.intValue(); + clampScroll(); + return; + } } var activeDocument = getActiveDocument(); if (activeDocument != null && isInsideDocument(mouseX, mouseY)) { @@ -6567,6 +6597,7 @@ private void updateSearchQuery(String query) { refreshCurrentPageTitle(); rebuildSearchDocumentIfNeeded(true); scrollY = 0; + invalidateScrollbarOutline(); rebuildToolbar(); syncSearchFieldsToCurrentRoute(); } @@ -6599,6 +6630,7 @@ private void applySpecialPageSearchQuery(String query) { clearInteractionState(); layoutDocument = null; lastLayoutWidth = -1; + invalidateScrollbarOutline(); clampScroll(); } @@ -6614,6 +6646,87 @@ private void updateSpecialSearchBlocks(LytNode root, String query) { } } + private GuideScreenScrollbarOutline.ScrollbarBounds scrollbarBounds() { + return new GuideScreenScrollbarOutline.ScrollbarBounds( + panelX + panelW - SCROLLBAR_W, + getDocumentViewportY(), + SCROLLBAR_W, + getDocumentViewportHeight(), + getContentHeight(), + getDocumentViewportHeight(), + getMaxScroll()); + } + + private void updateScrollbarOutlineHover(int mouseX, int mouseY) { + if (getMaxScroll() <= 0 || isGuideEditorActive() || isHomeRoute()) { + scrollbarOutline.clearHover(); + return; + } + scrollbarOutline.update( + currentPage, + getActiveDocument(), + scrollbarBounds(), + currentZoom, + scrollY, + mouseX, + mouseY, + System.currentTimeMillis(), + null, + SCROLLBAR_OUTLINE_LABEL_MAX_WIDTH, + SCROLLBAR_OUTLINE_LABEL_GAP, + panelX, + panelX + panelW); + } + + private void drawScrollbarOutlineMarkers(GuideScreenScrollbarOutline.RenderState renderState) { + for (int index = 0; index < renderState.entries() + .size(); index++) { + var entry = renderState.entries() + .get(index); + int color = index == renderState.activeIndex() ? withAlpha(lighten(entry.colorArgb(), 12), 220) + : withAlpha(entry.colorArgb(), 140); + drawRect( + entry.markerX(), + entry.markerY(), + entry.markerX() + entry.markerWidth(), + entry.markerY() + entry.markerHeight(), + color); + } + } + + private void drawScrollbarOutlineLabel(GuideScreenScrollbarOutline.RenderState renderState, + FontRenderer fontRenderer) { + var label = renderState.labelLayout(); + if (label == null || label.lines() + .isEmpty()) { + return; + } + int bubbleWidth = label.width() + SCROLLBAR_OUTLINE_LABEL_PADDING_X * 2; + int bubbleHeight = label.height() + SCROLLBAR_OUTLINE_LABEL_PADDING_Y * 2; + int background = Colors.argb(label.alpha(), 16, 16, 16); + int border = Colors.argb(label.alpha(), 216, 216, 216); + drawRect(label.x(), label.y(), label.x() + bubbleWidth, label.y() + bubbleHeight, background); + drawBorder(label.x(), label.y(), bubbleWidth, bubbleHeight, border); + int textY = label.y() + SCROLLBAR_OUTLINE_LABEL_PADDING_Y; + for (String line : label.lines()) { + fontRenderer.drawStringWithShadow(line, label.x() + SCROLLBAR_OUTLINE_LABEL_PADDING_X, textY, 0xFFFFFF); + textY += fontRenderer.FONT_HEIGHT; + } + } + + private int withAlpha(int argb, int alpha) { + int clampedAlpha = Math.max(0, Math.min(255, alpha)); + return (argb & 0x00FFFFFF) | (clampedAlpha << 24); + } + + private int lighten(int argb, int percent) { + return DiamondAnnotation.lighten(argb, percent); + } + + private void invalidateScrollbarOutline() { + scrollbarOutline.invalidateLayout(); + } + private void recordHomeHistoryIfEligible() { if (currentRoute == null || !currentRoute.isContent() || currentAnchor == null diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java new file mode 100644 index 00000000..07d34ffd --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreenScrollbarOutline.java @@ -0,0 +1,346 @@ +package com.hfstudio.guidenh.guide.internal; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import net.minecraft.client.gui.FontRenderer; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.GuidePage; +import com.hfstudio.guidenh.guide.color.ColorValue; +import com.hfstudio.guidenh.guide.color.LightDarkMode; +import com.hfstudio.guidenh.guide.document.DefaultStyles; +import com.hfstudio.guidenh.guide.document.block.LytDocument; +import com.hfstudio.guidenh.guide.document.block.LytHeading; +import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; + +public class GuideScreenScrollbarOutline { + + public static final long HOVER_DELAY_MILLIS = 500L; + + private static final int H1_MARKER_WIDTH = 10; + private static final int H2_MARKER_WIDTH = 7; + private static final int MARKER_HEIGHT = 2; + private static final int HIT_PADDING_X = 2; + private static final int HIT_PADDING_Y = 2; + private static final int LABEL_MAX_LINES = 2; + private static final String ELLIPSIS = "..."; + + private List anchors = List.of(); + private List entries = List.of(); + private int lastLayoutSignature; + @Nullable + private HoverState hoverState; + + public record ScrollbarBounds(int x, int y, int width, int height, int contentHeight, int viewportHeight, + int maxScroll) {} + + public record HeadingEntry(String text, int depth, int documentY, int colorArgb, int markerX, int markerY, + int markerWidth, int markerHeight, int hitX, int hitY, int hitWidth, int hitHeight) {} + + public record LabelLayout(List lines, int x, int y, int width, int height, int alpha) {} + + public record RenderState(List entries, int activeIndex, @Nullable Integer hoveredIndex, + @Nullable LabelLayout labelLayout) {} + + public void invalidateLayout() { + lastLayoutSignature = 0; + entries = List.of(); + anchors = List.of(); + hoverState = null; + } + + public void clearHover() { + hoverState = null; + } + + public RenderState update(@Nullable GuidePage page, @Nullable LytDocument document, ScrollbarBounds bounds, + float zoom, int viewportTopDocumentY, int mouseX, int mouseY, long nowMillis, + @Nullable FontRenderer fontRenderer, int labelMaxWidth, int labelGap, int panelLeft, int panelRight) { + rebuildIfNeeded(page, document, bounds, zoom, viewportTopDocumentY); + updateHoverCandidate(mouseX, mouseY, nowMillis); + int activeIndex = resolveActiveIndex(viewportTopDocumentY); + Integer hoveredIndex = hoverState != null ? hoverState.index() : null; + LabelLayout labelLayout = hoveredIndex != null && fontRenderer != null ? buildLabelLayout( + entries.get(hoveredIndex), + fontRenderer, + nowMillis, + labelMaxWidth, + labelGap, + panelLeft, + panelRight) : null; + return new RenderState(entries, activeIndex, hoveredIndex, labelLayout); + } + + public @Nullable Integer findJumpTarget(int mouseX, int mouseY) { + for (var entry : entries) { + if (contains(entry, mouseX, mouseY)) { + clearHover(); + return entry.documentY(); + } + } + return null; + } + + public int resolveActiveIndex(int viewportTopDocumentY) { + int activeIndex = -1; + for (int index = 0; index < entries.size(); index++) { + var entry = entries.get(index); + if (entry.documentY() > viewportTopDocumentY) { + break; + } + activeIndex = index; + } + return activeIndex >= 0 ? activeIndex : entries.isEmpty() ? -1 : 0; + } + + public void updateHoverCandidateForTest(int mouseX, int mouseY, long nowMillis) { + updateHoverCandidate(mouseX, mouseY, nowMillis); + } + + public void setEntriesForTest(List testEntries) { + entries = List.copyOf(testEntries); + } + + public void setAnchorsForTest(List testAnchors) { + anchors = List.copyOf(testAnchors); + } + + public HeadingEntry testEntry(String text, int depth, int documentY) { + return testEntry(text, depth, documentY, 0xFFFFFFFF, 100, 50, markerWidth(depth), MARKER_HEIGHT, 98, 48, 16, 6); + } + + public HeadingEntry testEntry(String text, int depth, int documentY, int colorArgb, int markerX, int markerY, + int markerWidth, int markerHeight, int hitX, int hitY, int hitWidth, int hitHeight) { + return new HeadingEntry( + text, + depth, + documentY, + colorArgb, + markerX, + markerY, + markerWidth, + markerHeight, + hitX, + hitY, + hitWidth, + hitHeight); + } + + public OutlineAnchor testAnchor(String text, int depth, int documentY, int colorArgb) { + return new OutlineAnchor(text, depth, documentY, colorArgb); + } + + public RenderState rebuildFromAnchorsForTest(ScrollbarBounds bounds) { + entries = mapAnchorsToEntries(anchors, bounds); + return new RenderState(entries, -1, null, null); + } + + @Nullable + public LabelLayout visibleHoverLabelForTest(long nowMillis) { + if (hoverState == null || hoverState.index() < 0 || hoverState.index() >= entries.size()) { + return null; + } + if (nowMillis - hoverState.startedAtMillis() < HOVER_DELAY_MILLIS) { + return null; + } + return new LabelLayout( + List.of( + entries.get(hoverState.index()) + .text()), + 0, + 0, + 0, + 0, + 255); + } + + private void rebuildIfNeeded(@Nullable GuidePage page, @Nullable LytDocument document, ScrollbarBounds bounds, + float zoom, int viewportTopDocumentY) { + int layoutSignature = Objects.hash( + page, + page != null ? page.titleHeading() : null, + document, + bounds.x(), + bounds.y(), + bounds.width(), + bounds.height(), + bounds.contentHeight(), + bounds.viewportHeight(), + bounds.maxScroll(), + viewportTopDocumentY, + Float.floatToIntBits(zoom)); + if (layoutSignature == lastLayoutSignature) { + return; + } + anchors = collectAnchors(page, document); + entries = mapAnchorsToEntries(anchors, bounds); + lastLayoutSignature = layoutSignature; + if (hoverState != null) { + if (hoverState.index() < 0 || hoverState.index() >= entries.size()) { + hoverState = null; + } else { + var previous = hoverState; + hoverState = new HoverState(previous.index(), previous.startedAtMillis()); + } + } + } + + private List collectAnchors(@Nullable GuidePage page, @Nullable LytDocument document) { + if (document == null || !document.hasLayout()) { + return List.of(); + } + List collected = new ArrayList<>(); + for (var block : document.getBlocks()) { + if (block instanceof LytHeading heading && heading.getBounds() != null) { + int depth = heading.getDepth(); + if (depth >= 1 && depth <= 2) { + String headingText = heading.getTextContent(); + if (!headingText.isEmpty()) { + collected.add( + new OutlineAnchor( + headingText, + depth, + heading.getBounds() + .y(), + resolveHeadingColor(depth))); + } + } + } + } + collected.sort( + Comparator.comparingInt(OutlineAnchor::documentY) + .thenComparingInt(OutlineAnchor::depth)); + return collected; + } + + private List mapAnchorsToEntries(List anchorEntries, ScrollbarBounds bounds) { + if (anchorEntries.isEmpty() || bounds.height() <= 0) { + return List.of(); + } + int track = Math.max(1, bounds.height() - MARKER_HEIGHT); + int barRight = bounds.x() + bounds.width(); + List mapped = new ArrayList<>(anchorEntries.size()); + for (var anchor : anchorEntries) { + int markerWidth = markerWidth(anchor.depth()); + int clampedDocumentY = Math.max(0, Math.min(anchor.documentY(), bounds.maxScroll())); + int markerY = bounds.y() + + (bounds.maxScroll() > 0 ? (int) ((long) track * clampedDocumentY / bounds.maxScroll()) : 0); + int markerX = barRight - markerWidth - 1; + mapped.add( + new HeadingEntry( + anchor.text(), + anchor.depth(), + anchor.documentY(), + anchor.colorArgb(), + markerX, + markerY, + markerWidth, + MARKER_HEIGHT, + markerX - HIT_PADDING_X, + markerY - HIT_PADDING_Y, + markerWidth + HIT_PADDING_X * 2, + MARKER_HEIGHT + HIT_PADDING_Y * 2)); + } + return mapped; + } + + private void updateHoverCandidate(int mouseX, int mouseY, long nowMillis) { + Integer targetIndex = nearestHoveredIndex(mouseX, mouseY); + if (targetIndex == null) { + hoverState = null; + return; + } + if (hoverState != null && hoverState.index() == targetIndex) { + return; + } + hoverState = new HoverState(targetIndex, nowMillis); + } + + private @Nullable Integer nearestHoveredIndex(int mouseX, int mouseY) { + Integer nearestIndex = null; + int nearestDistance = Integer.MAX_VALUE; + for (int index = 0; index < entries.size(); index++) { + var entry = entries.get(index); + if (!contains(entry, mouseX, mouseY)) { + continue; + } + int centerY = entry.hitY() + entry.hitHeight() / 2; + int distance = Math.abs(mouseY - centerY); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestIndex = index; + } + } + return nearestIndex; + } + + private @Nullable LabelLayout buildLabelLayout(HeadingEntry entry, FontRenderer fontRenderer, long nowMillis, + int labelMaxWidth, int labelGap, int panelLeft, int panelRight) { + if (hoverState == null || nowMillis - hoverState.startedAtMillis() < HOVER_DELAY_MILLIS) { + return null; + } + List wrappedLines = fontRenderer.listFormattedStringToWidth(entry.text(), labelMaxWidth); + if (wrappedLines.isEmpty()) { + wrappedLines = List.of(entry.text()); + } + List lines = new ArrayList<>(wrappedLines.subList(0, Math.min(LABEL_MAX_LINES, wrappedLines.size()))); + if (wrappedLines.size() > LABEL_MAX_LINES && !lines.isEmpty()) { + int lastIndex = lines.size() - 1; + lines.set(lastIndex, truncateLineWithEllipsis(fontRenderer, lines.get(lastIndex), labelMaxWidth)); + } + int width = 0; + for (String line : lines) { + width = Math.max(width, fontRenderer.getStringWidth(line)); + } + width = Math.min(labelMaxWidth, width); + int height = lines.size() * fontRenderer.FONT_HEIGHT; + int x = Math.max(panelLeft + 4, entry.markerX() - labelGap - width - 10); + x = Math.min(x, panelRight - width - 4); + int y = entry.markerY() - height / 2; + return new LabelLayout(List.copyOf(lines), x, y, width, height, 255); + } + + private String truncateLineWithEllipsis(FontRenderer fontRenderer, String line, int maxWidth) { + if (fontRenderer.getStringWidth(line) <= maxWidth) { + return line; + } + int ellipsisWidth = fontRenderer.getStringWidth(ELLIPSIS); + String trimmed = line; + while (!trimmed.isEmpty() && fontRenderer.getStringWidth(trimmed) + ellipsisWidth > maxWidth) { + trimmed = trimmed.substring(0, trimmed.length() - 1); + } + return trimmed + ELLIPSIS; + } + + private boolean contains(HeadingEntry entry, int mouseX, int mouseY) { + return mouseX >= entry.hitX() && mouseX < entry.hitX() + entry.hitWidth() + && mouseY >= entry.hitY() + && mouseY < entry.hitY() + entry.hitHeight(); + } + + private int resolveHeadingColor(int depth) { + ResolvedTextStyle baseStyle = DefaultStyles.BASE_STYLE; + ResolvedTextStyle headingStyle = switch (depth) { + case 1 -> DefaultStyles.HEADING1.mergeWith(baseStyle); + case 2 -> DefaultStyles.HEADING2.mergeWith(baseStyle); + default -> baseStyle; + }; + ColorValue colorValue = headingStyle.color() != null ? headingStyle.color() : baseStyle.color(); + return colorValue.resolve(LightDarkMode.LIGHT_MODE); + } + + private static int markerWidth(int depth) { + return switch (depth) { + case 1 -> H1_MARKER_WIDTH; + default -> H2_MARKER_WIDTH; + }; + } + + public record OutlineAnchor(String text, int depth, int documentY, int colorArgb) {} + + record HoverState(int index, long startedAtMillis) {} +} From 58115ad0e9af25633b03c4a8afed485c92055a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:00:52 +0800 Subject: [PATCH 105/136] fix blockquote directive text stripping --- .../guide/compiler/tags/BlockquoteCompiler.java | 15 ++++++++++++++- .../com/hfstudio/guidenh/libs/mdx/FactoryTag.java | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java index 8560dbca..6a04283a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java @@ -19,6 +19,8 @@ import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; import com.hfstudio.guidenh.guide.style.BorderStyle; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; +import com.hfstudio.guidenh.libs.mdast.model.MdAstText; public class BlockquoteCompiler extends BlockTagCompiler { @@ -81,7 +83,8 @@ private void compileDirectiveBody(PageCompiler compiler, BlockquoteDirective dir && directive.remainingText() != null && !directive.remainingText() .isEmpty()) { - // Clone the first paragraph with the remaining text overriding the leading text + // Strip directive prefix from the first paragraph's leading text + stripLeadingText(directive.firstParagraph(), directive.remainingText()); compiler.compileBlockContext(Collections.singletonList(directive.firstParagraph()), parent); for (int i = 1; i < directive.children() .size(); i++) { @@ -122,4 +125,14 @@ private LytFlowContent buildQuoteIcon(@Nullable QuoteIconSpec icon) { // For now return null — icon rendering will be added in a later phase. return null; } + + private static void stripLeadingText(MdxJsxFlowElement paragraph, String replacementText) { + for (Object child : paragraph.children()) { + if (child instanceof MdAstText text && !text.value.trim() + .isEmpty()) { + text.setValue(replacementText); + return; + } + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java index 163b1ac0..d3d4bf7a 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java +++ b/src/main/java/com/hfstudio/guidenh/libs/mdx/FactoryTag.java @@ -626,7 +626,8 @@ private State lineStart(int code) { } private static boolean isPascalTagStart(int code) { - return code >= Codes.uppercaseA && code <= Codes.uppercaseZ; + return (code >= Codes.uppercaseA && code <= Codes.uppercaseZ) + || (code >= Codes.lowercaseA && code <= Codes.lowercaseZ); } } From 9a237152c6b74431f1208b0dc09205a97ff500fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:00:59 +0800 Subject: [PATCH 106/136] fix task list item remaining text propagation --- .../guidenh/guide/compiler/tags/ListItemCompiler.java | 2 ++ .../guide/internal/markdown/MarkdownListSemantics.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java index a8c77f68..339d64c7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ListItemCompiler.java @@ -27,6 +27,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (taskMarker != null) { LytTaskListItem taskItem = new LytTaskListItem(); taskItem.setChecked(taskMarker.checked()); + taskMarker.textNode() + .setValue(taskMarker.remainingText()); listItem = taskItem; } else { listItem = new LytListItem(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java index 04d1d775..142c4d44 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownListSemantics.java @@ -35,7 +35,7 @@ private MarkdownListSemantics() {} .get(0); Matcher matcher = TASK_PATTERN.matcher(text.value); if (matcher.matches()) { - return new TaskMarker(!" ".equals(matcher.group(1)), matcher.group(2)); + return new TaskMarker(!" ".equals(matcher.group(1)), matcher.group(2), text); } } } @@ -43,5 +43,5 @@ private MarkdownListSemantics() {} } @Desugar - public record TaskMarker(boolean checked, String remainingText) {} + public record TaskMarker(boolean checked, String remainingText, MdAstText textNode) {} } From 749ea8967d998f501723ebf8102076d60eec1f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:01:09 +0800 Subject: [PATCH 107/136] fix LytListItem missing from float-aware wrapping --- .../com/hfstudio/guidenh/guide/compiler/PageCompiler.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 2da30066..1fceaa70 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -36,6 +36,7 @@ import com.hfstudio.guidenh.guide.document.block.LytHeading; import com.hfstudio.guidenh.guide.document.block.LytLatexBlock; import com.hfstudio.guidenh.guide.document.block.LytLatexDisplayBlock; +import com.hfstudio.guidenh.guide.document.block.LytListItem; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.table.LytTable; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; @@ -725,7 +726,9 @@ private LytBlock compileTable(GfmTable astTable, List widthHints) { } public static LytBlock wrapFloatAwareIfNeeded(LytBlock block) { - if (block instanceof LytParagraph || block instanceof LytDocumentFloat || block instanceof LytFloatAwareBlock) { + if (block instanceof LytParagraph || block instanceof LytDocumentFloat + || block instanceof LytFloatAwareBlock + || block instanceof LytListItem) { return block; } return new LytFloatAwareBlock(block); From 6742abe75d26eeab026b29700f8b3a904578ddaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:01:09 +0800 Subject: [PATCH 108/136] fix page cache not cleared on guide reload --- .../guide/internal/GuideLightweightReloadService.java | 3 +++ .../com/hfstudio/guidenh/guide/internal/GuideScreen.java | 4 ++++ .../com/hfstudio/guidenh/guide/internal/host/LytHost.java | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index cc753da1..ced81a21 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -14,6 +14,7 @@ import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; +import com.hfstudio.guidenh.ClientProxy; import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; @@ -66,6 +67,8 @@ public static void reloadGuides(IResourceManager resourceManager) { StructureLibRuntimeFacade.IMPORT_RESULT_CACHE.clear(); StructureLibElementTooltipResolver.BLOCK_CANDIDATE_CACHE.clear(); StructureLibElementTooltipResolver.HATCH_CANDIDATE_CACHE.clear(); + ClientProxy.getLytHost() + .clearPageCaches(); long stageStartedAt = System.nanoTime(); GuideRegistry.setDataDriven(DataDrivenGuideLoader.load()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index e3f30f2a..e77a9f56 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -1430,6 +1430,10 @@ private void rebuildGuideEditorPreview() { resolveVisualScale(getVisualReferenceContentWidth(), previewWidth)); } guideEditorPreviewDirty = false; + ClientProxy.getLytHost() + .invalidatePage( + currentAnchor.pageId() + .toString()); if (canApplyGuideEditorParsedPage(parsedDraft)) { guideEditorDraftPage = parsedDraft; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java index cfc96fd1..4a94e7e8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/LytHost.java @@ -138,6 +138,13 @@ public void invalidatePage(String pageId) { preheatScheduled.remove(pageId); } + public void clearPageCaches() { + cachedDocuments.clear(); + pageNodeCounters.clear(); + preheatQueue.clear(); + preheatScheduled.clear(); + } + public void setCurrentPageId(String pageId) { this.currentPageId = pageId; } From 1c3a39435a367b9f0b1e5a3a40c656150c0a2ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:01:09 +0800 Subject: [PATCH 109/136] fix keybind and player name placeholder style leak --- .../guidenh/guide/internal/host/scripts/KeyBindScript.java | 2 ++ .../guidenh/guide/internal/host/scripts/PlayerNameScript.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java index 6ac8f3a8..7beeb856 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/KeyBindScript.java @@ -7,6 +7,7 @@ import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.style.TextStyle; public class KeyBindScript implements LytScript { @@ -28,6 +29,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { var mapping = KeyBindTagCompiler.findMapping(bindId); String display = mapping != null ? KeyBindTagCompiler.describeMapping(mapping) : "[" + bindId + "]"; placeholder.setText(display); + placeholder.setStyle(TextStyle.EMPTY); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java index 90875e22..b416d104 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/PlayerNameScript.java @@ -8,6 +8,7 @@ import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.style.TextStyle; public class PlayerNameScript implements LytScript { @@ -33,6 +34,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { username = ""; } placeholder.setText(username); + placeholder.setStyle(TextStyle.EMPTY); } } } From adf339410fd5557f597dddf843ecef5f0b8a74d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:01:09 +0800 Subject: [PATCH 110/136] fix Mermaid parser HTML tag stripping --- .../guidenh/guide/internal/mermaid/MermaidMindmapParser.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/mermaid/MermaidMindmapParser.java b/src/main/java/com/hfstudio/guidenh/guide/internal/mermaid/MermaidMindmapParser.java index abe442ca..ce5c7d41 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/mermaid/MermaidMindmapParser.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/mermaid/MermaidMindmapParser.java @@ -279,9 +279,8 @@ private static String toPlainText(String text) { .replace("++", "") .replace("^^", "") .replace("::", "") - .replace("`", "") - .replace("<", "") - .replace(">", ""); + .replace("`", ""); + normalized = normalized.replaceAll("]*>", ""); return stripWrappingQuotes(normalized.trim()); } From 58c8ad6e20dbb58892b7bfb9ac332d8050a3899c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:01:17 +0800 Subject: [PATCH 111/136] fix Mermaid canvas Scene integration --- .../block/LytMermaidMindmapCanvas.java | 182 +++++++++++++----- 1 file changed, 139 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 616ffb7f..4ca39eb3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -18,6 +18,7 @@ import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.color.LightDarkMode; import com.hfstudio.guidenh.guide.document.LytRect; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.interaction.DocumentDragTarget; import com.hfstudio.guidenh.guide.document.interaction.DocumentInteractionSnapshot; import com.hfstudio.guidenh.guide.document.interaction.FlowInteractionPath; @@ -120,6 +121,13 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar private boolean dragging; private int dragLastDocumentX; private int dragLastDocumentY; + private int lastPickDocX; + private int lastPickDocY; + private boolean lastPickValid; + @Nullable + private LytParagraph lastFlowHoverParagraph; + @Nullable + private LytFlowContent lastFlowHoverContent; public LytMermaidMindmapCanvas(MermaidMindmapDocument mindmap, Map nodeContentBlocks) { this.mindmap = mindmap; @@ -179,6 +187,7 @@ public void render(RenderContext context) { context.pushLocalScissor(viewport); try { + refreshFlowHover(); renderConnectors(context, layout.root(), baseX, baseY); renderNodes(context, layout.root(), baseX, baseY); } finally { @@ -186,15 +195,26 @@ public void render(RenderContext context) { } } + @Override + public LytNode pickNode(int x, int y) { + if (!getBounds().contains(x, y)) return null; + lastPickDocX = x; + lastPickDocY = y; + lastPickValid = true; + NodeHit hit = pickNodeHit(x, y); + return hit != null ? hit.node() : this; + } + + @Override + public List getChildren() { + return new ArrayList<>(nodeContentBlocks.values()); + } + @Override public boolean beginDrag(int documentX, int documentY, int button) { - if (button != 0 || layout == null) { - return false; - } + if (button != 0 || layout == null) return false; LytRect viewport = getInnerViewport(); - if (!viewport.contains(documentX, documentY)) { - return false; - } + if (!viewport.contains(documentX, documentY)) return false; dragging = true; dragLastDocumentX = documentX; dragLastDocumentY = documentY; @@ -203,9 +223,7 @@ public boolean beginDrag(int documentX, int documentY, int button) { @Override public void dragTo(int documentX, int documentY) { - if (!dragging || layout == null) { - return; - } + if (!dragging || layout == null) return; int dx = documentX - dragLastDocumentX; int dy = documentY - dragLastDocumentY; dragLastDocumentX = documentX; @@ -222,9 +240,7 @@ public void endDrag() { @Override public boolean scroll(int documentX, int documentY, int wheelDelta) { - if (wheelDelta == 0 || layout == null || !getInnerViewport().contains(documentX, documentY)) { - return false; - } + if (wheelDelta == 0 || layout == null || !getInnerViewport().contains(documentX, documentY)) return false; float previousZoom = zoom; if (wheelDelta > 0) { zoom = Math.min(MAX_ZOOM, zoom * ZOOM_STEP); @@ -240,28 +256,21 @@ public boolean scroll(int documentX, int documentY, int wheelDelta) { @Override public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolean doubleClick) { - if (layout == null || !getInnerViewport().contains(x, y)) { - return false; - } + if (layout == null || !getInnerViewport().contains(x, y)) return false; NodeHit hit = pickNodeHit(x, y); - if (hit == null) { - return false; - } + if (hit == null) return false; boolean handled = false; for (var content : hit.flowPath() .targets()) { - if (handled) { - break; - } if (content instanceof InteractiveElement interactiveElement) { handled = interactiveElement.mouseClicked(screen, hit.localX(), hit.localY(), button, doubleClick); + if (handled) return true; } } - if (!handled) { - for (LytNode current = hit.node(); current != null && !handled; current = current.getParent()) { - if (current instanceof InteractiveElement interactiveElement) { - handled = interactiveElement.mouseClicked(screen, hit.localX(), hit.localY(), button, doubleClick); - } + for (LytNode current = hit.node(); current != null && current != this + && !handled; current = current.getParent()) { + if (current instanceof InteractiveElement interactiveElement) { + handled = interactiveElement.mouseClicked(screen, hit.localX(), hit.localY(), button, doubleClick); } } return handled; @@ -269,28 +278,20 @@ public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolea @Override public Optional getTooltip(float x, float y) { - if (layout == null || !getInnerViewport().contains((int) x, (int) y)) { - return Optional.empty(); - } + if (layout == null || !getInnerViewport().contains((int) x, (int) y)) return Optional.empty(); NodeHit hit = pickNodeHit((int) x, (int) y); - if (hit == null) { - return Optional.empty(); - } + if (hit == null) return Optional.empty(); for (var content : hit.flowPath() .targets()) { if (content instanceof InteractiveElement interactiveElement) { Optional tooltip = interactiveElement.getTooltip(hit.localX(), hit.localY()); - if (tooltip.isPresent()) { - return tooltip; - } + if (tooltip.isPresent()) return tooltip; } } - for (LytNode current = hit.node(); current != null; current = current.getParent()) { + for (LytNode current = hit.node(); current != null && current != this; current = current.getParent()) { if (current instanceof InteractiveElement interactiveElement) { Optional tooltip = interactiveElement.getTooltip(hit.localX(), hit.localY()); - if (tooltip.isPresent()) { - return tooltip; - } + if (tooltip.isPresent()) return tooltip; } } return Optional.empty(); @@ -601,10 +602,15 @@ private void renderNodes(RenderContext context, NodeLayout node, int baseX, int scaled(baseY, node.y), Math.max(1, Math.round(node.width * zoom)), Math.max(1, Math.round(node.height * zoom))); + // Widen the node box when it contains a scene so the background and + // border extend behind the floating button column. + LytRect boxRect = containsScene(node.contentLayout != null ? node.contentLayout.block() : null) + ? new LytRect(rect.x(), rect.y(), rect.width() + Math.round(38 * zoom), rect.height()) + : rect; NodeColors colors = resolveColors(node.node); - context.fillRect(rect, colors.background); - context.drawBorder(rect, colors.border, node.node.getShape() == MermaidMindmapNodeShape.BANG ? 2 : 1); - context.fillRect(new LytRect(rect.x(), rect.y(), 3, rect.height()), colors.accent); + context.fillRect(boxRect, colors.background); + context.drawBorder(boxRect, colors.border, node.node.getShape() == MermaidMindmapNodeShape.BANG ? 2 : 1); + context.fillRect(new LytRect(boxRect.x(), boxRect.y(), 3, boxRect.height()), colors.accent); ResolvedTextStyle style = node.depth == 0 ? scaledRootTextStyle : scaledNodeTextStyle; ResolvedTextStyle badgeStyle = scaledIconTextStyle; @@ -654,6 +660,17 @@ private void renderNodeContent(RenderContext context, NodeLayout node, LytRect r contentY, Math.max(1, rect.width() - paddingX * 2), Math.max(1, rect.bottom() - contentY - Math.max(1, Math.round(NODE_PADDING_Y * zoom)))); + // Scene buttons sit outside the viewport; the viewport itself is also + // widened so the background extends behind the button column. + // Extra = BTN_SIZE(16) + BTN_OUTSIDE_GAP(3) + extra margin. + if (containsScene(node.contentLayout.block())) { + int btnExtra = Math.round(38 * zoom); + contentViewport = new LytRect( + contentViewport.x(), + contentViewport.y(), + contentViewport.width() + btnExtra, + contentViewport.height()); + } LytRect clip = intersect(viewport, contentViewport); if (clip == null) { return; @@ -688,6 +705,7 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod scene.bounds = new LytRect(contentViewport.x(), contentViewport.y(), scaledW, scaledH); scene.setSceneViewportOverride(scaledW, scaledH); scene.setCameraViewportOverride(savedW, savedH); + scene.setButtonZoom(zoom); try { scene.render(nativeContext); } finally { @@ -704,6 +722,7 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod renderNodeContentBlock(childBlock, nodeContext, nativeContext, contentViewport); } } + renderContainerDecoration(container, nodeContext); } else if (usesRawGl(block)) { // This block renders with raw GL, bypassing RenderContext coordinate // mapping. Apply mindmap zoom via GL matrix and use nativeContext so @@ -721,6 +740,79 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod } } + private void refreshFlowHover() { + if (!lastPickValid || layout == null) return; + NodeHit hit = pickNodeHit(lastPickDocX, lastPickDocY); + LytFlowContent hoveredFlow = null; + LytParagraph hoveredParagraph = null; + if (hit != null) { + for (var content : hit.flowPath() + .targets()) { + if (content instanceof InteractiveElement) { + hoveredFlow = content; + break; + } + } + if (hoveredFlow != null) { + for (LytNode node = hit.node(); node != null; node = node.getParent()) { + if (node instanceof LytParagraph p) { + hoveredParagraph = p; + break; + } + } + } + } + if (hoveredParagraph != lastFlowHoverParagraph || hoveredFlow != lastFlowHoverContent) { + if (lastFlowHoverParagraph != null) lastFlowHoverParagraph.onMouseLeave(); + if (hoveredParagraph != null) hoveredParagraph.onMouseEnter(hoveredFlow); + lastFlowHoverParagraph = hoveredParagraph; + lastFlowHoverContent = hoveredFlow; + } + } + + private static boolean containsScene(@Nullable LytBlock block) { + if (block == null) return false; + if (block instanceof LytGuidebookScene) return true; + if (block instanceof LytNode container) { + for (var child : container.getChildren()) { + if (child instanceof LytBlock childBlock && containsScene(childBlock)) return true; + } + } + return false; + } + + private static void renderContainerDecoration(LytNode container, RenderContext context) { + if (!(container instanceof LytBox box)) return; + LytRect b = container.getBounds(); + if (box.getBackgroundColor() != null) { + context.fillRect(b, box.getBackgroundColor()); + } + int topW = box.getBorderTop() + .width(); + int bottomW = box.getBorderBottom() + .width(); + if (topW > 0) { + context.fillRect( + b.x(), + b.y(), + b.width(), + topW, + context.resolveColor( + box.getBorderTop() + .color())); + } + if (bottomW > 0) { + context.fillRect( + b.x(), + b.bottom() - bottomW, + b.width(), + bottomW, + context.resolveColor( + box.getBorderBottom() + .color())); + } + } + private static boolean usesRawGl(LytBlock block) { return block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock || block instanceof LytItemImage @@ -777,10 +869,14 @@ private LytRect resolveNodeContentRect(NodeLayout node, int baseX, int baseY) { int paddingX = Math.max(1, Math.round(NODE_PADDING_X * zoom)); int paddingY = Math.max(1, Math.round(NODE_PADDING_Y * zoom)); int textY = nodeRect.y() + paddingY + resolveNodeBadgeHeight(node); + int contentW = Math.max(1, Math.round(node.contentLayout.width() * zoom)); + if (containsScene(node.contentLayout.block())) { + contentW += Math.round(38 * zoom); + } return new LytRect( nodeRect.x() + paddingX, textY, - Math.max(1, Math.round(node.contentLayout.width() * zoom)), + contentW, Math.max(1, Math.round(node.contentLayout.height() * zoom))); } From e58747fb9c4d167bab9e8bd34d811dd431b1cbca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:01:17 +0800 Subject: [PATCH 112/136] fix Scene button zoom consistency --- .../guide/scene/LytGuidebookScene.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index e15859c2..4c021bbb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -225,6 +225,12 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private int cachedPonderBtnAbsY; private boolean interactive = true; + float buttonZoom = 1f; + + public void setButtonZoom(float buttonZoom) { + this.buttonZoom = Math.max(0.1f, buttonZoom); + } + private boolean sceneButtonsVisible = true; private boolean bottomControlsVisible = true; private boolean reserveBottomControlArea = true; @@ -3266,11 +3272,12 @@ private void drawSceneButtons(int drawX, int drawY, int screenW, int screenH, in // GL drawing uses layout coords; convert screen pixels back to layout units for bx. int layoutW = Math.round(screenW / docZoom); - int bx = drawX + layoutW + BTN_OUTSIDE_GAP; + float bz = buttonZoom; + int bx = drawX + layoutW + Math.round(BTN_OUTSIDE_GAP * bz); int by = drawY; - int btnScreenSize = Math.round(BTN_SIZE * docZoom); - int btnScreenStep = Math.round((BTN_SIZE + BTN_GAP) * docZoom); - int absBx = absX + screenW + Math.round(BTN_OUTSIDE_GAP * docZoom); + int btnScreenSize = Math.round(BTN_SIZE * docZoom * bz); + int btnScreenStep = Math.round((BTN_SIZE + BTN_GAP) * docZoom * bz); + int absBx = absX + screenW + Math.round(BTN_OUTSIDE_GAP * docZoom * bz); int absBy = absY; sceneButtonsAbsX = absBx; sceneButtonsAbsY = absBy; @@ -3286,10 +3293,12 @@ private void drawSceneButtons(int drawX, int drawY, int screenW, int screenH, in my = -1; } GuideIconButton.Role[] roles = cachedSceneButtonRoles(); + int btnDocSize = Math.round(BTN_SIZE * bz); + int btnDocStep = Math.round((BTN_SIZE + BTN_GAP) * bz); for (var role : roles) { boolean hover = mx >= absBx && my >= absBy && mx < absBx + btnScreenSize && my < absBy + btnScreenSize; - drawOneSceneButton(bx, by, role, hover); - by += BTN_SIZE + BTN_GAP; + drawOneSceneButton(bx, by, btnDocSize, role, hover); + by += btnDocStep; absBy += btnScreenStep; } } @@ -3336,7 +3345,7 @@ private GuideIconButton.Role[] cachedSceneButtonRoles() { return cachedSceneButtonRoles; } - private void drawOneSceneButton(int x, int y, GuideIconButton.Role role, boolean hovered) { + private void drawOneSceneButton(int x, int y, int btnSize, GuideIconButton.Role role, boolean hovered) { Minecraft.getMinecraft() .getTextureManager() .bindTexture(BUTTONS_TEXTURE); @@ -3355,9 +3364,9 @@ private void drawOneSceneButton(int x, int y, GuideIconButton.Role role, boolean float v1 = (role.iconSrcY() + role.iconSrcHeight()) / texSize; var tess = Tessellator.instance; tess.startDrawingQuads(); - tess.addVertexWithUV(x, y + BTN_SIZE, 0, u0, v1); - tess.addVertexWithUV(x + BTN_SIZE, y + BTN_SIZE, 0, u1, v1); - tess.addVertexWithUV(x + BTN_SIZE, y, 0, u1, v0); + tess.addVertexWithUV(x, y + btnSize, 0, u0, v1); + tess.addVertexWithUV(x + btnSize, y + btnSize, 0, u1, v1); + tess.addVertexWithUV(x + btnSize, y, 0, u1, v0); tess.addVertexWithUV(x, y, 0, u0, v0); tess.draw(); GL11.glColor4f(1f, 1f, 1f, 1f); @@ -3386,8 +3395,8 @@ public GuideIconButton.Role sceneButtonAt(int mouseX, int mouseY) { if (lastW <= 0 || lastH <= 0) return null; int bx = sceneButtonsAbsX; int by = sceneButtonsAbsY; - int btnScreenSize = Math.round(BTN_SIZE * lastDocZoom); - int btnScreenStep = Math.round((BTN_SIZE + BTN_GAP) * lastDocZoom); + int btnScreenSize = Math.round(BTN_SIZE * lastDocZoom * buttonZoom); + int btnScreenStep = Math.round((BTN_SIZE + BTN_GAP) * lastDocZoom * buttonZoom); int rolesHeight = btnScreenSize + Math.max(0, cachedSceneButtonRoles().length - 1) * btnScreenStep; LytRect buttonColumnRect = new LytRect(bx, by, btnScreenSize, rolesHeight); if (renderedContentClip != null) { From 8084a2e737c26ad2f0ccef82d6534d62ab80073e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:07:09 +0800 Subject: [PATCH 113/136] stop tracking CLAUDE.md --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f23567ed..0e96f79e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ layout.json /docs /gradle-user .claude/ +CLAUDE.md From 278601fe2891964d7f1ec69e5b5e224c556e5b62 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:59:23 +0800 Subject: [PATCH 114/136] sa --- dependencies.gradle | 2 +- .../block/LytMermaidMindmapCanvas.java | 4 ++-- .../guide/layout/flow/LineBuilder.java | 20 ++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index af1231e9..396548a8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ dependencies { - api("com.github.GTNewHorizons:GTNHLib:0.10.3:dev") + api("com.github.GTNewHorizons:GTNHLib:0.10.8:dev") implementation("io.github.legacymoddingmc:unimixins:0.3.1:dev") shadowImplementation("org.yaml:snakeyaml:1.33") { transitive = false } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index e20c37f2..4ca39eb3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -769,7 +769,7 @@ private void refreshFlowHover() { lastFlowHoverContent = hoveredFlow; } } - + private static boolean containsScene(@Nullable LytBlock block) { if (block == null) return false; if (block instanceof LytGuidebookScene) return true; @@ -812,7 +812,7 @@ private static void renderContainerDecoration(LytNode container, RenderContext c .color())); } } - + private static boolean usesRawGl(LytBlock block) { return block instanceof LytLatexBlock || block instanceof LytLatexDisplayBlock || block instanceof LytItemImage diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java index 52fd0c4c..5b577d5f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java @@ -81,15 +81,17 @@ public LineBuilder(LayoutContext context, int x, int y, int availableWidth, List @Override public void accept(LytFlowContent content) { - switch (content) { - case LytFlowText text -> appendText(text.getText(), content); - case LytFlowBreak lytFlowBreak -> appendBreak(content); - case LytFlowInlineBlock inlineBlock -> appendInlineBlock(inlineBlock); - case LytFlowAnchor anchor -> - // Simply set the current layout position for the anchor - anchor.setLayoutY(lineBoxY); - case null, default -> - throw new IllegalArgumentException("Don't know how to layout flow content: " + content); + if (content instanceof LytFlowText text) { + appendText(text.getText(), content); + } else if (content instanceof LytFlowBreak) { + appendBreak(content); + } else if (content instanceof LytFlowInlineBlock inlineBlock) { + appendInlineBlock(inlineBlock); + } else if (content instanceof LytFlowAnchor anchor) { + // Simply set the current layout position for the anchor + anchor.setLayoutY(lineBoxY); + } else { + throw new IllegalArgumentException("Don't know how to layout flow content: " + content); } } From 089c64efc8973c8e2fdcc4d90b109705a5f933c7 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:41:47 +0800 Subject: [PATCH 115/136] bugfix --- .../guide/document/block/LytLatexBlock.java | 7 + .../document/block/LytLatexDisplayBlock.java | 9 + .../block/LytMermaidMindmapCanvas.java | 203 ++++++++++++------ .../guide/scene/LytGuidebookScene.java | 4 +- 4 files changed, 157 insertions(+), 66 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexBlock.java index 8c297202..ee059db3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexBlock.java @@ -171,6 +171,13 @@ public int getOffsetY() { return offsetY; } + public LytRect getVisualBounds() { + if (bounds == null || bounds.isEmpty()) { + return LytRect.empty(); + } + return new LytRect(bounds.x() + offsetX, bounds.y() + renderYOffset, bounds.width(), bounds.height()); + } + @Nullable @Override public LytRect getBounds() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexDisplayBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexDisplayBlock.java index c6866f36..6280155b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexDisplayBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytLatexDisplayBlock.java @@ -147,6 +147,15 @@ public int getOffsetY() { return offsetY; } + public LytRect getVisualBounds() { + if (bounds == null || bounds.isEmpty() || formulaDisplayW <= 0 || formulaDisplayH <= 0) { + return bounds != null ? bounds : LytRect.empty(); + } + int centeredX = bounds.x() + (bounds.width() - formulaDisplayW) / 2; + int formulaY = bounds.y() + VERTICAL_MARGIN; + return new LytRect(centeredX + offsetX, formulaY + offsetY, formulaDisplayW, formulaDisplayH); + } + @Nullable @Override public LytRect getBounds() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 4ca39eb3..1615b19a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -149,6 +149,11 @@ public void setPreferredSize(int width, int height) { @Override protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + DiagramLayout previousLayout = layout; + int previousContentOffsetX = contentOffsetX; + int previousContentOffsetY = contentOffsetY; + int previousViewportWidth = Math.max(1, bounds.width() - CANVAS_PADDING * 2); + int previousViewportHeight = Math.max(1, bounds.height() - CANVAS_PADDING * 2); int safeWidth = preferredWidth > 0 ? Math.max(1, Math.min(preferredWidth, availableWidth)) : Math.max(1, availableWidth); layout = buildLayout(context, safeWidth); @@ -158,7 +163,16 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab if (preferredHeight > 0 && safeWidth < resolvePreferredViewportWidth()) { viewportHeight = Math.max(viewportHeight, Math.min(MAX_HEIGHT, desiredHeight)); } - centerDiagram(safeWidth, viewportHeight); + int viewportWidth = Math.max(1, safeWidth - CANVAS_PADDING * 2); + int innerViewportHeight = Math.max(1, viewportHeight - CANVAS_PADDING * 2); + restoreViewportAfterLayout( + previousLayout, + previousContentOffsetX, + previousContentOffsetY, + previousViewportWidth, + previousViewportHeight, + viewportWidth, + innerViewportHeight); return new LytRect(x, y, safeWidth, viewportHeight); } @@ -369,7 +383,7 @@ private DiagramLayout buildLayout(LayoutContext context, int availableWidth) { } private DiagramLayout buildDiagramLayout(NodeLayout root) { - LytRect contentBounds = collectContentBounds(root); + LytRect contentBounds = collectRenderedBounds(root); return new DiagramLayout( root, Math.max(1, contentBounds.width()), @@ -378,10 +392,10 @@ private DiagramLayout buildDiagramLayout(NodeLayout root) { collectContentNodes(root, new ArrayList<>())); } - private LytRect collectContentBounds(NodeLayout node) { - LytRect bounds = new LytRect(node.x, node.y, node.width, node.height); + private LytRect collectRenderedBounds(NodeLayout node) { + LytRect bounds = resolveNodeVisualRect(node); for (NodeLayout child : node.children) { - bounds = LytRect.union(bounds, collectContentBounds(child)); + bounds = LytRect.union(bounds, collectRenderedBounds(child)); } return bounds; } @@ -428,8 +442,10 @@ private NodeLayout prepareLayout(LayoutContext context, MermaidMindmapNode node, int lineHeight = context.getLineHeight(style); textHeight = Math.max(1, lines.size()) * lineHeight; } else { - textWidth = contentLayout.width(); - textHeight = contentLayout.height(); + textWidth = contentLayout.visualBounds() + .width(); + textHeight = contentLayout.visualBounds() + .height(); } int badgeWidth = 0; @@ -477,8 +493,9 @@ private NodeLayout prepareLayout(LayoutContext context, MermaidMindmapNode node, } LayoutContext localContext = new LayoutContext(context).withVisualScale(context.getVisualScale()); int contentWidth = Math.clamp(maxNodeTextWidth + 60, 96, 240); - LytRect contentBounds = block.layout(localContext, 0, 0, contentWidth); - return new NodeContentLayout(block, contentBounds.width(), contentBounds.height()); + block.layout(localContext, 0, 0, contentWidth); + LytRect visualBounds = resolveBlockVisualBounds(block); + return new NodeContentLayout(block, visualBounds); } private void measureSideTree(NodeLayout node) { @@ -602,11 +619,7 @@ private void renderNodes(RenderContext context, NodeLayout node, int baseX, int scaled(baseY, node.y), Math.max(1, Math.round(node.width * zoom)), Math.max(1, Math.round(node.height * zoom))); - // Widen the node box when it contains a scene so the background and - // border extend behind the floating button column. - LytRect boxRect = containsScene(node.contentLayout != null ? node.contentLayout.block() : null) - ? new LytRect(rect.x(), rect.y(), rect.width() + Math.round(38 * zoom), rect.height()) - : rect; + LytRect boxRect = rect; NodeColors colors = resolveColors(node.node); context.fillRect(boxRect, colors.background); context.drawBorder(boxRect, colors.border, node.node.getShape() == MermaidMindmapNodeShape.BANG ? 2 : 1); @@ -655,34 +668,20 @@ private void renderNodeContent(RenderContext context, NodeLayout node, LytRect r return; } LytRect viewport = getInnerViewport(); - LytRect contentViewport = new LytRect( - rect.x() + paddingX, - contentY, - Math.max(1, rect.width() - paddingX * 2), - Math.max(1, rect.bottom() - contentY - Math.max(1, Math.round(NODE_PADDING_Y * zoom)))); - // Scene buttons sit outside the viewport; the viewport itself is also - // widened so the background extends behind the button column. - // Extra = BTN_SIZE(16) + BTN_OUTSIDE_GAP(3) + extra margin. - if (containsScene(node.contentLayout.block())) { - int btnExtra = Math.round(38 * zoom); - contentViewport = new LytRect( - contentViewport.x(), - contentViewport.y(), - contentViewport.width() + btnExtra, - contentViewport.height()); - } + LytRect contentViewport = resolveNodeContentRect(node, rect, paddingX, contentY); LytRect clip = intersect(viewport, contentViewport); if (clip == null) { return; } context.pushLocalScissor(clip); try { - NodeContentRenderContext nodeContext = new NodeContentRenderContext( - context, - clip, - contentViewport.x(), - contentViewport.y(), - zoom); + int originX = contentViewport.x() - Math.round( + node.contentLayout.visualBounds() + .x() * zoom); + int originY = contentViewport.y() - Math.round( + node.contentLayout.visualBounds() + .y() * zoom); + NodeContentRenderContext nodeContext = new NodeContentRenderContext(context, clip, originX, originY, zoom); renderNodeContentBlock(node.contentLayout.block(), nodeContext, context, contentViewport); } finally { context.popScissor(); @@ -853,31 +852,34 @@ private static boolean usesRawGl(LytBlock block) { if (node.contentLayout == null) { return null; } - return resolveNodeContentRect(node, baseX, baseY); - } - - private int contextLineHeight(ResolvedTextStyle style) { - return Math.max(1, Math.round((9 + 1) * style.fontScale())); - } - - private LytRect resolveNodeContentRect(NodeLayout node, int baseX, int baseY) { LytRect nodeRect = new LytRect( scaled(baseX, node.x), scaled(baseY, node.y), Math.max(1, Math.round(node.width * zoom)), Math.max(1, Math.round(node.height * zoom))); int paddingX = Math.max(1, Math.round(NODE_PADDING_X * zoom)); - int paddingY = Math.max(1, Math.round(NODE_PADDING_Y * zoom)); - int textY = nodeRect.y() + paddingY + resolveNodeBadgeHeight(node); - int contentW = Math.max(1, Math.round(node.contentLayout.width() * zoom)); - if (containsScene(node.contentLayout.block())) { - contentW += Math.round(38 * zoom); - } + int contentY = nodeRect.y() + Math.max(1, Math.round(NODE_PADDING_Y * zoom)) + resolveNodeBadgeHeight(node); + return resolveNodeContentRect(node, nodeRect, paddingX, contentY); + } + + private int contextLineHeight(ResolvedTextStyle style) { + return Math.max(1, Math.round((9 + 1) * style.fontScale())); + } + + private LytRect resolveNodeContentRect(NodeLayout node, LytRect nodeRect, int paddingX, int contentY) { return new LytRect( nodeRect.x() + paddingX, - textY, - contentW, - Math.max(1, Math.round(node.contentLayout.height() * zoom))); + contentY, + Math.max( + 1, + Math.round( + node.contentLayout.visualBounds() + .width() * zoom)), + Math.max( + 1, + Math.round( + node.contentLayout.visualBounds() + .height() * zoom))); } private int resolveNodeBadgeHeight(NodeLayout node) { @@ -906,6 +908,85 @@ private int unscaleCoordinate(int coordinate) { return new LytRect(left, top, right - left, bottom - top); } + private void restoreViewportAfterLayout(@Nullable DiagramLayout previousLayout, int previousOffsetX, + int previousOffsetY, int previousViewportWidth, int previousViewportHeight, int viewportWidth, + int viewportHeight) { + if (previousLayout == null) { + centerDiagram(viewportWidth + CANVAS_PADDING * 2, viewportHeight + CANVAS_PADDING * 2); + return; + } + float anchorX = previousLayout.contentBounds() + .x() + (previousViewportWidth * 0.5f - previousOffsetX) / Math.max(zoom, 0.0001f); + float anchorY = previousLayout.contentBounds() + .y() + (previousViewportHeight * 0.5f - previousOffsetY) / Math.max(zoom, 0.0001f); + contentOffsetX = Math.round( + viewportWidth * 0.5f - (anchorX - layout.contentBounds() + .x()) * zoom); + contentOffsetY = Math.round( + viewportHeight * 0.5f - (anchorY - layout.contentBounds() + .y()) * zoom); + clampOffsets(); + } + + private LytRect resolveNodeVisualRect(NodeLayout node) { + LytRect nodeRect = new LytRect(node.x, node.y, node.width, node.height); + if (node.contentLayout == null) { + return nodeRect; + } + int contentY = node.y + NODE_PADDING_Y + resolveNodeBadgeHeightUnscaled(node); + LytRect contentRect = new LytRect( + node.x + NODE_PADDING_X, + contentY, + node.contentLayout.visualBounds() + .width(), + node.contentLayout.visualBounds() + .height()); + return LytRect.union(nodeRect, contentRect); + } + + private int resolveNodeBadgeHeightUnscaled(NodeLayout node) { + if (!node.showBadge || node.badgeText == null) { + return 0; + } + return contextLineHeight(ICON_TEXT_STYLE) + 4 + ICON_GAP_Y; + } + + private LytRect resolveBlockVisualBounds(LytBlock block) { + LytRect[] result = { LytRect.empty() }; + block.visit(new LytVisitor() { + + @Override + public Result beforeNode(LytNode node) { + if (node instanceof LytBlock childBlock) { + result[0] = LytRect.union(result[0], resolveSelfVisualBounds(childBlock)); + } + return Result.CONTINUE; + } + }); + return result[0]; + } + + private LytRect resolveSelfVisualBounds(LytBlock block) { + LytRect bounds = block.getBounds(); + if (bounds == null) { + return LytRect.empty(); + } + if (block instanceof LytLatexBlock latexBlock) { + return latexBlock.getVisualBounds(); + } + if (block instanceof LytLatexDisplayBlock latexDisplayBlock) { + return latexDisplayBlock.getVisualBounds(); + } + if (block instanceof LytGuidebookScene scene && scene.isSceneButtonsVisible()) { + return new LytRect( + bounds.x(), + bounds.y(), + bounds.width() + LytGuidebookScene.BTN_OUTSIDE_GAP + LytGuidebookScene.BTN_SIZE, + bounds.height()); + } + return bounds; + } + private NodeColors resolveColors(MermaidMindmapNode node) { int accent = 0xFF7AA2F7; for (String className : node.getClasses()) { @@ -1184,25 +1265,19 @@ public interface AdvanceFunction { public static class NodeContentLayout { private final LytBlock block; - private final int width; - private final int height; + private final LytRect visualBounds; - public NodeContentLayout(LytBlock block, int width, int height) { + public NodeContentLayout(LytBlock block, LytRect visualBounds) { this.block = block; - this.width = Math.max(1, width); - this.height = Math.max(1, height); + this.visualBounds = visualBounds != null && !visualBounds.isEmpty() ? visualBounds : LytRect.empty(); } public LytBlock block() { return block; } - public int width() { - return width; - } - - public int height() { - return height; + public LytRect visualBounds() { + return visualBounds; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 4c021bbb..e57abd9d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -1940,7 +1940,7 @@ private int blockStatsDockHeightForLayout() { BLOCK_STATS_MIN_HEIGHT, Math.min(blockStatsMaxHeight, rows * rowHeight + BLOCK_STATS_PADDING_Y * 2)); } - int rows = Math.max(1, Math.min(count, Math.max(1, height / rowHeight))); + int rows = Math.min(count, Math.max(1, height / rowHeight)); return Math .max(BLOCK_STATS_MIN_HEIGHT, Math.min(blockStatsMaxHeight, rows * rowHeight + BLOCK_STATS_PADDING_Y * 2)); } @@ -5785,7 +5785,7 @@ private int estimatePonderParticleBurstSize(PonderKeyframe keyframe) { int density = particle.getWeatherDensityPerTick( GuidebookSceneWeatherSupport .defaultDensity(GuidebookSceneWeatherType.fromSerializedName(particle.getWeatherType()))); - total += Math.clamp(density * 4, 1, 128); + total += Math.clamp(density * 4L, 1, 128); continue; } if (particle.isIndicatorPreset()) { From 8a1af5474093864779494db51ce9121714582140 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:32:15 +0800 Subject: [PATCH 116/136] move logs --- .../hfstudio/guidenh/guide/color/Colors.java | 5 +- .../guidenh/guide/compiler/IdUtils.java | 14 ++- .../guidenh/guide/compiler/PageCompiler.java | 54 ++++------ .../compiler/tags/FloatingImageCompiler.java | 6 +- .../guide/compiler/tags/PreCompiler.java | 16 ++- .../document/block/LytCodeBlockToolbar.java | 41 ++++++- .../document/block/LytMermaidMindmap.java | 1 + .../document/block/LytPlaceholderBlock.java | 6 +- .../guidenh/guide/indices/ItemIndex.java | 39 ++++--- .../guidenh/guide/indices/OreIndex.java | 21 ++-- .../guidenh/guide/indices/UniqueIndex.java | 20 ++-- .../GuideDevelopmentResourcePackWatcher.java | 22 ++-- .../GuideLightweightReloadService.java | 69 ++++++------ .../guidenh/guide/internal/GuideME.java | 6 +- .../guide/internal/GuideOnStartup.java | 22 ++-- .../guidenh/guide/internal/GuideRegistry.java | 19 ++-- .../guide/internal/GuideScopedView.java | 12 +-- .../guidenh/guide/internal/GuideScreen.java | 71 ++++++------ .../guide/internal/GuideSourceWatcher.java | 101 ++++++++---------- .../guide/internal/GuideStartupOptions.java | 14 ++- .../guidenh/guide/internal/MutableGuide.java | 77 ++++++------- .../datadriven/DataDrivenGuideLoader.java | 90 +++++++--------- .../io/SceneEditorClipboardExporter.java | 5 +- .../host/scripts/BlockImageScript.java | 6 +- .../host/scripts/CommandLinkScript.java | 6 +- .../internal/host/scripts/MermaidScript.java | 25 ++--- .../internal/host/scripts/RecipeScript.java | 11 +- .../internal/host/scripts/SceneScript.java | 9 +- .../localization/GuidePageLanguageIndex.java | 31 +++--- .../GuideResourceLanguageIndex.java | 34 +++--- .../internal/markdown/FileTreeCompiler.java | 19 ++-- .../guide/internal/search/GuideSearch.java | 56 ++++------ .../guide/internal/search/PageIndexer.java | 15 ++- .../guide/internal/util/NavigationUtil.java | 9 +- .../guide/latex/GuideLatexRenderer.java | 18 ++-- .../mediawiki/MediaWikiCategoryParser.java | 6 +- .../MediaWikiSpecialDataIndexer.java | 47 ++++---- .../MediaWikiSyntheticPageFactory.java | 16 ++- .../guide/navigation/NavigationTree.java | 20 ++-- .../guide/scene/LytGuidebookScene.java | 46 ++++---- .../guidenh/guide/scene/SceneTagCompiler.java | 7 +- .../guide/scene/support/GuideDebugLog.java | 76 ++++++++++++- .../guidenh/guide/siteexport/ExportTask.java | 9 +- .../siteexport/site/GuideSiteExportTask.java | 57 +++++----- .../site/GuideSiteHrefResolver.java | 14 ++- .../site/GuideSiteItemIconExporter.java | 11 +- .../site/GuideSiteLatexExporter.java | 22 ++-- .../GuideSiteNeiPhase1BackgroundExporter.java | 23 ++-- .../site/GuideSitePageAssetExporter.java | 27 +++-- .../site/GuideSitePageCollector.java | 26 +++-- .../site/GuideSiteSceneRuntimeExporter.java | 11 +- .../GuideSiteSceneTessellatorCapture.java | 52 +++++---- .../guide/sound/GuideSoundPlayback.java | 5 +- .../betterquesting/QuestIndex.java | 30 +++--- .../integration/nei/NeiRecipeLookup.java | 30 ++---- .../NeiCustomDiagramBridge.java | 16 +-- 56 files changed, 719 insertions(+), 802 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/color/Colors.java b/src/main/java/com/hfstudio/guidenh/guide/color/Colors.java index bc179c08..eac6605f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/color/Colors.java +++ b/src/main/java/com/hfstudio/guidenh/guide/color/Colors.java @@ -1,6 +1,6 @@ package com.hfstudio.guidenh.guide.color; -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class Colors { @@ -56,8 +56,7 @@ public static int hexToRgb(String hexColor) { } } - FMLLog.getLogger() - .error("[GuideNH] [Colors] Tried to parse an invalid hexadecimal color string: '{}'", hexColor); + GuideDebugLog.error("[GuideNH] [Colors] Tried to parse an invalid hexadecimal color string: '{}'", hexColor); return 0; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/IdUtils.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/IdUtils.java index 14f190f7..e5dd922d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/IdUtils.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/IdUtils.java @@ -18,8 +18,7 @@ import com.github.bsideup.jabel.Desugar; import com.gtnewhorizon.gtnhlib.util.data.ItemId; import com.hfstudio.guidenh.guide.internal.structure.GuideTextNbtCodec; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class IdUtils { @@ -107,12 +106,11 @@ public static ParsedItemRef parseItemRef(String idText, String defaultNamespace) nbt = tc; } } catch (Throwable t) { - FMLLog.getLogger() - .warn( - "[GuideNH] [IdUtils] Failed to parse SNBT tail '{}' for id '{}'; ignoring NBT", - snbt, - idText, - t); + GuideDebugLog.warnAlways( + "[GuideNH] [IdUtils] Failed to parse SNBT tail '{}' for id '{}'; ignoring NBT", + snbt, + idText, + t); } } else { head = idText; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 1fceaa70..49f721e1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -59,6 +59,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.internal.util.LangUtil; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.guide.sound.GuideSoundParsers; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; @@ -84,8 +85,6 @@ import com.hfstudio.guidenh.libs.unist.UnistPoint; import com.hfstudio.guidenh.libs.unist.UnistPosition; -import cpw.mods.fml.common.FMLLog; - public class PageCompiler { /** @@ -225,8 +224,7 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource parseFailureTo = e.getTo(); } String errorMessage = formatParseFailureMessage(id, language, sourcePack, parseFailureFrom); - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] {}", errorMessage, t); + GuideDebugLog.error("[GuideNH] [PageCompiler] {}", errorMessage, t); parseFailureMessage = errorMessage + ": \n" + t; astRoot = buildErrorPage(parseFailureMessage); frontmatter = new Frontmatter(null, Collections.emptyMap()); @@ -238,23 +236,22 @@ public static ParsedGuidePage parse(String sourcePack, String language, Resource } long totalNs = System.nanoTime() - parseStartedAt; - FMLLog.getLogger() - .info( - "[GuideNH] [PageCompiler] Parsed page {} lang={} totalNs={} normalizeNs={} footnoteNs={} sourceFrontmatterNs={} latexMaskNs={} commentMaskNs={} markdownParseNs={} latexRestoreNs={} htmlNormalizeNs={} mdAstConvertNs={} astFrontmatterNs={} parseFailed={}", - id, - language, - totalNs, - normalizeNs, - footnoteNs, - sourceFrontmatterNs, - latexMaskNs, - commentMaskNs, - markdownParseNs, - latexRestoreNs, - htmlNormalizeNs, - mdAstConvertNs, - astFrontmatterNs, - parseFailureMessage != null); + GuideDebugLog.info( + "[GuideNH] [PageCompiler] Parsed page {} lang={} totalNs={} normalizeNs={} footnoteNs={} sourceFrontmatterNs={} latexMaskNs={} commentMaskNs={} markdownParseNs={} latexRestoreNs={} htmlNormalizeNs={} mdAstConvertNs={} astFrontmatterNs={} parseFailed={}", + id, + language, + totalNs, + normalizeNs, + footnoteNs, + sourceFrontmatterNs, + latexMaskNs, + commentMaskNs, + markdownParseNs, + latexRestoreNs, + htmlNormalizeNs, + mdAstConvertNs, + astFrontmatterNs, + parseFailureMessage != null); return new ParsedGuidePage( sourcePack, @@ -408,15 +405,13 @@ public static Frontmatter parseFrontmatter(ResourceLocation pageId, MdAstRoot ro for (var child : root.children()) { if (child instanceof MdAstYamlFrontmatter frontmatter) { if (result != null) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Found more than one frontmatter!"); + GuideDebugLog.error("[GuideNH] [PageCompiler] Found more than one frontmatter!"); continue; } try { result = Frontmatter.parse(pageId, frontmatter.value); } catch (Exception e) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Failed to parse frontmatter for page {}", pageId, e); + GuideDebugLog.error("[GuideNH] [PageCompiler] Failed to parse frontmatter for page {}", pageId, e); break; } } @@ -438,8 +433,7 @@ public static Frontmatter parseFrontmatterFromSource(ResourceLocation pageId, St try { return Frontmatter.parse(pageId, yamlText); } catch (Exception e) { - FMLLog.getLogger() - .error("[GuideNH] [PageCompiler] Failed to parse frontmatter for page {}", pageId, e); + GuideDebugLog.error("[GuideNH] [PageCompiler] Failed to parse frontmatter for page {}", pageId, e); return new Frontmatter(null, Collections.emptyMap()); } } @@ -1093,11 +1087,9 @@ public LytFlowContent createErrorFlowContent(String text, UnistNode child) { span.appendText(tildes + "^"); span.appendBreak(); - FMLLog.getLogger() - .warn("[GuideNH] [PageCompiler] {}\n{}\n{}\n", text, line, tildes + "^"); + GuideDebugLog.warnAlways("[GuideNH] [PageCompiler] {}\n{}\n{}\n", text, line, tildes + "^"); } else { - FMLLog.getLogger() - .warn("[GuideNH] [PageCompiler] {}\n", text); + GuideDebugLog.warnAlways("[GuideNH] [PageCompiler] {}\n", text); } return span; diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java index 003e1bc6..94281d4f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/FloatingImageCompiler.java @@ -20,12 +20,11 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; import com.hfstudio.guidenh.guide.document.interaction.ContentTooltip; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.guide.sound.GuideSoundParsers; import com.hfstudio.guidenh.guide.sound.GuideSoundTrigger; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import cpw.mods.fml.common.FMLLog; - public class FloatingImageCompiler extends FlowTagCompiler { public static final String TAG_NAME = "FloatingImage"; @@ -68,8 +67,7 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen var imageId = IdUtils.resolveLink(src, compiler.getPageId()); resolvedSrc = imageId.toString(); } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .error("[GuideNH] [FloatingImageCompiler] Invalid image id: {}", src); + GuideDebugLog.error("[GuideNH] [FloatingImageCompiler] Invalid image id: {}", src); if (block.getTitle() == null) { block.setTitle("Invalid image URL: " + src); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java index 5a96cb4e..0bdcec1a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java @@ -21,11 +21,10 @@ import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguageDetector; import com.hfstudio.guidenh.guide.internal.markdown.FileTreeCompiler; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstText; -import cpw.mods.fml.common.FMLLog; - public class PreCompiler extends BlockTagCompiler { private static final Pattern CODEBLOCK_META_WIDTH = Pattern.compile("(^|\\s)width=(\"([^\"]+)\"|'([^']+)'|(\\S+))"); @@ -190,15 +189,14 @@ private record CsvFenceMeta(boolean header, List widthHints) {} try { String normalized = MermaidMindmapParser.normalize(source); LytMermaidMindmap block = new LytMermaidMindmap(MermaidMindmapParser.parse(normalized), normalized); - FMLLog.getLogger() - .info("[GuideNH] [PreCompiler] Compiled fenced Mermaid runtime block ({} chars)", normalized.length()); + GuideDebugLog + .debug("[GuideNH] [PreCompiler] Compiled fenced Mermaid runtime block ({} chars)", normalized.length()); return block; } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [PreCompiler] Failed to compile fenced Mermaid runtime block from source: {}", - source, - e); + GuideDebugLog.error( + "[GuideNH] [PreCompiler] Failed to parse fenced Mermaid runtime block from source: {}", + source, + e); return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java index b4ec386e..d22b9ab6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java @@ -14,6 +14,7 @@ import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.GuiSprite; +import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.style.BorderStyle; import com.hfstudio.guidenh.guide.ui.GuideUiHost; @@ -29,6 +30,7 @@ public class LytCodeBlockToolbar extends LytBox implements InteractiveElement { private boolean copied; private long copiedUntilMillis; private int preferredWidth; + private boolean copyButtonVisible = true; public LytCodeBlockToolbar() { languageLabel.setMarginTop(0); @@ -57,21 +59,32 @@ public void setPreferredWidth(int preferredWidth) { this.preferredWidth = Math.max(0, preferredWidth); } + public void setCopyButtonVisible(boolean copyButtonVisible) { + this.copyButtonVisible = copyButtonVisible; + } + @Override protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { int toolbarWidth = preferredWidth > 0 ? Math.min(availableWidth, preferredWidth) : availableWidth; - int labelWidth = Math.max(1, toolbarWidth - 16 - 8); + int labelWidth = Math.max(1, toolbarWidth - (copyButtonVisible ? 16 + 8 : 0)); LytRect labelBounds = languageLabel.layout(context, x, y, labelWidth); int buttonX = x + Math.max(0, toolbarWidth - 16); - LytRect buttonBounds = copyButton.layout(context, buttonX, y, 16); - int height = Math.max(labelBounds.height(), buttonBounds.height()); + LytRect buttonBounds = copyButtonVisible ? copyButton.layout(context, buttonX, y, 16) : LytRect.empty(); + int height = Math.max(labelBounds.height(), copyButtonVisible ? buttonBounds.height() : 0); languageLabel.setLayoutPos(new LytPoint(labelBounds.x(), y + (height - labelBounds.height()) / 2f)); - copyButton.setLayoutPos(new LytPoint(buttonX, y + (height - buttonBounds.height()) / 2f)); + if (copyButtonVisible) { + copyButton.setLayoutPos(new LytPoint(buttonX, y + (height - buttonBounds.height()) / 2f)); + } else { + copyButton.setLayoutPos(new LytPoint(x + toolbarWidth, y)); + } return new LytRect(x, y, toolbarWidth, height); } @Override public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolean doubleClick) { + if (!copyButtonVisible) { + return false; + } LytRect bounds = copyButton.getBounds(); if (button != 0 || bounds == null || !bounds.contains(x, y)) { return false; @@ -85,6 +98,9 @@ public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolea @Override public Optional getTooltip(float x, float y) { + if (!copyButtonVisible) { + return Optional.empty(); + } LytRect bounds = copyButton.getBounds(); if (bounds != null && bounds.contains((int) x, (int) y)) { return Optional.of(new TextTooltip(getCopyTooltipText())); @@ -96,6 +112,23 @@ public void setPaddingBottom(int paddingBottom) { this.paddingBottom = paddingBottom; } + @Override + public void render(RenderContext context) { + if (getBackgroundColor() != null) { + context.fillRect(bounds, getBackgroundColor()); + } + languageLabel.render(context); + if (copyButtonVisible) { + copyButton.render(context); + } + if (getBorderTop().width() > 0 || getBorderLeft().width() > 0 + || getBorderRight().width() > 0 + || getBorderBottom().width() > 0) { + new BorderRenderer() + .render(context, bounds, getBorderTop(), getBorderLeft(), getBorderRight(), getBorderBottom()); + } + } + private void markCopied() { copied = true; copiedUntilMillis = System.currentTimeMillis() + COPY_TOOLTIP_RESET_DELAY_MILLIS; diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java index 5cd18924..c108c0cd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java @@ -34,6 +34,7 @@ public LytMermaidMindmap(MermaidMindmapDocument mindmap, String sourceText, Map< toolbar.setLanguageDisplayName("Mermaid"); toolbar.setCopyText(this.sourceText); + toolbar.setCopyButtonVisible(false); append(toolbar); append(canvas); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java index 2f11944e..8fc3e4fd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java @@ -8,8 +8,7 @@ import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * This layout block shows a loading indicator and will ultimately replace itself with the final content. @@ -45,8 +44,7 @@ private void setCurrent(LytBlock block) { private void onLoad(LytBlock element, Throwable error) { if (error != null || element == null) { - FMLLog.getLogger() - .error("[GuideNH] [LytPlaceholderBlock] Failed to load an asynchronous guide element.", error); + GuideDebugLog.error("[GuideNH] [LytPlaceholderBlock] Failed to load an asynchronous guide element.", error); var errorParagraph = new LytParagraph(); errorParagraph.setStyle(DefaultStyles.ERROR_TEXT); if (error == null) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/indices/ItemIndex.java b/src/main/java/com/hfstudio/guidenh/guide/indices/ItemIndex.java index eeddb05d..deb2cfd8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/indices/ItemIndex.java +++ b/src/main/java/com/hfstudio/guidenh/guide/indices/ItemIndex.java @@ -14,8 +14,7 @@ import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * An index of Minecraft items to the main guidebook page describing it. @@ -74,8 +73,8 @@ public static List> getItemAnchors(ParsedGuidePage page List itemIdList = normalizeItemIdEntries(page, itemIdsNode); if (itemIdList == null) { - FMLLog.getLogger() - .warn("[GuideNH] [ItemIndex] Page {} contains malformed item_ids frontmatter", page.getId()); + GuideDebugLog + .warnAlways("[GuideNH] [ItemIndex] Page {} contains malformed item_ids frontmatter", page.getId()); return List.of(); } @@ -98,30 +97,27 @@ public static List> getItemAnchors(ParsedGuidePage page page.getId() .getResourceDomain()); } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [ItemIndex] Page {} contains a malformed item_ids frontmatter entry: {}", - page.getId(), - listEntry); + GuideDebugLog.warnAlways( + "[GuideNH] [ItemIndex] Page {} contains a malformed item_ids frontmatter entry: {}", + page.getId(), + listEntry); continue; } if (itemId == null) { - FMLLog.getLogger() - .warn( - "[GuideNH] [ItemIndex] Page {} references an unknown item {} in its item_ids frontmatter", - page.getId(), - listEntry); + GuideDebugLog.warnAlways( + "[GuideNH] [ItemIndex] Page {} references an unknown item {} in its item_ids frontmatter", + page.getId(), + listEntry); continue; } itemAnchors.add(Pair.of(itemId, new PageAnchor(page.getId(), anchor))); } else { - FMLLog.getLogger() - .warn( - "[GuideNH] [ItemIndex] Page {} contains a malformed item_ids frontmatter entry: {}", - page.getId(), - listEntry); + GuideDebugLog.warnAlways( + "[GuideNH] [ItemIndex] Page {} contains a malformed item_ids frontmatter entry: {}", + page.getId(), + listEntry); } } @@ -136,8 +132,9 @@ private static List normalizeItemIdEntries(ParsedGuidePage page, Object itemI if (itemIdsNode instanceof String itemIdEntry) { String trimmed = itemIdEntry.trim(); if (trimmed.isEmpty()) { - FMLLog.getLogger() - .warn("[GuideNH] [ItemIndex] Page {} contains an empty item_ids frontmatter entry", page.getId()); + GuideDebugLog.warnAlways( + "[GuideNH] [ItemIndex] Page {} contains an empty item_ids frontmatter entry", + page.getId()); return List.of(); } return List.of(trimmed); diff --git a/src/main/java/com/hfstudio/guidenh/guide/indices/OreIndex.java b/src/main/java/com/hfstudio/guidenh/guide/indices/OreIndex.java index 2de90593..52d5b4e2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/indices/OreIndex.java +++ b/src/main/java/com/hfstudio/guidenh/guide/indices/OreIndex.java @@ -17,8 +17,7 @@ import com.hfstudio.guidenh.guide.GuidePageChange; import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * An index of Forge ore-dictionary names to the main guidebook page describing them. @@ -116,8 +115,8 @@ public static List> getOreAnchors(ParsedGuidePage page) } if (!(oreIdsNode instanceof ListoreIdList)) { - FMLLog.getLogger() - .warn("[GuideNH] [OreIndex] Page {} contains malformed ore_ids frontmatter", page.getId()); + GuideDebugLog + .warnAlways("[GuideNH] [OreIndex] Page {} contains malformed ore_ids frontmatter", page.getId()); return List.of(); } @@ -127,17 +126,17 @@ public static List> getOreAnchors(ParsedGuidePage page) if (listEntry instanceof String oreName) { String trimmed = oreName.trim(); if (trimmed.isEmpty()) { - FMLLog.getLogger() - .warn("[GuideNH] [OreIndex] Page {} contains an empty ore_ids frontmatter entry", page.getId()); + GuideDebugLog.warnAlways( + "[GuideNH] [OreIndex] Page {} contains an empty ore_ids frontmatter entry", + page.getId()); continue; } oreAnchors.add(Pair.of(trimmed, new PageAnchor(page.getId(), null))); } else { - FMLLog.getLogger() - .warn( - "[GuideNH] [OreIndex] Page {} contains a malformed ore_ids frontmatter entry: {}", - page.getId(), - listEntry); + GuideDebugLog.warnAlways( + "[GuideNH] [OreIndex] Page {} contains a malformed ore_ids frontmatter entry: {}", + page.getId(), + listEntry); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/indices/UniqueIndex.java b/src/main/java/com/hfstudio/guidenh/guide/indices/UniqueIndex.java index 153e5d23..3b98bda9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/indices/UniqueIndex.java +++ b/src/main/java/com/hfstudio/guidenh/guide/indices/UniqueIndex.java @@ -15,8 +15,7 @@ import com.google.gson.stream.JsonWriter; import com.hfstudio.guidenh.guide.GuidePageChange; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * Maintains an index for any given page using a mapping function for keys and values of the index. @@ -100,15 +99,14 @@ private void addToIndex(ParsedGuidePage page) { var value = entry.getValue(); var previousPage = index.putIfAbsent(key, new Record<>(page.getId(), value)); if (previousPage != null) { - FMLLog.getLogger() - .warn( - "[GuideNH] [UniqueIndex] Key conflict in index {}: {} is used by pages {} and {}; keeping {} and ignoring {}", - name, - key, - previousPage.pageId, - page, - previousPage.pageId, - page.getId()); + GuideDebugLog.warnAlways( + "[GuideNH] [UniqueIndex] Key conflict in index {}: {} is used by pages {} and {}; keeping {} and ignoring {}", + name, + key, + previousPage.pageId, + page, + previousPage.pageId, + page.getId()); hadDuplicates = true; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevelopmentResourcePackWatcher.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevelopmentResourcePackWatcher.java index 545fa20d..8130775f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevelopmentResourcePackWatcher.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideDevelopmentResourcePackWatcher.java @@ -9,12 +9,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.logging.log4j.Logger; - import com.hfstudio.guidenh.config.ModConfig; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.FMLLog; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.TickEvent; import io.methvin.watcher.DirectoryChangeEvent; @@ -147,17 +145,12 @@ public void processPendingReloadForTests() { } private static void logInfo(String message, Object... args) { - Logger logger = FMLLog.getLogger(); - if (logger != null && ModConfig.debug.enableDebugMode) { - logger.info("[GuideNH] [GuideDevelopmentResourcePackWatcher] " + message, args); - } + GuideDebugLog + .info(ModConfig.debug.enableDebugMode, "[GuideNH] [GuideDevelopmentResourcePackWatcher] " + message, args); } private static void logError(String message, Object arg, Throwable throwable) { - Logger logger = FMLLog.getLogger(); - if (logger != null) { - logger.error("[GuideNH] [GuideDevelopmentResourcePackWatcher] " + message, arg, throwable); - } + GuideDebugLog.error("[GuideNH] [GuideDevelopmentResourcePackWatcher] " + message, arg, throwable); } private final class Listener implements DirectoryChangeListener { @@ -176,11 +169,8 @@ public boolean isWatching() { @Override public void onException(Exception e) { - Logger logger = FMLLog.getLogger(); - if (logger != null) { - logger - .error("[GuideNH] [GuideDevelopmentResourcePackWatcher] Failed watching development resources", e); - } + GuideDebugLog + .error("[GuideNH] [GuideDevelopmentResourcePackWatcher] Failed watching development resources", e); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index ced81a21..8de7f20a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -15,7 +15,6 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.ClientProxy; -import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.datadriven.GuidePageResourceSelector; @@ -30,11 +29,10 @@ import com.hfstudio.guidenh.guide.mediawiki.MediaWikiTranslationStats; import com.hfstudio.guidenh.guide.render.GuidePageTexture; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCache; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.integration.structurelib.StructureLibElementTooltipResolver; import com.hfstudio.guidenh.integration.structurelib.StructureLibRuntimeFacade; -import cpw.mods.fml.common.FMLLog; - public class GuideLightweightReloadService { private GuideLightweightReloadService() {} @@ -48,10 +46,7 @@ public static void reloadDevelopmentGuides() { } public static void reloadGuides(IResourceManager resourceManager) { - if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info("[GuideNH] [GuideLightweightReloadService] Reloading guide data..."); - } + GuideDebugLog.info("[GuideNH] [GuideLightweightReloadService] Reloading guide data..."); long startedAt = System.nanoTime(); var activeResourcePacks = DataDrivenGuideLoader.getActiveResourcePacks(); RecipeCache.clear(); @@ -106,8 +101,8 @@ public static void reloadGuides(IResourceManager resourceManager) { GuideME.getSearch() .indexAll(); } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [GuideLightweightReloadService] Failed to reindex search after reload", t); + GuideDebugLog + .warnAlways("[GuideNH] [GuideLightweightReloadService] Failed to reindex search after reload", t); } long searchIndexNs = System.nanoTime() - stageStartedAt; @@ -115,17 +110,16 @@ public static void reloadGuides(IResourceManager resourceManager) { int loadedLanguageCount = countLoadedLanguages(guidePages); long totalNs = System.nanoTime() - startedAt; - FMLLog.getLogger() - .info( - "[GuideNH] [GuideLightweightReloadService] Guide reload complete, loaded {} guides, {} pages, {} languages in {} ns (dataDrivenLoadNs={}, pageLoadNs={}, registryUpdateNs={}, searchIndexNs={})", - guidePages.size(), - loadedPageCount, - loadedLanguageCount, - totalNs, - dataDrivenLoadNs, - pageLoadNs, - registryUpdateNs, - searchIndexNs); + GuideDebugLog.info( + "[GuideNH] [GuideLightweightReloadService] Guide reload complete, loaded {} guides, {} pages, {} languages in {} ns (dataDrivenLoadNs={}, pageLoadNs={}, registryUpdateNs={}, searchIndexNs={})", + guidePages.size(), + loadedPageCount, + loadedLanguageCount, + totalNs, + dataDrivenLoadNs, + pageLoadNs, + registryUpdateNs, + searchIndexNs); } /** @@ -173,8 +167,8 @@ static Map loadPages(IResourceManager resourc ParsedGuidePage parsed = loadResult != null ? loadResult.page() : null; if (parsed == null) { failedLoads++; - FMLLog.getLogger() - .warn("[GuideNH] [GuideLightweightReloadService] Failed to load guide page {}", pageId); + GuideDebugLog + .warnAlways("[GuideNH] [GuideLightweightReloadService] Failed to load guide page {}", pageId); continue; } switch (loadResult.kind()) { @@ -194,22 +188,19 @@ static Map loadPages(IResourceManager resourc } long totalNs = System.nanoTime() - startedAt; - if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideLightweightReloadService] Loaded {} pages for guide {} folder {} requestedLanguage={} defaultLanguage={} discoveredPaths={} localizedHits={} defaultLanguageHits={} rawSourceHits={} failedLoads={} durationNs={}", - pages.size(), - guideId, - folder, - lang, - defaultLanguage, - pagePaths.size(), - localizedHits, - defaultLanguageHits, - rawSourceHits, - failedLoads, - totalNs); - } + GuideDebugLog.info( + "[GuideNH] [GuideLightweightReloadService] Loaded {} pages for guide {} folder {} requestedLanguage={} defaultLanguage={} discoveredPaths={} localizedHits={} defaultLanguageHits={} rawSourceHits={} failedLoads={} durationNs={}", + pages.size(), + guideId, + folder, + lang, + defaultLanguage, + pagePaths.size(), + localizedHits, + defaultLanguageHits, + rawSourceHits, + failedLoads, + totalNs); return pages; } @@ -300,7 +291,7 @@ private static ParsedGuidePage parsePageBytes(String sourcePack, String language return GuideLocalizedPageSourceResolver .parseFrontmatterOnly(sourcePack, language, contentRootFolder, pageId, bytes); } catch (Exception ex) { - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [GuideLightweightReloadService] Error parsing page {} from {}", pageId, sourceId, ex); return null; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideME.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideME.java index 20f35f2a..830f9e87 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideME.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideME.java @@ -6,8 +6,7 @@ import com.hfstudio.guidenh.GuideNH; import com.hfstudio.guidenh.guide.internal.search.GuideSearch; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideME { @@ -44,8 +43,7 @@ public static synchronized void closeSearch() { try { SEARCH.close(); } catch (Exception e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideME] Failed to close GuideSearch", e); + GuideDebugLog.error("[GuideNH] [GuideME] Failed to close GuideSearch", e); } finally { SEARCH = null; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideOnStartup.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideOnStartup.java index 6b66ad77..df77fefe 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideOnStartup.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideOnStartup.java @@ -7,9 +7,9 @@ import net.minecraft.util.ResourceLocation; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.FMLLog; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.TickEvent; @@ -68,18 +68,16 @@ public void onClientTick(TickEvent.ClientTickEvent event) { for (ResourceLocation guideId : guidesToValidate) { MutableGuide guide = GuideRegistry.getById(guideId); if (guide == null) { - FMLLog.getLogger() - .error( - "[GuideNH] [GuideOnStartup] Cannot validate guide '{}' because it is not registered.", - guideId); + GuideDebugLog.error( + "[GuideNH] [GuideOnStartup] Cannot validate guide '{}' because it is not registered.", + guideId); continue; } try { guide.validateAll(); } catch (RuntimeException e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideOnStartup] Failed to validate guide '{}'", guideId, e); + GuideDebugLog.error("[GuideNH] [GuideOnStartup] Failed to validate guide '{}'", guideId, e); } } @@ -89,10 +87,9 @@ public void onClientTick(TickEvent.ClientTickEvent event) { MutableGuide guide = GuideRegistry.getById(showOnStartup.guideId()); if (guide == null) { - FMLLog.getLogger() - .error( - "[GuideNH] [GuideOnStartup] Cannot open guide '{}' because it is not registered.", - showOnStartup.guideId()); + GuideDebugLog.error( + "[GuideNH] [GuideOnStartup] Cannot open guide '{}' because it is not registered.", + showOnStartup.guideId()); return; } @@ -100,8 +97,7 @@ public void onClientTick(TickEvent.ClientTickEvent event) { PageAnchor anchor = showOnStartup.anchor(); GuideScreen.open(showOnStartup.guideId(), anchor); } catch (RuntimeException e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideOnStartup] Failed to open guide '{}'", showOnStartup.guideId(), e); + GuideDebugLog.error("[GuideNH] [GuideOnStartup] Failed to open guide '{}'", showOnStartup.guideId(), e); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideRegistry.java index bcab58f6..79706aa2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideRegistry.java @@ -15,8 +15,7 @@ import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.navigation.NavigationTree; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * Internal registry for Guides. @@ -104,11 +103,10 @@ public static void setDataDriven(Map guides) { dataDrivenGuides.clear(); dataDrivenGuides.putAll(guides); if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideRegistry] Replaced {} data-driven guides with {} freshly loaded guides", - previousCount, - dataDrivenGuides.size()); + GuideDebugLog.infoAlways( + "[GuideNH] [GuideRegistry] Replaced {} data-driven guides with {} freshly loaded guides", + previousCount, + dataDrivenGuides.size()); } rebuildGuides(); @@ -149,10 +147,9 @@ public static void rebuildGuides() { if (!overridden.isEmpty()) { overridden.sort(Comparator.comparing(ResourceLocation::toString)); if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideRegistry] The following guides are overridden in resource packs: {}", - overridden); + GuideDebugLog.infoAlways( + "[GuideNH] [GuideRegistry] The following guides are overridden in resource packs: {}", + overridden); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScopedView.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScopedView.java index 3f2def60..4eba9196 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScopedView.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScopedView.java @@ -25,8 +25,7 @@ import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialDataIndex; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSpecialDataIndexer; import com.hfstudio.guidenh.guide.navigation.NavigationTree; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideScopedView implements Guide, MediaWikiListContextProvider { @@ -145,11 +144,10 @@ public boolean isPageFailed(ResourceLocation pageId) { long startNanos = System.nanoTime(); mediaWikiListContext = createFallbackMediaWikiListContext(); if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideScopedView] Built preview MediaWikiListContext in {} ms for guide {}", - nanosToMillis(System.nanoTime() - startNanos), - delegate.getId()); + GuideDebugLog.infoAlways( + "[GuideNH] [GuideScopedView] Built preview MediaWikiListContext in {} ms for guide {}", + nanosToMillis(System.nanoTime() - startNanos), + delegate.getId()); } return mediaWikiListContext; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index e77a9f56..f73eb320 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -135,13 +135,13 @@ import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; import com.hfstudio.guidenh.guide.scene.annotation.DiamondAnnotation; import com.hfstudio.guidenh.guide.scene.support.GuideBlockDisplayResolver; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.guide.scene.support.GuideEntityDisplayResolver; import com.hfstudio.guidenh.guide.sound.GuideSoundPlayback; import com.hfstudio.guidenh.guide.ui.GuideUiHost; import com.hfstudio.guidenh.integration.nei.GuideScreenNeiBridge; import com.hfstudio.guidenh.libs.unist.UnistPoint; -import cpw.mods.fml.common.FMLLog; import cpw.mods.fml.common.Loader; public class GuideScreen extends GuiContainer @@ -566,7 +566,7 @@ private static GuideScreenViewState contentState(ResourceLocation guideId, @Null private static GuideScreenRoute contentRoute(ResourceLocation guideId, @Nullable PageAnchor anchor) { MutableGuide guide = GuideRegistry.getById(guideId); if (guide == null) { - FMLLog.warning("GuideScreen.open: no guide registered with id {}", guideId); + GuideDebugLog.warnAlways("GuideScreen.open: no guide registered with id {}", guideId); return null; } if (anchor == null) { @@ -1130,7 +1130,8 @@ private void createGuideEditorPageAtPath(MutableGuide activeGuide, ParsedGuidePa Path sourceRoot = guideEditorFileStore .findWritablePageResourcePackRoot(activeGuide, currentParsedPage.getId(), language); if (sourceRoot == null) { - FMLLog.warning("Failed to create guide editor page because current page has no writable resource pack"); + GuideDebugLog + .warnAlways("Failed to create guide editor page because current page has no writable resource pack"); return; } @@ -1147,7 +1148,7 @@ private void createGuideEditorPageAtPath(MutableGuide activeGuide, ParsedGuidePa refreshGuideEditorDraft(true); rebuildToolbar(); } catch (Throwable t) { - FMLLog.warning("Failed to create guide editor page from path {}", requestedPath, t); + GuideDebugLog.warnAlways("Failed to create guide editor page from path {}", requestedPath, t); } } @@ -1439,7 +1440,7 @@ private void rebuildGuideEditorPreview() { } cachedGuideEditorPreviewInteractionState = null; } catch (Throwable t) { - FMLLog.warning("Failed to compile guide editor preview for {}", currentAnchor.pageId(), t); + GuideDebugLog.warnAlways("Failed to compile guide editor preview for {}", currentAnchor.pageId(), t); } } @@ -1570,19 +1571,18 @@ private boolean saveGuideEditorDraft() { } updateToolbarButtonState(); if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideScreen] Saved guide editor draft for {} in {} ms (write: {} ms, parse: {} ms, stage: {} ms, reusedParsed={})", - currentAnchor.pageId(), - (System.nanoTime() - startedAt) / 1_000_000L, - saveFileNs / 1_000_000L, - parseNs / 1_000_000L, - stagePageApplyNs / 1_000_000L, - reusedGuideEditorParsedDraft(sourcePack, language)); + GuideDebugLog.infoAlways( + "[GuideNH] [GuideScreen] Saved guide editor draft for {} in {} ms (write: {} ms, parse: {} ms, stage: {} ms, reusedParsed={})", + currentAnchor.pageId(), + (System.nanoTime() - startedAt) / 1_000_000L, + saveFileNs / 1_000_000L, + parseNs / 1_000_000L, + stagePageApplyNs / 1_000_000L, + reusedGuideEditorParsedDraft(sourcePack, language)); } return true; } catch (Throwable t) { - FMLLog.warning("Failed to autosave guide editor page {}", currentAnchor.pageId(), t); + GuideDebugLog.warnAlways("Failed to autosave guide editor page {}", currentAnchor.pageId(), t); return false; } } @@ -1691,7 +1691,7 @@ private void refreshGuideEditorPreviewState() { scheduleGuideEditorNavigationRefresh(); } } catch (Throwable t) { - FMLLog.warning("Failed to refresh guide editor draft state for {}", currentAnchor.pageId(), t); + GuideDebugLog.warnAlways("Failed to refresh guide editor draft state for {}", currentAnchor.pageId(), t); } } @@ -1792,7 +1792,7 @@ private void updateGuideEditorNavigationRefresh() { GuideME.getSearch() .index(guide); } catch (Throwable t) { - FMLLog.warning("Guide editor navigation refresh failed", t); + GuideDebugLog.warnAlways("Guide editor navigation refresh failed", t); } } @@ -2349,7 +2349,7 @@ private void completePendingContentPageLoadIfNeeded() { try { loadedPage = guide.getPage(currentAnchor.pageId()); } catch (Throwable t) { - FMLLog.severe("Failed to compile guide page {}", currentAnchor.pageId(), t); + GuideDebugLog.error("Failed to compile guide page {}", currentAnchor.pageId(), t); loadedPage = null; } if (loadedPage != null) { @@ -2398,8 +2398,10 @@ private static void registerRuntimeScenes(GuidePage page) { } } if (found > 0) { - FMLLog.getLogger() - .info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); + GuideDebugLog.infoAlways( + "[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", + found, + list.size()); } } @@ -2725,12 +2727,12 @@ private static ResourceLocation getHomeLogoTexture() { try (InputStream inputStream = GuideScreen.class.getResourceAsStream(HOME_LOGO_RESOURCE_PATH)) { if (inputStream == null) { - FMLLog.warning("GuideScreen home logo resource not found at {}", HOME_LOGO_RESOURCE_PATH); + GuideDebugLog.warnAlways("GuideScreen home logo resource not found at {}", HOME_LOGO_RESOURCE_PATH); return null; } BufferedImage image = ImageIO.read(inputStream); if (image == null) { - FMLLog.warning("GuideScreen home logo failed to decode at {}", HOME_LOGO_RESOURCE_PATH); + GuideDebugLog.warnAlways("GuideScreen home logo failed to decode at {}", HOME_LOGO_RESOURCE_PATH); return null; } homeLogoWidth = image.getWidth(); @@ -2740,7 +2742,7 @@ private static ResourceLocation getHomeLogoTexture() { .getDynamicTextureLocation(HOME_LOGO_SOURCE.getResourcePath(), new DynamicTexture(image)); return homeLogoTexture; } catch (Exception e) { - FMLLog.warning("GuideScreen failed to load home logo from {}", HOME_LOGO_RESOURCE_PATH, e); + GuideDebugLog.warnAlways("GuideScreen failed to load home logo from {}", HOME_LOGO_RESOURCE_PATH, e); return null; } } @@ -2952,7 +2954,7 @@ private void renderGuideEditorPreview(int x, int y, int width, int height) { try { previewDocument.render(reusableRenderCtx); } catch (Throwable t) { - FMLLog.warning("Failed to render guide editor preview", t); + GuideDebugLog.warnAlways("Failed to render guide editor preview", t); } finally { GL11.glPopMatrix(); reusableRenderCtx.restoreExternalRenderState(); @@ -4058,7 +4060,7 @@ private void drawContentTooltip(ContentTooltip ct, int mouseX, int mouseY, ct.getContent() .render(ctx); } catch (Throwable t) { - FMLLog.warning("Error rendering ContentTooltip", t); + GuideDebugLog.warnAlways("Error rendering ContentTooltip", t); } finally { GL11.glPopMatrix(); ctx.restoreExternalRenderState(); @@ -4272,7 +4274,7 @@ private void renderDocument(int mouseX, int mouseY) { try { activeDocument.render(ctx); } catch (Throwable t) { - FMLLog.severe("Error rendering guide document {}", currentAnchor.pageId(), t); + GuideDebugLog.error("Error rendering guide document {}", currentAnchor.pageId(), t); } finally { GL11.glPopMatrix(); ctx.restoreExternalRenderState(); @@ -4440,8 +4442,7 @@ private boolean canSearchCurrentView() { private void drawTiledBackground() { drawRect(0, 0, this.width, this.height, BACKGROUND_DIM_COLOR); if (mc == null || mc.getTextureManager() == null) { - FMLLog.getLogger() - .warn("[GuideNH] drawTiledBackground: mc or textureManager is null, skipping"); + GuideDebugLog.warnAlways("[GuideNH] drawTiledBackground: mc or textureManager is null, skipping"); return; } mc.getTextureManager() @@ -5283,7 +5284,7 @@ private void openDirectory(Path directory) { return; } } catch (Exception e) { - FMLLog.warning("Failed to open guide directory {}", directory, e); + GuideDebugLog.warnAlways("Failed to open guide directory {}", directory, e); } tryOpenDirectoryWithCommand(directory); } @@ -5320,7 +5321,7 @@ private void revealFileTarget(Path fileTarget) { new ProcessBuilder(command).start(); return; } catch (Exception e) { - FMLLog.warning("Failed to reveal guide file {}", fileTarget, e); + GuideDebugLog.warnAlways("Failed to reveal guide file {}", fileTarget, e); } } Path parent = fileTarget.getParent(); @@ -5335,7 +5336,7 @@ private void revealFileTarget(Path fileTarget) { .open(fileTarget.toFile()); } } catch (Exception e) { - FMLLog.warning("Failed to open guide file {}", fileTarget, e); + GuideDebugLog.warnAlways("Failed to open guide file {}", fileTarget, e); } } @@ -5364,7 +5365,7 @@ private void tryOpenDirectoryWithCommand(Path directory) { try { new ProcessBuilder(command).start(); } catch (Exception e) { - FMLLog.warning("Failed to open guide directory {}", directory, e); + GuideDebugLog.warnAlways("Failed to open guide directory {}", directory, e); } } @@ -5983,7 +5984,7 @@ public boolean copyCodeBlock(String text) { codeBlockClipboardService.copy(text); return true; } catch (Exception e) { - FMLLog.severe("Failed to copy code block", e); + GuideDebugLog.error("Failed to copy code block", e); return false; } } @@ -5998,7 +5999,7 @@ private void browseExternalUrl(URI uri) { Desktop.getDesktop() .browse(uri); } catch (Exception e) { - FMLLog.warning("Failed to open external guide link {}", uri, e); + GuideDebugLog.warnAlways("Failed to open external guide link {}", uri, e); } } @@ -6391,7 +6392,7 @@ private LytDocument buildSearchDocument(String query) { clipSnippetForWidth(result.text(), getSearchSnippetLineWidth(textColumnWidth)))); } } catch (Throwable t) { - FMLLog.warning("Search failed", t); + GuideDebugLog.warnAlways("Search failed", t); } return GuideSearchResultDocumentBuilder diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java index f0eb6ec5..24fc7162 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideSourceWatcher.java @@ -25,15 +25,14 @@ import com.github.bsideup.jabel.Desugar; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.GuidePageChange; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.localization.GuideLocalizedPageSourceResolver; import com.hfstudio.guidenh.guide.internal.localization.GuidePageLanguageIndex; import com.hfstudio.guidenh.guide.internal.localization.GuideResourceLanguageIndex; import com.hfstudio.guidenh.guide.internal.util.LangUtil; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; -import cpw.mods.fml.common.FMLLog; import io.methvin.watcher.DirectoryChangeEvent; import io.methvin.watcher.DirectoryChangeListener; import io.methvin.watcher.DirectoryWatcher; @@ -205,10 +204,7 @@ public GuideSourceWatcher(String namespace, String contentRootFolder, String def if (!Files.isDirectory(sourceFolder)) { throw new RuntimeException("Cannot find the specified folder with guidebook sources: " + sourceFolder); } - if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info("[GuideNH] [GuideSourceWatcher] Watching guidebook sources in {}", sourceFolder); - } + GuideDebugLog.info("[GuideNH] [GuideSourceWatcher] Watching guidebook sources in {}", sourceFolder); watchExecutor = Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setDaemon(true) @@ -225,11 +221,10 @@ public GuideSourceWatcher(String namespace, String contentRootFolder, String def .listener(new Listener()) .build(); } catch (IOException e) { - FMLLog.getLogger() - .error( - "[GuideNH] [GuideSourceWatcher] Failed to watch for changes in the guidebook sources at {}", - sourceFolder, - e); + GuideDebugLog.error( + "[GuideNH] [GuideSourceWatcher] Failed to watch for changes in the guidebook sources at {}", + sourceFolder, + e); watcher = null; } sourceWatcher = watcher; @@ -273,33 +268,27 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { @Override public FileVisitResult visitFileFailed(Path file, IOException exc) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSourceWatcher] Failed to list page {}", file, exc); + GuideDebugLog.error("[GuideNH] [GuideSourceWatcher] Failed to list page {}", file, exc); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) { if (exc != null) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSourceWatcher] Failed to list all pages in {}", dir, exc); + GuideDebugLog.error("[GuideNH] [GuideSourceWatcher] Failed to list all pages in {}", dir, exc); } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSourceWatcher] Failed to list all pages in {}", sourceFolder, e); + GuideDebugLog.error("[GuideNH] [GuideSourceWatcher] Failed to list all pages in {}", sourceFolder, e); } long walkNs = System.nanoTime() - stageStartedAt; - if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideSourceWatcher] Loading {} guidebook pages from {} localized variants", - pageIds.size(), - pageSources.size()); - } + GuideDebugLog.info( + "[GuideNH] [GuideSourceWatcher] Loading {} guidebook pages from {} localized variants", + pageIds.size(), + pageSources.size()); stageStartedAt = System.nanoTime(); Map> localizedSourcesByNamespace = loadLocalizedSourceOverrides( currentLanguage, @@ -324,7 +313,7 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) { Files.readAllBytes(pageSource.path()), pageSource.localizedSourceOverride())); } catch (Exception e) { - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [GuideSourceWatcher] Failed to reload guidebook page {}", pageSource.path(), e); } } @@ -332,21 +321,18 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) { int sourceLanguageCount = countSourceLanguages(pageSources.keySet()); long totalNs = System.nanoTime() - startedAt; - if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideSourceWatcher] Loaded {} pages from {} with namespaceFilter={} sourceVariants={} sourceLanguages={} localizedNamespaces={} walkNs={} localizedOverrideNs={} parseNs={} totalNs={}", - loadedPages.size(), - sourceFolder, - namespaceFilter != null ? namespaceFilter : "", - pageSources.size(), - sourceLanguageCount, - localizedSourcesByNamespace.size(), - walkNs, - localizedOverrideNs, - parseNs, - totalNs); - } + GuideDebugLog.info( + "[GuideNH] [GuideSourceWatcher] Loaded {} pages from {} with namespaceFilter={} sourceVariants={} sourceLanguages={} localizedNamespaces={} walkNs={} localizedOverrideNs={} parseNs={} totalNs={}", + loadedPages.size(), + sourceFolder, + namespaceFilter != null ? namespaceFilter : "", + pageSources.size(), + sourceLanguageCount, + localizedSourcesByNamespace.size(), + walkNs, + localizedOverrideNs, + parseNs, + totalNs); return loadedPages; } @@ -460,7 +446,7 @@ public synchronized void close() { try { sourceWatcher.close(); } catch (IOException e) { - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [GuideSourceWatcher] Failed to close fileystem watcher for {}", sourceFolder); } } @@ -486,8 +472,7 @@ public boolean isWatching() { @Override public void onException(Exception e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSourceWatcher] Failed watching for changes", e); + GuideDebugLog.error("[GuideNH] [GuideSourceWatcher] Failed watching for changes", e); } } @@ -725,11 +710,10 @@ private void clearQueuedPageStates(Set pageIds) { activeSource.localizedSourceOverride()); return new ResolvedPageState(activeSource.requestedLanguage(), page, false); } catch (Exception e) { - FMLLog.getLogger() - .error( - "[GuideNH] [GuideSourceWatcher] Failed to resolve effective guidebook page {}", - activeSource.path(), - e); + GuideDebugLog.error( + "[GuideNH] [GuideSourceWatcher] Failed to resolve effective guidebook page {}", + activeSource.path(), + e); return null; } } @@ -769,8 +753,10 @@ private PageSource resolveActivePageSource(ResourceLocation pageId, String curre Map entries = GuidePageLanguageIndex.readPageKeys(input); return entries.get(GuideLocalizedPageSourceResolver.buildLangKey(contentRootFolder, pageId)); } catch (IOException e) { - FMLLog.getLogger() - .warn("[GuideNH] [GuideSourceWatcher] Failed to read localized page lang file {}", langFilePath, e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSourceWatcher] Failed to read localized page lang file {}", + langFilePath, + e); return null; } } @@ -808,11 +794,10 @@ private Set resolveNamespacesForLocalizedSources(@Nullable String namesp child.getFileName() .toString())); } catch (IOException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSourceWatcher] Failed to scan localized source namespaces in {}", - assetsPath, - e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSourceWatcher] Failed to scan localized source namespaces in {}", + assetsPath, + e); } return namespaces; } @@ -839,8 +824,10 @@ private Map loadLocalizedSourceOverridesForNamespace(String sour try (InputStream input = Files.newInputStream(langFilePath)) { return GuidePageLanguageIndex.readPageKeys(input); } catch (IOException e) { - FMLLog.getLogger() - .warn("[GuideNH] [GuideSourceWatcher] Failed to read localized page lang file {}", langFilePath, e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSourceWatcher] Failed to read localized page lang file {}", + langFilePath, + e); return Map.of(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideStartupOptions.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideStartupOptions.java index a68cede2..109dda44 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideStartupOptions.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideStartupOptions.java @@ -9,8 +9,7 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.PageAnchor; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideStartupOptions { @@ -39,7 +38,7 @@ public record ShowOnStartup(ResourceLocation guideId, @Nullable PageAnchor ancho return new ShowOnStartup(guideId, parseStartupAnchor(guideId, trimmedValue.substring(anchorSeparator + 1))); } catch (RuntimeException e) { - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [GuideStartupOptions] Failed to parse guideme.showOnStartup='{}'", trimmedValue, e); return null; } @@ -69,11 +68,10 @@ public static Set parseValidateAtStartup(@Nullable String valu try { result.add(new ResourceLocation(trimmedToken)); } catch (RuntimeException e) { - FMLLog.getLogger() - .error( - "[GuideNH] [GuideStartupOptions] Failed to parse validateAtStartup guide id '{}'", - trimmedToken, - e); + GuideDebugLog.error( + "[GuideNH] [GuideStartupOptions] Failed to parse validateAtStartup guide id '{}'", + trimmedToken, + e); } } if (end == length) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java index 61529eb7..c4e84cb6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/MutableGuide.java @@ -46,8 +46,7 @@ import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSyntheticPageFactory; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSyntheticPageFactory.SyntheticSourceSnapshot; import com.hfstudio.guidenh.guide.navigation.NavigationTree; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * Encapsulates a Guide, which consists of a collection of Markdown pages and associated content, loaded from a @@ -169,8 +168,7 @@ public T getIndex(Class indexClass) { @Nullable public ParsedGuidePage getParsedPage(ResourceLocation id) { if (pages == null) { - FMLLog.getLogger() - .warn("[GuideNH] [MutableGuide] Can't get page {}. Pages not loaded yet.", id); + GuideDebugLog.warnAlways("[GuideNH] [MutableGuide] Can't get page {}. Pages not loaded yet.", id); return null; } @@ -201,8 +199,7 @@ public GuidePage getPage(ResourceLocation id) { clearCompileFailure(id); } catch (Throwable t) { recordCompileFailure(id, buildCompileFailureText(id, t)); - FMLLog.getLogger() - .error("[GuideNH] [MutableGuide] Failed to compile guide page {}", id, t); + GuideDebugLog.error("[GuideNH] [MutableGuide] Failed to compile guide page {}", id, t); compiledPage = buildFailurePage(parsedPage, pageFailures.get(id)); } compiledPage.prepareForDisplay(); @@ -246,8 +243,7 @@ public byte[] loadAsset(ResourceLocation id) { try { return Files.readAllBytes(path); } catch (NoSuchFileException ignored) {} catch (IOException e) { - FMLLog.getLogger() - .error("[GuideNH] [MutableGuide] Failed to open guidebook asset {}", path); + GuideDebugLog.error("[GuideNH] [MutableGuide] Failed to open guidebook asset {}", path); return null; } } @@ -263,8 +259,7 @@ public byte[] loadAsset(ResourceLocation id) { return bytes; } - FMLLog.getLogger() - .error("[GuideNH] [MutableGuide] Failed to open guidebook asset {}", id); + GuideDebugLog.error("[GuideNH] [MutableGuide] Failed to open guidebook asset {}", id); return null; } @@ -361,13 +356,12 @@ public synchronized void close() { mediaWikiSpecialDataIndex = null; fallbackMediaWikiListContextRevision = Long.MIN_VALUE; requestedMediaWikiWarmupRevision = Long.MIN_VALUE; - FMLLog.getLogger() - .info( - "[GuideNH] [MutableGuide] Closed guide {} and cleared caches developmentPages={}, syntheticPages={}, failures={}", - id, - developmentPageCount, - syntheticPageCount, - failureCount); + GuideDebugLog.infoAlways( + "[GuideNH] [MutableGuide] Closed guide {} and cleared caches developmentPages={}, syntheticPages={}, failures={}", + id, + developmentPageCount, + syntheticPageCount, + failureCount); } private void applyChanges(List changes) { @@ -448,8 +442,7 @@ public void validateAll() { // Iterate and compile all pages to warn about errors on startup for (var entry : developmentPages.entrySet()) { if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info("[GuideNH] [MutableGuide] Compiling {}", entry.getKey()); + GuideDebugLog.infoAlways("[GuideNH] [MutableGuide] Compiling {}", entry.getKey()); } getPage(entry.getKey()); } @@ -628,12 +621,11 @@ private void rebuildSyntheticPages() { syntheticSourceCache, this::parseSyntheticPage); syntheticPages = Map.copyOf(rebuiltPages); - FMLLog.getLogger() - .info( - "[GuideNH] [MutableGuide] Rebuilt {} synthetic pages in {} ms for guide {}", - syntheticPages.size(), - nanosToMillis(System.nanoTime() - startNanos), - id); + GuideDebugLog.infoAlways( + "[GuideNH] [MutableGuide] Rebuilt {} synthetic pages in {} ms for guide {}", + syntheticPages.size(), + nanosToMillis(System.nanoTime() - startNanos), + id); } private void invalidateMediaWikiDerivedCaches() { @@ -682,12 +674,11 @@ private void requestMediaWikiDerivedCacheWarmup(long revision) { } NavigationTree navigationSnapshot = aggregatedGuide.getNavigationTree(); if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [MutableGuide] Scheduling MediaWiki cache warmup for guide {} revision {} with {} pages", - id, - revision, - pagesSnapshot.size()); + GuideDebugLog.infoAlways( + "[GuideNH] [MutableGuide] Scheduling MediaWiki cache warmup for guide {} revision {} with {} pages", + id, + revision, + pagesSnapshot.size()); } mediaWikiRefreshController.requestRefresh(revision, () -> { try { @@ -712,13 +703,12 @@ private void requestMediaWikiDerivedCacheWarmup(long revision) { fallbackMediaWikiListContextRevision = revision; } if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [MutableGuide] Warmed MediaWiki caches asynchronously in {} ms for guide {} revision {} with {} pages", - nanosToMillis(System.nanoTime() - startNanos), - id, - revision, - pagesSnapshot.size()); + GuideDebugLog.infoAlways( + "[GuideNH] [MutableGuide] Warmed MediaWiki caches asynchronously in {} ms for guide {} revision {} with {} pages", + nanosToMillis(System.nanoTime() - startNanos), + id, + revision, + pagesSnapshot.size()); } } catch (Throwable t) { synchronized (this) { @@ -726,12 +716,11 @@ private void requestMediaWikiDerivedCacheWarmup(long revision) { requestedMediaWikiWarmupRevision = Long.MIN_VALUE; } } - FMLLog.getLogger() - .warn( - "[GuideNH] [MutableGuide] Failed to warm MediaWiki caches asynchronously for guide {} revision {}", - id, - revision, - t); + GuideDebugLog.warnAlways( + "[GuideNH] [MutableGuide] Failed to warm MediaWiki caches asynchronously for guide {} revision {}", + id, + revision, + t); } }); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java index bf630f1a..31e7ed34 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java @@ -22,11 +22,11 @@ import com.hfstudio.guidenh.guide.internal.MutableGuide; import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; import com.hfstudio.guidenh.guide.internal.util.LangUtil; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.mixins.early.fml.AccessorFMLClientHandler; import com.hfstudio.guidenh.mixins.early.minecraft.AccessorAbstractResourcePack; import cpw.mods.fml.client.FMLClientHandler; -import cpw.mods.fml.common.FMLLog; public class DataDrivenGuideLoader { @@ -62,16 +62,15 @@ public static Map load() { int discoveredLanguageCount = countDiscoveredLanguages(discoveredLanguages); long totalNs = System.nanoTime() - startedAt; if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [DataDrivenGuideLoader] Loaded {} guides across {} languages from {} resource packs in {} ns (resourcePackResolveNs={}, scanNs={}, buildNs={})", - guides.size(), - discoveredLanguageCount, - activeResourcePacks.size(), - totalNs, - resourcePackResolveNs, - scanNs, - buildNs); + GuideDebugLog.infoAlways( + "[GuideNH] [DataDrivenGuideLoader] Loaded {} guides across {} languages from {} resource packs in {} ns (resourcePackResolveNs={}, scanNs={}, buildNs={})", + guides.size(), + discoveredLanguageCount, + activeResourcePacks.size(), + totalNs, + resourcePackResolveNs, + scanNs, + buildNs); } return guides; } @@ -87,14 +86,13 @@ public static LinkedHashMap> discoverPagePaths(Str long totalNs = System.nanoTime() - startedAt; if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [DataDrivenGuideLoader] Discovered {} page paths across {} namespaces for folder {} from {} resource packs in {} ns", - countDiscoveredPagePaths(pagePaths), - pagePaths.size(), - folder, - activeResourcePacks.size(), - totalNs); + GuideDebugLog.infoAlways( + "[GuideNH] [DataDrivenGuideLoader] Discovered {} page paths across {} namespaces for folder {} from {} resource packs in {} ns", + countDiscoveredPagePaths(pagePaths), + pagePaths.size(), + folder, + activeResourcePacks.size(), + totalNs); } return pagePaths; } @@ -204,11 +202,10 @@ private static void scanZipPagePathsAllNamespaces(File resourcePackFile, String } } } catch (IOException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [DataDrivenGuideLoader] Failed to scan guide pages from resource pack {}", - resourcePackFile.getAbsolutePath(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to scan guide pages from resource pack {}", + resourcePackFile.getAbsolutePath(), + e); } } @@ -248,10 +245,9 @@ public static List getActiveResourcePacks() { resourcePacks.addAll(basePacks); } } catch (RuntimeException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [DataDrivenGuideLoader] Failed to inspect the currently loaded base resource packs", - e); + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to inspect the currently loaded base resource packs", + e); } var repository = Minecraft.getMinecraft() @@ -311,11 +307,10 @@ public static File getResourcePackFile(IResourcePack resourcePack) { try { return ((AccessorAbstractResourcePack) resourcePack).guidenh$getResourcePackFile(); } catch (RuntimeException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [DataDrivenGuideLoader] Failed to resolve the backing file for resource pack {}", - resourcePack.getPackName(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to resolve the backing file for resource pack {}", + resourcePack.getPackName(), + e); return null; } } @@ -327,12 +322,11 @@ public static byte[] readBytes(IResourcePack resourcePack, ResourceLocation reso try (var input = resourcePack.getInputStream(resourceLocation)) { return GuideResourceAccess.readFully(input); } catch (IOException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [DataDrivenGuideLoader] Failed to read resource {} from resource pack {}", - resourceLocation, - resourcePack.getPackName(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to read resource {} from resource pack {}", + resourceLocation, + resourcePack.getPackName(), + e); return null; } } @@ -430,11 +424,10 @@ public static void scanResourcePackZip(File resourcePackFile, .add(toLanguageCode(languageFolder)); } } catch (IOException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [DataDrivenGuideLoader] Failed to scan guide languages from resource pack {}", - resourcePackFile.getAbsolutePath(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to scan guide languages from resource pack {}", + resourcePackFile.getAbsolutePath(), + e); } } @@ -484,11 +477,10 @@ public static void scanZipPagePaths(File resourcePackFile, String prefix, Set { if (Minecraft.getMinecraft().thePlayer == null) return; - FMLLog.getLogger() - .info("[GuideNH] [CommandLink] Sending command: {}", command); + GuideDebugLog.infoAlways("[GuideNH] [CommandLink] Sending command: {}", command); Minecraft.getMinecraft().thePlayer.sendChatMessage(command); if (Boolean.TRUE.equals(close)) { Minecraft.getMinecraft() diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java index 41d6da13..5e6a9145 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/MermaidScript.java @@ -14,8 +14,7 @@ import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapParser; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class MermaidScript implements LytScript { @@ -75,18 +74,17 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } // Dispatch MOUNT events into NodeContent subtrees (Recipe/BlockImage placeholders) if (ph.nodeContentBlocks != null) { - FMLLog.getLogger() - .info("[MermaidDebug] Dispatching into {} NodeContent blocks", ph.nodeContentBlocks.size()); + GuideDebugLog + .debug("[MermaidDebug] Dispatching into {} NodeContent blocks", ph.nodeContentBlocks.size()); for (var entry : ph.nodeContentBlocks.entrySet()) { var contentBlock = entry.getValue(); - FMLLog.getLogger() - .info( - "[MermaidDebug] NodeContent '{}' block type={} children={}", - entry.getKey(), - contentBlock.getClass() - .getSimpleName(), - contentBlock instanceof LytNode n ? n.getChildren() - .size() : -1); + GuideDebugLog.debug( + "[MermaidDebug] NodeContent '{}' block type={} children={}", + entry.getKey(), + contentBlock.getClass() + .getSimpleName(), + contentBlock instanceof LytNode n ? n.getChildren() + .size() : -1); if (contentBlock instanceof LytNode root) { ctx.dispatchSubtree(root); } @@ -94,8 +92,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { } ctx.replace(block); } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn("[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); + GuideDebugLog.error("[GuideNH] [MermaidScript] Failed to parse Mermaid source: {}", sourceText, e); replaceWithError(ctx, "Failed to parse: " + e.getMessage()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java index 4bd0f856..72a57af5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/RecipeScript.java @@ -28,13 +28,12 @@ import com.hfstudio.guidenh.guide.internal.recipe.LytNeiRecipeBox; import com.hfstudio.guidenh.guide.internal.recipe.RecipeCache; import com.hfstudio.guidenh.guide.internal.recipe.RecipeLookup; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.integration.api.GuideNhIntegrationRegistry; import com.hfstudio.guidenh.integration.api.RecipeEntry; import com.hfstudio.guidenh.integration.api.RecipeSlot; import com.hfstudio.guidenh.integration.nei.NeiRecipeLookup; -import cpw.mods.fml.common.FMLLog; - public class RecipeScript implements LytScript { @Override @@ -155,11 +154,9 @@ public List readIngredientSlots(Object h, int ri) { handlerPart = " with handler " + (ph.handlerName != null ? ph.handlerName : ph.handlerId); } showFallback(ctx, ph, "No recipe found for " + ph.idStr + handlerPart); - } else if (FMLLog.getLogger() - .isDebugEnabled()) { - FMLLog.getLogger() - .debug("Recipe handler filter eliminated all candidates for {}", ph.idStr); - } + } else if (GuideDebugLog.isDebugEnabled()) { + GuideDebugLog.debugAlways("Recipe handler filter eliminated all candidates for {}", ph.idStr); + } return; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java index d3bfe961..b086d61f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/SceneScript.java @@ -37,13 +37,12 @@ import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureCompileScope; import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstRoot; import com.hfstudio.guidenh.libs.unist.UnistNode; -import cpw.mods.fml.common.FMLLog; - public class SceneScript implements LytScript { public SceneScript() {} @@ -115,8 +114,7 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { MdAstToMdxConverter.convert(ast, Collections.emptyMap()); } } catch (Exception e) { - FMLLog.getLogger() - .warn("[GuideNH] [SceneScript] Failed to re-parse scene children", e); + GuideDebugLog.error("[GuideNH] [SceneScript] Failed to parse scene children", e); ctx.replace(LytParagraph.error("[Scene] Failed to parse scene elements")); return; } @@ -301,8 +299,7 @@ private static class ExceptionCollector implements LytErrorSink { @Override public void appendError(PageCompiler compiler, String text, UnistNode node) { - FMLLog.getLogger() - .warn("[GuideNH] [SceneScript] {}", text); + GuideDebugLog.warnAlways("[GuideNH] [SceneScript] {}", text); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java index 95fb9336..2c7e76b2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java @@ -19,8 +19,7 @@ import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.util.LangUtil; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuidePageLanguageIndex { @@ -72,13 +71,12 @@ private static Map loadLanguage(String normalizedLanguage) { } long totalNs = System.nanoTime() - startedAt; if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuidePageLanguageIndex] Loaded {} page language keys for language {} from {} resource packs in {} ns", - merged.size(), - normalizedLanguage, - activeResourcePacks.size(), - totalNs); + GuideDebugLog.infoAlways( + "[GuideNH] [GuidePageLanguageIndex] Loaded {} page language keys for language {} from {} resource packs in {} ns", + merged.size(), + normalizedLanguage, + activeResourcePacks.size(), + totalNs); } return merged.isEmpty() ? Map.of() : Map.copyOf(merged); } @@ -131,8 +129,10 @@ private static void loadDirectoryLanguageEntries(File directory, String normaliz try (InputStream input = new FileInputStream(child)) { mergePageKeys(input, target); } catch (IOException e) { - FMLLog.getLogger() - .warn("[GuideNH] [GuidePageLanguageIndex] Failed to read lang file {}", child.getAbsolutePath(), e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuidePageLanguageIndex] Failed to read lang file {}", + child.getAbsolutePath(), + e); } } } @@ -161,11 +161,10 @@ private static void loadZipLanguage(File resourcePackFile, String normalizedLang } } } catch (IOException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuidePageLanguageIndex] Failed to scan lang entries from resource pack {}", - resourcePackFile.getAbsolutePath(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuidePageLanguageIndex] Failed to scan lang entries from resource pack {}", + resourcePackFile.getAbsolutePath(), + e); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java index 6216c481..1a5a36c3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java @@ -19,8 +19,7 @@ import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.util.LangUtil; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideResourceLanguageIndex { @@ -50,13 +49,12 @@ private static Map load(String normalizedLanguage) { } long totalNs = System.nanoTime() - startedAt; if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideResourceLanguageIndex] Loaded {} lang entries for language {} from {} resource packs in {} ns", - merged.size(), - normalizedLanguage, - activeResourcePacks.size(), - totalNs); + GuideDebugLog.infoAlways( + "[GuideNH] [GuideResourceLanguageIndex] Loaded {} lang entries for language {} from {} resource packs in {} ns", + merged.size(), + normalizedLanguage, + activeResourcePacks.size(), + totalNs); } return merged.isEmpty() ? Map.of() : Map.copyOf(merged); } @@ -109,11 +107,10 @@ private static void loadDirectoryLanguageEntries(File directory, String normaliz try (InputStream input = new FileInputStream(child)) { target.putAll(StringTranslate.parseLangFile(input)); } catch (IOException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideResourceLanguageIndex] Failed to read lang file {}", - child.getAbsolutePath(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideResourceLanguageIndex] Failed to read lang file {}", + child.getAbsolutePath(), + e); } } } @@ -142,11 +139,10 @@ private static void loadZipLanguage(File resourcePackFile, String normalizedLang } } } catch (IOException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideResourceLanguageIndex] Failed to scan lang entries from resource pack {}", - resourcePackFile.getAbsolutePath(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideResourceLanguageIndex] Failed to scan lang entries from resource pack {}", + resourcePackFile.getAbsolutePath(), + e); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/FileTreeCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/FileTreeCompiler.java index ad21478b..65534014 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/FileTreeCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/FileTreeCompiler.java @@ -11,8 +11,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.FileTreeParser.FileTreeIcon; import com.hfstudio.guidenh.guide.internal.markdown.FileTreeParser.FileTreeIconKind; import com.hfstudio.guidenh.guide.internal.markdown.FileTreeParser.FileTreeModel; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * Turns a textual file tree string into a {@link LytFileTree} block. Each entry payload is @@ -61,17 +60,15 @@ private static LytBlock buildIconBlock(PageCompiler compiler, FileTreeIcon icon) var imageId = IdUtils.resolveLink(value, compiler.getPageId()); var imageContent = compiler.loadAsset(imageId); if (imageContent == null) { - FMLLog.getLogger() - .warn("[GuideNH] [FileTreeCompiler] File tree iconPng not found: {}", value); + GuideDebugLog.warnAlways("[GuideNH] [FileTreeCompiler] File tree iconPng not found: {}", value); image.setTitle("Missing image: " + value); } image.setImage(imageId, imageContent); } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [FileTreeCompiler] File tree iconPng has invalid id '{}': {}", - value, - e.getMessage()); + GuideDebugLog.warnAlways( + "[GuideNH] [FileTreeCompiler] File tree iconPng has invalid id '{}': {}", + value, + e.getMessage()); image.setTitle("Invalid image: " + value); } return image; @@ -82,8 +79,8 @@ private static LytBlock buildIconBlock(PageCompiler compiler, FileTreeIcon icon) compiler.getPageId() .getResourceDomain()); if (stack == null) { - FMLLog.getLogger() - .warn("[GuideNH] [FileTreeCompiler] File tree iconItem could not be resolved: {}", value); + GuideDebugLog + .warnAlways("[GuideNH] [FileTreeCompiler] File tree iconItem could not be resolved: {}", value); LytParagraph fallback = new LytParagraph(); fallback.setMarginTop(0); fallback.setMarginBottom(0); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/search/GuideSearch.java b/src/main/java/com/hfstudio/guidenh/guide/internal/search/GuideSearch.java index 28c97d97..3089fb5f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/search/GuideSearch.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/search/GuideSearch.java @@ -39,7 +39,6 @@ import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; -import com.hfstudio.guidenh.config.ModConfig; import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.Guides; import com.hfstudio.guidenh.guide.compiler.IndexingSink; @@ -48,10 +47,9 @@ import com.hfstudio.guidenh.guide.internal.util.LangUtil; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageIds; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageTitleResolver; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.libs.unist.UnistNode; -import cpw.mods.fml.common.FMLLog; - /** * Manages the in-memory Lucene index for guide search. */ @@ -105,8 +103,7 @@ public void index(Guide guide) { guide.getId() .toString())); } catch (IOException e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSearch] Failed to delete all documents before re-indexing.", e); + GuideDebugLog.error("[GuideNH] [GuideSearch] Failed to delete all documents before re-indexing.", e); } if (pendingTasks.isEmpty()) { @@ -171,8 +168,7 @@ public void processWork(long budgetNanos) { try { indexWriter.addDocument(pageDoc); } catch (IOException e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSearch] Failed to index document {}{}", guide, page, e); + GuideDebugLog.error("[GuideNH] [GuideSearch] Failed to index document {}{}", guide, page, e); } var searchLang = pageDoc.get(IndexSchema.FIELD_SEARCH_LANG); @@ -195,13 +191,10 @@ public void processWork(long budgetNanos) { throw new UncheckedIOException(e); } - if (ModConfig.debug.enableDebugMode) { - FMLLog.getLogger() - .info( - "[GuideNH] [GuideSearch] Indexing of {} pages finished in {}", - pagesIndexed, - Duration.between(indexingStarted, Instant.now())); - } + GuideDebugLog.info( + "[GuideNH] [GuideSearch] Indexing of {} pages finished in {}", + pagesIndexed, + Duration.between(indexingStarted, Instant.now())); } public void processAllWork() { @@ -238,8 +231,7 @@ public List searchGuide(String queryText, @Nullable Guide onlyFrom try { query = GuideQueryParser.parse(queryText, analyzer, indexedLanguages); } catch (Exception e) { - FMLLog.getLogger() - .debug("[GuideNH] [GuideSearch] Failed to parse search query: '{}'", queryText, e); + GuideDebugLog.debug("[GuideNH] [GuideSearch] Failed to parse search query: '{}'", queryText, e); return List.of(); } @@ -256,15 +248,13 @@ public List searchGuide(String queryText, @Nullable Guide onlyFrom .build(); } - FMLLog.getLogger() - .debug("[GuideNH] [GuideSearch] Running GuideME search query: {}", query); + GuideDebugLog.debug("[GuideNH] [GuideSearch] Running GuideME search query: {}", query); TopDocs topDocs; try { topDocs = indexSearcher.search(query, 25); } catch (IOException e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSearch] Failed to search for '{}'", queryText, e); + GuideDebugLog.error("[GuideNH] [GuideSearch] Failed to search for '{}'", queryText, e); return List.of(); } @@ -278,20 +268,18 @@ public List searchGuide(String queryText, @Nullable Guide onlyFrom var guide = Guides.getById(guideId); if (guide == null) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSearch] Search index produced guide id {} which couldn't be found.", - guideId); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSearch] Search index produced guide id {} which couldn't be found.", + guideId); continue; } var page = guide.getParsedPage(pageId); if (page == null) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSearch] Search index produced page {} in guide {}, which couldn't be found.", - pageId, - guideId); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSearch] Search index produced page {} in guide {}, which couldn't be found.", + pageId, + guideId); continue; } @@ -305,8 +293,7 @@ public List searchGuide(String queryText, @Nullable Guide onlyFrom bestFragment = ""; } } catch (InvalidTokenOffsetsException e) { - FMLLog.getLogger() - .error("[GuideNH] [GuideSearch] Cannot determine text to highlight for result", e); + GuideDebugLog.error("[GuideNH] [GuideSearch] Cannot determine text to highlight for result", e); } var pageTitle = document.get(IndexSchema.FIELD_TITLE); @@ -386,10 +373,9 @@ private String getLuceneLanguageFromMinecraft(String language) { var luceneLang = Analyzers.MINECRAFT_TO_LUCENE_LANG.get(language); if (luceneLang == null) { if (warnedAboutLanguage.add(language)) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSearch] Minecraft language '{}' is unknown, so search falls back to english.", - language); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSearch] Minecraft language '{}' is unknown, so search falls back to english.", + language); } return Analyzers.LANG_ENGLISH; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java index a05ed861..bb3c2190 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/search/PageIndexer.java @@ -14,6 +14,7 @@ import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.extensions.ExtensionPoint; import com.hfstudio.guidenh.guide.indices.PageIndex; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.libs.mdast.MdAstYamlFrontmatter; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; @@ -22,8 +23,6 @@ import com.hfstudio.guidenh.libs.mdast.model.MdAstText; import com.hfstudio.guidenh.libs.unist.UnistNode; -import cpw.mods.fml.common.FMLLog; - public class PageIndexer implements IndexingContext { private final PageCollection pages; @@ -65,8 +64,9 @@ public void indexContent(MdAstAnyContent content, IndexingSink sink) { } else if (content instanceof MdxJsxElementFields el) { var compiler = tagCompilers.get(el.name()); if (compiler == null) { - FMLLog.getLogger() - .warn("[GuideNH] [PageIndexer] Unhandled MDX element in guide search indexing: {}", el.name()); + GuideDebugLog.warnAlways( + "[GuideNH] [PageIndexer] Unhandled MDX element in guide search indexing: {}", + el.name()); // Fallback: index children content indexContent(el.children(), sink); } else { @@ -75,10 +75,9 @@ public void indexContent(MdAstAnyContent content, IndexingSink sink) { } else if (content instanceof MdAstDefinition || content instanceof MdAstYamlFrontmatter) { // Handled via conversion } else { - FMLLog.getLogger() - .warn( - "[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}", - ((UnistNode) content).type()); + GuideDebugLog.warnAlways( + "[GuideNH] [PageIndexer] Unhandled node type in guide search indexing: {}", + ((UnistNode) content).type()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/util/NavigationUtil.java b/src/main/java/com/hfstudio/guidenh/guide/internal/util/NavigationUtil.java index 251b4be2..d5c5380a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/util/NavigationUtil.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/util/NavigationUtil.java @@ -16,8 +16,7 @@ import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.compiler.YamlNbtConverter; import com.hfstudio.guidenh.guide.render.GuidePageTexture; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class NavigationUtil { @@ -108,7 +107,7 @@ private static ItemStack resolveItemStack(ParsedGuidePage page, String itemId, i @Nullable java.util.Map nbt) { var item = (Item) Item.itemRegistry.getObject(itemId); if (item == null) { - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [NavigationUtil] Couldn't find icon item {} for page {}", itemId, page.getId()); return null; } @@ -124,13 +123,13 @@ private static ItemStack resolveItemStack(ParsedGuidePage page, String itemId, i private static GuidePageTexture loadTexture(ParsedGuidePage page, PageCollection pages, ResourceLocation iconId) { var data = pages.loadAsset(iconId); if (data == null || data.length == 0) { - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [NavigationUtil] Couldn't find icon texture {} for page {}", iconId, page.getId()); return null; } var texture = GuidePageTexture.load(iconId, data); if (texture.isMissing()) { - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [NavigationUtil] Couldn't decode icon texture {} for page {}", iconId, page.getId()); return null; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/latex/GuideLatexRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/latex/GuideLatexRenderer.java index 191f40f1..a0f8e8e7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/latex/GuideLatexRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/latex/GuideLatexRenderer.java @@ -17,7 +17,7 @@ import org.scilab.forge.jlatexmath.TeXFormula; import org.scilab.forge.jlatexmath.TeXIcon; -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideLatexRenderer { @@ -55,8 +55,8 @@ public int calibrateRefHeight(float sourceScale) { int h = icon.getIconHeight(); return Math.max(1, h); } catch (ParseException e) { - FMLLog.getLogger() - .warn("[GuideNH/LaTeX] Failed to calibrate reference height for scale {}", sourceScale, e); + GuideDebugLog + .warnAlways("[GuideNH/LaTeX] Failed to calibrate reference height for scale {}", sourceScale, e); return 16; } }); @@ -120,13 +120,11 @@ public int[] measureSize(String formula, int fillColorArgb, float sourceScale) { GuideLatexTextureCache.INSTANCE.putSize(sizeKey, w, h, d); return new int[] { w, h, d }; } catch (ParseException e) { - FMLLog.getLogger() - .warn("[GuideNH/LaTeX] Parse error measuring '{}': {}", formula, e.getMessage()); + GuideDebugLog.warnAlways("[GuideNH/LaTeX] Parse error measuring '{}': {}", formula, e.getMessage()); GuideLatexTextureCache.INSTANCE.markFailed(formula, e.getMessage()); return null; } catch (Exception e) { - FMLLog.getLogger() - .warn("[GuideNH/LaTeX] Unexpected error measuring '{}': {}", formula, e.getMessage(), e); + GuideDebugLog.warnAlways("[GuideNH/LaTeX] Unexpected error measuring '{}': {}", formula, e.getMessage(), e); GuideLatexTextureCache.INSTANCE.markFailed(formula, e.getMessage()); return null; } @@ -177,13 +175,11 @@ public int[] getOrCreateTexture(String formula, int fillColorArgb, float sourceS return new int[] { textureId, w, h }; } catch (ParseException e) { - FMLLog.getLogger() - .warn("[GuideNH/LaTeX] Parse error rendering '{}': {}", formula, e.getMessage()); + GuideDebugLog.warnAlways("[GuideNH/LaTeX] Parse error rendering '{}': {}", formula, e.getMessage()); GuideLatexTextureCache.INSTANCE.markFailed(formula, e.getMessage()); return null; } catch (Exception e) { - FMLLog.getLogger() - .warn("[GuideNH/LaTeX] Unexpected error rendering '{}': {}", formula, e.getMessage(), e); + GuideDebugLog.warnAlways("[GuideNH/LaTeX] Unexpected error rendering '{}': {}", formula, e.getMessage(), e); GuideLatexTextureCache.INSTANCE.markFailed( formula, e.getMessage() == null ? e.getClass() diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiCategoryParser.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiCategoryParser.java index 36f7479f..d1a1675f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiCategoryParser.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiCategoryParser.java @@ -7,8 +7,7 @@ import java.util.Map; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class MediaWikiCategoryParser { @@ -88,8 +87,7 @@ private static MediaWikiCategoryReference parseReference(ParsedGuidePage page, O } private static void warnMalformedCategories(ParsedGuidePage page, String message) { - FMLLog.getLogger() - .warn("[GuideNH] [MediaWikiCategoryParser] Page {} {}", page.getId(), message); + GuideDebugLog.warnAlways("[GuideNH] [MediaWikiCategoryParser] Page {} {}", page.getId(), message); } private static String normalizeCategoryKey(String categoryName) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialDataIndexer.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialDataIndexer.java index cafad3cc..d18c716b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialDataIndexer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialDataIndexer.java @@ -30,11 +30,10 @@ import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; import com.hfstudio.guidenh.guide.internal.datadriven.GuidePageResourceSelector; import com.hfstudio.guidenh.guide.internal.util.LangUtil; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.integration.betterquesting.QuestIndex; import com.hfstudio.guidenh.libs.unist.UnistPoint; -import cpw.mods.fml.common.FMLLog; - public class MediaWikiSpecialDataIndexer { private static final Pattern EXTERNAL_LINK_PATTERN = Pattern.compile("https?://[^\\s)>\\]]+"); @@ -97,29 +96,27 @@ public MediaWikiSpecialDataIndex build(Guide guide, Collection Map.copyOf(overrides), Set.copyOf(unusedFiles)); long totalElapsedNanos = System.nanoTime() - totalStartNanos; - FMLLog.getLogger() - .info( - "[GuideNH] [MediaWikiSpecialDataIndexer] Built special data index for {} pages in {} ms (assets: {} ms, usage: {} ms, metadata: {} ms)", - normalPages.size(), - nanosToMillis(totalElapsedNanos), - nanosToMillis(assetElapsedNanos), - nanosToMillis(usageElapsedNanos), - nanosToMillis(metadataElapsedNanos)); - FMLLog.getLogger() - .info( - "[GuideNH] [MediaWikiSpecialDataIndexer] Details assets={}, assetAliases={}, fileUsageKeys={}, translations={}, propertyPages={}, externalLinkPages={}, pageSizes={}, lintPages={}, lintIssues={}, ambiguousBindings={}, overrides={}, unusedFiles={}", - assetSizesById.size(), - assetVariantsByReference.size(), - fileUsageByPath.size(), - translations.size(), - pageProperties.size(), - externalLinks.size(), - pageSizes.size(), - lintIssues.size(), - countLintIssues(lintIssues), - ambiguousBindings.size(), - overrides.size(), - unusedFiles.size()); + GuideDebugLog.infoAlways( + "[GuideNH] [MediaWikiSpecialDataIndexer] Built special data index for {} pages in {} ms (assets: {} ms, usage: {} ms, metadata: {} ms)", + normalPages.size(), + nanosToMillis(totalElapsedNanos), + nanosToMillis(assetElapsedNanos), + nanosToMillis(usageElapsedNanos), + nanosToMillis(metadataElapsedNanos)); + GuideDebugLog.infoAlways( + "[GuideNH] [MediaWikiSpecialDataIndexer] Details assets={}, assetAliases={}, fileUsageKeys={}, translations={}, propertyPages={}, externalLinkPages={}, pageSizes={}, lintPages={}, lintIssues={}, ambiguousBindings={}, overrides={}, unusedFiles={}", + assetSizesById.size(), + assetVariantsByReference.size(), + fileUsageByPath.size(), + translations.size(), + pageProperties.size(), + externalLinks.size(), + pageSizes.size(), + lintIssues.size(), + countLintIssues(lintIssues), + ambiguousBindings.size(), + overrides.size(), + unusedFiles.size()); return dataIndex; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSyntheticPageFactory.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSyntheticPageFactory.java index 76777a97..9da6b0fc 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSyntheticPageFactory.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSyntheticPageFactory.java @@ -12,8 +12,7 @@ import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.indices.CategoryIndex; import com.hfstudio.guidenh.guide.internal.MutableGuide; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class MediaWikiSyntheticPageFactory { @@ -49,13 +48,12 @@ public static Map buildPages(Guide guide, Col .removeIf(pageId -> !seenIds.contains(pageId)); long categoryElapsedNanos = System.nanoTime() - categoryStartNanos; long totalElapsedNanos = System.nanoTime() - startNanos; - FMLLog.getLogger() - .info( - "[GuideNH] [MediaWikiSyntheticPageFactory] Built {} synthetic pages in {} ms (special: {} ms, category: {} ms)", - syntheticPages.size(), - nanosToMillis(totalElapsedNanos), - nanosToMillis(specialElapsedNanos), - nanosToMillis(categoryElapsedNanos)); + GuideDebugLog.infoAlways( + "[GuideNH] [MediaWikiSyntheticPageFactory] Built {} synthetic pages in {} ms (special: {} ms, category: {} ms)", + syntheticPages.size(), + nanosToMillis(totalElapsedNanos), + nanosToMillis(specialElapsedNanos), + nanosToMillis(categoryElapsedNanos)); return syntheticPages; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java b/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java index ad414dbc..9e9cce21 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java +++ b/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java @@ -21,8 +21,8 @@ import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.internal.MutableGuide; import com.hfstudio.guidenh.guide.internal.util.NavigationUtil; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; -import cpw.mods.fml.common.FMLLog; import cpw.mods.fml.common.Loader; public class NavigationTree { @@ -267,10 +267,9 @@ public static NavigationNode createNode(Map no } if (!parents.add(pageId)) { - FMLLog.getLogger() - .error( - "[GuideNH] [NavigationTree] Detected a cycle in the navigation tree parent-child relationship for page {}", - pageId); + GuideDebugLog.error( + "[GuideNH] [NavigationTree] Detected a cycle in the navigation tree parent-child relationship for page {}", + pageId); return null; } @@ -279,7 +278,7 @@ public static NavigationNode createNode(Map no if (page == null) { // These children had a parent that doesn't exist - FMLLog.getLogger() + GuideDebugLog .error("[GuideNH] [NavigationTree] Pages {} had unknown navigation parent {}", children, pageId); return null; } @@ -344,17 +343,16 @@ private static NavigationNode createMergedNode(Map resetViewToInitialCamera(); case PONDER_PREV_KEYFRAME -> { - FMLLog.getLogger() - .info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); + GuideDebugLog.info("[PonderDebug] activateSceneButton: PONDER_PREV_KEYFRAME"); ponderPrevKeyframe(); } case PONDER_PLAY_PAUSE -> { - FMLLog.getLogger() - .info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); + GuideDebugLog.info("[PonderDebug] activateSceneButton: PONDER_PLAY_PAUSE"); ponderTogglePlay(); } case PONDER_RESTART -> { - FMLLog.getLogger() - .info("[PonderDebug] activateSceneButton: PONDER_RESTART"); + GuideDebugLog.info("[PonderDebug] activateSceneButton: PONDER_RESTART"); ponderRestart(); } default -> {} @@ -4453,23 +4449,21 @@ public void ponderTick() { sceneAnimationTick++; if (ponderSceneData == null || ponderPaused || ponderFinished) { if (sceneAnimationTick % 100 == 1) { - FMLLog.getLogger() - .info( - "[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", - ponderSceneData != null, - ponderPaused, - ponderFinished, - ponderCurrentTick); + GuideDebugLog.info( + "[PonderDebug] ponderTick blocked: data={} paused={} finished={} tick={}", + ponderSceneData != null, + ponderPaused, + ponderFinished, + ponderCurrentTick); } return; } ponderCurrentTick++; if (ponderCurrentTick % 20 == 0) { - FMLLog.getLogger() - .info( - "[PonderDebug] ponderTick advancing: tick={}/{}", - ponderCurrentTick, - ponderSceneData.getTotalTime()); + GuideDebugLog.info( + "[PonderDebug] ponderTick advancing: tick={}/{}", + ponderCurrentTick, + ponderSceneData.getTotalTime()); } if (ponderCurrentTick >= ponderSceneData.getTotalTime()) { ponderCurrentTick = ponderSceneData.getTotalTime(); @@ -4496,16 +4490,14 @@ public void ponderTick() { public void ponderTogglePlay() { if (ponderSceneData == null) { - FMLLog.getLogger() - .info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); + GuideDebugLog.info("[PonderDebug] ponderTogglePlay blocked: no ponderSceneData"); return; } - FMLLog.getLogger() - .info( - "[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", - ponderPaused, - ponderFinished, - ponderCurrentTick); + GuideDebugLog.info( + "[PonderDebug] ponderTogglePlay: paused={} finished={} tick={}", + ponderPaused, + ponderFinished, + ponderCurrentTick); if (ponderFinished) { ponderCurrentTick = 0; ponderFinished = false; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java index 243e6926..f8c1fdb9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/SceneTagCompiler.java @@ -21,6 +21,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.scene.cache.GuideSceneStructureFingerprintResolver; import com.hfstudio.guidenh.guide.scene.element.SceneElementTagCompiler; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstNode; @@ -28,8 +29,6 @@ import com.hfstudio.guidenh.libs.unist.UnistNode; import com.hfstudio.guidenh.libs.unist.UnistParent; -import cpw.mods.fml.common.FMLLog; - public class SceneTagCompiler extends BlockTagCompiler { private static final String[] SCENE_ROOT_TAG_NAMES = { "GameScene", "Scene" }; @@ -135,8 +134,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl preParsedAst = MdAst.fromMarkdown(childrenSource, GuideMarkdownOptions.runtime()); MdAstToMdxConverter.convert(preParsedAst, Collections.emptyMap()); } catch (RuntimeException e) { - FMLLog.getLogger() - .warn("[GuideNH] [SceneTagCompiler] Failed to pre-parse scene children", e); + GuideDebugLog + .error("[GuideNH] [SceneTagCompiler] Failed to parse scene children during pre-processing", e); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java b/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java index 8db30b94..4d8fd745 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java @@ -17,6 +17,10 @@ public static boolean isEnabled() { return ModConfig.debug.enableDebugMode; } + public static boolean isDebugEnabled() { + return isEnabled(); + } + public static void run(@Nullable Runnable action) { if (!isEnabled() || action == null) { return; @@ -33,8 +37,34 @@ public static void runOnce(@Nullable Set onceKeys, @Nullable String key, } } + public static void error(@Nullable CharSequence message, Object... args) { + if (message == null || message.length() <= 0) { + return; + } + FMLLog.getLogger() + .error(message.toString(), args); + } + + public static void error(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { + error(message, args); + } + + public static void warn(boolean enabled, @Nullable CharSequence message, Object... args) { + if (!enabled) { + return; + } + warnAlways(message, args); + } + public static void warn(@Nullable CharSequence message, Object... args) { - if (!isEnabled() || message == null || message.length() <= 0) { + if (!isEnabled()) { + return; + } + warnAlways(message, args); + } + + public static void warnAlways(@Nullable CharSequence message, Object... args) { + if (message == null || message.length() <= 0) { return; } FMLLog.getLogger() @@ -45,8 +75,26 @@ public static void warn(@Nullable Logger ignoredLogger, @Nullable String message warn(message, args); } + public static void warnAlways(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { + warnAlways(message, args); + } + + public static void info(boolean enabled, @Nullable CharSequence message, Object... args) { + if (!enabled) { + return; + } + infoAlways(message, args); + } + public static void info(@Nullable CharSequence message, Object... args) { - if (!isEnabled() || message == null || message.length() <= 0) { + if (!isEnabled()) { + return; + } + infoAlways(message, args); + } + + public static void infoAlways(@Nullable CharSequence message, Object... args) { + if (message == null || message.length() <= 0) { return; } FMLLog.getLogger() @@ -57,8 +105,26 @@ public static void info(@Nullable Logger ignoredLogger, @Nullable String message info(message, args); } + public static void infoAlways(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { + infoAlways(message, args); + } + + public static void debug(boolean enabled, @Nullable CharSequence message, Object... args) { + if (!enabled) { + return; + } + debugAlways(message, args); + } + public static void debug(@Nullable CharSequence message, Object... args) { - if (!isEnabled() || message == null || message.length() <= 0) { + if (!isEnabled()) { + return; + } + debugAlways(message, args); + } + + public static void debugAlways(@Nullable CharSequence message, Object... args) { + if (message == null || message.length() <= 0) { return; } FMLLog.getLogger() @@ -68,4 +134,8 @@ public static void debug(@Nullable CharSequence message, Object... args) { public static void debug(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { debug(message, args); } + + public static void debugAlways(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { + debugAlways(message, args); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java index 1611fc2a..efd7c49c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java @@ -22,8 +22,7 @@ import com.google.gson.GsonBuilder; import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class ExportTask { @@ -64,8 +63,7 @@ public Result run() throws IOException { .toString()); ok++; } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [ExportTask] Failed to export page {}", page.getId(), t); + GuideDebugLog.warnAlways("[GuideNH] [ExportTask] Failed to export page {}", page.getId(), t); failed++; } } @@ -86,8 +84,7 @@ public Result run() throws IOException { } assetsCopied++; } catch (IOException e) { - FMLLog.getLogger() - .debug("[GuideNH] [ExportTask] Skipping missing asset {}", id, e); + GuideDebugLog.debugAlways("[GuideNH] [ExportTask] Skipping missing asset {}", id, e); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteExportTask.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteExportTask.java index eb93ac38..52796f01 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteExportTask.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteExportTask.java @@ -50,12 +50,11 @@ import com.hfstudio.guidenh.guide.scene.SceneBlockStatsEntry; import com.hfstudio.guidenh.guide.scene.SceneSoundCue; import com.hfstudio.guidenh.guide.scene.StructureLibSceneBinding; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.guide.sound.GuideSoundSpec; import com.hfstudio.guidenh.integration.structurelib.StructureLibPreviewSelection; import com.hfstudio.guidenh.integration.structurelib.StructureLibSceneMetadata; -import cpw.mods.fml.common.FMLLog; - public class GuideSiteExportTask { public static final Gson GSON = new GsonBuilder().disableHtmlEscaping() @@ -124,11 +123,10 @@ public Result run() throws Exception { try { variants = collector.collect(guide, discoveredLanguages); } catch (Throwable t) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSiteExportTask] Failed to collect page variants for guide {}", - guide.getId(), - t); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSiteExportTask] Failed to collect page variants for guide {}", + guide.getId(), + t); recordFailure(outDir, "collect " + guide.getId(), t); pagesFailed++; continue; @@ -294,12 +292,11 @@ public Result run() throws Exception { } pagesExported++; } catch (Throwable t) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSiteExportTask] Failed to export page {} for language {}", - variant.pageId(), - language, - t); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSiteExportTask] Failed to export page {} for language {}", + variant.pageId(), + language, + t); recordFailure(outDir, "page " + variant.pageId() + " (" + language + ")", t); pagesFailed++; } @@ -346,10 +343,9 @@ private static void switchMinecraftLanguage(String requested) { } } if (target == null) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSiteExportTask] Cannot switch Minecraft locale to {}: no matching Language registered", - requested); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSiteExportTask] Cannot switch Minecraft locale to {}: no matching Language registered", + requested); return; } Language current = manager.getCurrentLanguage(); @@ -365,8 +361,8 @@ private static void switchMinecraftLanguage(String requested) { manager.onResourceManagerReload(rm); } } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [GuideSiteExportTask] Failed to switch Minecraft locale to {}", requested, t); + GuideDebugLog + .warnAlways("[GuideNH] [GuideSiteExportTask] Failed to switch Minecraft locale to {}", requested, t); } } @@ -389,8 +385,7 @@ private static void restoreMinecraftLanguage(Language original) { manager.onResourceManagerReload(rm); } } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [GuideSiteExportTask] Failed to restore original Minecraft locale", t); + GuideDebugLog.warnAlways("[GuideNH] [GuideSiteExportTask] Failed to restore original Minecraft locale", t); } } @@ -776,12 +771,11 @@ private List exportScenes(MutableGuide guide, ParsedGuid exportedScenesByScene.put(scene, exportedScene); } } catch (Throwable t) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSiteExportTask] Failed to export scene for page {} in guide {}", - parsedPage.getId(), - guide.getId(), - t); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSiteExportTask] Failed to export scene for page {} in guide {}", + parsedPage.getId(), + guide.getId(), + t); } } @@ -1366,11 +1360,10 @@ private void addUniquePonderTickState(List states, int tick) { } private void warnSceneStateVariantLimit(long variantCount) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSiteExportTask] Skipping scene state manifest export because {} variants exceed limit {}.", - variantCount, - MAX_SCENE_STATE_VARIANTS); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSiteExportTask] Skipping scene state manifest export because {} variants exceed limit {}.", + variantCount, + MAX_SCENE_STATE_VARIANTS); } private List buildTierStates(StructureLibSceneBinding binding, StructureLibSceneMetadata metadata) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHrefResolver.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHrefResolver.java index 518b5188..8ba594fc 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHrefResolver.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHrefResolver.java @@ -17,8 +17,7 @@ import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiExternalLinkSupport; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiPageIds; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideSiteHrefResolver { @@ -62,12 +61,11 @@ public static String resolveRawHref(@Nullable ResourceLocation currentPageId, St : resolveTargetPageId(currentPageId, target); return resolvePageAnchor(currentPageId, new PageAnchor(targetPageId, fragment)); } catch (IllegalArgumentException e) { - FMLLog.getLogger() - .debug( - "[GuideNH] [GuideSiteHrefResolver] Failed to resolve href {} from page {}", - href, - currentPageId, - e); + GuideDebugLog.debugAlways( + "[GuideNH] [GuideSiteHrefResolver] Failed to resolve href {} from page {}", + href, + currentPageId, + e); return href; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteItemIconExporter.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteItemIconExporter.java index e92d245f..2fb5bea0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteItemIconExporter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteItemIconExporter.java @@ -18,7 +18,7 @@ import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideSiteItemIconExporter implements GuideSiteItemIconResolver { @@ -53,11 +53,10 @@ public synchronized String exportIcon(@Nullable ItemStack stack) { exportedIcons.put(cacheKey, exportedPath); return exportedPath; } catch (Throwable t) { - FMLLog.getLogger() - .debug( - "[GuideNH] [GuideSiteItemIconExporter] Failed to export offline icon for {}", - GuideSiteItemSupport.itemId(stack), - t); + GuideDebugLog.debugAlways( + "[GuideNH] [GuideSiteItemIconExporter] Failed to export offline icon for {}", + GuideSiteItemSupport.itemId(stack), + t); exportedIcons.put(cacheKey, ""); return ""; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteLatexExporter.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteLatexExporter.java index 4e3a6a78..6852e8b2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteLatexExporter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteLatexExporter.java @@ -16,7 +16,7 @@ import org.scilab.forge.jlatexmath.TeXFormula; import org.scilab.forge.jlatexmath.TeXIcon; -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideSiteLatexExporter { @@ -55,19 +55,17 @@ public ExportedLatex export(String formula, int fillColorArgb, float sourceScale exports.put(key, exported); return exported; } catch (ParseException e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSiteLatexExporter] Failed to parse LaTeX formula '{}': {}", - formula, - e.getMessage()); + GuideDebugLog.error( + "[GuideNH] [GuideSiteLatexExporter] Failed to parse LaTeX formula '{}': {}", + formula, + e.getMessage()); return null; } catch (Exception e) { - FMLLog.getLogger() - .warn( - "[GuideNH] [GuideSiteLatexExporter] Failed to export LaTeX formula '{}': {}", - formula, - e.getMessage(), - e); + GuideDebugLog.warnAlways( + "[GuideNH] [GuideSiteLatexExporter] Failed to export LaTeX formula '{}': {}", + formula, + e.getMessage(), + e); return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteNeiPhase1BackgroundExporter.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteNeiPhase1BackgroundExporter.java index 718e153a..9b2e4aff 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteNeiPhase1BackgroundExporter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteNeiPhase1BackgroundExporter.java @@ -18,11 +18,10 @@ import com.hfstudio.guidenh.guide.internal.recipe.LytNeiRecipeBox; import com.hfstudio.guidenh.guide.internal.recipe.NeiRecipeLayoutMetrics; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.integration.nei.NeiRecipeLookup; import com.hfstudio.guidenh.integration.neicustomdiagram.NeiCustomDiagramBridge; -import cpw.mods.fml.common.FMLLog; - /** * Renders NEI handler Phase1 ({@code drawBackground} / optionally {@code drawForeground} / * {@code drawExtras}) off-screen and writes a PNG shared asset for static site overlays. @@ -78,11 +77,10 @@ public GuideSiteNeiPhase1BackgroundExporter(GuideSiteAssetRegistry assets) { int vw = bodyW + 2 * m; int vh = bodyH + 2 * m; if (vw > MAX_EXPORT_EDGE || vh > MAX_EXPORT_EDGE) { - FMLLog.getLogger() - .debug( - "[GuideNH] [GuideSiteNeiPhase1BackgroundExporter] Skip NEI Phase1 export: {}x{} exceeds cap", - vw, - vh); + GuideDebugLog.debugAlways( + "[GuideNH] [GuideSiteNeiPhase1BackgroundExporter] Skip NEI Phase1 export: {}x{} exceeds cap", + vw, + vh); return null; } @@ -100,12 +98,11 @@ public GuideSiteNeiPhase1BackgroundExporter(GuideSiteAssetRegistry assets) { cache.put(cacheKey, res); return res; } catch (Throwable t) { - FMLLog.getLogger() - .debug( - "[GuideNH] [GuideSiteNeiPhase1BackgroundExporter] NEI Phase1 snapshot failed for {} recipe {}", - handler.getClass(), - recipeIndex, - t); + GuideDebugLog.debugAlways( + "[GuideNH] [GuideSiteNeiPhase1BackgroundExporter] NEI Phase1 snapshot failed for {} recipe {}", + handler.getClass(), + recipeIndex, + t); return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageAssetExporter.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageAssetExporter.java index 34d9bfb9..99a6cbe9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageAssetExporter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageAssetExporter.java @@ -9,8 +9,7 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.compiler.IdUtils; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideSitePageAssetExporter { @@ -40,12 +39,11 @@ public String resolveImageSrc(String rawUrl, @Nullable ResourceLocation currentP try { uri = URI.create(rawUrl); } catch (Exception e) { - FMLLog.getLogger() - .debug( - "[GuideNH] [GuideSitePageAssetExporter] Failed to parse image source {} from page {}", - rawUrl, - currentPageId, - e); + GuideDebugLog.error( + "[GuideNH] [GuideSitePageAssetExporter] Failed to parse image source {} from page {}", + rawUrl, + currentPageId, + e); uri = null; } if (uri != null && uri.isAbsolute()) { @@ -60,12 +58,11 @@ public String resolveImageSrc(String rawUrl, @Nullable ResourceLocation currentP String exportedPath = exportResource(imageId); return exportedPath.isEmpty() ? rawUrl : exportedPath; } catch (Exception e) { - FMLLog.getLogger() - .debug( - "[GuideNH] [GuideSitePageAssetExporter] Failed to resolve image source {} from page {}", - rawUrl, - currentPageId, - e); + GuideDebugLog.debug( + "[GuideNH] [GuideSitePageAssetExporter] Failed to resolve image source {} from page {}", + rawUrl, + currentPageId, + e); return rawUrl; } } @@ -104,7 +101,7 @@ private String exportUncached(ResourceLocation assetId, String bucket) { String exportedPath = assets.writeShared(bucket, extensionOf(assetId), content); return ROOT_PREFIX + exportedPath; } catch (Exception e) { - FMLLog.getLogger() + GuideDebugLog .debug("[GuideNH] [GuideSitePageAssetExporter] Failed to export {} resource {}", bucket, assetId, e); return ""; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java index 8a2e8fd4..52d1e59d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java @@ -22,8 +22,7 @@ import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSyntheticPageFactory; import com.hfstudio.guidenh.guide.mediawiki.MediaWikiSyntheticPageFactory.SyntheticSourceSnapshot; import com.hfstudio.guidenh.guide.navigation.NavigationTree; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideSitePageCollector { @@ -55,11 +54,10 @@ public List collect(MutableGuide guide, @Nullable List(); } } @@ -79,11 +77,10 @@ public List collect(MutableGuide guide, @Nullable List(); } for (ParsedGuidePage page : guide.getPages()) { @@ -157,8 +154,9 @@ public static List discoverLanguagesOrEmpty() { try { return discoverLanguages(); } catch (Throwable t) { - FMLLog.getLogger() - .debug("[GuideNH] [GuideSitePageCollector] Falling back to no discovered site export languages", t); + GuideDebugLog.debugAlways( + "[GuideNH] [GuideSitePageCollector] Falling back to no discovered site export languages", + t); return new ArrayList<>(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneRuntimeExporter.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneRuntimeExporter.java index 080099c7..b01402e7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneRuntimeExporter.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneRuntimeExporter.java @@ -28,8 +28,8 @@ import com.hfstudio.guidenh.guide.scene.GuidebookLevelRenderer; import com.hfstudio.guidenh.guide.scene.GuidebookSceneLayerSelection; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; -import cpw.mods.fml.common.FMLLog; import guideme.flatbuffers.scene.ExpAnimatedTexturePart; import guideme.flatbuffers.scene.ExpAnimatedTexturePartFrame; import guideme.flatbuffers.scene.ExpCameraSettings; @@ -134,11 +134,10 @@ private byte[] exportScenePayload(LytGuidebookScene scene) throws Exception { GuideSiteSceneTessellatorCapture.RecordingResult result = recorder.finish(); if (result.meshes.isEmpty()) { - FMLLog.getLogger() - .warn( - "Scene site export captured no tessellated meshes for a {}x{} scene; exported 3D preview will be blank.", - width, - height); + GuideDebugLog.warnAlways( + "Scene site export captured no tessellated meshes for a {}x{} scene; exported 3D preview will be blank.", + width, + height); } return encodeScene(scene.getCamera(), result); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java index da4f8c71..e5e908de 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java @@ -23,7 +23,8 @@ import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; + import guideme.flatbuffers.scene.ExpDepthTest; import guideme.flatbuffers.scene.ExpIndexElementType; import guideme.flatbuffers.scene.ExpPrimitiveType; @@ -110,10 +111,9 @@ public void setCurrentSourceTextureId(@Nullable String currentSourceTextureId) { public void startDrawing(int drawMode) { if (drawing) { // A previous batch was not properly closed, so drop it before recording a new one. - FMLLog.getLogger() - .warn( - "Scene capture startDrawing called while already drawing (mode={}); discarding previous unclosed batch", - drawMode); + GuideDebugLog.warnAlways( + "Scene capture startDrawing called while already drawing (mode={}); discarding previous unclosed batch", + drawMode); drawing = false; currentVertices.clear(); } @@ -150,8 +150,7 @@ public int draw() { captureCurrentMesh(); } } catch (Throwable e) { - FMLLog.getLogger() - .warn("Scene capture mesh export failed ({} vertices)", vertexCount, e); + GuideDebugLog.warnAlways("Scene capture mesh export failed ({} vertices)", vertexCount, e); } finally { currentVertices.clear(); } @@ -335,12 +334,11 @@ private TextureExport exportCurrentTexture() throws Exception { int level0Width = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_WIDTH); int level0Height = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_HEIGHT); if (level0Width <= 0 || level0Height <= 0) { - FMLLog.getLogger() - .warn( - "exportCurrentTexture: bound texture id={} has invalid level-0 dimensions {}x{}; skipping", - textureId, - level0Width, - level0Height); + GuideDebugLog.warnAlways( + "exportCurrentTexture: bound texture id={} has invalid level-0 dimensions {}x{}; skipping", + textureId, + level0Width, + level0Height); return null; } @@ -361,22 +359,20 @@ private TextureExport exportCurrentTexture() throws Exception { exportHeight = lh; if (lw <= MAX_EXPORT_TEXTURE_SIZE && lh <= MAX_EXPORT_TEXTURE_SIZE) break; } - FMLLog.getLogger() - .debug( - "exportCurrentTexture: texture id={} is {}x{} - using mip level {} ({}x{}) for site export", - textureId, - level0Width, - level0Height, - exportMipLevel, - exportWidth, - exportHeight); + GuideDebugLog.debugAlways( + "exportCurrentTexture: texture id={} is {}x{} - using mip level {} ({}x{}) for site export", + textureId, + level0Width, + level0Height, + exportMipLevel, + exportWidth, + exportHeight); } else { - FMLLog.getLogger() - .debug( - "exportCurrentTexture: exporting texture id={} ({}x{})", - textureId, - exportWidth, - exportHeight); + GuideDebugLog.debugAlways( + "exportCurrentTexture: exporting texture id={} ({}x{})", + textureId, + exportWidth, + exportHeight); } ByteBuffer pixels = BufferUtils.createByteBuffer(exportWidth * exportHeight * 4); diff --git a/src/main/java/com/hfstudio/guidenh/guide/sound/GuideSoundPlayback.java b/src/main/java/com/hfstudio/guidenh/guide/sound/GuideSoundPlayback.java index 2f45cb3a..3d034684 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/sound/GuideSoundPlayback.java +++ b/src/main/java/com/hfstudio/guidenh/guide/sound/GuideSoundPlayback.java @@ -13,7 +13,7 @@ import org.jetbrains.annotations.Nullable; -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideSoundPlayback { @@ -114,7 +114,6 @@ private static void warnLowFrequency(ResourceLocation soundId, RuntimeException return; } LAST_WARNED_AT.put(key, now); - FMLLog.getLogger() - .warn("[GuideNH] [GuideSoundPlayback] Failed to play sound {}", soundId, e); + GuideDebugLog.warnAlways("[GuideNH] [GuideSoundPlayback] Failed to play sound {}", soundId, e); } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/QuestIndex.java b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/QuestIndex.java index 54f6b4d4..f095084d 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/betterquesting/QuestIndex.java +++ b/src/main/java/com/hfstudio/guidenh/integration/betterquesting/QuestIndex.java @@ -12,8 +12,7 @@ import com.hfstudio.guidenh.guide.PageAnchor; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.indices.UniqueIndex; - -import cpw.mods.fml.common.FMLLog; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; /** * An index of BetterQuesting quest ids to the main guidebook page describing them. @@ -65,8 +64,8 @@ public static List> getQuestAnchors(ParsedGuidePage page) } if (!(questIdsNode instanceof ListquestIdList)) { - FMLLog.getLogger() - .warn("[GuideNH] [QuestIndex] Page {} contains malformed quest_ids frontmatter", page.getId()); + GuideDebugLog + .warnAlways("[GuideNH] [QuestIndex] Page {} contains malformed quest_ids frontmatter", page.getId()); return List.of(); } @@ -77,26 +76,25 @@ public static List> getQuestAnchors(ParsedGuidePage page) if (listEntry instanceof String questIdStr) { String trimmed = questIdStr.trim(); if (trimmed.isEmpty()) { - FMLLog.getLogger() - .warn("[GuideNH] [QuestIndex] Page {} contains an empty quest_ids frontmatter entry", pageId); + GuideDebugLog.warnAlways( + "[GuideNH] [QuestIndex] Page {} contains an empty quest_ids frontmatter entry", + pageId); continue; } UUID parsed = QuestIdParser.parse(trimmed); if (parsed == null) { - FMLLog.getLogger() - .warn( - "[GuideNH] [QuestIndex] Page {} contains a malformed quest_ids frontmatter entry: {}", - pageId, - trimmed); + GuideDebugLog.warnAlways( + "[GuideNH] [QuestIndex] Page {} contains a malformed quest_ids frontmatter entry: {}", + pageId, + trimmed); continue; } anchors.add(Pair.of(parsed, new PageAnchor(pageId, null))); } else { - FMLLog.getLogger() - .warn( - "[GuideNH] [QuestIndex] Page {} contains a malformed quest_ids frontmatter entry: {}", - pageId, - listEntry); + GuideDebugLog.warnAlways( + "[GuideNH] [QuestIndex] Page {} contains a malformed quest_ids frontmatter entry: {}", + pageId, + listEntry); } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java index f0cdb36e..7427fc32 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java +++ b/src/main/java/com/hfstudio/guidenh/integration/nei/NeiRecipeLookup.java @@ -7,10 +7,9 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.integration.Mods; -import cpw.mods.fml.common.FMLLog; - public class NeiRecipeLookup { public static final boolean AVAILABLE; @@ -18,9 +17,8 @@ public class NeiRecipeLookup { static { boolean ok = false; if (!Mods.NotEnoughItems.isModLoaded()) { - FMLLog.getLogger() - .info( - "[GuideNH] [NeiRecipeLookup] NEI mod not loaded; GuideNH recipe rendering falls back to vanilla."); + GuideDebugLog.infoAlways( + "[GuideNH] [NeiRecipeLookup] NEI mod not loaded; GuideNH recipe rendering falls back to vanilla."); } else { try { Class.forName( @@ -29,10 +27,9 @@ public class NeiRecipeLookup { NeiRecipeLookup.class.getClassLoader()); ok = true; } catch (Throwable t) { - FMLLog.getLogger() - .warn( - "[GuideNH] [NeiRecipeLookup] NEI API incompatible; recipe rendering falls back to vanilla. Reason: {}", - t.toString()); + GuideDebugLog.warnAlways( + "[GuideNH] [NeiRecipeLookup] NEI API incompatible; recipe rendering falls back to vanilla. Reason: {}", + t.toString()); } } AVAILABLE = ok; @@ -101,8 +98,7 @@ public static List findCraftingRecipeRefs(ItemStack target) { } return out; } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [NeiRecipeLookup] NEI crafting refs query failed", t); + GuideDebugLog.warnAlways("[GuideNH] [NeiRecipeLookup] NEI crafting refs query failed", t); return List.of(); } } @@ -121,8 +117,7 @@ public static List findUsages(ItemStack target) { try { return processHandlers(NeiDirectCalls.getUsageHandlers(target)); } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [NeiRecipeLookup] NEI usage query failed", t); + GuideDebugLog.warnAlways("[GuideNH] [NeiRecipeLookup] NEI usage query failed", t); return List.of(); } } @@ -136,8 +131,7 @@ public static List queryRawCraftingHandlers(ItemStack target) { try { return NeiDirectCalls.getCraftingHandlers(target); } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [NeiRecipeLookup] queryRawCraftingHandlers failed", t); + GuideDebugLog.warnAlways("[GuideNH] [NeiRecipeLookup] queryRawCraftingHandlers failed", t); return List.of(); } } @@ -151,8 +145,7 @@ public static List queryRawUsageHandlers(ItemStack target) { try { return NeiDirectCalls.getUsageHandlers(target); } catch (Throwable t) { - FMLLog.getLogger() - .warn("[GuideNH] [NeiRecipeLookup] queryRawUsageHandlers failed", t); + GuideDebugLog.warnAlways("[GuideNH] [NeiRecipeLookup] queryRawUsageHandlers failed", t); return List.of(); } } @@ -380,8 +373,7 @@ public static void drawHandlerImage(Object drawable, int x, int y) { } return out; } catch (Throwable t) { - FMLLog.getLogger() - .debug("[GuideNH] [NeiRecipeLookup] NEI handler {} read failed", handler.getClass(), t); + GuideDebugLog.debugAlways("[GuideNH] [NeiRecipeLookup] NEI handler {} read failed", handler.getClass(), t); return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/neicustomdiagram/NeiCustomDiagramBridge.java b/src/main/java/com/hfstudio/guidenh/integration/neicustomdiagram/NeiCustomDiagramBridge.java index 85e34895..78efbffe 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/neicustomdiagram/NeiCustomDiagramBridge.java +++ b/src/main/java/com/hfstudio/guidenh/integration/neicustomdiagram/NeiCustomDiagramBridge.java @@ -19,10 +19,9 @@ import com.hfstudio.guidenh.guide.internal.recipe.NeiHandlerRenderer; import com.hfstudio.guidenh.guide.internal.tooltip.AppendedItemTooltip; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.integration.Mods; -import cpw.mods.fml.common.FMLLog; - public class NeiCustomDiagramBridge { private static final String DIAGRAM_GROUP_CLASS_NAME = "com.github.dcysteine.neicustomdiagram.api.diagram.DiagramGroup"; @@ -217,8 +216,9 @@ public class NeiCustomDiagramBridge { .getMethod("get"); available = true; } catch (Throwable t) { - FMLLog.getLogger() - .debug("[GuideNH] [NeiCustomDiagramBridge] nei-custom-diagram bridge unavailable: {}", t.toString()); + GuideDebugLog.debugAlways( + "[GuideNH] [NeiCustomDiagramBridge] nei-custom-diagram bridge unavailable: {}", + t.toString()); } AVAILABLE = available; @@ -331,8 +331,8 @@ public static void renderEmbedded(Object handler, int recipeIndex, int renderX, METHOD_DIAGRAM_DRAW_BACKGROUND.invoke(diagram, diagramState); renderForeground(diagram, diagramState, guiScissorAbsX, guiScissorAbsY, gw, gh); } catch (Throwable t) { - FMLLog.getLogger() - .debug("[GuideNH] [NeiCustomDiagramBridge] Embedded nei-custom-diagram render failed", t); + GuideDebugLog + .debugAlways("[GuideNH] [NeiCustomDiagramBridge] Embedded nei-custom-diagram render failed", t); } finally { GL11.glPopMatrix(); GL11.glPopAttrib(); @@ -368,8 +368,8 @@ public static GuideTooltip getEmbeddedTooltip(Object handler, int recipeIndex, i return lines.isEmpty() ? null : new TextTooltip(String.join("\n", lines)); } } catch (Throwable t) { - FMLLog.getLogger() - .debug("[GuideNH] [NeiCustomDiagramBridge] Embedded nei-custom-diagram tooltip lookup failed", t); + GuideDebugLog + .debugAlways("[GuideNH] [NeiCustomDiagramBridge] Embedded nei-custom-diagram tooltip lookup failed", t); } return null; } From a4380416827c3c72fb77781a5851f9cbe9850d48 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:11:25 +0800 Subject: [PATCH 117/136] refactor code highlighting --- .../guide/compiler/tags/PreCompiler.java | 6 +- .../block/CodeHighlightFlowBuilder.java | 61 +++ .../guide/document/block/LytCodeBlock.java | 364 +++------------- .../document/block/LytCodeBlockToolbar.java | 16 +- .../markdown/CodeBlockLanguageDetector.java | 2 +- .../markdown/CodeBlockLanguageRegistry.java | 87 +++- .../markdown/highlight/CodeHighlightLine.java | 8 + .../markdown/highlight/CodeHighlightMode.java | 7 + .../highlight/CodeHighlightResult.java | 13 + .../highlight/CodeHighlightTheme.java | 87 ++++ .../highlight/CodeHighlightToken.java | 6 + .../markdown/highlight/CodeHighlighter.java | 110 +++++ .../markdown/highlight/CodeTokenType.java | 15 + .../markdown/highlight/LanguageTokenizer.java | 6 + .../markdown/highlight/TokenizerSupport.java | 108 +++++ .../tokenizer/JavaLikeTokenizer.java | 403 ++++++++++++++++++ .../highlight/tokenizer/JsonTokenizer.java | 87 ++++ .../highlight/tokenizer/LuaTokenizer.java | 112 +++++ .../tokenizer/MarkdownTokenizer.java | 68 +++ .../tokenizer/PlainTextTokenizer.java | 14 + .../tokenizer/PropertiesTokenizer.java | 76 ++++ .../highlight/tokenizer/ShellTokenizer.java | 117 +++++ .../highlight/tokenizer/XmlTokenizer.java | 107 +++++ .../highlight/tokenizer/YamlTokenizer.java | 132 ++++++ .../site/CodeHighlightHtmlRenderer.java | 101 +++++ .../site/GuideSiteCodeBlockRenderer.java | 46 ++ .../site/GuideSiteHtmlCompiler.java | 41 +- .../site/GuideSiteMdxTagRenderer.java | 3 +- .../assets/guidenh/siteexport/app.css | 91 ++++ 29 files changed, 1915 insertions(+), 379 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/document/block/CodeHighlightFlowBuilder.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightLine.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightMode.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightResult.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightTheme.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightToken.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlighter.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeTokenType.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/LanguageTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/TokenizerSupport.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JavaLikeTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JsonTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/LuaTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/MarkdownTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PlainTextTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PropertiesTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/ShellTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/XmlTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/YamlTokenizer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/siteexport/site/CodeHighlightHtmlRenderer.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteCodeBlockRenderer.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java index 0bdcec1a..21161f50 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java @@ -80,9 +80,8 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // Default code block with syntax highlighting LytCodeBlock codeBlock = new LytCodeBlock(); - codeBlock.setLanguageFenceName(lang != null ? lang : language.id()); + codeBlock.setCodeContent(lang != null ? lang : language.id(), codeText); codeBlock.applyLanguage(language); - codeBlock.setCodeText(codeText); Integer preferredWidth = parseCodeBlockWidth(meta); if (preferredWidth != null) { codeBlock.setPreferredBodyWidth(preferredWidth); @@ -100,9 +99,8 @@ private LytBlock compileCsvCodeBlock(String source, @Nullable String meta) { List> rows = CsvTableParser.parse(source); if (rows.isEmpty()) { LytCodeBlock codeBlock = new LytCodeBlock(); - codeBlock.setLanguageFenceName("csv"); + codeBlock.setCodeContent("csv", source); codeBlock.applyLanguage(new CodeBlockLanguage("csv", "CSV")); - codeBlock.setCodeText(source); return codeBlock; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/CodeHighlightFlowBuilder.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/CodeHighlightFlowBuilder.java new file mode 100644 index 00000000..040a0417 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/CodeHighlightFlowBuilder.java @@ -0,0 +1,61 @@ +package com.hfstudio.guidenh.guide.document.block; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.guide.document.flow.LytFlowText; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightTheme; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; + +public class CodeHighlightFlowBuilder { + + private final Map colors; + + public CodeHighlightFlowBuilder(CodeHighlightTheme theme) { + colors = buildColors(theme); + } + + public List buildLines(CodeHighlightResult result) { + List lines = new ArrayList<>( + Math.max( + 1, + result.lines() + .size())); + for (CodeHighlightLine line : result.lines()) { + lines.add(buildLine(line)); + } + if (lines.isEmpty()) { + lines.add(new LytFlowSpan()); + } + return lines; + } + + private LytFlowSpan buildLine(CodeHighlightLine line) { + LytFlowSpan span = new LytFlowSpan(); + for (CodeHighlightToken token : line.tokens()) { + var node = LytFlowText.of(token.text()); + node.modifyStyle(style -> style.color(colorOf(token.type()))); + span.append(node); + } + return span; + } + + private ConstantColor colorOf(CodeTokenType type) { + return colors.getOrDefault(type, colors.get(CodeTokenType.PLAIN)); + } + + private Map buildColors(CodeHighlightTheme theme) { + Map result = new EnumMap<>(CodeTokenType.class); + for (CodeTokenType type : CodeTokenType.values()) { + result.put(type, new ConstantColor(theme.colorOf(type))); + } + return Map.copyOf(result); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java index 921df903..817db9a7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java @@ -1,22 +1,20 @@ package com.hfstudio.guidenh.guide.document.block; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.Objects; import com.hfstudio.guidenh.guide.color.ConstantColor; -import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; -import com.hfstudio.guidenh.guide.document.flow.LytFlowText; import com.hfstudio.guidenh.guide.document.interaction.DocumentDragTarget; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorVerticalScrollbar; import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguage; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightTheme; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlighter; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; @@ -26,17 +24,15 @@ public class LytCodeBlock extends LytVBox implements InteractiveElement, DocumentDragTarget { - private static final ConstantColor CODE_DEFAULT = new ConstantColor(0xFFD7DEE7); - private static final ConstantColor CODE_KEYWORD = new ConstantColor(0xFF7FD7FF); - private static final ConstantColor CODE_STRING = new ConstantColor(0xFF9BE28F); - private static final ConstantColor CODE_NUMBER = new ConstantColor(0xFFFFC774); - private static final ConstantColor CODE_COMMENT = new ConstantColor(0xFF7D8794); - private static final ConstantColor CODE_PUNCT = new ConstantColor(0xFFB7C0CD); + private static final CodeHighlightTheme CODE_THEME = CodeHighlightTheme.GITHUB_DARK_DEFAULT; + private static final CodeHighlighter CODE_HIGHLIGHTER = new CodeHighlighter(); + private static final CodeHighlightFlowBuilder FLOW_BUILDER = new CodeHighlightFlowBuilder(CODE_THEME); + private static final ConstantColor CODE_DEFAULT = new ConstantColor(CODE_THEME.colorOf(CodeTokenType.PLAIN)); + private static final ConstantColor CODE_BACKGROUND = new ConstantColor(CODE_THEME.backgroundArgb()); + private static final ConstantColor CODE_BORDER = new ConstantColor(CODE_THEME.borderArgb()); private static final int BODY_PADDING = 6; private static final int SCROLLBAR_WIDTH = 5; private static final int MIN_SCROLLBAR_THUMB = 14; - private static final Map> LANGUAGE_KEYWORDS = buildKeywordMap(); - private static final String[] ASCII_STRINGS = buildAsciiStrings(); private final LytCodeBlockToolbar toolbar = new LytCodeBlockToolbar(); private final LytParagraph body = new LytParagraph(); @@ -56,13 +52,14 @@ public class LytCodeBlock extends LytVBox implements InteractiveElement, Documen private boolean draggingScrollbar; private int scrollbarGrabOffsetY; private int lastBodyLineCount; + private CodeHighlightResult highlightResult = new CodeHighlightResult("text", CodeHighlightMode.PLAIN, List.of()); + private List highlightedLines = List.of(); public LytCodeBlock() { setPadding(6); setGap(4); setFullWidth(true); - setBackgroundColor(SymbolicColor.BLOCKQUOTE_BACKGROUND); - setBorder(new BorderStyle(SymbolicColor.TABLE_BORDER, 1)); + setBorder(new BorderStyle(CODE_BORDER, 1)); body.setMarginTop(0); body.setMarginBottom(0); @@ -84,12 +81,7 @@ public String getCodeText() { } public void setCodeText(String codeText) { - this.codeText = codeText != null ? codeText : ""; - this.normalizedCodeText = this.codeText.replace("\r\n", "\n") - .replace('\r', '\n'); - this.lastBodyLineCount = countBodyLines(); - toolbar.setCopyText(this.codeText); - rebuildBody(); + setCodeContent(languageFenceName, codeText); } public String getLanguageFenceName() { @@ -97,7 +89,23 @@ public String getLanguageFenceName() { } public void setLanguageFenceName(String languageFenceName) { - this.languageFenceName = languageFenceName != null ? languageFenceName : ""; + setCodeContent(languageFenceName, codeText); + } + + public void setCodeContent(String languageFenceName, String codeText) { + String resolvedFenceName = languageFenceName != null ? languageFenceName : ""; + String resolvedCodeText = codeText != null ? codeText : ""; + String resolvedNormalizedCodeText = GuideStringLines.normalizeLineEndings(resolvedCodeText); + boolean changed = !Objects.equals(this.languageFenceName, resolvedFenceName) + || !Objects.equals(this.codeText, resolvedCodeText); + this.languageFenceName = resolvedFenceName; + this.codeText = resolvedCodeText; + this.normalizedCodeText = resolvedNormalizedCodeText; + this.lastBodyLineCount = countBodyLines(resolvedNormalizedCodeText); + toolbar.setCopyText(this.codeText); + if (changed) { + rebuildBody(); + } } public String getLanguageDisplayName() { @@ -114,6 +122,10 @@ public String getDetectedLanguageId() { return detectedLanguageId; } + public CodeHighlightResult getHighlightResult() { + return highlightResult; + } + public void applyLanguage(CodeBlockLanguage language) { if (language == null) { detectedLanguageId = "text"; @@ -233,6 +245,7 @@ public boolean scroll(int documentX, int documentY, int wheelDelta) { protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { int safeWidth = preferredBodyWidth > 0 ? Math.max(1, Math.min(availableWidth, preferredBodyWidth)) : Math.max(1, availableWidth); + toolbar.setPreferredWidth(safeWidth); LytRect toolbarBounds = toolbar.layout(context, x, y, safeWidth); int bodyY = toolbarBounds.bottom() + getGap(); @@ -258,9 +271,7 @@ public void render(RenderContext context) { if (ownBounds.isEmpty()) { return; } - if (getBackgroundColor() != null) { - context.fillRect(ownBounds, getBackgroundColor()); - } + context.fillRect(ownBounds, CODE_BACKGROUND); toolbar.render(context); @@ -283,23 +294,25 @@ private void syncToolbar() { } private void rebuildBody() { + highlightResult = CODE_HIGHLIGHTER.highlight(languageFenceName, normalizedCodeText); + detectedLanguageId = highlightResult.languageId(); + highlightedLines = FLOW_BUILDER.buildLines(highlightResult); body.clearContent(); - List lines = highlightLines(); - for (int i = 0; i < lines.size(); i++) { - body.append(lines.get(i)); - if (i < lines.size() - 1) { + for (int i = 0; i < highlightedLines.size(); i++) { + body.append(highlightedLines.get(i)); + if (i < highlightedLines.size() - 1) { body.appendBreak(); } } } - private int countBodyLines() { - if (normalizedCodeText.isEmpty()) { + private int countBodyLines(String text) { + if (text.isEmpty()) { return 1; } int lines = 1; - for (int i = 0; i < normalizedCodeText.length(); i++) { - if (normalizedCodeText.charAt(i) == '\n') { + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '\n') { lines++; } } @@ -318,10 +331,12 @@ private void renderScrollbar(RenderContext context) { if (track.isEmpty()) { return; } - context.fillRect(track, 0x30242B33); + context.fillRect(track, CODE_THEME.scrollbarTrackArgb()); LytRect thumb = getScrollbarThumbBounds(); if (!thumb.isEmpty()) { - context.fillRect(thumb, draggingScrollbar ? 0xFFCDD6E1 : 0xA0AAB5C2); + context.fillRect( + thumb, + draggingScrollbar ? CODE_THEME.scrollbarThumbActiveArgb() : CODE_THEME.scrollbarThumbArgb()); } } @@ -401,277 +416,4 @@ private void updateScrollFromMouseY(int mouseY) { int maxScroll = getMaxBodyScroll(); setBodyScrollOffset((int) ((long) (thumbTop - track.y()) * maxScroll / thumbTrack)); } - - private List highlightLines() { - List lines = GuideStringLines.splitLines(normalizedCodeText); - String lowerLanguage = detectedLanguageId.toLowerCase(Locale.ROOT); - List result = new ArrayList<>(lines.size()); - for (String line : lines) { - result.add(highlightLine(line, lowerLanguage)); - } - if (lines.isEmpty()) { - result.add(new LytFlowSpan()); - } - return result; - } - - private LytFlowSpan highlightLine(String line, String lowerLanguage) { - LytFlowSpan root = new LytFlowSpan(); - if (line.isEmpty()) { - root.append(LytFlowText.of("")); - return root; - } - - int index = 0; - while (index < line.length()) { - int commentStart = findCommentStart(line, index, lowerLanguage); - if (commentStart == index) { - appendStyled(root, line.substring(index), CODE_COMMENT); - break; - } - if (commentStart > index) { - index = appendTokens(root, line, index, commentStart, lowerLanguage); - continue; - } - index = appendTokens(root, line, index, line.length(), lowerLanguage); - } - return root; - } - - private int appendTokens(LytFlowSpan root, String line, int start, int end, String language) { - int index = start; - while (index < end) { - char current = line.charAt(index); - if (current == '"' || current == '\'') { - int close = findStringEnd(line, index + 1, current, end); - appendStyled(root, line.substring(index, close), CODE_STRING); - index = close; - continue; - } - if (Character.isDigit(current)) { - int close = index + 1; - while (close < end && (Character.isDigit(line.charAt(close)) || line.charAt(close) == '.')) { - close++; - } - appendStyled(root, line.substring(index, close), CODE_NUMBER); - index = close; - continue; - } - if (Character.isLetter(current) || current == '_' || current == '$') { - int close = index + 1; - while (close < end) { - char next = line.charAt(close); - if (!Character.isLetterOrDigit(next) && next != '_' && next != '$') { - break; - } - close++; - } - String token = line.substring(index, close); - appendStyled(root, token, isKeyword(token, language) ? CODE_KEYWORD : CODE_DEFAULT); - index = close; - continue; - } - if (!Character.isWhitespace(current)) { - appendStyled(root, singleChar(current), CODE_PUNCT); - index++; - continue; - } - int close = index + 1; - while (close < end && Character.isWhitespace(line.charAt(close))) { - close++; - } - appendStyled(root, line.substring(index, close), CODE_DEFAULT); - index = close; - } - return index; - } - - private int findCommentStart(String line, int start, String language) { - int result = -1; - if (supportsSlashComment(language)) { - result = minPositive(result, line.indexOf("//", start)); - } - if (supportsHashComment(language)) { - result = minPositive(result, line.indexOf('#', start)); - } - if (supportsDashDashComment(language)) { - result = minPositive(result, line.indexOf("--", start)); - } - if ("properties".equals(language)) { - result = minPositive(result, line.indexOf(';', start)); - } - return result; - } - - private int minPositive(int current, int next) { - if (next < 0) { - return current; - } - if (current < 0) { - return next; - } - return Math.min(current, next); - } - - private boolean supportsSlashComment(String language) { - return "java".equals(language) || "kotlin".equals(language) - || "scala".equals(language) - || "groovy".equals(language) - || "json".equals(language) - || "javascript".equals(language); - } - - private boolean supportsHashComment(String language) { - return "yaml".equals(language) || "bash".equals(language) - || "powershell".equals(language) - || "properties".equals(language) - || "mermaid".equals(language); - } - - private boolean supportsDashDashComment(String language) { - return "lua".equals(language); - } - - private int findStringEnd(String line, int start, char quote, int end) { - int index = start; - while (index < end) { - char current = line.charAt(index); - if (current == '\\') { - index += 2; - continue; - } - index++; - if (current == quote) { - break; - } - } - return Math.min(index, end); - } - - private boolean isKeyword(String token, String language) { - if ("markdown".equals(language)) { - return token.startsWith("#"); - } - Set keywords = LANGUAGE_KEYWORDS.get(language); - return keywords != null && keywords.contains(token); - } - - private static Map> buildKeywordMap() { - Map> m = new HashMap<>(); - m.put( - "java", - kwSet( - "public", - "private", - "protected", - "class", - "interface", - "enum", - "static", - "void", - "new", - "return", - "if", - "else", - "switch", - "case", - "for", - "while", - "try", - "catch", - "throws")); - m.put( - "kotlin", - kwSet( - "fun", - "val", - "var", - "class", - "object", - "when", - "is", - "in", - "return", - "if", - "else", - "data", - "sealed")); - m.put( - "scala", - kwSet( - "object", - "class", - "trait", - "case", - "def", - "val", - "var", - "extends", - "match", - "yield", - "given", - "using")); - m.put( - "lua", - kwSet( - "local", - "function", - "end", - "if", - "then", - "elseif", - "else", - "for", - "while", - "repeat", - "until", - "return", - "nil", - "true", - "false")); - m.put( - "groovy", - kwSet( - "def", - "class", - "interface", - "enum", - "return", - "if", - "else", - "switch", - "case", - "for", - "while", - "in", - "as")); - m.put("json", kwSet("true", "false", "null")); - m.put("yaml", kwSet("true", "false", "null", "yes", "no")); - m.put("bash", kwSet("if", "then", "else", "fi", "for", "do", "done", "case", "esac", "function")); - m.put("powershell", kwSet("function", "param", "if", "else", "foreach", "switch", "return")); - m.put("mermaid", kwSet("graph", "flowchart", "mindmap", "subgraph")); - return m; - } - - private static Set kwSet(String... words) { - return new HashSet<>(List.of(words)); - } - - private static String[] buildAsciiStrings() { - String[] arr = new String[128]; - for (int i = 0; i < 128; i++) { - arr[i] = String.valueOf((char) i); - } - return arr; - } - - private static String singleChar(char c) { - return c < 128 ? ASCII_STRINGS[c] : Character.toString(c); - } - - private void appendStyled(LytFlowSpan root, String text, ConstantColor color) { - var node = LytFlowText.of(text); - node.modifyStyle(style -> style.color(color)); - root.append(node); - } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java index d22b9ab6..d0c858e5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java @@ -3,7 +3,6 @@ import java.util.Optional; import com.hfstudio.guidenh.guide.color.ConstantColor; -import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.document.LytPoint; import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.LytSize; @@ -11,6 +10,7 @@ import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; import com.hfstudio.guidenh.guide.document.interaction.TextTooltip; import com.hfstudio.guidenh.guide.internal.GuidebookText; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightTheme; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.GuiSprite; @@ -22,6 +22,10 @@ public class LytCodeBlockToolbar extends LytBox implements InteractiveElement { private static final GuiSprite COPY_SPRITE = new GuiSprite(GuideIconButton.TEX, 0, 48, 16, 16, 64, 64); private static final long COPY_TOOLTIP_RESET_DELAY_MILLIS = 1500L; + private static final CodeHighlightTheme CODE_THEME = CodeHighlightTheme.GITHUB_DARK_DEFAULT; + private static final ConstantColor TOOLBAR_BACKGROUND = new ConstantColor(CODE_THEME.toolbarBackgroundArgb()); + private static final ConstantColor TOOLBAR_BORDER = new ConstantColor(CODE_THEME.borderArgb()); + private static final ConstantColor TOOLBAR_TEXT = new ConstantColor(CODE_THEME.toolbarTextArgb()); private final LytParagraph languageLabel = new LytParagraph(); private final LytGuiSprite copyButton = new LytGuiSprite(COPY_SPRITE, new LytSize(16, 16)); @@ -37,12 +41,12 @@ public LytCodeBlockToolbar() { languageLabel.setMarginBottom(0); languageLabel.modifyStyle( style -> style.bold(true) - .color(new ConstantColor(0xFFB8BEC9))); - copyButton.setColor(SymbolicColor.ICON_BUTTON_NORMAL); + .color(TOOLBAR_TEXT)); + copyButton.setColor(TOOLBAR_TEXT); append(languageLabel); append(copyButton); setPaddingBottom(4); - setBorderBottom(new BorderStyle(SymbolicColor.TABLE_BORDER, 1)); + setBorderBottom(new BorderStyle(TOOLBAR_BORDER, 1)); } public void setLanguageDisplayName(String languageDisplayName) { @@ -114,9 +118,7 @@ public void setPaddingBottom(int paddingBottom) { @Override public void render(RenderContext context) { - if (getBackgroundColor() != null) { - context.fillRect(bounds, getBackgroundColor()); - } + context.fillRect(bounds, TOOLBAR_BACKGROUND); languageLabel.render(context); if (copyButtonVisible) { copyButton.render(context); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageDetector.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageDetector.java index 2ea4d94a..061112fd 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageDetector.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageDetector.java @@ -61,7 +61,7 @@ public static CodeBlockLanguage detect(@Nullable String explicitFenceLanguage, S } private static CodeBlockLanguage require(String fenceName) { - CodeBlockLanguage language = CodeBlockLanguageRegistry.findByFenceName(fenceName); + CodeBlockLanguage language = CodeBlockLanguageRegistry.findById(fenceName); return language != null ? language : PLAIN_TEXT; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageRegistry.java index c5f03567..32c0f678 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/CodeBlockLanguageRegistry.java @@ -8,40 +8,85 @@ public class CodeBlockLanguageRegistry { - private static final Map BY_FENCE_NAME = buildFenceMap(); + private static final Map BY_LANGUAGE_ID = buildLanguageMap(); + private static final Map NORMALIZED_ALIASES = buildNormalizedAliases(); protected CodeBlockLanguageRegistry() {} + public static @Nullable CodeBlockLanguage findById(@Nullable String languageId) { + if (languageId == null || languageId.isEmpty()) { + return null; + } + return BY_LANGUAGE_ID.get(languageId); + } + public static @Nullable CodeBlockLanguage findByFenceName(@Nullable String fenceName) { - if (fenceName == null || fenceName.isEmpty()) { + String normalized = normalizeFenceLanguage(fenceName); + return normalized != null ? BY_LANGUAGE_ID.get(normalized) : null; + } + + public static @Nullable String normalizeFenceLanguage(@Nullable String fenceName) { + if (fenceName == null) { return null; } - return BY_FENCE_NAME.get(fenceName.toLowerCase(Locale.ROOT)); + String normalizedFenceName = fenceName.trim(); + if (normalizedFenceName.isEmpty()) { + return null; + } + return NORMALIZED_ALIASES.get(normalizedFenceName.toLowerCase(Locale.ROOT)); } - private static Map buildFenceMap() { + private static Map buildLanguageMap() { Map result = new HashMap<>(); - register(result, new CodeBlockLanguage("text", "Text"), "text", "plain", "plaintext", "txt"); - register(result, new CodeBlockLanguage("java", "Java"), "java"); - register(result, new CodeBlockLanguage("kotlin", "Kotlin"), "kt", "kotlin", "kts"); - register(result, new CodeBlockLanguage("scala", "Scala"), "scala", "sc"); - register(result, new CodeBlockLanguage("groovy", "Groovy"), "groovy", "gradle"); - register(result, new CodeBlockLanguage("lua", "Lua"), "lua"); - register(result, new CodeBlockLanguage("json", "JSON"), "json"); - register(result, new CodeBlockLanguage("yaml", "YAML"), "yaml", "yml"); - register(result, new CodeBlockLanguage("xml", "XML"), "xml"); - register(result, new CodeBlockLanguage("properties", "Properties"), "properties"); - register(result, new CodeBlockLanguage("bash", "Bash"), "bash", "sh", "shell"); - register(result, new CodeBlockLanguage("powershell", "PowerShell"), "powershell", "ps1", "pwsh"); - register(result, new CodeBlockLanguage("markdown", "Markdown"), "markdown", "md"); - register(result, new CodeBlockLanguage("csv", "CSV"), "csv"); - register(result, new CodeBlockLanguage("mermaid", "Mermaid"), "mermaid"); + register(result, new CodeBlockLanguage("text", "Text")); + register(result, new CodeBlockLanguage("java", "Java")); + register(result, new CodeBlockLanguage("kotlin", "Kotlin")); + register(result, new CodeBlockLanguage("scala", "Scala")); + register(result, new CodeBlockLanguage("groovy", "Groovy")); + register(result, new CodeBlockLanguage("lua", "Lua")); + register(result, new CodeBlockLanguage("json", "JSON")); + register(result, new CodeBlockLanguage("yaml", "YAML")); + register(result, new CodeBlockLanguage("xml", "XML")); + register(result, new CodeBlockLanguage("properties", "Properties")); + register(result, new CodeBlockLanguage("bash", "Bash")); + register(result, new CodeBlockLanguage("powershell", "PowerShell")); + register(result, new CodeBlockLanguage("markdown", "Markdown")); + register(result, new CodeBlockLanguage("csv", "CSV")); + register(result, new CodeBlockLanguage("mermaid", "Mermaid")); + register(result, new CodeBlockLanguage("javascript", "JavaScript")); + register(result, new CodeBlockLanguage("typescript", "TypeScript")); + return Map.copyOf(result); + } + + private static Map buildNormalizedAliases() { + Map result = new HashMap<>(); + registerAlias(result, "text", "text", "plain", "plaintext", "txt"); + registerAlias(result, "java", "java"); + registerAlias(result, "kotlin", "kt", "kotlin", "kts"); + registerAlias(result, "scala", "scala", "sc"); + registerAlias(result, "groovy", "groovy", "gradle"); + registerAlias(result, "lua", "lua"); + registerAlias(result, "json", "json"); + registerAlias(result, "yaml", "yaml", "yml"); + registerAlias(result, "xml", "xml", "html"); + registerAlias(result, "properties", "properties", "ini"); + registerAlias(result, "bash", "bash", "sh", "shell"); + registerAlias(result, "powershell", "powershell", "ps1", "pwsh"); + registerAlias(result, "markdown", "markdown", "md"); + registerAlias(result, "csv", "csv"); + registerAlias(result, "mermaid", "mermaid"); + registerAlias(result, "javascript", "javascript", "js"); + registerAlias(result, "typescript", "typescript", "ts"); return Map.copyOf(result); } - private static void register(Map result, CodeBlockLanguage language, String... aliases) { + private static void register(Map result, CodeBlockLanguage language) { + result.put(language.id(), language); + } + + private static void registerAlias(Map result, String languageId, String... aliases) { for (String alias : aliases) { - result.put(alias, language); + result.put(alias.toLowerCase(Locale.ROOT), languageId); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightLine.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightLine.java new file mode 100644 index 00000000..6589726d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightLine.java @@ -0,0 +1,8 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +import java.util.List; + +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record CodeHighlightLine(List tokens) {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightMode.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightMode.java new file mode 100644 index 00000000..ef7c24c7 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightMode.java @@ -0,0 +1,7 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +public enum CodeHighlightMode { + FULL, + FAST, + PLAIN +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightResult.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightResult.java new file mode 100644 index 00000000..e026f592 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightResult.java @@ -0,0 +1,13 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +import java.util.List; + +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record CodeHighlightResult(String languageId, CodeHighlightMode mode, List lines) { + + public boolean isPlain() { + return mode == CodeHighlightMode.PLAIN; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightTheme.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightTheme.java new file mode 100644 index 00000000..894d573a --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightTheme.java @@ -0,0 +1,87 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +import java.util.EnumMap; +import java.util.Map; + +public class CodeHighlightTheme { + + public static final CodeHighlightTheme GITHUB_DARK_DEFAULT = githubDarkDefault(); + + private final int backgroundArgb; + private final int toolbarBackgroundArgb; + private final int borderArgb; + private final int scrollbarTrackArgb; + private final int scrollbarThumbArgb; + private final int scrollbarThumbActiveArgb; + private final int toolbarTextArgb; + private final Map tokenColors; + + public CodeHighlightTheme(int backgroundArgb, int toolbarBackgroundArgb, int borderArgb, int scrollbarTrackArgb, + int scrollbarThumbArgb, int scrollbarThumbActiveArgb, int toolbarTextArgb, + Map tokenColors) { + this.backgroundArgb = backgroundArgb; + this.toolbarBackgroundArgb = toolbarBackgroundArgb; + this.borderArgb = borderArgb; + this.scrollbarTrackArgb = scrollbarTrackArgb; + this.scrollbarThumbArgb = scrollbarThumbArgb; + this.scrollbarThumbActiveArgb = scrollbarThumbActiveArgb; + this.toolbarTextArgb = toolbarTextArgb; + this.tokenColors = Map.copyOf(tokenColors); + } + + public int backgroundArgb() { + return backgroundArgb; + } + + public int toolbarBackgroundArgb() { + return toolbarBackgroundArgb; + } + + public int borderArgb() { + return borderArgb; + } + + public int scrollbarTrackArgb() { + return scrollbarTrackArgb; + } + + public int scrollbarThumbArgb() { + return scrollbarThumbArgb; + } + + public int scrollbarThumbActiveArgb() { + return scrollbarThumbActiveArgb; + } + + public int toolbarTextArgb() { + return toolbarTextArgb; + } + + public int colorOf(CodeTokenType type) { + return tokenColors.getOrDefault(type, tokenColors.get(CodeTokenType.PLAIN)); + } + + private static CodeHighlightTheme githubDarkDefault() { + Map colors = new EnumMap<>(CodeTokenType.class); + colors.put(CodeTokenType.PLAIN, 0xFFE6EDF3); + colors.put(CodeTokenType.KEYWORD, 0xFFFF7B72); + colors.put(CodeTokenType.STRING, 0xFFA5D6FF); + colors.put(CodeTokenType.NUMBER, 0xFF79C0FF); + colors.put(CodeTokenType.COMMENT, 0xFF8B949E); + colors.put(CodeTokenType.OPERATOR, 0xFFFF7B72); + colors.put(CodeTokenType.PUNCTUATION, 0xFFE6EDF3); + colors.put(CodeTokenType.TYPE, 0xFF7EE787); + colors.put(CodeTokenType.FUNCTION, 0xFFD2A8FF); + colors.put(CodeTokenType.ANNOTATION, 0xFFFFA657); + colors.put(CodeTokenType.PROPERTY, 0xFF79C0FF); + return new CodeHighlightTheme( + 0xFF0D1117, + 0xFF161B22, + 0xFF30363D, + 0x4D6E7681, + 0x80768496, + 0xCC768496, + 0xFF8B949E, + colors); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightToken.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightToken.java new file mode 100644 index 00000000..9ea8f10c --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlightToken.java @@ -0,0 +1,6 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +import com.github.bsideup.jabel.Desugar; + +@Desugar +public record CodeHighlightToken(String text, CodeTokenType type) {} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlighter.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlighter.java new file mode 100644 index 00000000..8515d945 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeHighlighter.java @@ -0,0 +1,110 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +import java.util.HashMap; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguage; +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguageDetector; +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguageRegistry; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.JavaLikeTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.JsonTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.LuaTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.MarkdownTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.PlainTextTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.PropertiesTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.ShellTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.XmlTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer.YamlTokenizer; +import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; + +public class CodeHighlighter { + + public static final int FULL_MODE_MAX_BYTES = 16 * 1024; + public static final int FULL_MODE_MAX_LINES = 400; + public static final int FAST_MODE_MAX_BYTES = 64 * 1024; + public static final int FAST_MODE_MAX_LINES = 1600; + private static final LanguageTokenizer PLAIN_TEXT_TOKENIZER = new PlainTextTokenizer(); + private static final Map TOKENIZERS = buildTokenizers(PLAIN_TEXT_TOKENIZER); + + public CodeHighlighter() {} + + public CodeHighlightResult highlight(@Nullable String explicitFenceLanguage, @Nullable String codeText) { + String safeCodeText = GuideStringLines.normalizeLineEndings(codeText != null ? codeText : ""); + String languageId = resolveLanguageId(explicitFenceLanguage, safeCodeText); + CodeHighlightMode mode = selectMode(safeCodeText); + LanguageTokenizer tokenizer = TOKENIZERS.getOrDefault(languageId, PLAIN_TEXT_TOKENIZER); + try { + return tokenizer.highlight(languageId, safeCodeText, mode); + } catch (RuntimeException e) { + GuideDebugLog.error("[GuideNH] [CodeHighlighter] Failed to highlight language {}", languageId, e); + return PLAIN_TEXT_TOKENIZER.highlight(languageId, safeCodeText, CodeHighlightMode.PLAIN); + } + } + + private String resolveLanguageId(@Nullable String explicitFenceLanguage, String safeCodeText) { + String normalized = CodeBlockLanguageRegistry.normalizeFenceLanguage(explicitFenceLanguage); + if (normalized != null) { + return normalized; + } + CodeBlockLanguage detected = CodeBlockLanguageDetector.detect(null, safeCodeText); + return detected != null ? detected.id() : "text"; + } + + private CodeHighlightMode selectMode(String safeCodeText) { + int length = safeCodeText.length(); + int lines = countLines(safeCodeText); + if (length > FAST_MODE_MAX_BYTES || lines > FAST_MODE_MAX_LINES) { + return CodeHighlightMode.PLAIN; + } + if (length > FULL_MODE_MAX_BYTES || lines > FULL_MODE_MAX_LINES) { + return CodeHighlightMode.FAST; + } + return CodeHighlightMode.FULL; + } + + private int countLines(String safeCodeText) { + if (safeCodeText.isEmpty()) { + return 1; + } + int lines = 1; + for (int index = 0; index < safeCodeText.length(); index++) { + if (safeCodeText.charAt(index) == '\n') { + lines++; + } + } + return lines; + } + + private static Map buildTokenizers(LanguageTokenizer plainTextTokenizer) { + Map result = new HashMap<>(); + LanguageTokenizer javaLike = new JavaLikeTokenizer(); + LanguageTokenizer json = new JsonTokenizer(); + LanguageTokenizer yaml = new YamlTokenizer(); + LanguageTokenizer xml = new XmlTokenizer(); + LanguageTokenizer properties = new PropertiesTokenizer(); + LanguageTokenizer shell = new ShellTokenizer(); + LanguageTokenizer markdown = new MarkdownTokenizer(); + LanguageTokenizer lua = new LuaTokenizer(); + LanguageTokenizer plain = plainTextTokenizer; + + result.put("text", plain); + result.put("java", javaLike); + result.put("kotlin", javaLike); + result.put("scala", javaLike); + result.put("groovy", javaLike); + result.put("json", json); + result.put("yaml", yaml); + result.put("xml", xml); + result.put("properties", properties); + result.put("bash", shell); + result.put("powershell", shell); + result.put("markdown", markdown); + result.put("lua", lua); + result.put("javascript", javaLike); + result.put("typescript", javaLike); + return Map.copyOf(result); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeTokenType.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeTokenType.java new file mode 100644 index 00000000..65711a89 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/CodeTokenType.java @@ -0,0 +1,15 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +public enum CodeTokenType { + PLAIN, + KEYWORD, + STRING, + NUMBER, + COMMENT, + OPERATOR, + PUNCTUATION, + TYPE, + FUNCTION, + ANNOTATION, + PROPERTY +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/LanguageTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/LanguageTokenizer.java new file mode 100644 index 00000000..a5652415 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/LanguageTokenizer.java @@ -0,0 +1,6 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +public interface LanguageTokenizer { + + CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode); +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/TokenizerSupport.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/TokenizerSupport.java new file mode 100644 index 00000000..1480e94b --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/TokenizerSupport.java @@ -0,0 +1,108 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight; + +import java.util.ArrayList; +import java.util.List; + +import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; + +public class TokenizerSupport { + + protected TokenizerSupport() {} + + public static void appendToken(List out, String text, CodeTokenType type) { + if (text == null || text.isEmpty()) { + return; + } + if (!out.isEmpty()) { + CodeHighlightToken last = out.get(out.size() - 1); + if (last.type() == type) { + out.set(out.size() - 1, new CodeHighlightToken(last.text() + text, type)); + return; + } + } + out.add(new CodeHighlightToken(text, type)); + } + + public static boolean isIdentifierStart(char value) { + return Character.isLetter(value) || value == '_' || value == '$'; + } + + public static boolean isIdentifierPart(char value) { + return Character.isLetterOrDigit(value) || value == '_' || value == '$'; + } + + public static boolean isWhitespace(char value) { + return Character.isWhitespace(value); + } + + public static boolean isNumericStart(char value) { + return Character.isDigit(value) || value == '-'; + } + + public static int skipWhitespace(String line, int index) { + int current = index; + while (current < line.length() && isWhitespace(line.charAt(current))) { + current++; + } + return current; + } + + public static int findQuotedLiteralEnd(String line, int start, char quote) { + int index = start; + while (index < line.length()) { + char current = line.charAt(index); + if (current == '\\') { + index += 2; + continue; + } + index++; + if (current == quote) { + break; + } + } + return Math.min(index, line.length()); + } + + public static int findCommentStartOutsideQuotes(String line, char commentChar) { + boolean inSingle = false; + boolean inDouble = false; + for (int index = 0; index < line.length(); index++) { + char current = line.charAt(index); + if (current == '\\') { + index++; + continue; + } + if (current == '\'' && !inDouble) { + inSingle = !inSingle; + continue; + } + if (current == '"' && !inSingle) { + inDouble = !inDouble; + continue; + } + if (!inSingle && !inDouble && current == commentChar) { + return index; + } + } + return -1; + } + + public static List splitLines(String codeText) { + return GuideStringLines.splitLines(codeText); + } + + public static List plainLines(String codeText) { + List lines = splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return result; + } + for (String line : lines) { + List tokens = new ArrayList<>(1); + appendToken(tokens, line, CodeTokenType.PLAIN); + result.add(new CodeHighlightLine(List.copyOf(tokens))); + } + return result; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JavaLikeTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JavaLikeTokenizer.java new file mode 100644 index 00000000..95a21510 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JavaLikeTokenizer.java @@ -0,0 +1,403 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class JavaLikeTokenizer implements LanguageTokenizer { + + private static final Set PUNCTUATION = Set.of('(', ')', '{', '}', '[', ']', ';', ',', '.'); + private static final Set OPERATORS = Set + .of('+', '-', '*', '/', '%', '=', '&', '|', '!', '<', '>', '?', '^', '~', ':'); + private static final Map> KEYWORDS = buildKeywords(); + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + boolean inBlockComment = false; + + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + + for (String line : lines) { + List tokens = new ArrayList<>(); + int index = 0; + + if (line.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + continue; + } + + while (index < line.length()) { + if (inBlockComment) { + int commentEnd = line.indexOf("*/", index); + if (commentEnd < 0) { + TokenizerSupport.appendToken(tokens, line.substring(index), CodeTokenType.COMMENT); + index = line.length(); + break; + } + TokenizerSupport.appendToken(tokens, line.substring(index, commentEnd + 2), CodeTokenType.COMMENT); + index = commentEnd + 2; + inBlockComment = false; + continue; + } + + if (startsWith(line, index, "//")) { + TokenizerSupport.appendToken(tokens, line.substring(index), CodeTokenType.COMMENT); + break; + } + + if (startsWith(line, index, "/*")) { + int commentEnd = line.indexOf("*/", index + 2); + if (commentEnd < 0) { + TokenizerSupport.appendToken(tokens, line.substring(index), CodeTokenType.COMMENT); + inBlockComment = true; + break; + } + TokenizerSupport.appendToken(tokens, line.substring(index, commentEnd + 2), CodeTokenType.COMMENT); + index = commentEnd + 2; + continue; + } + + char current = line.charAt(index); + if (current == '"' || current == '\'') { + int end = TokenizerSupport.findQuotedLiteralEnd(line, index + 1, current); + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.STRING); + index = end; + continue; + } + + if (mode == CodeHighlightMode.FULL && current == '@') { + int end = index + 1; + while (end < line.length() && TokenizerSupport.isIdentifierPart(line.charAt(end))) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.ANNOTATION); + index = end; + continue; + } + + if (Character.isDigit(current) + || current == '-' && index + 1 < line.length() && Character.isDigit(line.charAt(index + 1))) { + int end = index + 1; + while (end < line.length()) { + char next = line.charAt(end); + if (!Character.isDigit(next) && next != '.' && next != '_' && !Character.isLetter(next)) { + break; + } + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.NUMBER); + index = end; + continue; + } + + if (TokenizerSupport.isIdentifierStart(current)) { + int end = index + 1; + while (end < line.length() && TokenizerSupport.isIdentifierPart(line.charAt(end))) { + end++; + } + String token = line.substring(index, end); + TokenizerSupport.appendToken(tokens, token, classifyToken(languageId, line, token, end, mode)); + index = end; + continue; + } + + if (Character.isWhitespace(current)) { + int end = index + 1; + while (end < line.length() && Character.isWhitespace(line.charAt(end))) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.PLAIN); + index = end; + continue; + } + + CodeTokenType type = PUNCTUATION.contains(current) ? CodeTokenType.PUNCTUATION + : OPERATORS.contains(current) ? CodeTokenType.OPERATOR : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, Character.toString(current), type); + index++; + } + + result.add(new CodeHighlightLine(List.copyOf(tokens))); + } + + return new CodeHighlightResult(languageId, mode, result); + } + + private CodeTokenType classifyToken(String languageId, String line, String token, int endIndex, + CodeHighlightMode mode) { + if (KEYWORDS.getOrDefault(languageId, Set.of()) + .contains(token)) { + return CodeTokenType.KEYWORD; + } + if (mode == CodeHighlightMode.FULL && !token.isEmpty() && Character.isUpperCase(token.charAt(0))) { + return CodeTokenType.TYPE; + } + if (mode == CodeHighlightMode.FULL) { + int next = TokenizerSupport.skipWhitespace(line, endIndex); + if (next < line.length() && line.charAt(next) == '(') { + return CodeTokenType.FUNCTION; + } + } + return CodeTokenType.PLAIN; + } + + private static boolean startsWith(String line, int index, String expected) { + return index + expected.length() <= line.length() && line.startsWith(expected, index); + } + + private static Map> buildKeywords() { + Map> keywords = new HashMap<>(); + keywords.put( + "java", + setOf( + "abstract", + "boolean", + "break", + "case", + "catch", + "class", + "continue", + "default", + "do", + "else", + "enum", + "extends", + "final", + "finally", + "for", + "if", + "implements", + "import", + "instanceof", + "interface", + "new", + "package", + "private", + "protected", + "public", + "return", + "static", + "switch", + "throw", + "throws", + "try", + "void", + "while")); + keywords.put( + "kotlin", + setOf( + "as", + "break", + "class", + "continue", + "data", + "do", + "else", + "false", + "for", + "fun", + "if", + "in", + "interface", + "is", + "null", + "object", + "package", + "return", + "sealed", + "super", + "this", + "throw", + "true", + "try", + "typealias", + "val", + "var", + "when", + "while")); + keywords.put( + "scala", + setOf( + "case", + "class", + "def", + "do", + "else", + "enum", + "extends", + "false", + "for", + "given", + "if", + "import", + "match", + "new", + "object", + "override", + "package", + "return", + "then", + "trait", + "true", + "type", + "using", + "val", + "var", + "while", + "yield")); + keywords.put( + "groovy", + setOf( + "as", + "break", + "case", + "catch", + "class", + "continue", + "def", + "do", + "else", + "enum", + "extends", + "false", + "finally", + "for", + "if", + "implements", + "import", + "in", + "interface", + "new", + "null", + "package", + "return", + "static", + "switch", + "throw", + "trait", + "true", + "try", + "while")); + keywords.put( + "javascript", + setOf( + "async", + "await", + "break", + "case", + "catch", + "class", + "const", + "continue", + "default", + "delete", + "do", + "else", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "let", + "new", + "null", + "return", + "static", + "super", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "var", + "void", + "while", + "yield")); + keywords.put( + "typescript", + setOf( + "abstract", + "any", + "as", + "async", + "await", + "break", + "case", + "catch", + "class", + "const", + "constructor", + "continue", + "declare", + "default", + "do", + "else", + "enum", + "export", + "extends", + "false", + "finally", + "for", + "function", + "if", + "implements", + "import", + "in", + "infer", + "instanceof", + "interface", + "keyof", + "let", + "module", + "namespace", + "never", + "new", + "null", + "private", + "protected", + "public", + "readonly", + "return", + "static", + "super", + "switch", + "this", + "throw", + "true", + "try", + "type", + "typeof", + "undefined", + "unknown", + "var", + "void", + "while")); + return keywords; + } + + private static Set setOf(String... values) { + return new HashSet<>(List.of(values)); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JsonTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JsonTokenizer.java new file mode 100644 index 00000000..9c9a7ec2 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/JsonTokenizer.java @@ -0,0 +1,87 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class JsonTokenizer implements LanguageTokenizer { + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + + for (String line : lines) { + List tokens = new ArrayList<>(); + int index = 0; + while (index < line.length()) { + char current = line.charAt(index); + if (current == '"') { + int end = TokenizerSupport.findQuotedLiteralEnd(line, index + 1, current); + int next = TokenizerSupport.skipWhitespace(line, end); + CodeTokenType type = next < line.length() && line.charAt(next) == ':' + && mode == CodeHighlightMode.FULL ? CodeTokenType.PROPERTY : CodeTokenType.STRING; + TokenizerSupport.appendToken(tokens, line.substring(index, end), type); + index = end; + continue; + } + if (Character.isDigit(current) + || current == '-' && index + 1 < line.length() && Character.isDigit(line.charAt(index + 1))) { + int end = index + 1; + while (end < line.length()) { + char next = line.charAt(end); + if (!Character.isDigit(next) && next != '.' + && next != 'e' + && next != 'E' + && next != '+' + && next != '-') { + break; + } + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.NUMBER); + index = end; + continue; + } + if (Character.isLetter(current)) { + int end = index + 1; + while (end < line.length() && Character.isLetter(line.charAt(end))) { + end++; + } + String token = line.substring(index, end); + CodeTokenType type = "true".equals(token) || "false".equals(token) || "null".equals(token) + ? CodeTokenType.KEYWORD + : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, token, type); + index = end; + continue; + } + if (Character.isWhitespace(current)) { + int end = index + 1; + while (end < line.length() && Character.isWhitespace(line.charAt(end))) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.PLAIN); + index = end; + continue; + } + CodeTokenType type = "{}[],:".indexOf(current) >= 0 ? CodeTokenType.PUNCTUATION : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, Character.toString(current), type); + index++; + } + result.add(new CodeHighlightLine(List.copyOf(tokens))); + } + return new CodeHighlightResult(languageId, mode, result); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/LuaTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/LuaTokenizer.java new file mode 100644 index 00000000..836f968c --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/LuaTokenizer.java @@ -0,0 +1,112 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class LuaTokenizer implements LanguageTokenizer { + + private static final Set KEYWORDS = Set.of( + "and", + "break", + "do", + "else", + "elseif", + "end", + "false", + "for", + "function", + "if", + "in", + "local", + "nil", + "not", + "or", + "repeat", + "return", + "then", + "true", + "until", + "while"); + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + for (String line : lines) { + result.add(new CodeHighlightLine(List.copyOf(tokenizeLine(line, mode)))); + } + return new CodeHighlightResult(languageId, mode, result); + } + + private List tokenizeLine(String line, CodeHighlightMode mode) { + List tokens = new ArrayList<>(); + int commentIndex = line.indexOf("--"); + int endLimit = commentIndex >= 0 ? commentIndex : line.length(); + int index = 0; + + while (index < endLimit) { + char current = line.charAt(index); + if (current == '"' || current == '\'') { + int end = TokenizerSupport.findQuotedLiteralEnd(line, index + 1, current); + TokenizerSupport + .appendToken(tokens, line.substring(index, Math.min(end, endLimit)), CodeTokenType.STRING); + index = end; + continue; + } + if (Character.isDigit(current)) { + int end = index + 1; + while (end < endLimit && (Character.isDigit(line.charAt(end)) || line.charAt(end) == '.')) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.NUMBER); + index = end; + continue; + } + if (TokenizerSupport.isIdentifierStart(current)) { + int end = index + 1; + while (end < endLimit && TokenizerSupport.isIdentifierPart(line.charAt(end))) { + end++; + } + String token = line.substring(index, end); + CodeTokenType type = KEYWORDS.contains(token) ? CodeTokenType.KEYWORD + : mode == CodeHighlightMode.FULL && end < endLimit && line.charAt(end) == '(' + ? CodeTokenType.FUNCTION + : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, token, type); + index = end; + continue; + } + if (Character.isWhitespace(current)) { + int end = index + 1; + while (end < endLimit && Character.isWhitespace(line.charAt(end))) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.PLAIN); + index = end; + continue; + } + CodeTokenType type = "(){}[];,.:".indexOf(current) >= 0 ? CodeTokenType.PUNCTUATION + : "=+-*/%^#<>".indexOf(current) >= 0 ? CodeTokenType.OPERATOR : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, Character.toString(current), type); + index++; + } + + if (commentIndex >= 0) { + TokenizerSupport.appendToken(tokens, line.substring(commentIndex), CodeTokenType.COMMENT); + } + return tokens; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/MarkdownTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/MarkdownTokenizer.java new file mode 100644 index 00000000..5ffe1bfa --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/MarkdownTokenizer.java @@ -0,0 +1,68 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class MarkdownTokenizer implements LanguageTokenizer { + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + for (String line : lines) { + result.add(new CodeHighlightLine(List.copyOf(tokenizeLine(line)))); + } + return new CodeHighlightResult(languageId, mode, result); + } + + private List tokenizeLine(String line) { + if (line.isEmpty()) { + return List.of(); + } + List tokens = new ArrayList<>(); + int index = 0; + if (line.startsWith("#")) { + while (index < line.length() && line.charAt(index) == '#') { + index++; + } + TokenizerSupport.appendToken(tokens, line.substring(0, index), CodeTokenType.KEYWORD); + } else if (line.startsWith("- ") || line.startsWith("* ") || line.startsWith("> ")) { + TokenizerSupport.appendToken(tokens, line.substring(0, 1), CodeTokenType.PUNCTUATION); + index = 1; + } + while (index < line.length()) { + char current = line.charAt(index); + if (current == '`') { + int end = line.indexOf('`', index + 1); + end = end >= 0 ? end + 1 : line.length(); + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.STRING); + index = end; + continue; + } + if ("*_~[]()".indexOf(current) >= 0) { + TokenizerSupport.appendToken(tokens, Character.toString(current), CodeTokenType.PUNCTUATION); + index++; + continue; + } + int end = index + 1; + while (end < line.length() && "`*_~[]()".indexOf(line.charAt(end)) < 0) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.PLAIN); + index = end; + } + return tokens; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PlainTextTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PlainTextTokenizer.java new file mode 100644 index 00000000..92504774 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PlainTextTokenizer.java @@ -0,0 +1,14 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class PlainTextTokenizer implements LanguageTokenizer { + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + return new CodeHighlightResult(languageId, mode, TokenizerSupport.plainLines(codeText)); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PropertiesTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PropertiesTokenizer.java new file mode 100644 index 00000000..4bd2b757 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/PropertiesTokenizer.java @@ -0,0 +1,76 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class PropertiesTokenizer implements LanguageTokenizer { + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + for (String line : lines) { + result.add(new CodeHighlightLine(List.copyOf(tokenizeLine(line, mode)))); + } + return new CodeHighlightResult(languageId, mode, result); + } + + private List tokenizeLine(String line, CodeHighlightMode mode) { + if (line.isEmpty()) { + return List.of(); + } + String trimmed = line.trim(); + if (trimmed.startsWith("#") || trimmed.startsWith("!") || trimmed.startsWith(";")) { + return List.of(new CodeHighlightToken(line, CodeTokenType.COMMENT)); + } + + int separator = findSeparator(line); + if (separator < 0) { + return List.of(new CodeHighlightToken(line, CodeTokenType.PLAIN)); + } + + List tokens = new ArrayList<>(); + if (separator > 0) { + TokenizerSupport.appendToken( + tokens, + line.substring(0, separator), + mode == CodeHighlightMode.FULL ? CodeTokenType.PROPERTY : CodeTokenType.PLAIN); + } + TokenizerSupport.appendToken(tokens, Character.toString(line.charAt(separator)), CodeTokenType.PUNCTUATION); + if (separator + 1 < line.length()) { + TokenizerSupport.appendToken(tokens, line.substring(separator + 1), CodeTokenType.PLAIN); + } + return tokens; + } + + private int findSeparator(String line) { + boolean escaped = false; + for (int index = 0; index < line.length(); index++) { + char current = line.charAt(index); + if (escaped) { + escaped = false; + continue; + } + if (current == '\\') { + escaped = true; + continue; + } + if (current == '=' || current == ':') { + return index; + } + } + return -1; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/ShellTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/ShellTokenizer.java new file mode 100644 index 00000000..1e0ed206 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/ShellTokenizer.java @@ -0,0 +1,117 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class ShellTokenizer implements LanguageTokenizer { + + private static final Set BASH_KEYWORDS = Set + .of("if", "then", "else", "fi", "for", "do", "done", "case", "esac", "function", "while", "in"); + private static final Set POWERSHELL_KEYWORDS = Set + .of("function", "param", "if", "else", "foreach", "switch", "return", "trap", "throw", "while"); + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + for (String line : lines) { + result.add(new CodeHighlightLine(List.copyOf(tokenizeLine(languageId, line, mode)))); + } + return new CodeHighlightResult(languageId, mode, result); + } + + private List tokenizeLine(String languageId, String line, CodeHighlightMode mode) { + List tokens = new ArrayList<>(); + int commentIndex = TokenizerSupport.findCommentStartOutsideQuotes(line, '#'); + int endLimit = commentIndex >= 0 ? commentIndex : line.length(); + int index = 0; + + while (index < endLimit) { + char current = line.charAt(index); + if (current == '"' || current == '\'') { + int end = TokenizerSupport.findQuotedLiteralEnd(line, index + 1, current); + TokenizerSupport + .appendToken(tokens, line.substring(index, Math.min(end, endLimit)), CodeTokenType.STRING); + index = end; + continue; + } + if (current == '$') { + int end = index + 1; + while (end < endLimit) { + char next = line.charAt(end); + if (!TokenizerSupport.isIdentifierPart(next) && next != ':' && next != '{' && next != '}') { + break; + } + end++; + } + TokenizerSupport.appendToken( + tokens, + line.substring(index, end), + mode == CodeHighlightMode.FULL ? CodeTokenType.PROPERTY : CodeTokenType.PLAIN); + index = end; + continue; + } + if (Character.isDigit(current)) { + int end = index + 1; + while (end < endLimit && (Character.isDigit(line.charAt(end)) || line.charAt(end) == '.')) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.NUMBER); + index = end; + continue; + } + if (TokenizerSupport.isIdentifierStart(current)) { + int end = index + 1; + while (end < endLimit) { + char next = line.charAt(end); + if (!TokenizerSupport.isIdentifierPart(next) && next != '-') { + break; + } + end++; + } + String token = line.substring(index, end); + CodeTokenType type = keywordsFor(languageId).contains(token.toLowerCase()) ? CodeTokenType.KEYWORD + : mode == CodeHighlightMode.FULL && token.contains("-") ? CodeTokenType.FUNCTION + : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, token, type); + index = end; + continue; + } + if (Character.isWhitespace(current)) { + int end = index + 1; + while (end < endLimit && Character.isWhitespace(line.charAt(end))) { + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.PLAIN); + index = end; + continue; + } + CodeTokenType type = "|&;(){}[],:".indexOf(current) >= 0 ? CodeTokenType.PUNCTUATION + : "=+-*/%!<>".indexOf(current) >= 0 ? CodeTokenType.OPERATOR : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, Character.toString(current), type); + index++; + } + + if (commentIndex >= 0) { + TokenizerSupport.appendToken(tokens, line.substring(commentIndex), CodeTokenType.COMMENT); + } + return tokens; + } + + private Set keywordsFor(String languageId) { + return "powershell".equals(languageId) ? POWERSHELL_KEYWORDS : BASH_KEYWORDS; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/XmlTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/XmlTokenizer.java new file mode 100644 index 00000000..346635f7 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/XmlTokenizer.java @@ -0,0 +1,107 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class XmlTokenizer implements LanguageTokenizer { + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + for (String line : lines) { + result.add(new CodeHighlightLine(List.copyOf(tokenizeLine(line, mode)))); + } + return new CodeHighlightResult(languageId, mode, result); + } + + private List tokenizeLine(String line, CodeHighlightMode mode) { + List tokens = new ArrayList<>(); + int index = 0; + boolean insideTag = false; + while (index < line.length()) { + char current = line.charAt(index); + if (!insideTag && current == '<') { + insideTag = true; + TokenizerSupport.appendToken(tokens, "<", CodeTokenType.PUNCTUATION); + index++; + if (index < line.length() && line.charAt(index) == '/') { + TokenizerSupport.appendToken(tokens, "/", CodeTokenType.PUNCTUATION); + index++; + } + int nameStart = index; + while (index < line.length() + && (TokenizerSupport.isIdentifierPart(line.charAt(index)) || line.charAt(index) == ':' + || line.charAt(index) == '-')) { + index++; + } + if (index > nameStart) { + TokenizerSupport.appendToken(tokens, line.substring(nameStart, index), CodeTokenType.TYPE); + } + continue; + } + if (insideTag && (current == '"' || current == '\'')) { + int end = TokenizerSupport.findQuotedLiteralEnd(line, index + 1, current); + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.STRING); + index = end; + continue; + } + if (insideTag && current == '=') { + TokenizerSupport.appendToken(tokens, "=", CodeTokenType.OPERATOR); + index++; + continue; + } + if (insideTag && current == '>') { + TokenizerSupport.appendToken(tokens, ">", CodeTokenType.PUNCTUATION); + insideTag = false; + index++; + continue; + } + if (insideTag && current == '/' && index + 1 < line.length() && line.charAt(index + 1) == '>') { + TokenizerSupport.appendToken(tokens, "/>", CodeTokenType.PUNCTUATION); + insideTag = false; + index += 2; + continue; + } + if (insideTag && TokenizerSupport.isIdentifierStart(current)) { + int end = index + 1; + while (end < line.length() + && (TokenizerSupport.isIdentifierPart(line.charAt(end)) || line.charAt(end) == ':' + || line.charAt(end) == '-')) { + end++; + } + CodeTokenType type = mode == CodeHighlightMode.FULL ? CodeTokenType.PROPERTY : CodeTokenType.PLAIN; + TokenizerSupport.appendToken(tokens, line.substring(index, end), type); + index = end; + continue; + } + int end = index + 1; + while (end < line.length()) { + char next = line.charAt(end); + if (next == '<' || insideTag && (next == '"' || next == '\'' + || next == '=' + || next == '>' + || next == '/' && end + 1 < line.length() && line.charAt(end + 1) == '>' + || TokenizerSupport.isIdentifierStart(next))) { + break; + } + end++; + } + TokenizerSupport.appendToken(tokens, line.substring(index, end), CodeTokenType.PLAIN); + index = end; + } + return tokens; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/YamlTokenizer.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/YamlTokenizer.java new file mode 100644 index 00000000..362f0206 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/highlight/tokenizer/YamlTokenizer.java @@ -0,0 +1,132 @@ +package com.hfstudio.guidenh.guide.internal.markdown.highlight.tokenizer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightMode; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.LanguageTokenizer; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.TokenizerSupport; + +public class YamlTokenizer implements LanguageTokenizer { + + private static final Set KEYWORDS = Set.of("true", "false", "null", "yes", "no", "on", "off"); + + @Override + public CodeHighlightResult highlight(String languageId, String codeText, CodeHighlightMode mode) { + List lines = TokenizerSupport.splitLines(codeText); + List result = new ArrayList<>(Math.max(1, lines.size())); + if (lines.isEmpty()) { + result.add(new CodeHighlightLine(List.of())); + return new CodeHighlightResult(languageId, mode, result); + } + + for (String line : lines) { + result.add(new CodeHighlightLine(List.copyOf(tokenizeLine(line, mode)))); + } + return new CodeHighlightResult(languageId, mode, result); + } + + private List tokenizeLine(String line, CodeHighlightMode mode) { + if (line.isEmpty()) { + return List.of(); + } + + List tokens = new ArrayList<>(); + int commentStart = TokenizerSupport.findCommentStartOutsideQuotes(line, '#'); + String content = commentStart >= 0 ? line.substring(0, commentStart) : line; + String comment = commentStart >= 0 ? line.substring(commentStart) : ""; + + int index = 0; + while (index < content.length() && Character.isWhitespace(content.charAt(index))) { + index++; + } + if (index > 0) { + TokenizerSupport.appendToken(tokens, content.substring(0, index), CodeTokenType.PLAIN); + } + if (index < content.length() && content.charAt(index) == '-') { + TokenizerSupport.appendToken(tokens, "-", CodeTokenType.PUNCTUATION); + index++; + while (index < content.length() && Character.isWhitespace(content.charAt(index))) { + index++; + } + } + + int colonIndex = content.indexOf(':', index); + if (colonIndex > index) { + String key = content.substring(index, colonIndex) + .trim(); + int keyOffset = content.indexOf(key, index); + if (keyOffset > index) { + TokenizerSupport.appendToken(tokens, content.substring(index, keyOffset), CodeTokenType.PLAIN); + } + if (!key.isEmpty()) { + TokenizerSupport.appendToken( + tokens, + key, + mode == CodeHighlightMode.FULL ? CodeTokenType.PROPERTY : CodeTokenType.PLAIN); + } + TokenizerSupport.appendToken(tokens, ":", CodeTokenType.PUNCTUATION); + appendValueTokens(tokens, content.substring(colonIndex + 1)); + } else if (index < content.length()) { + appendValueTokens(tokens, content.substring(index)); + } + + if (!comment.isEmpty()) { + TokenizerSupport.appendToken(tokens, comment, CodeTokenType.COMMENT); + } + return tokens; + } + + private void appendValueTokens(List tokens, String value) { + if (value.isEmpty()) { + return; + } + int index = 0; + while (index < value.length() && Character.isWhitespace(value.charAt(index))) { + index++; + } + if (index > 0) { + TokenizerSupport.appendToken(tokens, value.substring(0, index), CodeTokenType.PLAIN); + } + if (index >= value.length()) { + return; + } + String trimmed = value.substring(index) + .trim(); + if (trimmed.isEmpty()) { + TokenizerSupport.appendToken(tokens, value.substring(index), CodeTokenType.PLAIN); + return; + } + if (trimmed.startsWith("\"") || trimmed.startsWith("'")) { + TokenizerSupport.appendToken(tokens, value.substring(index), CodeTokenType.STRING); + return; + } + if (KEYWORDS.contains(trimmed.toLowerCase())) { + TokenizerSupport.appendToken(tokens, value.substring(index), CodeTokenType.KEYWORD); + return; + } + if (isNumber(trimmed)) { + TokenizerSupport.appendToken(tokens, value.substring(index), CodeTokenType.NUMBER); + return; + } + TokenizerSupport.appendToken(tokens, value.substring(index), CodeTokenType.PLAIN); + } + + private boolean isNumber(String value) { + if (value.isEmpty()) { + return false; + } + for (int index = 0; index < value.length(); index++) { + char current = value.charAt(index); + if (!Character.isDigit(current) && current != '.' && current != '-' && current != '+') { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/CodeHighlightHtmlRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/CodeHighlightHtmlRenderer.java new file mode 100644 index 00000000..41475d0d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/CodeHighlightHtmlRenderer.java @@ -0,0 +1,101 @@ +package com.hfstudio.guidenh.guide.siteexport.site; + +import java.util.Locale; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightLine; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightToken; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; + +public class CodeHighlightHtmlRenderer { + + public String render(CodeHighlightResult result, @Nullable String languageLabel, @Nullable Integer width, + @Nullable Integer height) { + String resolvedLanguageLabel = languageLabel != null && !languageLabel.isEmpty() ? languageLabel : "Text"; + StringBuilder html = new StringBuilder(Math.max(256, estimateCapacity(result))); + html.append("
    ") + .append("
    ") + .append("") + .append(GuideSiteItemHtml.escapeHtml(resolvedLanguageLabel)) + .append("
    ") + .append(""); + appendLines(html, result); + html.append("
    "); + return html.toString(); + } + + private void appendPreAttributes(StringBuilder html, @Nullable Integer width, @Nullable Integer height) { + boolean constrained = width != null || height != null; + if (!constrained) { + return; + } + html.append(" class=\"guide-code-block-scroll\" style=\""); + if (width != null) { + html.append("width:") + .append(width) + .append("px;max-width:100%;"); + } + if (height != null) { + html.append("height:") + .append(height) + .append("px;overflow:auto;"); + } + html.append("\""); + } + + private void appendCodeAttributes(StringBuilder html, String languageId) { + if (languageId == null || languageId.isEmpty()) { + return; + } + html.append(" class=\"language-") + .append(GuideSiteItemHtml.escapeHtml(languageId)) + .append("\""); + } + + private void appendLines(StringBuilder html, CodeHighlightResult result) { + for (int lineIndex = 0; lineIndex < result.lines() + .size(); lineIndex++) { + CodeHighlightLine line = result.lines() + .get(lineIndex); + appendLine(html, line); + if (lineIndex < result.lines() + .size() - 1) { + html.append('\n'); + } + } + } + + private void appendLine(StringBuilder html, CodeHighlightLine line) { + for (CodeHighlightToken token : line.tokens()) { + if (token.type() == CodeTokenType.PLAIN) { + html.append(GuideSiteItemHtml.escapeHtml(token.text())); + continue; + } + html.append("") + .append(GuideSiteItemHtml.escapeHtml(token.text())) + .append(""); + } + } + + private int estimateCapacity(CodeHighlightResult result) { + int size = 0; + for (CodeHighlightLine line : result.lines()) { + for (CodeHighlightToken token : line.tokens()) { + size += token.text() + .length(); + } + } + return size * 2; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteCodeBlockRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteCodeBlockRenderer.java new file mode 100644 index 00000000..bea31028 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteCodeBlockRenderer.java @@ -0,0 +1,46 @@ +package com.hfstudio.guidenh.guide.siteexport.site; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguage; +import com.hfstudio.guidenh.guide.internal.markdown.CodeBlockLanguageRegistry; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightResult; +import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlighter; + +public class GuideSiteCodeBlockRenderer { + + private static final CodeHighlighter DEFAULT_HIGHLIGHTER = new CodeHighlighter(); + private static final CodeHighlightHtmlRenderer DEFAULT_HTML_RENDERER = new CodeHighlightHtmlRenderer(); + private final CodeHighlighter highlighter; + private final CodeHighlightHtmlRenderer htmlRenderer; + + public GuideSiteCodeBlockRenderer() { + this(DEFAULT_HIGHLIGHTER, DEFAULT_HTML_RENDERER); + } + + public GuideSiteCodeBlockRenderer(CodeHighlighter highlighter, CodeHighlightHtmlRenderer htmlRenderer) { + this.highlighter = highlighter; + this.htmlRenderer = htmlRenderer; + } + + public String render(@Nullable String languageFenceName, String codeText, @Nullable Integer width, + @Nullable Integer height) { + CodeHighlightResult result = highlighter.highlight(languageFenceName, codeText); + return htmlRenderer.render(result, resolveLanguageLabel(languageFenceName, result.languageId()), width, height); + } + + public String render(@Nullable String languageFenceName, String codeText) { + return render(languageFenceName, codeText, null, null); + } + + private String resolveLanguageLabel(@Nullable String languageFenceName, @Nullable String resolvedLanguageId) { + CodeBlockLanguage language = CodeBlockLanguageRegistry.findByFenceName(languageFenceName); + if (language == null && resolvedLanguageId != null && !resolvedLanguageId.isEmpty()) { + language = CodeBlockLanguageRegistry.findById(resolvedLanguageId); + } + if (language != null) { + return language.displayName(); + } + return languageFenceName != null && !languageFenceName.isEmpty() ? languageFenceName : "Text"; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java index 8e3bdfc1..3c909b25 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteHtmlCompiler.java @@ -40,6 +40,8 @@ public class GuideSiteHtmlCompiler { + private static final GuideSiteCodeBlockRenderer CODE_BLOCK_RENDERER = new GuideSiteCodeBlockRenderer(); + public interface RecipeTagRenderer { default String render(MdxJsxElementFields element, String defaultNamespace) { @@ -526,6 +528,8 @@ private String compileCodeBlockMdx(MdxJsxElementFields el) { String codeText = extractTextFromElement(el); String lang = el.getAttributeString("lang", null); String meta = el.getAttributeString("meta", null); + Integer width = parseMetaInt(meta, "width"); + Integer height = parseMetaInt(meta, "height"); if (lang != null) { lang = lang.toLowerCase(Locale.ROOT); } @@ -542,7 +546,7 @@ private String compileCodeBlockMdx(MdxJsxElementFields el) { MermaidMindmapDocument doc = MermaidMindmapParser.parse(codeText); return GuideSiteGraphRenderer.renderMermaidTree(doc); } catch (Exception ignored) { - return "
    " + escapeHtml(codeText) + "
    "; + return CODE_BLOCK_RENDERER.render("mermaid", codeText, width, height); } } if ("funcgraph".equals(lang) || "functiongraph".equals(lang)) { @@ -550,42 +554,11 @@ private String compileCodeBlockMdx(MdxJsxElementFields el) { LytFunctionGraph graph = FunctionGraphFenceParser.parse(codeText); return GuideSiteGraphRenderer.renderFunctionGraph(graph); } catch (RuntimeException ignored) { - return "
    "
    -                    + escapeHtml(codeText)
    -                    + "
    "; + return CODE_BLOCK_RENDERER.render(lang, codeText, width, height); } } - // Plain code block with optional size constraints - Integer width = parseMetaInt(meta, "width"); - Integer height = parseMetaInt(meta, "height"); - StringBuilder html = new StringBuilder(); - html.append("") - .append(escapeHtml(codeText)) - .append(""); - return html.toString(); + return CODE_BLOCK_RENDERER.render(lang, codeText, width, height); } @Nullable diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index 1c850c7d..d90c5990 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -86,6 +86,7 @@ public class GuideSiteMdxTagRenderer implements GuideSiteHtmlCompiler.MdxTagRenderer { private static final int SPECIAL_PAGES_GROUP_COLUMNS = 2; + private static final GuideSiteCodeBlockRenderer CODE_BLOCK_RENDERER = new GuideSiteCodeBlockRenderer(); private static final Comparator STRUCTURE_DRAW_ORDER = Comparator .comparingInt((StructureBlockView block) -> block.y) @@ -1260,7 +1261,7 @@ private String renderMermaid(MdxJsxElementFields element, String defaultNamespac sceneResolver, compiler)); } catch (Exception ex) { - return "
    " + escapeHtml(source) + "
    "; + return CODE_BLOCK_RENDERER.render("mermaid", source); } } diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index 882af0b1..153f5fd9 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -209,6 +209,97 @@ pre code { padding: 0; } +.guide-code-block { + display: flow-root; + margin-bottom: 0.8em; + background: #0d1117; + border: 1px solid #30363d; + overflow: hidden; +} + +.guide-code-block-toolbar { + display: flex; + align-items: center; + min-height: 36px; + padding: 0 12px; + background: #161b22; + border-bottom: 1px solid #30363d; +} + +.guide-code-block-language { + font-family: var(--minecraft-font); + color: #8b949e; + font-size: 0.9rem; + font-weight: 700; +} + +.guide-code-block pre, +.guide-code-block code { + background: none; + border: none; +} + +.guide-code-block pre { + margin: 0; + padding: 12px; + color: #e6edf3; + overflow-x: auto; +} + +.guide-code-block code { + display: block; + padding: 0; + white-space: pre; +} + +.guide-code-block-scroll { + max-width: 100%; +} + +.gh-plain { + color: #e6edf3; +} + +.gh-keyword { + color: #ff7b72; +} + +.gh-string { + color: #a5d6ff; +} + +.gh-number { + color: #79c0ff; +} + +.gh-comment { + color: #8b949e; +} + +.gh-operator { + color: #ff7b72; +} + +.gh-punctuation { + color: #e6edf3; +} + +.gh-type { + color: #7ee787; +} + +.gh-function { + color: #d2a8ff; +} + +.gh-annotation { + color: #ffa657; +} + +.gh-property { + color: #79c0ff; +} + blockquote { display: flow-root; border-left: calc(2px * var(--gui-scale)) solid rgba(210, 210, 210, 0.35); From 2fecc683e26107ecd127b137f2f8fe2ae304447e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:49:59 +0800 Subject: [PATCH 118/136] unify Scene rendering through RenderContext coordinate system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All Scene rendering now uses layout coordinates exclusively, with coordinate conversion handled by the context — no manual screen-space computation. - NodeContentRenderContext.toScreenRect: chain through delegate to include documentOrigin and scrollOffset, returning absolute screen coordinates - Scene: remove instanceof LytGuidebookScene special case from Mermaid canvas, render through generic block.render(nodeContext) path - Scene: camera viewport defaults to sceneW/sceneH (cameraViewportOverride retained only for offscreen export callers) - Scene buttons, ponder controls, slider tracks: all raw GL parts wrapped in conditional GL transform (push/pop only for non-VanillaRenderContext) - Slider render path: separate layout-space rects (resolveSliderTrackLayoutRect) from screen-space hit-test rects - VanillaRenderContext.toScreenRect: scale dimensions by zoom All drawSceneButtons, drawPonderControls, drawSlider, and resolveSliderTrackRect now use layout coordinates. Hover detection uses context.toScreenRect() to convert layout to screen coords before comparing against mouseX/mouseY. --- .../block/LytMermaidMindmapCanvas.java | 52 ++-- .../guide/render/VanillaRenderContext.java | 9 + .../guide/scene/LytGuidebookScene.java | 244 ++++++++++-------- 3 files changed, 158 insertions(+), 147 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 1615b19a..73132a1a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -690,30 +690,6 @@ private void renderNodeContent(RenderContext context, NodeLayout node, LytRect r private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nodeContext, RenderContext nativeContext, LytRect contentViewport) { - if (block instanceof LytGuidebookScene scene) { - // The camera's orthographic projection uses its own viewport size - // while the GL viewport comes from bounds + layoutSceneWidth/Height. - // By scaling the scene viewport (GL) and locking the camera viewport - // to the original size, the 3D content fills more pixels while the - // world-to-NDC mapping stays tight — content follows mindmap zoom. - LytRect savedBounds = scene.getBounds(); - int savedW = savedBounds.width(); - int savedH = savedBounds.height(); - int scaledW = Math.max(1, Math.round(savedW * zoom)); - int scaledH = Math.max(1, Math.round(savedH * zoom)); - scene.bounds = new LytRect(contentViewport.x(), contentViewport.y(), scaledW, scaledH); - scene.setSceneViewportOverride(scaledW, scaledH); - scene.setCameraViewportOverride(savedW, savedH); - scene.setButtonZoom(zoom); - try { - scene.render(nativeContext); - } finally { - scene.bounds = savedBounds; - scene.clearSceneViewportOverride(); - scene.clearCameraViewportOverride(); - } - return; - } if (block instanceof LytNode container && !container.getChildren() .isEmpty()) { for (var child : new ArrayList<>(container.getChildren())) { @@ -723,14 +699,11 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod } renderContainerDecoration(container, nodeContext); } else if (usesRawGl(block)) { - // This block renders with raw GL, bypassing RenderContext coordinate - // mapping. Apply mindmap zoom via GL matrix and use nativeContext so - // that the document-level GL transform chain remains intact. GL11.glPushMatrix(); - GL11.glTranslatef(contentViewport.x(), contentViewport.y(), 0f); + GL11.glTranslatef(nodeContext.getDocumentOriginX(), nodeContext.getDocumentOriginY(), 0f); GL11.glScalef(zoom, zoom, 1f); try { - block.render(nativeContext); + block.render(nodeContext); } finally { GL11.glPopMatrix(); } @@ -977,13 +950,6 @@ private LytRect resolveSelfVisualBounds(LytBlock block) { if (block instanceof LytLatexDisplayBlock latexDisplayBlock) { return latexDisplayBlock.getVisualBounds(); } - if (block instanceof LytGuidebookScene scene && scene.isSceneButtonsVisible()) { - return new LytRect( - bounds.x(), - bounds.y(), - bounds.width() + LytGuidebookScene.BTN_OUTSIDE_GAP + LytGuidebookScene.BTN_SIZE, - bounds.height()); - } return bounds; } @@ -1356,7 +1322,12 @@ public int getDocumentOriginY() { @Override public LytRect toScreenRect(LytRect rect) { - return scaleRect(rect); + LytRect s = scaleRect(rect); + return new LytRect( + s.x() + delegate.getDocumentOriginX(), + s.y() + delegate.getDocumentOriginY() - delegate.getScrollOffsetY(), + s.width(), + s.height()); } @Override @@ -1513,6 +1484,13 @@ public void pushScissor(LytRect rect) { delegate.pushScissor(scaleRect(rect)); } + @Override + public void pushLocalScissor(LytRect rect) { + // Override the default interface method to avoid double-scaling: + // the default chains toScreenRect(scale) + pushScissor(scale again). + delegate.pushScissor(scaleRect(rect)); + } + @Override public LytRect currentScissor() { return delegate.currentScissor(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java index 8fbb019c..5bc258b1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java @@ -93,6 +93,15 @@ public void setZoom(float zoom) { this.zoom = zoom > 0f ? zoom : 1.0f; } + @Override + public LytRect toScreenRect(LytRect rect) { + return new LytRect( + Math.round(rect.x() * zoom) + documentOriginX, + Math.round((rect.y() - scrollOffsetY) * zoom) + documentOriginY, + Math.max(1, Math.round(rect.width() * zoom)), + Math.max(1, Math.round(rect.height() * zoom))); + } + public int getScreenHeight() { return screenHeight; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 845d2394..5eb08706 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -224,11 +224,6 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private int cachedPonderBtnAbsY; private boolean interactive = true; - float buttonZoom = 1f; - - public void setButtonZoom(float buttonZoom) { - this.buttonZoom = Math.max(0.1f, buttonZoom); - } private boolean sceneButtonsVisible = true; private boolean bottomControlsVisible = true; @@ -270,8 +265,6 @@ public void setButtonZoom(float buttonZoom) { private boolean showBackground = true; @Nullable private LytSize cameraViewportOverride; - @Nullable - private LytSize sceneViewportOverride; private final List annotations = new ArrayList<>(); // Reuse annotation partitions instead of allocating new lists every frame. @@ -327,6 +320,8 @@ public void setButtonZoom(float buttonZoom) { private final Map cachedChannelSliderHitRects = new LinkedHashMap<>(); private int sceneButtonsAbsX; private int sceneButtonsAbsY; + private int lastBtnScreenSize; + private int lastBtnScreenStep; private boolean cachedSceneButtonRolesDirty = true; private boolean cachedSceneButtonsVisible = true; private boolean cachedSceneHasAnnotations = true; @@ -645,14 +640,6 @@ public void clearCameraViewportOverride() { this.cameraViewportOverride = null; } - public void setSceneViewportOverride(int width, int height) { - this.sceneViewportOverride = new LytSize(Math.max(16, width), Math.max(16, height)); - } - - public void clearSceneViewportOverride() { - this.sceneViewportOverride = null; - } - public boolean isGridButtonEnabled() { return gridButtonEnabled || ModConfig.debug.enableDebugMode; } @@ -2049,14 +2036,11 @@ protected void onLayoutMoved(int deltaX, int deltaY) {} @Override public void render(RenderContext context) { - LytSize vpOverride = sceneViewportOverride; - int sceneW = vpOverride != null ? vpOverride.width() - : layoutSceneWidth > 0 ? layoutSceneWidth - : getBounds().width() - buttonColumnReserve() - layoutSceneOffsetX; + int sceneW = layoutSceneWidth > 0 ? layoutSceneWidth + : getBounds().width() - buttonColumnReserve() - layoutSceneOffsetX; if (sceneW < 16) sceneW = 16; int sliderAreaHeight = getBottomControlAreaHeight(); - int sceneH = vpOverride != null ? vpOverride.height() - : layoutSceneHeight > 0 ? layoutSceneHeight : Math.max(16, getBounds().height() - sliderAreaHeight); + int sceneH = layoutSceneHeight > 0 ? layoutSceneHeight : Math.max(16, getBounds().height() - sliderAreaHeight); int totalH = reserveBottomControlArea ? Math.max(sceneH + sliderAreaHeight, getBounds().height()) : Math.max(sceneH, getBounds().height()); LytRect outerRect = new LytRect( @@ -2070,26 +2054,20 @@ public void render(RenderContext context) { outerRect.y(), outerRect.width(), sceneH); - // Resolve document zoom and compute screen-space absolute coords for both render paths. - float docZoom = context instanceof VanillaRenderContext mrc ? mrc.getZoom() : 1.0f; - int absX = sceneRect.x(); - int absY = sceneRect.y(); - int outerAbsX = outerRect.x(); - int outerAbsY = outerRect.y(); - int clipX = outerAbsX, clipY = outerAbsY, clipW = outerRect.width(), clipH = outerRect.height(); - if (context instanceof VanillaRenderContext mrc) { - absX = mrc.getDocumentOriginX() + Math.round(sceneRect.x() * docZoom); - absY = mrc.getDocumentOriginY() + Math.round((sceneRect.y() - mrc.getScrollOffsetY()) * docZoom); - outerAbsX = absX; - outerAbsY = absY; - } - // Scale layout dimensions to screen pixels when document zoom != 1. - int w = Math.round(sceneRect.width() * docZoom); - int h = Math.round(sceneRect.height() * docZoom); - int outerW = Math.round(outerRect.width() * docZoom); - int outerH = Math.round(outerRect.height() * docZoom); + LytRect screenRect = context.toScreenRect(sceneRect); + LytRect screenOuter = context.toScreenRect(outerRect); + int absX = screenRect.x(); + int absY = screenRect.y(); + int w = screenRect.width(); + int h = screenRect.height(); + int outerAbsX = screenOuter.x(); + int outerAbsY = screenOuter.y(); + int outerW = screenOuter.width(); + int outerH = screenOuter.height(); + float scale = sceneRect.width() > 0 ? (float) w / sceneRect.width() : 1.0f; int sceneRight = absX + w; int sceneBottom = absY + h; + int clipX = outerAbsX, clipY = outerAbsY, clipW = outerW, clipH = outerH; LytRect activeScissor = context.currentScissor(); if (activeScissor != null) { clipX = Math.max(absX, activeScissor.x()); @@ -2111,7 +2089,7 @@ public void render(RenderContext context) { this.lastAbsY = absY; this.lastW = w; this.lastH = h; - this.lastDocZoom = docZoom; + this.lastDocZoom = scale; this.lastOuterAbsX = outerAbsX; this.lastOuterAbsY = outerAbsY; this.lastOuterW = outerW; @@ -2122,7 +2100,7 @@ public void render(RenderContext context) { drawBlockStatsOverlay(context, sceneRect, outerRect); renderSceneBorder(context, sceneRect); if (interactive && sceneButtonsVisible) { - drawSceneButtons(sceneRect.x(), sceneRect.y(), w, h, absX, absY, docZoom); + drawSceneButtons(context, sceneRect, screenRect); } return; } @@ -2130,16 +2108,13 @@ public void render(RenderContext context) { renderSceneBackground(context, sceneRect); this.renderedContentClip = updateCachedRect(this.renderedContentClip, clipX, clipY, clipW, clipH); - if (cameraViewportOverride != null) { - camera.setViewportSize(cameraViewportOverride); - } else { - camera.setViewportSize(w, h); - } + LytSize camOverride = cameraViewportOverride; + camera.setViewportSize(camOverride != null ? camOverride : new LytSize(sceneW, sceneH)); this.lastAbsX = absX; this.lastAbsY = absY; this.lastW = w; this.lastH = h; - this.lastDocZoom = docZoom; + this.lastDocZoom = scale; this.lastOuterAbsX = outerAbsX; this.lastOuterAbsY = outerAbsY; this.lastOuterW = outerW; @@ -2287,15 +2262,11 @@ else if (pa instanceof OverlayAnnotation ov) { context.restoreExternalRenderState(); if (!overlays.isEmpty()) { - LytRect viewport = cachedOverlayViewport = updateCachedRect(cachedOverlayViewport, absX, absY, w, h); - // Scissor overlays to the scene rect so diamond icons etc. cannot escape. - // NOTE: pushScissor expects SCREEN coords (same space as GuideScreen.cachedScissorRect), - // not document-local coords. Passing sceneRect (doc-local) caused the scissor to clip - // overlays out of sight, making diamond annotations disappear. - context.pushScissor(viewport); + cachedOverlayViewport = updateCachedRect(cachedOverlayViewport, absX, absY, w, h); + context.pushLocalScissor(sceneRect); try { for (var o : overlays) { - o.render(camera, context, viewport); + o.render(camera, context, sceneRect); } } finally { context.popScissor(); @@ -2317,7 +2288,7 @@ else if (pa instanceof OverlayAnnotation ov) { renderSceneBorder(context, sceneRect); if (interactive && sceneButtonsVisible) { - drawSceneButtons(sceneRect.x(), sceneRect.y(), w, h, absX, absY, docZoom); + drawSceneButtons(context, sceneRect, screenRect); } } @@ -3255,12 +3226,43 @@ private static InWorldLineAnnotation createOriginAxisAnnotation(Vector3f to, Con return new InWorldLineAnnotation(new Vector3f(), to, color, ORIGIN_AXIS_THICKNESS); } - private void drawSceneButtons(int drawX, int drawY, int screenW, int screenH, int absX, int absY, float docZoom) { - this.lastAbsX = absX; - this.lastAbsY = absY; - this.lastW = screenW; - this.lastH = screenH; - this.lastDocZoom = docZoom; + private static boolean needsGlTransform(RenderContext context) { + return !(context instanceof VanillaRenderContext); + } + + private void pushGlTransform(RenderContext context, float scale) { + GL11.glPushMatrix(); + GL11.glTranslatef(context.getDocumentOriginX(), context.getDocumentOriginY(), 0f); + GL11.glScalef(scale, scale, 1f); + } + + private static void popGlTransform() { + GL11.glPopMatrix(); + } + + private void drawSceneButtons(RenderContext context, LytRect sceneRect, LytRect screenRect) { + this.lastAbsX = screenRect.x(); + this.lastAbsY = screenRect.y(); + this.lastW = screenRect.width(); + this.lastH = screenRect.height(); + float scale = sceneRect.width() > 0 ? (float) screenRect.width() / sceneRect.width() : 1.0f; + this.lastDocZoom = scale; + cachedScreenRect = updateCachedRect( + cachedScreenRect, + screenRect.x(), + screenRect.y(), + screenRect.width(), + screenRect.height()); + + // Draw each button in layout coords wrapped in the context's GL transform. + int layoutGap = BTN_OUTSIDE_GAP; + int layoutSize = BTN_SIZE; + int layoutStep = BTN_SIZE + BTN_GAP; + int bx = sceneRect.right() + layoutGap; + int by = sceneRect.y(); + + boolean useGl = needsGlTransform(context); + if (useGl) pushGlTransform(context, scale); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_TEXTURE_2D); @@ -3269,18 +3271,6 @@ private void drawSceneButtons(int drawX, int drawY, int screenW, int screenH, in GL11.glColor4f(1f, 1f, 1f, 1f); OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); - // GL drawing uses layout coords; convert screen pixels back to layout units for bx. - int layoutW = Math.round(screenW / docZoom); - float bz = buttonZoom; - int bx = drawX + layoutW + Math.round(BTN_OUTSIDE_GAP * bz); - int by = drawY; - int btnScreenSize = Math.round(BTN_SIZE * docZoom * bz); - int btnScreenStep = Math.round((BTN_SIZE + BTN_GAP) * docZoom * bz); - int absBx = absX + screenW + Math.round(BTN_OUTSIDE_GAP * docZoom * bz); - int absBy = absY; - sceneButtonsAbsX = absBx; - sceneButtonsAbsY = absBy; - cachedScreenRect = updateCachedRect(cachedScreenRect, absX, absY, screenW, screenH); int mx, my; try { var mc = Minecraft.getMinecraft(); @@ -3292,14 +3282,22 @@ private void drawSceneButtons(int drawX, int drawY, int screenW, int screenH, in my = -1; } GuideIconButton.Role[] roles = cachedSceneButtonRoles(); - int btnDocSize = Math.round(BTN_SIZE * bz); - int btnDocStep = Math.round((BTN_SIZE + BTN_GAP) * bz); for (var role : roles) { - boolean hover = mx >= absBx && my >= absBy && mx < absBx + btnScreenSize && my < absBy + btnScreenSize; - drawOneSceneButton(bx, by, btnDocSize, role, hover); - by += btnDocStep; - absBy += btnScreenStep; + LytRect btnLayout = new LytRect(bx, by, layoutSize, layoutSize); + LytRect btnScreen = context.toScreenRect(btnLayout); + boolean hover = mx >= btnScreen.x() && my >= btnScreen.y() + && mx < btnScreen.right() + && my < btnScreen.bottom(); + drawOneSceneButton(bx, by, layoutSize, role, hover); + if (role == roles[0]) { + sceneButtonsAbsX = btnScreen.x(); + sceneButtonsAbsY = btnScreen.y(); + lastBtnScreenSize = btnScreen.width(); + lastBtnScreenStep = Math.round((BTN_SIZE + BTN_GAP) * scale); + } + by += layoutStep; } + if (useGl) popGlTransform(); } private GuideIconButton.Role[] sceneButtonRoles() { @@ -3394,8 +3392,8 @@ public GuideIconButton.Role sceneButtonAt(int mouseX, int mouseY) { if (lastW <= 0 || lastH <= 0) return null; int bx = sceneButtonsAbsX; int by = sceneButtonsAbsY; - int btnScreenSize = Math.round(BTN_SIZE * lastDocZoom * buttonZoom); - int btnScreenStep = Math.round((BTN_SIZE + BTN_GAP) * lastDocZoom * buttonZoom); + int btnScreenSize = lastBtnScreenSize > 0 ? lastBtnScreenSize : Math.round(BTN_SIZE * lastDocZoom); + int btnScreenStep = lastBtnScreenStep > 0 ? lastBtnScreenStep : Math.round((BTN_SIZE + BTN_GAP) * lastDocZoom); int rolesHeight = btnScreenSize + Math.max(0, cachedSceneButtonRoles().length - 1) * btnScreenStep; LytRect buttonColumnRect = new LytRect(bx, by, btnScreenSize, rolesHeight); if (renderedContentClip != null) { @@ -6135,6 +6133,8 @@ private void drawPonderControls(RenderContext context, LytRect outerRect, int to int my = currentMousePositionAvailable ? currentMouseAbsY : -1; Minecraft mc = Minecraft.getMinecraft(); + boolean useGl = needsGlTransform(context); + if (useGl) pushGlTransform(context, lastDocZoom); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_TEXTURE_2D); @@ -6152,29 +6152,32 @@ private void drawPonderControls(RenderContext context, LytRect outerRect, int to int renderBarLeft = renderBtnX + PONDER_BTN_TOTAL_WIDTH + 2; int renderBarRight = outerRect.right() - SCENE_SLIDER_SIDE_PADDING; int renderBarW = renderBarRight - renderBarLeft; - if (renderBarW < 4) return; - int renderBarY = renderRowY + (rowH - GuideSliderRenderer.TRACK_HEIGHT) / 2; + if (renderBarW >= 4) { + int renderBarY = renderRowY + (rowH - GuideSliderRenderer.TRACK_HEIGHT) / 2; - int absBarLeft = absBtnX + PONDER_BTN_TOTAL_WIDTH + 2; - int absBarRight = lastOuterAbsX + lastOuterW - SCENE_SLIDER_SIDE_PADDING; - int absBarW = absBarRight - absBarLeft; - int absBarY = absRowY + (rowH - GuideSliderRenderer.TRACK_HEIGHT) / 2; + int absBarLeft = absBtnX + PONDER_BTN_TOTAL_WIDTH + 2; + int absBarRight = lastOuterAbsX + lastOuterW - SCENE_SLIDER_SIDE_PADDING; + int absBarW = absBarRight - absBarLeft; + int absBarY = absRowY + (rowH - GuideSliderRenderer.TRACK_HEIGHT) / 2; - float progress = ponderSceneData.getProgress(ponderCurrentTick); - GuideSliderRenderer.SliderGeometry renderGeom = GuideSliderRenderer - .layout(renderBarLeft, renderBarY, renderBarW, progress); - GuideSliderRenderer.SliderGeometry absGeom = GuideSliderRenderer.layout(absBarLeft, absBarY, absBarW, progress); + float progress = ponderSceneData.getProgress(ponderCurrentTick); + GuideSliderRenderer.SliderGeometry renderGeom = GuideSliderRenderer + .layout(renderBarLeft, renderBarY, renderBarW, progress); + GuideSliderRenderer.SliderGeometry absGeom = GuideSliderRenderer + .layout(absBarLeft, absBarY, absBarW, progress); - cachedPonderBarTrackRect = absGeom.trackRect(); - cachedPonderBarHitRect = absGeom.hitRect(); + cachedPonderBarTrackRect = absGeom.trackRect(); + cachedPonderBarHitRect = absGeom.hitRect(); - int hitPad = GuideSliderRenderer.HIT_PADDING_Y; - boolean barHighlighted = draggingPonderBar || (mx >= absBarLeft && mx < absBarRight - && my >= absBarY - hitPad - && my < absBarY + GuideSliderRenderer.TRACK_HEIGHT + hitPad); - GuideSliderRenderer.render(Gui::drawRect, renderGeom, barHighlighted); + int hitPad = GuideSliderRenderer.HIT_PADDING_Y; + boolean barHighlighted = draggingPonderBar || (mx >= absBarLeft && mx < absBarRight + && my >= absBarY - hitPad + && my < absBarY + GuideSliderRenderer.TRACK_HEIGHT + hitPad); + GuideSliderRenderer.render(Gui::drawRect, renderGeom, barHighlighted); - drawPonderKeyframeNodes(renderBarLeft, renderBarW, renderBarY, rowH, absBarLeft, absBarW, absBarY, mx, my); + drawPonderKeyframeNodes(renderBarLeft, renderBarW, renderBarY, rowH, absBarLeft, absBarW, absBarY, mx, my); + } + if (useGl) popGlTransform(); } private void drawPonderKeyframeNodes(int renderBarLeft, int renderBarW, int renderBarY, int rowH, int absBarLeft, @@ -6308,17 +6311,30 @@ private LytRect resolveSliderTrackRect(int rowIndex) { } private LytRect resolveSliderTrackRect(int originX, int originY, int outerWidth, int outerHeight, int rowIndex) { + return resolveSliderTrackRectImpl(originX, originY, outerWidth, outerHeight, rowIndex, false); + } + + private LytRect resolveSliderTrackLayoutRect(int originX, int originY, int outerWidth, int outerHeight, + int rowIndex) { + return resolveSliderTrackRectImpl(originX, originY, outerWidth, outerHeight, rowIndex, true); + } + + private LytRect resolveSliderTrackRectImpl(int originX, int originY, int outerWidth, int outerHeight, int rowIndex, + boolean layoutSpace) { if (rowIndex < 0 || outerWidth <= SCENE_SLIDER_SIDE_PADDING * 2 || getBottomControlRowCount() <= 0) { return LytRect.empty(); } - int totalControlHeight = getBottomControlAreaHeight(); + float z = layoutSpace ? 1.0f : lastDocZoom; + int totalControlHeight = Math.round(getBottomControlAreaHeight() * z); if (outerHeight < totalControlHeight) { return LytRect.empty(); } - int rowTop = originY + outerHeight - totalControlHeight + rowIndex * SCENE_SLIDER_AREA_HEIGHT; - int sliderX = originX + SCENE_SLIDER_SIDE_PADDING; - int sliderWidth = Math.max(24, outerWidth - SCENE_SLIDER_SIDE_PADDING * 2); - int sliderY = rowTop + (SCENE_SLIDER_AREA_HEIGHT - GuideSliderRenderer.TRACK_HEIGHT) / 2; + int rowH = Math.round(SCENE_SLIDER_AREA_HEIGHT * z); + int sidePad = Math.round(SCENE_SLIDER_SIDE_PADDING * z); + int rowTop = originY + outerHeight - totalControlHeight + rowIndex * rowH; + int sliderX = originX + sidePad; + int sliderWidth = Math.max(24, outerWidth - sidePad * 2); + int sliderY = rowTop + (rowH - GuideSliderRenderer.TRACK_HEIGHT) / 2; return new LytRect(sliderX, sliderY, sliderWidth, GuideSliderRenderer.TRACK_HEIGHT); } @@ -6356,7 +6372,7 @@ private LytRect resolveVisibleLayerSliderHitRect() { private void drawVisibleLayerSlider(RenderContext context, LytRect outerRect) { int rowIndex = resolveVisibleLayerRowIndex(); LytRect screenTrackRect = resolveVisibleLayerSliderTrackRect(); - LytRect renderTrackRect = resolveSliderTrackRect( + LytRect renderTrackRect = resolveSliderTrackLayoutRect( outerRect.x(), outerRect.y(), outerRect.width(), @@ -6426,7 +6442,7 @@ private LytRect resolveStructureLibTierSliderHitRect() { private void drawStructureLibTierSlider(RenderContext context, LytRect outerRect) { int rowIndex = resolveStructureLibTierRowIndex(); LytRect screenTrackRect = resolveStructureLibTierSliderTrackRect(); - LytRect renderTrackRect = resolveSliderTrackRect( + LytRect renderTrackRect = resolveSliderTrackLayoutRect( outerRect.x(), outerRect.y(), outerRect.width(), @@ -6532,7 +6548,7 @@ private void drawStructureLibChannelSlider(RenderContext context, LytRect outerR StructureLibSceneMetadata.ChannelData channelData) { int rowIndex = resolveStructureLibChannelRowIndex(channelData.getChannelId()); LytRect screenTrackRect = resolveStructureLibChannelSliderTrackRect(channelData.getChannelId()); - LytRect renderTrackRect = resolveSliderTrackRect( + LytRect renderTrackRect = resolveSliderTrackLayoutRect( outerRect.x(), outerRect.y(), outerRect.width(), @@ -6618,9 +6634,17 @@ private void nudgeStructureLibChannel(String channelId, int dwheel) { private void drawSlider(RenderContext context, GuideSliderRenderer.SliderGeometry geometry, boolean highlighted, LytRect outerRect, int rowIndex, String label, ResolvedTextStyle style) { - GuideSliderRenderer.render(Gui::drawRect, geometry, highlighted); - int textWidth = context.getStringWidth(label, style); - int textHeight = context.getLineHeight(style); + if (needsGlTransform(context)) { + pushGlTransform(context, lastDocZoom); + GuideSliderRenderer.render(Gui::drawRect, geometry, highlighted); + popGlTransform(); + } else { + GuideSliderRenderer.render(Gui::drawRect, geometry, highlighted); + } + + float z = Math.max(0.0001f, lastDocZoom); + int textWidth = Math.round(context.getStringWidth(label, style) / z); + int textHeight = Math.round(context.getLineHeight(style) / z); int textX = outerRect.x() + (outerRect.width() - textWidth) / 2; int rowTop = bottomControlAreaTop(outerRect.bottom()) + rowIndex * SCENE_SLIDER_AREA_HEIGHT; int textY = rowTop + (SCENE_SLIDER_AREA_HEIGHT - textHeight) / 2; From 47adfdb249f9fbafa66d737f5fd4d28d52823f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=B0=E6=98=8E?= <8456918+Windorain@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:48:25 +0800 Subject: [PATCH 119/136] add beginLocalView/endLocalView to RenderContext, clean up ponder coordinate mixing - RenderContext: add beginLocalView()/endLocalView() default no-ops - NodeContentRenderContext: override to push GL translate+scale for raw GL - drawSceneButtons, drawPonderControls, drawSlider: use context.beginLocalView() instead of instanceof VanillaRenderContext check + manual pushGlTransform - drawPonderControls: use context.toScreenRect() for all screen-space hit-test coordinates instead of manual lastDocZoom multiplication - drawPonderKeyframeNodes: pass context+scale for hover via toScreenRect - Overlay rendering: pass screen-space viewport to o.render(), keep scissor separate via pushLocalScissor(sceneRect) - Store cachedPonderBtnScreenW/H from toScreenRect for ponderButtonAt and containsPonderButtons --- .../block/LytMermaidMindmapCanvas.java | 12 ++ .../guidenh/guide/render/RenderContext.java | 15 ++ .../guide/scene/LytGuidebookScene.java | 129 ++++++++---------- 3 files changed, 87 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 73132a1a..dbbb6de1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -1506,6 +1506,18 @@ public void restoreExternalRenderState() { delegate.restoreExternalRenderState(); } + @Override + public void beginLocalView() { + GL11.glPushMatrix(); + GL11.glTranslatef(originX, originY, 0f); + GL11.glScalef(scale, scale, 1f); + } + + @Override + public void endLocalView() { + GL11.glPopMatrix(); + } + private ResolvedTextStyle scaleStyle(ResolvedTextStyle style) { return scaledStyleCache.computeIfAbsent( style, diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java index 5fa3b0ff..977875e9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java @@ -169,4 +169,19 @@ default void fillTexturedRect(LytRect rect, GuidePageTexture texture) { blitTexture(texture.getTexture(), rect.x(), rect.y(), 0, 0, rect.width(), rect.height()); } } + + /** + * Set up the GL modelview so that raw OpenGL calls (Tessellator, direct + * {@code Gui.drawRect}, etc.) operate in this context's coordinate space. + * This is a no-op when the GL state is already set up for this space + * (e.g. vanilla screen rendering). Subclasses that apply a local transform + * should override this pair. + */ + default void beginLocalView() {} + + /** + * Restore the GL modelview to the state it was in before + * {@link #beginLocalView()}. + */ + default void endLocalView() {} } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 5eb08706..30924c63 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -60,7 +60,6 @@ import com.hfstudio.guidenh.guide.internal.util.DisplayScale; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; -import com.hfstudio.guidenh.guide.render.VanillaRenderContext; import com.hfstudio.guidenh.guide.scene.annotation.DiamondAnnotation; import com.hfstudio.guidenh.guide.scene.annotation.InWorldAnnotation; import com.hfstudio.guidenh.guide.scene.annotation.InWorldBlockFaceOverlayAnnotation; @@ -222,6 +221,8 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private LytRect cachedPonderBarHitRect; private int cachedPonderBtnAbsX; private int cachedPonderBtnAbsY; + private int cachedPonderBtnScreenW; + private int cachedPonderBtnScreenH; private boolean interactive = true; @@ -2262,11 +2263,11 @@ else if (pa instanceof OverlayAnnotation ov) { context.restoreExternalRenderState(); if (!overlays.isEmpty()) { - cachedOverlayViewport = updateCachedRect(cachedOverlayViewport, absX, absY, w, h); + LytRect viewport = cachedOverlayViewport = updateCachedRect(cachedOverlayViewport, absX, absY, w, h); context.pushLocalScissor(sceneRect); try { for (var o : overlays) { - o.render(camera, context, sceneRect); + o.render(camera, context, viewport); } } finally { context.popScissor(); @@ -3226,20 +3227,6 @@ private static InWorldLineAnnotation createOriginAxisAnnotation(Vector3f to, Con return new InWorldLineAnnotation(new Vector3f(), to, color, ORIGIN_AXIS_THICKNESS); } - private static boolean needsGlTransform(RenderContext context) { - return !(context instanceof VanillaRenderContext); - } - - private void pushGlTransform(RenderContext context, float scale) { - GL11.glPushMatrix(); - GL11.glTranslatef(context.getDocumentOriginX(), context.getDocumentOriginY(), 0f); - GL11.glScalef(scale, scale, 1f); - } - - private static void popGlTransform() { - GL11.glPopMatrix(); - } - private void drawSceneButtons(RenderContext context, LytRect sceneRect, LytRect screenRect) { this.lastAbsX = screenRect.x(); this.lastAbsY = screenRect.y(); @@ -3261,8 +3248,7 @@ private void drawSceneButtons(RenderContext context, LytRect sceneRect, LytRect int bx = sceneRect.right() + layoutGap; int by = sceneRect.y(); - boolean useGl = needsGlTransform(context); - if (useGl) pushGlTransform(context, scale); + context.beginLocalView(); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_TEXTURE_2D); @@ -3297,7 +3283,7 @@ private void drawSceneButtons(RenderContext context, LytRect sceneRect, LytRect } by += layoutStep; } - if (useGl) popGlTransform(); + context.endLocalView(); } private GuideIconButton.Role[] sceneButtonRoles() { @@ -3377,14 +3363,13 @@ private boolean isSceneButtonActive(GuideIconButton.Role role) { @Nullable public GuideIconButton.Role sceneButtonAt(int mouseX, int mouseY) { - if (ponderSceneData != null && lastOuterH > 0) { - int btnSize = SCENE_SLIDER_AREA_HEIGHT; - if (mouseY >= cachedPonderBtnAbsY && mouseY < cachedPonderBtnAbsY + btnSize) { + if (ponderSceneData != null && lastOuterH > 0 && cachedPonderBtnScreenW > 0) { + if (mouseY >= cachedPonderBtnAbsY && mouseY < cachedPonderBtnAbsY + cachedPonderBtnScreenH) { GuideIconButton.Role[] pRoles = { GuideIconButton.Role.PONDER_PREV_KEYFRAME, GuideIconButton.Role.PONDER_PLAY_PAUSE, GuideIconButton.Role.PONDER_RESTART }; for (int i = 0; i < pRoles.length; i++) { - int bx = cachedPonderBtnAbsX + i * btnSize; - if (mouseX >= bx && mouseX < bx + btnSize) return pRoles[i]; + int bx = cachedPonderBtnAbsX + i * cachedPonderBtnScreenW; + if (mouseX >= bx && mouseX < bx + cachedPonderBtnScreenW) return pRoles[i]; } } } @@ -3606,14 +3591,13 @@ public boolean containsVisibleLayerSlider(int mouseX, int mouseY) { } public boolean containsPonderButtons(int mouseX, int mouseY) { - if (!hasBottomControls() || ponderSceneData == null) { + if (!hasBottomControls() || ponderSceneData == null || cachedPonderBtnScreenW <= 0) { return false; } - int btnW = SCENE_SLIDER_AREA_HEIGHT; - int totalW = PONDER_BTN_TOTAL_WIDTH; + int totalW = cachedPonderBtnScreenW * PONDER_BUTTON_ROLES.length; return mouseX >= cachedPonderBtnAbsX && mouseX < cachedPonderBtnAbsX + totalW && mouseY >= cachedPonderBtnAbsY - && mouseY < cachedPonderBtnAbsY + btnW; + && mouseY < cachedPonderBtnAbsY + cachedPonderBtnScreenH; } @Nullable @@ -3622,8 +3606,7 @@ public GuideIconButton.Role ponderButtonAt(int mouseX, int mouseY) { return null; } int relX = mouseX - cachedPonderBtnAbsX; - int btnW = SCENE_SLIDER_AREA_HEIGHT; - int idx = relX / btnW; + int idx = relX / cachedPonderBtnScreenW; return switch (idx) { case 0 -> GuideIconButton.Role.PONDER_PREV_KEYFRAME; case 1 -> GuideIconButton.Role.PONDER_PLAY_PAUSE; @@ -6117,31 +6100,35 @@ public int buttonColumnReserve() { } private void drawPonderControls(RenderContext context, LytRect outerRect, int totalControlH) { - int renderRowY = outerRect.bottom() - totalControlH; - int absRowY = lastOuterAbsY + lastOuterH - totalControlH; - int rowH = SCENE_SLIDER_AREA_HEIGHT; int btnSize = SCENE_SLIDER_AREA_HEIGHT; + int sidePad = SCENE_SLIDER_SIDE_PADDING; + int renderRowY = outerRect.bottom() - totalControlH; + int renderBtnX = outerRect.x() + sidePad; - int renderBtnX = outerRect.x() + SCENE_SLIDER_SIDE_PADDING; - int absBtnX = lastOuterAbsX + SCENE_SLIDER_SIDE_PADDING; - - cachedPonderBtnAbsX = absBtnX; - cachedPonderBtnAbsY = absRowY; + // Store screen-space button origin for hit-testing. + LytRect firstBtnLayout = new LytRect(renderBtnX, renderRowY, btnSize, btnSize); + LytRect firstBtnScreen = context.toScreenRect(firstBtnLayout); + cachedPonderBtnAbsX = firstBtnScreen.x(); + cachedPonderBtnAbsY = firstBtnScreen.y(); + cachedPonderBtnScreenW = firstBtnScreen.width(); + cachedPonderBtnScreenH = firstBtnScreen.height(); int mx = currentMousePositionAvailable ? currentMouseAbsX : -1; int my = currentMousePositionAvailable ? currentMouseAbsY : -1; Minecraft mc = Minecraft.getMinecraft(); - boolean useGl = needsGlTransform(context); - if (useGl) pushGlTransform(context, lastDocZoom); + context.beginLocalView(); GL11.glDisable(GL11.GL_DEPTH_TEST); GL11.glDisable(GL11.GL_LIGHTING); GL11.glEnable(GL11.GL_TEXTURE_2D); for (int i = 0; i < PONDER_BUTTON_ROLES.length; i++) { - int absX = absBtnX + i * btnSize; - boolean hover = mx >= absX && mx < absX + btnSize && my >= absRowY && my < absRowY + btnSize; + LytRect btnLayout = new LytRect(renderBtnX + i * btnSize, renderRowY, btnSize, btnSize); + LytRect btnScreen = context.toScreenRect(btnLayout); + boolean hover = mx >= btnScreen.x() && mx < btnScreen.right() + && my >= btnScreen.y() + && my < btnScreen.bottom(); boolean active = PONDER_BUTTON_ROLES[i] == GuideIconButton.Role.PONDER_PLAY_PAUSE && !ponderPaused && !ponderFinished; int color = GuideIconButton.resolveIconColor(true, hover, active); @@ -6150,38 +6137,37 @@ private void drawPonderControls(RenderContext context, LytRect outerRect, int to } int renderBarLeft = renderBtnX + PONDER_BTN_TOTAL_WIDTH + 2; - int renderBarRight = outerRect.right() - SCENE_SLIDER_SIDE_PADDING; + int renderBarRight = outerRect.right() - sidePad; int renderBarW = renderBarRight - renderBarLeft; if (renderBarW >= 4) { int renderBarY = renderRowY + (rowH - GuideSliderRenderer.TRACK_HEIGHT) / 2; - - int absBarLeft = absBtnX + PONDER_BTN_TOTAL_WIDTH + 2; - int absBarRight = lastOuterAbsX + lastOuterW - SCENE_SLIDER_SIDE_PADDING; - int absBarW = absBarRight - absBarLeft; - int absBarY = absRowY + (rowH - GuideSliderRenderer.TRACK_HEIGHT) / 2; + int trackH = GuideSliderRenderer.TRACK_HEIGHT; float progress = ponderSceneData.getProgress(ponderCurrentTick); GuideSliderRenderer.SliderGeometry renderGeom = GuideSliderRenderer .layout(renderBarLeft, renderBarY, renderBarW, progress); + + // Screen-space bar rect for hit-testing. + LytRect barScreen = context.toScreenRect(new LytRect(renderBarLeft, renderBarY, renderBarW, trackH)); GuideSliderRenderer.SliderGeometry absGeom = GuideSliderRenderer - .layout(absBarLeft, absBarY, absBarW, progress); + .layout(barScreen.x(), barScreen.y(), barScreen.width(), progress); cachedPonderBarTrackRect = absGeom.trackRect(); cachedPonderBarHitRect = absGeom.hitRect(); int hitPad = GuideSliderRenderer.HIT_PADDING_Y; - boolean barHighlighted = draggingPonderBar || (mx >= absBarLeft && mx < absBarRight - && my >= absBarY - hitPad - && my < absBarY + GuideSliderRenderer.TRACK_HEIGHT + hitPad); + boolean barHighlighted = draggingPonderBar || (mx >= barScreen.x() && mx < barScreen.right() + && my >= barScreen.y() - hitPad + && my < barScreen.bottom() + hitPad); GuideSliderRenderer.render(Gui::drawRect, renderGeom, barHighlighted); - drawPonderKeyframeNodes(renderBarLeft, renderBarW, renderBarY, rowH, absBarLeft, absBarW, absBarY, mx, my); + drawPonderKeyframeNodes(context, renderBarLeft, renderBarW, renderBarY, rowH, mx, my); } - if (useGl) popGlTransform(); + context.endLocalView(); } - private void drawPonderKeyframeNodes(int renderBarLeft, int renderBarW, int renderBarY, int rowH, int absBarLeft, - int absBarW, int absBarY, int mx, int my) { + private void drawPonderKeyframeNodes(RenderContext context, int renderBarLeft, int renderBarW, int renderBarY, + int rowH, int mx, int my) { if (ponderSceneData == null || renderBarW <= 0) return; int totalTime = ponderSceneData.getTotalTime(); int nodeW = 2; @@ -6195,10 +6181,13 @@ private void drawPonderKeyframeNodes(int renderBarLeft, int renderBarW, int rend } float frac = totalTime > 0 ? kf.getTime() / (float) totalTime : 0f; int renderNx = renderBarLeft + Math.round(frac * (renderBarW - nodeW)); - int absNx = absBarLeft + Math.round(frac * (absBarW - nodeW)); - boolean hovered = mx >= absNx - 2 && mx < absNx + nodeW + 4 - && my >= absBarY - hitPad - && my < absBarY + GuideSliderRenderer.TRACK_HEIGHT + hitPad; + + // Screen-space hit region. + LytRect nodeScreen = context + .toScreenRect(new LytRect(renderNx, nodeRenderY, nodeW, GuideSliderRenderer.TRACK_HEIGHT)); + boolean hovered = mx >= nodeScreen.x() - 2 && mx < nodeScreen.right() + 4 + && my >= nodeScreen.y() - hitPad + && my < nodeScreen.bottom() + hitPad; int nodeColor = hovered ? PONDER_KEYFRAME_NODE_HOVER_COLOR : PONDER_KEYFRAME_NODE_COLOR; int drawH = hovered ? nodeH + 2 : nodeH; int drawY = hovered ? nodeRenderY - 1 : nodeRenderY; @@ -6209,7 +6198,13 @@ private void drawPonderKeyframeNodes(int renderBarLeft, int renderBarW, int rend if (label != null && !label.isEmpty()) { Minecraft mc = Minecraft.getMinecraft(); boolean isAhead = ponderCurrentTick < kf.getTime(); - int textX = isAhead ? renderNx - mc.fontRenderer.getStringWidth(label) - 4 : renderNx + nodeW + 4; + int textW = mc.fontRenderer.getStringWidth(label); + int gap = 4; + // Convert screen-space text metrics to layout for GL-transform drawing. + float z = Math.max(0.0001f, lastDocZoom); + int layoutTextW = Math.round(textW / z); + int layoutGap = Math.round(gap / z); + int textX = isAhead ? renderNx - layoutTextW - layoutGap : renderNx + nodeW + layoutGap; mc.fontRenderer.drawStringWithShadow(label, textX, nodeRenderY, PONDER_KEYFRAME_NODE_HOVER_COLOR); } } @@ -6634,13 +6629,9 @@ private void nudgeStructureLibChannel(String channelId, int dwheel) { private void drawSlider(RenderContext context, GuideSliderRenderer.SliderGeometry geometry, boolean highlighted, LytRect outerRect, int rowIndex, String label, ResolvedTextStyle style) { - if (needsGlTransform(context)) { - pushGlTransform(context, lastDocZoom); - GuideSliderRenderer.render(Gui::drawRect, geometry, highlighted); - popGlTransform(); - } else { - GuideSliderRenderer.render(Gui::drawRect, geometry, highlighted); - } + context.beginLocalView(); + GuideSliderRenderer.render(Gui::drawRect, geometry, highlighted); + context.endLocalView(); float z = Math.max(0.0001f, lastDocZoom); int textWidth = Math.round(context.getStringWidth(label, style) / z); From 157d650be8568c698a3c6186d1f3e020ed34e62f Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:18:21 +0800 Subject: [PATCH 120/136] add content tabs --- README.md | 2 +- README_zh.md | 2 +- .../guide/compiler/tags/ContentTabsSpec.java | 24 +++ .../compiler/tags/ContentTabsTagCompiler.java | 133 ++++++++++++ .../document/block/LytContentTabsBlock.java | 200 ++++++++++++++++++ .../autocomplete/TagAttributeRegistry.java | 5 + .../extensions/DefaultExtensions.java | 1 + .../guidenh/guide/render/RenderContext.java | 8 + .../guide/render/VanillaRenderContext.java | 68 ++++++ .../site/GuideSiteMdxTagRenderer.java | 130 ++++++++++++ .../assets/guidenh/siteexport/app.css | 63 ++++++ wiki/Tags-Reference-zh-CN.md | 24 +++ wiki/Tags-Reference.md | 24 +++ .../assets/guidenh/guidenh/_en_us/markdown.md | 21 ++ .../assets/guidenh/guidenh/_zh_cn/markdown.md | 21 ++ 15 files changed, 724 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java diff --git a/README.md b/README.md index 6cb0cabf..f6b7f8a1 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ## **Features** * Markdown pages with YAML frontmatter, navigation metadata, categories, anchors, tables, footnotes, Mermaid, LaTeX, charts, and highlighted text. -* MDX-style runtime tags such as ``, ``, ``, ``, ``, ``, ``, and ``. +* MDX-style runtime tags such as ``, ``, ``, ``, ``, ``, ``, ``, and ``. * Interactive 3D GameScene previews with block/entity placement, StructureLib import, Ponder playback, layer sliders, grid controls, annotations, and block statistics. * Live guide editing mode with split editor/preview, toolbar actions, debounced saving, external-change handling, and resource-pack page creation. * Multi-language guide folders with fallback, item index navigation, search, server integration, and resource reload support. diff --git a/README_zh.md b/README_zh.md index 0109a8c4..c43236cb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -22,7 +22,7 @@ ## **功能** * 支持带 YAML 表头的 Markdown 页面、导航元数据、分类、锚点、表格、脚注、Mermaid、LaTeX、图表与文本高亮。 -* 支持 ``、``、``、``、``、``、``、`` 等 MDX 风格运行时标签。 +* 支持 ``、``、``、``、``、``、``、``、`` 等 MDX 风格运行时标签。 * 支持交互式 3D GameScene 预览,包括方块/实体放置、StructureLib 导入、Ponder 播放、层滑条、网格按钮、注解与方块统计表。 * 支持游戏内指南编辑模式,包括编辑/预览分屏、工具栏操作、短延迟自动保存、外部变更处理与资源包页面创建。 * 支持多语言指南目录与回退、物品索引跳转、搜索、服务端集成和资源重载。 diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java new file mode 100644 index 00000000..52f0a911 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java @@ -0,0 +1,24 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import com.github.bsideup.jabel.Desugar; +import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.libs.unist.UnistNode; + +@Desugar +public record ContentTabsSpec(@Nullable String defaultTitle, @Nullable Integer defaultIndex, List tabs, + List issues) { + + public boolean hasRenderableTabs() { + return !tabs.isEmpty(); + } + + @Desugar + public record TabEntry(String title, LytBlock body, UnistNode sourceNode) {} + + @Desugar + public record ValidationIssue(String message, UnistNode sourceNode) {} +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java new file mode 100644 index 00000000..5cd50f1d --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java @@ -0,0 +1,133 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; +import com.hfstudio.guidenh.guide.document.block.LytContentTabsBlock; +import com.hfstudio.guidenh.guide.document.block.LytVBox; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; +import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; +import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; +import com.hfstudio.guidenh.libs.unist.UnistNode; + +public class ContentTabsTagCompiler extends BlockTagCompiler { + + @Override + public Set getTagNames() { + return Collections.singleton("ContentTabs"); + } + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + ContentTabsSpec spec = parseSpec(compiler, el); + for (ContentTabsSpec.ValidationIssue issue : spec.issues()) { + parent.appendError(compiler, issue.message(), issue.sourceNode()); + } + if (!spec.hasRenderableTabs()) { + return; + } + parent.append(new LytContentTabsBlock(resolveInitialIndex(spec), spec.tabs())); + } + + private ContentTabsSpec parseSpec(PageCompiler compiler, MdxJsxElementFields el) { + List children = resolveChildren(compiler, el); + List tabs = new ArrayList<>(); + List issues = new ArrayList<>(); + Integer defaultIndex = parseDefaultIndex(el, issues, el); + String defaultTitle = el.getAttributeString("default", null); + collectTabs(compiler, children, tabs, issues); + validateDefaultTarget(defaultTitle, defaultIndex, tabs, issues, el); + return new ContentTabsSpec(defaultTitle, defaultIndex, tabs, issues); + } + + private List resolveChildren(PageCompiler compiler, MdxJsxElementFields el) { + String source = compiler.getBlockTagChildrenSource(el); + return source != null ? compiler.reparseBlockTagChildren(el) : el.children(); + } + + private void collectTabs(PageCompiler compiler, List children, + List tabs, List issues) { + for (MdAstAnyContent child : children) { + if (!(child instanceof MdxJsxFlowElement element)) { + issues.add(new ContentTabsSpec.ValidationIssue("ContentTabs only accepts children.", child)); + continue; + } + if (!"Tab".equals(element.name())) { + issues.add(new ContentTabsSpec.ValidationIssue("ContentTabs only accepts children.", element)); + continue; + } + String title = element.getAttributeString("title", null); + if (title == null || title.trim() + .isEmpty()) { + issues.add(new ContentTabsSpec.ValidationIssue(" requires a non-empty title attribute.", element)); + continue; + } + LytVBox body = new LytVBox(); + body.setGap(4); + compiler.compileBlockContextInSourceContext(element.children(), body); + tabs.add(new ContentTabsSpec.TabEntry(title.trim(), body, element)); + } + } + + private @Nullable Integer parseDefaultIndex(MdxJsxElementFields el, List issues, + UnistNode sourceNode) { + String raw = el.getAttributeString("defaultIndex", null); + if (raw == null || raw.trim() + .isEmpty()) { + return null; + } + try { + return Integer.parseInt(raw.trim()); + } catch (NumberFormatException ignored) { + issues.add(new ContentTabsSpec.ValidationIssue("defaultIndex must be an integer.", sourceNode)); + return null; + } + } + + private void validateDefaultTarget(@Nullable String defaultTitle, @Nullable Integer defaultIndex, + List tabs, List issues, UnistNode sourceNode) { + if (defaultIndex != null && (defaultIndex < 0 || defaultIndex >= tabs.size())) { + issues + .add(new ContentTabsSpec.ValidationIssue("defaultIndex is out of range for ContentTabs.", sourceNode)); + return; + } + if (defaultIndex == null && defaultTitle != null) { + boolean matched = tabs.stream() + .anyMatch(tab -> defaultTitle.equals(tab.title())); + if (!matched) { + issues.add(new ContentTabsSpec.ValidationIssue("default does not match any title.", sourceNode)); + } + } + } + + static int resolveInitialIndex(ContentTabsSpec spec) { + if (spec.tabs() + .isEmpty()) { + return 0; + } + if (spec.defaultIndex() != null && spec.defaultIndex() >= 0 + && spec.defaultIndex() < spec.tabs() + .size()) { + return spec.defaultIndex(); + } + if (spec.defaultTitle() != null) { + for (int index = 0; index < spec.tabs() + .size(); index++) { + if (spec.defaultTitle() + .equals( + spec.tabs() + .get(index) + .title())) { + return index; + } + } + } + return 0; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java new file mode 100644 index 00000000..43259b23 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java @@ -0,0 +1,200 @@ +package com.hfstudio.guidenh.guide.document.block; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.ContentTabsSpec; +import com.hfstudio.guidenh.guide.document.LytRect; +import com.hfstudio.guidenh.guide.document.interaction.GuideTooltip; +import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; +import com.hfstudio.guidenh.guide.layout.LayoutContext; +import com.hfstudio.guidenh.guide.render.RenderContext; +import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; +import com.hfstudio.guidenh.guide.style.TextAlignment; +import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; +import com.hfstudio.guidenh.guide.ui.GuideUiHost; + +public class LytContentTabsBlock extends LytBlock implements InteractiveElement { + + private static final int HEADER_GAP = 4; + private static final int HEADER_PAD_X = 8; + private static final int HEADER_PAD_Y = 5; + private static final int HEADER_RADIUS = 4; + private static final int BODY_GAP = 6; + private static final int SELECTED_FILL = 0xFF2E5C8A; + private static final int SELECTED_BORDER = 0xFF8FC7FF; + private static final int IDLE_FILL = 0xFF1F2430; + private static final int IDLE_BORDER = 0xFF5A6372; + private static final ResolvedTextStyle SELECTED_STYLE = new ResolvedTextStyle( + 1.0f, + false, + false, + false, + false, + false, + false, + false, + "", + new ConstantColor(0xFFFFFFFF), + WhiteSpaceMode.NORMAL, + TextAlignment.LEFT, + false, + null, + false); + private static final ResolvedTextStyle IDLE_STYLE = new ResolvedTextStyle( + 1.0f, + false, + false, + false, + false, + false, + false, + false, + "", + new ConstantColor(0xFFD8DEEA), + WhiteSpaceMode.NORMAL, + TextAlignment.LEFT, + false, + null, + false); + + private final List tabs = new ArrayList<>(); + private int selectedIndex; + private LytRect headerBounds = LytRect.empty(); + + public LytContentTabsBlock(int selectedIndex, List entries) { + this.selectedIndex = Math.max(0, selectedIndex); + for (ContentTabsSpec.TabEntry entry : entries) { + tabs.add(new TabState(entry.title(), entry.body())); + entry.body().parent = this; + } + setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); + setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); + setFullWidth(true); + } + + @Override + public List getChildren() { + return tabs.isEmpty() ? List.of() : List.of(tabs.get(selectedIndex).body); + } + + @Override + protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + if (tabs.isEmpty()) { + headerBounds = LytRect.empty(); + return new LytRect(x, y, 0, 0); + } + + selectedIndex = Math.max(0, Math.min(selectedIndex, tabs.size() - 1)); + + int cursorX = x; + int cursorY = y; + int rowHeight = 0; + int maxRight = x; + int headerBottom = y; + + for (TabState tab : tabs) { + int tabWidth = tab.measureWidth(context); + int tabHeight = tab.measureHeight(context); + if (cursorX > x && cursorX + tabWidth > x + availableWidth) { + cursorX = x; + cursorY += rowHeight + HEADER_GAP; + rowHeight = 0; + } + tab.bounds = new LytRect(cursorX, cursorY, tabWidth, tabHeight); + cursorX += tabWidth + HEADER_GAP; + rowHeight = Math.max(rowHeight, tabHeight); + maxRight = Math.max(maxRight, tab.bounds.right()); + headerBottom = Math.max(headerBottom, tab.bounds.bottom()); + } + + headerBounds = new LytRect(x, y, Math.max(0, maxRight - x), Math.max(0, headerBottom - y)); + LytBlock activeBody = tabs.get(selectedIndex).body; + LytRect bodyBounds = activeBody.layout(context, x, headerBounds.bottom() + BODY_GAP, availableWidth); + return new LytRect(x, y, Math.max(headerBounds.width(), bodyBounds.width()), bodyBounds.bottom() - y); + } + + @Override + protected void onLayoutMoved(int deltaX, int deltaY) { + headerBounds = headerBounds.move(deltaX, deltaY); + for (TabState tab : tabs) { + tab.bounds = tab.bounds.move(deltaX, deltaY); + tab.body.moveLayoutPos(deltaX, deltaY); + } + } + + @Override + public void render(RenderContext context) { + if (tabs.isEmpty()) { + return; + } + for (int index = 0; index < tabs.size(); index++) { + TabState tab = tabs.get(index); + boolean selected = index == selectedIndex; + context.fillRoundedRect(tab.bounds, selected ? SELECTED_FILL : IDLE_FILL, HEADER_RADIUS); + context.drawRoundedBorder(tab.bounds, selected ? SELECTED_BORDER : IDLE_BORDER, 1, HEADER_RADIUS); + context + .drawText(tab.title, tab.bounds.x() + HEADER_PAD_X, tab.bounds.y() + HEADER_PAD_Y, tab.style(selected)); + } + tabs.get(selectedIndex).body.render(context); + } + + @Override + public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolean doubleClick) { + if (tabs.isEmpty()) { + return false; + } + if (button == 0) { + for (int index = 0; index < tabs.size(); index++) { + if (tabs.get(index).bounds.contains(x, y)) { + if (selectedIndex != index) { + selectedIndex = index; + if (getDocument() != null) { + getDocument().invalidateLayout(); + } + } + return true; + } + } + } + LytBlock activeBody = tabs.get(selectedIndex).body; + return activeBody instanceof InteractiveElement interactive + && interactive.mouseClicked(screen, x, y, button, doubleClick); + } + + @Override + public Optional getTooltip(float x, float y) { + if (tabs.isEmpty()) { + return Optional.empty(); + } + LytBlock activeBody = tabs.get(selectedIndex).body; + return activeBody instanceof InteractiveElement interactive ? interactive.getTooltip(x, y) : Optional.empty(); + } + + private static class TabState { + + private final String title; + private final LytBlock body; + private LytRect bounds = LytRect.empty(); + + private TabState(String title, LytBlock body) { + this.title = title; + this.body = body; + } + + private int measureWidth(LayoutContext context) { + return context.getWidth(title, style(false)) + HEADER_PAD_X * 2; + } + + private int measureHeight(LayoutContext context) { + return context.getLineHeight(style(false)) + HEADER_PAD_Y * 2; + } + + private ResolvedTextStyle style(boolean selected) { + return selected ? SELECTED_STYLE : IDLE_STYLE; + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 9a677abb..aad34b50 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -125,6 +125,11 @@ public static void initialize() { new AttributeSpec("header", AttrType.BOOLEAN), new AttributeSpec("widths", AttrType.STRING)); register("details", new AttributeSpec("open", AttrType.BOOLEAN)); + register( + "ContentTabs", + new AttributeSpec("default", AttrType.STRING), + new AttributeSpec("defaultIndex", AttrType.INT)); + register("Tab", new AttributeSpec("title", AttrType.STRING)); register( "Row", new AttributeSpec("gap", AttrType.INT), diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java b/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java index 50dbe9f5..ff1e1045 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java @@ -124,6 +124,7 @@ public static List tagCompilers() { new ItemLinkCompiler(), new FloatingImageCompiler(), new BreakCompiler(), + new ContentTabsTagCompiler(), new DetailsTagCompiler(), new FileTreeTagCompiler(), new RecipeCompiler(), diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java index 977875e9..24425699 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/RenderContext.java @@ -66,6 +66,14 @@ default void drawBorder(int x, int y, int width, int height, int argbColor, int drawBorder(new LytRect(x, y, width, height), argbColor, thickness); } + default void fillRoundedRect(LytRect rect, int argbColor, int radius) { + fillRect(rect, argbColor); + } + + default void drawRoundedBorder(LytRect rect, int argbColor, int thickness, int radius) { + drawBorder(rect, argbColor, thickness); + } + void drawText(String text, int x, int y, ResolvedTextStyle style); int getStringWidth(String text, ResolvedTextStyle style); diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java index 5bc258b1..e2a770e1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java @@ -158,6 +158,74 @@ public void drawBorder(int x, int y, int width, int height, int argbColor, int t Gui.drawRect(right - thickness, y + thickness, right, bottom - thickness, argbColor); } + @Override + public void fillRoundedRect(LytRect rect, int argbColor, int radius) { + int clampedRadius = Math.max(0, Math.min(radius, Math.min(rect.width(), rect.height()) / 2)); + if (clampedRadius == 0) { + fillRect(rect, argbColor); + return; + } + fillRect(rect.x() + clampedRadius, rect.y(), rect.width() - clampedRadius * 2, rect.height(), argbColor); + fillRect(rect.x(), rect.y() + clampedRadius, clampedRadius, rect.height() - clampedRadius * 2, argbColor); + fillRect( + rect.right() - clampedRadius, + rect.y() + clampedRadius, + clampedRadius, + rect.height() - clampedRadius * 2, + argbColor); + fillCircle(rect.x() + clampedRadius, rect.y() + clampedRadius, clampedRadius, argbColor); + fillCircle(rect.right() - clampedRadius, rect.y() + clampedRadius, clampedRadius, argbColor); + fillCircle(rect.x() + clampedRadius, rect.bottom() - clampedRadius, clampedRadius, argbColor); + fillCircle(rect.right() - clampedRadius, rect.bottom() - clampedRadius, clampedRadius, argbColor); + } + + @Override + public void drawRoundedBorder(LytRect rect, int argbColor, int thickness, int radius) { + int clampedRadius = Math.max(0, Math.min(radius, Math.min(rect.width(), rect.height()) / 2)); + if (clampedRadius == 0) { + drawBorder(rect, argbColor, thickness); + return; + } + fillRect(rect.x() + clampedRadius, rect.y(), rect.width() - clampedRadius * 2, thickness, argbColor); + fillRect( + rect.x() + clampedRadius, + rect.bottom() - thickness, + rect.width() - clampedRadius * 2, + thickness, + argbColor); + fillRect(rect.x(), rect.y() + clampedRadius, thickness, rect.height() - clampedRadius * 2, argbColor); + fillRect( + rect.right() - thickness, + rect.y() + clampedRadius, + thickness, + rect.height() - clampedRadius * 2, + argbColor); + drawCircleOutline( + rect.x() + clampedRadius, + rect.y() + clampedRadius, + clampedRadius - thickness * 0.5f, + thickness, + argbColor); + drawCircleOutline( + rect.right() - clampedRadius, + rect.y() + clampedRadius, + clampedRadius - thickness * 0.5f, + thickness, + argbColor); + drawCircleOutline( + rect.x() + clampedRadius, + rect.bottom() - clampedRadius, + clampedRadius - thickness * 0.5f, + thickness, + argbColor); + drawCircleOutline( + rect.right() - clampedRadius, + rect.bottom() - clampedRadius, + clampedRadius - thickness * 0.5f, + thickness, + argbColor); + } + @Override public void drawText(String text, int x, int y, ResolvedTextStyle style) { if (text == null || text.isEmpty()) return; diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index d90c5990..1f2d3a26 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import net.minecraft.block.Block; import net.minecraft.client.Minecraft; @@ -108,6 +109,7 @@ public class GuideSiteMdxTagRenderer implements GuideSiteHtmlCompiler.MdxTagRend @Nullable private Map itemAnchorsByItemId; private final MediaWikiSpecialPageResolver specialPageResolver = new MediaWikiSpecialPageResolver(); + private final AtomicInteger contentTabsSequence = new AtomicInteger(); public GuideSiteMdxTagRenderer(Guide guide, Map parsedPagesById, NavigationTree navigationTree) { @@ -232,6 +234,9 @@ public GuideSiteMdxTagRenderer(Guide guide, Map") + .append(escapeHtml(error)) + .append(""); + } + if (parsed.tabs.isEmpty()) { + return html.toString(); + } + String groupName = "guide-content-tabs-" + contentTabsSequence.getAndIncrement(); + html.append("
    "); + for (int index = 0; index < parsed.tabs.size(); index++) { + ParsedTab tab = parsed.tabs.get(index); + boolean checked = index == parsed.selectedIndex; + String inputId = groupName + '-' + index; + html.append(""); + html.append(""); + html.append("
    ") + .append( + compiler.compileFragment(tab.children, templates, defaultNamespace, sceneResolver, currentPageId)) + .append("
    "); + } + html.append("
    "); + return html.toString(); + } + + private ParsedTabs parseContentTabs(MdxJsxElementFields element) { + List errors = new ArrayList<>(); + List tabs = new ArrayList<>(); + Integer defaultIndex = readPositiveOrZeroInt(readOptional(element, "defaultIndex")); + String defaultTitle = readOptional(element, "default"); + for (MdAstAnyContent child : element.children()) { + if (!(child instanceof MdxJsxFlowElement flowElement) || !"Tab".equals(flowElement.name())) { + errors.add("ContentTabs only accepts children."); + continue; + } + String title = flowElement.getAttributeString("title", null); + if (title == null || title.trim() + .isEmpty()) { + errors.add(" requires a non-empty title attribute."); + continue; + } + tabs.add(new ParsedTab(title.trim(), flowElement.children())); + } + int selectedIndex = resolveSelectedIndex(defaultTitle, defaultIndex, tabs, errors); + return new ParsedTabs(tabs, selectedIndex, errors); + } + + private int resolveSelectedIndex(@Nullable String defaultTitle, @Nullable Integer defaultIndex, + List tabs, List errors) { + if (tabs.isEmpty()) { + return 0; + } + if (defaultIndex != null) { + if (defaultIndex >= 0 && defaultIndex < tabs.size()) { + return defaultIndex; + } + errors.add("defaultIndex is out of range for ContentTabs."); + return 0; + } + if (defaultTitle != null) { + for (int index = 0; index < tabs.size(); index++) { + if (defaultTitle.equals(tabs.get(index).title)) { + return index; + } + } + errors.add("default does not match any title."); + } + return 0; + } + private String renderFileTree(MdxJsxElementFields element, String defaultNamespace, @Nullable ResourceLocation currentPageId, GuideSiteTemplateRegistry templates, GuideSiteHtmlCompiler.SceneResolver sceneResolver, GuideSiteHtmlCompiler compiler) { @@ -2696,6 +2788,20 @@ private Integer readPositiveInt(@Nullable String raw) { } } + @Nullable + private Integer readPositiveOrZeroInt(@Nullable String raw) { + if (raw == null || raw.trim() + .isEmpty()) { + return null; + } + try { + int value = Integer.parseInt(raw.trim()); + return value >= 0 ? value : null; + } catch (NumberFormatException ignored) { + return null; + } + } + private String renderFallbackItemLabel(String itemId, @Nullable ResourceLocation currentPageId, GuideSiteTemplateRegistry templates, boolean inline, @Nullable Float scale) { GuideSiteExportedItem exportedItem = GuideSiteItemSupport.unresolved(itemId); @@ -2901,4 +3007,28 @@ private StructureLegendEntry(GuideSiteExportedItem item, String abbreviation, @N this.templateId = templateId; } } + + private static class ParsedTabs { + + private final List tabs; + private final int selectedIndex; + private final List errors; + + private ParsedTabs(List tabs, int selectedIndex, List errors) { + this.tabs = tabs; + this.selectedIndex = selectedIndex; + this.errors = errors; + } + } + + private static class ParsedTab { + + private final String title; + private final List children; + + private ParsedTab(String title, List children) { + this.title = title; + this.children = children; + } + } } diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index 153f5fd9..78fa3ead 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -1751,6 +1751,69 @@ p { margin-bottom: 0; } +.guide-content-tabs { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 1rem 0; + padding: 0.75rem; + border: 1px solid #3d4656; + border-radius: 12px; + background: rgba(64, 64, 64, 0.25); +} + +.guide-content-tabs-input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.guide-content-tabs-label { + display: inline-flex; + align-items: center; + min-height: 2.25rem; + padding: 0.45rem 0.8rem; + border: 1px solid #586275; + border-radius: 999px; + background: #242b36; + color: #d8deea; + cursor: pointer; +} + +.guide-content-tabs-panel { + display: none; + order: 999; + width: 100%; + padding-top: 0.5rem; +} + +.guide-content-tabs-panel > :first-child { + margin-top: 0; +} + +.guide-content-tabs-panel > :last-child { + margin-bottom: 0; +} + +.guide-content-tabs-input:checked + .guide-content-tabs-label { + border-color: #8fc7ff; + background: #2a5278; + color: #ffffff; +} + +.guide-content-tabs-input:checked + .guide-content-tabs-label + .guide-content-tabs-panel { + display: block; +} + +.guide-content-tabs-error { + margin: 0.75rem 0; + padding: 0.65rem 0.8rem; + border: 1px solid #8f3030; + border-radius: 10px; + background: #331818; + color: #ffd4d4; +} + .guide-tooltip-scene-placeholder { display: inline-block; background: #0c1117; diff --git a/wiki/Tags-Reference-zh-CN.md b/wiki/Tags-Reference-zh-CN.md index 49eb2e64..726ff503 100644 --- a/wiki/Tags-Reference-zh-CN.md +++ b/wiki/Tags-Reference-zh-CN.md @@ -48,6 +48,8 @@ | 标签 | 用途 | 关键属性 | | --- | --- | --- | | `
    ` | 透传块包装器 | 无 | +| `` | 将可替代的富内容分组到独立标签页中 | `default`、`defaultIndex` | +| `` | `` 内的单个内容面板 | `title` | | `
    ` | 可折叠运行时块 | `open`、`width`、`height`、`wrap`、`align` | | `` | 目录树式大纲(带连接线) | `indent`、`gap` | | `` | 横向 flex 布局 | `gap`, `alignItems`, `fullWidth`, `width` | @@ -140,6 +142,28 @@ Water is H2O and x2 is a square. - `wrap` — 支持常见块嵌入模式,例如 `square`、`tight`、`through` - `align` — `left`、`center` 或 `right`;与浮动型 `wrap` 搭配时会让整个 details 块浮动 +### `` + +将可替代的富内容分组到独立标签页中。`` 只接受直接的 `` 子标签。 + +````mdx + + + ```java + System.out.println("Hello GuideNH"); + ``` + + + + + +```` + +- `default` 会匹配第一个 `title` 完全相同的标签页 +- `defaultIndex` 使用从 `0` 开始的下标,并且在同时出现时优先级高于 `default` +- `title` 仅支持纯文本 +- 非法子节点或非法默认值会渲染为面向作者的可见错误 + ### `` 渲染目录树式大纲,并依据每行前缀符号绘制真实的连接线。前缀同时支持 Unicode 框线(`│ ├ └ ─`)与 ASCII 形式(`| +-- \-- ` / 4 个空格),可任意混用。每行的文本部分支持常规行内 Markdown(链接、**加粗**、`代码` 等),这些链接在游戏内和内置站点导出中都可以点击。等价语法是 ` ```tree ` / ` ```filetree ` 围栏代码块。 diff --git a/wiki/Tags-Reference.md b/wiki/Tags-Reference.md index 60ccc0b4..4558212c 100644 --- a/wiki/Tags-Reference.md +++ b/wiki/Tags-Reference.md @@ -46,6 +46,8 @@ Inline markdown also supports action links for sound playback: | Tag | Purpose | Key attributes | | --- | --- | --- | | `
    ` | pass-through block wrapper | none | +| `` | groups alternative rich content under independent tabs | `default`, `defaultIndex` | +| `` | one content panel inside `` | `title` | | `
    ` | collapsible runtime block | `open`, `width`, `height`, `wrap`, `align` | | `` | directory-style outline with connector lines | `indent`, `gap` | | `` | horizontal flex layout | `gap`, `alignItems`, `fullWidth`, `width` | @@ -140,6 +142,28 @@ Attributes: - `wrap` — supports the usual block embedding modes such as `square`, `tight`, and `through` - `align` — `left`, `center`, or `right`; when combined with a floating wrap mode, the whole details block floats +### `` + +Groups alternative rich content under independent tabs. Only direct `` children are valid. + +````mdx + + + ```java + System.out.println("Hello GuideNH"); + ``` + + + + + +```` + +- `default` matches the first tab whose `title` matches exactly +- `defaultIndex` is zero-based and wins over `default` when both are present +- `title` is plain text only +- invalid children or invalid defaults render visible author-facing errors + ### `` Renders a directory-style outline with real connector lines drawn from the prefix glyphs on each row. Both Unicode box-drawing (`│ ├ └ ─`) and ASCII (`| +-- \-- ` / four spaces) forms are accepted and may be mixed. Payload text supports the usual inline markdown (links, **bold**, `code`, …), and those links are clickable both in-game and in the built-in site export. The same content can also be written as a fenced ` ```tree ` or ` ```filetree ` block. diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md index efbdd948..645014d6 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md @@ -295,6 +295,27 @@ together inside the same scrollable panel. Text outside the block should still wrap around it when `wrap="square"` is used. +## Content Tabs + +`` groups alternative rich content under one tab strip. Each child must be a direct +``, and the selected tab can be chosen with either `default` or `defaultIndex`. + + + + ```java + System.out.println("Hello GuideNH"); + ``` + + + + + + + + $$a^2 + b^2 = c^2$$ + + + ## Code Blocks Explicit language: diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md index e345e311..95843c8f 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md @@ -284,6 +284,27 @@ Markdown: 当使用 `wrap="square"` 时,块外文本仍应继续围绕它排版。 +## 内容标签页 + +`` 可以把可替代的富内容放进同一组标签页中。每个子节点都必须是直接的 +``,默认选中项可以通过 `default` 或 `defaultIndex` 指定。 + + + + ```java + System.out.println("Hello GuideNH"); + ``` + + + + + + + + $$a^2 + b^2 = c^2$$ + + + ## 代码块 显式语言: From 62b1da0d5ddcb4cfe8a21febb9bd145d86095fa4 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:39:00 +0800 Subject: [PATCH 121/136] opti --- .../guide/compiler/tags/ContentTabsSpec.java | 5 +- .../compiler/tags/ContentTabsTagCompiler.java | 22 ++- .../document/block/LytContentTabsBlock.java | 127 ++++++++++++++---- .../autocomplete/TagAttributeRegistry.java | 3 +- .../extensions/DefaultExtensions.java | 1 + .../guidenh/guide/layout/FontMetrics.java | 13 ++ .../site/GuideSiteMdxTagRenderer.java | 24 +++- .../assets/guidenh/siteexport/app.css | 22 +-- wiki/Tags-Reference-zh-CN.md | 3 +- wiki/Tags-Reference.md | 3 +- .../assets/guidenh/guidenh/_en_us/markdown.md | 3 +- .../assets/guidenh/guidenh/_zh_cn/markdown.md | 3 +- 12 files changed, 178 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java index 52f0a911..56b7bc75 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java @@ -5,12 +5,13 @@ import org.jetbrains.annotations.Nullable; import com.github.bsideup.jabel.Desugar; +import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.libs.unist.UnistNode; @Desugar -public record ContentTabsSpec(@Nullable String defaultTitle, @Nullable Integer defaultIndex, List tabs, - List issues) { +public record ContentTabsSpec(@Nullable String defaultTitle, @Nullable Integer defaultIndex, + @Nullable ColorValue accentColor, List tabs, List issues) { public boolean hasRenderableTabs() { return !tabs.isEmpty(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java index 5cd50f1d..6f0b6ed7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java @@ -7,7 +7,9 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.LytErrorSink; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytContentTabsBlock; import com.hfstudio.guidenh.guide.document.block.LytVBox; @@ -32,7 +34,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (!spec.hasRenderableTabs()) { return; } - parent.append(new LytContentTabsBlock(resolveInitialIndex(spec), spec.tabs())); + parent.append(new LytContentTabsBlock(resolveInitialIndex(spec), spec.accentColor(), spec.tabs())); } private ContentTabsSpec parseSpec(PageCompiler compiler, MdxJsxElementFields el) { @@ -41,9 +43,10 @@ private ContentTabsSpec parseSpec(PageCompiler compiler, MdxJsxElementFields el) List issues = new ArrayList<>(); Integer defaultIndex = parseDefaultIndex(el, issues, el); String defaultTitle = el.getAttributeString("default", null); + ColorValue accentColor = MdxAttrs.getColor(compiler, new ValidationIssueSink(issues), el, "color", null); collectTabs(compiler, children, tabs, issues); validateDefaultTarget(defaultTitle, defaultIndex, tabs, issues, el); - return new ContentTabsSpec(defaultTitle, defaultIndex, tabs, issues); + return new ContentTabsSpec(defaultTitle, defaultIndex, accentColor, tabs, issues); } private List resolveChildren(PageCompiler compiler, MdxJsxElementFields el) { @@ -130,4 +133,19 @@ static int resolveInitialIndex(ContentTabsSpec spec) { } return 0; } + + private static class ValidationIssueSink implements LytErrorSink { + + private final List issues; + + private ValidationIssueSink(List issues) { + this.issues = issues; + } + + @Override + public void appendError(PageCompiler compiler, String text, UnistNode node) { + issues.add(new ContentTabsSpec.ValidationIssue(text, node)); + } + + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java index 43259b23..8f4f4bb9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java @@ -4,7 +4,11 @@ import java.util.List; import java.util.Optional; +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.ContentTabsSpec; import com.hfstudio.guidenh.guide.document.LytRect; @@ -12,6 +16,7 @@ import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; +import com.hfstudio.guidenh.guide.style.BorderStyle; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.WhiteSpaceMode; @@ -19,15 +24,18 @@ public class LytContentTabsBlock extends LytBlock implements InteractiveElement { - private static final int HEADER_GAP = 4; - private static final int HEADER_PAD_X = 8; - private static final int HEADER_PAD_Y = 5; + private static final int ACCENT_WIDTH = 3; + private static final int CONTAINER_PAD_X = 10; + private static final int CONTAINER_PAD_Y = 6; + private static final int HEADER_GAP = 6; + private static final int HEADER_PAD_X = 7; + private static final int HEADER_PAD_Y = 3; private static final int HEADER_RADIUS = 4; private static final int BODY_GAP = 6; - private static final int SELECTED_FILL = 0xFF2E5C8A; - private static final int SELECTED_BORDER = 0xFF8FC7FF; - private static final int IDLE_FILL = 0xFF1F2430; - private static final int IDLE_BORDER = 0xFF5A6372; + private static final ConstantColor DEFAULT_ACCENT = new ConstantColor(0xFF7C8795); + private static final int SELECTED_FILL = 0x403E4B59; + private static final int IDLE_FILL = 0x241A1F26; + private static final int IDLE_BORDER = 0x66586275; private static final ResolvedTextStyle SELECTED_STYLE = new ResolvedTextStyle( 1.0f, false, @@ -38,7 +46,7 @@ public class LytContentTabsBlock extends LytBlock implements InteractiveElement false, false, "", - new ConstantColor(0xFFFFFFFF), + new ConstantColor(0xFFF4F7FB), WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, @@ -54,7 +62,7 @@ public class LytContentTabsBlock extends LytBlock implements InteractiveElement false, false, "", - new ConstantColor(0xFFD8DEEA), + new ConstantColor(0xFFD5DCE7), WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, @@ -62,45 +70,58 @@ public class LytContentTabsBlock extends LytBlock implements InteractiveElement false); private final List tabs = new ArrayList<>(); + private final List tabBodies = new ArrayList<>(); + private final ColorValue accentColor; private int selectedIndex; private LytRect headerBounds = LytRect.empty(); + private LytRect contentBounds = LytRect.empty(); - public LytContentTabsBlock(int selectedIndex, List entries) { + public LytContentTabsBlock(int selectedIndex, @Nullable ColorValue accentColor, + List entries) { + this.accentColor = accentColor != null ? accentColor : DEFAULT_ACCENT; this.selectedIndex = Math.max(0, selectedIndex); for (ContentTabsSpec.TabEntry entry : entries) { tabs.add(new TabState(entry.title(), entry.body())); + tabBodies.add(entry.body()); entry.body().parent = this; } setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); setFullWidth(true); + setBorderLeft(new BorderStyle(this.accentColor, ACCENT_WIDTH)); } @Override public List getChildren() { - return tabs.isEmpty() ? List.of() : List.of(tabs.get(selectedIndex).body); + // Expose every tab body to tree visitors so search, anchors, resource export, + // scene collection, and mount-time traversal still see hidden tabs. + return tabBodies; } @Override protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { if (tabs.isEmpty()) { headerBounds = LytRect.empty(); + contentBounds = LytRect.empty(); return new LytRect(x, y, 0, 0); } selectedIndex = Math.max(0, Math.min(selectedIndex, tabs.size() - 1)); - int cursorX = x; - int cursorY = y; + int contentX = x + ACCENT_WIDTH + CONTAINER_PAD_X; + int contentY = y + CONTAINER_PAD_Y; + int contentWidth = Math.max(0, availableWidth - ACCENT_WIDTH - CONTAINER_PAD_X * 2); + int cursorX = contentX; + int cursorY = contentY; int rowHeight = 0; - int maxRight = x; - int headerBottom = y; + int maxRight = contentX; + int headerBottom = contentY; for (TabState tab : tabs) { int tabWidth = tab.measureWidth(context); int tabHeight = tab.measureHeight(context); - if (cursorX > x && cursorX + tabWidth > x + availableWidth) { - cursorX = x; + if (cursorX > contentX && cursorX + tabWidth > contentX + contentWidth) { + cursorX = contentX; cursorY += rowHeight + HEADER_GAP; rowHeight = 0; } @@ -111,18 +132,37 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab headerBottom = Math.max(headerBottom, tab.bounds.bottom()); } - headerBounds = new LytRect(x, y, Math.max(0, maxRight - x), Math.max(0, headerBottom - y)); - LytBlock activeBody = tabs.get(selectedIndex).body; - LytRect bodyBounds = activeBody.layout(context, x, headerBounds.bottom() + BODY_GAP, availableWidth); - return new LytRect(x, y, Math.max(headerBounds.width(), bodyBounds.width()), bodyBounds.bottom() - y); + headerBounds = new LytRect( + contentX, + contentY, + Math.max(0, maxRight - contentX), + Math.max(0, headerBottom - contentY)); + int safeSelectedIndex = getSafeSelectedIndex(); + LytBlock activeBody = tabs.get(safeSelectedIndex).body; + LytRect bodyBounds = activeBody.layout(context, contentX, headerBounds.bottom() + BODY_GAP, contentWidth); + int contentRight = Math.max(headerBounds.right(), bodyBounds.right()); + int contentBottom = Math.max(headerBounds.bottom(), bodyBounds.bottom()); + contentBounds = new LytRect( + contentX, + contentY, + Math.max(0, contentRight - contentX), + Math.max(0, contentBottom - contentY)); + return new LytRect( + x, + y, + Math.max(availableWidth, ACCENT_WIDTH + CONTAINER_PAD_X * 2 + contentBounds.width()), + contentBounds.height() + CONTAINER_PAD_Y * 2); } @Override protected void onLayoutMoved(int deltaX, int deltaY) { headerBounds = headerBounds.move(deltaX, deltaY); + contentBounds = contentBounds.move(deltaX, deltaY); for (TabState tab : tabs) { tab.bounds = tab.bounds.move(deltaX, deltaY); - tab.body.moveLayoutPos(deltaX, deltaY); + } + if (!tabs.isEmpty()) { + tabs.get(getSafeSelectedIndex()).body.moveLayoutPos(deltaX, deltaY); } } @@ -131,15 +171,39 @@ public void render(RenderContext context) { if (tabs.isEmpty()) { return; } + int safeSelectedIndex = getSafeSelectedIndex(); + int accentArgb = context.resolveColor(accentColor); + context.fillRect(bounds, context.resolveColor(SymbolicColor.BLOCKQUOTE_BACKGROUND)); + context.fillRect(bounds.x(), bounds.y(), ACCENT_WIDTH, bounds.height(), accentArgb); for (int index = 0; index < tabs.size(); index++) { TabState tab = tabs.get(index); - boolean selected = index == selectedIndex; + boolean selected = index == safeSelectedIndex; + int border = selected ? accentArgb : IDLE_BORDER; context.fillRoundedRect(tab.bounds, selected ? SELECTED_FILL : IDLE_FILL, HEADER_RADIUS); - context.drawRoundedBorder(tab.bounds, selected ? SELECTED_BORDER : IDLE_BORDER, 1, HEADER_RADIUS); + context.drawRoundedBorder(tab.bounds, border, 1, HEADER_RADIUS); context .drawText(tab.title, tab.bounds.x() + HEADER_PAD_X, tab.bounds.y() + HEADER_PAD_Y, tab.style(selected)); } - tabs.get(selectedIndex).body.render(context); + tabs.get(safeSelectedIndex).body.render(context); + } + + @Override + public @Nullable LytNode pickNode(int x, int y) { + if (!bounds.contains(x, y)) { + return null; + } + for (TabState tab : tabs) { + if (tab.bounds.contains(x, y)) { + return this; + } + } + if (!tabs.isEmpty()) { + LytNode activeNode = tabs.get(getSafeSelectedIndex()).body.pickNode(x, y); + if (activeNode != null) { + return activeNode; + } + } + return this; } @Override @@ -160,7 +224,7 @@ public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolea } } } - LytBlock activeBody = tabs.get(selectedIndex).body; + LytBlock activeBody = tabs.get(getSafeSelectedIndex()).body; return activeBody instanceof InteractiveElement interactive && interactive.mouseClicked(screen, x, y, button, doubleClick); } @@ -170,10 +234,17 @@ public Optional getTooltip(float x, float y) { if (tabs.isEmpty()) { return Optional.empty(); } - LytBlock activeBody = tabs.get(selectedIndex).body; + LytBlock activeBody = tabs.get(getSafeSelectedIndex()).body; return activeBody instanceof InteractiveElement interactive ? interactive.getTooltip(x, y) : Optional.empty(); } + private int getSafeSelectedIndex() { + if (tabs.isEmpty()) { + return 0; + } + return Math.max(0, Math.min(selectedIndex, tabs.size() - 1)); + } + private static class TabState { private final String title; @@ -186,7 +257,7 @@ private TabState(String title, LytBlock body) { } private int measureWidth(LayoutContext context) { - return context.getWidth(title, style(false)) + HEADER_PAD_X * 2; + return context.getStringWidth(title, style(false)) + HEADER_PAD_X * 2; } private int measureHeight(LayoutContext context) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index aad34b50..0262c7d9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -128,7 +128,8 @@ public static void initialize() { register( "ContentTabs", new AttributeSpec("default", AttrType.STRING), - new AttributeSpec("defaultIndex", AttrType.INT)); + new AttributeSpec("defaultIndex", AttrType.INT), + new AttributeSpec("color", AttrType.COLOR)); register("Tab", new AttributeSpec("title", AttrType.STRING)); register( "Row", diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java b/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java index ff1e1045..fc10c43f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/extensions/DefaultExtensions.java @@ -18,6 +18,7 @@ import com.hfstudio.guidenh.guide.compiler.tags.ColorTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CommandLinkCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CommentTagCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.ContentTabsTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.compiler.tags.DelUWaveMarkCompiler; import com.hfstudio.guidenh.guide.compiler.tags.DetailsTagCompiler; diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java index 76148f99..aad97361 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java @@ -7,4 +7,17 @@ public interface FontMetrics { float getAdvance(int codePoint, ResolvedTextStyle style); int getLineHeight(ResolvedTextStyle style); + + default int getStringWidth(String text, ResolvedTextStyle style) { + if (text == null || text.isEmpty()) { + return 0; + } + float width = 0f; + for (int index = 0; index < text.length();) { + int codePoint = text.codePointAt(index); + width += getAdvance(codePoint, style); + index += Character.charCount(codePoint); + } + return Math.round(width); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index 1f2d3a26..0a63682f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -1082,7 +1082,13 @@ private String renderContentTabs(MdxJsxElementFields element, String defaultName return html.toString(); } String groupName = "guide-content-tabs-" + contentTabsSequence.getAndIncrement(); - html.append("
    "); + html.append("
    "); for (int index = 0; index < parsed.tabs.size(); index++) { ParsedTab tab = parsed.tabs.get(index); boolean checked = index == parsed.selectedIndex; @@ -1115,6 +1121,14 @@ private ParsedTabs parseContentTabs(MdxJsxElementFields element) { List tabs = new ArrayList<>(); Integer defaultIndex = readPositiveOrZeroInt(readOptional(element, "defaultIndex")); String defaultTitle = readOptional(element, "default"); + String accentCssColor = null; + String rawColor = readOptional(element, "color"); + if (rawColor != null) { + accentCssColor = parseLiteralColor(rawColor); + if (accentCssColor == null) { + errors.add("Malformed color value"); + } + } for (MdAstAnyContent child : element.children()) { if (!(child instanceof MdxJsxFlowElement flowElement) || !"Tab".equals(flowElement.name())) { errors.add("ContentTabs only accepts children."); @@ -1129,7 +1143,7 @@ private ParsedTabs parseContentTabs(MdxJsxElementFields element) { tabs.add(new ParsedTab(title.trim(), flowElement.children())); } int selectedIndex = resolveSelectedIndex(defaultTitle, defaultIndex, tabs, errors); - return new ParsedTabs(tabs, selectedIndex, errors); + return new ParsedTabs(tabs, selectedIndex, errors, accentCssColor); } private int resolveSelectedIndex(@Nullable String defaultTitle, @Nullable Integer defaultIndex, @@ -3013,11 +3027,15 @@ private static class ParsedTabs { private final List tabs; private final int selectedIndex; private final List errors; + @Nullable + private final String accentCssColor; - private ParsedTabs(List tabs, int selectedIndex, List errors) { + private ParsedTabs(List tabs, int selectedIndex, List errors, + @Nullable String accentCssColor) { this.tabs = tabs; this.selectedIndex = selectedIndex; this.errors = errors; + this.accentCssColor = accentCssColor; } } diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index 78fa3ead..4dfbf1f2 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -1752,13 +1752,13 @@ p { } .guide-content-tabs { + --guide-content-tabs-accent: rgba(124, 135, 149, 1); display: flex; flex-wrap: wrap; gap: 6px; margin: 1rem 0; - padding: 0.75rem; - border: 1px solid #3d4656; - border-radius: 12px; + padding: 0.45rem 0.7rem 0.7rem 0.7rem; + border-left: calc(2px * var(--gui-scale)) solid var(--guide-content-tabs-accent); background: rgba(64, 64, 64, 0.25); } @@ -1771,12 +1771,12 @@ p { .guide-content-tabs-label { display: inline-flex; align-items: center; - min-height: 2.25rem; - padding: 0.45rem 0.8rem; - border: 1px solid #586275; + min-height: 1.9rem; + padding: 0.3rem 0.65rem; + border: 1px solid rgba(88, 98, 117, 0.75); border-radius: 999px; - background: #242b36; - color: #d8deea; + background: rgba(26, 31, 38, 0.45); + color: #d5dce7; cursor: pointer; } @@ -1796,9 +1796,9 @@ p { } .guide-content-tabs-input:checked + .guide-content-tabs-label { - border-color: #8fc7ff; - background: #2a5278; - color: #ffffff; + border-color: var(--guide-content-tabs-accent); + background: rgba(62, 75, 89, 0.64); + color: #f4f7fb; } .guide-content-tabs-input:checked + .guide-content-tabs-label + .guide-content-tabs-panel { diff --git a/wiki/Tags-Reference-zh-CN.md b/wiki/Tags-Reference-zh-CN.md index 726ff503..7dca5d4e 100644 --- a/wiki/Tags-Reference-zh-CN.md +++ b/wiki/Tags-Reference-zh-CN.md @@ -48,7 +48,7 @@ | 标签 | 用途 | 关键属性 | | --- | --- | --- | | `
    ` | 透传块包装器 | 无 | -| `` | 将可替代的富内容分组到独立标签页中 | `default`、`defaultIndex` | +| `` | 将可替代的富内容分组到独立标签页中 | `default`、`defaultIndex`、`color` | | `` | `` 内的单个内容面板 | `title` | | `
    ` | 可折叠运行时块 | `open`、`width`、`height`、`wrap`、`align` | | `` | 目录树式大纲(带连接线) | `indent`、`gap` | @@ -161,6 +161,7 @@ Water is H2O and x2 is a square. - `default` 会匹配第一个 `title` 完全相同的标签页 - `defaultIndex` 使用从 `0` 开始的下标,并且在同时出现时优先级高于 `default` +- `color` 可选,用 `#RRGGBB` 或 `#AARRGGBB` 覆盖左侧强调线与选中标签的高亮颜色 - `title` 仅支持纯文本 - 非法子节点或非法默认值会渲染为面向作者的可见错误 diff --git a/wiki/Tags-Reference.md b/wiki/Tags-Reference.md index 4558212c..f1ad0e8d 100644 --- a/wiki/Tags-Reference.md +++ b/wiki/Tags-Reference.md @@ -46,7 +46,7 @@ Inline markdown also supports action links for sound playback: | Tag | Purpose | Key attributes | | --- | --- | --- | | `
    ` | pass-through block wrapper | none | -| `` | groups alternative rich content under independent tabs | `default`, `defaultIndex` | +| `` | groups alternative rich content under independent tabs | `default`, `defaultIndex`, `color` | | `` | one content panel inside `` | `title` | | `
    ` | collapsible runtime block | `open`, `width`, `height`, `wrap`, `align` | | `` | directory-style outline with connector lines | `indent`, `gap` | @@ -161,6 +161,7 @@ Groups alternative rich content under independent tabs. Only direct `` chil - `default` matches the first tab whose `title` matches exactly - `defaultIndex` is zero-based and wins over `default` when both are present +- `color` optionally overrides the left accent line and selected-tab highlight with `#RRGGBB` or `#AARRGGBB` - `title` is plain text only - invalid children or invalid defaults render visible author-facing errors diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md index 645014d6..9e683cf1 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md @@ -298,7 +298,8 @@ Text outside the block should still wrap around it when `wrap="square"` is used. ## Content Tabs `` groups alternative rich content under one tab strip. Each child must be a direct -``, and the selected tab can be chosen with either `default` or `defaultIndex`. +``, the selected tab can be chosen with either `default` or `defaultIndex`, and +`color` can override the quote-like accent line/highlight. diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md index 95843c8f..3fff6340 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md @@ -287,7 +287,8 @@ Markdown: ## 内容标签页 `` 可以把可替代的富内容放进同一组标签页中。每个子节点都必须是直接的 -``,默认选中项可以通过 `default` 或 `defaultIndex` 指定。 +``,默认选中项可以通过 `default` 或 `defaultIndex` 指定,`color` +还可以覆盖类似引用块的强调线与高亮颜色。 From 5cd74e95845b97e19f1b3f3b2458790699421c04 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:32:46 +0800 Subject: [PATCH 122/136] change content tabs block --- .../document/block/LytContentTabsBlock.java | 52 ++++++++++++------- .../assets/guidenh/siteexport/app.css | 25 +++++---- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java index 8f4f4bb9..448cf43a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java @@ -27,15 +27,16 @@ public class LytContentTabsBlock extends LytBlock implements InteractiveElement private static final int ACCENT_WIDTH = 3; private static final int CONTAINER_PAD_X = 10; private static final int CONTAINER_PAD_Y = 6; - private static final int HEADER_GAP = 6; - private static final int HEADER_PAD_X = 7; - private static final int HEADER_PAD_Y = 3; - private static final int HEADER_RADIUS = 4; + private static final int HEADER_GAP_X = 10; + private static final int HEADER_GAP_Y = 5; + private static final int HEADER_PAD_X = 2; + private static final int HEADER_PAD_TOP = 1; + private static final int HEADER_PAD_BOTTOM = 5; + private static final int HEADER_RULE_THICKNESS = 1; + private static final int ACTIVE_RULE_THICKNESS = 2; private static final int BODY_GAP = 6; private static final ConstantColor DEFAULT_ACCENT = new ConstantColor(0xFF7C8795); - private static final int SELECTED_FILL = 0x403E4B59; - private static final int IDLE_FILL = 0x241A1F26; - private static final int IDLE_BORDER = 0x66586275; + private static final int HEADER_RULE_COLOR = 0x66586275; private static final ResolvedTextStyle SELECTED_STYLE = new ResolvedTextStyle( 1.0f, false, @@ -106,7 +107,7 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab return new LytRect(x, y, 0, 0); } - selectedIndex = Math.max(0, Math.min(selectedIndex, tabs.size() - 1)); + selectedIndex = Math.clamp(selectedIndex, 0, tabs.size() - 1); int contentX = x + ACCENT_WIDTH + CONTAINER_PAD_X; int contentY = y + CONTAINER_PAD_Y; @@ -114,7 +115,6 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab int cursorX = contentX; int cursorY = contentY; int rowHeight = 0; - int maxRight = contentX; int headerBottom = contentY; for (TabState tab : tabs) { @@ -122,20 +122,19 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab int tabHeight = tab.measureHeight(context); if (cursorX > contentX && cursorX + tabWidth > contentX + contentWidth) { cursorX = contentX; - cursorY += rowHeight + HEADER_GAP; + cursorY += rowHeight + HEADER_GAP_Y; rowHeight = 0; } tab.bounds = new LytRect(cursorX, cursorY, tabWidth, tabHeight); - cursorX += tabWidth + HEADER_GAP; + cursorX += tabWidth + HEADER_GAP_X; rowHeight = Math.max(rowHeight, tabHeight); - maxRight = Math.max(maxRight, tab.bounds.right()); headerBottom = Math.max(headerBottom, tab.bounds.bottom()); } headerBounds = new LytRect( contentX, contentY, - Math.max(0, maxRight - contentX), + contentWidth, Math.max(0, headerBottom - contentY)); int safeSelectedIndex = getSafeSelectedIndex(); LytBlock activeBody = tabs.get(safeSelectedIndex).body; @@ -175,14 +174,29 @@ public void render(RenderContext context) { int accentArgb = context.resolveColor(accentColor); context.fillRect(bounds, context.resolveColor(SymbolicColor.BLOCKQUOTE_BACKGROUND)); context.fillRect(bounds.x(), bounds.y(), ACCENT_WIDTH, bounds.height(), accentArgb); + float panelRuleY = headerBounds.bottom() + HEADER_RULE_THICKNESS * 0.5f; + context.drawLine( + headerBounds.x(), + panelRuleY, + headerBounds.right(), + panelRuleY, + HEADER_RULE_THICKNESS, + HEADER_RULE_COLOR); for (int index = 0; index < tabs.size(); index++) { TabState tab = tabs.get(index); boolean selected = index == safeSelectedIndex; - int border = selected ? accentArgb : IDLE_BORDER; - context.fillRoundedRect(tab.bounds, selected ? SELECTED_FILL : IDLE_FILL, HEADER_RADIUS); - context.drawRoundedBorder(tab.bounds, border, 1, HEADER_RADIUS); context - .drawText(tab.title, tab.bounds.x() + HEADER_PAD_X, tab.bounds.y() + HEADER_PAD_Y, tab.style(selected)); + .drawText(tab.title, tab.bounds.x() + HEADER_PAD_X, tab.bounds.y() + HEADER_PAD_TOP, tab.style(selected)); + if (selected) { + float activeRuleY = tab.bounds.bottom() - ACTIVE_RULE_THICKNESS * 0.5f; + context.drawLine( + tab.bounds.x(), + activeRuleY, + tab.bounds.right(), + activeRuleY, + ACTIVE_RULE_THICKNESS, + accentArgb); + } } tabs.get(safeSelectedIndex).body.render(context); } @@ -242,7 +256,7 @@ private int getSafeSelectedIndex() { if (tabs.isEmpty()) { return 0; } - return Math.max(0, Math.min(selectedIndex, tabs.size() - 1)); + return Math.clamp(selectedIndex, 0, tabs.size() - 1); } private static class TabState { @@ -261,7 +275,7 @@ private int measureWidth(LayoutContext context) { } private int measureHeight(LayoutContext context) { - return context.getLineHeight(style(false)) + HEADER_PAD_Y * 2; + return context.getLineHeight(style(false)) + HEADER_PAD_TOP + HEADER_PAD_BOTTOM; } private ResolvedTextStyle style(boolean selected) { diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index 4dfbf1f2..c67377a8 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -1755,7 +1755,9 @@ p { --guide-content-tabs-accent: rgba(124, 135, 149, 1); display: flex; flex-wrap: wrap; - gap: 6px; + align-items: flex-end; + column-gap: 10px; + row-gap: 5px; margin: 1rem 0; padding: 0.45rem 0.7rem 0.7rem 0.7rem; border-left: calc(2px * var(--gui-scale)) solid var(--guide-content-tabs-accent); @@ -1771,20 +1773,24 @@ p { .guide-content-tabs-label { display: inline-flex; align-items: center; - min-height: 1.9rem; - padding: 0.3rem 0.65rem; - border: 1px solid rgba(88, 98, 117, 0.75); - border-radius: 999px; - background: rgba(26, 31, 38, 0.45); + position: relative; + order: 0; + min-height: 1.6rem; + padding: 0.05rem 0.15rem 0.35rem; + border-bottom: 2px solid transparent; color: #d5dce7; cursor: pointer; + font-family: var(--minecraft-font); + font-weight: 700; + letter-spacing: 0.01em; } .guide-content-tabs-panel { display: none; - order: 999; + order: 1; width: 100%; - padding-top: 0.5rem; + padding-top: 0.55rem; + border-top: 1px solid rgba(88, 98, 117, 0.75); } .guide-content-tabs-panel > :first-child { @@ -1796,9 +1802,8 @@ p { } .guide-content-tabs-input:checked + .guide-content-tabs-label { - border-color: var(--guide-content-tabs-accent); - background: rgba(62, 75, 89, 0.64); color: #f4f7fb; + border-bottom-color: var(--guide-content-tabs-accent); } .guide-content-tabs-input:checked + .guide-content-tabs-label + .guide-content-tabs-panel { From 27c6ab338d9711a6e79773ed2e4fc5a291d7d9dc Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 01:15:24 +0800 Subject: [PATCH 123/136] add content tab title --- .../compiler/tags/BlockquoteCompiler.java | 16 +--- .../compiler/tags/CalloutIconSupport.java | 66 +++++++++++++ .../guide/compiler/tags/ContentTabsSpec.java | 6 +- .../compiler/tags/ContentTabsTagCompiler.java | 13 ++- .../document/block/LytContentTabsBlock.java | 96 +++++++++++++++---- .../autocomplete/TagAttributeRegistry.java | 8 +- .../markdown/MarkdownRuntimeBlocks.java | 32 +++++++ .../site/GuideSiteMdxTagRenderer.java | 76 +++++++++++++-- .../assets/guidenh/siteexport/app.css | 30 ++++++ wiki/Tags-Reference-zh-CN.md | 14 ++- wiki/Tags-Reference.md | 14 ++- .../assets/guidenh/guidenh/_en_us/markdown.md | 15 ++- .../assets/guidenh/guidenh/_zh_cn/markdown.md | 14 ++- 13 files changed, 340 insertions(+), 60 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CalloutIconSupport.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java index 6a04283a..c20eb27c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/BlockquoteCompiler.java @@ -3,8 +3,6 @@ import java.util.Collections; import java.util.Set; -import org.jetbrains.annotations.Nullable; - import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytAlertBox; @@ -13,10 +11,8 @@ import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.block.LytQuoteBox; import com.hfstudio.guidenh.guide.document.block.LytVBox; -import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks; import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.BlockquoteDirective; -import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; import com.hfstudio.guidenh.guide.style.BorderStyle; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; @@ -48,7 +44,10 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (directive != null && (directive.title() != null || directive.icon() != null)) { LytQuoteBox quoteBox = new LytQuoteBox(); - quoteBox.setQuoteStyle(directive.accentColor(), directive.title(), buildQuoteIcon(directive.icon())); + quoteBox.setQuoteStyle( + directive.accentColor(), + directive.title(), + CalloutIconSupport.buildFlowIcon(compiler, directive.icon())); quoteBox.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); quoteBox.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); compileDirectiveBody(compiler, directive, quoteBox); @@ -119,13 +118,6 @@ private void shiftFirstParagraphDown(LytNode box, int pixels) { } } - @Nullable - private LytFlowContent buildQuoteIcon(@Nullable QuoteIconSpec icon) { - // The original buildQuoteIcon resolved item stacks from icon specs. - // For now return null — icon rendering will be added in a later phase. - return null; - } - private static void stripLeadingText(MdxJsxFlowElement paragraph, String replacementText) { for (Object child : paragraph.children()) { if (child instanceof MdAstText text && !text.value.trim() diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CalloutIconSupport.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CalloutIconSupport.java new file mode 100644 index 00000000..cd116f74 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CalloutIconSupport.java @@ -0,0 +1,66 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.compiler.IdUtils; +import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.document.block.LytImage; +import com.hfstudio.guidenh.guide.document.block.LytItemImage; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; +import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; +import com.hfstudio.guidenh.guide.document.flow.LytFlowText; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; + +public class CalloutIconSupport { + + private static final int INLINE_PNG_SIZE = 8; + private static final float INLINE_ITEM_SCALE = 0.75f; + + private CalloutIconSupport() {} + + public static @Nullable LytFlowContent buildFlowIcon(PageCompiler compiler, @Nullable QuoteIconSpec icon) { + if (icon == null || icon.value() == null + || icon.value() + .trim() + .isEmpty()) { + return null; + } + String value = icon.value() + .trim(); + return switch (icon.kind()) { + case TEXT -> LytFlowText.of(value); + case PNG -> buildPngIcon(compiler, value); + case ITEM -> buildItemIcon(compiler, value); + }; + } + + private static LytFlowContent buildPngIcon(PageCompiler compiler, String value) { + LytImage image = new LytImage(); + image.setExplicitWidth(INLINE_PNG_SIZE); + image.setExplicitHeight(INLINE_PNG_SIZE); + try { + ResourceLocation imageId = IdUtils.resolveLink(value, compiler.getPageId()); + image.setImage(imageId, compiler.loadAsset(imageId)); + } catch (IllegalArgumentException ignored) { + image.setTitle("Invalid image: " + value); + } + return LytFlowInlineBlock.of(image); + } + + private static LytFlowContent buildItemIcon(PageCompiler compiler, String value) { + ItemStack stack = IdUtils.resolveItemStack( + value, + compiler.getPageId() + .getResourceDomain()); + if (stack == null) { + return LytFlowText.of(value); + } + LytItemImage image = new LytItemImage(stack); + image.setInline(true); + image.setScale(INLINE_ITEM_SCALE); + return LytFlowInlineBlock.of(image); + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java index 56b7bc75..b48fd351 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsSpec.java @@ -7,11 +7,13 @@ import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.document.block.LytBlock; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; import com.hfstudio.guidenh.libs.unist.UnistNode; @Desugar -public record ContentTabsSpec(@Nullable String defaultTitle, @Nullable Integer defaultIndex, - @Nullable ColorValue accentColor, List tabs, List issues) { +public record ContentTabsSpec(@Nullable String title, @Nullable QuoteIconSpec icon, @Nullable String defaultTitle, + @Nullable Integer defaultIndex, @Nullable ColorValue accentColor, List tabs, + List issues) { public boolean hasRenderableTabs() { return !tabs.isEmpty(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java index 6f0b6ed7..c814ebfa 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ContentTabsTagCompiler.java @@ -13,6 +13,7 @@ import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytContentTabsBlock; import com.hfstudio.guidenh.guide.document.block.LytVBox; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; @@ -34,19 +35,27 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (!spec.hasRenderableTabs()) { return; } - parent.append(new LytContentTabsBlock(resolveInitialIndex(spec), spec.accentColor(), spec.tabs())); + parent.append( + new LytContentTabsBlock( + spec.title(), + CalloutIconSupport.buildFlowIcon(compiler, spec.icon()), + resolveInitialIndex(spec), + spec.accentColor(), + spec.tabs())); } private ContentTabsSpec parseSpec(PageCompiler compiler, MdxJsxElementFields el) { List children = resolveChildren(compiler, el); List tabs = new ArrayList<>(); List issues = new ArrayList<>(); + String title = el.getAttributeString("title", null); + var icon = MarkdownRuntimeBlocks.parseQuoteIconAttributes(el); Integer defaultIndex = parseDefaultIndex(el, issues, el); String defaultTitle = el.getAttributeString("default", null); ColorValue accentColor = MdxAttrs.getColor(compiler, new ValidationIssueSink(issues), el, "color", null); collectTabs(compiler, children, tabs, issues); validateDefaultTarget(defaultTitle, defaultIndex, tabs, issues, el); - return new ContentTabsSpec(defaultTitle, defaultIndex, accentColor, tabs, issues); + return new ContentTabsSpec(title, icon, defaultTitle, defaultIndex, accentColor, tabs, issues); } private List resolveChildren(PageCompiler compiler, MdxJsxElementFields el) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java index 448cf43a..6f0ab6ee 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java @@ -12,6 +12,7 @@ import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.ContentTabsSpec; import com.hfstudio.guidenh.guide.document.LytRect; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.interaction.GuideTooltip; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; import com.hfstudio.guidenh.guide.layout.LayoutContext; @@ -34,9 +35,19 @@ public class LytContentTabsBlock extends LytBlock implements InteractiveElement private static final int HEADER_PAD_BOTTOM = 5; private static final int HEADER_RULE_THICKNESS = 1; private static final int ACTIVE_RULE_THICKNESS = 2; + private static final int TITLE_GAP = 4; private static final int BODY_GAP = 6; private static final ConstantColor DEFAULT_ACCENT = new ConstantColor(0xFF7C8795); private static final int HEADER_RULE_COLOR = 0x66586275; + private final List tabs = new ArrayList<>(); + private final List children = new ArrayList<>(); + private final ColorValue accentColor; + @Nullable + private final LytParagraph titleParagraph; + private int selectedIndex; + private LytRect titleBounds = LytRect.empty(); + private LytRect headerBounds = LytRect.empty(); + private LytRect contentBounds = LytRect.empty(); private static final ResolvedTextStyle SELECTED_STYLE = new ResolvedTextStyle( 1.0f, false, @@ -70,20 +81,18 @@ public class LytContentTabsBlock extends LytBlock implements InteractiveElement null, false); - private final List tabs = new ArrayList<>(); - private final List tabBodies = new ArrayList<>(); - private final ColorValue accentColor; - private int selectedIndex; - private LytRect headerBounds = LytRect.empty(); - private LytRect contentBounds = LytRect.empty(); - - public LytContentTabsBlock(int selectedIndex, @Nullable ColorValue accentColor, - List entries) { + public LytContentTabsBlock(@Nullable String title, @Nullable LytFlowContent icon, int selectedIndex, + @Nullable ColorValue accentColor, List entries) { this.accentColor = accentColor != null ? accentColor : DEFAULT_ACCENT; this.selectedIndex = Math.max(0, selectedIndex); + this.titleParagraph = buildTitleParagraph(title, icon); + if (titleParagraph != null) { + titleParagraph.parent = this; + children.add(titleParagraph); + } for (ContentTabsSpec.TabEntry entry : entries) { tabs.add(new TabState(entry.title(), entry.body())); - tabBodies.add(entry.body()); + children.add(entry.body()); entry.body().parent = this; } setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); @@ -96,12 +105,13 @@ public LytContentTabsBlock(int selectedIndex, @Nullable ColorValue accentColor, public List getChildren() { // Expose every tab body to tree visitors so search, anchors, resource export, // scene collection, and mount-time traversal still see hidden tabs. - return tabBodies; + return children; } @Override protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { if (tabs.isEmpty()) { + titleBounds = LytRect.empty(); headerBounds = LytRect.empty(); contentBounds = LytRect.empty(); return new LytRect(x, y, 0, 0); @@ -112,10 +122,17 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab int contentX = x + ACCENT_WIDTH + CONTAINER_PAD_X; int contentY = y + CONTAINER_PAD_Y; int contentWidth = Math.max(0, availableWidth - ACCENT_WIDTH - CONTAINER_PAD_X * 2); + int tabsY = contentY; + if (titleParagraph != null) { + titleBounds = titleParagraph.layout(context, contentX, contentY, contentWidth); + tabsY = titleBounds.bottom() + TITLE_GAP; + } else { + titleBounds = LytRect.empty(); + } int cursorX = contentX; - int cursorY = contentY; + int cursorY = tabsY; int rowHeight = 0; - int headerBottom = contentY; + int headerBottom = tabsY; for (TabState tab : tabs) { int tabWidth = tab.measureWidth(context); @@ -131,15 +148,11 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab headerBottom = Math.max(headerBottom, tab.bounds.bottom()); } - headerBounds = new LytRect( - contentX, - contentY, - contentWidth, - Math.max(0, headerBottom - contentY)); + headerBounds = new LytRect(contentX, tabsY, contentWidth, Math.max(0, headerBottom - tabsY)); int safeSelectedIndex = getSafeSelectedIndex(); LytBlock activeBody = tabs.get(safeSelectedIndex).body; LytRect bodyBounds = activeBody.layout(context, contentX, headerBounds.bottom() + BODY_GAP, contentWidth); - int contentRight = Math.max(headerBounds.right(), bodyBounds.right()); + int contentRight = Math.max(Math.max(titleBounds.right(), headerBounds.right()), bodyBounds.right()); int contentBottom = Math.max(headerBounds.bottom(), bodyBounds.bottom()); contentBounds = new LytRect( contentX, @@ -155,8 +168,12 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab @Override protected void onLayoutMoved(int deltaX, int deltaY) { + titleBounds = titleBounds.move(deltaX, deltaY); headerBounds = headerBounds.move(deltaX, deltaY); contentBounds = contentBounds.move(deltaX, deltaY); + if (titleParagraph != null) { + titleParagraph.moveLayoutPos(deltaX, deltaY); + } for (TabState tab : tabs) { tab.bounds = tab.bounds.move(deltaX, deltaY); } @@ -174,6 +191,9 @@ public void render(RenderContext context) { int accentArgb = context.resolveColor(accentColor); context.fillRect(bounds, context.resolveColor(SymbolicColor.BLOCKQUOTE_BACKGROUND)); context.fillRect(bounds.x(), bounds.y(), ACCENT_WIDTH, bounds.height(), accentArgb); + if (titleParagraph != null) { + titleParagraph.render(context); + } float panelRuleY = headerBounds.bottom() + HEADER_RULE_THICKNESS * 0.5f; context.drawLine( headerBounds.x(), @@ -185,8 +205,11 @@ public void render(RenderContext context) { for (int index = 0; index < tabs.size(); index++) { TabState tab = tabs.get(index); boolean selected = index == safeSelectedIndex; - context - .drawText(tab.title, tab.bounds.x() + HEADER_PAD_X, tab.bounds.y() + HEADER_PAD_TOP, tab.style(selected)); + context.drawText( + tab.title, + tab.bounds.x() + HEADER_PAD_X, + tab.bounds.y() + HEADER_PAD_TOP, + tab.style(selected)); if (selected) { float activeRuleY = tab.bounds.bottom() - ACTIVE_RULE_THICKNESS * 0.5f; context.drawLine( @@ -206,6 +229,12 @@ public void render(RenderContext context) { if (!bounds.contains(x, y)) { return null; } + if (titleParagraph != null) { + LytNode titleNode = titleParagraph.pickNode(x, y); + if (titleNode != null) { + return titleNode; + } + } for (TabState tab : tabs) { if (tab.bounds.contains(x, y)) { return this; @@ -252,6 +281,31 @@ public Optional getTooltip(float x, float y) { return activeBody instanceof InteractiveElement interactive ? interactive.getTooltip(x, y) : Optional.empty(); } + @Nullable + private LytParagraph buildTitleParagraph(@Nullable String title, @Nullable LytFlowContent icon) { + boolean hasTitle = title != null && !title.trim() + .isEmpty(); + if (!hasTitle && icon == null) { + return null; + } + LytParagraph paragraph = new LytParagraph(); + paragraph.setMarginTop(0); + paragraph.setMarginBottom(0); + paragraph.modifyStyle( + style -> style.bold(true) + .color(accentColor)); + if (icon != null) { + paragraph.append(icon); + if (hasTitle) { + paragraph.appendText(" "); + } + } + if (hasTitle) { + paragraph.appendText(title.trim()); + } + return paragraph; + } + private int getSafeSelectedIndex() { if (tabs.isEmpty()) { return 0; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 0262c7d9..6b8e73a9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -127,9 +127,15 @@ public static void initialize() { register("details", new AttributeSpec("open", AttrType.BOOLEAN)); register( "ContentTabs", + new AttributeSpec("title", AttrType.STRING), new AttributeSpec("default", AttrType.STRING), new AttributeSpec("defaultIndex", AttrType.INT), - new AttributeSpec("color", AttrType.COLOR)); + new AttributeSpec("color", AttrType.COLOR), + new AttributeSpec("icon", AttrType.STRING), + new AttributeSpec("iconPng", AttrType.FILE_PATH), + new AttributeSpec("icon_png", AttrType.FILE_PATH), + new AttributeSpec("iconItem", AttrType.ITEM_ID), + new AttributeSpec("icon_item", AttrType.ITEM_ID)); register("Tab", new AttributeSpec("title", AttrType.STRING)); register( "Row", diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java index 8ac4aec8..e731552d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/markdown/MarkdownRuntimeBlocks.java @@ -25,6 +25,26 @@ private MarkdownRuntimeBlocks() {} return new GithubAlertBlock(directive.alertType(), directive.children(), directive.remainingText()); } + public static @Nullable QuoteIconSpec parseQuoteIconAttributes(MdxJsxElementFields element) { + String item = firstNonBlank( + element.getAttributeString("iconItem", null), + element.getAttributeString("icon_item", null)); + if (item != null) { + return new QuoteIconSpec(QuoteIconKind.ITEM, item); + } + String png = firstNonBlank( + element.getAttributeString("iconPng", null), + element.getAttributeString("icon_png", null)); + if (png != null) { + return new QuoteIconSpec(QuoteIconKind.PNG, png); + } + String text = firstNonBlank(element.getAttributeString("icon", null)); + if (text != null) { + return new QuoteIconSpec(QuoteIconKind.TEXT, text); + } + return null; + } + public static @Nullable BlockquoteDirective parseBlockquoteDirective(MdxJsxElementFields blockquote) { FirstParagraphText firstParagraph = findFirstParagraphText(blockquote); if (firstParagraph == null) { @@ -218,6 +238,18 @@ private static ColorValue parseColor(String value) { return null; } + private static @Nullable String firstNonBlank(@Nullable String... values) { + for (String value : values) { + if (value != null) { + String trimmed = value.trim(); + if (!trimmed.isEmpty()) { + return trimmed; + } + } + } + return null; + } + @Desugar public record BlockquoteDirective(@Nullable GithubAlertType alertType, ColorValue accentColor, @Nullable String title, @Nullable QuoteIconSpec icon, String remainingText, diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index 0a63682f..37c5e061 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -55,6 +55,8 @@ import com.hfstudio.guidenh.guide.internal.markdown.FileTreeParser.FileTreeIcon; import com.hfstudio.guidenh.guide.internal.markdown.FileTreeParser.FileTreeModel; import com.hfstudio.guidenh.guide.internal.markdown.FileTreeParser.SlotKind; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks; +import com.hfstudio.guidenh.guide.internal.markdown.MarkdownRuntimeBlocks.QuoteIconSpec; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapDocument; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNode; import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeContentExtractor; @@ -1089,6 +1091,20 @@ private String renderContentTabs(MdxJsxElementFields element, String defaultName .append(";\""); } html.append(">"); + if (parsed.title != null || parsed.icon != null) { + html.append("
    "); + if (parsed.icon != null) { + html.append( + renderContentTabsIcon(parsed.icon, templates, defaultNamespace, currentPageId, sceneResolver)); + if (parsed.title != null) { + html.append(' '); + } + } + if (parsed.title != null) { + html.append(escapeHtml(parsed.title)); + } + html.append("
    "); + } for (int index = 0; index < parsed.tabs.size(); index++) { ParsedTab tab = parsed.tabs.get(index); boolean checked = index == parsed.selectedIndex; @@ -1119,6 +1135,8 @@ private String renderContentTabs(MdxJsxElementFields element, String defaultName private ParsedTabs parseContentTabs(MdxJsxElementFields element) { List errors = new ArrayList<>(); List tabs = new ArrayList<>(); + String containerTitle = readOptional(element, "title"); + QuoteIconSpec icon = MarkdownRuntimeBlocks.parseQuoteIconAttributes(element); Integer defaultIndex = readPositiveOrZeroInt(readOptional(element, "defaultIndex")); String defaultTitle = readOptional(element, "default"); String accentCssColor = null; @@ -1134,16 +1152,56 @@ private ParsedTabs parseContentTabs(MdxJsxElementFields element) { errors.add("ContentTabs only accepts children."); continue; } - String title = flowElement.getAttributeString("title", null); - if (title == null || title.trim() + String tabTitle = flowElement.getAttributeString("title", null); + if (tabTitle == null || tabTitle.trim() .isEmpty()) { errors.add(" requires a non-empty title attribute."); continue; } - tabs.add(new ParsedTab(title.trim(), flowElement.children())); + tabs.add(new ParsedTab(tabTitle.trim(), flowElement.children())); } int selectedIndex = resolveSelectedIndex(defaultTitle, defaultIndex, tabs, errors); - return new ParsedTabs(tabs, selectedIndex, errors, accentCssColor); + return new ParsedTabs( + containerTitle != null && !containerTitle.trim() + .isEmpty() ? containerTitle.trim() : null, + icon, + tabs, + selectedIndex, + errors, + accentCssColor); + } + + private String renderContentTabsIcon(QuoteIconSpec icon, GuideSiteTemplateRegistry templates, + String defaultNamespace, @Nullable ResourceLocation currentPageId, + GuideSiteHtmlCompiler.SceneResolver sceneResolver) { + String value = icon.value(); + if (value == null || value.trim() + .isEmpty()) { + return ""; + } + return switch (icon.kind()) { + case PNG -> { + String resolved = assetExporter != null ? assetExporter.resolveImageSrc(value, currentPageId) : value; + if (resolved == null || resolved.isEmpty()) { + yield "" + escapeHtml(value) + ""; + } + yield "\"\""; + } + case ITEM -> { + List attrs = new ArrayList<>(); + attrs.add(new MdxJsxAttribute("id", value)); + MdxJsxFlowElement synthetic = new MdxJsxFlowElement("ItemImage", attrs); + String rendered = renderItemImage(synthetic, defaultNamespace, currentPageId, templates, true); + if (rendered == null || rendered.isEmpty()) { + yield "" + escapeHtml(value) + ""; + } + yield "" + rendered + + ""; + } + case TEXT -> "" + escapeHtml(value) + ""; + }; } private int resolveSelectedIndex(@Nullable String defaultTitle, @Nullable Integer defaultIndex, @@ -3024,14 +3082,20 @@ private StructureLegendEntry(GuideSiteExportedItem item, String abbreviation, @N private static class ParsedTabs { + @Nullable + private final String title; + @Nullable + private final QuoteIconSpec icon; private final List tabs; private final int selectedIndex; private final List errors; @Nullable private final String accentCssColor; - private ParsedTabs(List tabs, int selectedIndex, List errors, - @Nullable String accentCssColor) { + private ParsedTabs(@Nullable String title, @Nullable QuoteIconSpec icon, List tabs, + int selectedIndex, List errors, @Nullable String accentCssColor) { + this.title = title; + this.icon = icon; this.tabs = tabs; this.selectedIndex = selectedIndex; this.errors = errors; diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index c67377a8..882fd460 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -1764,6 +1764,36 @@ p { background: rgba(64, 64, 64, 0.25); } +.guide-content-tabs-title { + width: 100%; + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + color: var(--guide-content-tabs-accent); + font-family: var(--minecraft-font); + font-weight: 700; +} + +.guide-content-tabs-title-icon { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.guide-content-tabs-title-icon-image img { + width: calc(8px * var(--gui-scale)); + height: calc(8px * var(--gui-scale)); + image-rendering: pixelated; + vertical-align: middle; +} + +.guide-content-tabs-title-icon-item .guide-inline-item, +.guide-content-tabs-title-icon-item .item-icon { + transform: translateY(calc(-0.5px * var(--gui-scale))); +} + .guide-content-tabs-input { position: absolute; opacity: 0; diff --git a/wiki/Tags-Reference-zh-CN.md b/wiki/Tags-Reference-zh-CN.md index 7dca5d4e..084c51d9 100644 --- a/wiki/Tags-Reference-zh-CN.md +++ b/wiki/Tags-Reference-zh-CN.md @@ -48,7 +48,7 @@ | 标签 | 用途 | 关键属性 | | --- | --- | --- | | `
    ` | 透传块包装器 | 无 | -| `` | 将可替代的富内容分组到独立标签页中 | `default`、`defaultIndex`、`color` | +| `` | 将可替代的富内容分组到独立标签页中 | `title`、`color`、`icon`、`iconPng`、`icon_png`、`iconItem`、`icon_item`、`default`、`defaultIndex` | | `` | `` 内的单个内容面板 | `title` | | `
    ` | 可折叠运行时块 | `open`、`width`、`height`、`wrap`、`align` | | `` | 目录树式大纲(带连接线) | `indent`、`gap` | @@ -144,10 +144,15 @@ Water is H2O and x2 is a square. ### `` -将可替代的富内容分组到独立标签页中。`` 只接受直接的 `` 子标签。 +将可替代的富内容分组到独立标签页中。`` 只接受直接的 `` 子标签。容器本身也可以在标签页上方渲染一个类似 Markdown 标记/引用块的标题行。 ````mdx - + ```java System.out.println("Hello GuideNH"); @@ -162,7 +167,8 @@ Water is H2O and x2 is a square. - `default` 会匹配第一个 `title` 完全相同的标签页 - `defaultIndex` 使用从 `0` 开始的下标,并且在同时出现时优先级高于 `default` - `color` 可选,用 `#RRGGBB` 或 `#AARRGGBB` 覆盖左侧强调线与选中标签的高亮颜色 -- `title` 仅支持纯文本 +- `title` 会在标签栏上方增加一个可选的纯文本标题 +- `icon`、`iconPng` / `icon_png`、`iconItem` / `icon_item` 与 Markdown 标记/引用块标题的图标语义完全一致 - 非法子节点或非法默认值会渲染为面向作者的可见错误 ### `` diff --git a/wiki/Tags-Reference.md b/wiki/Tags-Reference.md index f1ad0e8d..a4fda5c1 100644 --- a/wiki/Tags-Reference.md +++ b/wiki/Tags-Reference.md @@ -46,7 +46,7 @@ Inline markdown also supports action links for sound playback: | Tag | Purpose | Key attributes | | --- | --- | --- | | `
    ` | pass-through block wrapper | none | -| `` | groups alternative rich content under independent tabs | `default`, `defaultIndex`, `color` | +| `` | groups alternative rich content under independent tabs | `title`, `color`, `icon`, `iconPng`, `icon_png`, `iconItem`, `icon_item`, `default`, `defaultIndex` | | `` | one content panel inside `` | `title` | | `
    ` | collapsible runtime block | `open`, `width`, `height`, `wrap`, `align` | | `` | directory-style outline with connector lines | `indent`, `gap` | @@ -144,10 +144,15 @@ Attributes: ### `` -Groups alternative rich content under independent tabs. Only direct `` children are valid. +Groups alternative rich content under independent tabs. Only direct `` children are valid. The container itself can also render a quote-style heading row above the tabs, matching the visual language of markdown callouts. ````mdx - + ```java System.out.println("Hello GuideNH"); @@ -162,7 +167,8 @@ Groups alternative rich content under independent tabs. Only direct `` chil - `default` matches the first tab whose `title` matches exactly - `defaultIndex` is zero-based and wins over `default` when both are present - `color` optionally overrides the left accent line and selected-tab highlight with `#RRGGBB` or `#AARRGGBB` -- `title` is plain text only +- `title` adds an optional plain-text heading above the tab strip +- `icon`, `iconPng` / `icon_png`, and `iconItem` / `icon_item` use the same heading icon semantics as markdown quote-style callouts - invalid children or invalid defaults render visible author-facing errors ### `` diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md index 9e683cf1..64916ade 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md @@ -298,10 +298,17 @@ Text outside the block should still wrap around it when `wrap="square"` is used. ## Content Tabs `` groups alternative rich content under one tab strip. Each child must be a direct -``, the selected tab can be chosen with either `default` or `defaultIndex`, and -`color` can override the quote-like accent line/highlight. - - +``. The selected tab can be chosen with either `default` or `defaultIndex`, +`color` can override the quote-like accent line/highlight, and the container itself can expose +an optional callout-style heading through `title` plus `icon`, `iconPng` / `icon_png`, or +`iconItem` / `icon_item`. + + ```java System.out.println("Hello GuideNH"); diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md index 3fff6340..b4c1f191 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md @@ -287,10 +287,16 @@ Markdown: ## 内容标签页 `` 可以把可替代的富内容放进同一组标签页中。每个子节点都必须是直接的 -``,默认选中项可以通过 `default` 或 `defaultIndex` 指定,`color` -还可以覆盖类似引用块的强调线与高亮颜色。 - - +``。默认选中项可以通过 `default` 或 `defaultIndex` 指定,`color` +可以覆盖类似引用块的强调线与高亮颜色,容器本身还支持通过 `title` 配合 +`icon`、`iconPng` / `icon_png`、`iconItem` / `icon_item` 渲染一个可选的标记式标题行。 + + ```java System.out.println("Hello GuideNH"); From 7cf352fe2f469698995bd36d44df95374d0e7824 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:41:54 +0800 Subject: [PATCH 124/136] opti --- .../GuideSiteSceneTessellatorCapture.java | 500 ++++++++---------- .../MixinTessellatorSceneExportCapture.java | 146 +---- 2 files changed, 246 insertions(+), 400 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java index e5e908de..c29639c8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteSceneTessellatorCapture.java @@ -3,7 +3,6 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -32,36 +31,29 @@ public class GuideSiteSceneTessellatorCapture { - private static final ThreadLocal ACTIVE = new ThreadLocal<>(); + // Scene export capture is only expected to run on the client render thread, so a + // direct active reference is cheaper than paying a ThreadLocal lookup on every vertex. + private static volatile @Nullable GuideSiteSceneTessellatorCapture ACTIVE; private final GuideSiteAssetRegistry assets; private final Matrix4f inverseViewMatrix; private final Matrix4f currentWorldMatrix = new Matrix4f(); + private final Matrix4f modelViewMatrix = new Matrix4f(); private final Matrix3f currentNormalMatrix = new Matrix3f(); + private final VertexDecodeScratch decodeScratch = new VertexDecodeScratch(); private final FloatBuffer modelViewBuffer = BufferUtils.createFloatBuffer(16); - private final Vector3f transformedPosition = new Vector3f(); - private final Vector3f transformedNormal = new Vector3f(); private final List meshes = new ArrayList<>(); private final Map textures = new LinkedHashMap<>(); - private final ArrayList currentVertices = new ArrayList<>(); + private static final int RAW_VERTEX_STRIDE = 8; + private static final byte[] EMPTY_VERTEX_BYTES = new byte[0]; private boolean drawing; private int drawMode; private boolean hasTexture; - private boolean hasColor; private boolean hasBrightness; private boolean hasNormals; - private boolean colorDisabled; - private float normalX; - private float normalY; - private float normalZ; - private float textureU; - private float textureV; - private int color = 0xFFFFFFFF; - private int brightness; - private double xOffset; - private double yOffset; - private double zOffset; + private byte[] currentVertexBytes = EMPTY_VERTEX_BYTES; + private int currentVertexCount; @Nullable private String currentSourceTextureId; @@ -74,20 +66,20 @@ public static void activate(GuideSiteSceneTessellatorCapture capture) { if (capture == null) { throw new IllegalArgumentException("capture"); } - GuideSiteSceneTessellatorCapture previous = ACTIVE.get(); + GuideSiteSceneTessellatorCapture previous = ACTIVE; if (previous != null && previous != capture) { throw new IllegalStateException("Another scene tessellator capture is already active."); } - ACTIVE.set(capture); + ACTIVE = capture; } public static void deactivate() { - ACTIVE.remove(); + ACTIVE = null; } @Nullable public static GuideSiteSceneTessellatorCapture getActive() { - return ACTIVE.get(); + return ACTIVE; } public RecordingResult finish() { @@ -115,26 +107,16 @@ public void startDrawing(int drawMode) { "Scene capture startDrawing called while already drawing (mode={}); discarding previous unclosed batch", drawMode); drawing = false; - currentVertices.clear(); + currentVertexBytes = EMPTY_VERTEX_BYTES; + currentVertexCount = 0; } drawing = true; - currentVertices.clear(); + currentVertexBytes = EMPTY_VERTEX_BYTES; + currentVertexCount = 0; this.drawMode = drawMode; hasTexture = false; - hasColor = false; hasBrightness = false; hasNormals = false; - colorDisabled = false; - textureU = 0.0f; - textureV = 0.0f; - color = 0xFFFFFFFF; - brightness = 0; - normalX = 0.0f; - normalY = 0.0f; - normalZ = 0.0f; - xOffset = 0.0D; - yOffset = 0.0D; - zOffset = 0.0D; captureCurrentWorldTransform(); } @@ -144,7 +126,7 @@ public int draw() { return 0; } drawing = false; - int vertexCount = currentVertices.size(); + int vertexCount = currentVertexCount; try { if (vertexCount > 0) { captureCurrentMesh(); @@ -152,119 +134,34 @@ public int draw() { } catch (Throwable e) { GuideDebugLog.warnAlways("Scene capture mesh export failed ({} vertices)", vertexCount, e); } finally { - currentVertices.clear(); + currentVertexBytes = EMPTY_VERTEX_BYTES; + currentVertexCount = 0; } return vertexCount * 32; } - public void setTextureUV(double u, double v) { - hasTexture = true; - textureU = (float) u; - textureV = (float) v; - } - - public void addVertexWithUV(double x, double y, double z, double u, double v) { - setTextureUV(u, v); - addVertex(x, y, z); - } - - public void setBrightness(int brightness) { - hasBrightness = true; - this.brightness = brightness; - } - - public void setColorOpaque_F(float red, float green, float blue) { - setColorOpaque((int) (red * 255.0F), (int) (green * 255.0F), (int) (blue * 255.0F)); - } - - public void setColorRGBA_F(float red, float green, float blue, float alpha) { - setColorRGBA((int) (red * 255.0F), (int) (green * 255.0F), (int) (blue * 255.0F), (int) (alpha * 255.0F)); - } - - public void setColorOpaque(int red, int green, int blue) { - setColorRGBA(red, green, blue, 255); - } - - public void setColorRGBA(int red, int green, int blue, int alpha) { - if (colorDisabled) { + public void captureRawBuffer(int[] rawBuffer, int vertexCount, boolean hasTexture, boolean hasColor, + boolean hasBrightness, boolean hasNormals) { + this.hasTexture = hasTexture; + this.hasBrightness = hasBrightness; + this.hasNormals = hasNormals; + currentVertexCount = sanitizeVertexCount(rawBuffer, vertexCount); + if (currentVertexCount <= 0) { + currentVertexBytes = EMPTY_VERTEX_BYTES; return; } - hasColor = true; - color = clamp(alpha) << 24 | clamp(blue) << 16 | clamp(green) << 8 | clamp(red); - } - - public void func_154352_a(byte red, byte green, byte blue) { - setColorOpaque(red & 255, green & 255, blue & 255); - } - - public void setColorOpaque_I(int color) { - setColorOpaque(color >> 16 & 255, color >> 8 & 255, color & 255); - } - - public void setColorRGBA_I(int color, int alpha) { - setColorRGBA(color >> 16 & 255, color >> 8 & 255, color & 255, alpha); - } - - public void disableColor() { - colorDisabled = true; - } - - public void setNormal(float x, float y, float z) { - hasNormals = true; - normalX = x; - normalY = y; - normalZ = z; - } - - public void setTranslation(double x, double y, double z) { - xOffset = x; - yOffset = y; - zOffset = z; - } - - public void addTranslation(float x, float y, float z) { - xOffset += x; - yOffset += y; - zOffset += z; - } - - public void addVertex(double x, double y, double z) { - int rgba = hasColor ? color : 0xFFFFFFFF; - float px = (float) (x + xOffset); - float py = (float) (y + yOffset); - float pz = (float) (z + zOffset); - - transformedPosition.set(px, py, pz); - currentWorldMatrix.transformPosition(transformedPosition); - - byte nx = 0; - byte ny = 0; - byte nz = 0; - if (hasNormals) { - transformedNormal.set(normalX, normalY, normalZ); - currentNormalMatrix.transform(transformedNormal); - if (transformedNormal.lengthSquared() > 1.0e-12f) { - transformedNormal.normalize(); - } - nx = packNormalComponent(transformedNormal.x); - ny = packNormalComponent(transformedNormal.y); - nz = packNormalComponent(transformedNormal.z); - } - - currentVertices.add( - new RecordedVertex( - transformedPosition.x, - transformedPosition.y, - transformedPosition.z, - hasTexture ? textureU : 0.0f, - hasTexture ? textureV : 0.0f, - rgba & 255, - rgba >> 8 & 255, - rgba >> 16 & 255, - rgba >> 24 & 255, - nx, - ny, - nz)); + currentVertexBytes = new byte[currentVertexCount * exportVertexStride(hasTexture, hasNormals)]; + appendCapturedVertices( + rawBuffer, + currentVertexCount, + hasTexture, + hasColor, + hasNormals, + currentWorldMatrix, + currentNormalMatrix, + decodeScratch, + currentVertexBytes, + 0); } private void captureCurrentWorldTransform() { @@ -272,7 +169,7 @@ private void captureCurrentWorldTransform() { GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, modelViewBuffer); modelViewBuffer.flip(); - Matrix4f modelViewMatrix = new Matrix4f().set(modelViewBuffer); + modelViewMatrix.set(modelViewBuffer); currentWorldMatrix.set(inverseViewMatrix) .mul(modelViewMatrix); @@ -286,21 +183,19 @@ private void captureCurrentWorldTransform() { } private void captureCurrentMesh() throws Exception { - if (currentVertices.isEmpty()) { + if (currentVertexCount <= 0) { return; } TextureExport texture = hasTexture ? exportCurrentTexture() : null; MaterialKey material = createMaterialKey(texture); VertexFormatKey vertexFormat = new VertexFormatKey(hasTexture, hasNormals); - - ByteBuffer vertexBuffer = buildVertexBuffer(vertexFormat); - IndexData indexData = buildIndexData(); + EncodedIndexData indexData = buildIndexData(currentVertexCount); meshes.add( new CapturedMesh( - toByteArray(vertexBuffer), - toByteArray(indexData.buffer), + currentVertexBytes, + indexData.indexBuffer(), indexData.indexCount, indexData.indexType, indexData.primitiveType, @@ -484,104 +379,180 @@ private int mapDepthTest(boolean depthEnabled) { return ExpDepthTest.LEQUAL; } - private ByteBuffer buildVertexBuffer(VertexFormatKey vertexFormat) { - int stride = 12 + (vertexFormat.hasUv ? 8 : 0) + 4 + (vertexFormat.hasNormal ? 4 : 0); - ByteBuffer buffer = ByteBuffer.allocate(currentVertices.size() * stride) - .order(ByteOrder.LITTLE_ENDIAN); + private EncodedIndexData buildIndexData(int vertexCount) { + return encodeIndexData(drawMode, vertexCount); + } - for (RecordedVertex vertex : currentVertices) { - buffer.putFloat(vertex.x); - buffer.putFloat(vertex.y); - buffer.putFloat(vertex.z); - if (vertexFormat.hasUv) { - buffer.putFloat(vertex.u); - buffer.putFloat(vertex.v); - } - buffer.put((byte) vertex.r); - buffer.put((byte) vertex.g); - buffer.put((byte) vertex.b); - buffer.put((byte) vertex.a); - if (vertexFormat.hasNormal) { - buffer.put(vertex.nx); - buffer.put(vertex.ny); - buffer.put(vertex.nz); - buffer.put((byte) 0); - } + private static int mapPrimitiveType(int drawMode) { + if (drawMode == GL11.GL_LINES) { + return ExpPrimitiveType.LINES; + } + if (drawMode == GL11.GL_LINE_STRIP) { + return ExpPrimitiveType.LINE_STRIP; + } + if (drawMode == GL11.GL_TRIANGLE_STRIP) { + return ExpPrimitiveType.TRIANGLE_STRIP; + } + if (drawMode == GL11.GL_TRIANGLE_FAN) { + return ExpPrimitiveType.TRIANGLE_FAN; + } + if (drawMode == GL11.GL_POINTS) { + return ExpPrimitiveType.POINTS; } + return ExpPrimitiveType.TRIANGLES; + } - buffer.flip(); - return buffer; + static int exportVertexStride(boolean hasUv, boolean hasNormal) { + return 12 + (hasUv ? 8 : 0) + 4 + (hasNormal ? 4 : 0); } - private IndexData buildIndexData() { + static EncodedIndexData encodeIndexData(int drawMode, int vertexCount) { int primitiveType = mapPrimitiveType(drawMode); - int[] indices = buildIndices(drawMode, currentVertices.size()); - boolean useUInt = currentVertices.size() > 0xFFFF; - ByteBuffer buffer = ByteBuffer.allocate(indices.length * (useUInt ? 4 : 2)) - .order(ByteOrder.LITTLE_ENDIAN); - for (int index : indices) { - if (useUInt) { - buffer.putInt(index); - } else { - buffer.putShort((short) index); - } - } - buffer.flip(); - return new IndexData( - buffer, - indices.length, - useUInt ? ExpIndexElementType.UINT : ExpIndexElementType.USHORT, - primitiveType); - } + boolean useUInt = vertexCount > 0xFFFF; + int bytesPerIndex = useUInt ? Integer.BYTES : Short.BYTES; + int indexCount = drawMode == GL11.GL_QUADS ? vertexCount / 4 * 6 : vertexCount; + byte[] encoded = new byte[indexCount * bytesPerIndex]; + int cursor = 0; - private int[] buildIndices(int drawMode, int vertexCount) { if (drawMode == GL11.GL_QUADS) { int quadCount = vertexCount / 4; - int[] indices = new int[quadCount * 6]; - int cursor = 0; for (int quad = 0; quad < quadCount; quad++) { int base = quad * 4; - indices[cursor++] = base; - indices[cursor++] = base + 1; - indices[cursor++] = base + 2; - indices[cursor++] = base + 2; - indices[cursor++] = base + 3; - indices[cursor++] = base; + cursor = writeIndexLE(encoded, cursor, useUInt, base); + cursor = writeIndexLE(encoded, cursor, useUInt, base + 1); + cursor = writeIndexLE(encoded, cursor, useUInt, base + 2); + cursor = writeIndexLE(encoded, cursor, useUInt, base + 2); + cursor = writeIndexLE(encoded, cursor, useUInt, base + 3); + cursor = writeIndexLE(encoded, cursor, useUInt, base); + } + } else { + for (int index = 0; index < vertexCount; index++) { + cursor = writeIndexLE(encoded, cursor, useUInt, index); } - return indices; } - int[] indices = new int[vertexCount]; - for (int i = 0; i < vertexCount; i++) { - indices[i] = i; - } - return indices; + return new EncodedIndexData( + encoded, + indexCount, + useUInt ? ExpIndexElementType.UINT : ExpIndexElementType.USHORT, + primitiveType); } - private int mapPrimitiveType(int drawMode) { - if (drawMode == GL11.GL_LINES) { - return ExpPrimitiveType.LINES; - } - if (drawMode == GL11.GL_LINE_STRIP) { - return ExpPrimitiveType.LINE_STRIP; + static void appendCapturedVertices(int[] rawBuffer, int vertexCount, boolean hasTexture, boolean hasColor, + boolean hasNormals, Matrix4f worldMatrix, Matrix3f normalMatrix, ByteBuffer target) { + appendCapturedVertices( + rawBuffer, + vertexCount, + hasTexture, + hasColor, + hasNormals, + worldMatrix, + normalMatrix, + new VertexDecodeScratch(), + target); + } + + static void appendCapturedVertices(int[] rawBuffer, int vertexCount, boolean hasTexture, boolean hasColor, + boolean hasNormals, Matrix4f worldMatrix, Matrix3f normalMatrix, VertexDecodeScratch scratch, + ByteBuffer target) { + int start = target.position(); + if (target.hasArray()) { + int written = appendCapturedVertices( + rawBuffer, + vertexCount, + hasTexture, + hasColor, + hasNormals, + worldMatrix, + normalMatrix, + scratch, + target.array(), + target.arrayOffset() + start); + target.position(start + written); + return; } - if (drawMode == GL11.GL_TRIANGLE_STRIP) { - return ExpPrimitiveType.TRIANGLE_STRIP; + + int sanitizedVertexCount = sanitizeVertexCount(rawBuffer, vertexCount); + int written = sanitizedVertexCount * exportVertexStride(hasTexture, hasNormals); + if (written <= 0) { + return; } - if (drawMode == GL11.GL_TRIANGLE_FAN) { - return ExpPrimitiveType.TRIANGLE_FAN; + byte[] temp = new byte[written]; + appendCapturedVertices( + rawBuffer, + sanitizedVertexCount, + hasTexture, + hasColor, + hasNormals, + worldMatrix, + normalMatrix, + scratch, + temp, + 0); + target.put(temp, 0, written); + } + + static int appendCapturedVertices(int[] rawBuffer, int vertexCount, boolean hasTexture, boolean hasColor, + boolean hasNormals, Matrix4f worldMatrix, Matrix3f normalMatrix, VertexDecodeScratch scratch, byte[] target, + int targetOffset) { + if (rawBuffer == null || vertexCount <= 0) { + return 0; } - if (drawMode == GL11.GL_POINTS) { - return ExpPrimitiveType.POINTS; + + int stride = exportVertexStride(hasTexture, hasNormals); + int sanitizedVertexCount = sanitizeVertexCount(rawBuffer, vertexCount); + int cursor = targetOffset; + for (int vertexIndex = 0; vertexIndex < sanitizedVertexCount; vertexIndex++) { + int base = vertexIndex * RAW_VERTEX_STRIDE; + scratch.transformedPosition.set( + Float.intBitsToFloat(rawBuffer[base]), + Float.intBitsToFloat(rawBuffer[base + 1]), + Float.intBitsToFloat(rawBuffer[base + 2])); + worldMatrix.transformPosition(scratch.transformedPosition); + writeFloatLE(target, cursor, scratch.transformedPosition.x); + cursor += Float.BYTES; + writeFloatLE(target, cursor, scratch.transformedPosition.y); + cursor += Float.BYTES; + writeFloatLE(target, cursor, scratch.transformedPosition.z); + cursor += Float.BYTES; + + if (hasTexture) { + writeFloatLE(target, cursor, Float.intBitsToFloat(rawBuffer[base + 3])); + cursor += Float.BYTES; + writeFloatLE(target, cursor, Float.intBitsToFloat(rawBuffer[base + 4])); + cursor += Float.BYTES; + } + + int rgba = hasColor ? rawBuffer[base + 5] : 0xFFFFFFFF; + target[cursor++] = (byte) (rgba & 255); + target[cursor++] = (byte) (rgba >> 8 & 255); + target[cursor++] = (byte) (rgba >> 16 & 255); + target[cursor++] = (byte) (rgba >> 24 & 255); + + if (hasNormals) { + int packedNormal = rawBuffer[base + 6]; + scratch.transformedNormal.set( + unpackNormalComponent(packedNormal), + unpackNormalComponent(packedNormal >> 8), + unpackNormalComponent(packedNormal >> 16)); + normalMatrix.transform(scratch.transformedNormal); + if (scratch.transformedNormal.lengthSquared() > 1.0e-12f) { + scratch.transformedNormal.normalize(); + } + target[cursor++] = packNormalComponent(scratch.transformedNormal.x); + target[cursor++] = packNormalComponent(scratch.transformedNormal.y); + target[cursor++] = packNormalComponent(scratch.transformedNormal.z); + target[cursor++] = 0; + } } - return ExpPrimitiveType.TRIANGLES; + return sanitizedVertexCount * stride; } - private static int clamp(int value) { - if (value < 0) { + private static int sanitizeVertexCount(int[] rawBuffer, int vertexCount) { + if (rawBuffer == null || vertexCount <= 0) { return 0; } - return Math.min(value, 255); + return Math.min(vertexCount, rawBuffer.length / RAW_VERTEX_STRIDE); } private static byte packNormalComponent(float value) { @@ -594,10 +565,29 @@ private static byte packNormalComponent(float value) { return (byte) packed; } - private static byte[] toByteArray(ByteBuffer buffer) { - byte[] out = new byte[buffer.remaining()]; - buffer.get(out); - return out; + private static float unpackNormalComponent(int packedByte) { + return (byte) (packedByte & 0xFF) / 127.0f; + } + + private static void writeFloatLE(byte[] target, int offset, float value) { + int bits = Float.floatToRawIntBits(value); + target[offset] = (byte) bits; + target[offset + 1] = (byte) (bits >> 8); + target[offset + 2] = (byte) (bits >> 16); + target[offset + 3] = (byte) (bits >> 24); + } + + private static int writeIndexLE(byte[] target, int offset, boolean useUInt, int index) { + if (useUInt) { + target[offset] = (byte) index; + target[offset + 1] = (byte) (index >> 8); + target[offset + 2] = (byte) (index >> 16); + target[offset + 3] = (byte) (index >> 24); + return offset + Integer.BYTES; + } + target[offset] = (byte) index; + target[offset + 1] = (byte) (index >> 8); + return offset + Short.BYTES; } public static class RecordingResult { @@ -630,6 +620,14 @@ public static class ExportedTexture { } } + static final class VertexDecodeScratch { + + final Vector3f transformedPosition = new Vector3f(); + final Vector3f transformedNormal = new Vector3f(); + } + + record EncodedIndexData(byte[] indexBuffer, long indexCount, int indexType, int primitiveType) {} + public static class CapturedMesh { final byte[] vertexBuffer; @@ -772,50 +770,4 @@ private static class TextureExport { } } - private static class RecordedVertex { - - private final float x; - private final float y; - private final float z; - private final float u; - private final float v; - private final int r; - private final int g; - private final int b; - private final int a; - private final byte nx; - private final byte ny; - private final byte nz; - - private RecordedVertex(float x, float y, float z, float u, float v, int r, int g, int b, int a, byte nx, - byte ny, byte nz) { - this.x = x; - this.y = y; - this.z = z; - this.u = u; - this.v = v; - this.r = r; - this.g = g; - this.b = b; - this.a = a; - this.nx = nx; - this.ny = ny; - this.nz = nz; - } - } - - private static class IndexData { - - private final ByteBuffer buffer; - private final long indexCount; - private final int indexType; - private final int primitiveType; - - private IndexData(ByteBuffer buffer, long indexCount, int indexType, int primitiveType) { - this.buffer = buffer; - this.indexCount = indexCount; - this.indexType = indexType; - this.primitiveType = primitiveType; - } - } } diff --git a/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java index 6dbc7efc..3f2eede7 100644 --- a/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java +++ b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java @@ -5,6 +5,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -19,6 +20,24 @@ public abstract class MixinTessellatorSceneExportCapture { @Unique private static final Logger LOGGER = LogManager.getLogger("GuideNH/TessCapture"); + @Shadow + private int[] rawBuffer; + + @Shadow + private int vertexCount; + + @Shadow + private boolean hasTexture; + + @Shadow + private boolean hasColor; + + @Shadow + private boolean hasBrightness; + + @Shadow + private boolean hasNormals; + @Inject(method = "startDrawing", at = @At("HEAD")) private void guidenh$captureStartDrawing(int drawMode, CallbackInfo ci) { GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); @@ -39,135 +58,10 @@ public abstract class MixinTessellatorSceneExportCapture { return; } try { + capture.captureRawBuffer(rawBuffer, vertexCount, hasTexture, hasColor, hasBrightness, hasNormals); capture.draw(); } catch (Throwable e) { LOGGER.warn("Scene capture draw failed", e); } } - - @Inject(method = "setTextureUV", at = @At("HEAD")) - private void guidenh$captureTextureUv(double u, double v, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setTextureUV(u, v); - } - - @Inject(method = "setBrightness", at = @At("HEAD")) - private void guidenh$captureBrightness(int brightness, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setBrightness(brightness); - } - - @Inject(method = "setColorOpaque_F", at = @At("HEAD")) - private void guidenh$captureColorOpaqueF(float red, float green, float blue, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setColorOpaque_F(red, green, blue); - } - - @Inject(method = "setColorRGBA_F", at = @At("HEAD")) - private void guidenh$captureColorRgbaF(float red, float green, float blue, float alpha, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setColorRGBA_F(red, green, blue, alpha); - } - - @Inject(method = "setColorOpaque", at = @At("HEAD")) - private void guidenh$captureColorOpaque(int red, int green, int blue, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setColorOpaque(red, green, blue); - } - - @Inject(method = "setColorRGBA", at = @At("HEAD")) - private void guidenh$captureColorRgba(int red, int green, int blue, int alpha, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setColorRGBA(red, green, blue, alpha); - } - - @Inject(method = "func_154352_a", at = @At("HEAD")) - private void guidenh$capturePackedColor(byte red, byte green, byte blue, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.func_154352_a(red, green, blue); - } - - @Inject(method = "setColorOpaque_I", at = @At("HEAD")) - private void guidenh$captureColorOpaqueI(int color, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setColorOpaque_I(color); - } - - @Inject(method = "setColorRGBA_I", at = @At("HEAD")) - private void guidenh$captureColorRgbaI(int color, int alpha, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setColorRGBA_I(color, alpha); - } - - @Inject(method = "disableColor", at = @At("HEAD")) - private void guidenh$captureDisableColor(CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.disableColor(); - } - - @Inject(method = "setNormal", at = @At("HEAD")) - private void guidenh$captureNormal(float x, float y, float z, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setNormal(x, y, z); - } - - @Inject(method = "setTranslation", at = @At("HEAD")) - private void guidenh$captureSetTranslation(double x, double y, double z, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.setTranslation(x, y, z); - } - - @Inject(method = "addTranslation", at = @At("HEAD")) - private void guidenh$captureAddTranslation(float x, float y, float z, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.addTranslation(x, y, z); - } - - @Inject(method = "addVertex", at = @At("HEAD")) - private void guidenh$captureVertex(double x, double y, double z, CallbackInfo ci) { - GuideSiteSceneTessellatorCapture capture = GuideSiteSceneTessellatorCapture.getActive(); - if (capture == null) { - return; - } - capture.addVertex(x, y, z); - } } From 57d661f0f33c4dbb62c7ebb743fd2708afc87ae0 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:10:45 +0800 Subject: [PATCH 125/136] use guide debug log --- .../com/hfstudio/guidenh/ClientProxy.java | 5 ++-- .../java/com/hfstudio/guidenh/GuideNH.java | 4 --- .../guidenh/bridge/GuideNhRuntimeBridge.java | 8 +++--- .../bridge/GuideNhRuntimeBridgeServer.java | 26 ++++++++++------- .../transport/RuntimeBridgeConnection.java | 23 ++++++++------- .../guide/render/GuidePageTexture.java | 20 +++++++------ .../guide/scene/support/GuideDebugLog.java | 28 ------------------- .../ae2/Ae2PreviewPrepareContributor.java | 6 +--- .../BuildCraftPreviewPrepareContributor.java | 6 +--- .../integration/gregtech/GregTechHelpers.java | 7 +---- .../GregTechPreviewPrepareContributor.java | 5 ---- ...gisticsPipesPreviewPrepareContributor.java | 7 +---- .../RailcraftPreviewPrepareContributor.java | 5 ---- .../StructureLibRuntimeFacade.java | 26 +++++++---------- .../StructureLibSceneImportService.java | 15 ++++------ .../TinkersConstructHelpers.java | 5 ---- ...ersConstructPreviewPrepareContributor.java | 5 ---- .../MixinTessellatorSceneExportCapture.java | 10 ++----- 18 files changed, 69 insertions(+), 142 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 0ee45df4..15601cf3 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -72,6 +72,7 @@ import com.hfstudio.guidenh.guide.internal.scheduler.SearchIndexWorkItem; import com.hfstudio.guidenh.guide.scene.level.GuidebookFakeWorld; import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.integration.GuideNhClientIntegrationBootstrap; import com.hfstudio.guidenh.integration.Mods; import com.hfstudio.guidenh.integration.ae2.network.Ae2NetworkRegistration; @@ -199,7 +200,7 @@ public void init(FMLInitializationEvent event) { lytHost.registerScript("RecipesFor", recipeScript); MinecraftForge.EVENT_BUS.register(this); - GuideNH.LOG.info( + GuideDebugLog.infoAlways( "GuideNH runtime bridge configuration loaded. enabled={}, hostConfigured={}, port={}, tokenConfigured={}", ModConfig.runtimeBridge.enabled, ModConfig.runtimeBridge.host != null && !ModConfig.runtimeBridge.host.trim() @@ -235,7 +236,7 @@ public void completeInit(FMLLoadCompleteEvent event) { @SubscribeEvent public void onClientDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) { - GuideNH.LOG.info("Minecraft client disconnected. Stopping GuideNH runtime bridge session state"); + GuideDebugLog.infoAlways("Minecraft client disconnected. Stopping GuideNH runtime bridge session state"); runtimeBridge.stop(); GuideME.closeSearch(); lytHost.getNavigation() diff --git a/src/main/java/com/hfstudio/guidenh/GuideNH.java b/src/main/java/com/hfstudio/guidenh/GuideNH.java index d6366e3c..a9b448eb 100644 --- a/src/main/java/com/hfstudio/guidenh/GuideNH.java +++ b/src/main/java/com/hfstudio/guidenh/GuideNH.java @@ -3,9 +3,6 @@ import static com.hfstudio.guidenh.GuideNH.MODID; import static com.hfstudio.guidenh.GuideNH.MODNAME; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import com.hfstudio.guidenh.guide.internal.GuideCommand; import com.hfstudio.guidenh.guide.internal.GuideNhBridgeCommand; import com.hfstudio.guidenh.guide.internal.item.GuideItem; @@ -35,7 +32,6 @@ public class GuideNH { public static final String MODNAME = Tags.MODNAME; public static final String VERSION = Tags.VERSION; public static final String AUTHOR = "HFstudio"; - public static final Logger LOG = LogManager.getLogger(MODID); public static boolean debug = false; diff --git a/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridge.java b/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridge.java index 274e5b8b..67c50a26 100644 --- a/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridge.java +++ b/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridge.java @@ -1,6 +1,6 @@ package com.hfstudio.guidenh.bridge; -import com.hfstudio.guidenh.GuideNH; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideNhRuntimeBridge { @@ -9,7 +9,7 @@ public class GuideNhRuntimeBridge { public void start(GuideNhRuntimeBridgeSettings settings) { stop(); if (!settings.canStart()) { - GuideNH.LOG.info( + GuideDebugLog.infoAlways( "GuideNH runtime bridge start skipped. enabled={}, hostConfigured={}, portConfigured={}, tokenConfigured={}", settings.isEnabled(), !settings.getHost() @@ -19,7 +19,7 @@ public void start(GuideNhRuntimeBridgeSettings settings) { .isEmpty()); return; } - GuideNH.LOG.info( + GuideDebugLog.infoAlways( "Starting GuideNH runtime bridge. host={}, port={}, maxConnections={}, maxMessageBytes={}, maxPageSize={}, maxSubscriptions={}, maxDeltaEntries={}", settings.getHost(), settings.getPort(), @@ -34,7 +34,7 @@ public void start(GuideNhRuntimeBridgeSettings settings) { public void stop() { if (server != null) { - GuideNH.LOG.info("Stopping GuideNH runtime bridge"); + GuideDebugLog.infoAlways("Stopping GuideNH runtime bridge"); server.stop(); server = null; } diff --git a/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java b/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java index a123a2db..42cf60af 100644 --- a/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java +++ b/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java @@ -16,7 +16,6 @@ import org.jetbrains.annotations.NotNull; -import com.hfstudio.guidenh.GuideNH; import com.hfstudio.guidenh.bridge.preview.ItemPreviewCache; import com.hfstudio.guidenh.bridge.preview.ItemPreviewSearchService; import com.hfstudio.guidenh.bridge.preview.ItemPreviewService; @@ -27,6 +26,7 @@ import com.hfstudio.guidenh.bridge.semantic.SemanticProviderRegistry; import com.hfstudio.guidenh.bridge.semantic.providers.RuntimeSemanticProviders; import com.hfstudio.guidenh.bridge.transport.RuntimeBridgeConnection; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class GuideNhRuntimeBridgeServer { @@ -63,15 +63,21 @@ public void start() { return; } try { - GuideNH.LOG.info("Binding GuideNH runtime bridge server to {}:{}", settings.getHost(), settings.getPort()); + GuideDebugLog.infoAlways( + "Binding GuideNH runtime bridge server to {}:{}", + settings.getHost(), + settings.getPort()); serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(settings.getHost(), settings.getPort())); executor.execute(this::acceptConnections); - GuideNH.LOG.info("GuideNH runtime bridge started at ws://{}:{}", settings.getHost(), settings.getPort()); + GuideDebugLog.infoAlways( + "GuideNH runtime bridge started at ws://{}:{}", + settings.getHost(), + settings.getPort()); } catch (IOException e) { running.set(false); closeServerSocket(); - GuideNH.LOG.warn("Failed to start GuideNH runtime bridge", e); + GuideDebugLog.warnAlways("Failed to start GuideNH runtime bridge", e); } } @@ -79,7 +85,7 @@ public void stop() { if (!running.getAndSet(false)) { return; } - GuideNH.LOG.info("GuideNH runtime bridge server stopping"); + GuideDebugLog.infoAlways("GuideNH runtime bridge server stopping"); closeServerSocket(); List snapshot; synchronized (connections) { @@ -101,9 +107,9 @@ private void acceptConnections() { try { Socket socket = serverSocket.accept(); String remoteAddress = describeRemote(socket); - GuideNH.LOG.info("GuideNH runtime bridge accepted socket from {}", remoteAddress); + GuideDebugLog.infoAlways("GuideNH runtime bridge accepted socket from {}", remoteAddress); if (connections.size() >= limits.getMaxConnections()) { - GuideNH.LOG.warn( + GuideDebugLog.warnAlways( "GuideNH runtime bridge rejected socket from {} because maxConnections={} has been reached", remoteAddress, limits.getMaxConnections()); @@ -119,14 +125,14 @@ private void acceptConnections() { limits, this::handleClosedConnection); connections.add(connection); - GuideNH.LOG.info( + GuideDebugLog.infoAlways( "GuideNH runtime bridge starting session for {}. activeConnections={}", remoteAddress, connections.size()); executor.execute(connection); } catch (IOException e) { if (running.get()) { - GuideNH.LOG.warn("GuideNH runtime bridge accept loop failed", e); + GuideDebugLog.warnAlways("GuideNH runtime bridge accept loop failed", e); } } } @@ -149,7 +155,7 @@ private String describeRemote(Socket socket) { private void handleClosedConnection(RuntimeBridgeConnection connection) { connections.remove(connection); - GuideNH.LOG.info( + GuideDebugLog.infoAlways( "GuideNH runtime bridge session closed for {}. activeConnections={}", connection.getRemoteAddress(), connections.size()); diff --git a/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java b/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java index 677289a9..134fb17f 100644 --- a/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java +++ b/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java @@ -7,7 +7,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; -import com.hfstudio.guidenh.GuideNH; import com.hfstudio.guidenh.bridge.preview.PreviewQueryFactory; import com.hfstudio.guidenh.bridge.preview.PreviewResolveQuery; import com.hfstudio.guidenh.bridge.preview.PreviewResolveResult; @@ -22,6 +21,7 @@ import com.hfstudio.guidenh.bridge.semantic.SemanticProviderRegistry; import com.hfstudio.guidenh.bridge.semantic.SemanticQuery; import com.hfstudio.guidenh.bridge.semantic.SemanticQueryFactory; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class RuntimeBridgeConnection implements Runnable { @@ -60,19 +60,22 @@ public RuntimeBridgeConnection(Socket socket, BridgeMessageCodec messageCodec, public void run() { try { socket.setSoTimeout(SOCKET_TIMEOUT_MILLIS); - GuideNH.LOG.info("GuideNH runtime bridge waiting for WebSocket handshake from {}", describeRemote()); + GuideDebugLog.infoAlways("GuideNH runtime bridge waiting for WebSocket handshake from {}", describeRemote()); if (!new WebSocketHandshake().accept(socket.getInputStream(), socket.getOutputStream())) { - GuideNH.LOG - .warn("GuideNH runtime bridge rejected invalid WebSocket handshake from {}", describeRemote()); + GuideDebugLog.warnAlways( + "GuideNH runtime bridge rejected invalid WebSocket handshake from {}", + describeRemote()); return; } - GuideNH.LOG.info("GuideNH runtime bridge WebSocket handshake completed for {}", describeRemote()); + GuideDebugLog.infoAlways( + "GuideNH runtime bridge WebSocket handshake completed for {}", + describeRemote()); readFrames(); } catch (SocketTimeoutException ignored) { - GuideNH.LOG.warn("GuideNH runtime bridge connection timed out for {}", describeRemote()); + GuideDebugLog.warnAlways("GuideNH runtime bridge connection timed out for {}", describeRemote()); closeQuietly(); } catch (IOException e) { - GuideNH.LOG.warn("GuideNH runtime bridge connection I/O failed for {}", describeRemote(), e); + GuideDebugLog.warnAlways("GuideNH runtime bridge connection I/O failed for {}", describeRemote(), e); closeQuietly(); } finally { close(); @@ -83,7 +86,7 @@ public void close() { if (!closed.compareAndSet(false, true)) { return; } - GuideNH.LOG.info("GuideNH runtime bridge closing connection for {}", describeRemote()); + GuideDebugLog.infoAlways("GuideNH runtime bridge closing connection for {}", describeRemote()); closeQuietly(); closeCallback.accept(this); } @@ -156,12 +159,12 @@ private BridgeEnvelope handleHello(BridgeEnvelope envelope) { .get("token") .getAsString() : ""; if (!authenticator.matches(token)) { - GuideNH.LOG.warn("GuideNH runtime bridge authentication failed for {}", describeRemote()); + GuideDebugLog.warnAlways("GuideNH runtime bridge authentication failed for {}", describeRemote()); return responseFactory .error(envelope.getId(), envelope.getMethod(), "unauthorized", "Invalid bridge token", false); } authenticated = true; - GuideNH.LOG.info("GuideNH runtime bridge authenticated {}", describeRemote()); + GuideDebugLog.infoAlways("GuideNH runtime bridge authenticated {}", describeRemote()); return responseFactory.hello(envelope.getId(), limits); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java b/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java index 39ecb719..25ca2641 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java @@ -14,17 +14,14 @@ import net.minecraft.client.renderer.texture.TextureUtil; import net.minecraft.util.ResourceLocation; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.document.LytSize; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import cpw.mods.fml.relauncher.ReflectionHelper; public class GuidePageTexture { - - public static final Logger LOG = LogManager.getLogger("GuideNH/GuidePageTexture"); public static final GuidePageTexture MISSING = new GuidePageTexture(null, 0, 0, null); private static final String TEXTURE_OBJECTS_FIELD = "mapTextureObjects"; private static final String TEXTURE_OBJECTS_SRG_FIELD = "field_110585_a"; @@ -66,14 +63,14 @@ public static synchronized GuidePageTexture load(ResourceLocation id, byte[] ima try { BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageData)); if (img == null) { - LOG.warn("Failed to decode image {} (ImageIO returned null)", id); + GuideDebugLog.warnAlways("Failed to decode image {} (ImageIO returned null)", id); return missing(); } var gpt = new GuidePageTexture(id, img.getWidth(), img.getHeight(), imageData); CACHE.put(id, gpt); return gpt; } catch (Throwable t) { - LOG.error("Failed to load guide page texture {}", id, t); + GuideDebugLog.error("Failed to load guide page texture {}", id, t); return missing(); } } @@ -114,7 +111,9 @@ public ResourceLocation getTexture() { try { BufferedImage img = ImageIO.read(new ByteArrayInputStream(data)); if (img == null) { - LOG.warn("Failed to decode image {} while creating dynamic texture (ImageIO returned null)", sourceId); + GuideDebugLog.warnAlways( + "Failed to decode image {} while creating dynamic texture (ImageIO returned null)", + sourceId); imageData = null; return null; } @@ -126,7 +125,7 @@ public ResourceLocation getTexture() { imageData = null; return texture; } catch (Throwable t) { - LOG.error("Failed to create guide page dynamic texture {}", sourceId, t); + GuideDebugLog.error("Failed to create guide page dynamic texture {}", sourceId, t); imageData = null; return null; } @@ -158,7 +157,10 @@ private static ITextureObject removeTextureObject(TextureManager textureManager, TEXTURE_OBJECTS_SRG_FIELD); return textureObjects.remove(location); } catch (Throwable t) { - LOG.warn("Failed to remove dynamic guide page texture {} from Minecraft texture manager", location, t); + GuideDebugLog.warnAlways( + "Failed to remove dynamic guide page texture {} from Minecraft texture manager", + location, + t); return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java b/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java index 4d8fd745..07fe05f8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/support/GuideDebugLog.java @@ -2,7 +2,6 @@ import java.util.Set; -import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.config.ModConfig; @@ -45,10 +44,6 @@ public static void error(@Nullable CharSequence message, Object... args) { .error(message.toString(), args); } - public static void error(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { - error(message, args); - } - public static void warn(boolean enabled, @Nullable CharSequence message, Object... args) { if (!enabled) { return; @@ -71,14 +66,6 @@ public static void warnAlways(@Nullable CharSequence message, Object... args) { .warn(message.toString(), args); } - public static void warn(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { - warn(message, args); - } - - public static void warnAlways(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { - warnAlways(message, args); - } - public static void info(boolean enabled, @Nullable CharSequence message, Object... args) { if (!enabled) { return; @@ -101,14 +88,6 @@ public static void infoAlways(@Nullable CharSequence message, Object... args) { .info(message.toString(), args); } - public static void info(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { - info(message, args); - } - - public static void infoAlways(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { - infoAlways(message, args); - } - public static void debug(boolean enabled, @Nullable CharSequence message, Object... args) { if (!enabled) { return; @@ -131,11 +110,4 @@ public static void debugAlways(@Nullable CharSequence message, Object... args) { .debug(message.toString(), args); } - public static void debug(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { - debug(message, args); - } - - public static void debugAlways(@Nullable Logger ignoredLogger, @Nullable String message, Object... args) { - debugAlways(message, args); - } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/ae2/Ae2PreviewPrepareContributor.java b/src/main/java/com/hfstudio/guidenh/integration/ae2/Ae2PreviewPrepareContributor.java index 849b1da2..047a3ac3 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/ae2/Ae2PreviewPrepareContributor.java +++ b/src/main/java/com/hfstudio/guidenh/integration/ae2/Ae2PreviewPrepareContributor.java @@ -1,8 +1,5 @@ package com.hfstudio.guidenh.integration.ae2; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.snapshot.PreviewPrepareContributor; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -10,7 +7,6 @@ public class Ae2PreviewPrepareContributor implements PreviewPrepareContributor { - private static final Logger LOG = LogManager.getLogger("GuideNH/ScenePreview"); private static volatile boolean invokeFailureLogged; @Override @@ -28,7 +24,7 @@ public void prepare(GuidebookLevel level) { } catch (Throwable t) { if (!invokeFailureLogged) { invokeFailureLogged = true; - GuideDebugLog.warn(LOG, "AE2 preview state preparation failed; 3D cable preview may be incomplete", t); + GuideDebugLog.warn("AE2 preview state preparation failed; 3D cable preview may be incomplete", t); } } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/buildcraft/BuildCraftPreviewPrepareContributor.java b/src/main/java/com/hfstudio/guidenh/integration/buildcraft/BuildCraftPreviewPrepareContributor.java index b5dfe124..900c357e 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/buildcraft/BuildCraftPreviewPrepareContributor.java +++ b/src/main/java/com/hfstudio/guidenh/integration/buildcraft/BuildCraftPreviewPrepareContributor.java @@ -1,8 +1,5 @@ package com.hfstudio.guidenh.integration.buildcraft; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.snapshot.PreviewPrepareContributor; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -10,7 +7,6 @@ public class BuildCraftPreviewPrepareContributor implements PreviewPrepareContributor { - private static final Logger LOG = LogManager.getLogger("GuideNH/ScenePreview"); private static volatile boolean invokeFailureLogged; @Override @@ -28,7 +24,7 @@ public void prepare(GuidebookLevel level) { } catch (Throwable t) { if (!invokeFailureLogged) { invokeFailureLogged = true; - GuideDebugLog.warn(LOG, "BuildCraft preview state preparation failed; pipe textures may be wrong", t); + GuideDebugLog.warn("BuildCraft preview state preparation failed; pipe textures may be wrong", t); } } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java b/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java index 70f8504d..6d44a529 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java +++ b/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java @@ -14,8 +14,6 @@ import net.minecraft.world.World; import net.minecraftforge.common.util.ForgeDirection; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; @@ -38,8 +36,6 @@ import gregtech.common.misc.GTStructureChannels; public class GregTechHelpers { - - public static final Logger LOG = LogManager.getLogger("GuideNH/GregTechHelpers"); public static final Set LOGGED_KEYS = Collections.synchronizedSet(new HashSet<>()); public static ItemStack applyOreDictUnification(ItemStack stack) { @@ -476,7 +472,6 @@ private static void synchronizeMultiblockPreviewStateImpl(TileEntity controllerT applyPreviewTextureUpdate(metaTileEntity); } GuideDebugLog.info( - LOG, "GregTech preview sync controller={} meta={} facing={} valid={} activeRequested={} activeBefore={} activeAfter={} machineBefore={} machineAfter={} machineApplied={}", describeTile(controllerTile), describeMetaTile(metaTileEntity), @@ -697,7 +692,7 @@ public static void logInfoOnce(String key, String message, Object... args) { if (key == null || key.isEmpty() || message == null || message.isEmpty()) { return; } - GuideDebugLog.runOnce(LOGGED_KEYS, key, () -> LOG.info(message, args)); + GuideDebugLog.runOnce(LOGGED_KEYS, key, () -> GuideDebugLog.info(message, args)); } public static String describeBlock(@Nullable Block block) { diff --git a/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechPreviewPrepareContributor.java b/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechPreviewPrepareContributor.java index d8ce570e..73b55de8 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechPreviewPrepareContributor.java +++ b/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechPreviewPrepareContributor.java @@ -1,8 +1,5 @@ package com.hfstudio.guidenh.integration.gregtech; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.snapshot.PreviewPrepareContributor; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -10,7 +7,6 @@ public class GregTechPreviewPrepareContributor implements PreviewPrepareContributor { - private static final Logger LOG = LogManager.getLogger("GuideNH/ScenePreview"); private static volatile boolean invokeFailureLogged; @Override @@ -30,7 +26,6 @@ public void prepare(GuidebookLevel level) { if (!invokeFailureLogged) { invokeFailureLogged = true; GuideDebugLog.warn( - LOG, "GT5 preview preparation failed; pipe connections or multiblock formed state may be wrong", t); } diff --git a/src/main/java/com/hfstudio/guidenh/integration/logisticspipes/LogisticsPipesPreviewPrepareContributor.java b/src/main/java/com/hfstudio/guidenh/integration/logisticspipes/LogisticsPipesPreviewPrepareContributor.java index 10b03bf4..d063b645 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/logisticspipes/LogisticsPipesPreviewPrepareContributor.java +++ b/src/main/java/com/hfstudio/guidenh/integration/logisticspipes/LogisticsPipesPreviewPrepareContributor.java @@ -1,8 +1,5 @@ package com.hfstudio.guidenh.integration.logisticspipes; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.snapshot.PreviewPrepareContributor; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -10,7 +7,6 @@ public class LogisticsPipesPreviewPrepareContributor implements PreviewPrepareContributor { - public static final Logger LOG = LogManager.getLogger("GuideNH/ScenePreview"); public static volatile boolean invokeFailureLogged; @Override @@ -28,8 +24,7 @@ public void prepare(GuidebookLevel level) { } catch (Throwable t) { if (!invokeFailureLogged) { invokeFailureLogged = true; - GuideDebugLog - .warn(LOG, "LogisticsPipes preview state preparation failed; pipe rendering may be wrong", t); + GuideDebugLog.warn("LogisticsPipes preview state preparation failed; pipe rendering may be wrong", t); } } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java b/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java index a954779e..4ebc0d2c 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java +++ b/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java @@ -1,8 +1,5 @@ package com.hfstudio.guidenh.integration.railcraft; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import com.hfstudio.guidenh.guide.scene.level.GuidebookLevel; import com.hfstudio.guidenh.guide.scene.snapshot.PreviewPrepareContributor; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -10,7 +7,6 @@ public class RailcraftPreviewPrepareContributor implements PreviewPrepareContributor { - private static final Logger LOG = LogManager.getLogger("GuideNH/ScenePreview"); private static volatile boolean invokeFailureLogged; @Override @@ -29,7 +25,6 @@ public void prepare(GuidebookLevel level) { if (!invokeFailureLogged) { invokeFailureLogged = true; GuideDebugLog.warn( - LOG, "Railcraft multiblock preview preparation failed; multiblock textures may be inactive", t); } diff --git a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java index da4bd1a4..45d64d69 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java +++ b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java @@ -24,8 +24,6 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.ForgeDirection; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import com.gtnewhorizon.structurelib.StructureLibAPI; @@ -48,8 +46,6 @@ import cpw.mods.fml.common.registry.GameRegistry; public class StructureLibRuntimeFacade implements StructureLibFacade { - - public static final Logger LOG = LogManager.getLogger("GuideNH/ScenePreview"); public static final int CONTROLLER_X = 0; public static final int CONTROLLER_Y = 64; public static final int CONTROLLER_Z = 0; @@ -87,7 +83,7 @@ public StructureLibImportResult importScene(StructureLibImportRequest request) { context.clear(); } } catch (Throwable t) { - GuideDebugLog.warn(LOG, "Failed to create Guidebook fake world for StructureLib preview", t); + GuideDebugLog.warn("Failed to create Guidebook fake world for StructureLib preview", t); return StructureLibImportResult.failure("StructureLib preview requires an active client world."); } } @@ -154,7 +150,7 @@ public StructureLibImportResult importScene(StructureLibImportRequest request, B IMPORT_RESULT_CACHE.put(importCacheKey, result); return result; } catch (Throwable t) { - GuideDebugLog.warn(LOG, "StructureLib import failed for controller {}", request.getController(), t); + GuideDebugLog.warn("StructureLib import failed for controller {}", request.getController(), t); return StructureLibImportResult .failure("StructureLib import failed: " + sanitizeMessage(t.getMessage()), warnings, null); } finally { @@ -171,7 +167,7 @@ public static ControlAnalysis analyzeControls(StructureLibImportRequest request, context.clear(); } } catch (Throwable t) { - GuideDebugLog.warn(LOG, "Failed to create Guidebook fake world for StructureLib control analysis", t); + GuideDebugLog.warn("Failed to create Guidebook fake world for StructureLib control analysis", t); return new ControlAnalysis(MIN_TIER, Map.of()); } } @@ -211,7 +207,7 @@ public static int estimateMaxTotalTier(StructureLibImportRequest request, Resolv context.clear(); } } catch (Throwable t) { - GuideDebugLog.warn(LOG, "Failed to create Guidebook fake world for StructureLib tier analysis", t); + GuideDebugLog.warn("Failed to create Guidebook fake world for StructureLib tier analysis", t); return MIN_TIER; } } @@ -262,7 +258,7 @@ public static LinkedHashMap estimateChannelMaxTiers(StructureLi context.clear(); } } catch (Throwable t) { - GuideDebugLog.warn(LOG, "Failed to create Guidebook fake world for StructureLib channel analysis", t); + GuideDebugLog.warn("Failed to create Guidebook fake world for StructureLib channel analysis", t); return new LinkedHashMap<>(); } } @@ -382,7 +378,7 @@ public static BuildSnapshot buildSnapshot(StructureLibImportRequest request, Res context.clear(); } } catch (Throwable t) { - GuideDebugLog.warn(LOG, "Failed to create Guidebook fake world for StructureLib preview", t); + GuideDebugLog.warn("Failed to create Guidebook fake world for StructureLib preview", t); return BuildSnapshot.failure("StructureLib preview requires an active client world."); } } @@ -487,7 +483,6 @@ public static PreparedPreviewWorld preparePreviewWorld(StructureLibImportRequest } catch (Throwable t) { warnings.add("StructureLib instrumentation setup failed; preview tooltip metadata may be incomplete."); GuideDebugLog.warn( - LOG, "Failed to enable StructureLib instrumentation for controller {}", request.getController(), t); @@ -506,7 +501,7 @@ public static PreparedPreviewWorld preparePreviewWorld(StructureLibImportRequest } synchronizePreviewState(controllerTile, triggerStack, selection, warnings); } catch (Throwable t) { - GuideDebugLog.warn(LOG, "StructureLib construct() failed for controller {}", request.getController(), t); + GuideDebugLog.warn("StructureLib construct() failed for controller {}", request.getController(), t); context.resetPreviewState(); return PreparedPreviewWorld.failure("StructureLib construct() failed: " + sanitizeMessage(t.getMessage())); } finally { @@ -544,7 +539,7 @@ public static TileEntity placeControllerDirectly(GuidebookLevel level, World wor return integratedTile; } } catch (Throwable t) { - GuideDebugLog.warn(LOG, "StructureLib controller placement integration failed", t); + GuideDebugLog.warn("StructureLib controller placement integration failed", t); } } @@ -557,7 +552,7 @@ public static TileEntity placeControllerDirectly(GuidebookLevel level, World wor if (warnings != null) { warnings.add("Direct controller tile creation failed for " + controller.blockId + "."); } - GuideDebugLog.warn(LOG, "Direct controller tile creation failed for {}", controller.blockId, t); + GuideDebugLog.warn("Direct controller tile creation failed for {}", controller.blockId, t); return null; } @@ -717,7 +712,7 @@ public static void synchronizePreviewState(TileEntity controllerTile, ItemStack try { synchronizer.synchronizePreviewState(controllerTile, triggerStack, selection, warnings); } catch (Throwable t) { - GuideDebugLog.warn(LOG, "StructureLib preview state synchronizer failed", t); + GuideDebugLog.warn("StructureLib preview state synchronizer failed", t); } } } @@ -861,7 +856,6 @@ public static NBTTagCompound serializeTile(@Nullable TileEntity tile) { return tag; } catch (Throwable t) { GuideDebugLog.warn( - LOG, "Failed to serialize preview tile entity {}", tile.getClass() .getName(), diff --git a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java index 7d1f88ed..63b68e85 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java +++ b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java @@ -2,8 +2,6 @@ import java.util.function.Supplier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; @@ -13,8 +11,6 @@ public class StructureLibSceneImportService { - public static final Logger LOG = LogManager.getLogger("GuideNH/ScenePreview"); - private final StructureLibFacade facade; public StructureLibSceneImportService() { @@ -33,7 +29,7 @@ public boolean isAvailable() { try { return facade.isAvailable(); } catch (Throwable t) { - GuideDebugLog.warn(LOG, "StructureLib facade availability check failed", t); + GuideDebugLog.warn("StructureLib facade availability check failed", t); return false; } } @@ -50,7 +46,7 @@ public StructureLibImportResult importScene(@Nullable StructureLibImportRequest } return StructureLibImportResult.failure("StructureLib facade returned no import result"); } catch (Throwable t) { - GuideDebugLog.warn(LOG, "StructureLib import failed for controller {}", request.getController(), t); + GuideDebugLog.warn("StructureLib import failed for controller {}", request.getController(), t); return StructureLibImportResult.failure(resolveFailureMessage(t)); } } @@ -71,7 +67,7 @@ public StructureLibImportResult importScene(@Nullable StructureLibImportRequest } return StructureLibImportResult.failure("StructureLib facade returned no import result"); } catch (Throwable t) { - GuideDebugLog.warn(LOG, "StructureLib import failed for controller {}", request.getController(), t); + GuideDebugLog.warn("StructureLib import failed for controller {}", request.getController(), t); return StructureLibImportResult.failure(resolveFailureMessage(t)); } } @@ -93,7 +89,7 @@ public static StructureLibFacade resolveFacade(@Nullable Supplier Date: Sat, 6 Jun 2026 16:02:44 +0800 Subject: [PATCH 126/136] bugfix --- .../guide/document/block/LytCodeBlock.java | 27 +- .../document/block/LytCodeBlockToolbar.java | 3 + .../block/LytMermaidMindmapCanvas.java | 164 +++++++---- .../guide/document/block/LytSizeBox.java | 36 ++- .../guidenh/guide/internal/GuideScreen.java | 77 ++++- .../internal/editor/SceneEditorScreen.java | 20 ++ .../autocomplete/ui/AutocompletePopup.java | 20 +- .../gui/SceneEditorMultilineTextArea.java | 77 +++-- .../guide/GuideScreenEditorContextMenu.java | 30 +- .../internal/home/HomePageController.java | 29 +- .../guide/internal/screen/GuideNavBar.java | 13 +- .../guide/internal/util/SmoothFloatState.java | 42 +++ .../guide/render/VanillaRenderContext.java | 9 +- .../guide/scene/LytGuidebookScene.java | 269 ++++++++++-------- 14 files changed, 586 insertions(+), 230 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/util/SmoothFloatState.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java index 817db9a7..5ab4fed2 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java @@ -15,6 +15,7 @@ import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightTheme; import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlighter; import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; @@ -47,6 +48,7 @@ public class LytCodeBlock extends LytVBox implements InteractiveElement, Documen private int bodyContentHeight; private int bodyViewportHeight; private int bodyScrollOffsetY; + private final SmoothFloatState visualBodyScrollOffsetY = new SmoothFloatState(); private boolean draggingBody; private int dragLastDocumentY; private boolean draggingScrollbar; @@ -262,11 +264,13 @@ protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int avai bodyViewportHeight = forcedBodyHeight > 0 ? forcedBodyHeight : bodyContentHeight; setBodyScrollOffset(bodyScrollOffsetY); + snapVisualScrollToTarget(); return new LytRect(x, y, safeWidth, toolbarBounds.height() + getGap() + bodyViewportHeight); } @Override public void render(RenderContext context) { + updateVisualScroll(); LytRect ownBounds = getBounds(); if (ownBounds.isEmpty()) { return; @@ -278,7 +282,18 @@ public void render(RenderContext context) { LytRect bodyViewport = getBodyViewportBounds(); context.pushLocalScissor(bodyViewport); try { - body.render(context); + int offsetY = visualBodyScrollOffsetY.rounded() - bodyScrollOffsetY; + if (offsetY != 0) { + org.lwjgl.opengl.GL11.glPushMatrix(); + try { + org.lwjgl.opengl.GL11.glTranslatef(0f, -offsetY, 0f); + body.render(context); + } finally { + org.lwjgl.opengl.GL11.glPopMatrix(); + } + } else { + body.render(context); + } } finally { context.popScissor(); } @@ -374,7 +389,7 @@ private LytRect getScrollbarThumbBounds() { int thumbTrack = Math.max(1, track.height() - thumbHeight); int thumbY = track.y(); if (maxScroll > 0) { - thumbY += (int) ((long) thumbTrack * bodyScrollOffsetY / maxScroll); + thumbY += (int) ((long) thumbTrack * visualBodyScrollOffsetY.rounded() / maxScroll); } return new LytRect(track.x(), thumbY, track.width(), thumbHeight); } @@ -416,4 +431,12 @@ private void updateScrollFromMouseY(int mouseY) { int maxScroll = getMaxBodyScroll(); setBodyScrollOffset((int) ((long) (thumbTop - track.y()) * maxScroll / thumbTrack)); } + + private void snapVisualScrollToTarget() { + visualBodyScrollOffsetY.snapTo(bodyScrollOffsetY); + } + + private void updateVisualScroll() { + visualBodyScrollOffsetY.updateTowards(bodyScrollOffsetY, 28f, 0.25f, 0.01f, Math.max(128f, bodyViewportHeight * 2f)); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java index d0c858e5..d965521b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java @@ -45,6 +45,9 @@ public LytCodeBlockToolbar() { copyButton.setColor(TOOLBAR_TEXT); append(languageLabel); append(copyButton); + setPaddingLeft(8); + setPaddingTop(4); + setPaddingRight(8); setPaddingBottom(4); setBorderBottom(new BorderStyle(TOOLBAR_BORDER, 1)); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index dbbb6de1..95c2b803 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -30,6 +30,7 @@ import com.hfstudio.guidenh.guide.internal.mermaid.MermaidMindmapNodeShape; import com.hfstudio.guidenh.guide.internal.recipe.LytNeiRecipeBox; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.GuiSprite; import com.hfstudio.guidenh.guide.render.RenderContext; @@ -110,7 +111,10 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar private DiagramLayout layout; private int contentOffsetX; private int contentOffsetY; + private final SmoothFloatState visualContentOffsetX = new SmoothFloatState(); + private final SmoothFloatState visualContentOffsetY = new SmoothFloatState(); private float zoom = 1f; + private final SmoothFloatState visualZoom = new SmoothFloatState(); private int preferredWidth; private int preferredHeight; private float scaledStyleZoom = Float.NaN; @@ -184,20 +188,21 @@ public void render(RenderContext context) { if (layout == null) { return; } - ensureScaledStyles(); + updateVisualState(); + ensureScaledStyles(visualZoom.value()); context.fillRect(bounds, 0x1A0C1117); context.drawBorder(bounds, 0x66434C57, 1); LytRect viewport = getInnerViewport(); - int baseX = viewport.x() + contentOffsetX + int baseX = viewport.x() + visualContentOffsetX.rounded() - Math.round( layout.contentBounds() - .x() * zoom); - int baseY = viewport.y() + contentOffsetY + .x() * visualZoom.value()); + int baseY = viewport.y() + visualContentOffsetY.rounded() - Math.round( layout.contentBounds() - .y() * zoom); + .y() * visualZoom.value()); context.pushLocalScissor(viewport); try { @@ -255,6 +260,9 @@ public void endDrag() { @Override public boolean scroll(int documentX, int documentY, int wheelDelta) { if (wheelDelta == 0 || layout == null || !getInnerViewport().contains(documentX, documentY)) return false; + LytRect viewport = getInnerViewport(); + int previousOffsetX = contentOffsetX; + int previousOffsetY = contentOffsetY; float previousZoom = zoom; if (wheelDelta > 0) { zoom = Math.min(MAX_ZOOM, zoom * ZOOM_STEP); @@ -264,6 +272,12 @@ public boolean scroll(int documentX, int documentY, int wheelDelta) { if (Math.abs(previousZoom - zoom) < 0.0001f) { return false; } + float anchorX = layout.contentBounds() + .x() + (documentX - viewport.x() - previousOffsetX) / Math.max(previousZoom, 0.0001f); + float anchorY = layout.contentBounds() + .y() + (documentY - viewport.y() - previousOffsetY) / Math.max(previousZoom, 0.0001f); + contentOffsetX = Math.round((documentX - viewport.x()) - (anchorX - layout.contentBounds().x()) * zoom); + contentOffsetY = Math.round((documentY - viewport.y()) - (anchorY - layout.contentBounds().y()) * zoom); clampOffsets(); return true; } @@ -588,25 +602,26 @@ private void layoutTopDown(NodeLayout node, int x, int y) { } private void renderConnectors(RenderContext context, NodeLayout node, int baseX, int baseY) { + float activeZoom = visualZoom.value(); for (NodeLayout child : node.children) { if (mindmap.getLayoutMode() == MermaidMindmapLayoutMode.TIDY_TREE) { drawVerticalConnector( context, - scaled(baseX, node.centerX()), - scaled(baseY, node.bottom()), - scaled(baseX, child.centerX()), - scaled(baseY, child.y), + scaled(baseX, node.centerX(), activeZoom), + scaled(baseY, node.bottom(), activeZoom), + scaled(baseX, child.centerX(), activeZoom), + scaled(baseY, child.y, activeZoom), 0xFF5D6C7C); } else { boolean rightSide = child.centerX() >= node.centerX(); - int parentEdgeX = scaled(baseX, rightSide ? node.right() : node.x); - int childEdgeX = scaled(baseX, rightSide ? child.x : child.right()); + int parentEdgeX = scaled(baseX, rightSide ? node.right() : node.x, activeZoom); + int childEdgeX = scaled(baseX, rightSide ? child.x : child.right(), activeZoom); drawHorizontalConnector( context, parentEdgeX, - scaled(baseY, node.centerY()), + scaled(baseY, node.centerY(), activeZoom), childEdgeX, - scaled(baseY, child.centerY()), + scaled(baseY, child.centerY(), activeZoom), 0xFF5D6C7C); } renderConnectors(context, child, baseX, baseY); @@ -614,11 +629,12 @@ private void renderConnectors(RenderContext context, NodeLayout node, int baseX, } private void renderNodes(RenderContext context, NodeLayout node, int baseX, int baseY) { + float activeZoom = visualZoom.value(); LytRect rect = new LytRect( - scaled(baseX, node.x), - scaled(baseY, node.y), - Math.max(1, Math.round(node.width * zoom)), - Math.max(1, Math.round(node.height * zoom))); + scaled(baseX, node.x, activeZoom), + scaled(baseY, node.y, activeZoom), + Math.max(1, Math.round(node.width * activeZoom)), + Math.max(1, Math.round(node.height * activeZoom))); LytRect boxRect = rect; NodeColors colors = resolveColors(node.node); context.fillRect(boxRect, colors.background); @@ -627,11 +643,11 @@ private void renderNodes(RenderContext context, NodeLayout node, int baseX, int ResolvedTextStyle style = node.depth == 0 ? scaledRootTextStyle : scaledNodeTextStyle; ResolvedTextStyle badgeStyle = scaledIconTextStyle; - int paddingX = Math.max(1, Math.round(NODE_PADDING_X * zoom)); - int paddingY = Math.max(1, Math.round(NODE_PADDING_Y * zoom)); - int iconGapY = Math.max(1, Math.round(ICON_GAP_Y * zoom)); - int badgePaddingX = Math.max(2, Math.round(4 * zoom)); - int badgePaddingY = Math.max(1, Math.round(2 * zoom)); + int paddingX = Math.max(1, Math.round(NODE_PADDING_X * activeZoom)); + int paddingY = Math.max(1, Math.round(NODE_PADDING_Y * activeZoom)); + int iconGapY = Math.max(1, Math.round(ICON_GAP_Y * activeZoom)); + int badgePaddingX = Math.max(2, Math.round(4 * activeZoom)); + int badgePaddingY = Math.max(1, Math.round(2 * activeZoom)); int textY = rect.y() + paddingY; if (node.showBadge && node.badgeText != null) { int badgeWidth = Math.max(1, context.getStringWidth(node.badgeText, badgeStyle) + badgePaddingX * 2); @@ -647,7 +663,7 @@ private void renderNodes(RenderContext context, NodeLayout node, int baseX, int } if (node.contentLayout != null) { - renderNodeContent(context, node, rect, paddingX, textY); + renderNodeContent(context, node, rect, paddingX, textY, activeZoom); } else { int lineHeight = context.getLineHeight(style); for (String line : node.lines) { @@ -663,12 +679,13 @@ private void renderNodes(RenderContext context, NodeLayout node, int baseX, int } } - private void renderNodeContent(RenderContext context, NodeLayout node, LytRect rect, int paddingX, int contentY) { + private void renderNodeContent(RenderContext context, NodeLayout node, LytRect rect, int paddingX, int contentY, + float activeZoom) { if (node.contentLayout == null) { return; } LytRect viewport = getInnerViewport(); - LytRect contentViewport = resolveNodeContentRect(node, rect, paddingX, contentY); + LytRect contentViewport = resolveNodeContentRect(node, rect, paddingX, contentY, activeZoom); LytRect clip = intersect(viewport, contentViewport); if (clip == null) { return; @@ -677,11 +694,16 @@ private void renderNodeContent(RenderContext context, NodeLayout node, LytRect r try { int originX = contentViewport.x() - Math.round( node.contentLayout.visualBounds() - .x() * zoom); + .x() * activeZoom); int originY = contentViewport.y() - Math.round( node.contentLayout.visualBounds() - .y() * zoom); - NodeContentRenderContext nodeContext = new NodeContentRenderContext(context, clip, originX, originY, zoom); + .y() * activeZoom); + NodeContentRenderContext nodeContext = new NodeContentRenderContext( + context, + clip, + originX, + originY, + activeZoom); renderNodeContentBlock(node.contentLayout.block(), nodeContext, context, contentViewport); } finally { context.popScissor(); @@ -701,7 +723,7 @@ private void renderNodeContentBlock(LytBlock block, NodeContentRenderContext nod } else if (usesRawGl(block)) { GL11.glPushMatrix(); GL11.glTranslatef(nodeContext.getDocumentOriginX(), nodeContext.getDocumentOriginY(), 0f); - GL11.glScalef(zoom, zoom, 1f); + GL11.glScalef(nodeContext.getScale(), nodeContext.getScale(), 1f); try { block.render(nodeContext); } finally { @@ -796,23 +818,24 @@ private static boolean usesRawGl(LytBlock block) { return null; } LytRect viewport = getInnerViewport(); - int baseX = viewport.x() + contentOffsetX + float activeZoom = visualZoom.value(); + int baseX = viewport.x() + visualContentOffsetX.rounded() - Math.round( layout.contentBounds() - .x() * zoom); - int baseY = viewport.y() + contentOffsetY + .x() * activeZoom); + int baseY = viewport.y() + visualContentOffsetY.rounded() - Math.round( layout.contentBounds() - .y() * zoom); + .y() * activeZoom); List contentNodes = layout.contentNodes(); for (int index = contentNodes.size() - 1; index >= 0; index--) { NodeLayout node = contentNodes.get(index); - LytRect contentScreenRect = getNodeContentScreenRect(node, baseX, baseY); + LytRect contentScreenRect = getNodeContentScreenRect(node, baseX, baseY, activeZoom); if (contentScreenRect == null || !contentScreenRect.contains(documentX, documentY)) { continue; } - int localX = unscaleCoordinate(documentX - contentScreenRect.x()); - int localY = unscaleCoordinate(documentY - contentScreenRect.y()); + int localX = unscaleCoordinate(documentX - contentScreenRect.x(), activeZoom); + int localY = unscaleCoordinate(documentY - contentScreenRect.y(), activeZoom); DocumentInteractionSnapshot hit = LytDocument.pick(node.contentLayout.block(), localX, localY); if (hit != null) { return new NodeHit(hit.node(), hit.flowPath(), localX, localY); @@ -821,25 +844,27 @@ private static boolean usesRawGl(LytBlock block) { return null; } - private @Nullable LytRect getNodeContentScreenRect(NodeLayout node, int baseX, int baseY) { + private @Nullable LytRect getNodeContentScreenRect(NodeLayout node, int baseX, int baseY, float activeZoom) { if (node.contentLayout == null) { return null; } LytRect nodeRect = new LytRect( - scaled(baseX, node.x), - scaled(baseY, node.y), - Math.max(1, Math.round(node.width * zoom)), - Math.max(1, Math.round(node.height * zoom))); - int paddingX = Math.max(1, Math.round(NODE_PADDING_X * zoom)); - int contentY = nodeRect.y() + Math.max(1, Math.round(NODE_PADDING_Y * zoom)) + resolveNodeBadgeHeight(node); - return resolveNodeContentRect(node, nodeRect, paddingX, contentY); + scaled(baseX, node.x, activeZoom), + scaled(baseY, node.y, activeZoom), + Math.max(1, Math.round(node.width * activeZoom)), + Math.max(1, Math.round(node.height * activeZoom))); + int paddingX = Math.max(1, Math.round(NODE_PADDING_X * activeZoom)); + int contentY = nodeRect.y() + Math.max(1, Math.round(NODE_PADDING_Y * activeZoom)) + + resolveNodeBadgeHeight(node, activeZoom); + return resolveNodeContentRect(node, nodeRect, paddingX, contentY, activeZoom); } private int contextLineHeight(ResolvedTextStyle style) { return Math.max(1, Math.round((9 + 1) * style.fontScale())); } - private LytRect resolveNodeContentRect(NodeLayout node, LytRect nodeRect, int paddingX, int contentY) { + private LytRect resolveNodeContentRect(NodeLayout node, LytRect nodeRect, int paddingX, int contentY, + float activeZoom) { return new LytRect( nodeRect.x() + paddingX, contentY, @@ -847,27 +872,27 @@ private LytRect resolveNodeContentRect(NodeLayout node, LytRect nodeRect, int pa 1, Math.round( node.contentLayout.visualBounds() - .width() * zoom)), + .width() * activeZoom)), Math.max( 1, Math.round( node.contentLayout.visualBounds() - .height() * zoom))); + .height() * activeZoom))); } - private int resolveNodeBadgeHeight(NodeLayout node) { + private int resolveNodeBadgeHeight(NodeLayout node, float activeZoom) { if (!node.showBadge || node.badgeText == null) { return 0; } - ensureScaledStyles(); + ensureScaledStyles(activeZoom); ResolvedTextStyle badgeStyle = scaledIconTextStyle; - int badgePaddingY = Math.max(1, Math.round(2 * zoom)); - int iconGapY = Math.max(1, Math.round(ICON_GAP_Y * zoom)); + int badgePaddingY = Math.max(1, Math.round(2 * activeZoom)); + int iconGapY = Math.max(1, Math.round(ICON_GAP_Y * activeZoom)); return contextLineHeight(badgeStyle) + badgePaddingY * 2 + iconGapY; } - private int unscaleCoordinate(int coordinate) { - return Math.max(0, Math.round(coordinate / Math.max(zoom, 0.0001f))); + private int unscaleCoordinate(int coordinate, float activeZoom) { + return Math.max(0, Math.round(coordinate / Math.max(activeZoom, 0.0001f))); } private @Nullable LytRect intersect(LytRect a, LytRect b) { @@ -1149,6 +1174,9 @@ private void centerDiagram(int viewportWidth, int viewportHeight) { int innerHeight = Math.max(1, viewportHeight - CANVAS_PADDING * 2); contentOffsetX = (innerWidth - Math.round(layout.diagramWidth * zoom)) / 2; contentOffsetY = (innerHeight - Math.round(layout.diagramHeight * zoom)) / 2; + visualContentOffsetX.snapTo(contentOffsetX); + visualContentOffsetY.snapTo(contentOffsetY); + visualZoom.snapTo(zoom); clampOffsets(); } @@ -1163,6 +1191,12 @@ private void clampOffsets() { contentOffsetY = clampAxis(contentOffsetY, innerHeight, Math.round(layout.diagramHeight * zoom)); } + private void updateVisualState() { + visualContentOffsetX.updateTowards(contentOffsetX, 26f, 0.05f, 0.01f, Math.max(128f, bounds.width() * 2f)); + visualContentOffsetY.updateTowards(contentOffsetY, 26f, 0.05f, 0.01f, Math.max(128f, bounds.height() * 2f)); + visualZoom.updateTowards(zoom, 24f, 0.05f, 0.0001f, 4f); + } + private int clampAxis(int offset, int viewportSize, int contentSize) { if (contentSize <= viewportSize) { return (viewportSize - contentSize) / 2; @@ -1184,13 +1218,13 @@ private int resolvePreferredViewportWidth() { return preferredWidth > 0 ? preferredWidth : MIN_WIDTH; } - private int scaled(int base, int value) { - return base + Math.round(value * zoom); + private int scaled(int base, int value, float activeZoom) { + return base + Math.round(value * activeZoom); } - private ResolvedTextStyle scaleStyle(ResolvedTextStyle baseStyle) { + private ResolvedTextStyle scaleStyle(ResolvedTextStyle baseStyle, float activeZoom) { return new ResolvedTextStyle( - baseStyle.fontScale() * zoom, + baseStyle.fontScale() * activeZoom, baseStyle.bold(), baseStyle.italic(), baseStyle.underlined(), @@ -1207,16 +1241,16 @@ private ResolvedTextStyle scaleStyle(ResolvedTextStyle baseStyle) { baseStyle.inlineCode()); } - private void ensureScaledStyles() { - if (Float.compare(scaledStyleZoom, zoom) == 0 && scaledRootTextStyle != null + private void ensureScaledStyles(float activeZoom) { + if (Float.compare(scaledStyleZoom, activeZoom) == 0 && scaledRootTextStyle != null && scaledNodeTextStyle != null && scaledIconTextStyle != null) { return; } - scaledStyleZoom = zoom; - scaledRootTextStyle = scaleStyle(ROOT_TEXT_STYLE); - scaledNodeTextStyle = scaleStyle(NODE_TEXT_STYLE); - scaledIconTextStyle = scaleStyle(ICON_TEXT_STYLE); + scaledStyleZoom = activeZoom; + scaledRootTextStyle = scaleStyle(ROOT_TEXT_STYLE, activeZoom); + scaledNodeTextStyle = scaleStyle(NODE_TEXT_STYLE, activeZoom); + scaledIconTextStyle = scaleStyle(ICON_TEXT_STYLE, activeZoom); } LytRect getContentBoundsForTesting() { @@ -1320,6 +1354,10 @@ public int getDocumentOriginY() { return originY; } + public float getScale() { + return scale; + } + @Override public LytRect toScreenRect(LytRect rect) { LytRect s = scaleRect(rect); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java index 9bef6be1..34cdf2bf 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java @@ -5,6 +5,7 @@ import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.interaction.DocumentDragTarget; import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorVerticalScrollbar; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; @@ -25,6 +26,7 @@ public class LytSizeBox extends LytVBox implements DocumentDragTarget { private int viewportHeight; private int scrollOffsetY; private int appliedScrollOffsetY; + private final SmoothFloatState visualScrollOffsetY = new SmoothFloatState(); private boolean draggingContent; private int dragLastDocumentY; private boolean draggingScrollbar; @@ -67,6 +69,7 @@ protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int avai viewportHeight = preferredHeight > 0 ? preferredHeight : contentHeight; setScrollOffset(scrollOffsetY); + snapVisualScrollToTarget(); int totalWidth = preferredWidth > 0 ? measuredWidth : contentBounds.width() + (hasVerticalScroll() ? SCROLLBAR_WIDTH + SCROLLBAR_GAP : 0); @@ -75,6 +78,7 @@ protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int avai @Override public void render(RenderContext context) { + updateVisualScroll(); if (!hasVerticalScroll()) { super.render(context); return; @@ -87,8 +91,21 @@ public void render(RenderContext context) { LytRect viewportBounds = getViewportBounds(); context.pushLocalScissor(viewportBounds); try { - for (LytBlock child : children) { - child.render(context); + int offsetY = visualScrollOffsetY.rounded() - appliedScrollOffsetY; + if (offsetY != 0) { + org.lwjgl.opengl.GL11.glPushMatrix(); + try { + org.lwjgl.opengl.GL11.glTranslatef(0f, -offsetY, 0f); + for (LytBlock child : children) { + child.render(context); + } + } finally { + org.lwjgl.opengl.GL11.glPopMatrix(); + } + } else { + for (LytBlock child : children) { + child.render(context); + } } } finally { context.popScissor(); @@ -255,7 +272,20 @@ private LytRect getScrollbarThumbBounds() { } SceneEditorVerticalScrollbar.Thumb thumb = SceneEditorVerticalScrollbar - .computeThumb(trackBounds.y(), trackBounds.height(), contentHeight, viewportHeight, scrollOffsetY); + .computeThumb( + trackBounds.y(), + trackBounds.height(), + contentHeight, + viewportHeight, + visualScrollOffsetY.rounded()); return new LytRect(trackBounds.x(), thumb.start(), trackBounds.width(), thumb.size()); } + + private void snapVisualScrollToTarget() { + visualScrollOffsetY.snapTo(scrollOffsetY); + } + + private void updateVisualScroll() { + visualScrollOffsetY.updateTowards(scrollOffsetY, 28f, 0.25f, 0.01f, Math.max(128f, viewportHeight * 2f)); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index f73eb320..386c8a21 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -187,6 +187,7 @@ public class GuideScreen extends GuiContainer private final Deque forwardHistory = new ArrayDeque<>(); private int scrollY; + private float visualScrollY; private boolean pendingAnchorScroll; private float currentZoom = 1.0f; private float currentVisualScale = 1.0f; @@ -387,6 +388,7 @@ public class GuideScreen extends GuiContainer private long guideLastMouseEvent; private int guideTouchValue; private boolean temporaryScreenChangeExpected; + private long lastVisualUpdateNanos; public static class SceneButtonHit { @@ -709,6 +711,7 @@ private void restoreViewState(GuideScreenViewState state) { lastLayoutWidth = -1; invalidateScrollbarOutline(); scrollY = 0; + snapVisualScrollToTarget(); loadCurrentPage(); ensureLayout(); scrollToCurrentAnchor(); @@ -727,6 +730,7 @@ private void applyPendingRestoreScroll() { scrollY = pendingRestoreViewState.scrollY(); pendingRestoreViewState = null; clampScroll(); + snapVisualScrollToTarget(); } private void finalizePendingViewState() { @@ -2471,12 +2475,14 @@ private void scrollToCurrentAnchor() { var layoutY = flowAnchor.getLayoutY(); if (layoutY.isPresent()) { scrollY = layoutY.getAsInt(); + snapVisualScrollToTarget(); return; } } LytRect bounds = blockNode.getBounds(); if (bounds != null) { scrollY = bounds.y(); + snapVisualScrollToTarget(); } } @@ -2566,6 +2572,9 @@ private void clampScroll() { int max = getMaxScroll(); if (scrollY < 0) scrollY = 0; if (scrollY > max) scrollY = max; + if (Math.abs(visualScrollY - scrollY) > Math.max(96f, getDocumentViewportHeight() * 2f)) { + visualScrollY = scrollY; + } ClientProxy.getLytHost() .getViewport() .updateContent(contentW, contentH); @@ -2574,11 +2583,39 @@ private void clampScroll() { .scrollTo(scrollY); } + private void snapVisualScrollToTarget() { + visualScrollY = scrollY; + } + + private void updateVisualState() { + long now = System.nanoTime(); + if (lastVisualUpdateNanos == 0L) { + lastVisualUpdateNanos = now; + visualScrollY = scrollY; + return; + } + float deltaSeconds = Math.min(0.05f, (now - lastVisualUpdateNanos) / 1_000_000_000f); + lastVisualUpdateNanos = now; + visualScrollY = smoothApproach(visualScrollY, scrollY, deltaSeconds, 28f); + if (Math.abs(visualScrollY - scrollY) < 0.01f) { + visualScrollY = scrollY; + } + } + + private static float smoothApproach(float current, float target, float deltaSeconds, float sharpness) { + if (deltaSeconds <= 0f) { + return current; + } + float blend = 1f - (float) Math.exp(-sharpness * deltaSeconds); + return current + (target - current) * blend; + } + @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { lastMouseX = mouseX; lastMouseY = mouseY; hoveredItemStack = null; + updateVisualState(); drawTiledBackground(); recomputePanelBounds(); if (consumePanelBoundsChanged()) { @@ -2591,6 +2628,7 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { currentZoom = resolveCurrentZoom(); ensureLayout(); clampScroll(); + pollContinuousMouseDrag(mouseX, mouseY); updateScrollbarOutlineHover(mouseX, mouseY); int navX = panelX; @@ -2753,6 +2791,17 @@ private void pollActiveSceneDrag() { } } + private void pollContinuousMouseDrag(int mouseX, int mouseY) { + if (guideMouseEventButton < 0 || guideLastMouseEvent <= 0L) { + return; + } + if (guideMouseEventButton <= 2 && !Mouse.isButtonDown(guideMouseEventButton)) { + return; + } + long heldTime = Minecraft.getSystemTime() - guideLastMouseEvent; + mouseClickMove(mouseX, mouseY, guideMouseEventButton, heldTime); + } + private void drawGuideEditorScreen(int mouseX, int mouseY) { ensureGuideEditorTextArea(); updateGuideEditorTextFromArea(); @@ -4255,14 +4304,16 @@ private void renderDocument(int mouseX, int mouseY) { var ctx = reusableRenderCtx; ctx.setLightDarkMode(LightDarkMode.LIGHT_MODE); int documentRenderOffsetY = getDocumentRenderOffsetY(activeDocument); - int viewportTopInDocument = Math.max(0, scrollY - documentRenderOffsetY); + int renderedScrollY = Math.round(visualScrollY); + int viewportTopInDocument = Math.max(0, renderedScrollY - documentRenderOffsetY); cachedViewportRect = cachedRect(cachedViewportRect, 0, viewportTopInDocument, contentW, documentH); cachedScissorRect = cachedRect(cachedScissorRect, contentX, documentY, contentW, documentH); ctx.setViewport(cachedViewportRect); ctx.setScreenHeight(this.height); int documentRenderY = getDocumentViewportY() + documentRenderOffsetY; ctx.setDocumentOrigin(contentX, documentRenderY); - ctx.setScrollOffsetY(scrollY); + ctx.setScrollOffsetY(renderedScrollY); + ctx.setPreciseScrollOffsetY(visualScrollY); ctx.setZoom(currentZoom); ctx.pushScissor(cachedScissorRect); GL11.glPushMatrix(); @@ -4270,7 +4321,7 @@ private void renderDocument(int mouseX, int mouseY) { if (currentZoom != 1.0f) { GL11.glScalef(currentZoom, currentZoom, 1f); } - GL11.glTranslatef(0f, -(float) scrollY, 0f); + GL11.glTranslatef(0f, -visualScrollY, 0f); try { activeDocument.render(ctx); } catch (Throwable t) { @@ -4480,7 +4531,7 @@ private void drawScrollbar() { getActiveDocument(), bounds, currentZoom, - scrollY, + Math.round(visualScrollY), lastMouseX, lastMouseY, System.currentTimeMillis(), @@ -4494,7 +4545,7 @@ private void drawScrollbar() { int thumbH = Math .max(16, (int) ((long) bounds.height() * bounds.viewportHeight() / Math.max(1, bounds.contentHeight()))); int thumbY = bounds.maxScroll() > 0 - ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * scrollY / bounds.maxScroll()) + ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * Math.round(visualScrollY) / bounds.maxScroll()) : bounds.y(); int thumbColor = draggingScrollbar ? 0xFFFFFFFF : 0xFFCCCCCC; drawRect(bounds.x(), thumbY, bounds.x() + bounds.width(), thumbY + thumbH, thumbColor); @@ -4506,7 +4557,7 @@ private int[] scrollbarThumbRect() { int thumbH = Math .max(16, (int) ((long) bounds.height() * bounds.viewportHeight() / Math.max(1, bounds.contentHeight()))); int thumbY = bounds.maxScroll() > 0 - ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * scrollY / bounds.maxScroll()) + ? bounds.y() + (int) ((long) (bounds.height() - thumbH) * Math.round(visualScrollY) / bounds.maxScroll()) : bounds.y(); return new int[] { bounds.x(), thumbY, bounds.width(), thumbH, bounds.y(), bounds.height() }; } @@ -4778,6 +4829,7 @@ protected void mouseClicked(int mouseX, int mouseY, int button) { if (markerTarget != null) { scrollY = markerTarget.intValue(); clampScroll(); + snapVisualScrollToTarget(); return; } } @@ -5656,11 +5708,11 @@ private DocumentInteractionState getDocumentInteractionState(int mouseX, int mou getDocumentRenderY(activeDocument), contentW, getDocumentViewportHeight(), - scrollY)) { + Math.round(visualScrollY))) { return interaction; } int docX = Math.round((mouseX - contentX) / currentZoom); - int docY = Math.round((mouseY - getDocumentRenderY(activeDocument)) / currentZoom) + scrollY; + int docY = Math.round((mouseY - getDocumentRenderY(activeDocument)) / currentZoom) + Math.round(visualScrollY); var hit = activeDocument.pick(docX, docY); var scene = hit != null ? findSceneAncestor(hit.node()) : null; SceneButtonHit sceneButtonHit = null; @@ -5686,7 +5738,7 @@ private DocumentInteractionState getDocumentInteractionState(int mouseX, int mou getDocumentRenderY(activeDocument), contentW, getDocumentViewportHeight(), - scrollY, + Math.round(visualScrollY), contentX, getDocumentViewportY(), contentW, @@ -5735,10 +5787,10 @@ private int[] screenToDocumentPoint(int mouseX, int mouseY) { var activeDocument = getActiveDocument(); if (activeDocument == null) { return new int[] { Math.round((mouseX - contentX) / currentZoom), - Math.round((mouseY - getDocumentViewportY()) / currentZoom) + scrollY }; + Math.round((mouseY - getDocumentViewportY()) / currentZoom) + Math.round(visualScrollY) }; } return new int[] { Math.round((mouseX - contentX) / currentZoom), - Math.round((mouseY - getDocumentRenderY(activeDocument)) / currentZoom) + scrollY }; + Math.round((mouseY - getDocumentRenderY(activeDocument)) / currentZoom) + Math.round(visualScrollY) }; } private int[] screenToActiveDocumentPoint(int mouseX, int mouseY) { @@ -5808,6 +5860,7 @@ protected void keyTyped(char typedChar, int keyCode) { } if (keyCode == Keyboard.KEY_HOME) { scrollY = 0; + snapVisualScrollToTarget(); return; } if (keyCode == Keyboard.KEY_END) { @@ -6602,6 +6655,7 @@ private void updateSearchQuery(String query) { refreshCurrentPageTitle(); rebuildSearchDocumentIfNeeded(true); scrollY = 0; + snapVisualScrollToTarget(); invalidateScrollbarOutline(); rebuildToolbar(); syncSearchFieldsToCurrentRoute(); @@ -6624,6 +6678,7 @@ private void updateSpecialPageQuery(String query) { pendingAnchorScroll = false; applySpecialPageSearchQuery(query); scrollY = 0; + snapVisualScrollToTarget(); syncSearchFieldsToCurrentRoute(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java index ca0481db..d3ed3ef3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java @@ -290,6 +290,8 @@ public class SceneEditorScreen extends GuiScreen { private boolean previewFrameOverlayVisible; private boolean previewDirty; private boolean preservePreviewCameraOnNextRebuild; + private int activeMouseDragButton = -1; + private long activeMouseDragStartedAt; @Nullable private Integer previewVisibleLayerOverride; @Nullable @@ -626,6 +628,17 @@ private void pollActivePreviewSceneDrag() { } } + private void pollContinuousMouseDrag(int mouseX, int mouseY) { + if (activeMouseDragButton < 0 || activeMouseDragStartedAt <= 0L) { + return; + } + if (activeMouseDragButton <= 2 && !Mouse.isButtonDown(activeMouseDragButton)) { + return; + } + long heldTime = Minecraft.getSystemTime() - activeMouseDragStartedAt; + mouseClickMove(mouseX, mouseY, activeMouseDragButton, heldTime); + } + @Override protected void actionPerformed(GuiButton button) { if (button.id == CLOSE_BUTTON_ID) { @@ -742,6 +755,7 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { addElementButton.visible = !rightPanelCollapsed; } syncToolbarToggleState(); + pollContinuousMouseDrag(mouseX, mouseY); pollActivePreviewSceneDrag(); drawTiledBackground(); @@ -847,6 +861,8 @@ public void handleMouseInput() { @Override protected void mouseClicked(int mouseX, int mouseY, int button) { + activeMouseDragButton = button; + activeMouseDragStartedAt = Minecraft.getSystemTime(); if (closeConfirmDialogOpen) { handleCloseConfirmDialogClick(mouseX, mouseY, button); return; @@ -1078,6 +1094,10 @@ protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, lo @Override protected void mouseMovedOrUp(int mouseX, int mouseY, int state) { + if (state != -1) { + activeMouseDragButton = -1; + activeMouseDragStartedAt = 0L; + } if (markdownTextArea != null && state != -1) { markdownTextArea.mouseReleased(state); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java index e49737b5..a24be30c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/ui/AutocompletePopup.java @@ -15,6 +15,7 @@ import com.hfstudio.guidenh.guide.internal.editor.autocomplete.provider.AutocompleteCandidate; import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorPopupLayout; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; public class AutocompletePopup { @@ -35,6 +36,7 @@ public class AutocompletePopup { private List lastCandidateKeys = Collections.emptyList(); private int selectedIndex; private int scrollY; + private final SmoothFloatState visualScrollY = new SmoothFloatState(); private int x, y, width, height; private int viewportWidth, viewportHeight; @@ -58,6 +60,7 @@ public void show(List candidates, int anchorX, int anchor this.scrollY = 0; lastCandidateKeys = candidateKeys(this.candidates); computeSize(fontRenderer); + snapVisualScrollToTarget(); } this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; @@ -88,6 +91,7 @@ public void close() { candidates = Collections.emptyList(); lastCandidateKeys = Collections.emptyList(); selectedIndex = -1; + snapVisualScrollToTarget(); } public void moveSelection(int delta) { @@ -163,13 +167,15 @@ public boolean contains(int mouseX, int mouseY) { public void draw(FontRenderer fontRenderer, int mouseX, int mouseY) { if (!open) return; + updateVisualScroll(); + int renderedScrollY = visualScrollY.rounded(); Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, BORDER_COLOR); Gui.drawRect(x, y, x + width, y + height, BACKGROUND_COLOR); pushScissor(x + 1, y + 1, width - 2, height - 2); - int drawY = y + PADDING_Y - scrollY; + int drawY = y + PADDING_Y - renderedScrollY; int itemIndex = 0; int itemHeight = computeMaxItemHeight(); for (AutocompleteCandidate candidate : candidates) { @@ -245,7 +251,7 @@ private void ensureSelectionVisible() { private int findItemIndex(int mouseX, int mouseY) { if (!contains(mouseX, mouseY)) return -1; int itemH = computeMaxItemHeight(); - int localY = mouseY - y - PADDING_Y + scrollY; + int localY = mouseY - y - PADDING_Y + visualScrollY.rounded(); int index = localY / itemH; if (index < 0 || index >= candidates.size()) return -1; return index; @@ -258,7 +264,7 @@ private void drawScrollbar() { Gui.drawRect(barX, y + 1, x + width - 1, y + height - 1, SCROLLBAR_TRACK_COLOR); int thumbH = Math.max(16, height * height / Math.max(1, contentH)); int maxScroll = Math.max(0, contentH - height); - int thumbY = y + (maxScroll > 0 ? (height - thumbH) * scrollY / maxScroll : 0); + int thumbY = y + (maxScroll > 0 ? (height - thumbH) * visualScrollY.rounded() / maxScroll : 0); Gui.drawRect(barX, thumbY, x + width - 1, thumbY + thumbH, SCROLLBAR_THUMB_COLOR); } @@ -273,6 +279,14 @@ private int clampScroll(int value) { return Math.min(value, maxScroll); } + private void snapVisualScrollToTarget() { + visualScrollY.snapTo(scrollY); + } + + private void updateVisualScroll() { + visualScrollY.updateTowards(scrollY, 28f, 0.25f, 0.01f, Math.max(96f, height * 2f)); + } + private static void pushScissor(int x, int y, int width, int height) { Minecraft mc = Minecraft.getMinecraft(); int scale = DisplayScale.scaleFactor(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index fc2a26d3..30589395 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -18,6 +18,7 @@ import com.hfstudio.guidenh.guide.compiler.GuideMarkdownOptions; import com.hfstudio.guidenh.guide.internal.markdown.MdAstToMdxConverter; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.libs.mdast.MdAst; import com.hfstudio.guidenh.libs.mdast.model.MdAstDefinition; import com.hfstudio.guidenh.libs.mdast.model.MdAstList; @@ -57,6 +58,8 @@ public class SceneEditorMultilineTextArea { private int textViewportWidth; private int textViewportHeight; private int horizontalOffsetPixels; + private final SmoothFloatState visualVerticalOffsetPixels = new SmoothFloatState(); + private final SmoothFloatState visualHorizontalOffsetPixels = new SmoothFloatState(); private boolean wrapEnabled; private boolean verticalScrollbarVisible; private boolean horizontalScrollbarVisible; @@ -329,6 +332,7 @@ public void setVerticalScrollFraction(float fraction) { int maxOffset = Math.max(0, scrollState.getContentPixels() - scrollState.getViewportPixels()); int offset = Math.round(Math.clamp(fraction, 0f, 1f) * maxOffset); scrollState.setOffsetPixels(offset); + snapVisualOffsetsToTarget(); syncImeFocusProxy(); } @@ -957,6 +961,9 @@ private static String normalizeLineEndings(String text) { } public void draw(boolean validationError) { + updateVisualOffsets(); + int renderedVerticalOffset = visualVerticalOffsetPixels.rounded(); + int renderedHorizontalOffset = visualHorizontalOffsetPixels.rounded(); int borderColor = validationError ? ERROR_BORDER_COLOR : (focused ? FOCUSED_BORDER_COLOR : BORDER_COLOR); Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, borderColor); Gui.drawRect(x, y, x + width, y + height, BACKGROUND_COLOR); @@ -973,13 +980,13 @@ public void draw(boolean validationError) { List lines = layoutCache.getVisualLines(); int lineHeight = getLineHeight(); - int drawY = y + PADDING - scrollState.getOffsetPixels(); + int drawY = y + PADDING - renderedVerticalOffset; for (SceneEditorMultilineTextLayoutCache.VisualLine line : lines) { if (drawY + lineHeight >= y && drawY < y + clipHeight) { - drawExternalHighlightForLine(line, drawY); - drawSelectionForLine(line, drawY); - fontRenderer.drawString(line.text(), x + PADDING - horizontalOffsetPixels, drawY, 0xF0F0F0); - drawSyntaxWarningForLine(line, drawY); + drawExternalHighlightForLine(line, drawY, renderedHorizontalOffset); + drawSelectionForLine(line, drawY, renderedHorizontalOffset); + fontRenderer.drawString(line.text(), x + PADDING - renderedHorizontalOffset, drawY, 0xF0F0F0); + drawSyntaxWarningForLine(line, drawY, renderedHorizontalOffset); } drawY += lineHeight; } @@ -988,8 +995,8 @@ public void draw(boolean validationError) { int cursorLine = getVisualLineIndex(selectionModel.getCursorIndex()); SceneEditorMultilineTextLayoutCache.VisualLine visualLine = lines.get(cursorLine); int cursorPixel = getCursorPixelOnLine(selectionModel.getCursorIndex(), visualLine); - int cursorX = x + PADDING + cursorPixel - horizontalOffsetPixels; - int cursorY = y + PADDING + cursorLine * lineHeight - scrollState.getOffsetPixels(); + int cursorX = x + PADDING + cursorPixel - renderedHorizontalOffset; + int cursorY = y + PADDING + cursorLine * lineHeight - renderedVerticalOffset; Gui.drawRect(cursorX, cursorY, cursorX + 1, cursorY + fontRenderer.FONT_HEIGHT + 1, 0xFFFFFFFF); } @@ -1035,19 +1042,22 @@ private void rebuildLayoutCache() { } scrollState.setViewportPixels(textViewportHeight); scrollState.setContentPixels(layoutCache.getContentHeightPixels()); + snapVisualOffsetsToTarget(); } private void syncImeFocusProxy() { int lineHeight = getLineHeight(); int cursorX = x + PADDING; int cursorY = y + PADDING; + int renderedVerticalOffset = visualVerticalOffsetPixels.rounded(); + int renderedHorizontalOffset = visualHorizontalOffsetPixels.rounded(); List lines = layoutCache.getVisualLines(); if (!lines.isEmpty()) { int cursorLine = getVisualLineIndex(selectionModel.getCursorIndex()); SceneEditorMultilineTextLayoutCache.VisualLine visualLine = lines.get(cursorLine); int cursorPixel = getCursorPixelOnLine(selectionModel.getCursorIndex(), visualLine); - cursorX += cursorPixel - horizontalOffsetPixels; - cursorY += cursorLine * lineHeight - scrollState.getOffsetPixels(); + cursorX += cursorPixel - renderedHorizontalOffset; + cursorY += cursorLine * lineHeight - renderedVerticalOffset; } int contentLeft = x + PADDING; @@ -1062,7 +1072,8 @@ private void syncImeFocusProxy() { imeFocusProxy.height = Math.max(1, lineHeight); } - private void drawSelectionForLine(SceneEditorMultilineTextLayoutCache.VisualLine line, int drawY) { + private void drawSelectionForLine(SceneEditorMultilineTextLayoutCache.VisualLine line, int drawY, + int renderedHorizontalOffset) { if (!selectionModel.hasSelection()) { return; } @@ -1081,7 +1092,7 @@ private void drawSelectionForLine(SceneEditorMultilineTextLayoutCache.VisualLine .substring(0, max); String selectedText = line.text() .substring(max, Math.max(0, highlightEnd - line.startIndex())); - int selectionX = x + PADDING + fontRenderer.getStringWidth(beforeSelection) - horizontalOffsetPixels; + int selectionX = x + PADDING + fontRenderer.getStringWidth(beforeSelection) - renderedHorizontalOffset; int selectionWidth = fontRenderer.getStringWidth(selectedText); if (selectionWidth <= 0 && spansLineBreak) { selectionWidth = 2; @@ -1096,7 +1107,8 @@ private void drawSelectionForLine(SceneEditorMultilineTextLayoutCache.VisualLine } } - private void drawExternalHighlightForLine(SceneEditorMultilineTextLayoutCache.VisualLine line, int drawY) { + private void drawExternalHighlightForLine(SceneEditorMultilineTextLayoutCache.VisualLine line, int drawY, + int renderedHorizontalOffset) { if (externalHighlightStart < 0 || externalHighlightEnd <= externalHighlightStart) { return; } @@ -1113,7 +1125,7 @@ private void drawExternalHighlightForLine(SceneEditorMultilineTextLayoutCache.Vi .substring(0, max); String highlightedText = line.text() .substring(max, Math.max(0, highlightEnd - line.startIndex())); - int highlightX = x + PADDING + fontRenderer.getStringWidth(beforeHighlight) - horizontalOffsetPixels; + int highlightX = x + PADDING + fontRenderer.getStringWidth(beforeHighlight) - renderedHorizontalOffset; int highlightWidth = fontRenderer.getStringWidth(highlightedText); if (highlightWidth <= 0 && spansLineBreak) { highlightWidth = 2; @@ -1128,7 +1140,8 @@ private void drawExternalHighlightForLine(SceneEditorMultilineTextLayoutCache.Vi } } - private void drawSyntaxWarningForLine(SceneEditorMultilineTextLayoutCache.VisualLine line, int drawY) { + private void drawSyntaxWarningForLine(SceneEditorMultilineTextLayoutCache.VisualLine line, int drawY, + int renderedHorizontalOffset) { if (syntaxWarningStart < 0 || syntaxWarningEnd <= syntaxWarningStart) { return; } @@ -1145,7 +1158,7 @@ private void drawSyntaxWarningForLine(SceneEditorMultilineTextLayoutCache.Visual .substring(0, max); String warnedText = line.text() .substring(max, Math.max(0, highlightEnd - line.startIndex())); - int warningX = x + PADDING + fontRenderer.getStringWidth(beforeWarning) - horizontalOffsetPixels; + int warningX = x + PADDING + fontRenderer.getStringWidth(beforeWarning) - renderedHorizontalOffset; int warningWidth = fontRenderer.getStringWidth(warnedText); if (warningWidth <= 0 && spansLineBreak) { warningWidth = 2; @@ -1206,7 +1219,7 @@ private SceneEditorVerticalScrollbar.Thumb getVerticalScrollbarThumb() { getVerticalScrollbarTrackLength(), scrollState.getContentPixels(), scrollState.getViewportPixels(), - scrollState.getOffsetPixels()); + visualVerticalOffsetPixels.rounded()); } private SceneEditorHorizontalScrollbar.Thumb getHorizontalScrollbarThumb() { @@ -1218,7 +1231,7 @@ private SceneEditorHorizontalScrollbar.Thumb getHorizontalScrollbarThumb() { getHorizontalScrollbarTrackLength(), layoutCache.getContentWidthPixels(), textViewportWidth, - horizontalOffsetPixels); + visualHorizontalOffsetPixels.rounded()); } private void moveCursorVertical(int direction, boolean keepSelection) { @@ -1257,8 +1270,8 @@ public int getCursorPixelX() { List lines = layoutCache.getVisualLines(); if (lines.isEmpty()) return PADDING; int lineIdx = getVisualLineIndex(selectionModel.getCursorIndex()); - return PADDING + getCursorPixelOnLine(selectionModel.getCursorIndex(), lines.get(lineIdx)) - - horizontalOffsetPixels; + return PADDING + getCursorPixelOnLine(selectionModel.getCursorIndex(), lines.get(lineIdx)) + - visualHorizontalOffsetPixels.rounded(); } /** Returns the pixel Y position of the cursor relative to this text area. */ @@ -1266,7 +1279,7 @@ public int getCursorPixelY() { List lines = layoutCache.getVisualLines(); if (lines.isEmpty()) return PADDING; int lineIdx = getVisualLineIndex(selectionModel.getCursorIndex()); - return PADDING + lineIdx * getLineHeight() - scrollState.getOffsetPixels(); + return PADDING + lineIdx * getLineHeight() - visualVerticalOffsetPixels.rounded(); } public boolean isCursorVisibleInViewport() { @@ -1282,7 +1295,7 @@ private int getCursorIndexAt(int mouseX, int mouseY) { if (lines.isEmpty()) { return 0; } - int localY = mouseY - y - PADDING + scrollState.getOffsetPixels(); + int localY = mouseY - y - PADDING + visualVerticalOffsetPixels.rounded(); int lineIndex = localY <= 0 ? 0 : localY / getLineHeight(); if (lineIndex < 0) { lineIndex = 0; @@ -1290,7 +1303,7 @@ private int getCursorIndexAt(int mouseX, int mouseY) { if (lineIndex >= lines.size()) { lineIndex = lines.size() - 1; } - int localX = Math.max(0, mouseX - x - PADDING + horizontalOffsetPixels); + int localX = Math.max(0, mouseX - x - PADDING + visualHorizontalOffsetPixels.rounded()); return getCursorIndexAtPixel(lines.get(lineIndex), localX); } @@ -1378,6 +1391,26 @@ private int clampHorizontalOffset(int requestedOffset) { return Math.min(requestedOffset, maxOffset); } + private void snapVisualOffsetsToTarget() { + visualVerticalOffsetPixels.snapTo(scrollState.getOffsetPixels()); + visualHorizontalOffsetPixels.snapTo(horizontalOffsetPixels); + } + + private void updateVisualOffsets() { + visualVerticalOffsetPixels.updateTowards( + scrollState.getOffsetPixels(), + 30f, + 0.25f, + 0.01f, + Math.max(160f, getContentClipHeight() * 2f)); + visualHorizontalOffsetPixels.updateTowards( + horizontalOffsetPixels, + 30f, + 0.25f, + 0.01f, + Math.max(160f, textViewportWidth * 2f)); + } + private int clamp(int value, int min, int max) { if (value < min) { return min; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java index f40e8864..80dac5d1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java @@ -14,6 +14,7 @@ import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorPopupLayout; import com.hfstudio.guidenh.guide.internal.screen.GuideIconButton; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; public class GuideScreenEditorContextMenu { @@ -116,6 +117,7 @@ public void open(int mouseX, int mouseY, int viewportWidth, int viewportHeight, panes.add(new MenuPane(entries, rootRect.x(), rootRect.y(), rootWidth, rootHeight)); open = true; draggingScrollbarPaneIndex = -1; + panes.getFirst().snapVisualScrollToTarget(); update(mouseX, mouseY, viewportWidth, viewportHeight, fontRenderer); } @@ -127,6 +129,7 @@ public void setViewport(int viewportWidth, int viewportHeight, FontRenderer font rootPane.width = computeMenuWidth(entries, fontRenderer); rootPane.height = clampMenuHeight(computeMenuContentHeight(entries), viewportHeight); rootPane.scrollY = clampScroll(rootPane.scrollY, rootPane.entries, rootPane.height); + rootPane.snapVisualScrollToTarget(); var rootRect = SceneEditorPopupLayout .clampToViewport(rootPane.x, rootPane.y, rootPane.width, rootPane.height, viewportWidth, viewportHeight, 2); rootPane.x = rootRect.x(); @@ -205,6 +208,7 @@ public void update(int mouseX, int mouseY, int viewportWidth, int viewportHeight } for (MenuPane pane : panes) { pane.scrollY = clampScroll(pane.scrollY, pane.entries, pane.height); + pane.updateVisualScroll(); } int paneIndex = findDeepestPaneIndex(mouseX, mouseY); @@ -223,7 +227,7 @@ public void update(int mouseX, int mouseY, int viewportWidth, int viewportHeight pane.y, pane.width, pane.height, - pane.scrollY, + pane.visualScrollY.rounded(), pane.entries); if (entryIndex < 0) { pane.hoveredIndex = -1; @@ -257,7 +261,7 @@ private void ensureChildPane(int paneIndex, int entryIndex, int viewportWidth, i childX = parentPane.x - childWidth + 1; } childX = clampToViewportX(childX, childWidth, viewportWidth); - int childY = parentPane.y + PADDING_Y + entryIndex * ITEM_HEIGHT - parentPane.scrollY; + int childY = parentPane.y + PADDING_Y + entryIndex * ITEM_HEIGHT - parentPane.visualScrollY.rounded(); childY = clampToViewportY(childY, childHeight, viewportHeight); int childPaneIndex = paneIndex + 1; @@ -267,6 +271,7 @@ private void ensureChildPane(int paneIndex, int entryIndex, int viewportWidth, i childPane.entries = childEntries; childPane.hoveredIndex = -1; childPane.scrollY = 0; + childPane.snapVisualScrollToTarget(); } childPane.x = childX; childPane.y = childY; @@ -274,7 +279,9 @@ private void ensureChildPane(int paneIndex, int entryIndex, int viewportWidth, i childPane.height = childHeight; childPane.scrollY = clampScroll(childPane.scrollY, childPane.entries, childPane.height); } else { - panes.add(new MenuPane(childEntries, childX, childY, childWidth, childHeight)); + MenuPane pane = new MenuPane(childEntries, childX, childY, childWidth, childHeight); + pane.snapVisualScrollToTarget(); + panes.add(pane); } } @@ -344,7 +351,7 @@ public void draw(FontRenderer fontRenderer, int mouseX, int mouseY) { pane.height, pane.entries, pane.hoveredIndex, - pane.scrollY); + pane.visualScrollY.rounded()); } } @@ -476,7 +483,11 @@ private boolean startScrollbarDrag(int mouseX, int mouseY) { pane.scrollY)) { draggingScrollbarPaneIndex = i; scrollbarGrabOffset = mouseY - - scrollbarThumbY(pane.y, pane.height, computeMenuContentHeight(pane.entries), pane.scrollY); + - scrollbarThumbY( + pane.y, + pane.height, + computeMenuContentHeight(pane.entries), + pane.visualScrollY.rounded()); return true; } } @@ -552,6 +563,7 @@ private static final class MenuPane { private int width; private int height; private int scrollY; + private final SmoothFloatState visualScrollY = new SmoothFloatState(); private int hoveredIndex = -1; private MenuPane(List entries, int x, int y, int width, int height) { @@ -561,5 +573,13 @@ private MenuPane(List entries, int x, int y, int width, int height) { this.width = width; this.height = height; } + + private void snapVisualScrollToTarget() { + visualScrollY.snapTo(scrollY); + } + + private void updateVisualScroll() { + visualScrollY.updateTowards(scrollY, 28f, 0.25f, 0.01f, Math.max(96f, height * 2f)); + } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java index f81a7409..41b644d7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java @@ -11,6 +11,7 @@ import com.hfstudio.guidenh.guide.internal.screen.GuideNavBar; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; public class HomePageController { @@ -36,6 +37,9 @@ public class HomePageController { private int recommendedScrollOffset; private int bookmarksScrollOffset; private int historyScrollOffset; + private final SmoothFloatState recommendedVisualScrollOffset = new SmoothFloatState(); + private final SmoothFloatState bookmarksVisualScrollOffset = new SmoothFloatState(); + private final SmoothFloatState historyVisualScrollOffset = new SmoothFloatState(); @Nullable private PendingClick pendingClick; @@ -44,6 +48,7 @@ public class HomePageController { public void render(Minecraft mc, HomePageDataBuilder.HomePageSections sections, HomePageLayout.LayoutRects layout, ResourceLocation logoTexture, int logoSourceWidth, int logoSourceHeight, int mouseX, int mouseY) { + updateVisualScrollOffsets(); drawLogo(mc, layout.logo(), logoTexture, logoSourceWidth, logoSourceHeight); drawSection(mc, layout.recommended(), sections.recommended(), mouseX, mouseY, layout.recommendedTitleSafeTop()); drawSection(mc, layout.bookmarks(), sections.bookmarks(), mouseX, mouseY, 0); @@ -190,13 +195,14 @@ private void drawSection(Minecraft mc, HomePageLayout.Rect rect, HomePageSection int maxScroll = Math.max(0, contentHeight - visibleHeight); int scrollOffset = clampScroll(section, rect, getScrollOffset(section), topInset); setScrollOffset(section, scrollOffset); + int renderedScrollOffset = getVisualScrollOffset(section).rounded(); pushScissor(contentX, contentY, rowWidth, visibleHeight); for (int i = 0; i < section.entries() .size(); i++) { HomePageEntry entry = section.entries() .get(i); - int rowY = contentY + i * (ROW_HEIGHT + ROW_GAP) - scrollOffset; + int rowY = contentY + i * (ROW_HEIGHT + ROW_GAP) - renderedScrollOffset; if (rowY + ROW_HEIGHT < contentY || rowY > contentY + visibleHeight) { continue; } @@ -205,7 +211,7 @@ private void drawSection(Minecraft mc, HomePageLayout.Rect rect, HomePageSection popScissor(); if (maxScroll > 0) { - drawScrollbar(rect, section, scrollOffset, metrics, contentHeight); + drawScrollbar(rect, section, renderedScrollOffset, metrics, contentHeight); } } @@ -285,6 +291,20 @@ private void setScrollOffset(HomePageSection section, int value) { } } + private SmoothFloatState getVisualScrollOffset(HomePageSection section) { + return switch (section.kind()) { + case RECOMMENDED -> recommendedVisualScrollOffset; + case BOOKMARKS -> bookmarksVisualScrollOffset; + case HISTORY -> historyVisualScrollOffset; + }; + } + + private void updateVisualScrollOffsets() { + recommendedVisualScrollOffset.updateTowards(recommendedScrollOffset, 28f, 0.25f, 0.01f, 256f); + bookmarksVisualScrollOffset.updateTowards(bookmarksScrollOffset, 28f, 0.25f, 0.01f, 256f); + historyVisualScrollOffset.updateTowards(historyScrollOffset, 28f, 0.25f, 0.01f, 256f); + } + private void pushScissor(int x, int y, int width, int height) { int scale = DisplayScale.scaleFactor(); Minecraft mc = Minecraft.getMinecraft(); @@ -323,7 +343,7 @@ private HomePageEntry findEntryAt(SectionTarget target, int mouseX, int mouseY) return null; } int contentY = metrics.contentY(); - int localY = mouseY - contentY + getScrollOffset(target.section()); + int localY = mouseY - contentY + getVisualScrollOffset(target.section()).rounded(); if (localY < 0) { return null; } @@ -366,7 +386,8 @@ private int scrollbarThumbOffset(HomePageLayout.Rect rect, SectionMetrics metric metrics.visibleHeight() * metrics.visibleHeight() / Math.max(metrics.visibleHeight(), computeContentHeight(section))); int travel = Math.max(1, metrics.visibleHeight() - thumbHeight); - int thumbY = metrics.contentY() + (int) ((long) getScrollOffset(section) * travel / Math.max(1, maxScroll)); + int thumbY = metrics.contentY() + + (int) ((long) getVisualScrollOffset(section).rounded() * travel / Math.max(1, maxScroll)); if (mouseY >= thumbY && mouseY < thumbY + thumbHeight) { return mouseY - thumbY; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java index 6ba6e3c1..8b333f19 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java @@ -25,6 +25,7 @@ import com.hfstudio.guidenh.guide.internal.GuideBookmarkState; import com.hfstudio.guidenh.guide.internal.GuidebookText; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.navigation.NavigationTree; import com.hfstudio.guidenh.guide.render.GuidePageTexture; @@ -168,6 +169,7 @@ public boolean shouldCreateNewPage() { private boolean contextMenuOpen; private int openWidth = WIDTH_OPEN; private int scrollY; + private final SmoothFloatState visualScrollY = new SmoothFloatState(); @Nullable private Row hoveredScrollingRow; private long hoveredScrollingStartedAtMillis; @@ -267,6 +269,7 @@ private void rebuildRows(@Nullable NavigationTree tree, GuideBookmarkState bookm public void render(Minecraft mc, @Nullable ResourceLocation currentGuideId, @Nullable ResourceLocation currentPageId, int mouseX, int mouseY, @Nullable PageCollection pageCollection, GuideBookmarkState bookmarkState, boolean showNewPageButton) { + updateVisualScroll(); GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_CURRENT_BIT | GL11.GL_COLOR_BUFFER_BIT); try { int w = currentWidth(); @@ -733,7 +736,7 @@ public void scroll(int dwheel) { } private int getFirstVisibleRowIndex() { - int firstVisibleRow = Math.floorDiv(scrollY - CONTENT_PADDING - 1, ROW_H); + int firstVisibleRow = Math.floorDiv(visualScrollY.rounded() - CONTENT_PADDING - 1, ROW_H); return Math.clamp(firstVisibleRow, 0, rows.size()); } @@ -746,7 +749,7 @@ private RowHit pickRowAt(int mouseY, StickyStack stickyStack) { } } - int relativeY = mouseY - y - TITLE_H + scrollY - CONTENT_PADDING; + int relativeY = mouseY - y - TITLE_H + visualScrollY.rounded() - CONTENT_PADDING; if (relativeY < 0) { return null; } @@ -761,7 +764,7 @@ private RowHit pickRowAt(int mouseY, StickyStack stickyStack) { } private int getRowY(int rowIndex) { - return y + TITLE_H + CONTENT_PADDING - scrollY + rowIndex * ROW_H; + return y + TITLE_H + CONTENT_PADDING - visualScrollY.rounded() + rowIndex * ROW_H; } private int getTitleButtonY() { @@ -784,6 +787,10 @@ private int getBodyHeight() { return Math.max(0, height - TITLE_H); } + private void updateVisualScroll() { + visualScrollY.updateTowards(scrollY, 28f, 0.25f, 0.01f, Math.max(128f, getBodyHeight() * 2f)); + } + private int getBodyBottom() { return getBodyY() + getBodyHeight(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/util/SmoothFloatState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/util/SmoothFloatState.java new file mode 100644 index 00000000..b2fb58bb --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/util/SmoothFloatState.java @@ -0,0 +1,42 @@ +package com.hfstudio.guidenh.guide.internal.util; + +public final class SmoothFloatState { + + private float value; + private long lastUpdateNanos; + + public float value() { + return value; + } + + public int rounded() { + return Math.round(value); + } + + public void snapTo(float target) { + value = target; + lastUpdateNanos = System.nanoTime(); + } + + public void updateTowards(float target, float response, float maxDeltaSeconds, float snapThreshold, + float jumpThreshold) { + long now = System.nanoTime(); + if (lastUpdateNanos == 0L) { + value = target; + lastUpdateNanos = now; + return; + } + + float deltaSeconds = Math.min((now - lastUpdateNanos) / 1_000_000_000f, maxDeltaSeconds); + lastUpdateNanos = now; + if (Math.abs(value - target) > jumpThreshold) { + value = target; + } else if (deltaSeconds > 0f) { + float blend = 1f - (float) Math.exp(-response * deltaSeconds); + value += (target - value) * blend; + } + if (Math.abs(value - target) < snapThreshold) { + value = target; + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java index e2a770e1..8d52a522 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java @@ -44,6 +44,7 @@ public class VanillaRenderContext implements RenderContext { private int documentOriginY = 0; private int scrollOffsetY = 0; + private float preciseScrollOffsetY = 0f; private float zoom = 1.0f; @@ -78,6 +79,12 @@ public int getDocumentOriginY() { public void setScrollOffsetY(int scrollOffsetY) { this.scrollOffsetY = scrollOffsetY; + this.preciseScrollOffsetY = scrollOffsetY; + } + + public void setPreciseScrollOffsetY(float scrollOffsetY) { + this.preciseScrollOffsetY = scrollOffsetY; + this.scrollOffsetY = Math.round(scrollOffsetY); } @Override @@ -97,7 +104,7 @@ public void setZoom(float zoom) { public LytRect toScreenRect(LytRect rect) { return new LytRect( Math.round(rect.x() * zoom) + documentOriginX, - Math.round((rect.y() - scrollOffsetY) * zoom) + documentOriginY, + Math.round((rect.y() - preciseScrollOffsetY) * zoom) + documentOriginY, Math.max(1, Math.round(rect.width() * zoom)), Math.max(1, Math.round(rect.height() * zoom))); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 30924c63..050dc2d1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -58,6 +58,7 @@ import com.hfstudio.guidenh.guide.internal.tooltip.AppendedItemTooltip; import com.hfstudio.guidenh.guide.internal.ui.GuideSliderRenderer; import com.hfstudio.guidenh.guide.internal.util.DisplayScale; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.scene.annotation.DiamondAnnotation; @@ -191,6 +192,12 @@ public boolean overlaps(int otherStartTick, int otherEndTickExclusive) { private float ponderCamRotZ = 0f; private float ponderCamOffX = 0f; private float ponderCamOffY = 0f; + private final SmoothFloatState visualCamZoom = new SmoothFloatState(); + private final SmoothFloatState visualCamRotX = new SmoothFloatState(); + private final SmoothFloatState visualCamRotY = new SmoothFloatState(); + private final SmoothFloatState visualCamRotZ = new SmoothFloatState(); + private final SmoothFloatState visualCamOffX = new SmoothFloatState(); + private final SmoothFloatState visualCamOffY = new SmoothFloatState(); private final List ponderActiveAnnotations = new ArrayList<>(); private final List ponderOutgoingAnnotations = new ArrayList<>(); private int ponderOutgoingFadeTick = 0; @@ -2220,67 +2227,45 @@ else if (pa instanceof OverlayAnnotation ov) { List weatherEffects = resolveRenderableWeatherEffectsForCurrentView( weatherLayerSelection); - boolean ponderCameraApplied = false; - float savedZoom = 0, savedRotX = 0, savedRotY = 0, savedRotZ = 0, savedOffX = 0, savedOffY = 0; - if (ponderSceneData != null) { - savedZoom = camera.getZoom(); - savedRotX = camera.getRotationX(); - savedRotY = camera.getRotationY(); - savedRotZ = camera.getRotationZ(); - savedOffX = camera.getOffsetX(); - savedOffY = camera.getOffsetY(); - camera.setZoom(ponderCamZoom); - camera.setRotationX(ponderCamRotX); - camera.setRotationY(ponderCamRotY); - camera.setRotationZ(ponderCamRotZ); - camera.setOffsetX(ponderCamOffX); - camera.setOffsetY(ponderCamOffY); - ponderCameraApplied = true; - } - - GuidebookLevelRenderer.getInstance() - .render( - level, - camera, - absX, - absY, - w, - h, - clipX, - clipY, - clipW, - clipH, - 0f, - inWorld, - context.lightDarkMode(), - weatherLayerSelection, - resolveRenderableSceneParticles(), - weatherEffects, - resolveWeatherAnimationTick()); - - context.restoreExternalRenderState(); - drawBlockStatsOverlay(context, sceneRect, outerRect); - context.restoreExternalRenderState(); - - if (!overlays.isEmpty()) { - LytRect viewport = cachedOverlayViewport = updateCachedRect(cachedOverlayViewport, absX, absY, w, h); - context.pushLocalScissor(sceneRect); - try { - for (var o : overlays) { - o.render(camera, context, viewport); + float[] savedCameraState = applyVisualCameraState(); + try { + GuidebookLevelRenderer.getInstance() + .render( + level, + camera, + absX, + absY, + w, + h, + clipX, + clipY, + clipW, + clipH, + 0f, + inWorld, + context.lightDarkMode(), + weatherLayerSelection, + resolveRenderableSceneParticles(), + weatherEffects, + resolveWeatherAnimationTick()); + + context.restoreExternalRenderState(); + drawBlockStatsOverlay(context, sceneRect, outerRect); + context.restoreExternalRenderState(); + + if (!overlays.isEmpty()) { + LytRect viewport = cachedOverlayViewport = updateCachedRect(cachedOverlayViewport, absX, absY, w, h); + context.pushLocalScissor(sceneRect); + try { + for (var o : overlays) { + o.render(camera, context, viewport); + } + } finally { + context.popScissor(); } - } finally { - context.popScissor(); } - } - - if (ponderCameraApplied) { - camera.setZoom(savedZoom); - camera.setRotationX(savedRotX); - camera.setRotationY(savedRotY); - camera.setRotationZ(savedRotZ); - camera.setOffsetX(savedOffX); - camera.setOffsetY(savedOffY); + } finally { + applyCapturedCameraState(savedCameraState); } drawBottomControls(context, outerRect); @@ -3665,35 +3650,13 @@ private PickRay resolvePickRay(int mouseAbsX, int mouseAbsY) { float relX = (mouseAbsX) - (lastAbsX + lastW * 0.5f); float relY = (mouseAbsY) - (lastAbsY + lastH * 0.5f); - // Apply ponder camera so the raycast matches the rendered view. - boolean ponderApplied = false; - float pSavedZoom = 0, pSavedRX = 0, pSavedRY = 0, pSavedRZ = 0, pSavedOX = 0, pSavedOY = 0; - if (ponderSceneData != null) { - pSavedZoom = camera.getZoom(); - pSavedRX = camera.getRotationX(); - pSavedRY = camera.getRotationY(); - pSavedRZ = camera.getRotationZ(); - pSavedOX = camera.getOffsetX(); - pSavedOY = camera.getOffsetY(); - camera.setZoom(ponderCamZoom); - camera.setRotationX(ponderCamRotX); - camera.setRotationY(ponderCamRotY); - camera.setRotationZ(ponderCamRotZ); - camera.setOffsetX(ponderCamOffX); - camera.setOffsetY(ponderCamOffY); - ponderApplied = true; - } - - camera.setViewportSize(lastW, lastH); - float[] ray = camera.screenToWorldRay(relX, relY, pickRayScratch); - - if (ponderApplied) { - camera.setZoom(pSavedZoom); - camera.setRotationX(pSavedRX); - camera.setRotationY(pSavedRY); - camera.setRotationZ(pSavedRZ); - camera.setOffsetX(pSavedOX); - camera.setOffsetY(pSavedOY); + float[] ray; + float[] savedCameraState = applyVisualCameraState(); + try { + camera.setViewportSize(lastW, lastH); + ray = camera.screenToWorldRay(relX, relY, pickRayScratch); + } finally { + applyCapturedCameraState(savedCameraState); } float ox = ray[0], oy = ray[1], oz = ray[2]; @@ -3940,6 +3903,7 @@ public void startDrag(int mouseX, int mouseY, int button) { this.dragButton = button; this.dragLastX = mouseX; this.dragLastY = mouseY; + updateVisualCameraState(); } public void updateSoundHover(int mouseX, int mouseY) { @@ -4013,39 +3977,101 @@ private void playSceneSound(GuideSoundSpec sound, int referenceX, int referenceY } private float[] projectSoundPosition(GuideSoundSpec sound) { - float[] saved = null; - if (ponderSceneData != null) { - saved = applyPonderCameraForProjection(); - } + float[] saved = applyVisualCameraState(); try { var projected = camera.worldToScreen(sound.x(), sound.y(), sound.z()); return new float[] { lastAbsX + projected.x, lastAbsY + projected.y }; } finally { - if (saved != null) { - restoreCameraFromProjection(saved); - } + applyCapturedCameraState(saved); + } + } + + private void updateVisualCameraState() { + float targetZoom = getLogicalCameraZoom(); + float targetRotX = getLogicalCameraRotationX(); + float targetRotY = getLogicalCameraRotationY(); + float targetRotZ = getLogicalCameraRotationZ(); + float targetOffX = getLogicalCameraOffsetX(); + float targetOffY = getLogicalCameraOffsetY(); + if (visualCamZoom.value() == 0f && visualCamRotX.value() == 0f && visualCamRotY.value() == 0f && visualCamRotZ.value() == 0f + && visualCamOffX.value() == 0f && visualCamOffY.value() == 0f) { + visualCamZoom.snapTo(targetZoom); + visualCamRotX.snapTo(targetRotX); + visualCamRotY.snapTo(targetRotY); + visualCamRotZ.snapTo(targetRotZ); + visualCamOffX.snapTo(targetOffX); + visualCamOffY.snapTo(targetOffY); + return; } + if (isDraggingCamera()) { + visualCamZoom.updateTowards(targetZoom, 48f, 0.05f, 0.0001f, 8f); + visualCamRotX.updateTowards(targetRotX, 48f, 0.05f, 0.01f, 360f); + visualCamRotY.updateTowards(targetRotY, 48f, 0.05f, 0.01f, 360f); + visualCamRotZ.updateTowards(targetRotZ, 48f, 0.05f, 0.01f, 360f); + visualCamOffX.updateTowards(targetOffX, 54f, 0.05f, 0.01f, 512f); + visualCamOffY.updateTowards(targetOffY, 54f, 0.05f, 0.01f, 512f); + return; + } + visualCamZoom.updateTowards(targetZoom, 22f, 0.05f, 0.0001f, 4f); + visualCamRotX.updateTowards(targetRotX, 22f, 0.05f, 0.01f, 180f); + visualCamRotY.updateTowards(targetRotY, 22f, 0.05f, 0.01f, 180f); + visualCamRotZ.updateTowards(targetRotZ, 22f, 0.05f, 0.01f, 180f); + visualCamOffX.updateTowards(targetOffX, 26f, 0.05f, 0.01f, 256f); + visualCamOffY.updateTowards(targetOffY, 26f, 0.05f, 0.01f, 256f); } - private float[] applyPonderCameraForProjection() { - float[] saved = new float[] { camera.getZoom(), camera.getRotationX(), camera.getRotationY(), - camera.getRotationZ(), camera.getOffsetX(), camera.getOffsetY() }; - camera.setZoom(ponderCamZoom); - camera.setRotationX(ponderCamRotX); - camera.setRotationY(ponderCamRotY); - camera.setRotationZ(ponderCamRotZ); - camera.setOffsetX(ponderCamOffX); - camera.setOffsetY(ponderCamOffY); + private float getLogicalCameraZoom() { + return ponderSceneData != null ? ponderCamZoom : camera.getZoom(); + } + + private float getLogicalCameraRotationX() { + return ponderSceneData != null ? ponderCamRotX : camera.getRotationX(); + } + + private float getLogicalCameraRotationY() { + return ponderSceneData != null ? ponderCamRotY : camera.getRotationY(); + } + + private float getLogicalCameraRotationZ() { + return ponderSceneData != null ? ponderCamRotZ : camera.getRotationZ(); + } + + private float getLogicalCameraOffsetX() { + return ponderSceneData != null ? ponderCamOffX : camera.getOffsetX(); + } + + private float getLogicalCameraOffsetY() { + return ponderSceneData != null ? ponderCamOffY : camera.getOffsetY(); + } + + private float[] captureCameraState() { + return new float[] { camera.getZoom(), camera.getRotationX(), camera.getRotationY(), camera.getRotationZ(), + camera.getOffsetX(), camera.getOffsetY() }; + } + + private void applyCapturedCameraState(float[] state) { + camera.setZoom(state[0]); + camera.setRotationX(state[1]); + camera.setRotationY(state[2]); + camera.setRotationZ(state[3]); + camera.setOffsetX(state[4]); + camera.setOffsetY(state[5]); + } + + private float[] applyVisualCameraState() { + updateVisualCameraState(); + float[] saved = captureCameraState(); + camera.setZoom(visualCamZoom.value()); + camera.setRotationX(visualCamRotX.value()); + camera.setRotationY(visualCamRotY.value()); + camera.setRotationZ(visualCamRotZ.value()); + camera.setOffsetX(visualCamOffX.value()); + camera.setOffsetY(visualCamOffY.value()); return saved; } - private void restoreCameraFromProjection(float[] saved) { - camera.setZoom(saved[0]); - camera.setRotationX(saved[1]); - camera.setRotationY(saved[2]); - camera.setRotationZ(saved[3]); - camera.setOffsetX(saved[4]); - camera.setOffsetY(saved[5]); + private boolean isDraggingCamera() { + return dragButton >= 0 && !isPonderPlaying(); } public static boolean isPanButton(int button) { @@ -4087,8 +4113,20 @@ public void drag(int mouseX, int mouseY) { } public void pollDrag() { - // Camera movement is driven by explicit mouse drag events. Polling raw Mouse.getDX/Y here - // also consumes tiny deltas left by a plain click, which makes a click rotate the scene. + if (dragButton < 0 || isPonderPlaying()) { + return; + } + Minecraft minecraft = Minecraft.getMinecraft(); + if (minecraft.currentScreen == null || minecraft.displayWidth <= 0 || minecraft.displayHeight <= 0) { + return; + } + int mouseX = Mouse.getX() * minecraft.currentScreen.width / minecraft.displayWidth; + int mouseY = minecraft.currentScreen.height - Mouse.getY() * minecraft.currentScreen.height / minecraft.displayHeight + - 1; + if (mouseX == dragLastX && mouseY == dragLastY) { + return; + } + drag(mouseX, mouseY); } private void applyCameraDrag(float dx, float dy) { @@ -4113,6 +4151,7 @@ private void applyCameraDrag(float dx, float dy) { camera.setRotationX(camera.getRotationX() + dy * DRAG_ROTATE_SENSITIVITY); } } + updateVisualCameraState(); } public void endDrag() { @@ -4158,6 +4197,7 @@ public void scroll(int mouseX, int mouseY, int dwheel) { if (dwheel > 0) z *= WHEEL_ZOOM_STEP; else z /= WHEEL_ZOOM_STEP; ponderCamZoom = Math.clamp(z, MIN_ZOOM, MAX_ZOOM); + updateVisualCameraState(); return; } float z = camera.getZoom(); @@ -4166,6 +4206,7 @@ public void scroll(int mouseX, int mouseY, int dwheel) { if (z < MIN_ZOOM) z = MIN_ZOOM; if (z > MAX_ZOOM) z = MAX_ZOOM; camera.setZoom(z); + updateVisualCameraState(); } public void scroll(int dwheel) { @@ -4177,6 +4218,7 @@ public void scroll(int dwheel) { if (dwheel > 0) z *= WHEEL_ZOOM_STEP; else z /= WHEEL_ZOOM_STEP; ponderCamZoom = Math.clamp(z, MIN_ZOOM, MAX_ZOOM); + updateVisualCameraState(); return; } float z = camera.getZoom(); @@ -4185,6 +4227,7 @@ public void scroll(int dwheel) { if (z < MIN_ZOOM) z = MIN_ZOOM; if (z > MAX_ZOOM) z = MAX_ZOOM; camera.setZoom(z); + updateVisualCameraState(); } public List getPonderActiveAnnotationsForTesting() { From 8c5707f27a5f37378d46c2691819b44325de730a Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:50:57 +0800 Subject: [PATCH 127/136] bugfix --- .../compiler/tags/DetailsTagCompiler.java | 15 ++----- .../guide/document/block/LytCodeBlock.java | 39 +++++++++---------- .../guide/document/block/LytSizeBox.java | 39 +++++++++++-------- 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java index bbb35e77..98bf733c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java @@ -9,7 +9,6 @@ import com.hfstudio.guidenh.guide.document.block.LytDetailsBlock; import com.hfstudio.guidenh.guide.document.block.LytSizeBox; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; -import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxFlowElement; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; public class DetailsTagCompiler extends BlockTagCompiler { @@ -27,11 +26,9 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl details.setOpen(el.hasAttribute("open")); details.setFallbackSummaryText("Details"); - String detailsBodySource = compiler.getBlockTagChildrenSource(el); - List children = detailsBodySource != null ? compiler.reparseBlockTagChildren(el) - : el.children(); + List children = el.children(); int bodyStart = 0; - if (!children.isEmpty() && children.getFirst() instanceof MdxJsxFlowElement summaryElement + if (!children.isEmpty() && children.getFirst() instanceof MdxJsxElementFields summaryElement && "summary".equals(summaryElement.name())) { details.getSummaryBox() .clearContent(); @@ -45,13 +42,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (bodyStart < children.size()) { List bodyChildren = children.subList(bodyStart, children.size()); - if (detailsBodySource != null) { - compiler.withSourceContext( - detailsBodySource, - () -> compiler.compileBlockContextInSourceContext(bodyChildren, details.getContentBox())); - } else { - compiler.compileBlockContextInSourceContext(bodyChildren, details.getContentBox()); - } + compiler.compileBlockContextInSourceContext(bodyChildren, details.getContentBox()); } Integer width = readOptionalInt(el, "width"); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java index 5ab4fed2..13d7e97a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java @@ -282,18 +282,7 @@ public void render(RenderContext context) { LytRect bodyViewport = getBodyViewportBounds(); context.pushLocalScissor(bodyViewport); try { - int offsetY = visualBodyScrollOffsetY.rounded() - bodyScrollOffsetY; - if (offsetY != 0) { - org.lwjgl.opengl.GL11.glPushMatrix(); - try { - org.lwjgl.opengl.GL11.glTranslatef(0f, -offsetY, 0f); - body.render(context); - } finally { - org.lwjgl.opengl.GL11.glPopMatrix(); - } - } else { - body.render(context); - } + renderBodyWithVisualOffset(context); } finally { context.popScissor(); } @@ -356,24 +345,18 @@ private void renderScrollbar(RenderContext context) { } private LytRect getBodyViewportBounds() { - LytRect own = getBounds(); LytRect toolbarBounds = toolbar.getBounds(); int viewportY = toolbarBounds.bottom() + getGap(); int viewportHeight = Math.max(0, bodyViewportHeight); - int viewportWidth = own.width(); - if (getMaxBodyScroll() > 0) { - viewportWidth = Math.max(1, viewportWidth - SCROLLBAR_WIDTH - 4); - } - return new LytRect(own.x(), viewportY, viewportWidth, viewportHeight); + return new LytRect(body.getBounds().x(), viewportY, body.getBounds().width(), viewportHeight); } private LytRect getScrollbarTrackBounds() { if (getMaxBodyScroll() <= 0) { return LytRect.empty(); } - LytRect own = getBounds(); LytRect viewport = getBodyViewportBounds(); - int x = own.right() - SCROLLBAR_WIDTH - 1; + int x = viewport.right() + 4; return new LytRect(x, viewport.y(), SCROLLBAR_WIDTH, viewport.height()); } @@ -414,7 +397,21 @@ private void updateBodyPosition() { 0, bodyViewportY - bodyScrollOffsetY - body.getBounds() - .y()); + .y()); + } + } + + private void renderBodyWithVisualOffset(RenderContext context) { + int renderDeltaY = bodyScrollOffsetY - visualBodyScrollOffsetY.rounded(); + if (renderDeltaY == 0) { + body.render(context); + return; + } + body.moveLayoutPos(0, renderDeltaY); + try { + body.render(context); + } finally { + body.moveLayoutPos(0, -renderDeltaY); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java index 34cdf2bf..25edd465 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java @@ -91,22 +91,7 @@ public void render(RenderContext context) { LytRect viewportBounds = getViewportBounds(); context.pushLocalScissor(viewportBounds); try { - int offsetY = visualScrollOffsetY.rounded() - appliedScrollOffsetY; - if (offsetY != 0) { - org.lwjgl.opengl.GL11.glPushMatrix(); - try { - org.lwjgl.opengl.GL11.glTranslatef(0f, -offsetY, 0f); - for (LytBlock child : children) { - child.render(context); - } - } finally { - org.lwjgl.opengl.GL11.glPopMatrix(); - } - } else { - for (LytBlock child : children) { - child.render(context); - } - } + renderChildrenWithVisualOffset(context); } finally { context.popScissor(); } @@ -288,4 +273,26 @@ private void snapVisualScrollToTarget() { private void updateVisualScroll() { visualScrollOffsetY.updateTowards(scrollOffsetY, 28f, 0.25f, 0.01f, Math.max(128f, viewportHeight * 2f)); } + + private void renderChildrenWithVisualOffset(RenderContext context) { + int renderDeltaY = appliedScrollOffsetY - visualScrollOffsetY.rounded(); + if (renderDeltaY != 0) { + moveChildrenLayoutY(renderDeltaY); + } + try { + for (LytBlock child : children) { + child.render(context); + } + } finally { + if (renderDeltaY != 0) { + moveChildrenLayoutY(-renderDeltaY); + } + } + } + + private void moveChildrenLayoutY(int deltaY) { + for (LytBlock child : children) { + child.moveLayoutPos(0, deltaY); + } + } } From 6eb774f09af5cccb30246baa53060a3aa7d23d38 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:44:12 +0800 Subject: [PATCH 128/136] sa --- .../bridge/GuideNhRuntimeBridgeServer.java | 12 ++++-------- .../bridge/transport/RuntimeBridgeConnection.java | 7 +++---- .../guide/document/block/LytCodeBlock.java | 15 +++++++++++---- .../document/block/LytMermaidMindmapCanvas.java | 8 ++++++-- .../guidenh/guide/document/block/LytSizeBox.java | 13 ++++++------- .../editor/gui/SceneEditorMultilineTextArea.java | 10 +++------- .../guide/GuideScreenEditorContextMenu.java | 14 +++++++------- .../guidenh/guide/render/GuidePageTexture.java | 1 + .../guidenh/guide/scene/LytGuidebookScene.java | 10 +++++++--- .../integration/gregtech/GregTechHelpers.java | 1 + .../RailcraftPreviewPrepareContributor.java | 5 ++--- .../structurelib/StructureLibRuntimeFacade.java | 7 +++---- .../StructureLibSceneImportService.java | 3 ++- .../MixinTessellatorSceneExportCapture.java | 1 - 14 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java b/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java index 42cf60af..77ddf64f 100644 --- a/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java +++ b/src/main/java/com/hfstudio/guidenh/bridge/GuideNhRuntimeBridgeServer.java @@ -63,17 +63,13 @@ public void start() { return; } try { - GuideDebugLog.infoAlways( - "Binding GuideNH runtime bridge server to {}:{}", - settings.getHost(), - settings.getPort()); + GuideDebugLog + .infoAlways("Binding GuideNH runtime bridge server to {}:{}", settings.getHost(), settings.getPort()); serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(settings.getHost(), settings.getPort())); executor.execute(this::acceptConnections); - GuideDebugLog.infoAlways( - "GuideNH runtime bridge started at ws://{}:{}", - settings.getHost(), - settings.getPort()); + GuideDebugLog + .infoAlways("GuideNH runtime bridge started at ws://{}:{}", settings.getHost(), settings.getPort()); } catch (IOException e) { running.set(false); closeServerSocket(); diff --git a/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java b/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java index 134fb17f..f98f6dc4 100644 --- a/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java +++ b/src/main/java/com/hfstudio/guidenh/bridge/transport/RuntimeBridgeConnection.java @@ -60,16 +60,15 @@ public RuntimeBridgeConnection(Socket socket, BridgeMessageCodec messageCodec, public void run() { try { socket.setSoTimeout(SOCKET_TIMEOUT_MILLIS); - GuideDebugLog.infoAlways("GuideNH runtime bridge waiting for WebSocket handshake from {}", describeRemote()); + GuideDebugLog + .infoAlways("GuideNH runtime bridge waiting for WebSocket handshake from {}", describeRemote()); if (!new WebSocketHandshake().accept(socket.getInputStream(), socket.getOutputStream())) { GuideDebugLog.warnAlways( "GuideNH runtime bridge rejected invalid WebSocket handshake from {}", describeRemote()); return; } - GuideDebugLog.infoAlways( - "GuideNH runtime bridge WebSocket handshake completed for {}", - describeRemote()); + GuideDebugLog.infoAlways("GuideNH runtime bridge WebSocket handshake completed for {}", describeRemote()); readFrames(); } catch (SocketTimeoutException ignored) { GuideDebugLog.warnAlways("GuideNH runtime bridge connection timed out for {}", describeRemote()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java index 13d7e97a..549a368c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java @@ -15,8 +15,8 @@ import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlightTheme; import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeHighlighter; import com.hfstudio.guidenh.guide.internal.markdown.highlight.CodeTokenType; -import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.style.BorderStyle; @@ -348,7 +348,13 @@ private LytRect getBodyViewportBounds() { LytRect toolbarBounds = toolbar.getBounds(); int viewportY = toolbarBounds.bottom() + getGap(); int viewportHeight = Math.max(0, bodyViewportHeight); - return new LytRect(body.getBounds().x(), viewportY, body.getBounds().width(), viewportHeight); + return new LytRect( + body.getBounds() + .x(), + viewportY, + body.getBounds() + .width(), + viewportHeight); } private LytRect getScrollbarTrackBounds() { @@ -397,7 +403,7 @@ private void updateBodyPosition() { 0, bodyViewportY - bodyScrollOffsetY - body.getBounds() - .y()); + .y()); } } @@ -434,6 +440,7 @@ private void snapVisualScrollToTarget() { } private void updateVisualScroll() { - visualBodyScrollOffsetY.updateTowards(bodyScrollOffsetY, 28f, 0.25f, 0.01f, Math.max(128f, bodyViewportHeight * 2f)); + visualBodyScrollOffsetY + .updateTowards(bodyScrollOffsetY, 28f, 0.25f, 0.01f, Math.max(128f, bodyViewportHeight * 2f)); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index 95c2b803..ee10a59e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -276,8 +276,12 @@ public boolean scroll(int documentX, int documentY, int wheelDelta) { .x() + (documentX - viewport.x() - previousOffsetX) / Math.max(previousZoom, 0.0001f); float anchorY = layout.contentBounds() .y() + (documentY - viewport.y() - previousOffsetY) / Math.max(previousZoom, 0.0001f); - contentOffsetX = Math.round((documentX - viewport.x()) - (anchorX - layout.contentBounds().x()) * zoom); - contentOffsetY = Math.round((documentY - viewport.y()) - (anchorY - layout.contentBounds().y()) * zoom); + contentOffsetX = Math.round( + (documentX - viewport.x()) - (anchorX - layout.contentBounds() + .x()) * zoom); + contentOffsetY = Math.round( + (documentY - viewport.y()) - (anchorY - layout.contentBounds() + .y()) * zoom); clampOffsets(); return true; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java index 25edd465..f7172839 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytSizeBox.java @@ -256,13 +256,12 @@ private LytRect getScrollbarThumbBounds() { return LytRect.empty(); } - SceneEditorVerticalScrollbar.Thumb thumb = SceneEditorVerticalScrollbar - .computeThumb( - trackBounds.y(), - trackBounds.height(), - contentHeight, - viewportHeight, - visualScrollOffsetY.rounded()); + SceneEditorVerticalScrollbar.Thumb thumb = SceneEditorVerticalScrollbar.computeThumb( + trackBounds.y(), + trackBounds.height(), + contentHeight, + viewportHeight, + visualScrollOffsetY.rounded()); return new LytRect(trackBounds.x(), thumb.start(), trackBounds.width(), thumb.size()); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java index 30589395..12106bd3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/gui/SceneEditorMultilineTextArea.java @@ -1270,7 +1270,7 @@ public int getCursorPixelX() { List lines = layoutCache.getVisualLines(); if (lines.isEmpty()) return PADDING; int lineIdx = getVisualLineIndex(selectionModel.getCursorIndex()); - return PADDING + getCursorPixelOnLine(selectionModel.getCursorIndex(), lines.get(lineIdx)) + return PADDING + getCursorPixelOnLine(selectionModel.getCursorIndex(), lines.get(lineIdx)) - visualHorizontalOffsetPixels.rounded(); } @@ -1403,12 +1403,8 @@ private void updateVisualOffsets() { 0.25f, 0.01f, Math.max(160f, getContentClipHeight() * 2f)); - visualHorizontalOffsetPixels.updateTowards( - horizontalOffsetPixels, - 30f, - 0.25f, - 0.01f, - Math.max(160f, textViewportWidth * 2f)); + visualHorizontalOffsetPixels + .updateTowards(horizontalOffsetPixels, 30f, 0.25f, 0.01f, Math.max(160f, textViewportWidth * 2f)); } private int clamp(int value, int min, int max) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java index 80dac5d1..88bb281b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/guide/GuideScreenEditorContextMenu.java @@ -117,7 +117,8 @@ public void open(int mouseX, int mouseY, int viewportWidth, int viewportHeight, panes.add(new MenuPane(entries, rootRect.x(), rootRect.y(), rootWidth, rootHeight)); open = true; draggingScrollbarPaneIndex = -1; - panes.getFirst().snapVisualScrollToTarget(); + panes.getFirst() + .snapVisualScrollToTarget(); update(mouseX, mouseY, viewportWidth, viewportHeight, fontRenderer); } @@ -482,12 +483,11 @@ private boolean startScrollbarDrag(int mouseX, int mouseY) { pane.entries, pane.scrollY)) { draggingScrollbarPaneIndex = i; - scrollbarGrabOffset = mouseY - - scrollbarThumbY( - pane.y, - pane.height, - computeMenuContentHeight(pane.entries), - pane.visualScrollY.rounded()); + scrollbarGrabOffset = mouseY - scrollbarThumbY( + pane.y, + pane.height, + computeMenuContentHeight(pane.entries), + pane.visualScrollY.rounded()); return true; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java b/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java index 25ca2641..f64e0aa0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/GuidePageTexture.java @@ -22,6 +22,7 @@ import cpw.mods.fml.relauncher.ReflectionHelper; public class GuidePageTexture { + public static final GuidePageTexture MISSING = new GuidePageTexture(null, 0, 0, null); private static final String TEXTURE_OBJECTS_FIELD = "mapTextureObjects"; private static final String TEXTURE_OBJECTS_SRG_FIELD = "field_110585_a"; diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 050dc2d1..18259db4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -3993,8 +3993,11 @@ private void updateVisualCameraState() { float targetRotZ = getLogicalCameraRotationZ(); float targetOffX = getLogicalCameraOffsetX(); float targetOffY = getLogicalCameraOffsetY(); - if (visualCamZoom.value() == 0f && visualCamRotX.value() == 0f && visualCamRotY.value() == 0f && visualCamRotZ.value() == 0f - && visualCamOffX.value() == 0f && visualCamOffY.value() == 0f) { + if (visualCamZoom.value() == 0f && visualCamRotX.value() == 0f + && visualCamRotY.value() == 0f + && visualCamRotZ.value() == 0f + && visualCamOffX.value() == 0f + && visualCamOffY.value() == 0f) { visualCamZoom.snapTo(targetZoom); visualCamRotX.snapTo(targetRotX); visualCamRotY.snapTo(targetRotY); @@ -4121,7 +4124,8 @@ public void pollDrag() { return; } int mouseX = Mouse.getX() * minecraft.currentScreen.width / minecraft.displayWidth; - int mouseY = minecraft.currentScreen.height - Mouse.getY() * minecraft.currentScreen.height / minecraft.displayHeight + int mouseY = minecraft.currentScreen.height + - Mouse.getY() * minecraft.currentScreen.height / minecraft.displayHeight - 1; if (mouseX == dragLastX && mouseY == dragLastY) { return; diff --git a/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java b/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java index 6d44a529..089400ab 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java +++ b/src/main/java/com/hfstudio/guidenh/integration/gregtech/GregTechHelpers.java @@ -36,6 +36,7 @@ import gregtech.common.misc.GTStructureChannels; public class GregTechHelpers { + public static final Set LOGGED_KEYS = Collections.synchronizedSet(new HashSet<>()); public static ItemStack applyOreDictUnification(ItemStack stack) { diff --git a/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java b/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java index 4ebc0d2c..36f5e278 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java +++ b/src/main/java/com/hfstudio/guidenh/integration/railcraft/RailcraftPreviewPrepareContributor.java @@ -24,9 +24,8 @@ public void prepare(GuidebookLevel level) { } catch (Throwable t) { if (!invokeFailureLogged) { invokeFailureLogged = true; - GuideDebugLog.warn( - "Railcraft multiblock preview preparation failed; multiblock textures may be inactive", - t); + GuideDebugLog + .warn("Railcraft multiblock preview preparation failed; multiblock textures may be inactive", t); } } } diff --git a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java index 45d64d69..fa63dea2 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java +++ b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibRuntimeFacade.java @@ -46,6 +46,7 @@ import cpw.mods.fml.common.registry.GameRegistry; public class StructureLibRuntimeFacade implements StructureLibFacade { + public static final int CONTROLLER_X = 0; public static final int CONTROLLER_Y = 64; public static final int CONTROLLER_Z = 0; @@ -482,10 +483,8 @@ public static PreparedPreviewWorld preparePreviewWorld(StructureLibImportRequest .add("StructureLib instrumentation was already active; preview tooltip metadata may be incomplete."); } catch (Throwable t) { warnings.add("StructureLib instrumentation setup failed; preview tooltip metadata may be incomplete."); - GuideDebugLog.warn( - "Failed to enable StructureLib instrumentation for controller {}", - request.getController(), - t); + GuideDebugLog + .warn("Failed to enable StructureLib instrumentation for controller {}", request.getController(), t); } try { diff --git a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java index 63b68e85..023fd5b9 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java +++ b/src/main/java/com/hfstudio/guidenh/integration/structurelib/StructureLibSceneImportService.java @@ -101,7 +101,8 @@ public static StructureLibFacade createDefaultFacade() { try { return createRuntimeFacade(); } catch (Throwable t) { - GuideDebugLog.warn("Failed to initialize StructureLib runtime facade, falling back to unavailable facade", t); + GuideDebugLog + .warn("Failed to initialize StructureLib runtime facade, falling back to unavailable facade", t); return new StructureLibUnavailableFacade(); } } diff --git a/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java index b2f676ae..98a9e2d3 100644 --- a/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java +++ b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/MixinTessellatorSceneExportCapture.java @@ -4,7 +4,6 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; From 9de425079aed02a02ba644b64c96693a69f2dda9 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:23:38 +0800 Subject: [PATCH 129/136] bugfix --- .../guidenh/guide/compiler/PageCompiler.java | 95 +----- .../tags/DetailsContentExtractor.java | 104 ++++++ .../compiler/tags/DetailsTagCompiler.java | 60 ++-- .../guide/document/block/LytDetailsBlock.java | 306 ++++++++++++++++-- .../site/GuideSiteMdxTagRenderer.java | 132 ++++++-- wiki/Tags-Reference-zh-CN.md | 2 +- wiki/Tags-Reference.md | 1 + .../assets/guidenh/guidenh/_en_us/markdown.md | 3 +- .../assets/guidenh/guidenh/_zh_cn/markdown.md | 2 +- 9 files changed, 561 insertions(+), 144 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsContentExtractor.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index 49f721e1..adb9ffc9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -26,6 +26,7 @@ import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.DetailsContentExtractor; import com.hfstudio.guidenh.guide.document.block.LatexRenderOptions; import com.hfstudio.guidenh.guide.document.block.LatexVerticalAlign; import com.hfstudio.guidenh.guide.document.block.LytBlock; @@ -529,6 +530,21 @@ public void compileInlineMarkdown(String source, LytFlowParent layoutParent) { layoutParent); } + public void compileBlockMarkdown(String source, LytBlockContainer layoutParent) { + if (source == null || source.isEmpty()) { + return; + } + ParsedGuidePage parsed = parse(sourcePack, language, pageId, source); + Map previousDefinitions = new HashMap<>(definitions); + definitions.putAll(GuideMarkdownDefinitions.collect(parsed.getAstRoot())); + try { + withSourceSlice(source, () -> compileBlockContext(parsed.getAstRoot(), layoutParent)); + } finally { + definitions.clear(); + definitions.putAll(previousDefinitions); + } + } + public void compileInlineFragment(Collection children, LytFlowParent layoutParent) { for (MdAstAnyContent child : children) { if (child instanceof MdxJsxFlowElement el && "p".equals(el.name())) { @@ -953,7 +969,7 @@ private String stripOptionalQuotes(String value) { return null; } - return new BlockTagChildSource(dedentBlockTagBody(body)); + return new BlockTagChildSource(DetailsContentExtractor.dedent(body)); } private BlockTagChildrenCacheEntry getBlockTagChildrenCacheEntry(MdxJsxElementFields element) { @@ -974,83 +990,6 @@ private BlockTagChildrenCacheEntry getBlockTagChildrenCacheEntry(MdxJsxElementFi return cachedEntry; } - private String dedentBlockTagBody(String body) { - String normalized = normalizeLineEndings(body); - if (normalized.isEmpty()) { - return normalized; - } - - List lines = GuideStringLines.splitLines(normalized); - int firstContentLine = 0; - while (firstContentLine < lines.size() && lines.get(firstContentLine) - .trim() - .isEmpty()) { - firstContentLine++; - } - - int minIndent = Integer.MAX_VALUE; - for (int i = firstContentLine; i < lines.size(); i++) { - String line = lines.get(i); - if (line.trim() - .isEmpty()) { - continue; - } - minIndent = Math.min(minIndent, leadingWhitespaceWidth(line)); - } - if (minIndent == Integer.MAX_VALUE) { - minIndent = 0; - } - - StringBuilder result = new StringBuilder(normalized.length()); - for (int i = firstContentLine; i < lines.size(); i++) { - if (i > firstContentLine) { - result.append('\n'); - } - result.append(removeLeadingWhitespace(lines.get(i), minIndent)); - } - - while (result.length() > 0 && result.charAt(result.length() - 1) == '\n') { - result.setLength(result.length() - 1); - } - if (Objects.equals(body, normalized) && body.endsWith("\n")) { - result.append('\n'); - } - return result.toString(); - } - - private int leadingWhitespaceWidth(String line) { - int width = 0; - for (int i = 0; i < line.length(); i++) { - char ch = line.charAt(i); - if (ch == ' ') { - width++; - } else if (ch == '\t') { - width += 4; - } else { - break; - } - } - return width; - } - - private String removeLeadingWhitespace(String line, int widthToRemove) { - int index = 0; - int removed = 0; - while (index < line.length() && removed < widthToRemove) { - char ch = line.charAt(index); - if (ch == ' ') { - removed++; - index++; - } else if (ch == '\t') { - removed += 4; - index++; - } else { - break; - } - } - return line.substring(index); - } - public LytBlock createErrorBlock(String text, UnistNode child) { var paragraph = new LytParagraph(); paragraph.append(createErrorFlowContent(text, child)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsContentExtractor.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsContentExtractor.java new file mode 100644 index 00000000..3847a48c --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsContentExtractor.java @@ -0,0 +1,104 @@ +package com.hfstudio.guidenh.guide.compiler.tags; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; + +import com.hfstudio.guidenh.guide.internal.util.GuideStringLines; + +public class DetailsContentExtractor { + + private static final Pattern SUMMARY_PATTERN = Pattern + .compile("(?is)^\\s*<\\s*summary\\b[^>]*>(.*?)\\s*(?:\\R)?"); + + private DetailsContentExtractor() {} + + public static DetailsContent extract(@Nullable String source) { + String body = source != null ? source : ""; + Matcher matcher = SUMMARY_PATTERN.matcher(body); + if (!matcher.find()) { + return new DetailsContent(null, body); + } + return new DetailsContent(matcher.group(1), body.substring(matcher.end())); + } + + public static String dedent(String body) { + String normalized = GuideStringLines.normalizeLineEndings(body != null ? body : ""); + if (normalized.isEmpty()) { + return normalized; + } + + var lines = GuideStringLines.splitLines(normalized); + int firstContentLine = 0; + while (firstContentLine < lines.size() && lines.get(firstContentLine) + .trim() + .isEmpty()) { + firstContentLine++; + } + + int minIndent = Integer.MAX_VALUE; + for (int i = firstContentLine; i < lines.size(); i++) { + String line = lines.get(i); + if (line.trim() + .isEmpty()) { + continue; + } + minIndent = Math.min(minIndent, leadingWhitespaceWidth(line)); + } + if (minIndent == Integer.MAX_VALUE) { + minIndent = 0; + } + + StringBuilder result = new StringBuilder(normalized.length()); + for (int i = firstContentLine; i < lines.size(); i++) { + if (i > firstContentLine) { + result.append('\n'); + } + result.append(removeLeadingWhitespace(lines.get(i), minIndent)); + } + + while (result.length() > 0 && result.charAt(result.length() - 1) == '\n') { + result.setLength(result.length() - 1); + } + if (body != null && body.equals(normalized) && body.endsWith("\n")) { + result.append('\n'); + } + return result.toString(); + } + + private static int leadingWhitespaceWidth(String line) { + int width = 0; + for (int i = 0; i < line.length(); i++) { + char ch = line.charAt(i); + if (ch == ' ') { + width++; + } else if (ch == '\t') { + width += 4; + } else { + break; + } + } + return width; + } + + private static String removeLeadingWhitespace(String line, int widthToRemove) { + int index = 0; + int removed = 0; + while (index < line.length() && removed < widthToRemove) { + char ch = line.charAt(index); + if (ch == ' ') { + removed++; + index++; + } else if (ch == '\t') { + removed += 4; + index++; + } else { + break; + } + } + return line.substring(index); + } + + public record DetailsContent(@Nullable String summaryMarkdown, String bodyMarkdown) {} +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java index 98bf733c..cb2ce387 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/DetailsTagCompiler.java @@ -5,9 +5,9 @@ import java.util.Set; import com.hfstudio.guidenh.guide.compiler.PageCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.DetailsContentExtractor.DetailsContent; import com.hfstudio.guidenh.guide.document.block.LytBlockContainer; import com.hfstudio.guidenh.guide.document.block.LytDetailsBlock; -import com.hfstudio.guidenh.guide.document.block.LytSizeBox; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; import com.hfstudio.guidenh.libs.mdast.model.MdAstAnyContent; @@ -26,10 +26,39 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl details.setOpen(el.hasAttribute("open")); details.setFallbackSummaryText("Details"); - List children = el.children(); + String childrenSource = compiler.getBlockTagChildrenSource(el); + if (childrenSource != null) { + DetailsContent extracted = DetailsContentExtractor.extract(childrenSource); + if (extracted.summaryMarkdown() != null) { + details.getSummaryBox() + .clearContent(); + compiler.compileInlineMarkdown(extracted.summaryMarkdown(), details.getSummaryBox()); + if (details.getSummaryBox() + .isEmpty()) { + details.setFallbackSummaryText("Details"); + } + } + compiler.compileBlockMarkdown(extracted.bodyMarkdown(), details.getContentBox()); + } else { + compileAstChildren(compiler, details, el.children()); + } + + Integer width = readOptionalInt(el, "width"); + Integer height = readOptionalInt(el, "height"); + if (width != null) { + details.setPreferredWidth(width); + } + if (height != null) { + details.setPreferredContentHeight(height); + } + parent.append(details); + } + + private void compileAstChildren(PageCompiler compiler, LytDetailsBlock details, + List children) { int bodyStart = 0; - if (!children.isEmpty() && children.getFirst() instanceof MdxJsxElementFields summaryElement - && "summary".equals(summaryElement.name())) { + MdxJsxElementFields summaryElement = findLeadingSummary(children); + if (summaryElement != null) { details.getSummaryBox() .clearContent(); compiler.compileInlineFragment(summaryElement.children(), details.getSummaryBox()); @@ -44,22 +73,17 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl List bodyChildren = children.subList(bodyStart, children.size()); compiler.compileBlockContextInSourceContext(bodyChildren, details.getContentBox()); } + } - Integer width = readOptionalInt(el, "width"); - Integer height = readOptionalInt(el, "height"); - if (width != null || height != null) { - LytSizeBox sizeBox = new LytSizeBox(); - if (width != null) { - sizeBox.setPreferredWidth(width); - } - if (height != null) { - sizeBox.setPreferredHeight(height); - } - sizeBox.append(details); - parent.append(sizeBox); - return; + private MdxJsxElementFields findLeadingSummary(List children) { + if (children.isEmpty()) { + return null; } - parent.append(details); + if (children.getFirst() instanceof MdxJsxElementFields summaryElement + && "summary".equals(summaryElement.name())) { + return summaryElement; + } + return null; } private Integer readOptionalInt(MdxJsxElementFields el, String name) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java index cddc0535..658b0800 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java @@ -8,39 +8,56 @@ import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.document.LytRect; +import com.hfstudio.guidenh.guide.document.interaction.DocumentDragTarget; import com.hfstudio.guidenh.guide.document.interaction.GuideTooltip; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; +import com.hfstudio.guidenh.guide.internal.editor.gui.SceneEditorVerticalScrollbar; +import com.hfstudio.guidenh.guide.internal.util.SmoothFloatState; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.RenderContext; import com.hfstudio.guidenh.guide.style.BorderStyle; import com.hfstudio.guidenh.guide.ui.GuideUiHost; -public class LytDetailsBlock extends LytBlock implements InteractiveElement, LytBlockContainer { +public class LytDetailsBlock extends LytBlock implements InteractiveElement, LytBlockContainer, DocumentDragTarget { private static final ConstantColor SUMMARY_COLOR = new ConstantColor(0xFFE2E6ED); private static final String SUMMARY_OPEN_MARKER = "v"; private static final String SUMMARY_CLOSED_MARKER = ">"; private static final String DEFAULT_SUMMARY_TEXT = "Details"; + private static final int PADDING = 6; + private static final int GAP = 4; + private static final int BORDER_WIDTH = 1; + private static final int SCROLLBAR_WIDTH = 5; + private static final int SCROLLBAR_GAP = 4; + private static final int MIN_SCROLLBAR_THUMB = 14; + private static final int MIN_WHEEL_STEP = 16; + private static final BorderStyle DETAILS_BORDER = new BorderStyle(SymbolicColor.TABLE_BORDER, BORDER_WIDTH); - private final LytVBox root = new LytVBox(); private final LytHBox summaryRow = new LytHBox(); private final LytParagraph summaryMarker = new LytParagraph(); private final LytParagraph summaryContent = new LytParagraph(); private final LytVBox content = new LytVBox(); + private final BorderRenderer borderRenderer = new BorderRenderer(); + private final SmoothFloatState visualContentScrollOffsetY = new SmoothFloatState(); private boolean open; @Nullable private String fallbackSummaryText; + private int preferredWidth; + private int preferredContentHeight; + private int contentHeight; + private int contentViewportX; + private int contentViewportY; + private int contentViewportWidth; + private int contentViewportHeight; + private int contentScrollOffsetY; + private boolean draggingContent; + private int dragLastDocumentY; + private boolean draggingScrollbar; + private int scrollbarGrabOffsetY; public LytDetailsBlock() { - root.parent = this; - root.setPadding(6); - root.setGap(4); - root.setFullWidth(true); - root.setBackgroundColor(SymbolicColor.BLOCKQUOTE_BACKGROUND); - root.setBorder(new BorderStyle(SymbolicColor.TABLE_BORDER, 1)); - - summaryRow.parent = root; + summaryRow.parent = this; summaryRow.setGap(4); summaryRow.setWrap(false); summaryRow.setFullWidth(true); @@ -58,18 +75,33 @@ public LytDetailsBlock() { style -> style.bold(true) .color(SUMMARY_COLOR)); - content.parent = root; + content.parent = this; content.setGap(4); content.setFullWidth(true); summaryRow.append(summaryMarker); summaryRow.append(summaryContent); - root.append(summaryRow); - root.append(content); syncSummaryMarker(); syncContentVisibility(); } + public int getPreferredWidth() { + return preferredWidth; + } + + public void setPreferredWidth(int preferredWidth) { + this.preferredWidth = Math.max(0, preferredWidth); + setFullWidth(this.preferredWidth <= 0); + } + + public int getPreferredContentHeight() { + return preferredContentHeight; + } + + public void setPreferredContentHeight(int preferredContentHeight) { + this.preferredContentHeight = Math.max(0, preferredContentHeight); + } + public LytParagraph getSummaryBox() { return summaryContent; } @@ -116,11 +148,6 @@ private void syncSummaryFallback() { private void syncContentVisibility() { syncSummaryFallback(); - root.clearContent(); - root.append(summaryRow); - if (open) { - root.append(content); - } } @Override @@ -135,27 +162,81 @@ public void removeChild(LytNode node) { @Override public void replaceChild(LytNode oldChild, LytNode newChild) { - root.replaceChild(oldChild, newChild); + content.replaceChild(oldChild, newChild); } @Override public List getChildren() { - return root.getChildren(); + return open ? List.of(summaryRow, content) : List.of(summaryRow); } @Override protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { - return root.layout(context, x, y, availableWidth); + int safeWidth = preferredWidth > 0 ? Math.max(1, Math.min(availableWidth, preferredWidth)) + : Math.max(1, availableWidth); + int innerX = x + PADDING + BORDER_WIDTH; + int innerY = y + PADDING + BORDER_WIDTH; + int innerWidth = Math.max(1, safeWidth - (PADDING + BORDER_WIDTH) * 2); + + LytRect summaryBounds = summaryRow.layout(context, innerX, innerY, innerWidth); + int totalHeight = PADDING + BORDER_WIDTH + summaryBounds.height() + PADDING + BORDER_WIDTH; + + contentHeight = 0; + contentViewportX = innerX; + contentViewportY = summaryBounds.bottom() + GAP; + contentViewportWidth = innerWidth; + contentViewportHeight = 0; + + if (open) { + LytRect measuredContent = content.layout(context, contentViewportX, contentViewportY, contentViewportWidth); + contentHeight = measuredContent.height(); + contentViewportHeight = preferredContentHeight > 0 ? preferredContentHeight : contentHeight; + if (preferredContentHeight > 0 && contentHeight > contentViewportHeight) { + contentViewportWidth = Math.max(1, innerWidth - SCROLLBAR_WIDTH - SCROLLBAR_GAP); + measuredContent = content.layout(context, contentViewportX, contentViewportY, contentViewportWidth); + contentHeight = measuredContent.height(); + } + contentViewportHeight = preferredContentHeight > 0 ? preferredContentHeight : contentHeight; + setContentScrollOffset(contentScrollOffsetY); + snapVisualScrollToTarget(); + totalHeight = PADDING + BORDER_WIDTH + + summaryBounds.height() + + GAP + + contentViewportHeight + + PADDING + + BORDER_WIDTH; + } else { + setContentScrollOffset(0); + snapVisualScrollToTarget(); + } + + return new LytRect(x, y, safeWidth, totalHeight); } @Override protected void onLayoutMoved(int deltaX, int deltaY) { - root.moveLayoutPos(deltaX, deltaY); + summaryRow.moveLayoutPos(deltaX, deltaY); + content.moveLayoutPos(deltaX, deltaY); + contentViewportX += deltaX; + contentViewportY += deltaY; } @Override public void render(RenderContext context) { - root.render(context); + updateVisualScroll(); + context.fillRect(bounds, SymbolicColor.BLOCKQUOTE_BACKGROUND); + summaryRow.render(context); + if (open) { + LytRect viewport = getContentViewportBounds(); + context.pushLocalScissor(viewport); + try { + renderContentWithVisualOffset(context); + } finally { + context.popScissor(); + } + renderScrollbar(context); + } + borderRenderer.render(context, bounds, DETAILS_BORDER, DETAILS_BORDER, DETAILS_BORDER, DETAILS_BORDER); } @Override @@ -172,8 +253,187 @@ public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolea return false; } + @Override + public boolean beginDrag(int documentX, int documentY, int button) { + if (!open || button != 0 || getMaxContentScroll() <= 0) { + return false; + } + if (getScrollbarTrackBounds().contains(documentX, documentY)) { + LytRect thumbBounds = getScrollbarThumbBounds(); + if (!thumbBounds.isEmpty() && thumbBounds.contains(documentX, documentY)) { + scrollbarGrabOffsetY = documentY - thumbBounds.y(); + } else { + scrollbarGrabOffsetY = thumbBounds.isEmpty() ? 0 : thumbBounds.height() / 2; + updateScrollFromMouseY(documentY); + } + draggingScrollbar = true; + draggingContent = false; + return true; + } + if (!getContentViewportBounds().contains(documentX, documentY)) { + return false; + } + draggingContent = true; + draggingScrollbar = false; + dragLastDocumentY = documentY; + return true; + } + + @Override + public void dragTo(int documentX, int documentY) { + if (draggingScrollbar) { + updateScrollFromMouseY(documentY); + return; + } + if (!draggingContent) { + return; + } + int deltaY = documentY - dragLastDocumentY; + dragLastDocumentY = documentY; + setContentScrollOffset(contentScrollOffsetY - deltaY); + } + + @Override + public void endDrag() { + draggingContent = false; + draggingScrollbar = false; + } + + @Override + public boolean scroll(int documentX, int documentY, int wheelDelta) { + if (!open || wheelDelta == 0 + || getMaxContentScroll() <= 0 + || !getContentViewportBounds().contains(documentX, documentY)) { + return false; + } + setContentScrollOffset(contentScrollOffsetY - Integer.signum(wheelDelta) * MIN_WHEEL_STEP); + return true; + } + + @Override + public @Nullable LytNode pickNode(int x, int y) { + if (!bounds.contains(x, y)) { + return null; + } + if (summaryRow.getBounds() != null && summaryRow.getBounds() + .contains(x, y)) { + LytNode node = summaryRow.pickNode(x, y); + return node != null ? node : this; + } + if (open && getScrollbarTrackBounds().contains(x, y)) { + return this; + } + if (open && getContentViewportBounds().contains(x, y)) { + LytNode node = content.pickNode(x, y); + return node != null ? node : this; + } + return this; + } + @Override public Optional getTooltip(float x, float y) { return Optional.empty(); } + + private void renderScrollbar(RenderContext context) { + LytRect trackBounds = getScrollbarTrackBounds(); + if (trackBounds.isEmpty()) { + return; + } + context.fillRect(trackBounds, 0x30242B33); + LytRect thumbBounds = getScrollbarThumbBounds(); + if (!thumbBounds.isEmpty()) { + context.fillRect(thumbBounds, draggingScrollbar ? 0xFFCDD6E1 : 0xA0AAB5C2); + } + } + + private LytRect getContentViewportBounds() { + return new LytRect(contentViewportX, contentViewportY, contentViewportWidth, contentViewportHeight); + } + + private LytRect getScrollbarTrackBounds() { + if (getMaxContentScroll() <= 0) { + return LytRect.empty(); + } + return new LytRect( + contentViewportX + contentViewportWidth + SCROLLBAR_GAP, + contentViewportY, + SCROLLBAR_WIDTH, + contentViewportHeight); + } + + private LytRect getScrollbarThumbBounds() { + LytRect track = getScrollbarTrackBounds(); + if (track.isEmpty()) { + return LytRect.empty(); + } + int thumbHeight = Math + .max(MIN_SCROLLBAR_THUMB, track.height() * track.height() / Math.max(track.height(), contentHeight)); + thumbHeight = Math.min(thumbHeight, track.height()); + int maxScroll = getMaxContentScroll(); + int thumbTrack = Math.max(1, track.height() - thumbHeight); + int thumbY = track.y(); + if (maxScroll > 0) { + thumbY += (int) ((long) thumbTrack * visualContentScrollOffsetY.rounded() / maxScroll); + } + return new LytRect(track.x(), thumbY, track.width(), thumbHeight); + } + + private int getMaxContentScroll() { + return Math.max(0, contentHeight - contentViewportHeight); + } + + private void setContentScrollOffset(int contentScrollOffsetY) { + this.contentScrollOffsetY = SceneEditorVerticalScrollbar.clamp(contentScrollOffsetY, 0, getMaxContentScroll()); + updateContentPosition(); + } + + private void updateContentPosition() { + if (!content.getBounds() + .isEmpty()) { + content.moveLayoutPos( + contentViewportX - content.getBounds() + .x(), + contentViewportY - contentScrollOffsetY + - content.getBounds() + .y()); + } + } + + private void renderContentWithVisualOffset(RenderContext context) { + int renderDeltaY = contentScrollOffsetY - visualContentScrollOffsetY.rounded(); + if (renderDeltaY == 0) { + content.render(context); + return; + } + content.moveLayoutPos(0, renderDeltaY); + try { + content.render(context); + } finally { + content.moveLayoutPos(0, -renderDeltaY); + } + } + + private void updateScrollFromMouseY(int mouseY) { + LytRect track = getScrollbarTrackBounds(); + LytRect thumb = getScrollbarThumbBounds(); + if (track.isEmpty() || thumb.isEmpty()) { + setContentScrollOffset(0); + return; + } + int thumbTrack = Math.max(1, track.height() - thumb.height()); + int thumbTop = SceneEditorVerticalScrollbar + .clamp(mouseY - scrollbarGrabOffsetY, track.y(), track.y() + thumbTrack); + int maxScroll = getMaxContentScroll(); + setContentScrollOffset((int) ((long) (thumbTop - track.y()) * maxScroll / thumbTrack)); + } + + private void snapVisualScrollToTarget() { + visualContentScrollOffsetY.snapTo(contentScrollOffsetY); + } + + private void updateVisualScroll() { + visualContentScrollOffsetY + .updateTowards(contentScrollOffsetY, 28f, 0.25f, 0.01f, Math.max(128f, contentViewportHeight * 2f)); + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index 37c5e061..d796c6c6 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -19,6 +19,7 @@ import org.jetbrains.annotations.Nullable; +import com.github.bsideup.jabel.Desugar; import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.GuidePageIcon; import com.hfstudio.guidenh.guide.PageAnchor; @@ -30,9 +31,12 @@ import com.hfstudio.guidenh.guide.compiler.FrontmatterNavigation; import com.hfstudio.guidenh.guide.compiler.GuideItemReferenceResolver; import com.hfstudio.guidenh.guide.compiler.IdUtils; +import com.hfstudio.guidenh.guide.compiler.MdxBlockTagSourceExtractor; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; import com.hfstudio.guidenh.guide.compiler.tags.CommandLinkCompiler; +import com.hfstudio.guidenh.guide.compiler.tags.DetailsContentExtractor; +import com.hfstudio.guidenh.guide.compiler.tags.DetailsContentExtractor.DetailsContent; import com.hfstudio.guidenh.guide.compiler.tags.ItemImageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.KeyBindTagCompiler; import com.hfstudio.guidenh.guide.compiler.tags.MdxAttrs; @@ -1036,21 +1040,16 @@ private String renderDetails(MdxJsxElementFields element, String defaultNamespac html.append(" open"); } html.append(">"); - List children = element.children(); - int bodyStart = 0; - if (!children.isEmpty() && children.getFirst() instanceof MdxJsxElementFields summaryEl - && "summary".equals(summaryEl.name())) { - String summaryBody = compiler - .compileFragment(summaryEl.children(), templates, defaultNamespace, sceneResolver, currentPageId); - html.append("") - .append(summaryBody) - .append(""); - bodyStart = 1; - } else { - html.append("") - .append(escapeHtml(GuidebookText.GuideEditorDetails.text())) - .append(""); - } + DetailsRenderContent content = resolveDetailsRenderContent( + element, + defaultNamespace, + currentPageId, + templates, + sceneResolver, + compiler); + html.append("") + .append(content.summaryHtml()) + .append(""); html.append("
    "); - html.append( - compiler.compileFragment( - children.subList(bodyStart, children.size()), - templates, - defaultNamespace, - sceneResolver, - currentPageId)); + html.append(content.bodyHtml()); html.append("
    "); html.append("
    "); return html.toString(); } + private DetailsRenderContent resolveDetailsRenderContent(MdxJsxElementFields element, String defaultNamespace, + @Nullable ResourceLocation currentPageId, GuideSiteTemplateRegistry templates, + GuideSiteHtmlCompiler.SceneResolver sceneResolver, GuideSiteHtmlCompiler compiler) { + DetailsContent sourceContent = extractDetailsSourceContent(element, currentPageId); + if (sourceContent != null) { + String summaryHtml = sourceContent.summaryMarkdown() != null ? compileInlineMarkdownFragment( + sourceContent.summaryMarkdown(), + defaultNamespace, + currentPageId, + templates, + sceneResolver, + compiler) : escapeHtml(GuidebookText.GuideEditorDetails.text()); + String bodyHtml = compileBlockMarkdownFragment( + sourceContent.bodyMarkdown(), + defaultNamespace, + currentPageId, + templates, + sceneResolver, + compiler); + return new DetailsRenderContent(summaryHtml, bodyHtml); + } + + List children = element.children(); + int bodyStart = 0; + String summaryHtml = escapeHtml(GuidebookText.GuideEditorDetails.text()); + if (!children.isEmpty() && children.getFirst() instanceof MdxJsxElementFields summaryEl + && "summary".equals(summaryEl.name())) { + summaryHtml = compiler + .compileInlineFragment(summaryEl.children(), templates, defaultNamespace, sceneResolver, currentPageId); + bodyStart = 1; + } + String bodyHtml = compiler.compileFragment( + children.subList(bodyStart, children.size()), + templates, + defaultNamespace, + sceneResolver, + currentPageId); + return new DetailsRenderContent(summaryHtml, bodyHtml); + } + + @Nullable + private DetailsContent extractDetailsSourceContent(MdxJsxElementFields element, @Nullable ResourceLocation pageId) { + ParsedGuidePage parsedPage = pageId != null ? parsedPagesById.get(pageId) : null; + if (parsedPage == null) { + return null; + } + String rawBody = MdxBlockTagSourceExtractor.extractRawBody(element, parsedPage.getSource()); + if (rawBody == null) { + return null; + } + return DetailsContentExtractor.extract(DetailsContentExtractor.dedent(rawBody)); + } + + private String compileInlineMarkdownFragment(String source, String defaultNamespace, + @Nullable ResourceLocation currentPageId, GuideSiteTemplateRegistry templates, + GuideSiteHtmlCompiler.SceneResolver sceneResolver, GuideSiteHtmlCompiler compiler) { + if (source == null || source.isEmpty()) { + return ""; + } + ParsedGuidePage parsed = parseSiteFragment(source, defaultNamespace, currentPageId); + return compiler.compileInlineFragment( + parsed.getAstRoot() + .children(), + templates, + defaultNamespace, + sceneResolver, + currentPageId); + } + + private String compileBlockMarkdownFragment(String source, String defaultNamespace, + @Nullable ResourceLocation currentPageId, GuideSiteTemplateRegistry templates, + GuideSiteHtmlCompiler.SceneResolver sceneResolver, GuideSiteHtmlCompiler compiler) { + if (source == null || source.isEmpty()) { + return ""; + } + ParsedGuidePage parsed = parseSiteFragment(source, defaultNamespace, currentPageId); + return compiler.compileFragment( + parsed.getAstRoot() + .children(), + templates, + defaultNamespace, + sceneResolver, + currentPageId); + } + + private ParsedGuidePage parseSiteFragment(String source, String defaultNamespace, + @Nullable ResourceLocation currentPageId) { + ResourceLocation parsePageId = currentPageId != null ? currentPageId + : new ResourceLocation(defaultNamespace, "site-export/details-fragment.md"); + return PageCompiler.parse(resolveSourcePack(parsePageId), "en_us", parsePageId, source); + } + private String renderContentTabs(MdxJsxElementFields element, String defaultNamespace, @Nullable ResourceLocation currentPageId, GuideSiteTemplateRegistry templates, GuideSiteHtmlCompiler.SceneResolver sceneResolver, GuideSiteHtmlCompiler compiler) { @@ -3065,6 +3150,9 @@ private StructureBlockView(int x, int y, int z, int projectedX, int projectedY, } } + @Desugar + private record DetailsRenderContent(String summaryHtml, String bodyHtml) {} + private static class StructureLegendEntry { private final GuideSiteExportedItem item; diff --git a/wiki/Tags-Reference-zh-CN.md b/wiki/Tags-Reference-zh-CN.md index 084c51d9..8806bc54 100644 --- a/wiki/Tags-Reference-zh-CN.md +++ b/wiki/Tags-Reference-zh-CN.md @@ -122,7 +122,7 @@ Water is H2O and x2 is a square. ### `
    ` -用于创建可折叠的运行时内容块。`` 行支持常规行内 Markdown/标签内容,正文则可以放普通文本与任意块级标签,例如 ``、``、``、表格、图表或布局容器。 +用于创建可折叠的运行时内容块。`` 行支持常规行内 Markdown/标签内容,正文则可以放普通文本与任意块级标签,例如 ``、``、``、表格、图表或布局容器。设置 `height` 时,只有正文区域滚动,标题行和外框保持固定。 ````md
    diff --git a/wiki/Tags-Reference.md b/wiki/Tags-Reference.md index a4fda5c1..e1684c63 100644 --- a/wiki/Tags-Reference.md +++ b/wiki/Tags-Reference.md @@ -123,6 +123,7 @@ Water is H2O and x2 is a square. Creates a collapsible runtime block with a summary row. The `` line supports normal inline markdown/tag content, and the body can hold ordinary text plus arbitrary block tags such as ``, ``, ``, tables, charts, and layout containers. +When `height` is set, only the body scrolls; the summary row and outer frame stay fixed. ````md
    diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md index 64916ade..6d13e9db 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/markdown.md @@ -275,7 +275,8 @@ Another width-hint sample with three columns: ## Details `
    ` accepts `width`, `height`, `wrap`, and `align`. The summary supports inline tags, and -the body can mix ordinary text with arbitrary runtime blocks. +the body can mix ordinary text with arbitrary runtime blocks. When a height is set, the summary and +outer frame stay fixed while only the body scrolls.
    Mixed runtime content diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md index b4c1f191..3205349e 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_zh_cn/markdown.md @@ -265,7 +265,7 @@ Markdown: ## 折叠详情 -`
    ` 支持 `width`、`height`、`wrap`、`align`。`` 支持行内标签,正文则可以混排普通文本与任意运行时块。 +`
    ` 支持 `width`、`height`、`wrap`、`align`。`` 支持行内标签,正文则可以混排普通文本与任意运行时块。设置高度时,标题和外框保持固定,只有正文区域滚动。
    混合运行时内容 From 36a3853e3c10cc41b26548872250add2f4105181 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:19:30 +0800 Subject: [PATCH 130/136] bugfix --- .../guide/document/block/LytCodeBlock.java | 17 +++--- .../document/block/LytCodeBlockToolbar.java | 52 +++++++++++++++---- .../guide/document/block/LytGuiSprite.java | 22 +++++++- .../document/block/LytMermaidMindmap.java | 3 ++ .../block/LytMermaidMindmapCanvas.java | 20 +++++-- .../internal/home/HomePageController.java | 20 +++++-- .../guide/internal/home/HomePageLayout.java | 32 ++++++++---- .../host/scripts/ItemImageScript.java | 25 +++------ .../guide/render/VanillaRenderContext.java | 45 ++++++++++++++++ .../guidenh/guidenh/_en_us/scene-import.md | 1 + 10 files changed, 180 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java index 549a368c..82a6063e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlock.java @@ -46,6 +46,9 @@ public class LytCodeBlock extends LytVBox implements InteractiveElement, Documen private int preferredBodyWidth; private int forcedBodyHeight; private int bodyContentHeight; + private int bodyViewportX; + private int bodyViewportY; + private int bodyViewportWidth; private int bodyViewportHeight; private int bodyScrollOffsetY; private final SmoothFloatState visualBodyScrollOffsetY = new SmoothFloatState(); @@ -263,6 +266,9 @@ protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int avai } bodyViewportHeight = forcedBodyHeight > 0 ? forcedBodyHeight : bodyContentHeight; + bodyViewportX = x; + bodyViewportY = bodyY; + bodyViewportWidth = bodyAvailableWidth; setBodyScrollOffset(bodyScrollOffsetY); snapVisualScrollToTarget(); return new LytRect(x, y, safeWidth, toolbarBounds.height() + getGap() + bodyViewportHeight); @@ -345,16 +351,7 @@ private void renderScrollbar(RenderContext context) { } private LytRect getBodyViewportBounds() { - LytRect toolbarBounds = toolbar.getBounds(); - int viewportY = toolbarBounds.bottom() + getGap(); - int viewportHeight = Math.max(0, bodyViewportHeight); - return new LytRect( - body.getBounds() - .x(), - viewportY, - body.getBounds() - .width(), - viewportHeight); + return new LytRect(bodyViewportX, bodyViewportY, bodyViewportWidth, Math.max(0, bodyViewportHeight)); } private LytRect getScrollbarTrackBounds() { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java index d965521b..80450585 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java @@ -2,7 +2,9 @@ import java.util.Optional; +import com.hfstudio.guidenh.guide.color.ColorValue; import com.hfstudio.guidenh.guide.color.ConstantColor; +import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.document.LytPoint; import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.LytSize; @@ -20,16 +22,29 @@ public class LytCodeBlockToolbar extends LytBox implements InteractiveElement { - private static final GuiSprite COPY_SPRITE = new GuiSprite(GuideIconButton.TEX, 0, 48, 16, 16, 64, 64); + private static final GuiSprite COPY_SPRITE = new GuiSprite( + GuideIconButton.TEX, + 0, + 48, + 16, + 16, + GuideIconButton.TEXTURE_SIZE, + GuideIconButton.TEXTURE_SIZE); private static final long COPY_TOOLTIP_RESET_DELAY_MILLIS = 1500L; + private static final int TEXT_CENTERING_OFFSET_Y = 1; private static final CodeHighlightTheme CODE_THEME = CodeHighlightTheme.GITHUB_DARK_DEFAULT; - private static final ConstantColor TOOLBAR_BACKGROUND = new ConstantColor(CODE_THEME.toolbarBackgroundArgb()); - private static final ConstantColor TOOLBAR_BORDER = new ConstantColor(CODE_THEME.borderArgb()); - private static final ConstantColor TOOLBAR_TEXT = new ConstantColor(CODE_THEME.toolbarTextArgb()); + private static final ConstantColor DEFAULT_TOOLBAR_BACKGROUND = new ConstantColor( + CODE_THEME.toolbarBackgroundArgb()); + private static final ConstantColor DEFAULT_TOOLBAR_BORDER = new ConstantColor(CODE_THEME.borderArgb()); + private static final ConstantColor DEFAULT_TOOLBAR_TEXT = new ConstantColor(CODE_THEME.toolbarTextArgb()); private final LytParagraph languageLabel = new LytParagraph(); private final LytGuiSprite copyButton = new LytGuiSprite(COPY_SPRITE, new LytSize(16, 16)); + private ColorValue toolbarBackground = DEFAULT_TOOLBAR_BACKGROUND; + private ColorValue toolbarBorder = DEFAULT_TOOLBAR_BORDER; + private ColorValue toolbarText = DEFAULT_TOOLBAR_TEXT; + private String copyText = ""; private boolean copied; private long copiedUntilMillis; @@ -41,15 +56,16 @@ public LytCodeBlockToolbar() { languageLabel.setMarginBottom(0); languageLabel.modifyStyle( style -> style.bold(true) - .color(TOOLBAR_TEXT)); - copyButton.setColor(TOOLBAR_TEXT); + .color(toolbarText)); + copyButton.setColor(toolbarText); + copyButton.setHoverColor(SymbolicColor.ICON_BUTTON_HOVER); append(languageLabel); append(copyButton); setPaddingLeft(8); setPaddingTop(4); setPaddingRight(8); setPaddingBottom(4); - setBorderBottom(new BorderStyle(TOOLBAR_BORDER, 1)); + setBorderBottom(new BorderStyle(toolbarBorder, 1)); } public void setLanguageDisplayName(String languageDisplayName) { @@ -70,6 +86,21 @@ public void setCopyButtonVisible(boolean copyButtonVisible) { this.copyButtonVisible = copyButtonVisible; } + public void setToolbarBackground(ColorValue toolbarBackground) { + this.toolbarBackground = toolbarBackground != null ? toolbarBackground : DEFAULT_TOOLBAR_BACKGROUND; + } + + public void setToolbarBorder(ColorValue toolbarBorder) { + this.toolbarBorder = toolbarBorder != null ? toolbarBorder : DEFAULT_TOOLBAR_BORDER; + setBorderBottom(new BorderStyle(this.toolbarBorder, getBorderBottom().width())); + } + + public void setToolbarText(ColorValue toolbarText) { + this.toolbarText = toolbarText != null ? toolbarText : DEFAULT_TOOLBAR_TEXT; + languageLabel.modifyStyle(style -> style.color(this.toolbarText)); + copyButton.setColor(this.toolbarText); + } + @Override protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { int toolbarWidth = preferredWidth > 0 ? Math.min(availableWidth, preferredWidth) : availableWidth; @@ -77,8 +108,9 @@ protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int avai LytRect labelBounds = languageLabel.layout(context, x, y, labelWidth); int buttonX = x + Math.max(0, toolbarWidth - 16); LytRect buttonBounds = copyButtonVisible ? copyButton.layout(context, buttonX, y, 16) : LytRect.empty(); - int height = Math.max(labelBounds.height(), copyButtonVisible ? buttonBounds.height() : 0); - languageLabel.setLayoutPos(new LytPoint(labelBounds.x(), y + (height - labelBounds.height()) / 2f)); + int height = Math.max(16, Math.max(labelBounds.height(), copyButtonVisible ? buttonBounds.height() : 0)); + float labelY = y + (height - labelBounds.height()) / 2f + TEXT_CENTERING_OFFSET_Y; + languageLabel.setLayoutPos(new LytPoint(labelBounds.x(), labelY)); if (copyButtonVisible) { copyButton.setLayoutPos(new LytPoint(buttonX, y + (height - buttonBounds.height()) / 2f)); } else { @@ -121,7 +153,7 @@ public void setPaddingBottom(int paddingBottom) { @Override public void render(RenderContext context) { - context.fillRect(bounds, TOOLBAR_BACKGROUND); + context.fillRect(bounds, toolbarBackground); languageLabel.render(context); if (copyButtonVisible) { copyButton.render(context); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytGuiSprite.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytGuiSprite.java index d6507c79..2b2879f9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytGuiSprite.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytGuiSprite.java @@ -6,6 +6,7 @@ import com.hfstudio.guidenh.guide.color.ConstantColor; import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.LytSize; +import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.interaction.InteractiveElement; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.render.GuiSprite; @@ -21,6 +22,11 @@ public class LytGuiSprite extends LytBlock implements InteractiveElement { private ColorValue color = ConstantColor.WHITE; + @Nullable + private ColorValue hoverColor; + + private boolean hovered; + private LytSize size = new LytSize(16, 16); public LytGuiSprite() {} @@ -47,6 +53,10 @@ public void setColor(ColorValue color) { this.color = color != null ? color : ConstantColor.WHITE; } + public void setHoverColor(@Nullable ColorValue hoverColor) { + this.hoverColor = hoverColor; + } + public LytSize getSize() { return size; } @@ -82,10 +92,20 @@ protected LytRect computeLayout(LayoutContext context, int x, int y, int availab @Override protected void onLayoutMoved(int deltaX, int deltaY) {} + @Override + public void onMouseEnter(@Nullable LytFlowContent hoveredContent) { + hovered = true; + } + + @Override + public void onMouseLeave() { + hovered = false; + } + @Override public void render(RenderContext context) { if (sprite != null) { - context.fillIcon(getBounds(), sprite, color); + context.fillIcon(getBounds(), sprite, hovered && hoverColor != null ? hoverColor : color); } } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java index c108c0cd..6a5cc277 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmap.java @@ -35,6 +35,9 @@ public LytMermaidMindmap(MermaidMindmapDocument mindmap, String sourceText, Map< toolbar.setLanguageDisplayName("Mermaid"); toolbar.setCopyText(this.sourceText); toolbar.setCopyButtonVisible(false); + toolbar.setToolbarBackground(LytMermaidMindmapCanvas.PANEL_BACKGROUND); + toolbar.setToolbarBorder(LytMermaidMindmapCanvas.PANEL_BORDER); + toolbar.setToolbarText(LytMermaidMindmapCanvas.NODE_TEXT_COLOR); append(toolbar); append(canvas); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java index ee10a59e..38e23f2b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytMermaidMindmapCanvas.java @@ -55,6 +55,11 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar private static final float ZOOM_STEP = 1.1f; private static final float MIN_ZOOM = 0.5f; private static final float MAX_ZOOM = 2.5f; + static final ConstantColor PANEL_BACKGROUND = new ConstantColor(0x1A0C1117); + static final ConstantColor PANEL_BORDER = new ConstantColor(0x66434C57); + static final ConstantColor ROOT_TEXT_COLOR = new ConstantColor(0xFFF1F6FB); + static final ConstantColor NODE_TEXT_COLOR = new ConstantColor(0xFFD7DEE7); + static final ConstantColor ICON_TEXT_COLOR = new ConstantColor(0xFFB8C2CF); private static final ResolvedTextStyle ROOT_TEXT_STYLE = new ResolvedTextStyle( 1f, @@ -66,7 +71,7 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar false, false, null, - new ConstantColor(0xFFF1F6FB), + ROOT_TEXT_COLOR, WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, @@ -82,7 +87,7 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar false, false, null, - new ConstantColor(0xFFD7DEE7), + NODE_TEXT_COLOR, WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, @@ -98,7 +103,7 @@ public class LytMermaidMindmapCanvas extends LytBlock implements DocumentDragTar false, false, null, - new ConstantColor(0xFFB8C2CF), + ICON_TEXT_COLOR, WhiteSpaceMode.NORMAL, TextAlignment.LEFT, false, @@ -191,8 +196,8 @@ public void render(RenderContext context) { updateVisualState(); ensureScaledStyles(visualZoom.value()); - context.fillRect(bounds, 0x1A0C1117); - context.drawBorder(bounds, 0x66434C57, 1); + context.fillRect(bounds, PANEL_BACKGROUND); + context.drawBorder(bounds, context.resolveColor(PANEL_BORDER), 1); LytRect viewport = getInnerViewport(); int baseX = viewport.x() + visualContentOffsetX.rounded() @@ -1467,6 +1472,11 @@ public void blitGuiSprite(LytRect rect, GuiSprite sprite) { } } + @Override + public void fillIcon(LytRect rect, GuiSprite sprite, ColorValue color) { + delegate.fillIcon(scaleRect(rect), sprite, color); + } + @Override public void blitTexture(ResourceLocation texture, int x, int y, int u, int v, int width, int height) { delegate.blitTexture(texture, scaleX(x), scaleY(y), u, v, scaleLength(width), scaleLength(height)); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java index 41b644d7..7938e13b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageController.java @@ -1,5 +1,7 @@ package com.hfstudio.guidenh.guide.internal.home; +import java.util.List; + import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; @@ -430,10 +432,20 @@ private void drawCenteredEmptyMessage(FontRenderer font, String text, HomePageLa if (text == null || text.isEmpty()) { return; } - int textWidth = font.getStringWidth(text); - int textX = rect.x() + (rect.width() - textWidth) / 2; - int textY = rect.y() + (rect.height() - font.FONT_HEIGHT) / 2; - font.drawString(text, textX, textY, EMPTY_COLOR, false); + int maxWidth = Math.max(1, rect.width() - SECTION_PADDING * 2); + List lines = font.listFormattedStringToWidth(text, maxWidth); + if (lines.isEmpty()) { + lines = List.of(text); + } + int lineHeight = font.FONT_HEIGHT + 1; + int textHeight = lines.size() * lineHeight - 1; + int textY = rect.y() + Math.max(0, (rect.height() - textHeight) / 2); + for (String line : lines) { + int textWidth = font.getStringWidth(line); + int textX = rect.x() + (rect.width() - textWidth) / 2; + font.drawString(line, textX, textY, EMPTY_COLOR, false); + textY += lineHeight; + } } private String trimToWidth(FontRenderer font, String text, int maxWidth) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageLayout.java b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageLayout.java index 2160567d..782e95b9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageLayout.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/home/HomePageLayout.java @@ -6,15 +6,23 @@ public class HomePageLayout { private static final float SHELL_HEIGHT_RATIO = 0.8f; private static final float LOGO_WIDTH_RATIO = 0.8f; private static final int PANEL_GAP = 12; - private static final int LOGO_RAISE_PIXELS = 5; + private static final int LOGO_PANEL_GAP = 6; private static final int PANEL_MIN_WIDTH = 100; + private static final int MIN_SHELL_WIDTH = 220; + private static final int MIN_SHELL_HEIGHT = 120; + private static final int MIN_PANEL_HEIGHT = 52; + private static final int MIN_LOGO_WIDTH = 80; + private static final int MIN_LOGO_HEIGHT = 24; + private static final int MAX_LOGO_HEIGHT = 72; private HomePageLayout() {} public static LayoutRects compute(int contentX, int contentY, int contentW, int contentH, int logoWidth, int logoHeight) { - int shellW = Math.max(220, Math.round(contentW * SHELL_WIDTH_RATIO)); - int shellH = Math.max(180, Math.round(contentH * SHELL_HEIGHT_RATIO)); + int shellW = Math.max(MIN_SHELL_WIDTH, Math.round(contentW * SHELL_WIDTH_RATIO)); + int shellMaxH = Math.max(1, contentH); + int shellMinH = Math.min(MIN_SHELL_HEIGHT, shellMaxH); + int shellH = Math.clamp(Math.round(contentH * SHELL_HEIGHT_RATIO), shellMinH, shellMaxH); int shellX = contentX + Math.max(0, (contentW - shellW) / 2); int shellY = contentY + Math.max(0, (contentH - shellH) / 2); @@ -22,17 +30,23 @@ public static LayoutRects compute(int contentX, int contentY, int contentW, int int columnsW = panelW * 2 + PANEL_GAP; int columnsX = shellX + Math.max(0, (shellW - columnsW) / 2); - int logoW = Math.max(80, Math.round(panelW * LOGO_WIDTH_RATIO)); int safeLogoWidth = Math.max(1, logoWidth); int safeLogoHeight = Math.max(1, logoHeight); - int logoH = Math.max(30, Math.round((float) logoW * safeLogoHeight / safeLogoWidth)); - int recommendY = shellY + logoH / 2; - int availableRecommendH = Math.max(80, shellH - logoH / 2); - int rightHalfH = Math.max(60, (availableRecommendH - PANEL_GAP) / 2); + int preferredLogoW = Math.max(MIN_LOGO_WIDTH, Math.round(panelW * LOGO_WIDTH_RATIO)); + int preferredLogoH = Math.round((float) preferredLogoW * safeLogoHeight / safeLogoWidth); + int logoH = Math.clamp(preferredLogoH, MIN_LOGO_HEIGHT, Math.min(MAX_LOGO_HEIGHT, Math.max(1, shellH / 3))); + int logoW = Math.max(1, Math.round((float) logoH * safeLogoWidth / safeLogoHeight)); + if (logoW > panelW) { + logoW = panelW; + logoH = Math.max(MIN_LOGO_HEIGHT, Math.round((float) logoW * safeLogoHeight / safeLogoWidth)); + } + int recommendY = shellY + logoH + LOGO_PANEL_GAP; + int availableRecommendH = Math.max(MIN_PANEL_HEIGHT * 2 + PANEL_GAP, shellY + shellH - recommendY); + int rightHalfH = Math.max(MIN_PANEL_HEIGHT, (availableRecommendH - PANEL_GAP) / 2); int recommendH = rightHalfH * 2 + PANEL_GAP; Rect recommended = new Rect(columnsX, recommendY, panelW, recommendH); - Rect logo = new Rect(columnsX, recommendY - logoH - LOGO_RAISE_PIXELS, logoW, logoH); + Rect logo = new Rect(columnsX + Math.max(0, (panelW - logoW) / 2), shellY, logoW, logoH); Rect bookmarks = new Rect(columnsX + panelW + PANEL_GAP, recommendY, panelW, rightHalfH); Rect history = new Rect(columnsX + panelW + PANEL_GAP, recommendY + rightHalfH + PANEL_GAP, panelW, rightHalfH); int recommendedTitleSafeTop = 0; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java index f07b55ac..069d1153 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemImageScript.java @@ -1,5 +1,7 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import java.util.List; + import net.minecraft.item.ItemStack; import net.minecraftforge.oredict.OreDictionary; @@ -33,20 +35,19 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { ItemImagePlaceholder ph = LytFlowInlineBlock.unwrapPlaceholder(node, ItemImagePlaceholder.class); if (ph == null) return; - boolean isWrapped = node instanceof LytFlowInlineBlock; ItemStack stack = resolveItemId(ph.itemId); if (stack == null) { // Fallback to ore dictionary if direct item lookup fails if (ph.ore != null) { - java.util.List oreStacks = OreDictionary.getOres(ph.ore); + List oreStacks = OreDictionary.getOres(ph.ore); if (oreStacks != null && !oreStacks.isEmpty()) { stack = oreStacks.get(0) .copy(); } } if (stack == null) { - replaceFlowError(ctx, isWrapped, "[ItemImage] Item not found: " + ph.itemId); + replaceFlowError(ctx, "[ItemImage] Item not found: " + ph.itemId); return; } } @@ -61,25 +62,13 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { if (ph.yOffset != null) image.setInlineYOffsetOverride(ph.yOffset); if (ph.labelYOffset != null) image.setLabelYOffsetOverride(ph.labelYOffset); - if (isWrapped) { - LytFlowInlineBlock newWrapper = new LytFlowInlineBlock(); - newWrapper.setBlock(image); - ctx.replace(newWrapper); - } else { - ctx.replace(image); - } + ctx.replace(image); } @SuppressWarnings("deprecation") - private void replaceFlowError(ScriptContext ctx, boolean isWrapped, String message) { + private void replaceFlowError(ScriptContext ctx, String message) { LytParagraph error = LytParagraph.error(message); - if (isWrapped) { - LytFlowInlineBlock wrapper = new LytFlowInlineBlock(); - wrapper.setBlock(error); - ctx.replace(wrapper); - } else { - ctx.replace(error); - } + ctx.replace(error); } private static ItemStack resolveItemId(String itemId) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java index 8d52a522..66fcebd7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java @@ -411,6 +411,51 @@ public void blitTexture(ResourceLocation texture, int x, int y, int u, int v, in tess.draw(); } + @Override + public void fillIcon(LytRect rect, GuiSprite sprite, ColorValue color) { + if (sprite == null || rect == null) { + return; + } + int resolved = resolveColor(color); + int a = (resolved >>> 24) & 0xFF; + int r = (resolved >>> 16) & 0xFF; + int g = (resolved >>> 8) & 0xFF; + int b = resolved & 0xFF; + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_CURRENT_BIT | GL11.GL_COLOR_BUFFER_BIT); + try { + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r / 255f, g / 255f, b / 255f, a / 255f); + Minecraft.getMinecraft() + .getTextureManager() + .bindTexture(sprite.getTexture()); + var tess = Tessellator.instance; + float texW = sprite.getTexWidth(); + float texH = sprite.getTexHeight(); + int x = rect.x(); + int y = rect.y(); + int u = sprite.getU(); + int v = sprite.getV(); + int spriteWidth = sprite.getWidth(); + int spriteHeight = sprite.getHeight(); + tess.startDrawingQuads(); + tess.addVertexWithUV(x, y + spriteHeight, 0, u / texW, (v + spriteHeight) / texH); + tess.addVertexWithUV( + x + spriteWidth, + y + spriteHeight, + 0, + (u + spriteWidth) / texW, + (v + spriteHeight) / texH); + tess.addVertexWithUV(x + spriteWidth, y, 0, (u + spriteWidth) / texW, v / texH); + tess.addVertexWithUV(x, y, 0, u / texW, v / texH); + tess.draw(); + } finally { + GL11.glColor4f(1f, 1f, 1f, 1f); + GL11.glPopAttrib(); + } + } + @Override public void fillTexturedRect(LytRect rect, GuidePageTexture texture) { if (texture == null || texture.isMissing()) { diff --git a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md index 33137ddd..3f1c6460 100644 --- a/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md +++ b/wiki/resourcepack/assets/guidenh/guidenh/_en_us/scene-import.md @@ -33,6 +33,7 @@ correctly: + `facing`, `rotation`, and `flip` use the same orientation vocabulary as StructureLib export. If a controller rejects the requested combination, GuideNH automatically falls back to the first valid From 99bd7ed73177f76fe2012f8217147aaf035de065 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:28:49 +0800 Subject: [PATCH 131/136] bugfix --- .../GuideLightweightReloadService.java | 12 +- .../datadriven/DataDrivenGuideLoader.java | 341 +++++++++++++++--- .../com/hfstudio/guidenh/mixins/Mixins.java | 6 +- .../AccessorFallbackResourceManager.java | 16 + ...cessorSimpleReloadableResourceManager.java | 16 + 5 files changed, 331 insertions(+), 60 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorFallbackResourceManager.java create mode 100644 src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorSimpleReloadableResourceManager.java diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index 8de7f20a..9201468d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -48,7 +48,7 @@ public static void reloadDevelopmentGuides() { public static void reloadGuides(IResourceManager resourceManager) { GuideDebugLog.info("[GuideNH] [GuideLightweightReloadService] Reloading guide data..."); long startedAt = System.nanoTime(); - var activeResourcePacks = DataDrivenGuideLoader.getActiveResourcePacks(); + var activeResourcePacks = DataDrivenGuideLoader.getActiveResourcePacks(resourceManager); RecipeCache.clear(); NeiAnimationTicker.clear(); GuidePageTexture.clear(); @@ -66,7 +66,7 @@ public static void reloadGuides(IResourceManager resourceManager) { .clearPageCaches(); long stageStartedAt = System.nanoTime(); - GuideRegistry.setDataDriven(DataDrivenGuideLoader.load()); + GuideRegistry.setDataDriven(DataDrivenGuideLoader.load(activeResourcePacks)); MediaWikiTranslationStats.invalidateCache(); long dataDrivenLoadNs = System.nanoTime() - stageStartedAt; @@ -134,7 +134,7 @@ static Map loadPages(IResourceManager resourc defaultLanguage, currentLanguage, new LinkedHashMap<>(), - DataDrivenGuideLoader.getActiveResourcePacks()); + DataDrivenGuideLoader.getActiveResourcePacks(resourceManager)); } static Map loadPages(IResourceManager resourceManager, ResourceLocation guideId, @@ -143,7 +143,11 @@ static Map loadPages(IResourceManager resourc Iterable activeResourcePacks) { long startedAt = System.nanoTime(); var pages = new HashMap(); - var pagePaths = pagePathsForGuide(guideId, folder, pagePathCache, DataDrivenGuideLoader::discoverPagePaths); + var pagePaths = pagePathsForGuide( + guideId, + folder, + pagePathCache, + lookupFolder -> DataDrivenGuideLoader.discoverPagePaths(lookupFolder, activeResourcePacks)); String lang = currentLanguage != null ? currentLanguage : defaultLanguage; String sourceNamespace = guideId.getResourceDomain(); String sourcePack = "resources:" + sourceNamespace; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java index 31e7ed34..6e0fffbe 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java @@ -2,7 +2,11 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -12,7 +16,10 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.resources.AbstractResourcePack; +import net.minecraft.client.resources.FallbackResourceManager; +import net.minecraft.client.resources.IResourceManager; import net.minecraft.client.resources.IResourcePack; +import net.minecraft.client.resources.SimpleReloadableResourceManager; import net.minecraft.util.ResourceLocation; import com.hfstudio.guidenh.config.ModConfig; @@ -25,6 +32,8 @@ import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.mixins.early.fml.AccessorFMLClientHandler; import com.hfstudio.guidenh.mixins.early.minecraft.AccessorAbstractResourcePack; +import com.hfstudio.guidenh.mixins.early.minecraft.AccessorFallbackResourceManager; +import com.hfstudio.guidenh.mixins.early.minecraft.AccessorSimpleReloadableResourceManager; import cpw.mods.fml.client.FMLClientHandler; @@ -32,18 +41,29 @@ public class DataDrivenGuideLoader { public static final String AUTO_GUIDE_FOLDER = "guidenh"; public static final String LANGUAGE_FOLDER_PREFIX = "_"; + private static final Map, Field> LOOSE_ROOT_FIELDS = new IdentityHashMap<>(); + private static volatile List lastResourceManagerResourcePacks = List.of(); + private static volatile Map> lastResourceManagerDomainsByPack = Map.of(); private DataDrivenGuideLoader() {} public static Map load() { + return load(getActiveResourcePacks()); + } + + public static Map load(IResourceManager resourceManager) { + return load(getActiveResourcePacks(resourceManager)); + } + + public static Map load(Iterable activeResourcePacks) { long startedAt = System.nanoTime(); long stageStartedAt = startedAt; - var activeResourcePacks = getActiveResourcePacks(); + var resolvedResourcePacks = toList(activeResourcePacks); long resourcePackResolveNs = System.nanoTime() - stageStartedAt; var discoveredLanguages = new LinkedHashMap>(); stageStartedAt = System.nanoTime(); - for (var resourcePack : activeResourcePacks) { + for (var resourcePack : resolvedResourcePacks) { scanResourcePack(resourcePack, discoveredLanguages); } long scanNs = System.nanoTime() - stageStartedAt; @@ -66,7 +86,7 @@ public static Map load() { "[GuideNH] [DataDrivenGuideLoader] Loaded {} guides across {} languages from {} resource packs in {} ns (resourcePackResolveNs={}, scanNs={}, buildNs={})", guides.size(), discoveredLanguageCount, - activeResourcePacks.size(), + resolvedResourcePacks.size(), totalNs, resourcePackResolveNs, scanNs, @@ -76,11 +96,16 @@ public static Map load() { } public static LinkedHashMap> discoverPagePaths(String folder) { + return discoverPagePaths(folder, getActiveResourcePacks()); + } + + public static LinkedHashMap> discoverPagePaths(String folder, + Iterable activeResourcePacks) { long startedAt = System.nanoTime(); - var activeResourcePacks = getActiveResourcePacks(); + var resolvedResourcePacks = toList(activeResourcePacks); var pagePaths = new LinkedHashMap>(); - for (var resourcePack : activeResourcePacks) { + for (var resourcePack : resolvedResourcePacks) { scanPagePathsAllNamespaces(resourcePack, folder, pagePaths); } @@ -91,7 +116,7 @@ public static LinkedHashMap> discoverPagePaths(Str countDiscoveredPagePaths(pagePaths), pagePaths.size(), folder, - activeResourcePacks.size(), + resolvedResourcePacks.size(), totalNs); } return pagePaths; @@ -115,16 +140,16 @@ private static int countDiscoveredPagePaths(LinkedHashMap> pagePaths) { - var resourcePackFile = getResourcePackFile(resourcePack); - if (resourcePackFile == null || !resourcePackFile.exists()) { + var resourcePackRoot = getLooseResourcePackRoot(resourcePack); + if (resourcePackRoot == null || !resourcePackRoot.exists()) { return; } - if (!resourcePackFile.isDirectory()) { - scanZipPagePathsAllNamespaces(resourcePackFile, folder, pagePaths); + if (!resourcePackRoot.isDirectory()) { + scanZipPagePathsAllNamespaces(resourcePackRoot, folder, pagePaths); return; } - scanPagePathsAllNamespaces(resourcePackFile, folder, pagePaths); + scanPagePathsAllNamespaces(resourcePack, resourcePackRoot, folder, pagePaths); } public static void scanPagePathsAllNamespaces(File resourcePackRoot, String folder, @@ -136,20 +161,47 @@ public static void scanPagePathsAllNamespaces(File resourcePackRoot, String fold var assetsDir = new File(resourcePackRoot, "assets"); var namespaceDirs = assetsDir.listFiles(File::isDirectory); - if (namespaceDirs == null) { + if (namespaceDirs != null) { + for (var namespaceDir : namespaceDirs) { + scanPagePathsForNamespaceRoot(resourcePackRoot, namespaceDir.getName(), folder, pagePaths); + } + } + scanLoosePagePathsAllNamespaces(resourcePackRoot, folder, pagePaths); + } + + private static void scanPagePathsAllNamespaces(IResourcePack resourcePack, File resourcePackRoot, String folder, + LinkedHashMap> pagePaths) { + for (String namespace : getResourceDomains(resourcePack)) { + scanPagePathsForNamespaceRoot(resourcePackRoot, namespace, folder, pagePaths); + } + } + + private static void scanPagePathsForNamespaceRoot(File resourcePackRoot, String namespace, String folder, + LinkedHashMap> pagePaths) { + if (!isValidNamespace(namespace)) { return; } + var discovered = new LinkedHashSet(); + scanFolderPagePaths(resourcePackRoot, toFolderPrefix(namespace, folder), discovered); + scanFolderPagePaths(resourcePackRoot, toLooseFolderPrefix(namespace, folder), discovered); + if (!discovered.isEmpty()) { + pagePaths.computeIfAbsent(namespace, k -> new LinkedHashSet<>()) + .addAll(discovered); + } + } + + private static void scanLoosePagePathsAllNamespaces(File resourcePackRoot, String folder, + LinkedHashMap> pagePaths) { + var namespaceDirs = resourcePackRoot.listFiles(File::isDirectory); + if (namespaceDirs == null) { + return; + } for (var namespaceDir : namespaceDirs) { - var guideRootDir = new File(namespaceDir, folder); - if (!guideRootDir.isDirectory()) { + if ("assets".equals(namespaceDir.getName())) { continue; } - - var namespace = namespaceDir.getName(); - var prefix = toFolderPrefix(namespace, folder); - var paths = pagePaths.computeIfAbsent(namespace, k -> new LinkedHashSet<>()); - scanFolderPagePaths(resourcePackRoot, prefix, paths); + scanPagePathsForNamespaceRoot(resourcePackRoot, namespaceDir.getName(), folder, pagePaths); } } @@ -210,8 +262,13 @@ private static void scanZipPagePathsAllNamespaces(File resourcePackFile, String } public static Set discoverPagePaths(ResourceLocation guideId, String folder) { + return discoverPagePaths(guideId, folder, getActiveResourcePacks()); + } + + public static Set discoverPagePaths(ResourceLocation guideId, String folder, + Iterable activeResourcePacks) { var result = new LinkedHashSet(); - for (var resourcePack : getActiveResourcePacks()) { + for (var resourcePack : activeResourcePacks) { scanPagePathsForNamespace(resourcePack, guideId.getResourceDomain(), folder, result); } return result; @@ -219,17 +276,18 @@ public static Set discoverPagePaths(ResourceLocation guideId, String fol public static void scanPagePathsForNamespace(IResourcePack resourcePack, String namespace, String folder, Set pagePaths) { - var resourcePackFile = getResourcePackFile(resourcePack); - if (resourcePackFile == null || !resourcePackFile.exists()) { + var resourcePackRoot = getLooseResourcePackRoot(resourcePack); + if (resourcePackRoot == null || !resourcePackRoot.exists()) { return; } - scanPagePathsForNamespace(resourcePackFile, namespace, folder, pagePaths); + scanPagePathsForNamespace(resourcePackRoot, namespace, folder, pagePaths); } public static void scanPagePathsForNamespace(File resourcePackRoot, String namespace, String folder, Set pagePaths) { if (resourcePackRoot.isDirectory()) { scanFolderPagePaths(resourcePackRoot, toFolderPrefix(namespace, folder), pagePaths); + scanFolderPagePaths(resourcePackRoot, toLooseFolderPrefix(namespace, folder), pagePaths); } else { scanZipPagePaths(resourcePackRoot, toFolderPrefix(namespace, folder), pagePaths); } @@ -237,7 +295,24 @@ public static void scanPagePathsForNamespace(File resourcePackRoot, String names public static List getActiveResourcePacks() { var resourcePacks = new LinkedHashSet(GuideDevelopmentResourcePacks.getConfiguredPacks()); + resourcePacks.addAll(lastResourceManagerResourcePacks); + addConfiguredResourcePacks(resourcePacks); + return new ArrayList<>(resourcePacks); + } + + public static List getActiveResourcePacks(IResourceManager resourceManager) { + var resourcePacks = new LinkedHashSet(GuideDevelopmentResourcePacks.getConfiguredPacks()); + var resourceManagerResourcePacks = new LinkedHashSet(); + var domainsByPack = new IdentityHashMap>(); + addResourceManagerResourcePacks(resourceManager, resourceManagerResourcePacks, domainsByPack); + lastResourceManagerResourcePacks = List.copyOf(resourceManagerResourcePacks); + lastResourceManagerDomainsByPack = freezeDomainsByPack(domainsByPack); + resourcePacks.addAll(resourceManagerResourcePacks); + addConfiguredResourcePacks(resourcePacks); + return new ArrayList<>(resourcePacks); + } + private static void addConfiguredResourcePacks(LinkedHashSet resourcePacks) { try { var accessor = (AccessorFMLClientHandler) FMLClientHandler.instance(); var basePacks = accessor.guidenh$getResourcePackList(); @@ -263,34 +338,86 @@ public static List getActiveResourcePacks() { if (serverPack != null) { resourcePacks.add(serverPack); } + } - return new ArrayList<>(resourcePacks); + private static void addResourceManagerResourcePacks(IResourceManager resourceManager, + LinkedHashSet resourcePacks, + IdentityHashMap> domainsByPack) { + if (!(resourceManager instanceof SimpleReloadableResourceManager)) { + return; + } + + try { + var accessor = (AccessorSimpleReloadableResourceManager) resourceManager; + Map domainManagers = accessor.guidenh$getDomainResourceManagers(); + if (domainManagers == null || domainManagers.isEmpty()) { + return; + } + + for (String domain : resourceManager.getResourceDomains()) { + FallbackResourceManager fallbackResourceManager = domainManagers.get(domain); + if (fallbackResourceManager == null) { + continue; + } + List packs = ((AccessorFallbackResourceManager) fallbackResourceManager) + .guidenh$getResourcePacks(); + if (packs != null) { + for (IResourcePack pack : packs) { + resourcePacks.add(pack); + domainsByPack.computeIfAbsent(pack, ignored -> new LinkedHashSet<>()) + .add(domain); + } + } + } + } catch (RuntimeException e) { + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to inspect the currently loaded resource manager packs", + e); + } + } + + private static Map> freezeDomainsByPack( + IdentityHashMap> domainsByPack) { + if (domainsByPack.isEmpty()) { + return Map.of(); + } + + var result = new IdentityHashMap>(); + for (var entry : domainsByPack.entrySet()) { + result.put(entry.getKey(), Set.copyOf(entry.getValue())); + } + return Collections.unmodifiableMap(result); + } + + private static Set getResourceDomains(IResourcePack resourcePack) { + Set cachedDomains = lastResourceManagerDomainsByPack.get(resourcePack); + return cachedDomains != null ? cachedDomains : resourcePack.getResourceDomains(); } public static void scanResourcePack(IResourcePack resourcePack, Map> discoveredLanguages) { - var resourcePackFile = getResourcePackFile(resourcePack); - if (resourcePackFile == null || !resourcePackFile.exists()) { + var resourcePackRoot = getLooseResourcePackRoot(resourcePack); + if (resourcePackRoot == null || !resourcePackRoot.exists()) { return; } - if (resourcePackFile.isDirectory()) { - scanResourcePackFolder(resourcePackFile, discoveredLanguages); + if (resourcePackRoot.isDirectory()) { + scanResourcePackFolder(resourcePack, resourcePackRoot, discoveredLanguages); } else { - scanResourcePackZip(resourcePackFile, discoveredLanguages); + scanResourcePackZip(resourcePackRoot, discoveredLanguages); } } public static void scanPagePaths(IResourcePack resourcePack, String prefix, Set pagePaths) { - var resourcePackFile = getResourcePackFile(resourcePack); - if (resourcePackFile == null || !resourcePackFile.exists()) { + var resourcePackRoot = getLooseResourcePackRoot(resourcePack); + if (resourcePackRoot == null || !resourcePackRoot.exists()) { return; } - if (resourcePackFile.isDirectory()) { - scanFolderPagePaths(resourcePackFile, prefix, pagePaths); + if (resourcePackRoot.isDirectory()) { + scanFolderPagePaths(resourcePackRoot, prefix, pagePaths); } else { - scanZipPagePaths(resourcePackFile, prefix, pagePaths); + scanZipPagePaths(resourcePackRoot, prefix, pagePaths); } } @@ -315,6 +442,61 @@ public static File getResourcePackFile(IResourcePack resourcePack) { } } + public static File getLooseResourcePackRoot(IResourcePack resourcePack) { + File resourcePackFile = getResourcePackFile(resourcePack); + if (resourcePackFile != null) { + return resourcePackFile; + } + + Field field = findLooseRootField(resourcePack.getClass()); + if (field == null) { + return null; + } + + try { + Object value = field.get(resourcePack); + if (value instanceof Path path) { + return path.toFile(); + } + if (value instanceof File file) { + return file; + } + } catch (IllegalAccessException e) { + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to resolve the directory root for resource pack {}", + resourcePack.getPackName(), + e); + } + return null; + } + + private static Field findLooseRootField(Class resourcePackClass) { + synchronized (LOOSE_ROOT_FIELDS) { + if (LOOSE_ROOT_FIELDS.containsKey(resourcePackClass)) { + return LOOSE_ROOT_FIELDS.get(resourcePackClass); + } + + Field field = discoverLooseRootField(resourcePackClass); + LOOSE_ROOT_FIELDS.put(resourcePackClass, field); + return field; + } + } + + private static Field discoverLooseRootField(Class resourcePackClass) { + Class current = resourcePackClass; + while (current != null && current != Object.class) { + for (Field field : current.getDeclaredFields()) { + Class type = field.getType(); + if (type == Path.class || type == File.class) { + field.setAccessible(true); + return field; + } + } + current = current.getSuperclass(); + } + return null; + } + public static byte[] readBytes(IResourcePack resourcePack, ResourceLocation resourceLocation) { if (!resourcePack.resourceExists(resourceLocation)) { return null; @@ -346,35 +528,70 @@ public static void scanResourcePackFolder(File resourcePackRoot, Map> discoveredLanguages) { var assetsDir = new File(resourcePackRoot, "assets"); var namespaceDirs = assetsDir.listFiles(File::isDirectory); - if (namespaceDirs == null) { - return; + if (namespaceDirs != null) { + for (var namespaceDir : namespaceDirs) { + scanResourcePackFolderNamespace(resourcePackRoot, namespaceDir.getName(), discoveredLanguages); + } } - for (var namespaceDir : namespaceDirs) { - var guideRootDir = new File(namespaceDir, AUTO_GUIDE_FOLDER); - if (!guideRootDir.isDirectory()) { + var looseNamespaceDirs = resourcePackRoot.listFiles(File::isDirectory); + if (looseNamespaceDirs == null) { + return; + } + for (var namespaceDir : looseNamespaceDirs) { + if ("assets".equals(namespaceDir.getName())) { continue; } + scanResourcePackFolderNamespace(resourcePackRoot, namespaceDir.getName(), discoveredLanguages); + } + } - var languageDirs = guideRootDir.listFiles(File::isDirectory); - if (languageDirs == null) { - continue; - } + private static void scanResourcePackFolder(IResourcePack resourcePack, File resourcePackRoot, + Map> discoveredLanguages) { + for (String namespace : getResourceDomains(resourcePack)) { + scanResourcePackFolderNamespace(resourcePackRoot, namespace, discoveredLanguages); + } + } - for (var languageDir : languageDirs) { - var languageFolder = languageDir.getName(); - if (!isLanguageFolder(languageFolder)) { - continue; - } + private static void scanResourcePackFolderNamespace(File resourcePackRoot, String namespace, + Map> discoveredLanguages) { + if (!isValidNamespace(namespace)) { + return; + } + scanResourcePackFolderNamespaceRoot( + namespace, + new File(resourcePackRoot, toFolderPrefix(namespace, AUTO_GUIDE_FOLDER).replace('/', File.separatorChar)), + discoveredLanguages); + scanResourcePackFolderNamespaceRoot( + namespace, + new File( + resourcePackRoot, + toLooseFolderPrefix(namespace, AUTO_GUIDE_FOLDER).replace('/', File.separatorChar)), + discoveredLanguages); + } - if (!containsMarkdownFiles(languageDir)) { - continue; - } + private static void scanResourcePackFolderNamespaceRoot(String namespace, File guideRootDir, + Map> discoveredLanguages) { + if (!guideRootDir.isDirectory()) { + return; + } + var languageDirs = guideRootDir.listFiles(File::isDirectory); + if (languageDirs == null) { + return; + } + for (var languageDir : languageDirs) { + var languageFolder = languageDir.getName(); + if (!isLanguageFolder(languageFolder)) { + continue; + } - var guideId = new ResourceLocation(namespaceDir.getName(), AUTO_GUIDE_FOLDER); - discoveredLanguages.computeIfAbsent(guideId, ignored -> new LinkedHashSet<>()) - .add(toLanguageCode(languageFolder)); + if (!containsMarkdownFiles(languageDir)) { + continue; } + + var guideId = new ResourceLocation(namespace, AUTO_GUIDE_FOLDER); + discoveredLanguages.computeIfAbsent(guideId, ignored -> new LinkedHashSet<>()) + .add(toLanguageCode(languageFolder)); } } @@ -535,4 +752,20 @@ public static String autoDiscoveredDefaultLanguage() { public static String toFolderPrefix(String namespace, String folder) { return "assets/" + namespace + "/" + folder + "/"; } + + public static String toLooseFolderPrefix(String namespace, String folder) { + return namespace + "/" + folder + "/"; + } + + private static boolean isValidNamespace(String namespace) { + return namespace != null && !namespace.isEmpty() && namespace.indexOf('/') < 0 && namespace.indexOf('\\') < 0; + } + + private static List toList(Iterable resourcePacks) { + var result = new ArrayList(); + for (IResourcePack resourcePack : resourcePacks) { + result.add(resourcePack); + } + return result; + } } diff --git a/src/main/java/com/hfstudio/guidenh/mixins/Mixins.java b/src/main/java/com/hfstudio/guidenh/mixins/Mixins.java index 76c67c96..95f5a90e 100644 --- a/src/main/java/com/hfstudio/guidenh/mixins/Mixins.java +++ b/src/main/java/com/hfstudio/guidenh/mixins/Mixins.java @@ -11,8 +11,10 @@ public enum Mixins implements IMixins { EARLY(Side.CLIENT, "forge.AccessorForgeHooksClient", "forge.AccessorGuiIngameForge", "fml.AccessorFMLClientHandler", - "minecraft.AccessorAbstractResourcePack", "forge.AccessorShapedOreRecipe", "forge.AccessorShapelessOreRecipe", - "minecraft.MixinModelRendererSceneExportCapture", "minecraft.MixinTessellatorSceneExportCapture"), + "minecraft.AccessorAbstractResourcePack", "minecraft.AccessorFallbackResourceManager", + "minecraft.AccessorSimpleReloadableResourceManager", "forge.AccessorShapedOreRecipe", + "forge.AccessorShapelessOreRecipe", "minecraft.MixinModelRendererSceneExportCapture", + "minecraft.MixinTessellatorSceneExportCapture"), BQ_PANEL_HOVER(Side.CLIENT, Phase.LATE, Mods.BetterQuesting, "compat.MixinPanelButtonQuest"), diff --git a/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorFallbackResourceManager.java b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorFallbackResourceManager.java new file mode 100644 index 00000000..2feaf959 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorFallbackResourceManager.java @@ -0,0 +1,16 @@ +package com.hfstudio.guidenh.mixins.early.minecraft; + +import java.util.List; + +import net.minecraft.client.resources.FallbackResourceManager; +import net.minecraft.client.resources.IResourcePack; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(FallbackResourceManager.class) +public interface AccessorFallbackResourceManager { + + @Accessor("resourcePacks") + List guidenh$getResourcePacks(); +} diff --git a/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorSimpleReloadableResourceManager.java b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorSimpleReloadableResourceManager.java new file mode 100644 index 00000000..ae6f6c25 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/mixins/early/minecraft/AccessorSimpleReloadableResourceManager.java @@ -0,0 +1,16 @@ +package com.hfstudio.guidenh.mixins.early.minecraft; + +import java.util.Map; + +import net.minecraft.client.resources.FallbackResourceManager; +import net.minecraft.client.resources.SimpleReloadableResourceManager; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SimpleReloadableResourceManager.class) +public interface AccessorSimpleReloadableResourceManager { + + @Accessor("domainResourceManagers") + Map guidenh$getDomainResourceManagers(); +} From 022d5ecd2e95ffedef2bfdbf89a011b00d66a07e Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:10:26 +0800 Subject: [PATCH 132/136] bugfix --- .../guide/compiler/tags/ImageCompiler.java | 9 +- .../guide/compiler/tags/ItemLinkCompiler.java | 12 +- .../guidenh/guide/internal/GuideScreen.java | 14 +- .../autocomplete/TagAttributeRegistry.java | 1 + .../AttributePresetValueProvider.java | 1 + .../internal/host/scripts/ItemLinkScript.java | 224 ++++++++++++------ .../guide/internal/screen/GuideNavBar.java | 20 ++ .../site/GuideSiteMdxTagRenderer.java | 69 ++++-- .../assets/guidenh/siteexport/app.css | 6 + 9 files changed, 252 insertions(+), 104 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java index 68b66246..6e47a2ed 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ImageCompiler.java @@ -3,11 +3,13 @@ import java.util.Collections; import java.util.Set; +import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.document.block.LytImageBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowParent; +import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; import com.hfstudio.guidenh.libs.mdast.mdx.model.MdxJsxElementFields; public class ImageCompiler extends FlowTagCompiler { @@ -24,9 +26,12 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen String src = el.getAttributeString("src", ""); if (!src.isEmpty()) { - var imageId = compiler.resolveId(src); - if (imageId != null) { + try { + var imageId = IdUtils.resolveLink(src, compiler.getPageId()); block.setSrc(imageId.toString()); + } catch (IllegalArgumentException e) { + GuideDebugLog.error("[GuideNH] [ImageCompiler] Invalid image id: {}", src); + block.setTitle("Invalid image URL: " + src); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java index cb20072a..dabb7f75 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/ItemLinkCompiler.java @@ -30,6 +30,8 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen boolean noTooltip = MdxAttrs.getBoolean(noTooltipAttr, false); Boolean showTooltipAttr = MdxAttrs.getOptionalBoolean(el, "showTooltip"); boolean showTooltip = showTooltipAttr != null ? showTooltipAttr : !noTooltip; + Boolean showTextAttr = MdxAttrs.getOptionalBoolean(el, "showText"); + boolean showText = showTextAttr == null || showTextAttr; // showIcon — null/falsy = no icon; "left", "right", or any truthy = icon at that side String showIconRaw = el.getAttributeString("showIcon", null); @@ -44,9 +46,17 @@ protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElemen link.setData("itemId", itemId != null ? itemId.trim() : null); link.setData("ore", ore != null ? ore.trim() : null); link.setData("showTooltip", showTooltip); + link.setData("showText", showText); link.setData("showIcon", iconPosition); link.setData("linksTo", linksTo); - link.setData("pageId", compiler.getPageId()); + link.setData( + "guideId", + compiler.getGuideId() + .toString()); + link.setData( + "pageId", + compiler.getPageId() + .toString()); compiler.compileFlowContext(el.children(), link); parent.append(link); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 386c8a21..3f26a5f5 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -517,7 +517,7 @@ private GuideScreen(GuideScreenRoute route, @Nullable GuideScreenViewState resto .setPreheatCompiler(pageId -> { if (guide == null) return null; try { - return guide.getPage(new net.minecraft.util.ResourceLocation(pageId)); + return guide.getPage(new ResourceLocation(pageId)); } catch (Exception e) { return null; } @@ -713,6 +713,7 @@ private void restoreViewState(GuideScreenViewState state) { scrollY = 0; snapVisualScrollToTarget(); loadCurrentPage(); + expandNavigationParentsToCurrentPage(); ensureLayout(); scrollToCurrentAnchor(); applyPendingRestoreScroll(); @@ -722,6 +723,13 @@ private void restoreViewState(GuideScreenViewState state) { } } + private void expandNavigationParentsToCurrentPage() { + if (!hasContentRoute()) { + return; + } + navBar.expandParentsTo(resolveNavigationTree(), currentAnchor.pageId(), bookmarkState); + } + private void applyPendingRestoreScroll() { if (pendingRestoreViewState == null || !pendingRestoreViewState.route() .equals(currentRoute)) { @@ -2373,6 +2381,8 @@ private void completePendingContentPageLoadIfNeeded() { if (document != null && isSpecialPageWithSearchField()) { applySpecialPageSearchQuery(queryFromCurrentAnchor()); } + ensureLayout(); + scrollToCurrentAnchor(); syncSearchFieldToCurrentRoute(); if (loadedPage != null) { queuePageSceneRegistrations(loadedPage); @@ -2466,9 +2476,9 @@ private void scrollToCurrentAnchor() { if (!pendingAnchorScroll) return; if (currentAnchor == null || currentAnchor.anchor() == null) return; if (document == null) return; - pendingAnchorScroll = false; var target = new AnchorIndexer(document).get(currentAnchor.anchor()); if (target == null) return; + pendingAnchorScroll = false; var blockNode = target.blockNode(); var flowContent = target.flowContent(); if (flowContent instanceof LytFlowAnchor flowAnchor) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java index 6b8e73a9..39bd6d55 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/TagAttributeRegistry.java @@ -54,6 +54,7 @@ public static void initialize() { new AttributeSpec("ore", AttrType.ORE_DICT), new AttributeSpec("noTooltip", AttrType.BOOLEAN), new AttributeSpec("showTooltip", AttrType.BOOLEAN), + new AttributeSpec("showText", AttrType.BOOLEAN), new AttributeSpec("showIcon", AttrType.STRING), new AttributeSpec("linksTo", AttrType.PAGE_PATH)); register( diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java index c0c4076a..2a37ada8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/autocomplete/provider/AttributePresetValueProvider.java @@ -17,6 +17,7 @@ public class AttributePresetValueProvider implements AutocompleteProvider { static { VALUES.put("showIcon", new String[] { "left", "right", "true", "false" }); VALUES.put("showTooltip", new String[] { "true", "false" }); + VALUES.put("showText", new String[] { "true", "false" }); VALUES.put("noTooltip", new String[] { "true", "false" }); VALUES.put("wrap", new String[] { "inline", "square", "tight", "through" }); VALUES.put("align", new String[] { "left", "center", "right" }); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java index 6164679d..3361c204 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/ItemLinkScript.java @@ -1,5 +1,8 @@ package com.hfstudio.guidenh.guide.internal.host.scripts; +import java.util.ArrayList; +import java.util.List; + import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; @@ -7,22 +10,27 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.GuideAnchor; import com.hfstudio.guidenh.guide.PageAnchor; +import com.hfstudio.guidenh.guide.PageCollection; +import com.hfstudio.guidenh.guide.color.SymbolicColor; import com.hfstudio.guidenh.guide.compiler.IdUtils; import com.hfstudio.guidenh.guide.document.block.LytItemImage; import com.hfstudio.guidenh.guide.document.block.LytParagraph; import com.hfstudio.guidenh.guide.document.flow.LytFlowContent; import com.hfstudio.guidenh.guide.document.flow.LytFlowInlineBlock; import com.hfstudio.guidenh.guide.document.flow.LytFlowLink; +import com.hfstudio.guidenh.guide.document.flow.LytFlowSpan; +import com.hfstudio.guidenh.guide.document.flow.LytFlowText; import com.hfstudio.guidenh.guide.document.flow.LytTooltipSpan; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; -import com.hfstudio.guidenh.guide.indices.ItemIndex; -import com.hfstudio.guidenh.guide.indices.OreIndex; +import com.hfstudio.guidenh.guide.internal.GuideRegistry; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; import com.hfstudio.guidenh.guide.internal.host.LytScript; import com.hfstudio.guidenh.guide.internal.host.ScriptContext; import com.hfstudio.guidenh.guide.internal.host.ScriptType; +import com.hfstudio.guidenh.guide.internal.item.GuideItemTargetResolver; public class ItemLinkScript implements LytScript { @@ -46,6 +54,8 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { String linksTo = (String) link.getData("linksTo"); String iconPosition = (String) link.getData("showIcon"); String currentPage = (String) link.getData("pageId"); + String currentGuide = (String) link.getData("guideId"); + Boolean showText = (Boolean) link.getData("showText"); // Neither target specified if ((itemId == null || itemId.isEmpty()) && (ore == null || ore.isEmpty())) { @@ -66,76 +76,40 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { return; } - PageAnchor anchor = findLinkTarget(stack, linksTo, ctx); - - // Handle fragment-only link (#heading) - if (anchor == null && linksTo != null && linksTo.startsWith("#") && linksTo.length() > 1) { - if (currentPage != null) { - try { - anchor = new PageAnchor(new ResourceLocation(currentPage), linksTo.substring(1)); - } catch (Exception ignored) {} - } + GuideAnchor target = findLinkTarget(stack, linksTo, currentPage, currentGuide, ctx); + if (Boolean.TRUE.equals(showTooltip)) { + link.setTooltip(new ItemTooltip(stack)); } - // Same-page detection - if (anchor != null && currentPage != null - && anchor.pageId() + boolean samePageWithoutAnchor = target != null && currentPage != null + && target.page() + .anchor() == null + && target.page() + .pageId() .toString() - .equals(currentPage)) { + .equals(currentPage); + if (target == null || samePageWithoutAnchor) { LytTooltipSpan span = new LytTooltipSpan(); span.setStyleClass("ItemLink"); - java.util.List linkChildren = new java.util.ArrayList<>(link.getChildren()); - link.getChildren() - .clear(); - for (LytFlowContent child : linkChildren) { - child.setParent(null); - span.append(child); - } + moveChildren(link, span); + appendIconAndFallbackText(span, stack, iconPosition, Boolean.TRUE.equals(showTooltip), showText); if (Boolean.TRUE.equals(showTooltip)) { span.setTooltip(new ItemTooltip(stack)); } - // If the span has no children text (self-closing tag), fall back to item display name - if (span.getChildren() - .isEmpty()) { - span.appendText(stack.getDisplayName()); - } - span.modifyStyle(style -> style.italic(true)); + span.modifyStyle( + style -> style.color(SymbolicColor.GRAY) + .italic(true)); ctx.replace(span); return; } - if (anchor != null) { - link.setPageLink(anchor); - } - if (Boolean.TRUE.equals(showTooltip)) { - link.setTooltip(new ItemTooltip(stack)); - } - - // Show icon support - if (iconPosition != null && !iconPosition.isEmpty() && itemId != null) { - ItemStack iconStack = resolveItemStack(itemId); - if (iconStack != null) { - var img = new LytItemImage(iconStack); - img.setScale(1f); - img.setInline(true); - img.setShowTooltip(Boolean.TRUE.equals(showTooltip)); - var wrapper = new LytFlowInlineBlock(); - wrapper.setBlock(img); - if ("left".equals(iconPosition)) { - link.getChildren() - .add(0, wrapper); - wrapper.setParent(link); - } else { - link.append(wrapper); - } - } - } - - // If the link has no children text (self-closing tag), fall back to item display name - if (link.getChildren() - .isEmpty()) { - link.appendText(stack.getDisplayName()); + ResourceLocation guideId = target.guideId(); + if (guideId != null) { + link.setGuideLink(guideId, target.page()); + } else { + link.setPageLink(target.page()); } + appendIconAndFallbackText(link, stack, iconPosition, Boolean.TRUE.equals(showTooltip), showText); } } @@ -149,32 +123,126 @@ private static ItemStack resolveItemStack(String itemId) { } @Nullable - private static PageAnchor findLinkTarget(ItemStack stack, @Nullable String linksTo, ScriptContext ctx) { - if (linksTo != null && !linksTo.isEmpty()) { - String pagePart = linksTo; + private static GuideAnchor findLinkTarget(ItemStack stack, @Nullable String linksTo, @Nullable String currentPage, + @Nullable String currentGuide, ScriptContext ctx) { + if (linksTo != null && !linksTo.trim() + .isEmpty()) { + ResourceLocation currentPageId; + try { + currentPageId = currentPage != null ? new ResourceLocation(currentPage) : null; + } catch (IllegalArgumentException ignored) { + currentPageId = null; + } + ResourceLocation currentGuideId; + try { + currentGuideId = currentGuide != null ? new ResourceLocation(currentGuide) : null; + } catch (IllegalArgumentException ignored) { + currentGuideId = null; + } + if (currentPageId == null) { + return null; + } + String pagePart = linksTo.trim(); String anchorPart = null; - int hashIdx = linksTo.indexOf('#'); + int hashIdx = pagePart.indexOf('#'); if (hashIdx >= 0) { - pagePart = linksTo.substring(0, hashIdx); - anchorPart = linksTo.substring(hashIdx + 1); + anchorPart = pagePart.substring(hashIdx + 1); + pagePart = pagePart.substring(0, hashIdx); } - if (!pagePart.isEmpty()) { - try { - ResourceLocation pageId = new ResourceLocation(pagePart); - return anchorPart != null ? new PageAnchor(pageId, anchorPart) : PageAnchor.page(pageId); - } catch (Exception ignored) {} + try { + ResourceLocation pageId = pagePart.isEmpty() ? currentPageId + : IdUtils.resolveLink(pagePart, currentPageId); + ResourceLocation guideId = resolveGuideId(currentPageId, currentGuideId, pageId); + if (!pageExists(guideId, pageId, ctx)) { + return null; + } + return new GuideAnchor(guideId, new PageAnchor(pageId, anchorPart)); + } catch (IllegalArgumentException ignored) { + return null; } } - ItemIndex itemIdx = ctx.getIndex(ItemIndex.class); - if (itemIdx != null) { - PageAnchor anchor = itemIdx.findByStack(stack); - if (anchor != null) return anchor; + + var resolved = GuideItemTargetResolver.resolve(stack, GuideRegistry.getAll()); + if (resolved == null || resolved.anchor() == null) { + return null; + } + return new GuideAnchor(resolved.guideId(), resolved.anchor()); + } + + private static ResourceLocation resolveGuideId(ResourceLocation currentPageId, + @Nullable ResourceLocation currentGuideId, ResourceLocation pageId) { + if (currentGuideId != null && pageId.getResourceDomain() + .equals(currentPageId.getResourceDomain())) { + return currentGuideId; + } + return new ResourceLocation( + pageId.getResourceDomain(), + currentGuideId != null ? currentGuideId.getResourcePath() : "guidenh"); + } + + private static boolean pageExists(ResourceLocation guideId, ResourceLocation pageId, ScriptContext ctx) { + PageCollection pageCollection = ctx.getPageCollection(); + if (pageCollection != null && guideId.equals(pageCollection.getId())) { + return pageCollection.pageExists(pageId); + } + var guide = GuideRegistry.getById(guideId); + return guide != null && guide.pageExists(pageId); + } + + private static void appendIconAndFallbackText(LytFlowSpan span, ItemStack stack, @Nullable String iconPosition, + boolean showTooltip, @Nullable Boolean showText) { + boolean shouldShowText = showText == null || showText; + boolean hasText = hasTextChild(span.getChildren()); + LytFlowInlineBlock icon = iconPosition == null || iconPosition.isEmpty() ? null + : createIcon(stack, showTooltip); + if (icon != null && "left".equals(iconPosition)) { + span.getChildren() + .add(0, icon); + icon.setParent(span); + } + if (shouldShowText && !hasText) { + span.appendText(stack.getDisplayName()); + } + if (icon != null && !"left".equals(iconPosition)) { + span.append(icon); + } + } + + @Nullable + private static LytFlowInlineBlock createIcon(ItemStack stack, boolean showTooltip) { + if (stack == null || stack.getItem() == null) { + return null; } - OreIndex oreIdx = ctx.getIndex(OreIndex.class); - if (oreIdx != null) { - return oreIdx.findByStack(stack); + var img = new LytItemImage(stack); + img.setScale(1f); + img.setInline(true); + img.setShowTooltip(showTooltip); + var wrapper = new LytFlowInlineBlock(); + wrapper.setBlock(img); + return wrapper; + } + + private static boolean hasTextChild(List children) { + for (LytFlowContent child : children) { + if (child instanceof LytFlowText text && !text.getText() + .isEmpty()) { + return true; + } + if (child instanceof LytFlowSpan span && hasTextChild(span.getChildren())) { + return true; + } + } + return false; + } + + private static void moveChildren(LytFlowSpan from, LytFlowSpan to) { + List linkChildren = new ArrayList<>(from.getChildren()); + from.getChildren() + .clear(); + for (LytFlowContent child : linkChildren) { + child.setParent(null); + to.append(child); } - return null; } private void replaceFlowError(ScriptContext ctx, String message) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java index 8b333f19..6f621784 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java @@ -243,6 +243,26 @@ public void restoreState(GuideNavBarState state, GuideBookmarkState bookmarkStat } } + public void expandParentsTo(@Nullable NavigationTree tree, @Nullable ResourceLocation pageId, + GuideBookmarkState bookmarkState) { + if (tree == null || pageId == null) { + return; + } + + var path = tree.getPathTo(pageId); + boolean changed = false; + for (int index = 0; index < path.size() - 1; index++) { + ResourceLocation parentPageId = path.get(index) + .pageId(); + if (parentPageId != null) { + changed |= expandedPageIds.add(parentPageId); + } + } + if (changed) { + rebuildRows(tree, bookmarkState); + } + } + private boolean shouldRebuildRows(@Nullable NavigationTree tree, GuideBookmarkState bookmarkState) { return tree != lastTree || lastBookmarkStateVersion != bookmarkState.version() || lastExpandedStateHash != expandedPageIds.hashCode(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java index d796c6c6..7982284b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSiteMdxTagRenderer.java @@ -850,6 +850,7 @@ private String renderItemLink(MdxJsxElementFields element, String defaultNamespa @Nullable ResourceLocation currentPageId, GuideSiteTemplateRegistry templates) { String rawId = readOptional(element, "id"); String rawOre = readOptional(element, "ore"); + String rawLinksTo = readOptional(element, "linksTo"); GuideItemReferenceResolver.ResolvedItemReference item = GuideItemReferenceResolver .resolveItemReference(defaultNamespace, rawId, rawOre); String itemId = resolveItemLabelKey(defaultNamespace, rawId, rawOre, item); @@ -863,6 +864,7 @@ private String renderItemLink(MdxJsxElementFields element, String defaultNamespa boolean noTooltip = readBoolean(noTooltipAttribute, "noTooltip", false); boolean showTooltip = showTooltipAttribute != null ? readBoolean(showTooltipAttribute, "showTooltip", true) : !noTooltip; + boolean showText = readBoolean(element, "showText", true); // showIcon — null/falsy = no icon; "left", "right", or any truthy = icon at that side String iconPosition = ItemImageCompiler.resolveLabelPosition(readOptional(element.getAttribute("showIcon"))); @@ -874,23 +876,13 @@ private String renderItemLink(MdxJsxElementFields element, String defaultNamespa : createTextTooltipTemplate(itemId, templates, currentPageId); } - PageAnchor linksTo = null; - try { - if (item != null && item.stack() != null) { - linksTo = guide.getIndex(ItemIndex.class) - .findByStack(item.stack()); - } - } catch (Exception ignored) {} - if (linksTo == null) { - linksTo = findPageAnchorByItemId(itemId); - } - - String innerHtml = buildItemLinkContent(exportedItem, iconPosition); - boolean samePageLink = linksTo != null && linksTo.anchor() == null + PageAnchor linksTo = resolveItemLinkTarget(rawLinksTo, currentPageId, item, itemId); + String innerHtml = buildItemLinkContent(exportedItem, iconPosition, showText); + boolean samePageWithoutAnchor = linksTo != null && linksTo.anchor() == null && currentPageId != null && currentPageId.equals(linksTo.pageId()); - if (linksTo == null || samePageLink) { - return buildTaggedSpanHtml("guide-item-link guide-tooltip", templateId, innerHtml); + if (linksTo == null || samePageWithoutAnchor) { + return buildTaggedSpanHtml("guide-item-link guide-item-link-muted guide-tooltip", templateId, innerHtml); } NavigationNode targetNode = navigationTree.getNodeById(linksTo.pageId()); @@ -903,16 +895,51 @@ private String renderItemLink(MdxJsxElementFields element, String defaultNamespa innerHtml); } - private String buildItemLinkContent(GuideSiteExportedItem item, @Nullable String iconPosition) { + @Nullable + private PageAnchor resolveItemLinkTarget(@Nullable String rawLinksTo, @Nullable ResourceLocation currentPageId, + @Nullable GuideItemReferenceResolver.ResolvedItemReference item, String itemId) { + if (rawLinksTo != null && !rawLinksTo.trim() + .isEmpty()) { + if (currentPageId == null) { + return null; + } + String target = rawLinksTo.trim(); + String anchorPart = null; + int hashIdx = target.indexOf('#'); + if (hashIdx >= 0) { + anchorPart = target.substring(hashIdx + 1); + target = target.substring(0, hashIdx); + } + try { + ResourceLocation pageId = target.isEmpty() ? currentPageId : IdUtils.resolveLink(target, currentPageId); + return parsedPagesById.containsKey(pageId) ? new PageAnchor(pageId, anchorPart) : null; + } catch (IllegalArgumentException ignored) { + return null; + } + } + + PageAnchor linksTo = null; + try { + if (item != null && item.stack() != null) { + linksTo = guide.getIndex(ItemIndex.class) + .findByStack(item.stack()); + } + } catch (Exception ignored) {} + return linksTo != null ? linksTo : findPageAnchorByItemId(itemId); + } + + private String buildItemLinkContent(GuideSiteExportedItem item, @Nullable String iconPosition, boolean showText) { StringBuilder html = new StringBuilder(); if ("left".equals(iconPosition)) { GuideSiteItemHtml.appendIcon(html, item, "guide-item-link-icon"); } - String name = item.displayName() - .isEmpty() ? item.itemId() : item.displayName(); - html.append("") - .append(escapeHtml(name)) - .append(""); + if (showText) { + String name = item.displayName() + .isEmpty() ? item.itemId() : item.displayName(); + html.append("") + .append(escapeHtml(name)) + .append(""); + } if ("right".equals(iconPosition)) { GuideSiteItemHtml.appendIcon(html, item, "guide-item-link-icon"); } diff --git a/src/main/resources/assets/guidenh/siteexport/app.css b/src/main/resources/assets/guidenh/siteexport/app.css index 882fd460..b8ae4de1 100644 --- a/src/main/resources/assets/guidenh/siteexport/app.css +++ b/src/main/resources/assets/guidenh/siteexport/app.css @@ -897,6 +897,12 @@ p { text-decoration: none; } +.guide-item-link-muted, +.guide-item-link-muted:hover { + color: #aaa; + font-style: italic; +} + .guide-item-summary-text, .guide-item-link-text { line-height: 1.3; From 39102ef23d1a61e078a3e007dd9539ab9a70a7e2 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:20:09 +0800 Subject: [PATCH 133/136] remove unused lang --- .../com/hfstudio/guidenh/guide/internal/GuidebookText.java | 4 ---- src/main/resources/assets/guidenh/lang/en_US.lang | 4 ---- src/main/resources/assets/guidenh/lang/zh_CN.lang | 4 ---- 3 files changed, 12 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java index 503231cd..315edf60 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuidebookText.java @@ -372,10 +372,6 @@ public enum GuidebookText implements LocalizationEnum { RegionWandTooltipPos, RegionWandEntitiesExported, RegionWandSavedSnbt, - Smelting, - Blasting, - ShapelessCrafting, - Crafting, OpenRecipeInNei, FullWidthView, CloseFullWidthView, diff --git a/src/main/resources/assets/guidenh/lang/en_US.lang b/src/main/resources/assets/guidenh/lang/en_US.lang index 8b654529..941aab26 100644 --- a/src/main/resources/assets/guidenh/lang/en_US.lang +++ b/src/main/resources/assets/guidenh/lang/en_US.lang @@ -95,7 +95,6 @@ guideme.gui.config.ui.sceneWheelInteractionDelayMillis=3D Preview Wheel Delay guideme.gui.config.ui.sceneWheelInteractionDelayMillis.tooltip=Controls how long page-wheel scrolling temporarily blocks 3D preview wheel interactions, in milliseconds. # Guidebook UI -guideme.guidebook.Blasting=Blasting guideme.guidebook.Close=Close guideme.guidebook.CloseFullWidthView=Close Full Width View guideme.guidebook.OpenRecipeInNei=Open This Recipe in NEI @@ -144,7 +143,6 @@ guideme.guidebook.CommandStructureSaved=Structure exported to %s guideme.guidebook.CommandStructureServerRequired=This structure command requires GuideNH on the server. guideme.guidebook.CommandUsage=/guide |reload|search > guideme.guidebook.ContentFrom=Content from -guideme.guidebook.Crafting=Crafting guideme.guidebook.FullWidthView=Full Width View guideme.guidebook.GuideEditorAdvancedToggle=Show Advanced Guide Editor Buttons guideme.guidebook.GuideEditorAlertCaution=Caution Alert @@ -475,9 +473,7 @@ guideme.guidebook.SearchNoMatch=(No matches) guideme.guidebook.SearchNoQuery=Enter a Search Query guideme.guidebook.SearchNoResults=No Results guideme.guidebook.SearchPlaceholder=Type to search... -guideme.guidebook.ShapelessCrafting=Crafting (Shapeless) guideme.guidebook.ShowAnnotations=Show Annotations -guideme.guidebook.Smelting=Smelting guideme.guidebook.ToggleBlockStats=Toggle Block Stats guideme.guidebook.ToggleGrid=Toggle Floor Grid guideme.guidebook.ZoomIn=Zoom In diff --git a/src/main/resources/assets/guidenh/lang/zh_CN.lang b/src/main/resources/assets/guidenh/lang/zh_CN.lang index b301f149..edafb7f6 100644 --- a/src/main/resources/assets/guidenh/lang/zh_CN.lang +++ b/src/main/resources/assets/guidenh/lang/zh_CN.lang @@ -95,7 +95,6 @@ guideme.gui.config.ui.sceneWheelInteractionDelayMillis=3D 预览滚轮延迟 guideme.gui.config.ui.sceneWheelInteractionDelayMillis.tooltip=控制页面滚轮滚动后,需要等待多久才重新允许 3D 预览响应滚轮,单位为毫秒。 # Guidebook UI -guideme.guidebook.Blasting=高炉熔炼 guideme.guidebook.Close=关闭 guideme.guidebook.CloseFullWidthView=关闭全宽视图 guideme.guidebook.OpenRecipeInNei=在 NEI 中打开此配方 @@ -144,7 +143,6 @@ guideme.guidebook.CommandStructureSaved=结构已导出到 %s guideme.guidebook.CommandStructureServerRequired=该结构指令需要服务端也安装 GuideNH。 guideme.guidebook.CommandUsage=/guide |reload|search > guideme.guidebook.ContentFrom=内容来自 -guideme.guidebook.Crafting=合成 guideme.guidebook.FullWidthView=全宽视图 guideme.guidebook.GuideEditorAdvancedToggle=显示高级编辑按钮 guideme.guidebook.GuideEditorAlertCaution=小心块 @@ -475,9 +473,7 @@ guideme.guidebook.SearchNoMatch=(无匹配结果) guideme.guidebook.SearchNoQuery=请输入搜索关键词 guideme.guidebook.SearchNoResults=无结果 guideme.guidebook.SearchPlaceholder=键入以搜索... -guideme.guidebook.ShapelessCrafting=合成(无序) guideme.guidebook.ShowAnnotations=显示标注 -guideme.guidebook.Smelting=熔炼 guideme.guidebook.ToggleBlockStats=切换方块统计 guideme.guidebook.ToggleGrid=切换地板网格 guideme.guidebook.ZoomIn=放大 From eb790143af86b24b820a701972ec78797979b909 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:31:57 +0800 Subject: [PATCH 134/136] opti --- .../GuideLightweightReloadService.java | 1 + .../datadriven/DataDrivenGuideLoader.java | 64 ++++++++++++++++++- .../resource/GuideResourceAccess.java | 24 ++++++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index 9201468d..cd83c7e7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -52,6 +52,7 @@ public static void reloadGuides(IResourceManager resourceManager) { RecipeCache.clear(); NeiAnimationTicker.clear(); GuidePageTexture.clear(); + GuideResourceAccess.clearCache(); GuidePageLanguageIndex.clear(); GuideResourceLanguageIndex.clear(); GuideLatexTextureCache.INSTANCE.clearAll(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java index 6e0fffbe..5541a5ff 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Field; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -42,6 +43,7 @@ public class DataDrivenGuideLoader { public static final String AUTO_GUIDE_FOLDER = "guidenh"; public static final String LANGUAGE_FOLDER_PREFIX = "_"; private static final Map, Field> LOOSE_ROOT_FIELDS = new IdentityHashMap<>(); + private static volatile List lastActiveResourcePacks = List.of(); private static volatile List lastResourceManagerResourcePacks = List.of(); private static volatile Map> lastResourceManagerDomainsByPack = Map.of(); @@ -297,7 +299,9 @@ public static List getActiveResourcePacks() { var resourcePacks = new LinkedHashSet(GuideDevelopmentResourcePacks.getConfiguredPacks()); resourcePacks.addAll(lastResourceManagerResourcePacks); addConfiguredResourcePacks(resourcePacks); - return new ArrayList<>(resourcePacks); + var resolved = new ArrayList<>(resourcePacks); + lastActiveResourcePacks = List.copyOf(resolved); + return resolved; } public static List getActiveResourcePacks(IResourceManager resourceManager) { @@ -309,7 +313,14 @@ public static List getActiveResourcePacks(IResourceManager resour lastResourceManagerDomainsByPack = freezeDomainsByPack(domainsByPack); resourcePacks.addAll(resourceManagerResourcePacks); addConfiguredResourcePacks(resourcePacks); - return new ArrayList<>(resourcePacks); + var resolved = new ArrayList<>(resourcePacks); + lastActiveResourcePacks = List.copyOf(resolved); + return resolved; + } + + public static List getLastActiveResourcePacks() { + List snapshot = lastActiveResourcePacks; + return snapshot.isEmpty() ? getActiveResourcePacks() : snapshot; } private static void addConfiguredResourcePacks(LinkedHashSet resourcePacks) { @@ -499,7 +510,7 @@ private static Field discoverLooseRootField(Class resourcePackClass) { public static byte[] readBytes(IResourcePack resourcePack, ResourceLocation resourceLocation) { if (!resourcePack.resourceExists(resourceLocation)) { - return null; + return readLooseBytes(resourcePack, resourceLocation); } try (var input = resourcePack.getInputStream(resourceLocation)) { return GuideResourceAccess.readFully(input); @@ -513,6 +524,47 @@ public static byte[] readBytes(IResourcePack resourcePack, ResourceLocation reso } } + public static byte[] readLooseBytes(IResourcePack resourcePack, ResourceLocation resourceLocation) { + File looseRoot = getLooseResourcePackRoot(resourcePack); + if (looseRoot == null || !looseRoot.isDirectory()) { + return null; + } + + Path root = looseRoot.toPath() + .toAbsolutePath() + .normalize(); + for (String candidate : looseResourceCandidates(resourceLocation)) { + Path path = root.resolve(candidate.replace('/', File.separatorChar)) + .normalize(); + if (!path.startsWith(root) || !Files.isRegularFile(path)) { + continue; + } + try { + return Files.readAllBytes(path); + } catch (IOException e) { + GuideDebugLog.warnAlways( + "[GuideNH] [DataDrivenGuideLoader] Failed to read loose resource {} from resource pack {}", + path, + resourcePack.getPackName(), + e); + return null; + } + } + return null; + } + + private static List looseResourceCandidates(ResourceLocation resourceLocation) { + String namespace = resourceLocation.getResourceDomain(); + String path = resourceLocation.getResourcePath(); + var candidates = new ArrayList(); + candidates.add("assets/" + namespace + "/" + path); + candidates.add(namespace + "/" + path); + if (path.startsWith(AUTO_GUIDE_FOLDER + "/")) { + candidates.add(path); + } + return candidates; + } + public static IResourcePack findResourcePack(ResourceLocation resourceLocation) { return findResourcePack(resourceLocation, getActiveResourcePacks()); } @@ -568,6 +620,12 @@ private static void scanResourcePackFolderNamespace(File resourcePackRoot, Strin resourcePackRoot, toLooseFolderPrefix(namespace, AUTO_GUIDE_FOLDER).replace('/', File.separatorChar)), discoveredLanguages); + if (AUTO_GUIDE_FOLDER.equals(namespace)) { + scanResourcePackFolderNamespaceRoot( + namespace, + new File(resourcePackRoot, AUTO_GUIDE_FOLDER.replace('/', File.separatorChar)), + discoveredLanguages); + } } private static void scanResourcePackFolderNamespaceRoot(String namespace, File guideRootDir, diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/resource/GuideResourceAccess.java b/src/main/java/com/hfstudio/guidenh/guide/internal/resource/GuideResourceAccess.java index 1ac3f6f8..cd2b392f 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/resource/GuideResourceAccess.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/resource/GuideResourceAccess.java @@ -4,6 +4,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import net.minecraft.client.resources.IResource; import net.minecraft.client.resources.IResourceManager; @@ -12,11 +15,18 @@ import org.jetbrains.annotations.Nullable; import com.hfstudio.guidenh.guide.internal.GuideDevelopmentResourcePacks; +import com.hfstudio.guidenh.guide.internal.datadriven.DataDrivenGuideLoader; public class GuideResourceAccess { + private static final ConcurrentMap> FALLBACK_CACHE = new ConcurrentHashMap<>(); + private GuideResourceAccess() {} + public static void clearCache() { + FALLBACK_CACHE.clear(); + } + public static @Nullable byte[] readBytes(IResourceManager resourceManager, ResourceLocation id) { byte[] developmentBytes = GuideDevelopmentResourcePacks.readBytes(id); if (developmentBytes != null) { @@ -29,7 +39,19 @@ private GuideResourceAccess() {} return readFully(input); } } catch (IOException ignored) {} - return null; + + return FALLBACK_CACHE.computeIfAbsent(id, GuideResourceAccess::readFallbackBytes) + .orElse(null); + } + + private static Optional readFallbackBytes(ResourceLocation id) { + for (var resourcePack : DataDrivenGuideLoader.getLastActiveResourcePacks()) { + byte[] bytes = DataDrivenGuideLoader.readLooseBytes(resourcePack, id); + if (bytes != null) { + return Optional.of(bytes); + } + } + return Optional.empty(); } public static @Nullable InputStream openStream(IResourceManager resourceManager, ResourceLocation id) { From b601bbecf0d0aaec69ee0e1731842f137aa2d063 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:35:29 +0800 Subject: [PATCH 135/136] add color & dynamic tooltip integration --- dependencies.gradle | 4 +- .../GuideLightweightReloadService.java | 5 +- .../datadriven/DataDrivenGuideLoader.java | 90 +++++++++++--- .../localization/GuidePageLanguageIndex.java | 36 ++++-- .../GuideResourceLanguageIndex.java | 32 +++-- .../tooltip/GuideChromaticTooltipCompat.java | 45 +++++++ .../tooltip/GuideItemTooltipLines.java | 16 ++- .../guidenh/guide/layout/FontMetrics.java | 4 + .../guidenh/guide/layout/LayoutContext.java | 5 + .../guide/layout/MinecraftFontMetrics.java | 28 +++-- .../guide/layout/flow/LineBuilder.java | 112 +++++++++++++++++- .../MediaWikiSpecialContributors.java | 21 ++-- .../guidenh/guide/render/GuideFontCompat.java | 110 +++++++++++++++++ .../guide/render/VanillaRenderContext.java | 58 ++------- .../level/GuidebookPreviewBlockPlacer.java | 13 +- .../ponder/PonderKeyframeAnnotation.java | 24 ++-- .../guidenh/guide/siteexport/ExportTask.java | 14 +-- .../site/GuideSitePageCollector.java | 33 +++--- .../hfstudio/guidenh/integration/Mods.java | 1 + 19 files changed, 499 insertions(+), 152 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideChromaticTooltipCompat.java create mode 100644 src/main/java/com/hfstudio/guidenh/guide/render/GuideFontCompat.java diff --git a/dependencies.gradle b/dependencies.gradle index 396548a8..5815c5eb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -23,15 +23,17 @@ dependencies { devOnlyNonPublishable("com.github.GTNewHorizons:nei-custom-diagram:1.8.20:dev") devOnlyNonPublishable("com.github.GTNewHorizons:EnhancedLootBags:1.3.4:dev") devOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.40-GTNH:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:Angelica:2.1.32:dev") + devOnlyNonPublishable("com.github.GTNewHorizons:TX-Loader:1.8.11:dev") devOnlyNonPublishable("curse.maven:cofh-core-69162:2388751") runtimeOnlyNonPublishable("com.github.GTNewHorizons:Baubles-Expanded:2.2.13-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.16-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botanic-horizons:1.12.8-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:BlockRenderer6343:1.4.12:dev") { transitive = false } - runtimeOnlyNonPublishable("com.github.GTNewHorizons:Angelica:2.1.19:dev") runtimeOnlyNonPublishable(rfg.deobf("curse.maven:spark-361579:4271867")) + compileOnlyApi("com.github.slprime:ChromaticTooltips:1.0.28-GTNH:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:TinkersConstruct:1.14.64-GTNH:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:Mantle:0.5.1:dev") { transitive = false } compileOnlyApi("com.github.GTNewHorizons:Mobs-Info:0.5.14-GTNH:dev") { transitive = false } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java index cd83c7e7..dd648e2e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideLightweightReloadService.java @@ -49,6 +49,7 @@ public static void reloadGuides(IResourceManager resourceManager) { GuideDebugLog.info("[GuideNH] [GuideLightweightReloadService] Reloading guide data..."); long startedAt = System.nanoTime(); var activeResourcePacks = DataDrivenGuideLoader.getActiveResourcePacks(resourceManager); + DataDrivenGuideLoader.clearCaches(); RecipeCache.clear(); NeiAnimationTicker.clear(); GuidePageTexture.clear(); @@ -302,11 +303,11 @@ private static ParsedGuidePage parsePageBytes(String sourcePack, String language } } - static @Nullable byte[] selectPageCandidate(ResourceLocation sourceId) { + static byte @Nullable [] selectPageCandidate(ResourceLocation sourceId) { return selectPageCandidate(sourceId, DataDrivenGuideLoader.getActiveResourcePacks()); } - static @Nullable byte[] selectPageCandidate(ResourceLocation sourceId, + static byte @Nullable [] selectPageCandidate(ResourceLocation sourceId, Iterable resourcePacks) { GuidePageResourceSelector.SelectedPageResource winner = GuidePageResourceSelector .select(sourceId, resourcePacks); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java index 5541a5ff..750a2aae 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/datadriven/DataDrivenGuideLoader.java @@ -46,6 +46,8 @@ public class DataDrivenGuideLoader { private static volatile List lastActiveResourcePacks = List.of(); private static volatile List lastResourceManagerResourcePacks = List.of(); private static volatile Map> lastResourceManagerDomainsByPack = Map.of(); + private static volatile GuideLanguageDiscoverySnapshot lastGuideLanguageDiscovery = GuideLanguageDiscoverySnapshot + .empty(); private DataDrivenGuideLoader() {} @@ -63,11 +65,8 @@ public static Map load(Iterable>(); stageStartedAt = System.nanoTime(); - for (var resourcePack : resolvedResourcePacks) { - scanResourcePack(resourcePack, discoveredLanguages); - } + var discoveredLanguages = discoverGuideLanguages(resolvedResourcePacks); long scanNs = System.nanoTime() - stageStartedAt; stageStartedAt = System.nanoTime(); @@ -97,6 +96,28 @@ public static Map load(Iterable> discoverGuideLanguages() { + return discoverGuideLanguages(getActiveResourcePacks()); + } + + public static Map> discoverGuideLanguages( + Iterable activeResourcePacks) { + var resolvedResourcePacks = toList(activeResourcePacks); + GuideLanguageDiscoverySnapshot cached = lastGuideLanguageDiscovery; + if (cached.matches(resolvedResourcePacks)) { + return cached.discoveredLanguages(); + } + + var discoveredLanguages = new LinkedHashMap>(); + for (var resourcePack : resolvedResourcePacks) { + scanResourcePack(resourcePack, discoveredLanguages); + } + + var frozen = freezeDiscoveredLanguages(discoveredLanguages); + lastGuideLanguageDiscovery = new GuideLanguageDiscoverySnapshot(List.copyOf(resolvedResourcePacks), frozen); + return frozen; + } + public static LinkedHashMap> discoverPagePaths(String folder) { return discoverPagePaths(folder, getActiveResourcePacks()); } @@ -124,18 +145,18 @@ public static LinkedHashMap> discoverPagePaths(Str return pagePaths; } - private static int countDiscoveredLanguages(Map> discoveredLanguages) { + private static int countDiscoveredPagePaths(LinkedHashMap> pagePaths) { int total = 0; - for (var languages : discoveredLanguages.values()) { - total += languages.size(); + for (var namespacePaths : pagePaths.values()) { + total += namespacePaths.size(); } return total; } - private static int countDiscoveredPagePaths(LinkedHashMap> pagePaths) { + private static int countDiscoveredLanguages(Map> discoveredLanguages) { int total = 0; - for (var namespacePaths : pagePaths.values()) { - total += namespacePaths.size(); + for (var languages : discoveredLanguages.values()) { + total += languages.size(); } return total; } @@ -185,8 +206,9 @@ private static void scanPagePathsForNamespaceRoot(File resourcePackRoot, String } var discovered = new LinkedHashSet(); - scanFolderPagePaths(resourcePackRoot, toFolderPrefix(namespace, folder), discovered); - scanFolderPagePaths(resourcePackRoot, toLooseFolderPrefix(namespace, folder), discovered); + for (String prefix : pagePathPrefixes(namespace, folder)) { + scanFolderPagePaths(resourcePackRoot, prefix, discovered); + } if (!discovered.isEmpty()) { pagePaths.computeIfAbsent(namespace, k -> new LinkedHashSet<>()) .addAll(discovered); @@ -288,8 +310,9 @@ public static void scanPagePathsForNamespace(IResourcePack resourcePack, String public static void scanPagePathsForNamespace(File resourcePackRoot, String namespace, String folder, Set pagePaths) { if (resourcePackRoot.isDirectory()) { - scanFolderPagePaths(resourcePackRoot, toFolderPrefix(namespace, folder), pagePaths); - scanFolderPagePaths(resourcePackRoot, toLooseFolderPrefix(namespace, folder), pagePaths); + for (String prefix : pagePathPrefixes(namespace, folder)) { + scanFolderPagePaths(resourcePackRoot, prefix, pagePaths); + } } else { scanZipPagePaths(resourcePackRoot, toFolderPrefix(namespace, folder), pagePaths); } @@ -815,6 +838,16 @@ public static String toLooseFolderPrefix(String namespace, String folder) { return namespace + "/" + folder + "/"; } + private static List pagePathPrefixes(String namespace, String folder) { + var prefixes = new ArrayList(3); + prefixes.add(toFolderPrefix(namespace, folder)); + prefixes.add(toLooseFolderPrefix(namespace, folder)); + if (folder.equals(namespace)) { + prefixes.add(folder + "/"); + } + return prefixes; + } + private static boolean isValidNamespace(String namespace) { return namespace != null && !namespace.isEmpty() && namespace.indexOf('/') < 0 && namespace.indexOf('\\') < 0; } @@ -826,4 +859,33 @@ private static List toList(Iterable reso } return result; } + + public static void clearCaches() { + lastGuideLanguageDiscovery = GuideLanguageDiscoverySnapshot.empty(); + } + + private static Map> freezeDiscoveredLanguages( + Map> discoveredLanguages) { + if (discoveredLanguages.isEmpty()) { + return Map.of(); + } + + var frozen = new LinkedHashMap>(discoveredLanguages.size()); + for (var entry : discoveredLanguages.entrySet()) { + frozen.put(entry.getKey(), Set.copyOf(entry.getValue())); + } + return Collections.unmodifiableMap(frozen); + } + + private record GuideLanguageDiscoverySnapshot(List resourcePacks, + Map> discoveredLanguages) { + + private static GuideLanguageDiscoverySnapshot empty() { + return new GuideLanguageDiscoverySnapshot(List.of(), Map.of()); + } + + private boolean matches(List otherResourcePacks) { + return !resourcePacks.isEmpty() && resourcePacks.equals(otherResourcePacks); + } + } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java index 2c7e76b2..f4940a09 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuidePageLanguageIndex.java @@ -65,7 +65,7 @@ public static Map readPageKeys(InputStream input) throws IOExcep private static Map loadLanguage(String normalizedLanguage) { long startedAt = System.nanoTime(); Map merged = new LinkedHashMap<>(); - var activeResourcePacks = DataDrivenGuideLoader.getActiveResourcePacks(); + var activeResourcePacks = DataDrivenGuideLoader.getLastActiveResourcePacks(); for (IResourcePack resourcePack : activeResourcePacks) { loadResourcePackLanguage(resourcePack, normalizedLanguage, merged); } @@ -83,7 +83,7 @@ private static Map loadLanguage(String normalizedLanguage) { private static void loadResourcePackLanguage(IResourcePack resourcePack, String normalizedLanguage, Map target) { - File resourcePackFile = DataDrivenGuideLoader.getResourcePackFile(resourcePack); + File resourcePackFile = DataDrivenGuideLoader.getLooseResourcePackRoot(resourcePack); if (resourcePackFile == null || !resourcePackFile.exists()) { return; } @@ -98,19 +98,33 @@ private static void loadDirectoryLanguage(File resourcePackRoot, String normaliz Map target) { File assetsDir = new File(resourcePackRoot, "assets"); File[] namespaceDirs = assetsDir.listFiles(File::isDirectory); - if (namespaceDirs == null) { - return; + if (namespaceDirs != null) { + for (File namespaceDir : namespaceDirs) { + loadDirectoryLanguageNamespace(namespaceDir, normalizedLanguage, target); + } } - for (File namespaceDir : namespaceDirs) { - File langDir = new File(namespaceDir, "lang"); - if (!langDir.isDirectory()) { + File[] looseNamespaceDirs = resourcePackRoot.listFiles(File::isDirectory); + if (looseNamespaceDirs == null) { + return; + } + for (File namespaceDir : looseNamespaceDirs) { + if ("assets".equals(namespaceDir.getName())) { continue; } - loadDirectoryLanguageEntries(langDir, normalizedLanguage, target); + loadDirectoryLanguageNamespace(namespaceDir, normalizedLanguage, target); } } + private static void loadDirectoryLanguageNamespace(File namespaceDir, String normalizedLanguage, + Map target) { + File langDir = new File(namespaceDir, "lang"); + if (!langDir.isDirectory()) { + return; + } + loadDirectoryLanguageEntries(langDir, normalizedLanguage, target); + } + private static void loadDirectoryLanguageEntries(File directory, String normalizedLanguage, Map target) { File[] children = directory.listFiles(); @@ -146,7 +160,7 @@ private static void loadZipLanguage(File resourcePackFile, String normalizedLang continue; } String path = entry.getName(); - if (!path.startsWith("assets/") || !path.contains("/lang/")) { + if (!isLangZipPath(path)) { continue; } int fileNameStart = path.lastIndexOf('/') + 1; @@ -177,6 +191,10 @@ private static boolean isMatchingLangFile(String fileName, String normalizedLang .equals(normalizedLanguage); } + private static boolean isLangZipPath(String path) { + return path.contains("/lang/"); + } + private static void mergePageKeys(InputStream input, Map target) throws IOException { target.putAll(readPageKeys(input)); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java index 1a5a36c3..43b1b648 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/localization/GuideResourceLanguageIndex.java @@ -43,7 +43,7 @@ public static void clear() { private static Map load(String normalizedLanguage) { long startedAt = System.nanoTime(); Map merged = new LinkedHashMap<>(); - var activeResourcePacks = DataDrivenGuideLoader.getActiveResourcePacks(); + var activeResourcePacks = DataDrivenGuideLoader.getLastActiveResourcePacks(); for (IResourcePack resourcePack : activeResourcePacks) { loadResourcePackLanguage(resourcePack, normalizedLanguage, merged); } @@ -61,7 +61,7 @@ private static Map load(String normalizedLanguage) { private static void loadResourcePackLanguage(IResourcePack resourcePack, String normalizedLanguage, Map target) { - File resourcePackFile = DataDrivenGuideLoader.getResourcePackFile(resourcePack); + File resourcePackFile = DataDrivenGuideLoader.getLooseResourcePackRoot(resourcePack); if (resourcePackFile == null || !resourcePackFile.exists()) { return; } @@ -76,17 +76,31 @@ private static void loadDirectoryLanguage(File resourcePackRoot, String normaliz Map target) { File assetsDir = new File(resourcePackRoot, "assets"); File[] namespaceDirs = assetsDir.listFiles(File::isDirectory); - if (namespaceDirs == null) { - return; + if (namespaceDirs != null) { + for (File namespaceDir : namespaceDirs) { + loadDirectoryLanguageNamespace(namespaceDir, normalizedLanguage, target); + } } - for (File namespaceDir : namespaceDirs) { - File langDir = new File(namespaceDir, "lang"); - if (!langDir.isDirectory()) { + File[] looseNamespaceDirs = resourcePackRoot.listFiles(File::isDirectory); + if (looseNamespaceDirs == null) { + return; + } + for (File namespaceDir : looseNamespaceDirs) { + if ("assets".equals(namespaceDir.getName())) { continue; } - loadDirectoryLanguageEntries(langDir, normalizedLanguage, target); + loadDirectoryLanguageNamespace(namespaceDir, normalizedLanguage, target); + } + } + + private static void loadDirectoryLanguageNamespace(File namespaceDir, String normalizedLanguage, + Map target) { + File langDir = new File(namespaceDir, "lang"); + if (!langDir.isDirectory()) { + return; } + loadDirectoryLanguageEntries(langDir, normalizedLanguage, target); } private static void loadDirectoryLanguageEntries(File directory, String normalizedLanguage, @@ -124,7 +138,7 @@ private static void loadZipLanguage(File resourcePackFile, String normalizedLang continue; } String path = entry.getName(); - if (!path.startsWith("assets/") || !path.contains("/lang/")) { + if (!path.contains("/lang/")) { continue; } int fileNameStart = path.lastIndexOf('/') + 1; diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideChromaticTooltipCompat.java b/src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideChromaticTooltipCompat.java new file mode 100644 index 00000000..43aa6c24 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideChromaticTooltipCompat.java @@ -0,0 +1,45 @@ +package com.hfstudio.guidenh.guide.internal.tooltip; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import com.slprime.chromatictooltips.TooltipHandler; +import com.slprime.chromatictooltips.api.ITooltipComponent; +import com.slprime.chromatictooltips.component.DyncamicTextComponent; + +public class GuideChromaticTooltipCompat { + + private static final String COMPONENT_PREFIX = "\u00A7z"; + + protected GuideChromaticTooltipCompat() {} + + public static List expandLine(@Nullable String line) { + if (line == null) { + return List.of(""); + } + if (!line.startsWith(COMPONENT_PREFIX)) { + return List.of(line); + } + + ITooltipComponent component = TooltipHandler.getTooltipComponent(line); + if (!(component instanceof DyncamicTextComponent dynamicTextComponent)) { + return List.of(line); + } + String text = dynamicTextComponent.getHandler() + .get(); + if (text == null) { + return List.of(line); + } + return splitLines(text); + } + + private static List splitLines(String text) { + String[] parts = text.split("\n", -1); + List lines = new ArrayList<>(parts.length); + Collections.addAll(lines, parts); + return lines.isEmpty() ? List.of("") : lines; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideItemTooltipLines.java b/src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideItemTooltipLines.java index 9ea70d56..37440a70 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideItemTooltipLines.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/tooltip/GuideItemTooltipLines.java @@ -9,6 +9,7 @@ import com.hfstudio.guidenh.guide.document.interaction.ItemTooltip; import com.hfstudio.guidenh.guide.document.interaction.ItemTooltipAppender; +import com.hfstudio.guidenh.integration.Mods; public class GuideItemTooltipLines { @@ -26,11 +27,20 @@ private static String safeDisplayName(ItemStack stack) { public static List build(ItemTooltip tooltip, Minecraft mc) { ItemStack stack = tooltip.getStack(); - List lines; + List rawLines; try { - lines = new ArrayList<>(stack.getTooltip(mc.thePlayer, mc.gameSettings.advancedItemTooltips)); + rawLines = new ArrayList<>(stack.getTooltip(mc.thePlayer, mc.gameSettings.advancedItemTooltips)); } catch (Throwable t) { - lines = new ArrayList<>(); + rawLines = new ArrayList<>(); + } + + List lines = new ArrayList<>(rawLines.size()); + for (String rawLine : rawLines) { + if (Mods.ChromaticTooltips.isModLoaded()) { + lines.addAll(GuideChromaticTooltipCompat.expandLine(rawLine)); + } else { + lines.add(rawLine); + } } if (lines.isEmpty()) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java index aad97361..53214d36 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/FontMetrics.java @@ -6,6 +6,10 @@ public interface FontMetrics { float getAdvance(int codePoint, ResolvedTextStyle style); + default float getRenderedAdvance(int codePoint, ResolvedTextStyle style, boolean hasVisibleGlyphBefore) { + return getAdvance(codePoint, style); + } + int getLineHeight(ResolvedTextStyle style); default int getStringWidth(String text, ResolvedTextStyle style) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/LayoutContext.java b/src/main/java/com/hfstudio/guidenh/guide/layout/LayoutContext.java index a2232ec6..3b131c4c 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/LayoutContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/LayoutContext.java @@ -132,6 +132,11 @@ public float getAdvance(int codePoint, ResolvedTextStyle style) { return fontMetrics.getAdvance(codePoint, style); } + @Override + public float getRenderedAdvance(int codePoint, ResolvedTextStyle style, boolean hasVisibleGlyphBefore) { + return fontMetrics.getRenderedAdvance(codePoint, style, hasVisibleGlyphBefore); + } + @Override public int getLineHeight(ResolvedTextStyle style) { return fontMetrics.getLineHeight(style); diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java b/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java index e4f7adce..fe400d61 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/MinecraftFontMetrics.java @@ -3,6 +3,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; +import com.hfstudio.guidenh.guide.render.GuideFontCompat; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; public class MinecraftFontMetrics implements FontMetrics { @@ -19,15 +20,28 @@ public MinecraftFontMetrics(FontRenderer font) { @Override public float getAdvance(int codePoint, ResolvedTextStyle style) { - char ch = codePoint <= 0xFFFF ? (char) codePoint : '?'; - float raw = font.getCharWidth(ch); - if (style == null) { - return raw; + boolean bold = style != null && style.bold(); + float raw = GuideFontCompat.getRenderedAdvance(font, codePoint, bold, false); + if (raw <= 0f) { + return 0f; } - if (style.bold() && raw > 0) { - raw += 1f; + float scale = style != null ? style.fontScale() : 1f; + return scale == 1f ? raw : raw * scale; + } + + @Override + public int getStringWidth(String text, ResolvedTextStyle style) { + return GuideFontCompat.getStringWidth(font, text, style); + } + + @Override + public float getRenderedAdvance(int codePoint, ResolvedTextStyle style, boolean hasVisibleGlyphBefore) { + boolean bold = style != null && style.bold(); + float raw = GuideFontCompat.getRenderedAdvance(font, codePoint, bold, hasVisibleGlyphBefore); + if (raw <= 0f) { + return 0f; } - float scale = style.fontScale(); + float scale = style != null ? style.fontScale() : 1f; return scale == 1f ? raw : raw * scale; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java index 5b577d5f..8776bd0e 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java +++ b/src/main/java/com/hfstudio/guidenh/guide/layout/flow/LineBuilder.java @@ -20,6 +20,7 @@ import com.hfstudio.guidenh.guide.document.flow.LytFlowText; import com.hfstudio.guidenh.guide.document.flow.LytSpoilerSpan; import com.hfstudio.guidenh.guide.layout.LayoutContext; +import com.hfstudio.guidenh.guide.render.GuideFontCompat; import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; import com.hfstudio.guidenh.guide.style.TextAlignment; import com.hfstudio.guidenh.guide.style.TextStyle; @@ -198,6 +199,7 @@ private LineElement getEndOfOpenLine() { } private void appendText(String text, LytFlowContent flowContent) { + String layoutText = GuideFontCompat.preprocessText(text); var resolvedStyle = flowContent.resolveStyle(); var resolvedHoverStyle = flowContent.resolveHoverStyle(resolvedStyle); var spoiler = flowContent.findAncestor(LytSpoilerSpan.class); @@ -217,13 +219,13 @@ private void appendText(String text, LytFlowContent flowContent) { char lastChar = '\0'; var endOfOpenLine = getEndOfOpenLine(); if (endOfOpenLine instanceof LineTextRun textRun && !textRun.text.isEmpty()) { - lastChar = textRun.text.charAt(textRun.text.length() - 1); + lastChar = findLastVisibleChar(textRun.text); } else if (endOfOpenLine == null || endOfOpenLine.floating) { // Treat the first text in a line or text directly after a float as if it was after a line-break. lastChar = '\n'; } - iterateRuns(text, finalStyle, lastChar, (run, width, endLine) -> { + iterateRuns(layoutText, finalStyle, lastChar, (run, width, endLine) -> { if (!run.isEmpty()) { var el = new LineTextRun(run.toString(), finalStyle, finalRevealStyle, finalHoverStyle); el.flowContent = flowContent; @@ -276,11 +278,21 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh boolean lastCharWasWhitespace = Character.isWhitespace(lastChar); boolean canBreakAtStart = lastCharWasWhitespace; + boolean bold = style.bold(); + boolean visibleGlyphSeen = false; for (var i = 0; i < text.length(); i++) { char ch = text.charAt(i); int codePoint = ch; + if (GuideFontCompat.isFormattingCodeStart(text, i)) { + appendCharToLineBuffer(ch, 0f); + char formatChar = text.charAt(++i); + appendCharToLineBuffer(formatChar, 0f); + bold = GuideFontCompat.determineBold(bold, formatChar); + continue; + } + // UTF-16 surrogate handling if (Character.isHighSurrogate(ch) && i + 1 < text.length()) { // Always consume the next char if it's a low surrogate @@ -315,7 +327,7 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh lastCharWasWhitespace = false; } - var advance = context.getAdvance(codePoint, style); + var advance = context.getRenderedAdvance(codePoint, style, visibleGlyphSeen); // Break line if necessary if (curLineWidth + advance > remainingLineWidth) { int precedingBreakOpportunity; @@ -343,11 +355,13 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh consumer .visitRun(lineBuffer.subSequence(0, precedingBreakOpportunity), widthAtBreakOpportunity, true); - curLineWidth -= widthAtBreakOpportunity; deleteLineBufferPrefix(precedingBreakOpportunity); if (!lineBuffer.isEmpty() && Character.isWhitespace(lineBuffer.charAt(0))) { - curLineWidth -= deleteLineBufferPrefix(1); + deleteLineBufferPrefix(1); } + curLineWidth = rebuildLineBufferWidths(style); + bold = resolveTrailingBold(style, lineBuffer); + visibleGlyphSeen = containsVisibleGlyph(lineBuffer); } else { // We exceeded the line length, but did not find a break opportunity // this causes a forced break mid-word @@ -355,6 +369,8 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh lineBuffer.setLength(0); lineBufferPrefixWidths[0] = 0f; curLineWidth = 0; + bold = style.bold(); + visibleGlyphSeen = false; } remainingLineWidth = getAvailableHorizontalSpace(); // If a white-space character broke the line, ignore it as it @@ -365,6 +381,9 @@ private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastCh } curLineWidth += advance; appendCodePointToLineBuffer(codePoint, advance); + if (advance > 0f) { + visibleGlyphSeen = true; + } } if (!lineBuffer.isEmpty()) { @@ -383,6 +402,14 @@ private void appendCodePointToLineBuffer(int codePoint, float advance) { } } + private void appendCharToLineBuffer(char character, float advance) { + int previousLength = lineBuffer.length(); + float bufferWidth = lineBufferPrefixWidths[previousLength] + advance; + lineBuffer.append(character); + ensureLineBufferPrefixCapacity(previousLength + 2); + lineBufferPrefixWidths[previousLength + 1] = bufferWidth; + } + private float deleteLineBufferPrefix(int charCount) { if (charCount <= 0) { return 0f; @@ -400,6 +427,81 @@ private float deleteLineBufferPrefix(int charCount) { return removedWidth; } + private float rebuildLineBufferWidths(ResolvedTextStyle style) { + ensureLineBufferPrefixCapacity(lineBuffer.length() + 1); + lineBufferPrefixWidths[0] = 0f; + float width = 0f; + boolean bold = style.bold(); + boolean visibleGlyphSeen = false; + for (int index = 0; index < lineBuffer.length(); index++) { + char character = lineBuffer.charAt(index); + if (GuideFontCompat.isFormattingCodeStart(lineBuffer, index)) { + lineBufferPrefixWidths[index + 1] = width; + char formatChar = lineBuffer.charAt(index + 1); + bold = GuideFontCompat.determineBold(bold, formatChar); + lineBufferPrefixWidths[index + 2] = width; + index++; + continue; + } + int codePoint = character; + if (Character.isHighSurrogate(character) && index + 1 < lineBuffer.length()) { + char low = lineBuffer.charAt(index + 1); + if (Character.isLowSurrogate(low)) { + codePoint = Character.toCodePoint(character, low); + } + } + float advance = context.getRenderedAdvance(codePoint, style, visibleGlyphSeen); + width += Math.max(0f, advance); + int charCount = Character.charCount(codePoint); + for (int offset = 1; offset <= charCount; offset++) { + lineBufferPrefixWidths[index + offset] = width; + } + index += charCount - 1; + if (advance > 0f) { + visibleGlyphSeen = true; + } + } + return width; + } + + private boolean resolveTrailingBold(ResolvedTextStyle style, CharSequence text) { + boolean bold = style.bold(); + for (int index = 0; index < text.length() - 1; index++) { + if (!GuideFontCompat.isFormattingCodeStart(text, index)) { + continue; + } + bold = GuideFontCompat.determineBold(bold, text.charAt(index + 1)); + index++; + } + return bold; + } + + private boolean containsVisibleGlyph(CharSequence text) { + for (int index = 0; index < text.length(); index++) { + if (GuideFontCompat.isFormattingCodeStart(text, index)) { + index++; + continue; + } + return true; + } + return false; + } + + private char findLastVisibleChar(CharSequence text) { + for (int index = text.length() - 1; index >= 0; index--) { + char character = text.charAt(index); + if (character == GuideFontCompat.FORMATTING_CHAR && index + 1 < text.length()) { + continue; + } + if (index > 0 && text.charAt(index - 1) == GuideFontCompat.FORMATTING_CHAR) { + index--; + continue; + } + return character; + } + return '\0'; + } + private void ensureLineBufferPrefixCapacity(int requiredLength) { if (requiredLength <= lineBufferPrefixWidths.length) { return; diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialContributors.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialContributors.java index 799c10c3..ae4316a9 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialContributors.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialContributors.java @@ -1,20 +1,20 @@ package com.hfstudio.guidenh.guide.mediawiki; -import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.IResource; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; +import org.jetbrains.annotations.Nullable; + import com.github.bsideup.jabel.Desugar; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.hfstudio.guidenh.guide.Guide; +import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; public class MediaWikiSpecialContributors { @@ -72,15 +72,14 @@ private String resolveKey(String key) { return translated != null && !translated.equals(key) ? translated : key; } - private byte @org.jetbrains.annotations.Nullable [] loadContributorsBytes() { + private byte @Nullable [] loadContributorsBytes() { + var minecraft = Minecraft.getMinecraft(); + if (minecraft == null || minecraft.getResourceManager() == null) { + return null; + } try { - IResource resource = Minecraft.getMinecraft() - .getResourceManager() - .getResource(CONTRIBUTORS_ID); - try (InputStream stream = resource.getInputStream()) { - return org.apache.commons.io.IOUtils.toByteArray(stream); - } - } catch (IOException ignored) { + return GuideResourceAccess.readBytes(minecraft.getResourceManager(), CONTRIBUTORS_ID); + } catch (RuntimeException ignored) { return null; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/GuideFontCompat.java b/src/main/java/com/hfstudio/guidenh/guide/render/GuideFontCompat.java new file mode 100644 index 00000000..0b782655 --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/guide/render/GuideFontCompat.java @@ -0,0 +1,110 @@ +package com.hfstudio.guidenh.guide.render; + +import net.minecraft.client.gui.FontRenderer; + +import com.gtnewhorizon.gtnhlib.util.font.FontRendering; +import com.gtnewhorizon.gtnhlib.util.font.IFontParameters; +import com.hfstudio.guidenh.guide.style.ResolvedTextStyle; + +public class GuideFontCompat { + + public static final char FORMATTING_CHAR = '\u00a7'; + private static final String FORMAT_BOLD = "\u00a7l"; + private static final String FORMAT_ITALIC = "\u00a7o"; + private static final String FORMAT_STRIKETHROUGH = "\u00a7m"; + private static final String FORMAT_OBFUSCATED = "\u00a7k"; + + protected GuideFontCompat() {} + + public static String preprocessText(String text) { + return text == null ? null : FontRendering.preprocessText(text); + } + + public static String buildStyledText(String text, ResolvedTextStyle style) { + if (text == null || text.isEmpty() || style == null) { + return text; + } + StringBuilder builder = null; + if (style.bold() || style.italic() || style.strikethrough() || style.obfuscated()) { + builder = new StringBuilder(text.length() + 8); + if (style.bold()) { + builder.append(FORMAT_BOLD); + } + if (style.italic()) { + builder.append(FORMAT_ITALIC); + } + if (style.strikethrough()) { + builder.append(FORMAT_STRIKETHROUGH); + } + if (style.obfuscated()) { + builder.append(FORMAT_OBFUSCATED); + } + } + return builder != null ? builder.append(text) + .toString() : text; + } + + public static String prepareRenderedText(String text, ResolvedTextStyle style) { + return preprocessText(buildStyledText(text, style)); + } + + public static int getStringWidth(FontRenderer fontRenderer, String text) { + return FontRendering.getStringWidth(text, fontRenderer); + } + + public static int getStringWidth(FontRenderer fontRenderer, String text, ResolvedTextStyle style) { + int rawWidth = getStringWidth(fontRenderer, buildStyledText(text, style)); + if (style != null && style.italic() && text != null && !text.isEmpty()) { + rawWidth += 2; + } + float scale = style != null ? style.fontScale() : 1f; + return Math.round(rawWidth * scale); + } + + public static float getCharacterWidth(FontRenderer fontRenderer, int codePoint) { + char character = codePoint <= Character.MAX_VALUE ? (char) codePoint : '?'; + if (fontRenderer instanceof IFontParameters parameters) { + return parameters.getCharWidthFine(character); + } + return fontRenderer.getCharWidth(character); + } + + public static float getGlyphSpacing(FontRenderer fontRenderer) { + if (fontRenderer instanceof IFontParameters parameters) { + return parameters.getGlyphSpacing(); + } + return 0f; + } + + public static float getRenderedAdvance(FontRenderer fontRenderer, int codePoint, boolean bold, + boolean hasVisibleGlyphBefore) { + float width = getCharacterWidth(fontRenderer, codePoint); + if (width <= 0f) { + return width; + } + if (hasVisibleGlyphBefore) { + width += getGlyphSpacing(fontRenderer); + } + if (bold) { + width += 1f; + } + return width; + } + + public static boolean isFormattingCodeStart(CharSequence text, int index) { + return text != null && index >= 0 && index + 1 < text.length() && text.charAt(index) == FORMATTING_CHAR; + } + + public static boolean determineBold(boolean wasBold, char formatChar) { + char normalized = Character.toLowerCase(formatChar); + if (normalized == 'l') { + return true; + } + boolean numeric = normalized >= '0' && normalized <= '9'; + boolean color = normalized >= 'a' && normalized <= 'f'; + if (normalized == 'r' || numeric || color) { + return false; + } + return wasBold; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java index 66fcebd7..179dce44 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java +++ b/src/main/java/com/hfstudio/guidenh/guide/render/VanillaRenderContext.java @@ -24,10 +24,6 @@ public class VanillaRenderContext implements RenderContext { public static final RenderItem ITEM_RENDERER = new RenderItem(); - private static final String FORMAT_BOLD = "\u00a7l"; - private static final String FORMAT_ITALIC = "\u00a7o"; - private static final String FORMAT_STRIKETHROUGH = "\u00a7m"; - private static final String FORMAT_OBFUSCATED = "\u00a7k"; private final FontRenderer fontRenderer; private int screenHeight; @@ -37,9 +33,6 @@ public class VanillaRenderContext implements RenderContext { private final Deque scissorStack = new ArrayDeque<>(); - // Reuse the style buffer across text segments. - private final StringBuilder textStyleBuffer = new StringBuilder(32); - private int documentOriginX = 0; private int documentOriginY = 0; @@ -240,18 +233,7 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { if ((color >>> 24) == 0) { color |= 0xFF000000; } - - StringBuilder sb = null; - if (style.bold() || style.italic() || style.strikethrough() || style.obfuscated()) { - sb = textStyleBuffer; - sb.setLength(0); - if (style.bold()) sb.append(FORMAT_BOLD); - if (style.italic()) sb.append(FORMAT_ITALIC); - if (style.strikethrough()) sb.append(FORMAT_STRIKETHROUGH); - if (style.obfuscated()) sb.append(FORMAT_OBFUSCATED); - } - String drawn = sb != null ? sb.append(text) - .toString() : text; + String drawn = GuideFontCompat.prepareRenderedText(text, style); float scale = style.fontScale(); boolean scaled = Math.abs(scale - 1f) > 1e-4f; @@ -280,13 +262,12 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { int scaledFontHeight = Math.round(fontRenderer.FONT_HEIGHT * scale); int decorationY = y + scaledFontHeight - 1; - int decoratedWidth = -1; + int decoratedWidth = getStringWidth(text, style); if (style.underlined()) { - decoratedWidth = Math.round(fontRenderer.getStringWidth(drawn) * scale); Gui.drawRect(x, decorationY, x + decoratedWidth, decorationY + 1, color); } if (style.wavyUnderline()) { - int w = decoratedWidth >= 0 ? decoratedWidth : Math.round(fontRenderer.getStringWidth(drawn) * scale); + int w = decoratedWidth; // Draw a 2px-tall sine-like zig-zag using 1x1 rects: pattern of 4 px period. for (int i = 0; i < w; i++) { int phase = i & 3; // 0,1,2,3 @@ -297,49 +278,32 @@ public void drawText(String text, int x, int y, ResolvedTextStyle style) { if (style.dottedUnderline()) { // Center a single 2x2 dot under each rendered character cell. int cursor = 0; + boolean bold = style.bold(); + boolean visibleGlyphSeen = false; int len = drawn.length(); for (int i = 0; i < len; i++) { char c = drawn.charAt(i); - if (c == '\u00a7' && i + 1 < len) { + if (GuideFontCompat.isFormattingCodeStart(drawn, i)) { + bold = GuideFontCompat.determineBold(bold, drawn.charAt(i + 1)); i++; continue; } - int cw = Math.round(fontRenderer.getCharWidth(c) * scale); + float advance = GuideFontCompat.getRenderedAdvance(fontRenderer, c, bold, visibleGlyphSeen); + int cw = Math.round(advance * scale); if (cw <= 0) { - cursor += cw; continue; } int dotX = x + cursor + Math.max(0, (cw - 2) / 2); Gui.drawRect(dotX, decorationY, dotX + 2, decorationY + 2, color); cursor += cw; + visibleGlyphSeen = true; } } } @Override public int getStringWidth(String text, ResolvedTextStyle style) { - int raw = fontRenderer.getStringWidth(text); - if (style != null && style.bold() && text != null) { - raw += countRenderedChars(text); - } - if (style != null && style.italic() && text != null && !text.isEmpty()) { - raw += 2; - } - float scale = style != null ? style.fontScale() : 1f; - return Math.round(raw * scale); - } - - public static int countRenderedChars(String text) { - int n = 0; - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == '\u00a7' && i + 1 < text.length()) { - i++; - continue; - } - n++; - } - return n; + return GuideFontCompat.getStringWidth(fontRenderer, text, style); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/level/GuidebookPreviewBlockPlacer.java b/src/main/java/com/hfstudio/guidenh/guide/scene/level/GuidebookPreviewBlockPlacer.java index 285f93f1..50b9d3de 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/level/GuidebookPreviewBlockPlacer.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/level/GuidebookPreviewBlockPlacer.java @@ -583,17 +583,12 @@ public static TileEntity restoreTileAfterOnBlockAdded(GuidebookLevel level, int World world = level.getOrCreateFakeWorld(); TileEntity residentTile = resolveWorldResidentTile(world, x, y, z, preparedTileEntity); - if (residentTile == preparedTileEntity) { - return preparedTileEntity; - } - NBTTagCompound restoreTag = tileSnapshot != null ? tileSnapshot : tileTag; - TileEntity restoredTile = residentTile; - if (restoredTile == null || (restoreTag != null && !applyTileSnapshot(restoredTile, restoreTag, x, y, z))) { + TileEntity restoredTile = residentTile != null ? residentTile : preparedTileEntity; + if (restoreTag != null && !applyTileSnapshot(restoredTile, restoreTag, x, y, z) + && restoredTile != preparedTileEntity) { restoredTile = preparedTileEntity; - if (restoreTag != null) { - applyTileSnapshot(restoredTile, restoreTag, x, y, z); - } + applyTileSnapshot(restoredTile, restoreTag, x, y, z); } initializeGregTechMetaTile(restoredTile, placementData.metaTileId, restoreTag); diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderKeyframeAnnotation.java b/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderKeyframeAnnotation.java index cba09697..b18f8626 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderKeyframeAnnotation.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/ponder/PonderKeyframeAnnotation.java @@ -371,26 +371,24 @@ public float getHlMaxZ(float def) { } public int parseHighlightColor(int defaultArgb) { - if (highlightColor == null || highlightColor.isEmpty()) { - return defaultArgb; - } - try { - return (int) Long.parseLong( - highlightColor.replace("0x", "") - .replace("0X", ""), - 16); - } catch (NumberFormatException ignored) { - return defaultArgb; - } + return parseHexColor(highlightColor, defaultArgb); } public int parseColor(int defaultArgb) { - if (color == null || color.isEmpty()) { + return parseHexColor(color, defaultArgb); + } + + private static int parseHexColor(@Nullable String colorLiteral, int defaultArgb) { + if (colorLiteral == null || colorLiteral.isEmpty()) { return defaultArgb; } + String normalized = colorLiteral; + if (normalized.startsWith("ox") || normalized.startsWith("OX")) { + normalized = "0x" + normalized.substring(2); + } try { return (int) Long.parseLong( - color.replace("0x", "") + normalized.replace("0x", "") .replace("0X", ""), 16); } catch (NumberFormatException ignored) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java index efd7c49c..a3de2742 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/ExportTask.java @@ -1,10 +1,8 @@ package com.hfstudio.guidenh.guide.siteexport; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; @@ -13,7 +11,6 @@ import java.util.Set; import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.IResource; import net.minecraft.item.Item; import net.minecraft.util.ResourceLocation; @@ -22,6 +19,7 @@ import com.google.gson.GsonBuilder; import com.hfstudio.guidenh.guide.Guide; import com.hfstudio.guidenh.guide.compiler.ParsedGuidePage; +import com.hfstudio.guidenh.guide.internal.resource.GuideResourceAccess; import com.hfstudio.guidenh.guide.scene.support.GuideDebugLog; public class ExportTask { @@ -74,14 +72,14 @@ public Result run() throws IOException { Path assetsDir = outDir.resolve("assets"); for (ResourceLocation id : collector.textures) { try { - IResource res = mc.getResourceManager() - .getResource(id); + byte[] bytes = GuideResourceAccess.readBytes(mc.getResourceManager(), id); + if (bytes == null) { + continue; + } Path dest = assetsDir.resolve(id.getResourceDomain()) .resolve(id.getResourcePath()); Files.createDirectories(dest.getParent()); - try (InputStream in = res.getInputStream()) { - Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); - } + Files.write(dest, bytes); assetsCopied++; } catch (IOException e) { GuideDebugLog.debugAlways("[GuideNH] [ExportTask] Skipping missing asset {}", id, e); diff --git a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java index 52d1e59d..d4dfdb77 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java +++ b/src/main/java/com/hfstudio/guidenh/guide/siteexport/site/GuideSitePageCollector.java @@ -52,7 +52,7 @@ public List collect(MutableGuide guide, @Nullable List(discoveredLanguages); } else { try { - languages = discoverLanguages(); + languages = discoverLanguages(guide.getId()); } catch (Throwable t) { GuideDebugLog.debugAlways( "[GuideNH] [GuideSitePageCollector] Falling back to the guide default language for {}", @@ -70,11 +70,12 @@ public List collect(MutableGuide guide, @Nullable List pageIdSet; try { pageIdSet = new LinkedHashSet<>(); - var pathsByNs = DataDrivenGuideLoader.discoverPagePaths(guide.getContentRootFolder()); - for (var entry : pathsByNs.entrySet()) { - for (String path : entry.getValue()) { - pageIdSet.add(new ResourceLocation(entry.getKey(), path)); - } + for (String path : DataDrivenGuideLoader.discoverPagePaths(guide.getId(), guide.getContentRootFolder())) { + pageIdSet.add( + new ResourceLocation( + guide.getId() + .getResourceDomain(), + path)); } } catch (Throwable t) { GuideDebugLog.debugAlways( @@ -152,7 +153,7 @@ private static ParsedGuidePage parseSyntheticPage(ResourceLocation pageId, Strin public static List discoverLanguagesOrEmpty() { try { - return discoverLanguages(); + return discoverLanguages(null); } catch (Throwable t) { GuideDebugLog.debugAlways( "[GuideNH] [GuideSitePageCollector] Falling back to no discovered site export languages", @@ -189,14 +190,18 @@ public List collect(ResourceLocation guideId, String defau return variants; } - private static List discoverLanguages() { - Map> discovered = new LinkedHashMap<>(); - for (var resourcePack : DataDrivenGuideLoader.getActiveResourcePacks()) { - DataDrivenGuideLoader.scanResourcePack(resourcePack, discovered); - } + private static List discoverLanguages(@Nullable ResourceLocation guideId) { var merged = new LinkedHashSet(); - for (var langs : discovered.values()) { - merged.addAll(langs); + var discovered = DataDrivenGuideLoader.discoverGuideLanguages(); + if (guideId != null) { + var languages = discovered.get(guideId); + if (languages != null) { + merged.addAll(languages); + } + } else { + for (var langs : discovered.values()) { + merged.addAll(langs); + } } return new ArrayList<>(merged); } diff --git a/src/main/java/com/hfstudio/guidenh/integration/Mods.java b/src/main/java/com/hfstudio/guidenh/integration/Mods.java index bee8039a..58fd7385 100644 --- a/src/main/java/com/hfstudio/guidenh/integration/Mods.java +++ b/src/main/java/com/hfstudio/guidenh/integration/Mods.java @@ -31,6 +31,7 @@ public enum Mods implements IMod, ITargetMod { DistantHorizons("distanthorizons"), EtFuturum("etfuturum"), SimpleSkinBackport("simpleskinbackport"), + ChromaticTooltips("chromatictooltips"), Translocators("Translocator"), TinkersConstruct("TConstruct"), ; From fff0b9322fe9336afe6c2752880e0f240256eb53 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:14:55 +0800 Subject: [PATCH 136/136] add back and forward hotkey --- .../com/hfstudio/guidenh/ClientProxy.java | 2 + .../client/hotkey/GuidePageHistoryHotkey.java | 58 +++++++++++++++++++ .../guidenh/guide/compiler/PageCompiler.java | 17 +++++- .../guide/compiler/tags/CsvTableCompiler.java | 34 +++++++++-- .../guide/compiler/tags/PreCompiler.java | 6 +- .../guide/compiler/tags/TableCompiler.java | 6 +- .../guidenh/guide/internal/GuideScreen.java | 54 +++++++++++------ .../internal/host/scripts/CsvTableScript.java | 20 ++++++- .../resources/assets/guidenh/lang/en_US.lang | 2 + .../resources/assets/guidenh/lang/zh_CN.lang | 2 + 10 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/hfstudio/guidenh/client/hotkey/GuidePageHistoryHotkey.java diff --git a/src/main/java/com/hfstudio/guidenh/ClientProxy.java b/src/main/java/com/hfstudio/guidenh/ClientProxy.java index 15601cf3..ac234b63 100644 --- a/src/main/java/com/hfstudio/guidenh/ClientProxy.java +++ b/src/main/java/com/hfstudio/guidenh/ClientProxy.java @@ -11,6 +11,7 @@ import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController; import com.hfstudio.guidenh.client.command.GuideNhClientCommand; import com.hfstudio.guidenh.client.hotkey.CycleRegionWandModeHotkey; +import com.hfstudio.guidenh.client.hotkey.GuidePageHistoryHotkey; import com.hfstudio.guidenh.client.hotkey.OpenGuideHomeHotkey; import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey; import com.hfstudio.guidenh.client.hotkey.OpenSceneEditorHotkey; @@ -132,6 +133,7 @@ public void init(FMLInitializationEvent event) { } OpenGuideHomeHotkey.init(); OpenGuideHotkey.init(); + GuidePageHistoryHotkey.init(); OpenSceneEditorHotkey.init(); AutocompleteProviders.register(new ItemIdProvider()); TagAttributeRegistry.initialize(); diff --git a/src/main/java/com/hfstudio/guidenh/client/hotkey/GuidePageHistoryHotkey.java b/src/main/java/com/hfstudio/guidenh/client/hotkey/GuidePageHistoryHotkey.java new file mode 100644 index 00000000..19f6b11c --- /dev/null +++ b/src/main/java/com/hfstudio/guidenh/client/hotkey/GuidePageHistoryHotkey.java @@ -0,0 +1,58 @@ +package com.hfstudio.guidenh.client.hotkey; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.KeyBinding; + +import org.lwjgl.input.Keyboard; + +import com.hfstudio.guidenh.guide.internal.GuideScreen; + +import cpw.mods.fml.client.registry.ClientRegistry; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +@SideOnly(Side.CLIENT) +public class GuidePageHistoryHotkey { + + public static final KeyBinding GUIDE_PAGE_BACK_KEY = new KeyBinding( + "key.guidenh.guide_page_back", + Keyboard.KEY_NONE, + "key.categories.guidenh"); + + public static final KeyBinding GUIDE_PAGE_FORWARD_KEY = new KeyBinding( + "key.guidenh.guide_page_forward", + Keyboard.KEY_NONE, + "key.categories.guidenh"); + + private GuidePageHistoryHotkey() {} + + public static void init() { + ClientRegistry.registerKeyBinding(GUIDE_PAGE_BACK_KEY); + ClientRegistry.registerKeyBinding(GUIDE_PAGE_FORWARD_KEY); + FMLCommonHandler.instance() + .bus() + .register(new GuidePageHistoryHotkey()); + } + + @SubscribeEvent + public void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) { + return; + } + + if (!(Minecraft.getMinecraft().currentScreen instanceof GuideScreen guideScreen)) { + return; + } + + while (GUIDE_PAGE_BACK_KEY.isPressed()) { + guideScreen.navigateBackFromHotkey(); + } + + while (GUIDE_PAGE_FORWARD_KEY.isPressed()) { + guideScreen.navigateForwardFromHotkey(); + } + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java index adb9ffc9..ee6e47e7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/PageCompiler.java @@ -563,6 +563,21 @@ public void compileInlineFragment(Collection children } } + public void compileTableCellContent(MdAstParent markdownParent, LytBlockContainer layoutParent) { + compileTableCellContent(markdownParent.children(), layoutParent); + } + + public void compileTableCellContent(List children, LytBlockContainer layoutParent) { + var paragraph = new LytParagraph(); + paragraph.setMarginTop(0); + paragraph.setMarginBottom(0); + withChildrenSourceContext(children, () -> compileInlineFragment(children, paragraph)); + if (paragraph.isEmpty()) { + return; + } + layoutParent.append(paragraph); + } + public void compileBlockContextInSourceContext(List children, LytBlockContainer layoutParent) { withChildrenSourceContext(children, () -> compileBlockContext(children, layoutParent)); @@ -727,7 +742,7 @@ private LytBlock compileTable(GfmTable astTable, List widthHints) { } } - compileBlockContext(astCells.get(i), cell); + compileTableCellContent(astCells.get(i), cell); } rowIndex++; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java index 46a1ff41..6ffdf903 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/CsvTableCompiler.java @@ -46,7 +46,14 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl boolean header = MdxAttrs.getBoolean(compiler, parent, el, "header", true); List widths = parseWidthHints(MdxAttrs.getString(compiler, parent, el, "widths", null)); - CsvTablePlaceholder placeholder = new CsvTablePlaceholder(csvId.toString(), header, widths); + CsvTablePlaceholder placeholder = new CsvTablePlaceholder( + csvId.toString(), + header, + widths, + compiler.getSourcePack(), + compiler.getLanguage(), + compiler.getPageId() + .toString()); placeholder.appendText("[CsvTable]"); parent.append(placeholder); } @@ -87,6 +94,11 @@ public void index(IndexingContext indexer, MdxJsxElementFields el, IndexingSink } public static LytTable buildTable(List> rows, boolean header, List widthHints) { + return buildTable(null, rows, header, widthHints); + } + + public static LytTable buildTable(PageCompiler compiler, List> rows, boolean header, + List widthHints) { LytTable table = new LytTable(); table.setMarginTop(PageCompiler.DEFAULT_ELEMENT_SPACING); table.setMarginBottom(PageCompiler.DEFAULT_ELEMENT_SPACING); @@ -103,7 +115,7 @@ public static LytTable buildTable(List> rows, boolean header, List< table.getOrCreateColumn(columnIndex) .setPreferredWidth(widthHints.get(columnIndex)); } - appendCellContent(row.appendCell(), values.get(columnIndex)); + appendCellContent(compiler, row.appendCell(), values.get(columnIndex)); } firstRow = false; rowIndex++; @@ -145,7 +157,7 @@ public static List parseWidthHints(String rawWidths) { return result; } - private static void appendCellContent(LytTableCell cell, String value) { + private static void appendCellContent(PageCompiler compiler, LytTableCell cell, String value) { LytParagraph paragraph = new LytParagraph(); paragraph.setMarginTop(0); paragraph.setMarginBottom(0); @@ -157,7 +169,12 @@ private static void appendCellContent(LytTableCell cell, String value) { if (end < 0) { end = value.length(); } - paragraph.appendText(value.substring(start, end)); + String line = value.substring(start, end); + if (compiler != null) { + compiler.compileInlineMarkdown(line, paragraph); + } else { + paragraph.appendText(line); + } if (end == value.length()) { break; } @@ -173,11 +190,18 @@ public static class CsvTablePlaceholder extends LytParagraph { public final String src; public final boolean header; public final List widths; + public final String sourcePack; + public final String language; + public final String pageId; - public CsvTablePlaceholder(String src, boolean header, List widths) { + public CsvTablePlaceholder(String src, boolean header, List widths, String sourcePack, String language, + String pageId) { this.src = src; this.header = header; this.widths = widths; + this.sourcePack = sourcePack; + this.language = language; + this.pageId = pageId; setStyleClass("CsvTable"); setStyle(LytParagraph.PLACEHOLDER_STYLE); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java index 21161f50..f1aa2665 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/PreCompiler.java @@ -52,7 +52,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // CSV table if (lang != null && "csv".equals(language.id())) { - LytBlock csvBlock = compileCsvCodeBlock(codeText, meta); + LytBlock csvBlock = compileCsvCodeBlock(compiler, codeText, meta); parent.append(csvBlock); return; } @@ -95,7 +95,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl // ---- CSV code block compilation ---- - private LytBlock compileCsvCodeBlock(String source, @Nullable String meta) { + private LytBlock compileCsvCodeBlock(PageCompiler compiler, String source, @Nullable String meta) { List> rows = CsvTableParser.parse(source); if (rows.isEmpty()) { LytCodeBlock codeBlock = new LytCodeBlock(); @@ -105,7 +105,7 @@ private LytBlock compileCsvCodeBlock(String source, @Nullable String meta) { } CsvFenceMeta csvMeta = parseCsvFenceMeta(meta); - return CsvTableCompiler.buildTable(rows, csvMeta.header(), csvMeta.widthHints()); + return CsvTableCompiler.buildTable(compiler, rows, csvMeta.header(), csvMeta.widthHints()); } private CsvFenceMeta parseCsvFenceMeta(@Nullable String meta) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java index 4dec42cf..2b47437a 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java +++ b/src/main/java/com/hfstudio/guidenh/guide/compiler/tags/TableCompiler.java @@ -1,6 +1,7 @@ package com.hfstudio.guidenh.guide.compiler.tags; import java.util.Collections; +import java.util.List; import java.util.Set; import com.hfstudio.guidenh.guide.compiler.PageCompiler; @@ -34,8 +35,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl if (child instanceof MdxJsxFlowElement meta && "table-meta".equals(meta.name())) { String content = meta.getAttributeString("content", ""); if (!content.isEmpty()) { - java.util.List widths = com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler - .parseWidthHints(extractKramdownExpression(content)); + List widths = CsvTableCompiler.parseWidthHints(extractKramdownExpression(content)); var columns = table.getColumns(); for (int wi = 0; wi < widths.size() && wi < columns.size(); wi++) { columns.get(wi) @@ -67,7 +67,7 @@ protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxEl } } - compiler.compileBlockContext(td.children(), cell); + compiler.compileTableCellContent(td.children(), cell); cellIndex++; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 3f26a5f5..dbdca2f4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2130,25 +2130,9 @@ protected void actionPerformed(GuiButton btn) { if (btn == btnClose) { close(); } else if (btn == btnBack) { - if (!history.isEmpty()) { - confirmGuideEditorDirtyBefore(() -> { - rememberCurrentContentStateIfEligible(); - forwardHistory.push(captureCurrentViewState()); - var prev = history.pop(); - restoreViewState(prev); - rebuildToolbar(); - }); - } + navigateBackInHistory(); } else if (btn == btnForward) { - if (!forwardHistory.isEmpty()) { - confirmGuideEditorDirtyBefore(() -> { - rememberCurrentContentStateIfEligible(); - history.push(captureCurrentViewState()); - var next = forwardHistory.pop(); - restoreViewState(next); - rebuildToolbar(); - }); - } + navigateForwardInHistory(); } else if (btn == btnFullWidth) { fullWidth = !fullWidth; try { @@ -2205,6 +2189,40 @@ protected void actionPerformed(GuiButton btn) { } } + public void navigateBackFromHotkey() { + navigateBackInHistory(); + } + + public void navigateForwardFromHotkey() { + navigateForwardInHistory(); + } + + private void navigateBackInHistory() { + if (history.isEmpty()) { + return; + } + confirmGuideEditorDirtyBefore(() -> { + rememberCurrentContentStateIfEligible(); + forwardHistory.push(captureCurrentViewState()); + var prev = history.pop(); + restoreViewState(prev); + rebuildToolbar(); + }); + } + + private void navigateForwardInHistory() { + if (forwardHistory.isEmpty()) { + return; + } + confirmGuideEditorDirtyBefore(() -> { + rememberCurrentContentStateIfEligible(); + history.push(captureCurrentViewState()); + var next = forwardHistory.pop(); + restoreViewState(next); + rebuildToolbar(); + }); + } + private void loadCurrentPage() { clearInteractionState(); closeTransientContextMenus(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java index 2ffe9a74..02968de1 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/scripts/CsvTableScript.java @@ -5,10 +5,14 @@ import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.guide.Guide; +import com.hfstudio.guidenh.guide.PageCollection; +import com.hfstudio.guidenh.guide.compiler.PageCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler; import com.hfstudio.guidenh.guide.compiler.tags.CsvTableCompiler.CsvTablePlaceholder; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytParagraph; +import com.hfstudio.guidenh.guide.extensions.ExtensionCollection; import com.hfstudio.guidenh.guide.internal.csv.CsvTableParser; import com.hfstudio.guidenh.guide.internal.host.EventType; import com.hfstudio.guidenh.guide.internal.host.LytEvent; @@ -54,7 +58,21 @@ public void onEvent(Object node, LytEvent event, ScriptContext ctx) { try { List> rows = CsvTableParser.parse(new String(data, StandardCharsets.UTF_8)); - LytBlock table = CsvTableCompiler.buildTable(rows, ph.header, ph.widths); + PageCollection pageCollection = ctx.getPageCollection(); + if (pageCollection == null) { + ctx.replace(LytParagraph.error("[CsvTable] Missing page context for: " + ph.src)); + return; + } + ExtensionCollection extensions = pageCollection instanceof Guide guide ? guide.getExtensions() + : ExtensionCollection.EMPTY; + PageCompiler runtimeCompiler = new PageCompiler( + pageCollection, + extensions, + ph.sourcePack, + ph.language, + new ResourceLocation(ph.pageId), + ""); + LytBlock table = CsvTableCompiler.buildTable(runtimeCompiler, rows, ph.header, ph.widths); if (table != null) { ctx.replace(table); } else { diff --git a/src/main/resources/assets/guidenh/lang/en_US.lang b/src/main/resources/assets/guidenh/lang/en_US.lang index 941aab26..af349893 100644 --- a/src/main/resources/assets/guidenh/lang/en_US.lang +++ b/src/main/resources/assets/guidenh/lang/en_US.lang @@ -490,6 +490,8 @@ guideme.guidebook.SceneEditorMarkdownWrapOff=Word Wrap: Off guideme.guidebook.SceneEditorMarkdownWrapOn=Word Wrap: On key.categories.guidenh=GuideNH key.guidenh.cycle_region_wand_mode=Cycle Region Wand Export Mode +key.guidenh.guide_page_back=Guide Page Back +key.guidenh.guide_page_forward=Guide Page Forward key.guidenh.open_guide_home=Open Guide Home key.guidenh.open_guide=Open Guide for Items key.guidenh.open_scene_editor=Open Scene Editor diff --git a/src/main/resources/assets/guidenh/lang/zh_CN.lang b/src/main/resources/assets/guidenh/lang/zh_CN.lang index edafb7f6..d9ace3d8 100644 --- a/src/main/resources/assets/guidenh/lang/zh_CN.lang +++ b/src/main/resources/assets/guidenh/lang/zh_CN.lang @@ -490,6 +490,8 @@ guideme.guidebook.SceneEditorMarkdownWrapOff=自动换行:关 guideme.guidebook.SceneEditorMarkdownWrapOn=自动换行:开 key.categories.guidenh=GuideNH key.guidenh.cycle_region_wand_mode=切换 Region Wand 导出模式 +key.guidenh.guide_page_back=指南页面后退 +key.guidenh.guide_page_forward=指南页面前进 key.guidenh.open_guide_home=打开指南主页 key.guidenh.open_guide=打开物品对应的指南 key.guidenh.open_scene_editor=打开场景编辑器