diff --git a/build.gradle b/build.gradle index aeffa5403..efcd6cee1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.gradle.ext.ActionDelegationConfig + import static org.eclipse.jgit.lib.Repository.shortenRefName plugins { @@ -6,6 +8,7 @@ plugins { alias(libs.plugins.detekt) apply false alias(libs.plugins.aggregateJavadoc) alias(libs.plugins.gitPublish) + alias(libs.plugins.ideaExt) alias(libs.plugins.indra.git) alias(libs.plugins.indra.sonatype) alias(libs.plugins.nexusPublish) @@ -13,6 +16,13 @@ plugins { id 'org.spongepowered.configurate.build.base' } +idea.project.settings { + delegateActions { + delegateBuildRunToGradle = false + testRunner = ActionDelegationConfig.TestRunner.PLATFORM + } +} + tasks.named('aggregateJavadoc').configure { def gradleJdk = JavaVersion.current() // at least java 11, but not 12 (java 12 is broken for some reason :( ) diff --git a/core/src/main/java/org/spongepowered/configurate/util/UnmodifiableCollections.java b/core/src/main/java/org/spongepowered/configurate/util/UnmodifiableCollections.java index 6e5b454ad..508de4d2b 100644 --- a/core/src/main/java/org/spongepowered/configurate/util/UnmodifiableCollections.java +++ b/core/src/main/java/org/spongepowered/configurate/util/UnmodifiableCollections.java @@ -48,7 +48,7 @@ private UnmodifiableCollections() {} * * @since 4.0.0 */ - public static List copyOf(final List original) { + public static List copyOf(final List original) { switch (original.size()) { case 0: return Collections.emptyList(); @@ -67,7 +67,7 @@ public static List copyOf(final List original) { * @return a unmodifiable copy of the given {@link Set} instance * @since 4.0.0 */ - public static Set copyOf(final Set original) { + public static Set copyOf(final Set original) { switch (original.size()) { case 0: return Collections.emptySet(); @@ -87,12 +87,12 @@ public static Set copyOf(final Set original) { * @return an unmodifiable copy of the given {@link Map} instance. * @since 4.1.0 */ - public static Map copyOf(final Map original) { + public static Map copyOf(final Map original) { switch (original.size()) { case 0: return Collections.emptyMap(); case 1: - final Map.Entry entry = original.entrySet().iterator().next(); + final Map.Entry entry = original.entrySet().iterator().next(); return Collections.singletonMap(entry.getKey(), entry.getValue()); default: if (original instanceof LinkedHashMap) { diff --git a/core/src/main/java10/org/spongepowered/configurate/util/UnmodifiableCollections.java b/core/src/main/java10/org/spongepowered/configurate/util/UnmodifiableCollections.java index 4b24e34dc..3a86691f4 100644 --- a/core/src/main/java10/org/spongepowered/configurate/util/UnmodifiableCollections.java +++ b/core/src/main/java10/org/spongepowered/configurate/util/UnmodifiableCollections.java @@ -42,7 +42,7 @@ private UnmodifiableCollections() {} * @return a unmodifiable copy of the given {@link List} instance * @since 4.0.0 */ - public static List copyOf(final List original) { + public static List copyOf(final List original) { return List.copyOf(original); } @@ -54,7 +54,7 @@ public static List copyOf(final List original) { * @return a unmodifiable copy of the given {@link Set} instance * @since 4.0.0 */ - public static Set copyOf(final Set original) { + public static Set copyOf(final Set original) { return Set.copyOf(original); } diff --git a/extra/groovy/build.gradle b/extra/groovy/build.gradle new file mode 100644 index 000000000..0bf9cc876 --- /dev/null +++ b/extra/groovy/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'org.spongepowered.configurate.build.component' + id 'groovy' +} + +tasks.processResources { + inputs.property("version", project.version) + expand version: project.version +} + +dependencies { + api projects.core + + [ + libs.groovy, + libs.groovy.nio + ].each { + implementation variantOf(it) { classifier('indy') } + } + + testImplementation variantOf(libs.groovy.test) { classifier('indy') } +} diff --git a/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/ConfigurationNodeExtensions.groovy b/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/ConfigurationNodeExtensions.groovy new file mode 100644 index 000000000..2d50570ad --- /dev/null +++ b/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/ConfigurationNodeExtensions.groovy @@ -0,0 +1,38 @@ +/* + * 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.extra.groovy + +import org.spongepowered.configurate.ScopedConfigurationNode; + +class ConfigurationNodeExtensions { + + static > N getAt(final ScopedConfigurationNode self, Iterable path) { + return self.node(path) + } + + static > N getAt(final ScopedConfigurationNode self, Object... path) { + return self.node(path) + } + + static > boolean isCase(final ScopedConfigurationNode self, Iterable path) { + return self.hasChild(path) + } + + static > boolean isCase(final ScopedConfigurationNode self, Object... path) { + return self.hasChild(path) + } +} diff --git a/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/GStringTypeSerializer.groovy b/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/GStringTypeSerializer.groovy new file mode 100644 index 000000000..e771f9049 --- /dev/null +++ b/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/GStringTypeSerializer.groovy @@ -0,0 +1,38 @@ +/* + * 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.extra.groovy + +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer + +import java.lang.reflect.Type; + +class GStringTypeSerializer implements TypeSerializer { + + @Override + GString deserialize(final Type type, final ConfigurationNode node) throws SerializationException { + return GString. + return null; + } + + @Override + public void serialize( + final Type type, final GString obj, final ConfigurationNode node) throws SerializationException { + + } +} diff --git a/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/POGOFieldDiscoverer.groovy b/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/POGOFieldDiscoverer.groovy new file mode 100644 index 000000000..addbcad1c --- /dev/null +++ b/extra/groovy/src/main/groovy/org/spongepowered/configurate/extra/groovy/POGOFieldDiscoverer.groovy @@ -0,0 +1,34 @@ +/* + * 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.extra.groovy + +import io.leangen.geantyref.GenericTypeReflector +import org.spongepowered.configurate.objectmapping.FieldDiscoverer +import org.spongepowered.configurate.serialize.SerializationException + +import java.lang.reflect.AnnotatedType +import java.lang.reflect.Field + +class POGOFieldDiscoverer implements FieldDiscoverer> { + + @Override + def InstanceFactory> discover(final AnnotatedType target, final FieldCollector, V> collector) + throws SerializationException { + def clazz = GenericTypeReflector.erase(target.type) + clazz.metaClass.properties + } +} diff --git a/extra/groovy/src/main/resources/META-INF/groovy/org.codehause.groovy.runtime.ExtensionModule b/extra/groovy/src/main/resources/META-INF/groovy/org.codehause.groovy.runtime.ExtensionModule new file mode 100644 index 000000000..33ac856fa --- /dev/null +++ b/extra/groovy/src/main/resources/META-INF/groovy/org.codehause.groovy.runtime.ExtensionModule @@ -0,0 +1,4 @@ +moduleName=Extensions to configurate for compatibility with the Groovy environment +moduleVersion=${version} +extensionClasses=org.spongepowered.configurate.extra.groovy.ConfigurationNodeExtensions +staticExtensionClasses= \ No newline at end of file diff --git a/extra/groovy/src/test/groovy/org/spongepowered/configurate/extra/groovy/PogoFieldDiscovererTest.groovy b/extra/groovy/src/test/groovy/org/spongepowered/configurate/extra/groovy/PogoFieldDiscovererTest.groovy new file mode 100644 index 000000000..17fe85a30 --- /dev/null +++ b/extra/groovy/src/test/groovy/org/spongepowered/configurate/extra/groovy/PogoFieldDiscovererTest.groovy @@ -0,0 +1,27 @@ +/* + * 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.extra.groovy + +import org.junit.jupiter.api.Test + +class PogoFieldDiscovererTest { + + @Test + void testPogoFieldDiscoverer() { + + } +} diff --git a/format/yaml/build.gradle b/format/yaml/build.gradle index 1820023d5..703140079 100644 --- a/format/yaml/build.gradle +++ b/format/yaml/build.gradle @@ -1,6 +1,7 @@ plugins { id 'org.spongepowered.configurate.build.component' alias(libs.plugins.shadow) + id 'groovy' // for tests } description = "YAML format loader for Configurate" @@ -12,9 +13,19 @@ configurations { testImplementation { extendsFrom shaded } } +configurate.useAutoValue() dependencies { api projects.core shaded "configurate.thirdparty:snakeyaml:version-from-submodule" + + [ + libs.groovy, + libs.groovy.nio, + libs.groovy.test.junit5, + libs.groovy.templates + ].each { + testImplementation it + } } tasks { diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/MergeTag.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/MergeTag.java new file mode 100644 index 000000000..62c8f6a28 --- /dev/null +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/MergeTag.java @@ -0,0 +1,22 @@ +/* + * 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; + +// Marker for merge tag values +enum MergeTag { + INSTANCE +} diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/NodeStyle.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/NodeStyle.java index a269d280a..8fc9277dc 100644 --- a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/NodeStyle.java +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/NodeStyle.java @@ -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); + } + } + } diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/ScalarStyle.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/ScalarStyle.java new file mode 100644 index 000000000..4c462e2dc --- /dev/null +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/ScalarStyle.java @@ -0,0 +1,121 @@ +/* + * 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 org.yaml.snakeyaml.events.ImplicitTuple; + +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. + * + *
"hello world"
+ * + * @since 4.2.0 + */ + DOUBLE_QUOTED(DumperOptions.ScalarStyle.DOUBLE_QUOTED), + + /** + * A single-quoted string. + * + *
'hello world'
+ * + * @since 4.2.0 + */ + SINGLE_QUOTED(DumperOptions.ScalarStyle.SINGLE_QUOTED), + + /** + * String without any quotation. + * + *

This may be ambiguous with non-string types.

+ * + * @since 4.2.0 + */ + UNQUOTED(DumperOptions.ScalarStyle.PLAIN), + + /** + * Folded scalar. + * + *
{@code
+     * key: >
+     *   folded scalar
+     *   line breaks collapsed
+     * }
+ * + * @since 4.2.0 + */ + FOLDED(DumperOptions.ScalarStyle.FOLDED), + + /** + * Literal scalar. + * + *
{@code
+     * key: |
+     *   literal scalar
+     *   line breaks preserved
+     * }
+ * + * @since 4.2.0 + */ + LITERAL(DumperOptions.ScalarStyle.LITERAL) + ; + + private static final Map 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 ImplicitTuple implicity, + final @Nullable ScalarStyle fallback + ) { + // todo: allow customizing + if (style == null) { + if (implicity.canOmitTagInNonPlainScalar() && !implicity.canOmitTagInPlainScalar()) { + return fallback != null && fallback != ScalarStyle.UNQUOTED ? fallback.snake : DumperOptions.ScalarStyle.DOUBLE_QUOTED; + } else { + return fallback != null ? fallback.snake : DumperOptions.ScalarStyle.PLAIN; + } + } else { + return style.snake; + } + } + + 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); + } + } + +} diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/Tag.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/Tag.java new file mode 100644 index 000000000..8dd8ffdd4 --- /dev/null +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/Tag.java @@ -0,0 +1,141 @@ +/* + * 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.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.loader.ParsingException; +import org.spongepowered.configurate.util.UnmodifiableCollections; + +import java.net.URI; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +class Tag { + + private final URI tagUri; + private final Set> supportedTypes; + + Tag(final URI tagUri, final Set> supportedTypes) { + this.tagUri = tagUri; + this.supportedTypes = UnmodifiableCollections.copyOf(supportedTypes); + } + + public final URI tagUri() { + return this.tagUri; + } + + public final Set> supportedTypes() { + return this.supportedTypes; + } + + abstract static class Scalar extends Tag { + + private final @Nullable Pattern pattern; + private final @Nullable ScalarStyle preferredScalarStyle; + + // for unregistered tags on scalars + static Scalar ofUnknown(final URI tagUri) { + return new Scalar(tagUri, Collections.emptySet(), null, null) { + @Override + public String fromString(final String input) { + return input; + } + + @Override + public String toString(final String own) { + return own; + } + }; + } + + Scalar(final URI tagUri, final Set> supportedTypes, final @Nullable Pattern pattern) { + this(tagUri, supportedTypes, pattern, null); + } + + Scalar( + final URI tagUri, + final Set> supportedTypes, + final @Nullable Pattern pattern, + final @Nullable ScalarStyle preferredScalarStyle + ) { + super(tagUri, supportedTypes); + this.pattern = pattern; + this.preferredScalarStyle = preferredScalarStyle; + } + + /** + * Pattern to use to detect this tag. + * + *

May be {@code null} if this tag cannot be used as an + * implicit tag.

+ * + * @return the detection pattern + * @since 4.2.0 + */ + public final @Nullable Pattern pattern() { + return this.pattern; + } + + /** + * Get the preferred scalar style to use for this type, when none is + * specifically used. + * + * @return the preferred scalar style + * @since 4.2.0 + */ + public final @Nullable ScalarStyle preferredScalarStyle() { + return this.preferredScalarStyle; + } + + public abstract V fromString(String input) throws ParsingException; + + public abstract String toString(V own) throws ConfigurateException; + + } + + static class Mapping extends Tag { + + Mapping(final URI tagUri, final Set> supportedTypes) { + super(tagUri, supportedTypes); + } + + } + + static class Sequence extends Tag { + + Sequence(final URI tagUri, final Set> supportedTypes) { + super(tagUri, supportedTypes); + } + + } + + @Override + public boolean equals(final @Nullable Object that) { + // todo: ensure type of tag is equal + return that instanceof Tag + && ((Tag) that).tagUri().equals(this.tagUri); + } + + @Override + public int hashCode() { + return Objects.hash(this.tagUri); + } + +} diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/TagRepository.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/TagRepository.java new file mode 100644 index 000000000..395d74677 --- /dev/null +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/TagRepository.java @@ -0,0 +1,345 @@ +/* + * 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 static java.util.Objects.requireNonNull; + +import com.google.auto.value.AutoValue; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.util.UnmodifiableCollections; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A collection of tags that are understood when reading a document. + * + * @since 4.2.0 + */ +final class TagRepository { + + // fallback tag for each node type + final Tag unresolvedTag; + final Tag.Scalar stringTag; + final Tag.Sequence sequenceTag; + final Tag.Mapping mappingTag; + final List tags; + final Map, Tag> byErasedType; + final Map byName; + + TagRepository(final Builder builder) { + this.unresolvedTag = builder.unresolvedTag; + this.stringTag = builder.stringTag; + this.sequenceTag = builder.sequenceTag; + this.mappingTag = builder.mappingTag; + final List allTags = new ArrayList<>(builder.otherTags); + allTags.add(this.stringTag); + allTags.add(this.sequenceTag); + allTags.add(this.mappingTag); + allTags.add(this.unresolvedTag); + this.tags = UnmodifiableCollections.copyOf(allTags); + this.byErasedType = UnmodifiableCollections.copyOf(builder.byErasedType); + this.byName = UnmodifiableCollections.copyOf(builder.byName); + } + + static TagRepository.Builder builder() { + return new Builder(); + } + + /** + * Determine the implicit tag for a scalar value. + * + * @param scalar scalar to test + * @return the first matching tag + * @since 4.2.0 + */ + public Tag.@Nullable Scalar forInput(final String scalar) { + for (final Tag tag : this.tags) { + if (tag instanceof Tag.Scalar) { + final @Nullable Pattern pattern = ((Tag.Scalar) tag).pattern(); + if (pattern != null && pattern.matcher(scalar).matches()) { + return (Tag.Scalar) tag; + } + } + } + + return null; + } + + /** + * Resolve a tag by its URI. + * + * @param name the tag URI + * @return a tag, if any is present + * @since 4.2.0 + */ + public @Nullable Tag named(final URI name) { + return this.byName.get(name); + } + + /** + * Resolve a tag by the Java type it represents. + * + * @param type the type used + * @return a tag, if any is registered + * @since 4.2.0 + */ + public @Nullable Tag byType(final Class type) { + return this.byErasedType.get(type); + } + + /** + * Analyze a node to determine what tag its value should have. + * + * @param node the node to analyze + * @return a calculated tag + * @since 4.2.0 + */ + @SuppressWarnings("rawtypes") + AnalyzedTag analyze(final ConfigurationNode node) throws ConfigurateException { + final @Nullable Tag explicit = node.ownHint(YamlConfigurationLoader.TAG); + final @Nullable Tag calculated; + boolean isUnambiguous; + if (node.isMap()) { + calculated = this.mappingTag; + isUnambiguous = true; + } else if (node.isList()) { + calculated = this.sequenceTag; + isUnambiguous = true; + } else if (node.isNull()) { + calculated = this.byType(void.class); + isUnambiguous = true; + } else { + final @Nullable Object rawScalar = node.rawScalar(); + calculated = this.byType(rawScalar.getClass()); + isUnambiguous = true; + if (calculated != null && calculated instanceof Tag.Scalar) { + final String serialized = ((Tag.Scalar) calculated).toString(rawScalar); + for (final Tag tag : this.tags) { + if (tag != calculated && tag instanceof Tag.Scalar && ((Tag.Scalar) tag).pattern() != null) { + if (!tag.equals(this.stringTag) && ((Tag.Scalar) tag).pattern().matcher(serialized).matches()) { + isUnambiguous = false; + break; + } + } + } + } + + } + return AnalyzedTag.of(calculated == null ? this.unresolvedTag : calculated, explicit, isUnambiguous); + } + + public Tag.Scalar stringTag() { + return this.stringTag; + } + + public Tag.Sequence sequenceTag() { + return this.sequenceTag; + } + + public Tag.Mapping mappingTag() { + return this.mappingTag; + } + + public TagRepository.Builder toBuilder() { + return new Builder(this); + } + + /** + * A combination of resolved tag, and whether the tag is the same as the tag + * that would be implicitly calculated. + * + * @since 4.2.0 + */ + @AutoValue + abstract static class AnalyzedTag { + + /** + * Create a new resolved tag. + * + * @param resolved the resolved type + * @param specified the specified type + * @return the resolved tag + * @since 4.2.0 + */ + static AnalyzedTag of(final Tag resolved, final @Nullable Tag specified, final boolean defaultForType) { + return new AutoValue_TagRepository_AnalyzedTag(resolved, specified, defaultForType); + } + + AnalyzedTag() { + } + + /** + * Get the calculated tag, if any is present. + * + *

If no tag could be resolved, this will always return the parser's + * unresolved tag.

+ * + * @return the calculated tag + * @since 4.2.0 + */ + public abstract Tag resolved(); + + /** + * Get the manually specified tag for this node. + * + * @return the specified tag + * @since 4.2.0 + */ + public abstract @Nullable Tag specified(); + + /** + * Get whether this node's serialized scalar value unambiguously matched + * a certain tag. + * + * @return whether the calculated tag unambiguously matches + * @since 4.2.0 + */ + abstract boolean isUnambiguous(); + + /** + * Get the actual tag applicable to the analyzed node. + * + *

If a tag is explicitly specified, that tag will be returned. + * Otherwise, the specified tag will be used.

+ * + * @return the actual tag + */ + public final Tag actual() { + return this.specified() == null ? this.resolved() : this.specified(); + } + + /** + * Get whether the provided tag is an implicit tag or not. + * + *

A tag is implicit when no type has been specified, or the resolved + * type equals the specified type.

+ * + * @return whether the tag is implicit. + * @since 4.2.0 + */ + public final boolean implicit() { + return this.specified() == null ? this.isUnambiguous() : Objects.equals(this.resolved(), this.specified()); + } + + } + + static final class Builder { + private @MonotonicNonNull Tag unresolvedTag; + private Tag.@MonotonicNonNull Scalar stringTag; + private Tag.@MonotonicNonNull Sequence sequenceTag; + private Tag.@MonotonicNonNull Mapping mappingTag; + private final List otherTags = new ArrayList<>(); + private final Map, Tag> byErasedType = new HashMap<>(); + private final Map byName = new HashMap<>(); + + Builder() { + } + + Builder(final TagRepository existing) { + this.unresolvedTag = existing.unresolvedTag; + this.stringTag = existing.stringTag; + this.sequenceTag = existing.sequenceTag; + this.mappingTag = existing.mappingTag; + this.otherTags.addAll(existing.tags); + this.otherTags.remove(this.stringTag); + this.otherTags.remove(this.sequenceTag); + this.otherTags.remove(this.mappingTag); + this.otherTags.remove(this.unresolvedTag); + this.byErasedType.putAll(existing.byErasedType); + this.byName.putAll(existing.byName); + } + + Builder unresolvedTag(final Tag unresolvedTag) { + // if (this.unresolvedTag != null) + this.addTag0(this.unresolvedTag = requireNonNull(unresolvedTag, "unresolved")); + return this; + } + + Builder stringTag(final Tag.Scalar string) { + this.addTag0(this.stringTag = requireNonNull(string, "string")); + return this; + } + + Builder sequenceTag(final Tag.Sequence sequence) { + this.addTag0(this.sequenceTag = requireNonNull(sequence, "sequence")); + return this; + } + + Builder mappingTag(final Tag.Mapping mapping) { + this.addTag0(this.mappingTag = requireNonNull(mapping, "mapping")); + return this; + } + + /** + * Add a tag to this repository. + * + *

This must not receive any tag that is already the string, + * mapping, or sequence tags. If trying to register a tag that shares a + * URL or supported types with an already-registered tag, this operation + * will fail, unless that same tag instance is the one registered.

+ * + * @param tag the tag to register + * @return this builder + * @since 4.2.0 + */ + Builder addTag(final Tag tag) { + requireNonNull(tag, "tag"); + if (tag.equals(this.mappingTag) || tag.equals(this.sequenceTag) || tag.equals(this.stringTag) || tag.equals(this.unresolvedTag)) { + throw new IllegalArgumentException("Tag " + tag + + " was already registered as one of the mapping, sequence, string, or unresolved tags!"); + } + this.otherTags.add(tag); + return this.addTag0(tag); + } + + private Builder addTag0(final Tag tag) { + for (final Class clazz : tag.supportedTypes()) { + this.byErasedType.put(clazz, tag); + } + this.byName.put(tag.tagUri(), tag); + return this; + } + + TagRepository build() { + if (this.unresolvedTag == null) { + throw new IllegalArgumentException("Unresolved tag not set"); + } + if (this.stringTag == null) { + throw new IllegalArgumentException("String tag not set"); + } + if (this.mappingTag == null) { + throw new IllegalArgumentException("Mapping tag not set"); + } + if (this.sequenceTag == null) { + throw new IllegalArgumentException("Sequence tag not set"); + } + + return new TagRepository(this); + } + + } + +} diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/Yaml11Tags.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/Yaml11Tags.java new file mode 100644 index 000000000..5a319df9d --- /dev/null +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/Yaml11Tags.java @@ -0,0 +1,314 @@ +/* + * 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.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.loader.ParsingException; +import org.spongepowered.configurate.util.UnmodifiableCollections; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Standard types defined on the yaml.org + * tag repository. + * + * @since 4.2.0 + */ +final class Yaml11Tags { + + private Yaml11Tags() { + } + + private static URI yamlOrg(final String specific) { + return URI.create("tag:yaml.org,2002:" + specific); + } + + /** + * A binary data tag. + * + * @see tag:yaml.org,2002:binary + * @since 4.2.0 + */ + public static final Tag.Scalar BINARY = new Tag.Scalar( + yamlOrg("binary"), + UnmodifiableCollections.toSet(byte[].class), + null // base64 is not distinguishable from a normal string, require the tag to be provided explicitly + ) { + + @Override + public byte[] fromString(final String input) { + return Base64.getDecoder().decode(input); + } + + @Override + public String toString(final byte[] own) { + return Base64.getEncoder().encodeToString(own); + } + }; + + /** + * A boolean value. + * + * @implNote Canonically, these are y|n in YAML 1.1, but because YAML 1.2 + * will only support true|false, we will treat those as the default + * output format. We also modify the regex to only automatically match + * true/false and on/off in order to avoid cases of confusion (such as + * the classic Norway problem). + * @see tag:yaml.org,2002:bool + * @since 4.2.0 + */ + public static final Tag.Scalar BOOL = new Tag.Scalar( + yamlOrg("bool"), + UnmodifiableCollections.toSet(Boolean.class), + Pattern.compile("true|True|TRUE|false|False|FALSE" + + "|on|On|ON|off|Off|OFF") + ) { + private final Set trues = UnmodifiableCollections.toSet( + "y", "Y", "yes", "Yes", "YES", + "true", "True", "TRUE", + "on", "On", "ON" + ); + + @Override + public Boolean fromString(final String input) { + return this.trues.contains(input); + } + + @Override + public String toString(final Boolean own) { + // YAML 1.2 is a lot more strict. Only emit the standard boolean values for forwards compatibility + return own ? "true" : "false"; + } + }; + + /** + * A floating-point number. + * + * @see tag:yaml.org,2002:float + * @since 4.2.0 + */ + public static final Tag.Scalar FLOAT = new Tag.Scalar( + yamlOrg("float"), + UnmodifiableCollections.toSet(Float.class, Double.class, BigDecimal.class), + Pattern.compile("[-+]?([0-9][0-9_]*)?\\.[0-9.]*([eE][-+][0-9]+)?" // base 10 + + "|[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\\.[0-9]*" // base 60 + + "|[-+]?\\.(inf|Inf|INF)" // infinity + + "|\\.(nan|NaN|NAN)") // not a number + ) { + + @Override + public Number fromString(final String input) { + return Double.parseDouble(input); + } + + @Override + public String toString(final Number own) { + return own.toString(); + } + }; + + /** + * An integer. + * + * @see tag:yaml.org,2002:int + * @since 4.2.0 + */ + public static final Tag.Scalar INT = new Tag.Scalar( + yamlOrg("int"), + UnmodifiableCollections.toSet(Byte.class, Short.class, Integer.class, Long.class, BigInteger.class), + Pattern.compile("[-+]?0b[0-1_]+" // base 2 + + "|[-+]?0[0-7_]+" // base 8 + + "|[-+]?(0|[1-9][0-9_]*)" // base 10 + + "|[-+]?0x[0-9a-fA-F_]+" // base 16 + + "|[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+") // base 60 + ) { + + // todo: wrong + @Override + public Number fromString(final String input) { + // handle leading +/- + // if literal '0': return int + // handle 0/0x/0b prefixes + try { + final long ret = Long.parseLong(input); + if (ret >= Integer.MIN_VALUE && ret <= Integer.MAX_VALUE) { + return (int) ret; + } else { + return ret; + } + } catch (final NumberFormatException ex) { + return new BigInteger(input); + } + } + + @Override + public String toString(final Number own) { + // emit only number formats represented in yaml 1.2 core schema: base 10 or 16 + // todo: have a 'compatibility mode' that can be disabled to produce output that is valid yaml 1.1 but not valid 1.2? + return own.toString(); + } + }; + + /** + * A mapping merge. + * + *

This will not be supported in Configurate until reference-type nodes + * are fully implemented.

+ * + * @see tag:yaml.org,2002:merge + * @since 4.2.0 + */ + public static final Tag.Scalar MERGE = new Tag.Scalar( + yamlOrg("merge"), + UnmodifiableCollections.toSet(ConfigurationNode.class), + Pattern.compile("<<") + ) { + + // TODO: this can only really be implemented with full reference support + // used as map key, where the next node will be a reference that should be merged in to this node + + @Override + public Object fromString(final String input) { + return MergeTag.INSTANCE; + } + + @Override + public String toString(final Object own) throws ParsingException { + throw new ParsingException(ParsingException.UNKNOWN_POS, ParsingException.UNKNOWN_POS, null, "Merge keys cannot be serialized", null); + } + }; + + /** + * The value {@code null}. + * + *

Because Configurate has no distinction between a node with a + * {@code null} value, and a node that does not exist, this tag will most + * likely never be encountered in an in-memory representation.

+ * + * @see tag:yaml.org,2002:null + * @since 4.2.0 + */ + public static final Tag.Scalar NULL = new Tag.Scalar( + yamlOrg("null"), + UnmodifiableCollections.toSet(Void.class, void.class), + Pattern.compile("~" + + "|null|Null|NULL" + + "|$") + ) { + + @Override + public Void fromString(final String input) { + return null; + } + + @Override + public String toString(final Void own) { + return "null"; + } + }; + + /** + * Any string. + * + * @see tag:yaml.org,2002:str + * @since 4.2.0 + */ + public static final Tag.Scalar STR = new Tag.Scalar( + yamlOrg("str"), + UnmodifiableCollections.toSet(String.class), + Pattern.compile(".+") // empty scalar is NULL + ) { + @Override + public String fromString(final String input) { + return input; + } + + @Override + public String toString(final String own) { + return own; + } + }; + + /** + * A timestamp, containing date, time, and timezone. + * + * @see tag:yaml.org,2002:timestamp + * @since 4.2.0 + */ + public static final Tag.Scalar TIMESTAMP = new Tag.Scalar( + yamlOrg("timestamp"), + UnmodifiableCollections.toSet(ZonedDateTime.class), + Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}" // YYYY-MM-DD + + "|[0-9]{4}" // YYYY + + "-[0-9]{1,2}" // month + + "-[0-9]{1,2}" // day + + "([Tt]|[ \t]+)[0-9]{1,2}" // hour + + ":[0-9]{1,2}" // minute + + ":[0-9]{2}" // second + + "(\\.[0-9]*)?" // fraction + + "(([ \t]*)Z|[-+][0-9]{1,2}(:[0-9]{2})?)?") // time zone + ) { + @Override + public ZonedDateTime fromString(final String input) { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public String toString(final ZonedDateTime own) { + throw new UnsupportedOperationException("not yet implemented"); + } + }; + + /** + * A mapping. + * + * @see tag:yaml.org,2002:map + * @since 4.2.0 + */ + public static final Tag.Mapping MAP = new Tag.Mapping(yamlOrg("map"), UnmodifiableCollections.toSet(Map.class)); + + /** + * A sequence. + * + * @see tag:yaml.org,2002:seq + * @since 4.2.0 + */ + public static final Tag.Sequence SEQ = new Tag.Sequence(yamlOrg("seq"), UnmodifiableCollections.toSet(List.class, Set.class)); + + static final TagRepository REPOSITORY = TagRepository.builder() + .unresolvedTag(new Tag(URI.create("?"), UnmodifiableCollections.toSet(Object.class)) {}) + .stringTag(STR) + .mappingTag(MAP) + .sequenceTag(SEQ) + .addTag(BINARY) + .addTag(BOOL) + .addTag(INT) + .addTag(FLOAT) + .addTag(NULL) + .addTag(MERGE) + .addTag(TIMESTAMP) + .build(); + +} 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..5c671e614 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 @@ -18,18 +18,20 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurateException; 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; import org.spongepowered.configurate.loader.LoaderOptionSource; +import org.spongepowered.configurate.loader.ParsingException; 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.constructor.Constructor; -import org.yaml.snakeyaml.representer.Representer; +import org.yaml.snakeyaml.events.DocumentStartEvent; +import org.yaml.snakeyaml.reader.StreamReader; import java.io.BufferedReader; import java.io.Writer; @@ -39,13 +41,128 @@ import java.util.Set; /** - * A loader for YAML-formatted configurations, using the SnakeYAML library for - * parsing and generation. + * A loader for YAML 1.1 configurations. * + *

The YAML Format

+ * + *

YAML is an extremely flexible format for data serialization, designed to + * be easy for humans to work with.

+ * + *
{@code
+ * document:
+ *   hello: world!
+ *   this: [is, a list]
+ * name: abcd
+ * dependencies:
+ * - {org: org.spongepowered, name: configurate-yaml, version: 4.2.0}
+ * - {org: org.spongepowered, name: noise, version: 2.0.0}
+ * }
+ * + *

Usage

+ * + *

CAUTION:Comment support (added in 4.2.0) is currently + * classified as experimental. This means it will not be enabled by + * default, and must be enabled specifically on the builder, or by using the + * system property {@code configurate.yaml.commentsEnabled} to enable comments + * for loaders that do not make the choice themselves. In future versions once + * comment handling has stabilized, this will switch to become an opt-out for + * comment handling.

+ * + *

This loader can be configured like any other, by adjusting properties + * on the {@link Builder}. While almost every property is optional, an + * understanding of several is crucial:

+ * + *
    + *
  • Node Style: YAML has three modes for styling maps + * and sequences: block, flow, and auto. Flow is a more json-like style, + * while block is the whitespace-based style that is more specifically + * associated with the YAML format. The default for Configurate is + * auto, represented as a {@code null} {@link NodeStyle}, but users + * may wish to change to {@link NodeStyle#BLOCK}.
  • + *
  • Accepted types: The only accepted types handled by + * the YAML loader are those registered with a {@link TagRepository}. This + * will override any types set in a {@link ConfigurationOptions}.
  • + *
+ * + *

Custom Tags

+ * + *

Limitations

+ * + *

This loader bridges the YAML object model and representation lifecycle + * with Configurate's own model. Because these models are rather different, + * there are a few areas where the interactions produce less than + * ideal results.

+ * + *
    + *
  • Custom tags: primarily for scalars, use object mapper for the others + * (the object mapper *can* read/write the explicit tag for nodes + * where useful).
  • + *
  • Alias nodes and merge keys: flattened on load, not yet supported by + * the Configurate object model
  • + *
  • Keys: limited, tag and representation information is lost when using + * complex keys (since keys are not preserved as a node)
  • + *
+ * + * @see YAML 1.1 Spec + * @see YAML 1.2 Spec + * @see YAML Spec RFCs * @since 4.0.0 */ public final class YamlConfigurationLoader extends AbstractConfigurationLoader { + /** + * The identifier for a YAML anchor that can be used to refer to the node + * this hint is set on. + * + * @since 4.2.0 + */ + public static final RepresentationHint ANCHOR_ID = RepresentationHint.builder() + .identifier("configurate:yaml/anchor-id") + .valueType(String.class) + .inheritable(false) + .build(); + + /** + * The YAML scalar style this node should attempt to use. + * + *

If the chosen scalar style would produce syntactically invalid YAML, a + * valid one will replace it.

+ * + * @since 4.2.0 + */ + public static final RepresentationHint SCALAR_STYLE = RepresentationHint.of("configurate:yaml/scalar-style", 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 NODE_STYLE = RepresentationHint.of("configurate:yaml/node-style", NodeStyle.class); + + /** + * The explicitly specified tag for a node. + * + *

This can override default type conversion for a YAML document.

+ * + * @since 4.2.0 + */ + public static final RepresentationHint TAG = RepresentationHint.builder() + .identifier("configurate:yaml/tag") + .valueType(Tag.class) + .inheritable(false) + .build(); + + /** + * Whether comments will be enabled by default. + * + *

Comments will be introduced as an experimental feature, defaulting to + * {@code false} at first, but changed to {@code true} in a + * later release.

+ */ + private static final boolean COMMENTS_DEFAULT = Boolean.parseBoolean(System.getProperty("configurate.yaml.commentsEnabled", "false")); + /** * YAML native types from YAML 1.1 Global tags. * @@ -55,6 +172,8 @@ public final class YamlConfigurationLoader extends AbstractConfigurationLoader { private final DumperOptions options = new DumperOptions(); private @Nullable NodeStyle style; + private boolean enableComments = COMMENTS_DEFAULT; Builder() { this.indent(4); @@ -92,6 +212,8 @@ protected void populate(final LoaderOptionSource options) { if (declared != null) { this.style = declared; } + + this.enableComments = options.getBoolean(false, "yaml", "comments-enabled"); } /** @@ -121,19 +243,19 @@ public int indent() { * *
Flow
*
the compact, json-like representation.
- * Example: + * Example: {@code * {value: [list, of, elements], another: value} - *
+ * } * *
Block
*
expanded, traditional YAML
- * Example: + * Example: {@code * value: * - list * - of * - elements * another: value - *
+ * } *
* *

A {@code null} value will tell the loader to pick a value @@ -158,34 +280,87 @@ public Builder nodeStyle(final @Nullable NodeStyle style) { return this.style; } + /** + * Set whether comment handling is enabled on this loader. + * + *

Comment handling is available as an experimental feature + * in 4.1.0. There may be edge cases where parsing or writing while + * comments are enabled that can cause parse or emit errors, or badly + * formatted output data.

+ * + *

When comment handling is enabled, comments will be read from files + * and written back to files where possible.

+ * + * @param enableComments whether comment handling should be enabled + * @return this builder (for chaining) + * @since 4.1.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.1.0 + */ + public boolean commentsEnabled() { + return this.enableComments; + } + @Override public YamlConfigurationLoader build() { return new YamlConfigurationLoader(this); } } - private final ThreadLocal yaml; + private final LoaderOptions loader; + private final DumperOptions options; + private final YamlVisitor visitor; + private final @Nullable NodeStyle defaultNodeStyle; private YamlConfigurationLoader(final Builder builder) { super(builder, new CommentHandler[] {CommentHandlers.HASH}); - final LoaderOptions loaderOpts = new LoaderOptions() - .setAcceptTabs(true) - .setProcessComments(false); - 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.setDefaultFlowStyle(NodeStyle.asSnakeYaml(builder.nodeStyle())); + opts.setProcessComments(builder.commentsEnabled()); + opts.setIndentWithIndicator(true); + opts.setIndicatorIndent(builder.indent()); + opts.setWidth(DEFAULT_LINE_LENGTH); + this.defaultNodeStyle = builder.nodeStyle(); + this.options = opts; + this.loader = new LoaderOptions() + .setAcceptTabs(true) + .setProcessComments(builder.commentsEnabled()); + this.loader.setCodePointLimit(Integer.MAX_VALUE); + this.visitor = new YamlVisitor(true, Yaml11Tags.REPOSITORY); } @Override - protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) { - node.raw(this.yaml.get().load(reader)); + protected void loadInternal(final CommentedConfigurationNode node, final BufferedReader reader) throws ParsingException { + final YamlParserComposer parser = new YamlParserComposer(new StreamReader(reader), this.loader, Yaml11Tags.REPOSITORY); + parser.singleDocumentStream(node); } @Override - protected void saveInternal(final ConfigurationNode node, final Writer writer) { - this.yaml.get().dump(node.raw(), writer); + protected void saveInternal(final ConfigurationNode node, final Writer writer) throws ConfigurateException { + final YamlVisitor.State state = new YamlVisitor.State(this.options, writer, this.defaultNodeStyle); + // Initialize + state.start = node; + state.emit(YamlVisitor.STREAM_START); + state.emit(new DocumentStartEvent(null, null, this.options.isExplicitStart(), + this.options.getVersion(), this.options.getTags())); + + // Write out the node + node.visit(this.visitor, state); + + // Finish up + state.emit(YamlVisitor.DOCUMENT_END); + state.emit(YamlVisitor.STREAM_END); } @Override diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlParserComposer.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlParserComposer.java new file mode 100644 index 000000000..4c56f1f19 --- /dev/null +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlParserComposer.java @@ -0,0 +1,707 @@ +/* + * 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.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.BasicConfigurationNode; +import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationNodeFactory; +import org.spongepowered.configurate.loader.AbstractConfigurationLoader; +import org.spongepowered.configurate.loader.ParsingException; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.error.MarkedYAMLException; +import org.yaml.snakeyaml.events.AliasEvent; +import org.yaml.snakeyaml.events.CollectionStartEvent; +import org.yaml.snakeyaml.events.CommentEvent; +import org.yaml.snakeyaml.events.DocumentStartEvent; +import org.yaml.snakeyaml.events.Event; +import org.yaml.snakeyaml.events.MappingStartEvent; +import org.yaml.snakeyaml.events.NodeEvent; +import org.yaml.snakeyaml.events.ScalarEvent; +import org.yaml.snakeyaml.events.SequenceStartEvent; +import org.yaml.snakeyaml.parser.ParserImpl; +import org.yaml.snakeyaml.reader.StreamReader; +import org.yaml.snakeyaml.scanner.ScannerImpl; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * One combined object that handles parsing into an event stream and composing + * the node graph. + */ +final class YamlParserComposer extends ParserImpl { + + private static final int INITIAL_STACK_SIZE = 16; + private static final int FRAME_STACK_INCREMENT = 8; + + private @Nullable StringBuilder commentCollector; + private final boolean stripLeadingCommentWhitespace = true; + final Map aliases = new HashMap<>(); + final TagRepository tags; + final Map declaredTags = new HashMap<>(); + + private Frame[] frames = new Frame[INITIAL_STACK_SIZE]; + private int framePointer = -1; + + YamlParserComposer(final StreamReader reader, final LoaderOptions opts, final TagRepository tags) { + super(new ScannerImpl(reader, opts)); + this.tags = tags; + } + + // "api" // + + public void singleDocumentStream(final ConfigurationNode node) throws ParsingException { + this.requireEvent(Event.ID.StreamStart); + this.document(node); + this.requireEvent(Event.ID.StreamEnd); + } + + public void document(final ConfigurationNode node) throws ParsingException { + if (peekEvent().is(Event.ID.StreamEnd)) { + return; + } + + Frame active = this.pushFrame(DocumentStart.INSTANCE); + active.node = node; + try { + // parser loop + while (this.framePointer >= 0) { + active = this.peekFrame(); + if (this.peekEvent() == null) { + throw new IllegalStateException("Still within composer state loop while out of events!\n" + + " Active state is: " + this.peekFrame().state.getClass()); + } + if (active.state.accept(active, this) == null) { + // todo: validate non-null returns here? does it matter? + this.popFrame(); + } + } + } catch (final MarkedYAMLException ex) { + throw new ParsingException( + active.node, + ex.getProblemMark().getLine(), + ex.getProblemMark().getColumn(), + ex.getProblemMark().get_snippet(), + ex.getProblem() + ); + } catch (final ConfigurateException ex) { + ex.initPath(active.node::path); + throw ex; + } finally { + this.aliases.clear(); + this.declaredTags.clear(); + } + } + + ScannerImpl scanner() { + return (ScannerImpl) this.scanner; + } + + // events // + + void requireEvent(final Event.ID type) throws ParsingException { + final Event next = this.peekEvent(); + if (!next.is(type)) { + throw makeError(next.getStartMark(), "Expected next event of type" + type + " but was " + next.getEventId(), null); + } + this.getEvent(); + } + + @SuppressWarnings("unchecked") + T requireEvent(final Event.ID type, final Class clazz) throws ParsingException { + final Event next = this.peekEvent(); + if (!next.is(type)) { + throw makeError(next.getStartMark(), "Expected next event of type " + type + " but was " + next.getEventId(), null); + } + if (!clazz.isInstance(next)) { + throw makeError(next.getStartMark(), "Expected event of type " + clazz + " but got a " + next.getClass(), null); + } + + return (T) this.getEvent(); + } + + URI tagUri( + final String literalTag, + final Mark startMark, + final Frame head + ) throws ParsingException { + try { + return new URI(literalTag); + } catch (final URISyntaxException ex) { + throw head.makeError(startMark, "Invalid tag URI " + literalTag, ex); + } + } + + // frame states // + + Frame pushFrame(final ComposerState state) { + final int head = ++this.framePointer; + if (head >= this.frames.length) { + this.frames = Arrays.copyOf(this.frames, this.frames.length + FRAME_STACK_INCREMENT); + } + final Frame current; + if (this.frames[head] == null) { + current = this.frames[head] = new Frame(); + } else { + current = this.frames[head]; + } + + if (head > 0) { // inherit from parent state + current.init(state, this.frames[head - 1]); + } else { + current.init(state); + } + + return current; + } + + Frame swapState(final ComposerState state) { + final Frame ret = this.peekFrame(); + ret.state = state; + return ret; + } + + void popFrame() { + if (this.framePointer-- < 0) { + throw new IllegalStateException("Tried to pop beyond bounds of the frame stack"); + } + final Frame popped = this.frames[this.framePointer + 1]; + if (!popped.hasFlag(Frame.SAVE_NODE)) { + popped.node = null; // don't hold references + } + } + + Frame peekFrame() { + return this.peekFrame(0); + } + + Frame peekFrame(final int depth) { + if (depth < 0 || depth > this.framePointer) { + throw new IllegalStateException("Tried to peek beyond bounds of state stack. requested depth: " + depth + + ", actual depth: " + this.framePointer); + } + + return this.frames[this.framePointer - depth]; + } + + /** + * A frame in the state stack. + * + *

Contains the target node and current resolved tag (if any).

+ */ + static class Frame { + + static final int SUPPRESS_COMMENTS = 1; // whether to associate comment events with this node + static final int SAVE_NODE = 1 << 1; // don't clear node when popping + static final int MERGE_REFERENCE_VALUE = 1 << 2; // when values have an anchor + + static final int UNINHERITABLE_FLAGS = MERGE_REFERENCE_VALUE; + + @MonotonicNonNull ComposerState state; + + /** + * The resolved tag. + * + *

May be used by child states to perform their own + * tag resolution.

+ */ + @Nullable Tag resolvedTag; + @MonotonicNonNull ConfigurationNode node; + int flags; + + void init(final ComposerState state, final Frame parent) { + this.state = state; + this.node = parent.node; + this.flags = parent.flags & ~UNINHERITABLE_FLAGS; + this.resolvedTag = null; + } + + void init(final ComposerState state) { + this.state = state; + this.flags = 0; + this.resolvedTag = null; + } + + boolean hasFlag(final int flag) { + return (this.flags & flag) != 0; + } + + void addFlag(final int flag) { + this.flags |= flag; + } + + ParsingException makeError( + final Mark mark, + final @Nullable String message, + final @Nullable Throwable error + ) { + return new ParsingException(this.node, mark.getLine(), mark.getColumn(), mark.get_snippet(), message, error); + } + } + + // comments + + void applyComments(final ConfigurationNode node) { + if (!(node instanceof CommentedConfigurationNodeIntermediary<@NonNull?>)) { + return; // no comments are even collected + } + + if (this.commentCollector != null && this.commentCollector.length() > 0) { + final StringBuilder collector = this.commentCollector; + final CommentedConfigurationNodeIntermediary<@NonNull ?> commented = (CommentedConfigurationNodeIntermediary<@NonNull ?>) node; + if (commented.comment() != null) { + collector.insert(0, commented.comment()); + collector.insert(commented.comment().length(), '\n'); + } + commented.comment(collector.toString()); + collector.delete(0, collector.length()); + } + } + + @Nullable String popComment() { + if (this.peekFrame().hasFlag(Frame.SUPPRESS_COMMENTS)) { + return null; + } + + final String ret; + if (this.commentCollector != null && this.commentCollector.length() > 0) { + final StringBuilder collector = this.commentCollector; + ret = collector.toString(); + collector.delete(0, collector.length()); + } else { + ret = null; + } + this.collectComments(); + return ret; + } + + void applyComment(final @Nullable String comment, final ConfigurationNode node) { + if (comment == null || !(node instanceof CommentedConfigurationNodeIntermediary<@NonNull ?>)) { + return; + } + final CommentedConfigurationNodeIntermediary<@NonNull ?> commented = (CommentedConfigurationNodeIntermediary<@NonNull ?>) node; + if (commented.comment() != null) { + commented.comment( + commented.comment() + + '\n' + + comment + ); + } else { + commented.comment(comment); + } + + } + + void collectComments() { + while (this.peekEvent().is(Event.ID.Comment)) { + final CommentEvent event = (CommentEvent) this.getEvent(); + if (event.getCommentType() != CommentType.BLANK_LINE) { + @Nullable StringBuilder commentCollector = this.commentCollector; + if (commentCollector == null) { + this.commentCollector = commentCollector = new StringBuilder(); + } + if (commentCollector.length() > 0) { + commentCollector.append(AbstractConfigurationLoader.CONFIGURATE_LINE_SEPARATOR); + } + if (this.stripLeadingCommentWhitespace && event.getValue().startsWith(" ")) { + commentCollector.append(event.getValue(), 1, event.getValue().length()); + } else { + commentCollector.append(event.getValue()); + } + } else if (this.commentCollector != null + && this.commentCollector.length() > 0 + && this.peekEvent().is(Event.ID.Comment)) { // mid-comment blank line + this.commentCollector.append("\n"); + } + } + } + + public Stream stream(final ConfigurationNodeFactory factory) throws ParsingException { + this.requireEvent(Event.ID.StreamStart); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator() { + @Override + public boolean hasNext() { + return !YamlParserComposer.this.checkEvent(Event.ID.StreamEnd); + } + + @Override + public N next() { + if (!this.hasNext()) { + throw new IndexOutOfBoundsException(); + } + try { + final N node = factory.createNode(); + YamlParserComposer.this.document(node); + if (!this.hasNext()) { + YamlParserComposer.this.requireEvent(Event.ID.StreamEnd); + } + return node; + } catch (final ConfigurateException e) { + throw new RuntimeException(e); // TODO + } + } + }, Spliterator.IMMUTABLE | Spliterator.ORDERED | Spliterator.NONNULL), false); + } + + static ParsingException makeError( + final Mark mark, + final @Nullable String message, + final @Nullable Throwable error + ) { + return new ParsingException(mark.getLine(), mark.getColumn(), mark.get_snippet(), message, error); + } + + /** + * A phase in the composer state machine. + * + *

Each phase can manipulate the phase stack frames in response to one + * or more events. The frames manipulate their own stack.

+ * + *

Frames manage their own event consumption, and can swap to another + * state within the same frame, or push/pop additional frames.

+ */ + interface ComposerState { + + /** + * Perform one round of processing. + * + * @param head current state frame + * @param self the state to work with + * @return the next frame, or {@code null} to pop a frame. See {@link #pushFrame(ComposerState)} and {@link #swapState(ComposerState)} + * @throws ParsingException to indicate an error + * @see Frame#makeError(Mark, String, Throwable) + */ + @Nullable Frame accept(Frame head, YamlParserComposer self) throws ParsingException; + } + + /** + * The first state. + * + *

Expects a {@link Event.ID#DocumentStart} event, and will push a frame + * with Value state.

+ */ + static final class DocumentStart implements ComposerState { + + static final DocumentStart INSTANCE = new DocumentStart(); + + private DocumentStart() { + } + + @Override + public Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + self.collectComments(); + final DocumentStartEvent ds = self.requireEvent(Event.ID.DocumentStart, DocumentStartEvent.class); + if (ds.getTags() != null) { + self.declaredTags.putAll(ds.getTags()); + } + self.swapState(DocumentEnd.INSTANCE); // state to use after Value is complete + if (self.peekEvent().is(Event.ID.DocumentEnd)) { + return head; + } else { + return self.pushFrame(Value.INSTANCE); + } + } + } + + /** + * The final state. + * + *

Expects a {@link Event.ID#DocumentEnd} event, and will process any + * trailing comments.

+ */ + static final class DocumentEnd implements ComposerState { + + static final DocumentEnd INSTANCE = new DocumentEnd(); + + private DocumentEnd() { + } + + @Override + public @Nullable Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + self.requireEvent(Event.ID.DocumentEnd); + return null; + } + } + + /** + * Receives a value of unknown type, and figures out what to do with it. + * + *

This state performs pre-processing of values as well.

+ */ + static final class Value implements ComposerState { + + static final Value INSTANCE = new Value(); + + private Value() { + } + + @Override + public Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + final Event peeked = self.peekEvent(); + // extract event metadata + if (peeked instanceof NodeEvent && !(peeked instanceof AliasEvent)) { + final String anchor = ((NodeEvent) peeked).getAnchor(); + if (anchor != null) { + head.node.hint(YamlConfigurationLoader.ANCHOR_ID, anchor); + self.aliases.put(anchor, head.node); + } + if (peeked instanceof CollectionStartEvent) { + head.node.hint(YamlConfigurationLoader.NODE_STYLE, NodeStyle.fromSnakeYaml(((CollectionStartEvent) peeked).getFlowStyle())); + } + } + + // then handle the value + switch (peeked.getEventId()) { + case Scalar: + return self.swapState(Scalar.INSTANCE); + case MappingStart: + return self.swapState(MappingStart.INSTANCE); + case SequenceStart: + return self.swapState(SequenceStart.INSTANCE); + case Alias: + return self.swapState(Alias.INSTANCE); + default: + throw head.makeError(peeked.getStartMark(), "Unexpected event type " + peeked.getEventId(), null); + } + } + + } + + static final class Scalar implements ComposerState { + + static final Scalar INSTANCE = new Scalar(); + + private Scalar() { + } + + @Override + public @Nullable Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + final @Nullable String comments = self.popComment(); + // read scalar + final ScalarEvent scalar = self.requireEvent(Event.ID.Scalar, ScalarEvent.class); + head.node.hint(YamlConfigurationLoader.SCALAR_STYLE, ScalarStyle.fromSnakeYaml(scalar.getScalarStyle())); + // resolve tag + @Nullable Tag tag; + if (scalar.getTag() != null) { + // todo: handle ! tag + final URI tagUri = self.tagUri(scalar.getTag(), scalar.getStartMark(), head); + tag = self.tags.named(tagUri); + if (tag == null) { + tag = Tag.Scalar.ofUnknown(tagUri); + head.node.raw(scalar.getValue()); // TODO: tags and value types + } else if (!(tag instanceof Tag.Scalar)) { + throw head.makeError( + scalar.getStartMark(), + "Declared tag for node was expected to handle a Scalar, but actually is a " + tag.getClass(), + null + ); + } else { + head.node.raw(((Tag.Scalar) tag).fromString(scalar.getValue())); + } + } else { + // Only perform implicit tag resolution for plain scalars + tag = scalar.getScalarStyle() == DumperOptions.ScalarStyle.PLAIN ? self.tags.forInput(scalar.getValue()) : Yaml11Tags.STR; + if (tag == null) { + // todo: maybe throw here? + tag = Yaml11Tags.STR; + } + if (!Yaml11Tags.NULL.equals(tag)) { // we don't want to nuke node data with null values + head.node.raw(((Tag.Scalar) tag).fromString(scalar.getValue())); + } + } + self.applyComment(comments, head.node); + head.node.hint(YamlConfigurationLoader.TAG, tag); + head.resolvedTag = tag; + // pop state + return null; + } + + } + + static final class MappingStart implements ComposerState { + + static final MappingStart INSTANCE = new MappingStart(); + + private MappingStart() { + } + + @Override + public Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + final MappingStartEvent event = self.requireEvent(Event.ID.MappingStart, MappingStartEvent.class); + if (event.isFlow() || self.peekEvent().is(Event.ID.Comment)) { + self.applyComments(head.node); + } + head.node.raw(Collections.emptyMap()); + return self.swapState(MappingKeyOrEnd.INSTANCE); + } + } + + static final class MappingKeyOrEnd implements ComposerState { + + static final MappingKeyOrEnd INSTANCE = new MappingKeyOrEnd(); + + private MappingKeyOrEnd() { + } + + @Override + public @Nullable Frame accept(final Frame head, final YamlParserComposer self) { + self.collectComments(); + if (self.peekEvent().is(Event.ID.MappingEnd)) { + self.getEvent(); + return null; + } else { + // push state of MappingValue + self.pushFrame(MappingValue.INSTANCE); + // push destination node + final Frame child = self.pushFrame(Value.INSTANCE); // compute key node + child.addFlag(Frame.SUPPRESS_COMMENTS | Frame.SAVE_NODE); + child.node = BasicConfigurationNode.root(head.node.options()); + return child; + } + } + + } + + static final class MappingValue implements ComposerState { + + static final MappingValue INSTANCE = new MappingValue(); + + private MappingValue() { + } + + @Override + public Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + + // get value from next state, somehow? + // pop destination node + // set as 'next target' + final @Nullable ConfigurationNode keyHolder = self.frames[self.framePointer + 1].node; // todo: ugly + if (keyHolder == null) { + throw new IllegalStateException("null keyHolder"); + } + // if merge key, set a flag on the next value state + if (keyHolder.ownHint(YamlConfigurationLoader.TAG) == Yaml11Tags.MERGE) { + head.addFlag(Frame.MERGE_REFERENCE_VALUE); + } + + final @Nullable Object key = keyHolder.raw(); + if (key == null) { + throw head.makeError(self.scanner.peekToken().getStartMark(), "'null' is not permitted as a mapping key", null); + } + + final ConfigurationNode child = head.node.node(key); + if (!child.virtual()) { + // duplicate keys are forbidden (3.2.1.3) + // snakeyaml doesn't enforce this :( + throw makeError(self.scanner.peekToken().getStartMark(), "Duplicate key '" + child.key() + "' encountered!", null); + } + head.node = child; + self.applyComments(head.node); + self.collectComments(); + return self.swapState(Value.INSTANCE); + } + + } + + static final class SequenceStart implements ComposerState { + + static final SequenceStart INSTANCE = new SequenceStart(); + + private SequenceStart() { + } + + @Override + public Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + final SequenceStartEvent event = self.requireEvent(Event.ID.SequenceStart, SequenceStartEvent.class); + if (event.isFlow() || self.peekEvent().is(Event.ID.Comment)) { + self.applyComments(head.node); + } + head.node.raw(Collections.emptyList()); + return self.swapState(SequenceEntryOrEnd.INSTANCE); + } + + } + + static final class SequenceEntryOrEnd implements ComposerState { + + static final SequenceEntryOrEnd INSTANCE = new SequenceEntryOrEnd(); + + private SequenceEntryOrEnd() { + } + + @Override + public @Nullable Frame accept(final Frame head, final YamlParserComposer self) { + final @Nullable String comments = self.popComment(); + if (self.peekEvent().is(Event.ID.SequenceEnd)) { + self.getEvent(); + return null; + } else { + // push destination node as 'next target' + final Frame ret = self.pushFrame(Value.INSTANCE); + ret.node = self.peekFrame().node.appendListNode(); + self.applyComment(comments, ret.node); + return ret; + } + } + + } + + static final class Alias implements ComposerState { + + static final Alias INSTANCE = new Alias(); + + private Alias() { + } + + @Override + public @Nullable Frame accept(final Frame head, final YamlParserComposer self) throws ParsingException { + final AliasEvent event = self.requireEvent(Event.ID.Alias, AliasEvent.class); + final ConfigurationNode target = self.aliases.get(event.getAnchor()); + if (target == null) { + throw head.makeError(event.getStartMark(), "Unknown anchor '" + event.getAnchor() + "'", null); + } + final ConfigurationNode into; + if (head.hasFlag(Frame.MERGE_REFERENCE_VALUE)) { + into = head.node.parent(); + } else { + into = head.node; + } + into.from(target); // TODO: Reference node types + into.hint(YamlConfigurationLoader.ANCHOR_ID, null); // don't duplicate alias + return null; + } + + } + +} diff --git a/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlVisitor.java b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlVisitor.java new file mode 100644 index 000000000..b438438da --- /dev/null +++ b/format/yaml/src/main/java/org/spongepowered/configurate/yaml/YamlVisitor.java @@ -0,0 +1,237 @@ +/* + * 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.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.BasicConfigurationNode; +import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationVisitor; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.emitter.Emitter; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.events.CommentEvent; +import org.yaml.snakeyaml.events.DocumentEndEvent; +import org.yaml.snakeyaml.events.Event; +import org.yaml.snakeyaml.events.ImplicitTuple; +import org.yaml.snakeyaml.events.MappingEndEvent; +import org.yaml.snakeyaml.events.MappingStartEvent; +import org.yaml.snakeyaml.events.ScalarEvent; +import org.yaml.snakeyaml.events.SequenceEndEvent; +import org.yaml.snakeyaml.events.SequenceStartEvent; +import org.yaml.snakeyaml.events.StreamEndEvent; +import org.yaml.snakeyaml.events.StreamStartEvent; + +import java.io.IOException; +import java.io.Writer; +import java.util.regex.Pattern; + +final class YamlVisitor implements ConfigurationVisitor { + + private static final Pattern COMMENT_SPLIT = Pattern.compile("\r?\n"); + private static final CommentEvent WHITESPACE = new CommentEvent( + CommentType.BLANK_LINE, + YamlConfigurationLoader.CONFIGURATE_LINE_SEPARATOR, + null, + null + ); + private static final CommentEvent COMMENT_BLANK_LINE = new CommentEvent(CommentType.BLANK_LINE, "", null, null); + static final StreamStartEvent STREAM_START = new StreamStartEvent(null, null); + static final StreamEndEvent STREAM_END = new StreamEndEvent(null, null); + static final DocumentEndEvent DOCUMENT_END = new DocumentEndEvent(null, null, false); + private static final SequenceEndEvent SEQUENCE_END = new SequenceEndEvent(null, null); + private static final MappingEndEvent MAPPING_END = new MappingEndEvent(null, null); + + private final boolean shouldPadComments; + private final TagRepository tags; + + YamlVisitor(final boolean shouldPadComments, final TagRepository tags) { + this.shouldPadComments = shouldPadComments; + this.tags = tags; + } + + @Override + public State newState() throws ConfigurateException { + throw new ConfigurateException("States cannot be created as a writer must be provided"); + } + + @Override + public void beginVisit(final ConfigurationNode node, final State state) { + state.mapKeyHolder = BasicConfigurationNode.root(node.options()); + } + + @Override + public void enterNode(final ConfigurationNode node, final State state) throws ConfigurateException { + if (node instanceof CommentedConfigurationNodeIntermediary<@NonNull ?> && state.options.isProcessComments()) { + final @Nullable String comment = ((CommentedConfigurationNodeIntermediary<@NonNull ?>) node).comment(); + if (comment != null) { + if (this.shouldPadComments && node != state.start) { + if (!state.first) { + if (!node.parent().isList()) { + state.emit(WHITESPACE); + } + } else { + state.first = false; + } + } + for (final String line : COMMENT_SPLIT.split(comment, -1)) { + if (line.isEmpty()) { + state.emit(COMMENT_BLANK_LINE); + } else { + if (line.codePointAt(0) != '#') { // allow lines that are only the comment character, for box drawing + state.emit(new CommentEvent(CommentType.BLOCK, " " + line, null, null)); + } else { + state.emit(new CommentEvent(CommentType.BLOCK, line, null, null)); + } + } + } + } + } + + if (node != state.start && node.key() != null /* implies node.parent() != null */ && node.parent().isMap()) { // emit key + state.mapKeyHolder.raw(node.key()); + state.mapKeyHolder.visit(this, state); + } + + // detect null value + if (node.empty() && node.raw() == null) { + state.emit(new ScalarEvent( + this.anchor(node), + null, + new ImplicitTuple(true, false), + "", + null, + null, + DumperOptions.ScalarStyle.PLAIN + )); + } + } + + @Override + public void enterMappingNode(final ConfigurationNode node, final State state) throws ConfigurateException { + final TagRepository.AnalyzedTag analysis = this.tags.analyze(node); + state.first = true; + state.emit(new MappingStartEvent( + this.anchor(node), + analysis.actual().tagUri().toString(), + analysis.implicit(), + null, + null, + NodeStyle.asSnakeYaml(state.determineStyle(node)) + )); + } + + @Override + public void enterListNode(final ConfigurationNode node, final State state) throws ConfigurateException { + final TagRepository.AnalyzedTag analysis = this.tags.analyze(node); + state.emit(new SequenceStartEvent( + this.anchor(node), + analysis.actual().tagUri().toString(), + analysis.implicit(), + null, + null, + NodeStyle.asSnakeYaml(state.determineStyle(node)) + )); + } + + @SuppressWarnings("unchecked") + @Override + public void enterScalarNode(final ConfigurationNode node, final State state) throws ConfigurateException { + // determine + final TagRepository.AnalyzedTag analysis = this.tags.analyze(node); + final ImplicitTuple implicity = new ImplicitTuple(analysis.implicit(), analysis.resolved().equals(this.tags.stringTag())); + final Tag actual = analysis.actual(); + if (!(actual instanceof Tag.Scalar)) { + throw new ConfigurateException( + node, + "Tag '" + actual.tagUri() + "' is required to be a scalar tag, but was actually a " + actual.getClass() + ); + } + + state.emit(new ScalarEvent( + this.anchor(node), + actual.tagUri().toString(), + implicity, + ((Tag.Scalar) actual).toString(node.rawScalar()), + null, + null, + // todo: support configuring default scalar style + ScalarStyle.asSnakeYaml( + node.hint(YamlConfigurationLoader.SCALAR_STYLE), + implicity, + ((Tag.Scalar) actual).preferredScalarStyle() + ) + )); + } + + // TODO: emit alias events for enterReferenceNode + + @Override + public void exitMappingNode(final ConfigurationNode node, final State state) throws ConfigurateException { + state.first = false; // only true if empty map + state.emit(MAPPING_END); + } + + @Override + public void exitListNode(final ConfigurationNode node, final State state) throws ConfigurateException { + state.emit(SEQUENCE_END); + } + + @Override + public Void endVisit(final State state) { + return null; + } + + private @Nullable String anchor(final ConfigurationNode node) { + return node.hint(YamlConfigurationLoader.ANCHOR_ID); + } + + static final class State { + private final Emitter emit; + final DumperOptions options; + @Nullable ConfigurationNode start; + final @Nullable NodeStyle defaultStyle; + ConfigurationNode mapKeyHolder; + boolean first; // reset to true at the beginning of each mapping node + + State(final DumperOptions options, final Writer writer, final @Nullable NodeStyle defaultStyle) { + this.emit = new Emitter(writer, options); + this.options = options; + this.defaultStyle = defaultStyle; + } + + @Nullable NodeStyle determineStyle(final ConfigurationNode node) { + // todo: some basic rules: + // - if a node has any children with comments, convert it to block style + // - when the default style is `AUTO` and `flowLevel` == 0, + final @Nullable NodeStyle style = node.hint(YamlConfigurationLoader.NODE_STYLE); + return style == null ? this.defaultStyle : style; + } + + public void emit(final Event event) throws ConfigurateException { + try { + this.emit.emit(event); + } catch (final YAMLException | IOException ex) { + throw new ConfigurateException(ex); + } + } + } + +} diff --git a/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/CommentTest.groovy b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/CommentTest.groovy new file mode 100644 index 000000000..7ccfd9178 --- /dev/null +++ b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/CommentTest.groovy @@ -0,0 +1,300 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertNull + +import org.junit.jupiter.api.Test +import org.spongepowered.configurate.CommentedConfigurationNode + +class CommentTest implements YamlTest { + + @Test + void testLoadScalarComment() { + final CommentedConfigurationNode node = parseString normalize("""\ + # Hello world + "i'm a string" + """) + + assertEquals("Hello world", node.comment()) + assertEquals("i'm a string", node.raw()) + } + + @Test + void testLoadBlockMappingComment() { + final CommentedConfigurationNode node = parseString normalize("""\ + # outer + test: + # meow + cat: purrs + """) + + assertThat(node.node('test')) + .extracting { it.comment() } + .isEqualTo("outer") + + assertThat(node.node('test', 'cat')).with { + extracting { it.raw() } + .isEqualTo("purrs") + extracting { it.comment() } + .isEqualTo("meow") + } + } + + @Test + void testLoadBlockSequenceComment() { + final CommentedConfigurationNode node = parseString normalize("""\ + # first + - one + # second + - two + """) + + assertThat(node.node(0)) + .extracting { it.comment() } + .isEqualTo("first") + assertThat(node.node(1)) + .extracting { it.comment() } + .isEqualTo("second") + } + + @Test + void testLoadBlockScalarSequenceComment() { + final CommentedConfigurationNode test = parseString(normalize("""\ + - first + # i matter less + - second + - third + # we skipped one + - fourth + """)) + + assertNull(test.node(0).comment()) + assertEquals("i matter less", test.node(1).comment()) + assertEquals("we skipped one", test.node(3).comment()) + } + + @Test + void testLoadScalarCommentsInBlockMapping() { + final CommentedConfigurationNode test = parseString """\ + # on mapping key + blah: + # beginning sequence + - # first on map entry + test: hello + - # on second mapping + test2: goodbye + """.stripIndent(true) + + final CommentedConfigurationNode child = test.node("blah", 0) + assertFalse(child.virtual()) + assertEquals("on mapping key\nbeginning sequence", test.node('blah').comment()) + assertEquals("first on map entry", test.node('blah', 0, 'test').comment()) + assertEquals("on second mapping", test.node('blah', 1, "test2").comment()) + } + + // flow collections are a bit trickier + // we can't really do comments on one line, so these all have to have a line per element + + @Test + void testLoadCommentInFlowMapping() { + final CommentedConfigurationNode test = parseString(normalize("""\ + { + # hello + test: value, + uncommented: thing, + #hi there + last: bye + } + """)) + + assertEquals("hello", test.node("test").comment()) + assertNull(test.node("uncommented").comment()) + assertEquals("hi there", test.node("last").comment()) + } + + @Test + void testLoadCommentInFlowSequence() { + final CommentedConfigurationNode test = parseString(normalize("""\ + # on list + [ + # first + 'first entry', + # second + 'second entry' + ] + """)) + + assertEquals("on list", test.comment()) + assertEquals("first", test.node(0).comment()) + assertEquals("second", test.node(1).comment()) + } + + @Test + void testLoadMixedStructure() { + final CommentedConfigurationNode test = parseResource(getClass().getResource("comments-complex.yml")) + + assertEquals("very mapping", test.node("core", "users", 0, "second").comment()) + } + + @Test + void testWriteScalarCommented() { + final CommentedConfigurationNode node = CommentedConfigurationNode.root() + .raw("test") + .comment("i have a comment") + + assertEquals(normalize("""\ + # i have a comment + test"""), dump(node).trim()) + } + + @Test + void testWriteBlockMappingCommented() { + final CommentedConfigurationNode node = CommentedConfigurationNode.root { + node("a").set("Hello").comment("I'm first") + node("b", "one").set("World") + node("b", "two").set("eee").comment("also me") + } + + assertLinesEqual( + normalize("""\ + # I'm first + a: Hello + b: + one: World + + # also me + two: eee + """), + dump(node, NodeStyle.BLOCK) + ) + } + + @Test + void testWriteBlockSequence() { + final def node = CommentedConfigurationNode.root { + appendListNode().set("Hello") + appendListNode().set("World") + appendListNode().with { + node("one").set("aaa") + node("two").set("bbb") + } + } + + final def expected = normalize("""\ + - Hello + - World + - one: aaa + two: bbb + """) + assertLinesEqual(expected, this.dump(node, NodeStyle.BLOCK)) + } + + @Test + void testWriteBlockSequenceCommented() { + final def node = CommentedConfigurationNode.root { + appendListNode().set("red").comment("A colour") + appendListNode().set("orange").comment("Another colour") + appendListNode().set("yellow").comment("What? a THIRD colour???") + } + + final def expected = normalize("""\ + # A colour + - red + # Another colour + - orange + # What? a THIRD colour??? + - yellow + """) + assertLinesEqual(expected, this.dump(node, NodeStyle.BLOCK)) + } + + @Test + void testWriteFlowMappingCommented() { + final CommentedConfigurationNode node = CommentedConfigurationNode.root { + node("a").set("Hello").comment("I'm first") + node("b", "one").set("World") + node("b", "two").set("eee").comment("also me") + } + + final def expected = normalize("""\ + { + # I'm first + a: Hello, + b: { + one: World, + + # also me + two: eee + } + } + """) + + assertLinesEqual(expected, dump(node, NodeStyle.FLOW)) + } + + @Test + void testPrettyFlowForcedWhenEmittingCommentsEvenNotFirst() { + final CommentedConfigurationNode node = CommentedConfigurationNode.root { + node('one').set "two" + node("three").with { + set "four" + comment "hello" + } + } + + final def expected = normalize("""\ + {one: two, + + # hello + three: four + } + """) + + assertLinesEqual(expected, dump(node, NodeStyle.FLOW)) + } + + @Test + void testWriteFlowSequenceCommented() { + final def node = CommentedConfigurationNode.root { + appendListNode().set("red").comment("A colour") + appendListNode().set("orange").comment("Another colour") + appendListNode().set("yellow").comment("What? a THIRD colour???") + } + + final def expected = normalize("""\ + [ + # A colour + red, + # Another colour + orange, + # What? a THIRD colour??? + yellow + ] + """) + assertLinesEqual(expected, this.dump(node, NodeStyle.FLOW)) + } + + private static def assertLinesEqual(String expected, String actual) { + assertThat(actual.split("\r?\n", -1).collect { it.isAllWhitespace() ? "" : it}) + .containsAll(expected.split("\r?\n", -1).collect { it.isAllWhitespace() ? "" : it}) + } + +} diff --git a/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/ConfigurationNodeStaticExtensions.groovy b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/ConfigurationNodeStaticExtensions.groovy new file mode 100644 index 000000000..cf0e2a521 --- /dev/null +++ b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/ConfigurationNodeStaticExtensions.groovy @@ -0,0 +1,46 @@ +/* + * 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.spongepowered.configurate.BasicConfigurationNode +import org.spongepowered.configurate.CommentedConfigurationNode + +class ConfigurationNodeStaticExtensions { + + static BasicConfigurationNode root( + final BasicConfigurationNode unused, + final @DelegatesTo(value = BasicConfigurationNode, strategy = Closure.DELEGATE_FIRST) Closure action + ) { + def root = BasicConfigurationNode.root() + action.setDelegate(root) + action.resolveStrategy = Closure.DELEGATE_FIRST + action.call(root) + return root + } + + static CommentedConfigurationNode root( + final CommentedConfigurationNode unused, + final @DelegatesTo(value = CommentedConfigurationNode, strategy = Closure.DELEGATE_FIRST) Closure action + ) { + def root = CommentedConfigurationNode.root() + action.setDelegate(root) + action.resolveStrategy = Closure.DELEGATE_FIRST + action.call(root) + return root + } + +} diff --git a/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/IntegrationTests.groovy b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/IntegrationTests.groovy new file mode 100644 index 000000000..54729b001 --- /dev/null +++ b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/IntegrationTests.groovy @@ -0,0 +1,58 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat + +import org.junit.jupiter.api.Test + +/** + * End-to-end tests using sample configurations sourced + * from production projects. + */ +class IntegrationTests implements YamlTest { + + @Test + void testEssentialsXDefault() { + def input = this.class.getResourceAsStream("essx-example.yml").getText('utf-8') + def node = parseString(input) + + def serialized = dump(node) + + assertThat(serialized).isEqualTo(input) + } + + @Test + void testEssentialsXLegacy() { + def input = this.class.getResourceAsStream("essx-legacy.yml").getText('utf-8') + def node = parseString(input) + + def serialized = dump(node) + + assertThat(serialized).isEqualTo(input) + } + + @Test + void testMobCleaner() { + def input = this.class.getResourceAsStream("mobcleaner-example.yml").getText('utf-8') + def node = parseString(input) + + def serialized = dump(node) + + assertThat(serialized).isEqualTo(input) + } +} diff --git a/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlConfigurationLoaderTest.groovy b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlConfigurationLoaderTest.groovy new file mode 100644 index 000000000..7db0662aa --- /dev/null +++ b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlConfigurationLoaderTest.groovy @@ -0,0 +1,103 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals + +import io.leangen.geantyref.TypeToken +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.spongepowered.configurate.BasicConfigurationNode +import org.spongepowered.configurate.CommentedConfigurationNode +import org.spongepowered.configurate.ConfigurateException +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.loader.ConfigurationLoader + +import java.nio.charset.StandardCharsets +import java.nio.file.Path + +/** + * Basic sanity checks for the loader. + */ +class YamlConfigurationLoaderTest { + + @Test + void testSimpleLoading() throws ConfigurateException { + final def url = getClass().getResource("example.yml") + final def loader = YamlConfigurationLoader.builder() + .url(url) + .build() + final def node = loader.load() + + assertEquals("unicorn", node.node("test", "op-level").raw()) + assertEquals("dragon", node.node("other", "op-level").raw()) + assertEquals("dog park", node.node("other", "location").raw()) + + final def fooList = new ArrayList<>(node.node("foo") + .getList(new TypeToken>>>() {})) + assertEquals(1, fooList.get(0).get("bar").size()) + + } + + @Test + void testReadWithTabs() throws ConfigurateException { + final def expected = CommentedConfigurationNode.root { n -> + n.node("document").act{ d -> + d.node("we").raw("support tabs") + d.node("and").raw("literal tabs\tin strings") + d.node("with").act{ w -> + w.appendListNode().raw("more levels") + w.appendListNode().raw("of indentation") + } + } + } + + final URL url = getClass().getResource("tab-example.yml") + final ConfigurationLoader loader = YamlConfigurationLoader.builder() + .url(url).build() + final ConfigurationNode node = loader.load() + assertEquals(expected, node) + } + + @Test + void testWriteBasicFile(final @TempDir Path tempDir) throws ConfigurateException, IOException { + final Path target = tempDir.resolve("write-basic.yml") + final ConfigurationNode node = BasicConfigurationNode.root { n -> + n.node("mapping", "first").set("hello") + n.node("mapping", "second").set("world") + + n.node("list").act { c -> + c.appendListNode().set(1) + c.appendListNode().set(2) + c.appendListNode().set(3) + c.appendListNode().set(4) + } + } + + final YamlConfigurationLoader loader = YamlConfigurationLoader.builder() + .path(target) + .nodeStyle(NodeStyle.BLOCK) + .build() + + loader.save(node) + + assertEquals( + getClass().getResource("write-expected.yml").getText(StandardCharsets.UTF_8.name()).replace("\r\n", "\n"), + target.getText(StandardCharsets.UTF_8.name()).replace("\r\n", "\n") + ) + } +} diff --git a/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlParserComposerTest.groovy b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlParserComposerTest.groovy new file mode 100644 index 000000000..983dd1731 --- /dev/null +++ b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlParserComposerTest.groovy @@ -0,0 +1,239 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat +import static org.assertj.core.api.Assertions.assertThatThrownBy + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.loader.ParsingException + +/** + * Tests of the basic functionality of our composer implementation. + * + *

Comment-specific testing is handled in {@link CommentTest} + */ +class YamlParserComposerTest implements YamlTest { + + @Test + void testEmptyDocument() throws IOException { + final ConfigurationNode result = parseString("") + assertThat(result.empty()).isTrue() + assertThat(result.raw()).isNull() + } + + @Test + void testDuplicateKeysForbidden() throws IOException { + assertThatThrownBy { parseString '{duplicated: 1, duplicated: 2}' } + .isInstanceOf(ParsingException) + .hasMessageContaining("Duplicate key") + } + + // Different types of scalars (folded, block, etc) + + @Test + void testLoadPlainScalar() { + def result = parseString "hello world" + assertThat(result.raw()) + .isEqualTo("hello world") + + assertThat(result.hint(YamlConfigurationLoader.SCALAR_STYLE)) + .isEqualTo(ScalarStyle.UNQUOTED) + } + + @Test + void testLoadDoubleQuotedScalar() { + def result = parseString '"hello world"' + assertThat(result.raw()) + .isEqualTo("hello world") + + assertThat(result.hint(YamlConfigurationLoader.SCALAR_STYLE)) + .isEqualTo(ScalarStyle.DOUBLE_QUOTED) + } + + @Test + void testLoadSingleQuotedScalar() { + def result = parseString "'hello world'" + assertThat(result.raw()) + .isEqualTo("hello world") + + assertThat(result.hint(YamlConfigurationLoader.SCALAR_STYLE)) + .isEqualTo(ScalarStyle.SINGLE_QUOTED) + } + + @Test + void testLoadFoldedScalar() { + def result = parseString("""\ + test: > + hello + world\ + """.stripIndent(true).trim()).node("test") + + assertThat(result.raw()) + .isEqualTo("hello world") + + assertThat(result.hint(YamlConfigurationLoader.SCALAR_STYLE)) + .isEqualTo(ScalarStyle.FOLDED) + } + + @Test + void testLoadBlockScalar() { + def result = parseString("""\ + test: | + hello + world + """.stripIndent(true).trim()).node("test") + + assertThat(result.raw()) + .isEqualTo("hello\nworld") + + assertThat(result.hint(YamlConfigurationLoader.SCALAR_STYLE)) + .isEqualTo(ScalarStyle.LITERAL) + + } + + // More complex data structures + + @Test + void testLoadMap() { + def result = parseString """\ + hello: + world: yup! + two: [one, two, three] + "yes": + aaa: bbb + ccc: ddd + """.stripIndent(true) + + assertThat(result.node('hello', 'world').raw()) + .isEqualTo("yup!") + + assertThat(result.node('hello', 'two', 0).raw()) + .isEqualTo('one') + + assertThat(result.node('hello', 'two').getList(String)) + .containsExactly('one', 'two', 'three') + + assertThat(result.node('hello', 'yes', 'aaa').raw()) + .isEqualTo('bbb') + + assertThat(result.node('hello', 'yes', 'ccc').raw()) + .isEqualTo('ddd') + } + + @Test + void testLoadSequence() { + def result = parseString """\ + flow: [a, b, c] + block: + - d + - e + - f + """.stripIndent(true) + + assertThat(result.node('flow').getList(String)) + .containsExactly('a', 'b', 'c') + assertThat(result.node('flow').hint(YamlConfigurationLoader.NODE_STYLE)) + .isEqualTo(NodeStyle.FLOW) + + assertThat(result.node('block').getList(String)) + .containsExactly('d', 'e', 'f') + assertThat(result.node('block').hint(YamlConfigurationLoader.NODE_STYLE)) + .isEqualTo(NodeStyle.BLOCK) + } + + + @Test + void testLoadAlias() { + def result = parseString """\ + src: &ref [a, b, c] + dest: *ref + """.stripIndent(true) + + def src = result.node('src') + def dest = result.node('dest') + + // Value transferred + assertThat(dest.getList(String)) + .containsExactly('a', 'b', 'c') + + // Anchor information preserved + // TODO: this may be different once proper reference nodes are implemented + assertThat(src.hint(YamlConfigurationLoader.ANCHOR_ID)) + .isEqualTo('ref') + + assertThat(dest.hint(YamlConfigurationLoader.ANCHOR_ID)) + .isNull() + } + + @Test + void testCommentsOnNullValuePreserved() { + def result = parseString """\ + # the greetings + hello: + # - abc + # - def + """ + + assertThat(result.node('hello').comment()) + .isEqualTo("the greetings") + } + + // Test that implicit tags are resolved properly + + @Test + void testMergeKey() { + def result = parseString """\ + src: &ref + old: merged + dest: + <<: *ref + new: added + """.stripIndent(true) + + def src = result.node('src') + def dest = result.node('dest') + + // Value transferred + assertThat(dest.childrenMap().keySet()) + .containsExactly('old', 'new') + } + + @Test + void testYIsNotBoolean() { + def result = parseString """\ + asVal: y + y: asKey + """ + + assertThat(result.node('asVal')).with { + extracting { it.virtual() } + .is(false) + extracting { it.raw() } + .isEqualTo("y") + } + assertThat(result.node("y")).with { + extracting { it.virtual() } + .is(false) + extracting { it.raw() } + .isEqualTo('asKey') + } + } + +} diff --git a/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlTest.groovy b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlTest.groovy new file mode 100644 index 000000000..3f9a81291 --- /dev/null +++ b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlTest.groovy @@ -0,0 +1,100 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertNotNull + +import org.spongepowered.configurate.CommentedConfigurationNode +import org.yaml.snakeyaml.LoaderOptions +import org.yaml.snakeyaml.events.CollectionEndEvent +import org.yaml.snakeyaml.events.CollectionStartEvent +import org.yaml.snakeyaml.parser.ParserImpl +import org.yaml.snakeyaml.reader.StreamReader +import org.yaml.snakeyaml.scanner.ScannerImpl + +trait YamlTest { + + CommentedConfigurationNode parseString(final String input) { + // Print events + def loaderOpts = new LoaderOptions().tap { + processComments = true + acceptTabs = true + } + this.dumpEvents(new StreamReader(input), loaderOpts) + + final YamlParserComposer loader = new YamlParserComposer(new StreamReader(input), loaderOpts, Yaml11Tags.REPOSITORY) + final CommentedConfigurationNode result = CommentedConfigurationNode.root() + loader.singleDocumentStream(result) + return result + } + + CommentedConfigurationNode parseResource(final URL url) { + // Print events + def loaderOpts = new LoaderOptions().tap { + processComments = true + acceptTabs = true + } + url.openStream().withReader('UTF-8') {reader -> + this.dumpEvents(new StreamReader(reader), loaderOpts) + } + + assertNotNull(url, "Expected resource is missing") + url.openStream().withReader('UTF-8') { reader -> + final YamlParserComposer loader = new YamlParserComposer(new StreamReader(reader), loaderOpts, Yaml11Tags.REPOSITORY) + final CommentedConfigurationNode result = CommentedConfigurationNode.root() + loader.singleDocumentStream(result) + return result + } + } + + private void dumpEvents(StreamReader reader, LoaderOptions loaderOpts) { + def scanner = new ScannerImpl(reader, loaderOpts) + def parser = new ParserImpl(scanner) + int indentLevel = 0 + while (true) { + if (parser.peekEvent() instanceof CollectionEndEvent) { + indentLevel-- + } + indentLevel.times { + print " " + } + if (parser.peekEvent() instanceof CollectionStartEvent) { + indentLevel++ + } + + println parser.getEvent() + if (!parser.peekEvent()) break + } + } + + String dump(final CommentedConfigurationNode input) { + return dump(input, null) + } + + String dump(final CommentedConfigurationNode input, final NodeStyle preferredStyle) { + return YamlConfigurationLoader.builder() + .nodeStyle(preferredStyle) + .indent(2) + .commentsEnabled(true) + .buildAndSaveString(input) + } + + String normalize(final String input) { + return input.stripIndent(true) + } + +} diff --git a/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlVisitorTest.groovy b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlVisitorTest.groovy new file mode 100644 index 000000000..82b9f98b8 --- /dev/null +++ b/format/yaml/src/test/groovy/org/spongepowered/configurate/yaml/YamlVisitorTest.groovy @@ -0,0 +1,111 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals + +import org.junit.jupiter.api.Test +import org.spongepowered.configurate.CommentedConfigurationNode + +/** + * Testing YAML emmission. + */ +class YamlVisitorTest implements YamlTest { + + @Test + void testPlainScalar() { + def node = CommentedConfigurationNode.root().set("test") + + assertEquals("test\n", dump(node)) + } + + @Test + void testNumbersAreNonPlainScalar() { + def node = CommentedConfigurationNode.root().set("1234") + + assertEquals("\"1234\"\n", dump(node)) + } + + @Test + void testBlockSequence() { + final def node = CommentedConfigurationNode.root { + appendListNode().set("Hello") + appendListNode().set("World") + appendListNode().act { + it.node("one").set("aaa") + it.node("two").set("bbb") + } + } + + final def expected = normalize("""\ + - Hello + - World + - one: aaa + two: bbb + """) + assertEquals(expected, this.dump(node, NodeStyle.BLOCK)) + } + + @Test + void testBlockMapping() { + final def node = CommentedConfigurationNode.root { + node("meow").set("purr") + node("eight").set(1234) + node("fun").set(true) + } + + final def expected = normalize("""\ + meow: purr + eight: 1234 + fun: true + """) + assertEquals(expected, this.dump(node, NodeStyle.BLOCK)) + } + + @Test + void testFlowSequence() { + final def node = CommentedConfigurationNode.root { + appendListNode().set("Hello") + appendListNode().set("World") + appendListNode().act { + it.node("one").set("aaa") + it.node("two").set("bbb") + } + } + + final def expected = "[Hello, World, {one: aaa, two: bbb}]\n" + assertEquals(expected, this.dump(node, NodeStyle.FLOW)) + } + + @Test + void testFlowMapping() { + final def node = CommentedConfigurationNode.root { + node("meow").set("purr") + node("eight").set(1234) + node("fun").set(true) + } + + final def expected = "{meow: purr, eight: 1234, fun: true}\n" + assertEquals(expected, this.dump(node, NodeStyle.FLOW)) + } + + @Test + void testComplex() { + + } + +} diff --git a/format/yaml/src/test/java/org/spongepowered/configurate/yaml/YamlConfigurationLoaderTest.java b/format/yaml/src/test/java/org/spongepowered/configurate/yaml/YamlConfigurationLoaderTest.java deleted file mode 100644 index 0c74f0165..000000000 --- a/format/yaml/src/test/java/org/spongepowered/configurate/yaml/YamlConfigurationLoaderTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 static org.junit.jupiter.api.Assertions.assertEquals; - -import io.leangen.geantyref.TypeToken; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.spongepowered.configurate.BasicConfigurationNode; -import org.spongepowered.configurate.CommentedConfigurationNode; -import org.spongepowered.configurate.ConfigurateException; -import org.spongepowered.configurate.ConfigurationNode; -import org.spongepowered.configurate.loader.ConfigurationLoader; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Basic sanity checks for the loader. - */ -class YamlConfigurationLoaderTest { - - @Test - void testSimpleLoading() throws ConfigurateException { - final URL url = this.getClass().getResource("/example.yml"); - final ConfigurationLoader loader = YamlConfigurationLoader.builder() - .url(url).build(); - final ConfigurationNode node = loader.load(); - assertEquals("unicorn", node.node("test", "op-level").raw()); - assertEquals("dragon", node.node("other", "op-level").raw()); - assertEquals("dog park", node.node("other", "location").raw()); - - - final List>> fooList = new ArrayList<>(node.node("foo") - .getList(new TypeToken>>() {})); - assertEquals(0, fooList.get(0).get("bar").size()); - } - - @Test - void testReadWithTabs() throws ConfigurateException { - final ConfigurationNode expected = CommentedConfigurationNode.root(n -> { - n.node("document").act(d -> { - d.node("we").raw("support tabs"); - d.node("and").raw("literal tabs\tin strings"); - d.node("with").act(w -> { - w.appendListNode().raw("more levels"); - w.appendListNode().raw("of indentation"); - }); - }); - }); - - final URL url = this.getClass().getResource("/tab-example.yml"); - final ConfigurationLoader loader = YamlConfigurationLoader.builder() - .url(url).build(); - final ConfigurationNode node = loader.load(); - assertEquals(expected, node); - } - - @Test - void testWriteBasicFile(final @TempDir Path tempDir) throws ConfigurateException, IOException { - final Path target = tempDir.resolve("write-basic.yml"); - final ConfigurationNode node = BasicConfigurationNode.root(n -> { - n.node("mapping", "first").set("hello"); - n.node("mapping", "second").set("world"); - - n.node("list").act(c -> { - c.appendListNode().set(1); - c.appendListNode().set(2); - c.appendListNode().set(3); - c.appendListNode().set(4); - }); - }); - - final YamlConfigurationLoader loader = YamlConfigurationLoader.builder() - .path(target) - .nodeStyle(NodeStyle.BLOCK) - .build(); - - loader.save(node); - - assertEquals(readLines(this.getClass().getResource("write-expected.yml")), Files.readAllLines(target, StandardCharsets.UTF_8)); - } - - private static List readLines(final URL source) throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(source.openStream(), StandardCharsets.UTF_8))) { - return reader.lines().collect(Collectors.toList()); - } - } - -} diff --git a/format/yaml/src/test/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule b/format/yaml/src/test/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule new file mode 100644 index 000000000..6b18e9437 --- /dev/null +++ b/format/yaml/src/test/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule @@ -0,0 +1,4 @@ +moduleName=Configurate Extensions for YAML Tests +moduleVersion=test +extensionClasses= +staticExtensionClasses=org.spongepowered.configurate.yaml.ConfigurationNodeStaticExtensions \ No newline at end of file diff --git a/format/yaml/src/test/resources/example.yml b/format/yaml/src/test/resources/example.yml deleted file mode 100644 index 165d9b5c1..000000000 --- a/format/yaml/src/test/resources/example.yml +++ /dev/null @@ -1,8 +0,0 @@ -test: - op-level: unicorn -other: - op-level: dragon - location: dog park - -foo: - bar: [] diff --git a/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/comments-complex.yml b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/comments-complex.yml new file mode 100644 index 000000000..bd9288afa --- /dev/null +++ b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/comments-complex.yml @@ -0,0 +1,11 @@ +# Header +# This file contains a more complex example +# to test comment reading. + +core: + users: + # comment on list entry + - # ambiguous comment + first: one + # very mapping + second: two diff --git a/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/essx-example.yml b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/essx-example.yml new file mode 100644 index 000000000..b9668a823 --- /dev/null +++ b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/essx-example.yml @@ -0,0 +1,1171 @@ +############################################################ +# +------------------------------------------------------+ # +# | Notes | # +# +------------------------------------------------------+ # +############################################################ + +# This is the config file for EssentialsX. +# This config was generated for version 2.19.0-dev+227-87a6cf7. + +# If you want to use special characters in this document, such as accented letters, you MUST save the file as UTF-8, not ANSI. +# If you receive an error when Essentials loads, ensure that: +# - No tabs are present: YAML only allows spaces +# - Indents are correct: YAML hierarchy is based entirely on indentation +# - You have "escaped" all apostrophes in your text: If you want to write "don't", for example, write "don''t" instead (note the doubled apostrophe) +# - Text with symbols is enclosed in single or double quotation marks + +# If you need help, you can join the EssentialsX community: https://essentialsx.net/community.html + +############################################################ +# +------------------------------------------------------+ # +# | Essentials (Global) | # +# +------------------------------------------------------+ # +############################################################ + +# A color code between 0-9 or a-f. Set to 'none' to disable. +# In 1.16+ you can use hex color codes here as well. (For example, #613e1d is brown). +ops-name-color: '4' + +# The character(s) to prefix all nicknames, so that you know they are not true usernames. +nickname-prefix: '~' + +# The maximum length allowed in nicknames. The nickname prefix is included in this. +max-nick-length: 15 + +# A list of phrases that cannot be used in nicknames. You can include regular expressions here. +# Users with essentials.nick.blacklist.bypass will be able to bypass this filter. +nick-blacklist: +# - Notch +# - '^Dinnerbone' + +# When this option is enabled, nickname length checking will exclude color codes in player names. +# ie: "&6Notch" has 7 characters (2 are part of a color code), a length of 5 is used when this option is set to true +ignore-colors-in-max-nick-length: false + +# When this option is enabled, display names for hidden users will not be shown. This prevents players from being +# able to see that they are online while vanished. +hide-displayname-in-vanish: true + +# Disable this if you have any other plugin, that modifies the displayname of a user. +change-displayname: true + +# When this option is enabled, the (tab) player list will be updated with the displayname. +# The value of change-displayname (above) has to be true. +# change-playerlist: true + +# When EssentialsChat.jar isn't used, force essentials to add the prefix and suffix from permission plugins to displayname. +# This setting is ignored if EssentialsChat.jar is used, and defaults to 'true'. +# The value of change-displayname (above) has to be true. +# Do not edit this setting unless you know what you are doing! +# add-prefix-suffix: false + +# When this option is enabled, player prefixes will be shown in the playerlist. +# This feature only works for Minecraft version 1.8 and higher. +# This value of change-playerlist has to be true +# add-prefix-in-playerlist: true + +# When this option is enabled, player suffixes will be shown in the playerlist. +# This feature only works for Minecraft version 1.8 and higher. +# This value of change-playerlist has to be true +# add-suffix-in-playerlist: true + +# If the teleport destination is unsafe, should players be teleported to the nearest safe location? +# If this is set to true, Essentials will attempt to teleport players close to the intended destination. +# If this is set to false, attempted teleports to unsafe locations will be cancelled with a warning. +teleport-safety: true + +# This forcefully disables teleport safety checks without a warning if attempting to teleport to unsafe locations. +# teleport-safety and this option need to be set to true to force teleportation to dangerous locations. +force-disable-teleport-safety: false + +# If a player is teleporting to an unsafe location in creative, adventure, or god mode; they will not be teleported to a +# safe location. If you'd like players to be teleported to a safe location all of the time, set this option to true. +force-safe-teleport-location: false + +# If a player has any passengers, the teleport will fail. Should their passengers be dismounted before they are teleported? +# If this is set to true, Essentials will dismount the player's passengers before teleporting. +# If this is set to false, attempted teleports will be canceled with a warning. +teleport-passenger-dismount: true + +# The delay, in seconds, required between /home, /tp, etc. +teleport-cooldown: 0 + +# The delay, in seconds, before a user actually teleports. If the user moves or gets attacked in this timeframe, the teleport is cancelled. +teleport-delay: 0 + +# The delay, in seconds, a player can't be attacked by other players after they have been teleported by a command. +# This will also prevent the player attacking other players. +teleport-invulnerability: 4 + +# Whether to make all teleportations go to the center of the block; where the x and z coordinates decimal become .5 +teleport-to-center: true + +# The delay, in seconds, required between /heal or /feed attempts. +heal-cooldown: 60 + +# Do you want to remove potion effects when healing a player? +remove-effects-on-heal: true + +# Near Radius +# The default radius with /near +# Used to use chat radius but we are going to make it separate. +near-radius: 200 + +# What to prevent from /item and /give. +# e.g item-spawn-blacklist: 10,11,46 +item-spawn-blacklist: + +# Set this to true if you want permission based item spawn rules. +# Note: The blacklist above will be ignored then. +# Example permissions (these go in your permissions manager): +# - essentials.itemspawn.item-all +# - essentials.itemspawn.item-[itemname] +# - essentials.itemspawn.item-[itemid] +# - essentials.give.item-all +# - essentials.give.item-[itemname] +# - essentials.give.item-[itemid] +# - essentials.unlimited.item-all +# - essentials.unlimited.item-[itemname] +# - essentials.unlimited.item-[itemid] +# - essentials.unlimited.item-bucket # Unlimited liquid placing + +# For more information, visit http://wiki.ess3.net/wiki/Command_Reference/ICheat#Item.2FGive +permission-based-item-spawn: false + +# Mob limit on the /spawnmob command per execution. +spawnmob-limit: 10 + +# Shall we notify users when using /lightning? +warn-on-smite: true + +# Shall we drop items instead of adding to inventory if the target inventory is full? +drop-items-if-full: false + +# Essentials Mail Notification +# Should we notify players if they have no new mail? +notify-no-new-mail: true + +# Specifies the duration (in seconds) between each time a player is notified of mail they have. +# Useful for servers with a lot of mail traffic. +notify-player-of-mail-cooldown: 60 + +# The motd and rules are now configured in the files motd.txt and rules.txt. + +# When a command conflicts with another plugin, by default, Essentials will try to force the OTHER plugin to take priority. +# Commands in this list, will tell Essentials to 'not give up' the command to other plugins. +# In this state, which plugin 'wins' appears to be almost random. +# +# If you have two plugin with the same command and you wish to force Essentials to take over, you need an alias. +# To force essentials to take 'god' alias 'god' to 'egod'. +# See http://wiki.bukkit.org/Commands.yml#aliases for more information. + +overridden-commands: +# - god +# - info + +# Disabling commands here will prevent Essentials handling the command, this will not affect command conflicts. +# You should not have to disable commands used in other plugins, they will automatically get priority. +# See http://wiki.bukkit.org/Commands.yml#aliases to map commands to other plugins. +disabled-commands: +# - nick +# - clear + +# These commands will be shown to players with socialSpy enabled. +# You can add commands from other plugins you may want to track or +# remove commands that are used for something you dont want to spy on. +# Set - '*' in order to listen on all possible commands. +socialspy-commands: + - msg + - w + - r + - mail + - m + - t + - whisper + - emsg + - tell + - er + - reply + - ereply + - email + - action + - describe + - eme + - eaction + - edescribe + - etell + - ewhisper + - pm + +# Whether the private and public messages from muted players should appear in the social spy. +# If so, they will be differentiated from those sent by normal players. +socialspy-listen-muted-players: true + +# Whether social spy should spy on private messages or just the commands from the list above. +# If false, social spy will only monitor commands from the list above. +socialspy-messages: true + +# The following settings listen for when a player changes worlds. +# If you use another plugin to control speed and flight, you should change these to false. + +# When a player changes world, should EssentialsX reset their flight? +# This will disable flight if the player does not have essentials.fly. +world-change-fly-reset: true + +# When a player changes world, should we reset their speed according to their permissions? +# This resets the player's speed to the default if they don't have essentials.speed. +# If the player doesn't have essentials.speed.bypass, this resets their speed to the maximum specified above. +world-change-speed-reset: true + +# Mute Commands +# These commands will be disabled when a player is muted. +# Use '*' to disable every command. +# Essentials already disabled Essentials messaging commands by default. +# It only cares about the root command, not args after that (it sees /f chat the same as /f) +mute-commands: + - f + - kittycannon + # - '*' + +# If you do not wish to use a permission system, you can define a list of 'player perms' below. +# This list has no effect if you are using a supported permissions system. +# If you are using an unsupported permissions system, simply delete this section. +# Whitelist the commands and permissions you wish to give players by default (everything else is op only). +# These are the permissions without the "essentials." part. +# +# To enable this feature, please set use-bukkit-permissions to false. +player-commands: + - afk + - afk.auto + - back + - back.ondeath + - balance + - balance.others + - balancetop + - build + - chat.color + - chat.format + - chat.shout + - chat.question + - clearinventory + - compass + - depth + - delhome + - getpos + - geoip.show + - help + - helpop + - home + - home.others + - ignore + - info + - itemdb + - kit + - kits.tools + - list + - mail + - mail.send + - me + - motd + - msg + - msg.color + - nick + - near + - pay + - ping + - protect + - r + - rules + - realname + - seen + - sell + - sethome + - setxmpp + - signs.create.protection + - signs.create.trade + - signs.break.protection + - signs.break.trade + - signs.use.balance + - signs.use.buy + - signs.use.disposal + - signs.use.enchant + - signs.use.free + - signs.use.gamemode + - signs.use.heal + - signs.use.info + - signs.use.kit + - signs.use.mail + - signs.use.protection + - signs.use.repair + - signs.use.sell + - signs.use.time + - signs.use.trade + - signs.use.warp + - signs.use.weather + - spawn + - suicide + - time + - tpa + - tpaccept + - tpahere + - tpdeny + - warp + - warp.list + - world + - worth + - xmpp + +# Use this option to force superperms-based permissions handler regardless of detected installed perms plugin. +# This is useful if you want superperms-based permissions (with wildcards) for custom permissions plugins. +# If you wish to use EssentialsX's built-in permissions using the `player-commands` section above, set this to false. +# Default is true. +use-bukkit-permissions: true + +# When this option is enabled, one-time use kits (ie. delay < 0) will be +# removed from the /kit list when a player can no longer use it +skip-used-one-time-kits-from-kit-list: false + +# When enabled, armor from kits will automatically be equipped as long as the player's armor slots are empty. +kit-auto-equip: false + +# Determines the functionality of the /createkit command. +# If this is true, /createkit will give the user a link with the kit code. +# If this is false, /createkit will add the kit to the kits.yml config file directly. +# Default is false. +pastebin-createkit: false + +# Determines if /createkit will generate kits using NBT item serialization. +# If this is true, /createkit will store items as NBT; otherwise, it will use Essentials' human-readable item format. +# By using NBT serialization, /createkit can store items with complex metadata such as shulker boxes and weapons with custom attributes. +# WARNING: This option only works on 1.15.2+ Paper servers, and it will bypass any custom serializers from other plugins such as Magic. +# WARNING: When creating kits via /createkit with this option enabled, you will not be able to downgrade your server with these kit items. +# This option only affects /createkit - you can still create kits by hand in `kits.yml` using Essentials' human-readable item format. +# Default is false. +use-nbt-serialization-in-createkit: false + +# Essentials Sign Control +# See http://wiki.ess3.net/wiki/Sign_Tutorial for instructions on how to use these. +# To enable signs, remove # symbol. To disable all signs, comment/remove each sign. +# Essentials colored sign support will be enabled when any sign types are enabled. +# Color is not an actual sign, it's for enabling using color codes on signs, when the correct permissions are given. + +enabledSigns: + # - color + # - balance + # - buy + # - sell + # - trade + # - free + # - warp + # - kit + # - mail + # - enchant + # - gamemode + # - heal + # - info + # - spawnmob + # - repair + # - time + # - weather + # - anvil + # - cartography + # - disposal + # - grindstone + # - loom + # - smithing + # - workbench + +# How many times per second can Essentials signs be interacted with per player. +# Values should be between 1-20, 20 being virtually no lag protection. +# Lower numbers will reduce the possibility of lag, but may annoy players. +sign-use-per-second: 4 + +# Allow item IDs on pre-existing signs on 1.13 and above. +# You cannot use item IDs on new signs, but this will allow players to interact with signs that +# were placed before 1.13. +allow-old-id-signs: false + +# List of sign names Essentials should not protect. This feature is especially useful when +# another plugin provides a sign that EssentialsX provides, but Essentials overrides. +# For example, if a plugin provides a [kit] sign, and you wish to use theirs instead of +# Essentials's, then simply add kit below and Essentials will not protect it. +# +# See https://github.com/drtshock/Essentials/pull/699 for more information. +unprotected-sign-names: + # - kit + +# Backup runs a custom batch/bash command at a specified interval. +# The server will save the world before executing the backup command, and disable +# saving during the backup to prevent world corruption or other conflicts. +# Backups can also be triggered manually with /backup. +backup: + # Interval in minutes. + interval: 30 + + # If true, the backup task will run even if there are no players online. + always-run: false + + # Unless you add a valid backup command or script here, this feature will be useless. + # Use 'save-all' to simply force regular world saving without backup. + # The example command below utilizes rdiff-backup: https://rdiff-backup.net/ + # command: 'rdiff-backup World1 backups/World1' + +# Set this true to enable permission per warp. +per-warp-permission: false + +# Sort output of /list command by groups. +# You can hide and merge the groups displayed in /list by defining the desired behaviour here. +# Detailed instructions and examples can be found on the wiki: http://wiki.ess3.net/wiki/List +list: + # To merge groups, list the groups you wish to merge + # Staff: owner admin moderator + Admins: owner admin + # To limit groups, set a max user limit + # builder: 20 + # To hide groups, set the group as hidden + # default: hidden + # Uncomment the line below to simply list all players with no grouping + # Players: '*' + +# Displays real names in /list next to players who are using a nickname. +real-names-on-list: false + +# More output to the console. +debug: false + +# Set the locale for all messages. +# If you don't set this, the default locale of the server will be used. +# For example, to set language to English, set locale to en, to use the file "messages_en.properties". +# Don't forget to remove the # in front of the line. +# For more information, visit http://wiki.ess3.net/wiki/Locale +# locale: en + +# Turn off god mode when people leave the server. +remove-god-on-disconnect: false + +# Auto-AFK +# After this timeout in seconds, the user will be set as AFK. +# This feature requires the player to have essentials.afk.auto node. +# Set to -1 for no timeout. +auto-afk: 300 + +# Auto-AFK Kick +# After this timeout in seconds, the user will be kicked from the server. +# essentials.afk.kickexempt node overrides this feature. +# Set to -1 for no timeout. +auto-afk-kick: -1 + +# Set this to true, if you want to freeze the player, if the player is AFK. +# Other players or monsters can't push the player out of AFK mode then. +# This will also enable temporary god mode for the AFK player. +# The player has to use the command /afk to leave the AFK mode. +freeze-afk-players: false + +# When the player is AFK, should he be able to pickup items? +# Enable this, when you don't want people idling in mob traps. +disable-item-pickup-while-afk: false + +# This setting controls if a player is marked as active on interaction. +# When this setting is false, the player would need to manually un-AFK using the /afk command. +cancel-afk-on-interact: true + +# Should we automatically remove afk status when a player moves? +# Player will be removed from AFK on chat/command regardless of this setting. +# Disable this to reduce server lag. +cancel-afk-on-move: true + +# Should we automatically remove afk status when a player sends a chat message? +cancel-afk-on-chat: true + +# Should AFK players be ignored when other players are trying to sleep? +# When this setting is false, players won't be able to skip the night if some players are AFK. +# Users with the permission node essentials.sleepingignored will always be ignored. +sleep-ignores-afk-players: true + +# Should vanished players be ignored when other players are trying to sleep? +# When this setting is false, player's won't be able to skip the night if vanished players are not sleeping. +# Users with the permission node essentials.sleepingignored will always be ignored. +sleep-ignores-vanished-player: true + +# Set the player's list name when they are AFK. This is none by default which specifies that Essentials +# should not interfere with the AFK player's list name. +# You may use color codes, use {USERNAME} the player's name or {PLAYER} for the player's displayname. +afk-list-name: "none" + +# When a player enters or exits AFK mode, should the AFK notification be broadcast +# to the entire server, or just to the player? +# When this setting is false, only the player will be notified upon changing their AFK state. +broadcast-afk-message: true + +# You can disable the death messages of Minecraft here. +death-messages: true + +# How should essentials handle players with the essentials.keepinv permission who have items with +# curse of vanishing when they die? +# You can set this to "keep" (to keep the item), "drop" (to drop the item), or "delete" (to delete the item). +# Defaults to "keep" +vanishing-items-policy: keep + +# How should essentials handle players with the essentials.keepinv permission who have items with +# curse of binding when they die? +# You can set this to "keep" (to keep the item), "drop" (to drop the item), or "delete" (to delete the item). +# Defaults to "keep" +binding-items-policy: keep + +# When players die, should they receive the coordinates they died at? +send-info-after-death: false + +# Should players with permissions be able to join and part silently? +# You can control this with essentials.silentjoin and essentials.silentquit permissions if it is enabled. +# In addition, people with essentials.silentjoin.vanish will be vanished on join. +allow-silent-join-quit: false + +# You can set custom join and quit messages here. Set this to "none" to use the default Minecraft message, +# or set this to "" to hide the message entirely. + +# Available placeholders: +# {PLAYER} - The player's displayname. +# {USERNAME} - The player's username. +# {PREFIX} - The player's prefix. +# {SUFFIX} - The player's suffix. +# {ONLINE} - The number of players online. +# {UNIQUE} - The number of unique players to join the server. +# {UPTIME} - The amount of time the server has been online. +custom-join-message: "none" +custom-quit-message: "none" + +# You can set a custom join message for users who join with a new username here. +# This message will only be used if a user has joined before and have since changed their username. +# This will be displayed INSTEAD OF custom-join-message, so if you intend to keep them similar, make sure they match. +# Set this to "none" to use the the "custom-join-message" above for every join. + +# Available placeholders: +# {PLAYER} - The player's displayname. +# {USERNAME} - The player's username. +# {OLDUSERNAME} - The player's old username. +# {PREFIX} - The player's prefix. +# {SUFFIX} - The player's suffix. +# {ONLINE} - The number of players online. +# {UNIQUE} - The number of unique players to join the server. +# {UPTIME} - The amount of time the server has been online. +custom-new-username-message: "none" + +# Should Essentials override the vanilla "Server Full" message with its own from the language file? +# Set to false to keep the vanilla message. +use-custom-server-full-message: true + +# You can disable join and quit messages when the player count reaches a certain limit. +# When the player count is below this number, join/quit messages will always be shown. +# Set this to -1 to always show join and quit messages regardless of player count. +hide-join-quit-messages-above: -1 + +# Add worlds to this list, if you want to automatically disable god mode there. +no-god-in-worlds: +# - world_nether + +# Set to true to enable per-world permissions for teleporting between worlds with essentials commands. +# This applies to /world, /back, /tp[a|o][here|all], but not warps. +# Give someone permission to teleport to a world with essentials.worlds. +# This does not affect the /home command, there is a separate toggle below for this. +world-teleport-permissions: false + +# The number of items given if the quantity parameter is left out in /item or /give. +# If this number is below 1, the maximum stack size size is given. If over-sized stacks. +# are not enabled, any number higher than the maximum stack size results in more than one stack. +default-stack-size: -1 + +# Over-sized stacks are stacks that ignore the normal max stack size. +# They can be obtained using /give and /item, if the player has essentials.oversizedstacks permission. +# How many items should be in an over-sized stack? +oversized-stacksize: 64 + +# Allow repair of enchanted weapons and armor. +# If you set this to false, you can still allow it for certain players using the permission. +# essentials.repair.enchanted +repair-enchanted: true + +# Allow 'unsafe' enchantments in kits and item spawning. +# Warning: Mixing and overleveling some enchantments can cause issues with clients, servers and plugins. +unsafe-enchantments: false + +# Do you want Essentials to keep track of previous location for /back in the teleport listener? +# If you set this to true any plugin that uses teleport will have the previous location registered. +register-back-in-listener: false + +# Delay to wait before people can cause attack damage after logging in. +login-attack-delay: 5 + +# Set the max fly speed, values range from 0.1 to 1.0 +max-fly-speed: 0.8 + +# Set the max walk speed, values range from 0.1 to 1.0 +max-walk-speed: 0.8 + +# Set the maximum amount of mail that can be sent within a minute. +mails-per-minute: 1000 + +# Set the maximum time /mute can be used for in seconds. +# Set to -1 to disable, and essentials.mute.unlimited can be used to override. +max-mute-time: -1 + +# Set the maximum time /tempban can be used for in seconds. +# Set to -1 to disable, and essentials.tempban.unlimited can be used to override. +max-tempban-time: -1 + +# Changes the default /reply functionality. This can be changed on a per-player basis using /rtoggle. +# If true, /r goes to the person you messaged last, otherwise the first person that messaged you. +# If false, /r goes to the last person that messaged you. +last-message-reply-recipient: true + +# If last-message-reply-recipient is enabled for a particular player, +# this specifies the duration, in seconds, that would need to elapse for the +# reply-recipient to update when receiving a message. +# Default is 180 (3 minutes) +last-message-reply-recipient-timeout: 180 + +# Changes the default /reply functionality. +# If true, /reply will not check if the person you're replying to has vanished. +# If false, players will not be able to /reply to players who they can no longer see due to vanish. +last-message-reply-vanished: false + +# Toggles whether or not left clicking mobs with a milk bucket turns them into a baby. +milk-bucket-easter-egg: true + +# Toggles whether or not the fly status message should be sent to players on join +send-fly-enable-on-join: true + +# Set to true to enable per-world permissions for setting time for individual worlds with essentials commands. +# This applies to /time, /day, /eday, /night, /enight, /etime. +# Give someone permission to teleport to a world with essentials.time.world.. +world-time-permissions: false + +# Specify cooldown for both Essentials commands and external commands as well. +# All commands do not start with a Forward Slash (/). Instead of /msg, write msg +# +# Wildcards are supported. E.g. +# - '*i*': 50 +# adds a 50 second cooldown to all commands that include the letter i +# +# EssentialsX supports regex by starting the command with a caret ^ +# For example, to target commands starting with ban and not banip the following would be used: +# '^ban([^ip])( .*)?': 60 # 60 seconds /ban cooldown. +# Note: If you have a command that starts with ^, then you can escape it using backslash (\). e.g. \^command: 123 +command-cooldowns: +# feed: 100 # 100 second cooldown on /feed command +# '*': 5 # 5 Second cooldown on all commands + +# Whether command cooldowns should be persistent past server shutdowns +command-cooldown-persistence: true + +# Whether NPC balances should be listed in balance ranking features such as /balancetop. +# NPC balances can include features like factions from FactionsUUID plugin. +npcs-in-balance-ranking: false + +# Allow bulk buying and selling signs when the player is sneaking. +# This is useful when a sign sells or buys one item at a time and the player wants to sell a bunch at once. +allow-bulk-buy-sell: true + +# Allow selling of items with custom names with the /sell command. +# This may be useful to prevent players accidentally selling named items. +allow-selling-named-items: false + +# Delay for the MOTD display for players on join, in milliseconds. +# This has no effect if the MOTD command or permission are disabled. +delay-motd: 0 + +# A list of commands that should have their complementary confirm commands enabled by default. +# This is empty by default, for the latest list of valid commands see the latest source config.yml. +default-enabled-confirm-commands: +# - pay +# - clearinventory + +# Where should Essentials teleport players when they are freed from jail? +# You can set to "back" to have them teleported to where they were before they were jailed, "spawn" to have them +# teleport to spawn, or "off" to not have them teleport. +teleport-when-freed: back + +# Whether or not jail time should only be counted while the user is online. +# If true, a jailed player's time will only decrement when they are online. +jail-online-time: false + +# Set the timeout, in seconds for players to accept a tpa before the request is cancelled. +# Set to 0 for no timeout. +tpa-accept-cancellation: 120 + +# Allow players to set hats by clicking on their helmet slot. +allow-direct-hat: true + +# Allow in-game players to specify a world when running /broadcastworld. +# If false, running /broadcastworld in-game will always send a message to the player's current world. +# This doesn't affect running the command from the console, where a world is always required. +allow-world-in-broadcastworld: true + +# Consider water blocks as "safe," therefore allowing players to teleport +# using commands such as /home or /spawn to a location that is occupied +# by water blocks +is-water-safe: false + +# Should the usermap try to sanitise usernames before saving them? +# You should only change this to false if you use Minecraft China. +safe-usermap-names: true + +# Should Essentials output logs when a command block executes a command? +# Example: CommandBlock at ,, issued server command: / +log-command-block-commands: true + +# Set the maximum speed for projectiles spawned with /fireball. +max-projectile-speed: 8 + +# Should EssentialsX check for updates? +# If set to true, EssentialsX will show notifications when a new version is available. +# This uses the public GitHub API and no identifying information is sent or stored. +update-check: true + +############################################################ +# +------------------------------------------------------+ # +# | Homes | # +# +------------------------------------------------------+ # +############################################################ + +# Allows people to set their bed during the day. +# This setting has no effect in Minecraft 1.15+, as Minecraft will always allow the player to set their bed location during the day. +update-bed-at-daytime: true + +# Set to true to enable per-world permissions for using homes to teleport between worlds. +# This applies to the /home command only. +# Give someone permission to teleport to a world with essentials.worlds. +world-home-permissions: false + +# Allow players to have multiple homes. +# Players need essentials.sethome.multiple before they can have more than 1 home. +# You can set the default number of multiple homes using the 'default' rank below. +# To remove the home limit entirely, give people 'essentials.sethome.multiple.unlimited'. +# To grant different home amounts to different people, you need to define a 'home-rank' below. +# Create the 'home-rank' below, and give the matching permission: essentials.sethome.multiple. +# For more information, visit http://wiki.ess3.net/wiki/Multihome +sethome-multiple: + default: 3 + vip: 5 + staff: 10 + +# In this example someone with 'essentials.sethome.multiple' and 'essentials.sethome.multiple.vip' will have 5 homes. +# Remember, they MUST have both permission nodes in order to be able to set multiple homes. + +# Controls whether players need the permission "essentials.home.compass" in order to point +# the player's compass at their first home. + +# Leaving this as false will retain Essentials' original behaviour, which is to always +# change the compass' direction to point towards their first home. +compass-towards-home-perm: false + +# If no home is set, would you like to send the player to spawn? +# If set to false, players will not be teleported when they run /home without setting a home first. +spawn-if-no-home: true + +# Should players be asked to provide confirmation for homes which they attempt to overwrite? +confirm-home-overwrite: false + +############################################################ +# +------------------------------------------------------+ # +# | Economy | # +# +------------------------------------------------------+ # +############################################################ + +# For more information, visit http://wiki.ess3.net/wiki/Essentials_Economy + +# You can control the values of items that are sold to the server by using the /setworth command. + +# Defines the balance with which new players begin. Defaults to 0. +starting-balance: 0 + +# Defines the cost to use the given commands PER USE. +# Some commands like /repair have sub-costs, check the wiki for more information. +command-costs: + # /example costs $1000 PER USE + # example: 1000 + # /kit tools costs $1500 PER USE + # kit-tools: 1500 + +# Set this to a currency symbol you want to use. +# Remember, if you want to use special characters in this document, +# such as accented letters, you MUST save the file as UTF-8, not ANSI. +currency-symbol: '$' + +# Enable this to make the currency symbol appear at the end of the amount rather than at the start. +# For example, the euro symbol typically appears after the current amount. +currency-symbol-suffix: false + +# Set the maximum amount of money a player can have. +# The amount is always limited to 10 trillion because of the limitations of a java double. +max-money: 10000000000000 + +# Set the minimum amount of money a player can have (must be above the negative of max-money). +# Setting this to 0, will disable overdrafts/loans completely. Users need 'essentials.eco.loan' perm to go below 0. +min-money: -10000 + +# Enable this to log all interactions with trade/buy/sell signs and sell command. +economy-log-enabled: false + +# Enable this to also log all transactions from other plugins through Vault. +# This can cause the economy log to fill up quickly so should only be enabled for testing purposes! +economy-log-update-enabled: false + +# Minimum acceptable amount to be used in /pay. +minimum-pay-amount: 0.001 + +# Enable this to block users who try to /pay another user which ignore them. +pay-excludes-ignore-list: false + +# Whether or not users with a balance less than or equal to $0 should be shown in balance-top. +# Setting to false will not show people with balances <= 0 in balance-top. +# NOTE: After reloading the config, you must also run '/baltop force' for this to appear +show-zero-baltop: true + +# The format of currency, excluding symbols. See currency-symbol-format-locale for symbol configuration. + +# "#,##0.00" is how the majority of countries display currency. +# currency-format: "#,##0.00" + +# Format currency symbols. Some locales use , and . interchangeably. +# Some formats do not display properly in-game due to faulty Minecraft font rendering. + +# For 1.234,50 use de-DE +# For 1,234.50 use en-US +# For 1'234,50 use fr-ch +# currency-symbol-format-locale: en-US + +############################################################ +# +------------------------------------------------------+ # +# | Help | # +# +------------------------------------------------------+ # +############################################################ + +# Show other plugins commands in help. +non-ess-in-help: true + +# Hide plugins which do not give a permission. +# You can override a true value here for a single plugin by adding a permission to a user/group. +# The individual permission is: essentials.help., anyone with essentials.* or '*' will see all help regardless. +# You can use negative permissions to remove access to just a single plugins help if the following is enabled. +hide-permissionless-help: true + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsX Chat | # +# +------------------------------------------------------+ # +############################################################ + +# You need to install EssentialsX Chat for this section to work. +# See https://essentialsx.net/wiki/Module-Breakdown.html for more information. + +chat: + + # If EssentialsX Chat is installed, this will define how far a player's voice travels, in blocks. Set to 0 to make all chat global. + # Note that users with the "essentials.chat.spy" permission will hear everything, regardless of this setting. + # Users with essentials.chat.shout can override this by prefixing their message with an exclamation mark (!) + # Users with essentials.chat.question can override this by prefixing their message with a question mark (?) + # You can add command costs for shout/question by adding chat-shout and chat-question to the command costs section. + radius: 0 + + # Chat formatting can be done in two ways, you can either define a standard format for all chat. + # Or you can give a group specific chat format, to give some extra variation. + # For more information of chat formatting, check out the wiki: http://wiki.ess3.net/wiki/Chat_Formatting + # Note: Using the {PREFIX} and {SUFFIX} placeholders along with {DISPLAYNAME} may cause double prefixes/suffixes to be shown in chat unless add-prefix-suffix is uncommented and set to false. + + # Available placeholders: + # {MESSAGE} - The content of the chat message. + # {USERNAME} - The sender's username. + # {DISPLAYNAME} - The sender's display name. + # {NICKNAME} - The sender's Essentials nickname. If the sender has no nickname, the username is shown. + # {PREFIX} - The sender's prefix, supplied by a permissions plugin. + # {SUFFIX} - The sender's suffix, supplied by a permissions plugin. + # {GROUP} - The sender's primary group name, supplied by a permissions plugin. + # {WORLD} - The world alias of the sender's current world. See the world-aliases section below for details. + # {WORLDNAME} - The full name of the sender's current world. + # {SHORTWORLDNAME} - The first character of the sender's current world. + # {TEAMNAME} - The sender's scoreboard team name. + # {TEAMPREFIX} - The sender's scoreboard team prefix. + # {TEAMSUFFIX} - The sender's scoreboard team suffix. + + format: '<{DISPLAYNAME}> {MESSAGE}' + #format: '&7[{GROUP}]&r {DISPLAYNAME}&7:&r {MESSAGE}' + #format: '&7{PREFIX}&r {DISPLAYNAME}&r &7{SUFFIX}&r: {MESSAGE}' + + group-formats: + # default: '{WORLDNAME} {DISPLAYNAME}&7:&r {MESSAGE}' + # admins: '{WORLDNAME} &c[{GROUP}]&r {DISPLAYNAME}&7:&c {MESSAGE}' + + # If you are using group formats make sure to remove the '#' to allow the setting to be read. + # Note: Group names are case-sensitive so you must match them up with your permission plugin. + + # You can use permissions to control whether players can use formatting codes in their chat messages. + # See https://essentialsx.net/wiki/Color-Permissions.html for more information. + + # World aliases allow you to replace the world name with something different in the chat format. + # If you are using world aliases, make sure to remove the '#' at the start to allow the setting to be read. + world-aliases: + # plots: "&dP&r" + # creative: "&eC&r" + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsX Protect | # +# +------------------------------------------------------+ # +############################################################ + +# You need to install EssentialsX Protect for this section to work. +# See https://essentialsx.net/wiki/Module-Breakdown.html for more information. +protect: + # General physics/behavior modifications. Set these to true to disable behaviours. + prevent: + lava-flow: false + water-flow: false + water-bucket-flow: false + fire-spread: true + lava-fire-spread: true + lava-itemdamage: false + flint-fire: false + lightning-fire-spread: true + portal-creation: false + tnt-explosion: false + tnt-playerdamage: false + tnt-itemdamage: false + tnt-minecart-explosion: false + tnt-minecart-playerdamage: false + tnt-minecart-itemdamage: false + fireball-explosion: false + fireball-fire: false + fireball-playerdamage: false + fireball-itemdamage: false + witherskull-explosion: false + witherskull-playerdamage: false + witherskull-itemdamage: false + wither-spawnexplosion: false + wither-blockreplace: false + creeper-explosion: false + creeper-playerdamage: false + creeper-itemdamage: false + creeper-blockdamage: false + ender-crystal-explosion: false + enderdragon-blockdamage: true + enderman-pickup: false + villager-death: false + bed-explosion: false + respawn-anchor-explosion: false + # Monsters won't follow players. + # permission essentials.protect.entitytarget.bypass disables this. + entitytarget: false + + # Prevents zombies from breaking down doors + zombie-door-break: false + + # Prevents Ravagers from stealing blocks + ravager-thief: false + + # Prevents sheep from turning grass to dirt + sheep-eat-grass: false + + # Prevent certain transformations. + transformation: + # Prevent creepers becoming charged when struck by lightning. + charged-creeper: false + + # Prevent villagers becoming zombie villagers. + zombie-villager: false + + # Prevent zombie villagers being cured. + villager: false + + # Prevent villagers becoming witches when struck by lightning. + witch: false + + # Prevent pigs becoming zombie pigmen when struck by lightning. + zombie-pigman: false + + # Prevent zombies turning into drowneds, and husks turning into zombies. + drowned: false + + # Prevent mooshrooms changing colour when struck by lightning. + mooshroom: false + + # Prevent the spawning of creatures. If a creature is missing, you can add it following the format below. + spawn: + creeper: false + skeleton: false + spider: false + giant: false + zombie: false + slime: false + ghast: false + pig_zombie: false + enderman: false + cave_spider: false + silverfish: false + blaze: false + magma_cube: false + ender_dragon: false + pig: false + sheep: false + cow: false + chicken: false + squid: false + wolf: false + mushroom_cow: false + snowman: false + ocelot: false + iron_golem: false + villager: false + wither: false + bat: false + witch: false + horse: false + phantom: false + + # Maximum height the creeper should explode. -1 allows them to explode everywhere. + # Set prevent.creeper-explosion to true, if you want to disable creeper explosions. + creeper: + max-height: -1 + + # Disable various default physics and behaviors. + disable: + # Should fall damage be disabled? + fall: false + + # Users with the essentials.protect.pvp permission will still be able to attack each other if this is set to true. + # They will be unable to attack users without that same permission node. + pvp: false + + # Should drowning damage be disabled? + # (Split into two behaviors; generally, you want both set to the same value.) + drown: false + suffocate: false + + # Should damage via lava be disabled? Items that fall into lava will still burn to a crisp. ;) + lavadmg: false + + # Should arrow damage be disabled? + projectiles: false + + # This will disable damage from touching cacti. + contactdmg: false + + # Burn, baby, burn! Should fire damage be disabled? + firedmg: false + + # Should the damage after hit by a lightning be disabled? + lightning: false + + # Should Wither damage be disabled? + wither: false + + # Disable weather options? + weather: + storm: false + thunder: false + lightning: false + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsX AntiBuild | # +# +------------------------------------------------------+ # +############################################################ + + # You need to install EssentialsX AntiBuild for this section to work. + # See https://essentialsx.net/wiki/Module-Breakdown.html and http://wiki.ess3.net/wiki/AntiBuild for more information. + + # Should people without the essentials.build permission be allowed to build? + # Set true to disable building for those people. + # Setting to false means EssentialsAntiBuild will never prevent you from building. + build: true + + # Should people without the essentials.build permission be allowed to use items? + # Set true to disable using for those people. + # Setting to false means EssentialsAntiBuild will never prevent you from using items. + use: true + + # Should we warn people when they are not allowed to build? + warn-on-build-disallow: true + + # For which block types would you like to be alerted? + # You can find a list of items at https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html. + alert: + on-placement: LAVA,TNT,LAVA_BUCKET + on-use: LAVA_BUCKET + on-break: + + blacklist: + + # Which blocks should people be prevented from placing? + placement: LAVA,TNT,LAVA_BUCKET + + # Which items should people be prevented from using? + usage: LAVA_BUCKET + + # Which blocks should people be prevented from breaking? + break: + + # Which blocks should not be moved by pistons? + piston: + + # Which blocks should not be dispensed by dispensers + dispenser: + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsX Spawn + New Players | # +# +------------------------------------------------------+ # +############################################################ + +# You need to install EssentialsX Spawn for this section to work. +# See https://essentialsx.net/wiki/Module-Breakdown.html for more information. + +newbies: + # Should we announce to the server when someone logs in for the first time? + # If so, use this format, replacing {DISPLAYNAME} with the player name. + # If not, set to '' + # announce-format: '' + announce-format: '&dWelcome {DISPLAYNAME}&d to the server!' + + # When we spawn for the first time, which spawnpoint do we use? + # Set to "none" if you want to use the spawn point of the world. + spawnpoint: newbies + + # Do we want to give users anything on first join? Set to '' to disable + # This kit will be given regardless of cost and permissions, and will not trigger the kit delay. + # kit: '' + kit: tools + +# What priority should we use for handling respawns? +# Set this to none, if you want vanilla respawning behaviour. +# Set this to lowest, if you want Multiverse to handle the respawning. +# Set this to high, if you want EssentialsSpawn to handle the respawning. +# Set this to highest, if you want to force EssentialsSpawn to handle the respawning. +# Note: Changes will not apply until after the server is restarted. +respawn-listener-priority: high + +# What priority should we use for handling spawning on joining the server? +# See respawn-listener-priority for possible values. +# Note: Changing this may impact or break spawn-on-join functionality. +# Note: Changes will not apply until after the server is restarted. +spawn-join-listener-priority: high + +# When users die, should they respawn at their first home or bed, instead of the spawnpoint? +respawn-at-home: false + +# When users die, should they respawn at their bed instead of the spawnpoint? +# The value of respawn-at-home (above) has to be true. +respawn-at-home-bed: true + +# When users die, should EssentialsSpawn respect users' respawn anchors? +respawn-at-anchor: false + +# Teleport all joining players to the spawnpoint +spawn-on-join: false +# The following value of `guests` states that all players in group `guests` will be teleported to spawn when joining. +#spawn-on-join: guests +# The following list value states that all players in group `guests` and `admin` are to be teleported to spawn when joining. +#spawn-on-join: +# - guests +# - admin + +# End of file <-- No seriously, you're done with configuration. diff --git a/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/essx-legacy.yml b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/essx-legacy.yml new file mode 100644 index 000000000..00f9e55d2 --- /dev/null +++ b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/essx-legacy.yml @@ -0,0 +1,728 @@ +############################################################ +# +------------------------------------------------------+ # +# | Notes | # +# +------------------------------------------------------+ # +############################################################ + +# If you want to use special characters in this document, such as accented letters, you MUST save the file as UTF-8, not ANSI. +# If you receive an error when Essentials loads, ensure that: +# - No tabs are present: YAML only allows spaces +# - Indents are correct: YAML hierarchy is based entirely on indentation +# - You have "escaped" all apostrophes in your text: If you want to write "don't", for example, write "don''t" instead (note the doubled apostrophe) +# - Text with symbols is enclosed in single or double quotation marks + +# If you have problems join the Essentials help support channel: http://tiny.cc/EssentialsChat + +############################################################ +# +------------------------------------------------------+ # +# | Essentials (Global) | # +# +------------------------------------------------------+ # +############################################################ + +# A color code between 0-9 or a-f. Set to 'none' to disable. +ops-name-color: '4' + +# The character(s) to prefix all nicknames, so that you know they are not true usernames. +nickname-prefix: '~' + +# The maximum length allowed in nicknames. The nickname prefix is included in this. +max-nick-length: 120 + +# Disable this if you have any other plugin, that modifies the displayname of a user. +change-displayname: true + +# When this option is enabled, the (tab) player list will be updated with the displayname. +# The value of change-displayname (above) has to be true. +#change-playerlist: true + +# When EssentialsChat.jar isn't used, force essentials to add the prefix and suffix from permission plugins to displayname. +# This setting is ignored if EssentialsChat.jar is used, and defaults to 'true'. +# The value of change-displayname (above) has to be true. +# Do not edit this setting unless you know what you are doing! +#add-prefix-suffix: false + +# If the teleport destination is unsafe, should players be teleported to the nearest safe location? +# If this is set to true, Essentials will attempt to teleport players close to the intended destination. +# If this is set to false, attempted teleports to unsafe locations will be cancelled with a warning. +teleport-safety: true + +# The delay, in seconds, required between /home, /tp, etc. +teleport-cooldown: 0 + +# The delay, in seconds, before a user actually teleports. If the user moves or gets attacked in this timeframe, the teleport never occurs. +teleport-delay: 0 + +# The delay, in seconds, a player can't be attacked by other players after they have been teleported by a command. +# This will also prevent the player attacking other players. +teleport-invulnerability: 4 + +# The delay, in seconds, required between /heal or /feed attempts. +heal-cooldown: 60 + +# What to prevent from /item and /give. +# e.g item-spawn-blacklist: 10,11,46 +item-spawn-blacklist: + +# Set this to true if you want permission based item spawn rules. +# Note: The blacklist above will be ignored then. +# Example permissions (these go in your permissions manager): +# - essentials.itemspawn.item-all +# - essentials.itemspawn.item-[itemname] +# - essentials.itemspawn.item-[itemid] +# - essentials.give.item-all +# - essentials.give.item-[itemname] +# - essentials.give.item-[itemid] +# - essentials.unlimited.item-all +# - essentials.unlimited.item-[itemname] +# - essentials.unlimited.item-[itemid] +# - essentials.unlimited.item-bucket # Unlimited liquid placing +# +# For more information, visit http://wiki.ess3.net/wiki/Command_Reference/ICheat#Item.2FGive +permission-based-item-spawn: false + +# Mob limit on the /spawnmob command per execution. +spawnmob-limit: 100 + +# Shall we notify users when using /lightning? +warn-on-smite: true + +# The motd and rules are now configured in the files motd.txt and rules.txt. + +# When a command conflicts with another plugin, by default, Essentials will try to force the OTHER plugin to take priority. +# Commands in this list, will tell Essentials to 'not give up' the command to other plugins. +# In this state, which plugin 'wins' appears to be almost random. +# +# If you have two plugin with the same command and you wish to force Essentials to take over, you need an alias. +# To force essentials to take 'god' alias 'god' to 'egod'. +# See http://wiki.bukkit.org/Commands.yml#aliases for more information. + +overridden-commands: +# - god +# - info + +# Disabling commands here will prevent Essentials handling the command, this will not affect command conflicts. +# You should not have to disable commands used in other plugins, they will automatically get priority. +# See http://wiki.bukkit.org/Commands.yml#aliases to map commands to other plugins. +disabled-commands: +- bob +- sell +- worth +- formula +# - clear + +# These commands will be shown to players with socialSpy enabled. +# You can add commands from other plugins you may want to track or +# remove commands that are used for something you dont want to spy on. +# Set - '*' in order to listen on all possible commands. +socialspy-commands: +- '*' +- -bob + +# If you do not wish to use a permission system, you can define a list of 'player perms' below. +# This list has no effect if you are using a supported permissions system. +# If you are using an unsupported permissions system, simply delete this section. +# Whitelist the commands and permissions you wish to give players by default (everything else is op only). +# These are the permissions without the "essentials." part. +player-commands: +- afk +- afk.auto +- back +- back.ondeath +- balance +- balance.others +- balancetop +- build +- chat.color +- chat.format +- chat.shout +- chat.question +- clearinventory +- compass +- depth +- delhome +- getpos +- geoip.show +- help +- helpop +- home +- home.others +- ignore +- info +- itemdb +- kit +- kits.tools +- list +- mail +- mail.send +- me +- motd +- msg +- msg.color +- nick +- near +- pay +- ping +- protect +- r +- rules +- realname +- seen +- sell +- sethome +- setxmpp +- signs.create.protection +- signs.create.trade +- signs.break.protection +- signs.break.trade +- signs.use.balance +- signs.use.buy +- signs.use.disposal +- signs.use.enchant +- signs.use.free +- signs.use.gamemode +- signs.use.heal +- signs.use.info +- signs.use.kit +- signs.use.mail +- signs.use.protection +- signs.use.repair +- signs.use.sell +- signs.use.time +- signs.use.trade +- signs.use.warp +- signs.use.weather +- spawn +- suicide +- time +- tpa +- tpaccept +- tpahere +- tpdeny +- warp +- warp.list +- world +- worth +- xmpp + +# Note: All items MUST be followed by a quantity! +# All kit names should be lower case, and will be treated as lower in permissions/costs. +# Syntax: - itemID[:DataValue/Durability] Amount [Enchantment:Level].. [itemmeta:value]... +# For Item Meta information visit http://wiki.ess3.net/wiki/Item_Meta +# 'delay' refers to the cooldown between how often you can use each kit, measured in seconds. +# Set delay to -1 for a one time kit. +# For more information, visit http://wiki.ess3.net/wiki/Kits +kits: + tools: + delay: -1 + items: + - 272 1 + - 273 1 + - 274 1 + - 275 1 + dtools: + delay: 600 + items: + - 278 1 efficiency:1 durability:1 fortune:1 name:&4Gigadrill lore:The_drill_that_&npierces|the_heavens + - 277 1 digspeed:3 name:Dwarf lore:Diggy|Diggy|Hole + - 298 1 color:255,255,255 name:Top_Hat lore:Good_day,_Good_day + - 279:780 1 + notch: + delay: 6000 + items: + - 397:3 1 player:Notch + color: + delay: 6000 + items: + - 387 1 title:&4Book_&9o_&6Colors author:KHobbits lore:Ingame_color_codes book:Colors + firework: + delay: 6000 + items: + - 401 1 name:Angry_Creeper color:red fade:green type:creeper power:1 + - 401 1 name:StarryNight color:yellow,orange fade:blue type:star effect:trail,twinkle power:1 + - 401 2 name:SolarWind color:yellow,orange fade:red shape:large effect:twinkle color:yellow,orange fade:red shape:ball effect:trail color:red,purple fade:pink shape:star effect:trail power:1 + +# Essentials Sign Control +# See http://wiki.ess3.net/wiki/Sign_Tutorial for instructions on how to use these. +# To enable signs, remove # symbol. To disable all signs, comment/remove each sign. +# Essentials colored sign support will be enabled when any sign types are enabled. +# Color is not an actual sign, it's for enabling using color codes on signs, when the correct permissions are given. + +enabledSigns: +- color +- balance +- buy +- sell +#- trade +#- free +- disposal +#- warp +#- kit +#- mail +- enchant +#- gamemode +#- heal +#- info +#- spawnmob +#- repair +#- time +#- weather + +# How many times per second can Essentials signs be interacted with per player. +# Values should be between 1-20, 20 being virtually no lag protection. +# Lower numbers will reduce the possibility of lag, but may annoy players. +sign-use-per-second: 4 + +# Backup runs a batch/bash command while saving is disabled. +backup: + # Interval in minutes. + interval: 30 + # Unless you add a valid backup command or script here, this feature will be useless. + # Use 'save-all' to simply force regular world saving without backup. + #command: 'rdiff-backup World1 backups/World1' + +# Set this true to enable permission per warp. +per-warp-permission: false + +# Sort output of /list command by groups. +# You can hide and merge the groups displayed in /list by defining the desired behaviour here. +# Detailed instructions and examples can be found on the wiki: http://wiki.ess3.net/wiki/List +list: + # To merge groups, list the groups you wish to merge + #Staff: owner admin moderator + Staff: owner admin + # To limit groups, set a max user limit + #builder: 20 + # To hide groups, set the group as hidden + #default: hidden + Pro: Jerry + # Uncomment the line below to simply list all players with no grouping + Players: '*' + +# More output to the console. +debug: false + +# Set the locale for all messages. +# If you don't set this, the default locale of the server will be used. +# For example, to set language to English, set locale to en, to use the file "messages_en.properties". +# Don't forget to remove the # in front of the line. +# For more information, visit http://wiki.ess3.net/wiki/Locale +#locale: en + +# Turn off god mode when people leave the server. +remove-god-on-disconnect: false + +# Auto-AFK +# After this timeout in seconds, the user will be set as AFK. +# This feature requires the player to have essentials.afk.auto node. +# Set to -1 for no timeout. +auto-afk: 60 + +# Auto-AFK Kick +# After this timeout in seconds, the user will be kicked from the server. +# essentials.afk.kickexempt node overrides this feature. +# Set to -1 for no timeout. +auto-afk-kick: -1 + +# Set this to true, if you want to freeze the player, if the player is AFK. +# Other players or monsters can't push the player out of AFK mode then. +# This will also enable temporary god mode for the AFK player. +# The player has to use the command /afk to leave the AFK mode. +freeze-afk-players: false + +# When the player is AFK, should he be able to pickup items? +# Enable this, when you don't want people idling in mob traps. +disable-item-pickup-while-afk: false + +# This setting controls if a player is marked as active on interaction. +# When this setting is false, the player would need to manually un-AFK using the /afk command. +cancel-afk-on-interact: true + +# Should we automatically remove afk status when a player moves? +# Player will be removed from AFK on chat/command regardless of this setting. +# Disable this to reduce server lag. +cancel-afk-on-move: true + +# You can disable the death messages of Minecraft here. +death-messages: true + +# Should players with permissions be able to join and part silently? +# You can control this with essentials.silentjoin and essentials.silentquit permissions if it is enabled. +# In addition, people with essentials.silentjoin.vanish will be vanished on join. +allow-silent-join-quit: true + +# You can set a custom join message here, set to "none" to disable. +# You may use color codes, use {USERNAME} the player's name or {PLAYER} for the player's displayname. +custom-join-message: "" + +# You can set a custom quit message here, set to "none" to disable. +# You may use color codes, use {USERNAME} the player's name or {PLAYER} for the player's displayname. +custom-quit-message: "" + +# Add worlds to this list, if you want to automatically disable god mode there. +no-god-in-worlds: +# - world_nether + +# Set to true to enable per-world permissions for teleporting between worlds with essentials commands. +# This applies to /world, /back, /tp[a|o][here|all], but not warps. +# Give someone permission to teleport to a world with essentials.worlds. +# This does not affect the /home command, there is a separate toggle below for this. +world-teleport-permissions: false + +# The number of items given if the quantity parameter is left out in /item or /give. +# If this number is below 1, the maximum stack size size is given. If over-sized stacks. +# are not enabled, any number higher than the maximum stack size results in more than one stack. +default-stack-size: -1 + +# Over-sized stacks are stacks that ignore the normal max stack size. +# They can be obtained using /give and /item, if the player has essentials.oversizedstacks permission. +# How many items should be in an over-sized stack? +oversized-stacksize: 64 + +# Allow repair of enchanted weapons and armor. +# If you set this to false, you can still allow it for certain players using the permission. +# essentials.repair.enchanted +repair-enchanted: true + +# Allow 'unsafe' enchantments in kits and item spawning. +# Warning: Mixing and overleveling some enchantments can cause issues with clients, servers and plugins. +unsafe-enchantments: true + +#Do you want Essentials to keep track of previous location for /back in the teleport listener? +#If you set this to true any plugin that uses teleport will have the previous location registered. +register-back-in-listener: false + +#Delay to wait before people can cause attack damage after logging in. +login-attack-delay: 5 + +#Set the max fly speed, values range from 0.1 to 1.0 +max-fly-speed: 0.8 + +#Set the max walk speed, values range from 0.1 to 1.0 +max-walk-speed: 0.8 + +#Set the maximum amount of mail that can be sent within a minute. +mails-per-minute: 1000 + +# Set the maximum time /tempban can be used for in seconds. +# Set to -1 to disable, and essentials.tempban.unlimited can be used to override. +max-tempban-time: -1 + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsHome | # +# +------------------------------------------------------+ # +############################################################ + +# Allows people to set their bed at daytime. +update-bed-at-daytime: true + +# Set to true to enable per-world permissions for using homes to teleport between worlds. +# This applies to the /home only. +# Give someone permission to teleport to a world with essentials.worlds. +world-home-permissions: false + +# Allow players to have multiple homes. +# Players need essentials.sethome.multiple before they can have more than 1 home. +# You can set the default number of multiple homes using the 'default' rank below. +# To remove the home limit entirely, give people 'essentials.sethome.multiple.unlimited'. +# To grant different home amounts to different people, you need to define a 'home-rank' below. +# Create the 'home-rank' below, and give the matching permission: essentials.sethome.multiple. +# For more information, visit http://wiki.ess3.net/wiki/Multihome +sethome-multiple: + default: 2 + vip: 2 + staff: 2 + buy5: 5 + buy10: 10 + +# In this example someone with 'essentials.sethome.multiple' and 'essentials.sethome.multiple.vip' will have 5 homes. +# Remember, they MUST have both permission nodes in order to be able to set multiple homes. + +# Set the timeout, in seconds for players to accept a tpa before the request is cancelled. +# Set to 0 for no timeout. +tpa-accept-cancellation: 120 + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsEco | # +# +------------------------------------------------------+ # +############################################################ + +# For more information, visit http://wiki.ess3.net/wiki/Essentials_Economy + +# Defines the balance with which new players begin. Defaults to 0. +starting-balance: 0 + +# worth-# defines the value of an item when it is sold to the server via /sell. +# These are now defined in worth.yml + +# Defines the cost to use the given commands PER USE. +# Some commands like /repair have sub-costs, check the wiki for more information. +command-costs: +# /example costs $1000 PER USE +#example: 1000 +# /kit tools costs $1500 PER USE +#kit-tools: 1500 + +# Set this to a currency symbol you want to use. +# Remember, if you want to use special characters in this document, +# such as accented letters, you MUST save the file as UTF-8, not ANSI. +currency-symbol: '$' + +# Set the maximum amount of money a player can have. +# The amount is always limited to 10 trillion because of the limitations of a java double. +max-money: 10000000000000 + +# Set the minimum amount of money a player can have (must be above the negative of max-money). +# Setting this to 0, will disable overdrafts/loans completely. Users need 'essentials.eco.loan' perm to go below 0. +min-money: 0 + +# Enable this to log all interactions with trade/buy/sell signs and sell command. +economy-log-enabled: false + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsHelp | # +# +------------------------------------------------------+ # +############################################################ + +# Show other plugins commands in help. +non-ess-in-help: true + +# Hide plugins which do not give a permission. +# You can override a true value here for a single plugin by adding a permission to a user/group. +# The individual permission is: essentials.help., anyone with essentials.* or '*' will see all help regardless. +# You can use negative permissions to remove access to just a single plugins help if the following is enabled. +hide-permissionless-help: true + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsChat | # +# +------------------------------------------------------+ # +############################################################ + +# This section requires the EssentialsChat.jar to work. + +chat: + + # If EssentialsChat is installed, this will define how far a player's voice travels, in blocks. Set to 0 to make all chat global. + # Note that users with the "essentials.chat.spy" permission will hear everything, regardless of this setting. + # Users with essentials.chat.shout can override this by prefixing text with an exclamation mark (!) + # Users with essentials.chat.question can override this by prefixing text with a question mark (?) + # You can add command costs for shout/question by adding chat-shout and chat-question to the command costs section." + radius: 0 + + # Chat formatting can be done in two ways, you can either define a standard format for all chat. + # Or you can give a group specific chat format, to give some extra variation. + # For more information of chat formatting, check out the wiki: http://wiki.ess3.net/wiki/Chat_Formatting + + format: '{DISPLAYNAME}: {MESSAGE}' + #format: '&7[{GROUP}]&r {DISPLAYNAME}&7:&r {MESSAGE}' + + group-formats: + default: '{DISPLAYNAME}&7:&7 {MESSAGE}' + tardis: '{DISPLAYNAME}&7:&7 {MESSAGE}' + Helper: '{DISPLAYNAME}&7:&3 {MESSAGE}' + Mod: '{DISPLAYNAME}&7:&3 {MESSAGE}' + Admin: '{DISPLAYNAME}&7:&c {MESSAGE}' + owner: '{DISPLAYNAME}&7:&c {MESSAGE}' + + # If you are using group formats make sure to remove the '#' to allow the setting to be read. + +############################################################ +# +------------------------------------------------------+ # +# | EssentialsProtect | # +# +------------------------------------------------------+ # +############################################################ + +# This section requires the EssentialsProtect.jar to work. + +protect: + + # General physics/behavior modifications. + prevent: + lava-flow: false + water-flow: false + water-bucket-flow: false + fire-spread: true + lava-fire-spread: true + flint-fire: false + lightning-fire-spread: true + portal-creation: false + tnt-explosion: false + tnt-playerdamage: false + tnt-minecart-explosion: false + tnt-minecart-playerdamage: false + fireball-explosion: false + fireball-fire: false + fireball-playerdamage: false + witherskull-explosion: false + witherskull-playerdamage: false + wither-spawnexplosion: false + wither-blockreplace: false + creeper-explosion: false + creeper-playerdamage: false + creeper-blockdamage: false + enderdragon-blockdamage: true + enderman-pickup: false + villager-death: false + # Monsters won't follow players. + # permission essentials.protect.entitytarget.bypass disables this. + entitytarget: false + # Prevent the spawning of creatures. + spawn: + creeper: false + skeleton: false + spider: false + giant: false + zombie: false + slime: false + ghast: false + pig_zombie: false + enderman: false + cave_spider: false + silverfish: false + blaze: false + magma_cube: false + ender_dragon: false + pig: false + sheep: false + cow: false + chicken: false + squid: false + wolf: false + mushroom_cow: false + snowman: false + ocelot: false + iron_golem: false + villager: false + wither: false + bat: false + witch: false + horse: false + + # Maximum height the creeper should explode. -1 allows them to explode everywhere. + # Set prevent.creeper-explosion to true, if you want to disable creeper explosions. + creeper: + max-height: -1 + + # Disable various default physics and behaviors. + disable: + # Should fall damage be disabled? + fall: false + + # Users with the essentials.protect.pvp permission will still be able to attack each other if this is set to true. + # They will be unable to attack users without that same permission node. + pvp: false + + # Should drowning damage be disabled? + # (Split into two behaviors; generally, you want both set to the same value.) + drown: false + suffocate: false + + # Should damage via lava be disabled? Items that fall into lava will still burn to a crisp. ;) + lavadmg: false + + # Should arrow damage be disabled? + projectiles: false + + # This will disable damage from touching cacti. + contactdmg: false + + # Burn, baby, burn! Should fire damage be disabled? + firedmg: false + + # Should the damage after hit by a lightning be disabled? + lightning: false + + # Should Wither damage be disabled? + wither: false + + # Disable weather options? + weather: + storm: false + thunder: false + lightning: false + + ############################################################ + # +------------------------------------------------------+ # + # | EssentialsAntiBuild | # + # +------------------------------------------------------+ # + ############################################################ + + # This section requires the EssentialsAntiBuild.jar to work. + + # Disable various default physics and behaviors + # For more information, visit http://wiki.ess3.net/wiki/AntiBuild + + # Should people with build: false in permissions be allowed to build? + # Set true to disable building for those people. + # Setting to false means EssentialsAntiBuild will never prevent you from building. + build: true + + # Should people with build: false in permissions be allowed to use items? + # Set true to disable using for those people. + # Setting to false means EssentialsAntiBuild will never prevent you from using items. + use: true + + # Should we tell people they are not allowed to build? + warn-on-build-disallow: true + + # For which block types would you like to be alerted? + # You can find a list of IDs in plugins/Essentials/items.csv after loading Essentials for the first time. + # 10 = lava :: 11 = still lava :: 46 = TNT :: 327 = lava bucket + alert: + on-placement: 10,11,46,327 + on-use: 327 + on-break: + + blacklist: + + # Which blocks should people be prevented from placing? + placement: 10,11,46,327 + + # Which items should people be prevented from using? + usage: 327 + + # Which blocks should people be prevented from breaking? + break: + + # Which blocks should not be pushed by pistons? + piston: + + # Which blocks should not be dispensed by dispensers + dispenser: + +############################################################ +# +------------------------------------------------------+ # +# | Essentials Spawn / New Players | # +# +------------------------------------------------------+ # +############################################################ + +# This section requires essentialsspawn.jar to work. + +newbies: + # Should we announce to the server when someone logs in for the first time? + # If so, use this format, replacing {DISPLAYNAME} with the player name. + # If not, set to '' + #announce-format: '' + announce-format: '&dWelcome {DISPLAYNAME}&d to the server!' + + # When we spawn for the first time, which spawnpoint do we use? + # Set to "none" if you want to use the spawn point of the world. + spawnpoint: newbies + + # Do we want to give users anything on first join? Set to '' to disable + # This kit will be given regardless of cost and permissions, and will not trigger the kit delay. + #kit: '' + kit: tools + +# Set this to lowest, if you want Multiverse to handle the respawning. +# Set this to high, if you want EssentialsSpawn to handle the respawning. +# Set this to highest, if you want to force EssentialsSpawn to handle the respawning. +respawn-listener-priority: high + +# When users die, should they respawn at their first home or bed, instead of the spawnpoint? +respawn-at-home: false + +# End of file <-- No seriously, you're done with configuration. diff --git a/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/example.yml b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/example.yml new file mode 100644 index 000000000..63cee3fcb --- /dev/null +++ b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/example.yml @@ -0,0 +1,13 @@ +test: + op-level: unicorn +other: &ptr + op-level: dragon + location: dog park + age: 28 + big scalar: | + Hello world! +more: *ptr + +foo: + bar: + - {a: "best", no: help} diff --git a/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/mobcleaner-example.yml b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/mobcleaner-example.yml new file mode 100644 index 000000000..3a3a9d803 --- /dev/null +++ b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/mobcleaner-example.yml @@ -0,0 +1,138 @@ +# This config was generated for version 1.9.0 +# Find the latest config at https://github.com/JasonHorkles/EntityClearer/blob/main/config.yml + +# This plugin uses bStats metrics +# https://bstats.org/plugin/bukkit/EntityClearer/10915 +# Metrics can be disabled in the bStats config.yml + +# Interval in minutes the clear task should run +interval: 15 + +# The sound to play when showing the warnings +# See https://minecraft.fandom.com/wiki/Sounds.json#Java_Edition_values for all sounds +# Set to '' for no sound +sound: 'ui.button.click' + +# The worlds to check and remove entities from +# Make sure to add your world(s) if they're not already there +# See https://papermc.io/javadocs/paper/1.16/org/bukkit/entity/EntityType.html for all entities +worlds: + world: + # The entities to be removed from the world + entities: + - BEE + - CAVE_SPIDER + - CHICKEN + - CREEPER + - SHEEP + - SKELETON + - SLIME + - SPIDER + - SQUID + - WOLF + - ZOMBIE + + # Should only entities with a specific spawn reason be removed? + # Setting this to false will check for any spawn reason + spawn-reason: + enabled: false + # See https://papermc.io/javadocs/paper/1.16/org/bukkit/event/entity/CreatureSpawnEvent.SpawnReason.html for all spawn reasons + reasons: + - SPAWNER + - SPAWNER_EGG + world_nether: + entities: + - BLAZE + - ZOMBIFIED_PIGLIN + spawn-reason: + enabled: false + reasons: [] +# creative: +# entities: +# - ARMOR_STAND +# - ARROW +# - BOAT +# - DROPPED_ITEM +# - MINECART +# - MINECART_CHEST +# - MINECART_FURNACE +# - MINECART_HOPPER +# - MINECART_TNT +# spawn-reason: +# enabled: false +# reasons: [] + +nearby-entities: + # Should the plugin only remove entities that have multiple entities nearby? + # This is useful to only remove large groups of entities while leaving smaller entity groups alone + # Using this would prevent every entity from being removed then just respawning again naturally + enabled: true + + # Only apply to worlds with the mob spawning gamerule enabled? + gamerule-enabled-only: false + + # The distance in blocks that the plugin should check for extra entities + x: 3 + y: 3 + z: 3 + + # How many additional entities must be around the first entity to be removed? + count: 4 + +# Should using the command /clearentities start the countdown or instantly remove the entities? +countdown-on-command: true + +# Should named entities be removed? +remove-named: false + +messages: + # Should there be action bar messages? + actionbar: true + + # Should there be chat messages? + chat: false + actionbar-message: "&6&lCommon entities will be removed in &e&l{SECONDS} &6&lsecond{S}!" + actionbar-completed-message: "&6&lRemoved &e&l{ENTITIES} &6&lentities!" + chat-message: "&cCommon entities will be removed in &7{SECONDS} &csecond{S}!" + chat-completed-message: "&cRemoved &7{ENTITIES} ¢ities!" + +# When should the warning messages send? +# Time is in seconds remaining before the clear task +warning-messages: + 60-seconds: false + 45-seconds: false + 30-seconds: true + 15-seconds: false + 5-seconds: true + 4-seconds: true + 3-seconds: true + 2-seconds: true + 1-second: true + +# The command(s) to run after the entity clearing task runs +commands: +# - nuke + +low-tps: + # Should the entity removal task be triggered when the TPS is low? + enabled: false + + # Below what TPS should the plugin remove the entities? + threshold: 17 + + # Should the plugin remove the entities instantly or trigger the countdown? + # Warning: If the TPS is too low and remove-instantly is false, the plugin may + # not be able to remove the entities before the server crashes + remove-instantly: true + + # Should there be a chat message sent to players with the `entityclearer.lowtps` + # permission stating that the TPS is low? + chat: true + chat-message: "&c&lWarning: TPS low &8&l(&7&l{TPS}&8&l)&c&l! Removing entities..." + +############################################### + +# Debug stuff +# You shouldn't need to touch this unless specifically asked to do so :) +print-stack-traces: false +debug: false diff --git a/format/yaml/src/test/resources/tab-example.yml b/format/yaml/src/test/resources/org/spongepowered/configurate/yaml/tab-example.yml similarity index 100% rename from format/yaml/src/test/resources/tab-example.yml rename to format/yaml/src/test/resources/org/spongepowered/configurate/yaml/tab-example.yml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1ff64630..86b8d9170 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ autoValue="1.10.4" checkerQual="3.38.0" checkstyle="10.12.4" geantyref = "1.3.14" +groovy = "4.0.15" errorprone="2.22.0" indra = "3.1.3" junit="5.10.0" @@ -45,6 +46,13 @@ slf4j = "org.slf4j:slf4j-api:1.8.0-beta4" # Match version used in MC guice-compile = "com.google.inject:guice:4.1.0" guice-runtime = "com.google.inject:guice:5.0.1" +# Groovy +groovy = { module = "org.apache.groovy:groovy", version.ref = "groovy" } +groovy-nio = { module = "org.apache.groovy:groovy-nio", version.ref = "groovy" } +groovy-templates = { module = "org.apache.groovy:groovy-templates", version.ref = "groovy" } +groovy-test = { module = "org.apache.groovy:groovy-test", version.ref = "groovy" } +groovy-test-junit5 = { module = "org.apache.groovy:groovy-test-junit5", version.ref = "groovy" } + # Formats gson = "com.google.code.gson:gson:2.8.0" # Fixed version, to avoid using API not present in older MC hocon = "com.typesafe:config:1.4.2" @@ -78,6 +86,7 @@ detekt = "io.gitlab.arturbosch.detekt:1.23.1" dokka = "org.jetbrains.dokka:1.9.0" gitPublish = "org.ajoberstar.git-publish:3.0.1" gitpatcher = { id = "ca.stellardrift.gitpatcher", version = "1.0.0" } +ideaExt = "org.jetbrains.gradle.plugin.idea-ext:1.1.6" indra-sonatype = { id = "net.kyori.indra.publishing.sonatype", version.ref = "indra" } indra-git = { id = "net.kyori.indra.git", version.ref = "indra" } kotlin = "org.jetbrains.kotlin.jvm:1.9.10" diff --git a/tool/src/main/kotlin/org/spongepowered/configurate/tool/Tool.kt b/tool/src/main/kotlin/org/spongepowered/configurate/tool/Tool.kt index 48a63bca9..bb3ccfe9a 100644 --- a/tool/src/main/kotlin/org/spongepowered/configurate/tool/Tool.kt +++ b/tool/src/main/kotlin/org/spongepowered/configurate/tool/Tool.kt @@ -243,6 +243,9 @@ class Yaml : FormatSubcommand("YAML") { .default(DEFAULT_INDENT) private val flowStyle by option("-s", "--style", help = "What node style to use").enum() + private val comments by + option("-c", "--comments", help = "Whether or not to enable comment handling") + .flag("--no-comments", default = false) override fun createLoader(): ConfigurationLoader { return YamlConfigurationLoader.builder() @@ -250,6 +253,7 @@ class Yaml : FormatSubcommand("YAML") { .headerMode(header) .indent(indent) .nodeStyle(flowStyle) + .commentsEnabled(comments) .build() } } diff --git a/vendor/build.gradle.kts b/vendor/build.gradle.kts new file mode 100644 index 000000000..1d6bf54f2 --- /dev/null +++ b/vendor/build.gradle.kts @@ -0,0 +1,90 @@ +import javax.xml.parsers.DocumentBuilderFactory + +plugins { + id("net.minecraftforge.gitpatcher") version "0.10.+" +} + +patches { + submodule = "snakeyaml-upstream" + target = file("snakeyaml") + patches = file("snakeyaml-patches") +} + +subprojects { + apply(plugin = "java-library") + + group = "configurate.thirdparty" + version = "version-from-submodule" + + tasks.withType(JavaCompile::class) { + options.encoding = "UTF-8" + } + + extensions.configure(JavaPluginExtension::class) { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } + } +} + +project(":snakeyaml") { + val mavenPom = project.file("../snakeyaml-upstream/pom.xml") + if (mavenPom.exists()) { + val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val document = documentBuilder.parse(mavenPom) + val mavenProject = document.getElementsByTagName("project").item(0) + + fun org.w3c.dom.NodeList.element(tagName: String): org.w3c.dom.Element? { + for (i in 0..this.length) { + val element = this.item(i) + if (element is org.w3c.dom.Element && element.tagName == tagName) { + return element + } + } + return null + } + + dependencies { + val dependencies = mavenProject.childNodes.element("dependencies")!!.childNodes + for (i in 0..dependencies.length) { + val dep = dependencies.item(i) + if (dep is org.w3c.dom.Element && dep.tagName == "dependency") { + val children = dep.childNodes + val group = children.element("groupId")?.textContent + val artifact = children.element("artifactId")?.textContent + val version = children.element("version")?.textContent + val configuration = when (children.element("scope")?.textContent) { + "test" -> "testImplementation" + "compile" -> "implementation" + "runtime" -> "runtime" + "provided" -> "compileOnly" + else -> null + } + if (configuration != null) { + configuration(group!!, artifact!!, version) + } + } + } + } + } + + val applyPatches = rootProject.tasks.named("applyPatches") + tasks.withType(JavaCompile::class) { + options.release.set(7) + // dependsOn(applyPatches) + } + tasks.withType(ProcessResources::class) { + // dependsOn(applyPatches) + } + + tasks.named("test", Test::class) { + environment( + "EnvironmentKey1" to "EnvironmentValue1", + "environmentEmpty" to "" + ) + filter { + // needs classpath provided via expansions, doesn't seem to be easily doable with the maven-style property names + excludeTest("org.yaml.snakeyaml.issues.issue318.ContextClassLoaderTest", null) + } + } +} \ No newline at end of file