From 159412ca16d0710db99c14b2d961c05d419aee05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n?= <44579213+Rubenicos@users.noreply.github.com> Date: Sun, 17 Sep 2023 09:02:22 -0600 Subject: [PATCH] Added support for emit and load yaml comments --- .../yaml/YamlConfigurationLoader.java | 198 +++++++++++++++++- 1 file changed, 187 insertions(+), 11 deletions(-) diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlConfigurationLoader.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlConfigurationLoader.java index e68418492..d71ba7f52 100644 --- a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlConfigurationLoader.java +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlConfigurationLoader.java @@ -20,23 +20,22 @@ import org.spongepowered.configurate.CommentedConfigurationNode; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationOptions; -import org.spongepowered.configurate.loader.AbstractConfigurationLoader; -import org.spongepowered.configurate.loader.CommentHandler; -import org.spongepowered.configurate.loader.CommentHandlers; -import org.spongepowered.configurate.loader.LoaderOptionSource; +import org.spongepowered.configurate.loader.*; import org.spongepowered.configurate.util.UnmodifiableCollections; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.*; import org.yaml.snakeyaml.representer.Representer; import java.io.BufferedReader; import java.io.Writer; import java.math.BigInteger; import java.sql.Timestamp; -import java.util.Date; -import java.util.Set; +import java.util.*; /** * A loader for YAML-formatted configurations, using the SnakeYAML library for @@ -70,8 +69,14 @@ public static Builder builder() { * *

This builder supports the following options:

*
+ *
<prefix>.yaml.pretty-printing
+ *
Equivalent to {@link #prettyPrinting(boolean)}
*
<prefix>.yaml.node-style
*
Equivalent to {@link #nodeStyle(NodeStyle)}
+ *
<prefix>.yaml.emit-comments
+ *
Equivalent to {@link #emitComments(boolean)}
+ *
<prefix>.yaml.load-comments
+ *
Equivalent to {@link #loadComments(boolean)}
*
* * @since 4.0.0 @@ -79,6 +84,7 @@ public static Builder builder() { public static final class Builder extends AbstractConfigurationLoader.Builder { private final DumperOptions options = new DumperOptions(); private @Nullable NodeStyle style; + private boolean loadComments; Builder() { this.indent(4); @@ -88,10 +94,13 @@ public static final class Builder extends AbstractConfigurationLoader.BuilderComments will always be loaded from files and + * stored in memory.

+ * + * @param emitComments whether to emit comments + * @return this builder + * @since 4.2.0 + */ + public Builder emitComments(final boolean emitComments) { + this.options.setProcessComments(emitComments); + return this; + } + + /** + * Set whether comments should be loaded and stored in memory. + * + * @param loadComments whether to load comments + * @return this builder + * @since 4.2.0 + */ + public Builder loadComments(final boolean loadComments) { + this.loadComments = loadComments; + return this; + } + /** * Gets the node style to be used by the resultant loader. * @@ -164,28 +212,124 @@ public YamlConfigurationLoader build() { } } - private final ThreadLocal yaml; + private final ThreadLocal yaml; private YamlConfigurationLoader(final Builder builder) { super(builder, new CommentHandler[] {CommentHandlers.HASH}); final LoaderOptions loaderOpts = new LoaderOptions() .setAcceptTabs(true) - .setProcessComments(false); + .setProcessComments(builder.loadComments); loaderOpts.setCodePointLimit(Integer.MAX_VALUE); final DumperOptions opts = builder.options; opts.setDefaultFlowStyle(NodeStyle.asSnakeYaml(builder.style)); - this.yaml = ThreadLocal.withInitial(() -> new Yaml(new Constructor(loaderOpts), new Representer(opts), opts, loaderOpts)); + this.yaml = ThreadLocal.withInitial(() -> new PublicYaml(new PublicConstructor(loaderOpts), new Representer(opts), opts, loaderOpts)); } @Override protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) { - node.raw(this.yaml.get().load(reader)); + final PublicYaml yaml = this.yaml.get(); + readYamlNode(yaml.getConstructor(), realNode(yaml.compose(reader)), node); + } + + private static Node realNode(final Node yamlNode) { + if (yamlNode instanceof AnchorNode) { + return ((AnchorNode) yamlNode).getRealNode(); + } else { + return yamlNode; + } + } + + private static void readYamlNode(final PublicConstructor constructor, final Node yamlNode, final CommentedConfigurationNode node) { + readComment(yamlNode.getBlockComments(), node); + if (yamlNode instanceof MappingNode) { + if (((MappingNode) yamlNode).getValue().isEmpty()) { + node.raw(Collections.emptyMap()); + } else { + for (NodeTuple tuple : ((MappingNode) yamlNode).getValue()) { + final ScalarNode keyNode = (ScalarNode) tuple.getKeyNode(); + final CommentedConfigurationNode configNode = node.node(keyNode.getValue()); + + readComment(keyNode.getBlockComments(), configNode); + readYamlNode(constructor, realNode(tuple.getValueNode()), configNode); + } + } + } else if (yamlNode instanceof SequenceNode) { + if (((SequenceNode) yamlNode).getValue().isEmpty()) { + node.raw(Collections.emptyList()); + } else { + for (Node o : ((SequenceNode) yamlNode).getValue()) { + readYamlNode(constructor, realNode(o), node.appendListNode()); + } + } + } else { + node.raw(constructor.constructObject(yamlNode)); + } + } + + private static void readComment(final List list, final CommentedConfigurationNode node) { + if (list == null || node.comment() != null) { + return; + } + final StringJoiner comment = new StringJoiner(CONFIGURATE_LINE_SEPARATOR); + for (CommentLine line : list) { + String s; + if (line.getCommentType() == CommentType.BLANK_LINE) { + s = ""; + } else { + s = line.getValue().replace("\r", ""); + if (!s.isEmpty() && s.charAt(0) == ' ') { + s = s.substring(1); + } + } + comment.add(s); + } + node.comment(comment.toString()); } @Override protected void saveInternal(final ConfigurationNode node, final Writer writer) { - this.yaml.get().dump(node.raw(), writer); + final PublicYaml yaml = this.yaml.get(); + yaml.serialize(fromNode(yaml.getRepresenter(), node, true), writer); + } + + private static Node fromNode(final Representer representer, final ConfigurationNode node, final boolean comment) { + final Node yamlNode; + if (node.isMap()) { + final List value = new ArrayList<>(); + for (Map.Entry entry : node.childrenMap().entrySet()) { + final Node keyNode = representer.represent(entry.getKey()); + writeComment(entry.getValue(), keyNode); + value.add(new NodeTuple(keyNode, fromNode(representer, entry.getValue(), false))); + } + yamlNode = new MappingNode(Tag.MAP, value, representer.getDefaultFlowStyle()); + } else if (node.isList()) { + final List value = new ArrayList<>(); + for (ConfigurationNode child : node.childrenList()) { + value.add(fromNode(representer, child, true)); + } + yamlNode = new SequenceNode(Tag.SEQ, value, representer.getDefaultFlowStyle()); + } else { + yamlNode = representer.represent(node.rawScalar()); + } + if (comment) { + writeComment(node, yamlNode); + } + return yamlNode; + } + + private static void writeComment(final ConfigurationNode node, final Node yamlNode) { + if (node instanceof CommentedConfigurationNode && ((CommentedConfigurationNode) node).comment() != null) { + final List comment = new ArrayList<>(); + for (String line : CONFIGURATE_LINE_PATTERN.split(((CommentedConfigurationNode) node).comment())) { + if (line.trim().isEmpty()) { + comment.add(new CommentLine(null, null, "", CommentType.BLANK_LINE)); + } else { + comment.add(new CommentLine(null, null, ' ' + line, CommentType.BLOCK)); + } + } + yamlNode.setBlockComments(comment); + } } @Override @@ -193,4 +337,36 @@ public CommentedConfigurationNode createNode(final ConfigurationOptions options) return CommentedConfigurationNode.root(options); } + static final class PublicYaml extends Yaml { + + private final PublicConstructor constructor; + private final Representer representer; + + public PublicYaml(PublicConstructor constructor, Representer representer, DumperOptions dumperOptions, LoaderOptions loadingConfig) { + super(constructor, representer, dumperOptions, loadingConfig); + this.constructor = constructor; + this.representer = representer; + } + + public PublicConstructor getConstructor() { + return constructor; + } + + public Representer getRepresenter() { + return representer; + } + } + + static final class PublicConstructor extends Constructor { + + public PublicConstructor(LoaderOptions loadingConfig) { + super(loadingConfig); + } + + @Override + public Object constructObject(Node node) { + return super.constructObject(node); + } + } + }