Skip to content

Instantly share code, notes, and snippets.

@Joo200
Created December 29, 2022 21:17
Show Gist options
  • Select an option

  • Save Joo200/54cf800aabb065d9f7f2015ef9a24d9d to your computer and use it in GitHub Desktop.

Select an option

Save Joo200/54cf800aabb065d9f7f2015ef9a24d9d to your computer and use it in GitHub Desktop.
HtmlSerializer for Adventure
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