Created
December 29, 2022 21:17
-
-
Save Joo200/54cf800aabb065d9f7f2015ef9a24d9d to your computer and use it in GitHub Desktop.
HtmlSerializer for Adventure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package de.terraconia.core.lib.misc; | |
| /* | |
| * Important: you need owasp html sanitizer to sanitize the output and deny html injection. | |
| */ | |
| import de.terraconia.owasp.html.PolicyFactory; | |
| import de.terraconia.owasp.html.Sanitizers; | |
| import net.kyori.adventure.text.ComponentLike; | |
| import net.kyori.adventure.text.flattener.ComponentFlattener; | |
| import net.kyori.adventure.text.flattener.FlattenerListener; | |
| import net.kyori.adventure.text.format.Style; | |
| import net.kyori.adventure.text.format.TextColor; | |
| import net.kyori.adventure.text.format.TextDecoration; | |
| import net.kyori.adventure.text.format.TextFormat; | |
| import java.util.ArrayDeque; | |
| import java.util.Deque; | |
| import java.util.concurrent.ThreadLocalRandom; | |
| public class HtmlComponentSerializer { | |
| private static final PolicyFactory SANITIZER = Sanitizers.STYLES.and(Sanitizers.FORMATTING); | |
| private static final ComponentFlattener flattener = ComponentFlattener.textOnly(); | |
| private HtmlComponentSerializer() {} | |
| public static String serialize(final ComponentLike componentLike) { | |
| final HtmlFlattener state = new HtmlFlattener(); | |
| flattener.flatten(componentLike.asComponent(), state); | |
| return SANITIZER.sanitize(state.toString()); | |
| } | |
| private static final class HtmlFlattener implements FlattenerListener { | |
| private static final char[] OBFUSCATED_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); | |
| private static final String CLOSE_SPAN = "</span>"; | |
| private final StringBuilder sb = new StringBuilder(); | |
| private final Deque<Style> stack = new ArrayDeque<>(); | |
| private HtmlFlattener() { | |
| } | |
| @Override | |
| public void pushStyle(final Style style) { | |
| this.stack.push(style); | |
| } | |
| @Override | |
| public void component(final String text) { | |
| Style style = this.stack.stream().reduce(Style.empty(), (a, b) -> b.merge(a)); | |
| final int i = this.append(style); | |
| if (style.decorations().get(TextDecoration.OBFUSCATED) == TextDecoration.State.TRUE) { | |
| text.codePoints().forEach($ -> | |
| this.sb.append(OBFUSCATED_CHARS[ThreadLocalRandom.current().nextInt(OBFUSCATED_CHARS.length)])); | |
| } else { | |
| this.sb.append(text); | |
| } | |
| this.close(i); | |
| } | |
| @Override | |
| public void popStyle(final Style style) { | |
| this.stack.removeLastOccurrence(style); | |
| } | |
| private void append(final TextFormat format) { | |
| this.sb.append(asHtml(format)); | |
| } | |
| private void close(final int i) { | |
| this.sb.append(CLOSE_SPAN.repeat(Math.max(0, i))); | |
| } | |
| private int append(final Style style) { | |
| final int[] opened = {0}; | |
| final TextColor color = style.color(); | |
| if (color != null) { | |
| this.append(color); | |
| opened[0]++; | |
| } | |
| style.decorations().forEach((decoration, state) -> { | |
| if (decoration == TextDecoration.OBFUSCATED) { | |
| return; // handled elsewhere | |
| } | |
| if (state == TextDecoration.State.TRUE) { | |
| this.append(decoration); | |
| opened[0]++; | |
| } | |
| }); | |
| return opened[0]; | |
| } | |
| public String toString() { | |
| return this.sb.toString(); | |
| } | |
| private static String asHtml(final TextFormat format) { | |
| if (format instanceof TextColor textColor) { | |
| return "<span style='color:" + textColor.asHexString() + "'>"; | |
| } else if (format == TextDecoration.OBFUSCATED) { | |
| return ""; // handled elsewhere | |
| } else if (format instanceof TextDecoration decoration) { | |
| final String inner = switch (decoration) { | |
| case BOLD -> "font-weight:bold"; | |
| case ITALIC -> "font-style:italic"; | |
| case OBFUSCATED -> throw new IllegalStateException(); // needed to satisfy compiler | |
| case UNDERLINED -> "text-decoration:underline"; | |
| case STRIKETHROUGH -> "text-decoration:line-through"; | |
| }; | |
| return "<span style='" + inner + "'>"; | |
| } | |
| throw new IllegalArgumentException("Cannot handle format: " + format + " (" + format.getClass().getTypeName() + ")"); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment