Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

YAML comments support #410

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,17 @@ static DumperOptions.FlowStyle asSnakeYaml(final @Nullable NodeStyle style) {
return style == null ? DumperOptions.FlowStyle.AUTO : style.snake;
}

static @Nullable NodeStyle fromSnakeYaml(final DumperOptions.FlowStyle style) {
switch (style) {
case AUTO:
return null;
case BLOCK:
return BLOCK;
case FLOW:
return FLOW;
default:
throw new IllegalArgumentException("Unknown style " + style);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.spongepowered.configurate.yaml;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.yaml.snakeyaml.DumperOptions;

import java.util.EnumMap;
import java.util.Map;

/**
* Style that can be used to represent a scalar.
*
* @since 4.2.0
*/
public enum ScalarStyle {

/**
* A double-quoted string.
*
* <pre>"hello world"</pre>
*
* @since 4.2.0
*/
DOUBLE_QUOTED(DumperOptions.ScalarStyle.DOUBLE_QUOTED),

/**
* A single-quoted string.
*
* <pre>'hello world'</pre>
*
* @since 4.2.0
*/
SINGLE_QUOTED(DumperOptions.ScalarStyle.SINGLE_QUOTED),

/**
* String without any quotation.
*
* <p>This may be ambiguous with non-string types.</p>
*
* @since 4.2.0
*/
UNQUOTED(DumperOptions.ScalarStyle.PLAIN),

/**
* Folded scalar.
*
* <pre>{@code
* key: >
* folded scalar
* line breaks collapsed
* }</pre>
*
* @since 4.2.0
*/
FOLDED(DumperOptions.ScalarStyle.FOLDED),

/**
* Literal scalar.
*
* <pre>{@code
* key: |
* literal scalar
* line breaks preserved
* }</pre>
*
* @since 4.2.0
*/
LITERAL(DumperOptions.ScalarStyle.LITERAL)
;

private static final Map<DumperOptions.ScalarStyle, ScalarStyle> BY_SNAKE = new EnumMap<>(DumperOptions.ScalarStyle.class);
private final DumperOptions.ScalarStyle snake;

ScalarStyle(final DumperOptions.ScalarStyle snake) {
this.snake = snake;
}

static DumperOptions.ScalarStyle asSnakeYaml(
final @Nullable ScalarStyle style,
final DumperOptions.@Nullable ScalarStyle fallback
) {
if (style != null) {
return style.snake;
} else if (fallback != null) {
return fallback;
} else {
return DumperOptions.ScalarStyle.PLAIN;
}
}

static ScalarStyle fromSnakeYaml(final DumperOptions.ScalarStyle style) {
return BY_SNAKE.getOrDefault(style, UNQUOTED);
}

static {
for (final ScalarStyle style : values()) {
BY_SNAKE.put(style.snake, style);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.RepresentationHint;
import org.spongepowered.configurate.loader.AbstractConfigurationLoader;
import org.spongepowered.configurate.loader.CommentHandler;
import org.spongepowered.configurate.loader.CommentHandlers;
Expand All @@ -28,8 +29,6 @@
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;

import java.io.BufferedReader;
import java.io.Writer;
Expand All @@ -55,6 +54,25 @@ public final class YamlConfigurationLoader extends AbstractConfigurationLoader<C
Boolean.class, Integer.class, Long.class, BigInteger.class, Double.class, // numeric
byte[].class, String.class, Date.class, java.sql.Date.class, Timestamp.class); // complex types

/**
* The YAML scalar style this node should attempt to use.
*
* <p>If the chosen scalar style would produce syntactically invalid YAML, a
* valid one will replace it.</p>
*
* @since 4.2.0
*/
public static final RepresentationHint<ScalarStyle> SCALAR_STYLE = RepresentationHint.of("configurate:yaml/scalarstyle", ScalarStyle.class);

/**
* The YAML node style to use for collection nodes. A {@code null} value
* will instruct the emitter to fall back to the
* {@link Builder#nodeStyle()} setting.
*
* @since 4.2.0
*/
public static final RepresentationHint<NodeStyle> NODE_STYLE = RepresentationHint.of("configurate:yaml/nodestyle", NodeStyle.class);

/**
* Creates a new {@link YamlConfigurationLoader} builder.
*
Expand All @@ -72,13 +90,19 @@ public static Builder builder() {
* <dl>
* <dt>&lt;prefix&gt;.yaml.node-style</dt>
* <dd>Equivalent to {@link #nodeStyle(NodeStyle)}</dd>
* <dt>&lt;prefix&gt;.yaml.comments-enabled</dt>
* <dd>Equivalent to {@link #commentsEnabled(boolean)}</dd>
* <dt>&lt;prefix&gt;.yaml.line-length</dt>
* <dd>Equivalent to {@link #lineLength(int)}</dd>
* </dl>
*
* @since 4.0.0
*/
public static final class Builder extends AbstractConfigurationLoader.Builder<Builder, YamlConfigurationLoader> {
private final DumperOptions options = new DumperOptions();
private @Nullable NodeStyle style;
private boolean enableComments;
private int lineLength;

Builder() {
this.indent(4);
Expand All @@ -92,6 +116,8 @@ protected void populate(final LoaderOptionSource options) {
if (declared != null) {
this.style = declared;
}
this.enableComments = options.getBoolean(true, "yaml", "comments-enabled");
this.lineLength = options.getInt(150, "yaml", "line-length");
}

/**
Expand Down Expand Up @@ -158,34 +184,97 @@ public Builder nodeStyle(final @Nullable NodeStyle style) {
return this.style;
}

/**
* Set whether comment handling is enabled on this loader.
*
* <p>When comment handling is enabled, comments will be read from files
* and written back to files where possible.</p>
*
* <p>The default value is {@code true}</p>
*
* @param enableComments whether comment handling should be enabled
* @return this builder (for chaining)
* @since 4.2.0
*/
public Builder commentsEnabled(final boolean enableComments) {
this.enableComments = enableComments;
return this;
}

/**
* Get whether comment handling is enabled.
*
* @return whether comment handling is enabled
* @see #commentsEnabled(boolean) for details on comment handling
* @since 4.2.0
*/
public boolean commentsEnabled() {
return this.enableComments;
}

/**
* Set the maximum length of a configuration line.
*
* <p>The default value is {@code 150}</p>
*
* @param lineLength the maximum length of a configuration line
* @return this builder (for chaining)
* @since 4.2.0
*/
public Builder lineLength(final int lineLength) {
this.lineLength = lineLength;
return this;
}

/**
* Get the maximum length of a configuration line.
*
* @return the maximum length of a configuration line
* @see #lineLength(int) for details on the line length
* @since 4.2.0
*/
public int lineLength() {
return this.lineLength;
}

@Override
public YamlConfigurationLoader build() {
return new YamlConfigurationLoader(this);
}
}

private final ThreadLocal<YamlConstructor> constructor;
private final ThreadLocal<Yaml> yaml;

private YamlConfigurationLoader(final Builder builder) {
super(builder, new CommentHandler[] {CommentHandlers.HASH});
final LoaderOptions loaderOpts = new LoaderOptions()
.setAcceptTabs(true)
.setProcessComments(false);
.setProcessComments(builder.commentsEnabled());
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));
opts.setProcessComments(builder.commentsEnabled());
opts.setWidth(builder.lineLength());
opts.setIndicatorIndent(builder.indent());
opts.setIndentWithIndicator(true);
// the constructor needs ConfigurationOptions, which is only available when called (loadInternal)
this.constructor = ThreadLocal.withInitial(() -> new YamlConstructor(loaderOpts));
this.yaml = ThreadLocal.withInitial(() -> new Yaml(this.constructor.get(), new YamlRepresenter(true, opts), opts, loaderOpts));
}

@Override
protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) {
node.raw(this.yaml.get().load(reader));
// the constructor needs ConfigurationOptions for the to be created nodes
// and since it's a thread-local, this won't cause any issues
this.constructor.get().options = node.options();
node.from(this.yaml.get().load(reader));
Comment on lines 268 to +272
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a regression here: load returns null when the file is empty, which ConfigurationNode#from can't receive.

}

@Override
protected void saveInternal(final ConfigurationNode node, final Writer writer) {
this.yaml.get().dump(node.raw(), writer);
this.yaml.get().dump(node, writer);
}

@Override
Expand Down
Loading
Loading