From 732e0640dbf1dbe4b98d2a7f8682e9d10dcfac8f Mon Sep 17 00:00:00 2001 From: shedaniel Date: Thu, 17 Feb 2022 15:38:59 +0800 Subject: Implement changelog, close #772 --- .../rei/impl/client/config/ConfigManagerImpl.java | 2 - .../rei/impl/client/gui/ScreenOverlayImpl.java | 19 +- .../impl/client/gui/changelog/ChangelogLoader.java | 117 ++ .../rei/impl/client/gui/changelog/JParseDown.java | 1316 ++++++++++++++++++++ .../gui/changelog/JParseDownToMinecraft.java | 70 ++ .../client/gui/error/ErrorsEntryListWidget.java | 322 ++++- .../rei/impl/client/gui/error/ErrorsScreen.java | 23 +- .../client/gui/widget/CatchingExceptionUtils.java | 2 +- .../assets/roughlyenoughitems/lang/en_us.json | 3 + .../textures/gui/recipecontainer.png | Bin 3599 -> 5110 bytes .../main/resources/roughlyenoughitems.changes.json | 3 + .../roughlyenoughitems/2022.1/2022-02-18_01-30.png | Bin 0 -> 33618 bytes .../roughlyenoughitems/2022.1/2022-02-18_01-32.png | Bin 0 -> 9532 bytes .../roughlyenoughitems/2022.1/2022-02-18_09-05.png | Bin 0 -> 70861 bytes .../roughlyenoughitems/2022.1/changelog.md | 44 + 15 files changed, 1908 insertions(+), 13 deletions(-) create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/ChangelogLoader.java create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDown.java create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDownToMinecraft.java create mode 100644 runtime/src/main/resources/roughlyenoughitems.changes.json create mode 100644 runtime/src/main/resources/roughlyenoughitems/2022.1/2022-02-18_01-30.png create mode 100644 runtime/src/main/resources/roughlyenoughitems/2022.1/2022-02-18_01-32.png create mode 100644 runtime/src/main/resources/roughlyenoughitems/2022.1/2022-02-18_09-05.png create mode 100644 runtime/src/main/resources/roughlyenoughitems/2022.1/changelog.md (limited to 'runtime') diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigManagerImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigManagerImpl.java index 6f9c5a085..485b90271 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigManagerImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigManagerImpl.java @@ -80,7 +80,6 @@ import net.minecraft.nbt.TagParser; import net.minecraft.network.chat.*; import net.minecraft.util.Mth; import net.minecraft.world.InteractionResult; -import org.apache.commons.lang3.mutable.MutableLong; import org.jetbrains.annotations.ApiStatus; import java.util.*; @@ -371,7 +370,6 @@ public class ConfigManagerImpl implements ConfigManager { ((GlobalizedClothConfigScreen) screen).listWidget.children().add(0, (AbstractConfigEntry) feedbackEntry); ((GlobalizedClothConfigScreen) screen).listWidget.children().add(0, (AbstractConfigEntry) new EmptyEntry(4)); ScreenHooks.addRenderableWidget(screen, new Button(screen.width - 104, 4, 100, 20, new TranslatableComponent("text.rei.credits"), button -> { - MutableLong current = new MutableLong(0); CreditsScreen creditsScreen = new CreditsScreen(screen); Minecraft.getInstance().setScreen(creditsScreen); })); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl.java index 067e8d32e..7724f6d0a 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl.java @@ -62,6 +62,7 @@ import me.shedaniel.rei.impl.client.ClientHelperImpl; import me.shedaniel.rei.impl.client.REIRuntimeImpl; import me.shedaniel.rei.impl.client.config.ConfigManagerImpl; import me.shedaniel.rei.impl.client.config.ConfigObjectImpl; +import me.shedaniel.rei.impl.client.gui.changelog.ChangelogLoader; import me.shedaniel.rei.impl.client.gui.craftable.CraftableFilter; import me.shedaniel.rei.impl.client.gui.dragging.CurrentDraggingStack; import me.shedaniel.rei.impl.client.gui.modules.Menu; @@ -265,6 +266,21 @@ public class ScreenOverlayImpl extends ScreenOverlay { .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y)) .tooltipLine(new TranslatableComponent("text.rei.previous_page")) .focusable(false)); + Button changelogButton; + widgets.add(changelogButton = Widgets.createButton(new Rectangle(bounds.x + bounds.width - 18 - 18, bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 5, 16, 16), new TranslatableComponent("")) + .onClick(button -> { + ChangelogLoader.show(); + }) + .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y)) + .tooltipLine(new TranslatableComponent("text.rei.changelog.title")) + .focusable(false)); + widgets.add(Widgets.createDrawableWidget((helper, matrices, mouseX, mouseY, delta) -> { + helper.setBlitOffset(helper.getBlitOffset() + 1); + RenderSystem.setShaderTexture(0, CHEST_GUI_TEXTURE); + Rectangle bounds = changelogButton.getBounds(); + helper.blit(matrices, bounds.x + 1, bounds.y + 2, !ChangelogLoader.hasVisited() ? 28 : 14, 0, 14, 14); + helper.setBlitOffset(helper.getBlitOffset() - 1); + })); widgets.add(rightButton = Widgets.createButton(new Rectangle(bounds.x + bounds.width - 18, bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 5, 16, 16), new TranslatableComponent("text.rei.right_arrow")) .onClick(button -> { ENTRY_LIST_WIDGET.nextPage(); @@ -332,6 +348,7 @@ public class ScreenOverlayImpl extends ScreenOverlay { helper.setBlitOffset(helper.getBlitOffset() + 1); RenderSystem.setShaderTexture(0, CHEST_GUI_TEXTURE); helper.blit(matrices, configButtonArea.x + 3, configButtonArea.y + 3, 0, 0, 14, 14); + helper.setBlitOffset(helper.getBlitOffset() - 1); }) ), 0, 0, 600 @@ -349,7 +366,7 @@ public class ScreenOverlayImpl extends ScreenOverlay { }), 0, 0, 600))); } if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) { - widgets.add(Widgets.createClickableLabel(new Point(bounds.x + (bounds.width / 2), bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 10), NarratorChatListener.NO_TITLE, label -> { + widgets.add(Widgets.createClickableLabel(new Point(bounds.x + ((bounds.width - 18) / 2), bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 10), NarratorChatListener.NO_TITLE, label -> { if (!Screen.hasShiftDown()) { ENTRY_LIST_WIDGET.setPage(0); ENTRY_LIST_WIDGET.updateEntriesPosition(); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/ChangelogLoader.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/ChangelogLoader.java new file mode 100644 index 000000000..cf0b3fc10 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/ChangelogLoader.java @@ -0,0 +1,117 @@ +package me.shedaniel.rei.impl.client.gui.changelog; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import dev.architectury.platform.Platform; +import me.shedaniel.rei.impl.client.gui.error.ErrorsEntryListWidget; +import me.shedaniel.rei.impl.client.gui.error.ErrorsScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TranslatableComponent; +import org.apache.commons.io.IOUtils; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; + +public class ChangelogLoader { + public interface Builder { + default void add(Component component) { + add(width -> new ErrorsEntryListWidget.TextEntry(component, width)); + } + + void add(Function function); + } + + private static Boolean visited = false; + + public static boolean hasVisited() { + if (visited == null) { + visited = false; + File file = Platform.getConfigFolder().resolve("roughlyenoughitems/changelog.txt").toFile(); + if (file.exists()) { + try (InputStreamReader reader = new FileReader(file)) { + String version = IOUtils.toString(reader).trim(); + + InputStream changesJsonStream = ChangelogLoader.class.getClassLoader().getResourceAsStream("roughlyenoughitems.changes.json"); + if (changesJsonStream != null) { + JsonObject object = JsonParser.parseReader(new InputStreamReader(changesJsonStream)) + .getAsJsonObject(); + String currentVersion = object.getAsJsonPrimitive("version").getAsString(); + if (currentVersion.equals(version)) { + visited = true; + } + } + } catch (IOException e) { + } + } + } + + return visited; + } + + public static void show() { + class BuilderImpl implements Builder { + private final List components = new ArrayList<>(); + + @Override + public void add(Function function) { + components.add(function); + } + } + + visited = true; + BuilderImpl builder = new BuilderImpl(); + + InputStream changesJsonStream = ChangelogLoader.class.getClassLoader().getResourceAsStream("roughlyenoughitems.changes.json"); + if (changesJsonStream == null) { + builder.add(new TranslatableComponent("rei.changelog.error.missingChangelogFile")); + } else { + JsonObject object = JsonParser.parseReader(new InputStreamReader(changesJsonStream)) + .getAsJsonObject(); + String version = object.getAsJsonPrimitive("version").getAsString(); + Path file = Platform.getConfigFolder().resolve("roughlyenoughitems/changelog.txt"); + try { + Files.write(file, version.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + e.printStackTrace(); + } + + InputStream changelogStream = ChangelogLoader.class.getClassLoader().getResourceAsStream("roughlyenoughitems/" + version + "/changelog.md"); + + if (changelogStream == null) { + builder.add(new TranslatableComponent("rei.changelog.error.missingChangelogFile")); + } else { + try { + JParseDown parseDown = new JParseDown(); + LinkedList blocks = parseDown.linesElements(IOUtils.readLines(changelogStream, StandardCharsets.UTF_8).toArray(new String[0])); + for (JParseDown.Block block : blocks) { + if (block.autoBreak) { + builder.add(width -> new ErrorsEntryListWidget.EmptyEntry(6)); + } + Builder blockBuilder = builder; + if (block instanceof JParseDown.BlockHeader) { + blockBuilder = function -> { + builder.add(width -> new ErrorsEntryListWidget.ScaledEntry(function.apply(Math.round(width / 1.5F)), 1.5F)); + }; + } + JParseDownToMinecraft.build(blockBuilder, block); + if (block.autoBreak) { + builder.add(width -> new ErrorsEntryListWidget.EmptyEntry(6)); + } + } + } catch (IOException e) { + builder.add(new TranslatableComponent("rei.changelog.error.failedToReadChangelogFile")); + } + } + } + + Minecraft.getInstance().setScreen(new ErrorsScreen(new TranslatableComponent("text.rei.changelog.title"), builder.components, Minecraft.getInstance().screen, true)); + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDown.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDown.java new file mode 100644 index 000000000..6165f6a7a --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDown.java @@ -0,0 +1,1316 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.shedaniel.rei.impl.client.gui.changelog; + +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/* +Copyright (c) 2019 Ashur Rafiev +https://github.com/ashurrafiev/JParsedown +MIT Licence: https://github.com/ashurrafiev/JParsedown/blob/master/LICENSE + +This work is derived from Parsedown version 1.8.0-beta-5: +Copyright (c) 2013-2018 Emanuil Rusev +http://parsedown.org +*/ +@SuppressWarnings("RegExpRedundantEscape") +public class JParseDown { + public static final String version = "1.0.4"; + + public static class ReferenceData { + public String url; + public String title; + + public ReferenceData(String url, String title) { + this.url = url; + this.title = title; + } + } + + public static class Line { + public String body; + public String text; + public int indent; + + public Line(String line) { + body = line; + text = line.replaceFirst("^\\s+", ""); + indent = line.length() - text.length(); + } + } + + public abstract static class Component { + public String markup = null; + public boolean hidden = false; + public HashSet> nonNestables = new HashSet<>(); + } + + public interface BlockType { + Block startBlock(JParseDown parseDown, Line line, Block block); + } + + public interface InlineType { + Inline inline(JParseDown parseDown, String text, String context); + } + + public abstract static class Block extends Component { + public boolean identified = false; + public int interrupted = 0; + public List inlines; + public Boolean autoBreak = null; + + public boolean isContinuable() { + return false; + } + + public boolean isCompletable() { + return false; + } + + public Block continueBlock(Line line) { + return null; + } + + public Block completeBlock() { + return null; + } + + public abstract Collection inline(JParseDown parseDown); + } + + public static class BlockParagraph extends Block { + public String text; + + public BlockParagraph(String text) { + this.text = text; + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + return new BlockParagraph(line.text); + } + + @Override + public boolean isContinuable() { + return false; + } + + @Override + public Block continueBlock(Line line) { + if (interrupted > 0) + return null; + text += "\n" + line.text; + return this; + } + + @Override + public Collection inline(JParseDown parseDown) { + return parseDown.lineElements(text, nonNestables); + } + + @Override + public String toString() { + return "BlockParagraph{" + + "text='" + text + '\'' + + '}'; + } + } + + public static class BlockCode extends Block { + public String text; + + public BlockCode(String text) { + this.text = text; + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + if (block != null && block instanceof BlockParagraph && block.interrupted == 0) + return null; + if (line.indent >= 4) { + return new BlockCode(line.body.substring(4)); + } else + return null; + } + + @Override + public boolean isContinuable() { + return true; + } + + @Override + public Block continueBlock(Line line) { + if (line.indent >= 4) { + while (interrupted > 0) { + text += "\n"; + interrupted--; + } + text += "\n"; + text += line.body.substring(4); + return this; + } else + return null; + } + + @Override + public boolean isCompletable() { + return true; + } + + @Override + public Block completeBlock() { + return this; + } + + @Override + public Collection inline(JParseDown parseDown) { + return parseDown.lineElements(text, nonNestables); + } + + @Override + public String toString() { + return "BlockCode{" + + "text='" + text + '\'' + + '}'; + } + } + + public static class BlockComment extends Block { + public String text; + public boolean closed = false; + + public BlockComment(String text) { + this.text = text; + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + if (parseDown.markupEscaped || parseDown.safeMode) + return null; + if (line.text.indexOf("")) + b.closed = true; + return b; + } else + return null; + } + + @Override + public boolean isContinuable() { + return true; + } + + @Override + public Block continueBlock(Line line) { + if (closed) + return null; + text += "\n" + line.body; + if (line.text.contains("-->")) + closed = true; + return this; + } + + @Override + public Collection inline(JParseDown parseDown) { + return parseDown.lineElements(text, nonNestables); + } + + @Override + public String toString() { + return "BlockComment{" + + "text='" + text + '\'' + + '}'; + } + } + + public static class BlockFencedCode extends Block { + public final char marker; + public final int openerLength; + public final String infoString; + public String text = ""; + public boolean complete = false; + + public BlockFencedCode(char marker, int openerLength, String infoString) { + this.marker = marker; + this.openerLength = openerLength; + this.infoString = infoString; + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + char marker = line.text.charAt(0); + int openerLength = startSpan(line.text, marker); + if (openerLength < 3) + return null; + String infoString = line.text.substring(openerLength).trim(); + if (infoString.contains("`")) + return null; + return new BlockFencedCode(marker, openerLength, infoString); + } + + @Override + public boolean isContinuable() { + return true; + } + + @Override + public Block continueBlock(Line line) { + if (complete) + return null; + while (interrupted > 0) { + text += "\n"; + interrupted--; + } + int len = startSpan(line.text, marker); + if (len >= openerLength && line.text.substring(len).trim().isEmpty()) { + if (!text.isEmpty()) + text = text.substring(1); + complete = true; + return this; + } + text += "\n" + line.body; + return this; + } + + @Override + public boolean isCompletable() { + return true; + } + + @Override + public Block completeBlock() { + return this; + } + + @Override + public Collection inline(JParseDown parseDown) { + return parseDown.lineElements(text, nonNestables); + } + + @Override + public String toString() { + return "BlockFencedCode{" + + "infoString='" + infoString + '\'' + + ", text='" + text + '\'' + + '}'; + } + } + + public static class BlockHeader extends Block { + public final int level; + public final String line; + + public BlockHeader(int level, String line) { + this.level = level; + this.line = line; + this.autoBreak = true; + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + int level = startSpan(line.text, '#'); + if (level > 6) + return null; + + String text = line.text.substring(level); + if (parseDown.strictMode && !text.isEmpty() && text.charAt(0) != ' ') + return null; + text = text.trim(); + + return new BlockHeader(level, text); + } + + @Override + public Collection inline(JParseDown parseDown) { + return parseDown.lineElements(line, nonNestables); + } + + @Override + public String toString() { + return "BlockHeader{" + + "level=" + level + + ", line='" + line + '\'' + + '}'; + } + } + + public class BlockList extends Block { + public int indent; + public String pattern; + public boolean loose = false; + + public boolean ordered; + public String marker; + public String markerType; + public String markerTypeRegex; + + public LinkedList lines = new LinkedList<>(); + + public BlockList() { + autoBreak = true; + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + boolean ordered; + String pattern; + if (Character.isDigit(line.text.charAt(0))) { + ordered = true; // ol + pattern = "[0-9]{1,9}+[.\\)]"; + } else { + ordered = false; // ul + pattern = "[*+-]"; + } + Matcher m = Pattern.compile("^(" + pattern + "([ ]++|$))(.*+)").matcher(line.text); + if (m.find()) { + String marker = m.group(1); + String body = m.group(3); + + int contentIndent = m.group(2).length(); + if (contentIndent >= 5) { + contentIndent--; + marker = marker.substring(0, -contentIndent); + while (contentIndent > 0) { + body = " " + body; + contentIndent--; + } + } else if (contentIndent == 0) { + marker += " "; + } + String markerWithoutWhitespace = marker.substring(0, marker.indexOf(' ')); + + BlockList b = parseDown.new BlockList(); + b.indent = line.indent; + b.pattern = pattern; + b.ordered = ordered; + b.marker = marker; + b.markerType = !ordered ? + markerWithoutWhitespace : + markerWithoutWhitespace.substring(markerWithoutWhitespace.length() - 1, markerWithoutWhitespace.length()); + b.markerTypeRegex = Pattern.quote(b.markerType); + + b.lines.add(body); + + return b; + } else + return null; + } + + @Override + public boolean isContinuable() { + return true; + } + + @Override + public Block continueBlock(Line line) { + if (interrupted > 0 && lines.isEmpty()) + return null; + + int requiredIndent = indent + marker.length(); + Matcher m; + if (line.indent < requiredIndent && ( + (ordered && (m = Pattern.compile("^[0-9]++" + markerTypeRegex + "(?:[ ]++(.*)|$)").matcher(line.text)).find()) || + (!ordered && (m = Pattern.compile("^" + markerTypeRegex + "(?:[ ]++(.*)|$)").matcher(line.text)).find()) + )) { + if (interrupted > 0) { + lines.add(""); + loose = true; + interrupted = 0; + } + String text = m.group(1) != null ? m.group(1) : ""; + indent = line.indent; + lines.add(text); + return this; + } else if (line.indent < requiredIndent && BlockList.startBlock(JParseDown.this, line, null) != null) { + return null; + } + + if (line.text.charAt(0) == '[' && BlockReference.startBlock(JParseDown.this, line, null) != null) { + return this; + } + + if (line.indent >= requiredIndent) { + if (interrupted > 0) { + lines.add(""); + loose = true; + interrupted = 0; + } + String text = line.body.substring(requiredIndent); + lines.add(text); + return this; + } + + if (interrupted == 0) { + String text = line.body.replaceAll("^[ ]{0," + requiredIndent + "}+", ""); + lines.add(text); + return this; + } + + return null; + } + + @Override + public boolean isCompletable() { + return true; + } + + @Override + public Block completeBlock() { + if (loose) { + if (!lines.getLast().isEmpty()) + lines.add(""); + } + return this; + } + + @Override + public Collection inline(JParseDown parseDown) { + LinkedList inlines = new LinkedList<>(); + for (String line : lines) { + if (!inlines.isEmpty()) + inlines.add(new InlineLineBreak()); + inlines.addAll(parseDown.lineElements(line, nonNestables)); + } + return inlines; + } + + @Override + public String toString() { + return "BlockList{" + + "lines=[" + String.join(", ", lines) + + "]}"; + } + } + + public static class BlockQuote extends Block { + public final LinkedList lines = new LinkedList<>(); + + public BlockQuote(String text) { + if (text != null) + lines.add(text); + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + Matcher m; + if ((m = Pattern.compile("^>[ ]?+(.*+)").matcher(line.text)).find()) { + return new BlockQuote(m.group(1)); + } else + return null; + } + + @Override + public boolean isContinuable() { + return true; + } + + @Override + public Block continueBlock(Line line) { + if (interrupted > 0) + return null; + Matcher m; + if (line.text.charAt(0) == '>' && (m = Pattern.compile("^>[ ]?+(.*+)").matcher(line.text)).find()) { + lines.add(m.group(1)); + return this; + } + if (interrupted == 0) { + lines.add(line.text); + return this; + } + return null; + } + + @Override + public Collection inline(JParseDown parseDown) { + LinkedList inlines = new LinkedList<>(); + for (String line : lines) { + if (!inlines.isEmpty()) + inlines.add(new InlineLineBreak()); + inlines.addAll(parseDown.lineElements(line, nonNestables)); + } + return inlines; + } + + @Override + public String toString() { + return "BlockQuote{" + + "lines=[" + String.join(", ", lines) + + "]}"; + } + } + + public static class BlockRule { + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + char marker = line.text.charAt(0); + int count = startSpan(line.text, marker); + if (count >= 3 && line.text.trim().length() == count) { + return new BlockHorizontalRule(); + } else + return null; + } + } + + public static class BlockHorizontalRule extends Block { + public BlockHorizontalRule() { + autoBreak = true; + } + + @Override + public Collection inline(JParseDown parseDown) { + return Collections.singletonList(new InlineHorizontalRule()); + } + + @Override + public String toString() { + return "BlockHorizontalRule{}"; + } + } + + public static class InlineLineBreak extends Inline { + @Override + public String toString() { + return "InlineLineBreak{}"; + } + } + + public static class InlineHorizontalRule extends Inline { + @Override + public String toString() { + return "InlineHorizontalRule{}"; + } + } + + public static String regexHtmlAttribute = "[a-zA-Z_:][\\w:.-]*+(?:\\s*+=\\s*+(?:[^\"\\'=<>`\\s]+|\"[^\"]*+\"|\\'[^\\']*+\\'))?+"; + + public static class BlockReference extends Block { + public final String id; + public final ReferenceData data; + + public BlockReference(String id, ReferenceData data) { + this.id = id; + this.data = data; + } + + public static Block startBlock(JParseDown parseDown, Line line, Block block) { + Matcher m; + if (line.text.indexOf(']') >= 0 && (m = Pattern.compile("^\\[(.+?)\\]:[ ]*+?(?:[ ]+[\"\\'(](.+)[\"\\')])?[ ]*+$").matcher(line.text)).find()) { + String id = m.group(1).toLowerCase(); + ReferenceData data = new ReferenceData(parseDown.convertUrl(m.group(2)), m.group(3)); + parseDown.referenceDefinitions.put(id, data); + return new BlockReference(id, data); + } else + return null; + } + + @Override + public Collection inline(JParseDown parseDown) { + return Collections.emptyList(); + } + + @Override + public String toString() { + return "BlockReference{" + + "id='" + id + '\'' + + '}'; + } + } + + public abstract static class Inline extends Component { + public int extent; + public int position = -1; + + public Inline() { + } + + public Inline setExtent(String s) { + this.extent = s.length(); + return this; + } + + public Inline setExtent(int len) { + this.extent = len; + return this; + } + } + + public static class InlineText extends Inline { + public final String text; + + public InlineText(String text) { + this.text = text; + } + + public static Collection inline(JParseDown parseDown, String text, String context) { + return replaceAllElements( + parseDown.breaksEnabled ? "[ ]*+\\n" : "(?:[ ]*+\\\\|[ ]{2,}+)\\n", + new Inline[]{ + new InlineLineBreak() + }, + text, + t -> { + InlineText inlineText = new InlineText(text); + inlineText.setExtent(text); + return inlineText; + }).stream().filter(inline -> !(inline instanceof InlineText) || !((InlineText) inline).text.isEmpty()) + .collect(Collectors.toList()); + } + + @Override + public String toString() { + return "InlineText{" + + "text='" + text + '\'' + + '}'; + } + } + + public static class InlineCode extends Inline { + public final String text; + + public InlineCode(String text) { + this.text = text; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + char marker = text.charAt(0); + Pattern regex = Pattern.compile("^([" + marker + "]++)[ ]*+(.+?)[ ]*+(?') < 0) + return null; + String hostnameLabel = "[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"; + String commonMarkEmail = "[a-zA-Z0-9.!#$%&\\'*+\\/=?^_`{|}~-]++@" + + hostnameLabel + "(?:\\." + hostnameLabel + ")*"; + + Matcher m = Pattern.compile("^<((mailto:)?" + commonMarkEmail + ")>", Pattern.CASE_INSENSITIVE).matcher(text); + if (m.find()) { + String url = m.group(1); + return new InlineEmailTag(url).setExtent(m.group(0)); + } else + return null; + } + + @Override + public String toString() { + return "InlineEmailTag{" + + "url='" + url + '\'' + + '}'; + } + } + + public static Pattern[] strongRegex = { + Pattern.compile("^[*]{2}((?:\\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])", Pattern.DOTALL), + Pattern.compile("^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)", Pattern.DOTALL | Pattern.UNICODE_CHARACTER_CLASS), + }; + + public static Pattern[] emRegex = { + Pattern.compile("^[*]((?:\\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])", Pattern.DOTALL), + Pattern.compile("^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\\b", Pattern.DOTALL | Pattern.UNICODE_CHARACTER_CLASS), + }; + + public static class InlineBold extends Inline { + public final String text; + + public InlineBold(String text) { + this.text = text; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + if (text.length() < 2) + return null; + char marker = text.charAt(0); + int markerIndex = marker == '*' ? 0 : 1; + + Matcher m; + if (text.charAt(1) == marker && (m = strongRegex[markerIndex].matcher(text)).find()) + return new InlineBold(m.group(1)).setExtent(m.group(0)); + else if ((m = emRegex[markerIndex].matcher(text)).find()) + return new InlineItalic(m.group(1)).setExtent(m.group(0)); + else + return null; + } + + @Override + public String toString() { + return "InlineBold{" + + "text='" + text + '\'' + + '}'; + } + } + + public static class InlineItalic extends Inline { + public final String text; + + public InlineItalic(String text) { + this.text = text; + } + + @Override + public String toString() { + return "InlineItalic{" + + "text='" + text + '\'' + + '}'; + } + } + + public static String specialCharacters = "\\`*_{}[]()>#+-.!|~"; + + public static class InlineEscapeSequence extends Inline { + public final String rawHtml; + + public InlineEscapeSequence(String rawHtml) { + this.rawHtml = rawHtml; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + if (text.length() > 1 && specialCharacters.indexOf(text.charAt(1)) >= 0) { + return new InlineEscapeSequence(Character.toString(text.charAt(1))).setExtent(2); + } else + return null; + } + + @Override + public String toString() { + return "InlineEscapeSequence{" + + "rawHtml='" + rawHtml + '\'' + + '}'; + } + } + + public static class InlineImage extends Inline { + public final String src; + public final String alternateText; + + public InlineImage(String src, String alternateText) { + this.src = src; + this.alternateText = alternateText; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + if (text.length() < 2 || text.charAt(1) != '[') + return null; + text = text.substring(1); + + InlineLink link = (InlineLink) InlineLink.inline(parseDown, text, context); + if (link == null) + return null; + + return new InlineImage(link.url, link.text).setExtent(link.extent + 1); + } + + @Override + public String toString() { + return "InlineImage{" + + "src='" + src + '\'' + + ", alt='" + alternateText + '\'' + + '}'; + } + } + + public static class InlineLink extends Inline { + public final String text; + public final String url; + public final String title; + + public InlineLink(String text, String url, String title) { + this.text = text; + this.url = url; + this.title = title; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + String elementText; + String url; + String title = null; + + int extent = 0; + String remainder = text; + + Matcher m; + // Parsedown original pattern: "\\[((?:[^][]++|(?R))*+)\\]" (does not compile in Java) + if ((m = Pattern.compile("\\[((?:\\\\.|[^\\[\\]]|!\\[[^\\[\\]]*\\])*)\\]").matcher(remainder)).find()) { + elementText = m.group(1); + extent += m.group(0).length(); + remainder = remainder.substring(extent); + } else + return null; + + if ((m = Pattern.compile("^[(]\\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+(\"[^\"]*+\"|\\'[^\\']*+\'))?\\s*+[)]").matcher(remainder)).find()) { + url = parseDown.convertUrl(m.group(1)); + if (m.group(2) != null) + title = m.group(2).substring(1, m.group(2).length() - 1); + extent += m.group(0).length(); + } else { + String definition; + if ((m = Pattern.compile("^\\s*\\[(.*?)\\]").matcher(remainder)).find()) { + definition = !m.group(1).isEmpty() ? m.group(1) : elementText; + definition = definition.toLowerCase(); + extent += m.group(0).length(); + } else { + definition = elementText.toLowerCase(); + } + + ReferenceData reference = parseDown.referenceDefinitions.get(definition); + if (reference == null) + return null; + url = reference.url; + title = reference.title; + } + + Inline inline = new InlineLink(elementText, url, title).setExtent(extent); + inline.nonNestables.add(InlineUrl.class); + inline.nonNestables.add(InlineLink.class); + return inline; + } + + @Override + public String toString() { + return "InlineLink{" + + "text='" + text + '\'' + + ", url='" + url + '\'' + + ", title='" + title + '\'' + + '}'; + } + } + + public static class InlineMarkup extends Inline { + public final String rawHtml; + + public InlineMarkup(String rawHtml) { + this.rawHtml = rawHtml; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + if (parseDown.markupEscaped || parseDown.safeMode || text.indexOf('>') < 0) + return null; + + Matcher m; + if (text.charAt(1) == '/' && (m = Pattern.compile("^<\\/\\w[\\w-]*+[ ]*+>", Pattern.DOTALL).matcher(text)).find()) { + return new InlineMarkup(m.group(0)).setExtent(m.group(0)); + } + if (text.charAt(1) == '!' && (m = Pattern.compile("^", Pattern.DOTALL).matcher(text)).find()) { + return new InlineMarkup(m.group(0)).setExtent(m.group(0)); + } + if (text.charAt(1) != ' ' && (m = Pattern.compile("^<\\w[\\w-]*+(?:[ ]*+" + regexHtmlAttribute + ")*+[ ]*+\\/?>", Pattern.DOTALL).matcher(text)).find()) { + return new InlineMarkup(m.group(0)).setExtent(m.group(0)); + } + return null; + } + + @Override + public String toString() { + return "InlineMarkup{" + + "rawHtml='" + rawHtml + '\'' + + '}'; + } + } + + public static class InlineSpecialCharacter extends Inline { + public final String rawHtml; + + public InlineSpecialCharacter(String rawHtml) { + this.rawHtml = rawHtml; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + Matcher m; + if (text.length() > 1 && text.charAt(1) != ' ' && text.indexOf(';') >= 0 && + (m = Pattern.compile("^&(#?+[0-9a-zA-Z]++);").matcher(text)).find()) { + return new InlineSpecialCharacter("&" + m.group(1) + ";").setExtent(m.group(0)); + } else + return null; + } + + @Override + public String toString() { + return "InlineSpecialCharacter{" + + "rawHtml='" + rawHtml + '\'' + + '}'; + } + } + + public static class InlineStrikeThrough extends Inline { + public final String text; + + public InlineStrikeThrough(String text) { + this.text = text; + } + + public static Inline inline(JParseDown parseDown, String text, String context) { + if (text.length() < 2) + return null; + Matcher m; + if (text.charAt(1) == '~' && (m = Pattern.compile("^~~(?=\\S)(.+?)(?<=\\S)~~").matcher(text)).find()) { + return new InlineStrikeThrough(m.group(1)).setExtent(m.group(0)); + } else + return null; + } + + @Override + public String toString() { + return "InlineStrikeThrough{" + + "text='" + text + '\'' + + '}'; + } + } + + public static class InlineUrl { + public static Inline inline(JParseDown parseDown, String text, String context) { + if (!parseDown.urlsLinked || text.length() < 3 || text.charAt(2) != '/') + return null; + Matcher m; + if (context.contains("http") && (m = Pattern.compile("\\bhttps?:[\\/]{2}[^\\s<]+\\b\\/*", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS).matcher(context)).find()) { + String url = parseDown.convertUrl(m.group(0)); + Inline inline = new InlineLink(url, url, null).setExtent(url); + inline.position = m.start(0); + return inline; + } else + return null; + } + } + + public static class InlineUrlTag { + public static Inline inline(JParseDown parseDown, String text, String context) { + Matcher m; + if (text.indexOf('>') >= 0 && (m = Pattern.compile("^<(\\w+:\\/{2}[^ >]+)>", Pattern.DOTALL).matcher(text)).find()) { + String url = parseDown.convertUrl(m.group(1)); + return new InlineLink(url, url, null).setExtent(m.group(0)); + } else { + return null; + } + } + } + + public boolean breaksEnabled = false; + public boolean markupEscaped = false; + public boolean urlsLinked = true; + public boolean safeMode = false; + public boolean strictMode = false; + + public String mdUrlReplacement = null; + + public HashMap referenceDefinitions = new HashMap<>(); + + public LinkedList textElements(String text) { + text = text.replaceAll("\\r\\n?", "\n"); + text = text.replaceAll("(^\\n+)|(\\n+$)", ""); + String[] lines = text.split("\n"); + return this.linesElements(lines); + } + + public JParseDown setBreaksEnabled(boolean breaksEnabled) { + this.breaksEnabled = breaksEnabled; + return this; + } + + public JParseDown setMarkupEscaped(boolean markupEscaped) { + this.markupEscaped = markupEscaped; + return this; + } + + public JParseDown setUrlsLinked(boolean urlsLinked) { + this.urlsLinked = urlsLinked; + return this; + } + + public JParseDown setSafeMode(boolean safeMode) { + this.safeMode = safeMode; + return this; + } + + public JParseDown setStrictMode(boolean strictMode) { + this.strictMode = strictMode; + return this; + } + + public JParseDown setMdUrlReplacement(String replacement) { + this.mdUrlReplacement = replacement; + return this; + } + + public void getBlockTypes(char marker, LinkedList> types) { + switch (marker) { + case '#': + types.add(BlockHeader::startBlock); + return; + case '+': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + types.add(BlockList::startBlock); + return; + // case '*': + case '-': + types.add(BlockRule::startBlock); + types.add(BlockList::startBlock); + return; + case '>': + types.add(BlockQuote::startBlock); + return; + case '[': + types.add(BlockReference::startBlock); + return; + case '_': + types.add(BlockRule::startBlock); + return; + case '`': + case '~': + types.add(BlockFencedCode::startBlock); + return; + } + } + + public void getUnmarkedBlockTypes(LinkedList> types) { + types.add(BlockCode::startBlock); + } + + public LinkedList linesElements(LinkedList lines) { + return linesElements(lines.toArray(new String[0])); + } + + public LinkedList linesElements(String[] lines) { + LinkedList elements = new LinkedList<>(); + Block currentBlock = null; + + line: + for (String line : lines) { + if (line.trim().isEmpty()) { + if (currentBlock != null) + currentBlock.interrupted++; + continue; + } + + int tabIndex; + while ((tabIndex = line.indexOf('\t')) >= 0) { + int shortage = 4 - tabIndex % 4; + StringBuilder sb = new StringBuilder(); + sb.append(line.substring(0, tabIndex)); + for (int i = 0; i < shortage; i++) + sb.append(' '); + sb.append(line.substring(tabIndex + 1)); + line = sb.toString(); + } + + Line lineObj = new Line(line); + + if (currentBlock != null && currentBlock.isContinuable()) { + Block block = currentBlock.continueBlock(lineObj); + if (block != null) { + currentBlock = block; + continue; + } else if (currentBlock.isCompletable()) { + currentBlock = currentBlock.completeBlock(); + } + } + + LinkedList> blockTypes = new LinkedList<>(); + getUnmarkedBlockTypes(blockTypes); + getBlockTypes(lineObj.text.charAt(0), blockTypes); + + for (BlockType blockType : blockTypes) { + Block block = blockType.startBlock(this, lineObj, currentBlock); + if (block != null) { + if (!block.identified) { + if (currentBlock != null) { + elements.add(currentBlock); + } + block.identified = true; + } + currentBlock = block; + continue line; + } + } + + Block block = null; + if (currentBlock instanceof BlockParagraph) { + block = currentBlock.continueBlock(lineObj); + } + + if (block != null) { + currentBlock = block; + } else { + if (currentBlock != null) { + elements.add(currentBlock); + } + currentBlock = BlockParagraph.startBlock(this, lineObj, null); + currentBlock.identified = true; + } + } + + if (currentBlock != null && currentBlock.isContinuable() && currentBlock.isCompletable()) { + currentBlock = currentBlock.completeBlock(); + } + if (currentBlock != null) { + elements.add(currentBlock); + } + + for (Block element : elements) { + element.inlines = new LinkedList<>(element.inline(this)); + if (element.autoBreak == null) + element.autoBreak = false; + } + + return elements; + } + + public InlineType[] getInlineTypes(char marker) { + switch (marker) { + case '!': + return new InlineType[]{InlineImage::inline}; + case '&': + return new InlineType[]{InlineSpecialCharacter::inline}; + case '*': + return new InlineType[]{InlineBold::inline}; + case ':': + return new InlineType[]{InlineUrl::inline}; + case '<': + return new InlineType[]{InlineUrlTag::inline, InlineEmailTag::inline, InlineMarkup::inline}; + case '[': + return new InlineType[]{InlineLink::inline}; + case '_': + return new InlineType[]{InlineBold::inline}; + case '`': + return new InlineType[]{InlineCode::inline}; + case '~': + return new InlineType[]{InlineStrikeThrough::inline}; + case '\\': + return new InlineType[]{(parseDown, text, context) -> { + if (text.length() >= 2 && text.charAt(0) == '\\' && text.charAt(1) == 'n') { + return new InlineLineBreak().setExtent(2); + } + return null; + }}; + default: + return new InlineType[]{}; + } + } + + public Pattern inlineMarkerList = Pattern.compile("[!\\*_&\\[:<`~\\\\]"); + + public LinkedList lineElements(String text, HashSet> nonNestables) { + text = text.replaceAll("\\r\\n?", "\n"); + LinkedList elements = new LinkedList<>(); + if (nonNestables == null) + nonNestables = new HashSet<>(); + + text: + for (; ; ) { + Matcher m = inlineMarkerList.matcher(text); + if (!m.find()) + break; + int markerPosition = m.start(); + String excerpt = text.substring(markerPosition); + + for (InlineType inlineType : getInlineTypes(excerpt.charAt(0))) { + if (nonNestables.contains(inlineType.getClass())) + continue; + Inline inline = inlineType.inline(this, excerpt, text); + if (inline == null) + continue; + + if (inline.position >= 0 && inline.position > markerPosition) + continue; + if (inline.position < 0) + inline.position = markerPosition; + + inline.nonNestables.addAll(nonNestables); + + String unmarkedText = text.substring(0, inline.position); + elements.addAll(InlineText.inline(this, unmarkedText, null)); + + elements.add(inline); + + text = text.substring(inline.position + inline.extent); + continue text; + } + + String unmarkedText = text.substring(0, markerPosition + 1); + elements.addAll(InlineText.inline(this, unmarkedText, null)); + + text = text.substring(markerPosition + 1); + } + + elements.addAll(InlineText.inline(this, text, null)); + + return elements; + } + + public String convertUrl(String url) { + if (mdUrlReplacement == null || url.indexOf(':') >= 0) + return url; + Matcher m = Pattern.compile("(\\.md)(#.*)?$").matcher(url); + if (m.find()) + return m.replaceFirst(mdUrlReplacement + "$2"); + else + return url; + } + + public static LinkedList replaceAllElements(String regex, C[] elements, String text, Function elementFactory) { + LinkedList newElements = new LinkedList<>(); + Matcher m = Pattern.compile(regex).matcher(text); + int end = 0; + while (m.find()) { + String before = text.substring(end, m.start()); + newElements.add(elementFactory.apply(before)); + Collections.addAll(newElements, elements); + end = m.end(); + } + newElements.add(elementFactory.apply(text.substring(end))); + return newElements; + } + + public static int startSpan(String s, char c) { + int i = 0; + int len = s.length(); + while (i < len && s.charAt(i) == c) i++; + return i; + } +} \ No newline at end of file diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDownToMinecraft.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDownToMinecraft.java new file mode 100644 index 000000000..b4fd814c6 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/changelog/JParseDownToMinecraft.java @@ -0,0 +1,70 @@ +package me.shedaniel.rei.impl.client.gui.changelog; + +import com.mojang.blaze3d.platform.NativeImage; +import me.shedaniel.rei.api.common.util.ImmutableTextComponent; +import me.shedaniel.rei.impl.client.gui.error.ErrorsEntryListWidget; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.network.chat.*; +import net.minecraft.resources.ResourceLocation; + +import java.io.IOException; +import java.io.InputStream; + +public class JParseDownToMinecraft { + public static Component toComponent(JParseDown.Inline inline) { + if (inline instanceof JParseDown.InlineText) { + return new TextComponent(((JParseDown.InlineText) inline).text.replace("\n", " ")); + } else if (inline instanceof JParseDown.InlineBold) { + return new TextComponent(((JParseDown.InlineBold) inline).text.replace("\n", " ")) + .withStyle(ChatFormatting.BOLD); + } else if (inline instanceof JParseDown.InlineItalic) { + return new TextComponent(((JParseDown.InlineItalic) inline).text.replace("\n", " ")) + .withStyle(ChatFormatting.ITALIC); + } else if (inline instanceof JParseDown.InlineLink) { + return new TextComponent(((JParseDown.InlineLink) inline).text.replace("\n", " ")) + .withStyle(style -> style.withColor(TextColor.fromRgb(0x1fc3ff)).withUnderlined(true) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new TextComponent(((JParseDown.InlineLink) inline).url) + .withStyle(ChatFormatting.GRAY))) + .withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, ((JParseDown.InlineLink) inline).url))); + } else if (inline instanceof JParseDown.InlineStrikeThrough) { + return new TextComponent(((JParseDown.InlineStrikeThrough) inline).text.replace("\n", " ")) + .withStyle(ChatFormatting.STRIKETHROUGH); + } + return null; + } + + public static void build(ChangelogLoader.Builder builder, JParseDown.Block block) { + MutableComponent lastComponent = ImmutableTextComponent.EMPTY; + ChangelogLoader.Builder finalBuilder = builder; + for (JParseDown.Inline inline : block.inlines) { + Component component = toComponent(inline); + if (component != null) { + lastComponent = lastComponent.append(component); + } else { + builder.add(lastComponent); + lastComponent = ImmutableTextComponent.EMPTY; + if (inline instanceof JParseDown.InlineLineBreak) { + continue; + } else if (inline instanceof JParseDown.InlineHorizontalRule) { + builder.add(ErrorsEntryListWidget.HorizontalRuleEntry::new); + } else if (inline instanceof JParseDown.InlineImage) { + InputStream stream = builder.getClass().getClassLoader().getResourceAsStream(((JParseDown.InlineImage) inline).src); + if (stream != null) { + try { + DynamicTexture texture = new DynamicTexture(NativeImage.read(stream)); + ResourceLocation id = Minecraft.getInstance().getTextureManager().register("rei_md_image_", texture); + builder.add(width -> new ErrorsEntryListWidget.ImageEntry(width, texture, id)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + } + if (lastComponent != ImmutableTextComponent.EMPTY) { + builder.add(lastComponent); + } + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/error/ErrorsEntryListWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/error/ErrorsEntryListWidget.java index 33da45a56..ff4a99238 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/error/ErrorsEntryListWidget.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/error/ErrorsEntryListWidget.java @@ -23,20 +23,31 @@ package me.shedaniel.rei.impl.client.gui.error; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; impo