-
-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
YAML comments support #410
base: master
Are you sure you want to change the base?
Changes from 8 commits
57d820f
a0901bf
8b06a98
4b79298
90ee1ed
1d885c3
fb49dd9
ee483ea
cc14b5c
618f113
3cdcc04
8105881
3de5368
c33572d
ffcbfcb
f8acad9
e5986b4
ce36736
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
/* | ||
* 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.EnsuresNonNull; | ||
import org.checkerframework.checker.nullness.qual.Nullable; | ||
import org.spongepowered.configurate.CommentedConfigurationNode; | ||
import org.spongepowered.configurate.ConfigurationNode; | ||
import org.spongepowered.configurate.ConfigurationOptions; | ||
import org.yaml.snakeyaml.LoaderOptions; | ||
import org.yaml.snakeyaml.comments.CommentLine; | ||
import org.yaml.snakeyaml.constructor.Constructor; | ||
import org.yaml.snakeyaml.nodes.MappingNode; | ||
import org.yaml.snakeyaml.nodes.Node; | ||
import org.yaml.snakeyaml.nodes.NodeId; | ||
import org.yaml.snakeyaml.nodes.ScalarNode; | ||
|
||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.regex.Pattern; | ||
import java.util.stream.Collectors; | ||
|
||
class YamlConstructor extends Constructor { | ||
|
||
private static final Pattern LINE_BREAK_PATTERN = Pattern.compile("\\R"); | ||
|
||
@Nullable ConfigurationOptions options; | ||
|
||
YamlConstructor(final LoaderOptions loadingConfig) { | ||
super(loadingConfig); | ||
} | ||
|
||
@Override | ||
@EnsuresNonNull("options") | ||
public Object getSingleData(final Class<?> type) { | ||
if (this.options == null) { | ||
throw new IllegalStateException("options must be set before calling load!"); | ||
} | ||
return super.getSingleData(type); | ||
} | ||
|
||
@Override | ||
protected Object constructObjectNoCheck(final Node yamlNode) { | ||
//noinspection DataFlowIssue guarenteed NonNull by getSingleData, which load(Reader) uses | ||
final CommentedConfigurationNode node = CommentedConfigurationNode.root(this.options); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm really not a fan of the fact that by creating a new root node here, we effectively construct two nodes per node when loading in a file There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're then mainly talking about the mapping part right? When looking at the differences between a |
||
|
||
if (yamlNode.getNodeId() == NodeId.mapping) { | ||
// make sure to mark it as a map type, even if the map itself is empty | ||
node.raw(Collections.emptyMap()); | ||
|
||
((MappingNode) yamlNode).getValue().forEach(tuple -> { | ||
// I don't think it's possible to have a non-scalar node as key | ||
zml2008 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
final ScalarNode keyNode = (ScalarNode) tuple.getKeyNode(); | ||
final Node valueNode = tuple.getValueNode(); | ||
|
||
// comments are on the key, not the value | ||
node.node(keyNode.getValue()) | ||
.from((ConfigurationNode) constructObject(valueNode)) | ||
.comment(commentFor(keyNode.getBlockComments())); | ||
}); | ||
|
||
return node.comment(commentFor(yamlNode.getBlockComments())); | ||
} | ||
|
||
final Object raw = super.constructObjectNoCheck(yamlNode); | ||
if (raw instanceof Collection<?>) { | ||
// make sure to mark it as a list type, even if the collection itself is empty | ||
node.raw(Collections.emptyList()); | ||
|
||
((Collection<?>) raw).forEach(value -> { | ||
node.appendListNode().from((ConfigurationNode) value); | ||
}); | ||
} else { | ||
node.raw(raw); | ||
} | ||
|
||
return node.comment(commentFor(yamlNode.getBlockComments())); | ||
} | ||
|
||
private static @Nullable String commentFor(final @Nullable List<CommentLine> commentLines) { | ||
if (commentLines == null || commentLines.isEmpty()) { | ||
return null; | ||
} | ||
return commentLines.stream() | ||
.map(input -> { | ||
final String lineStripped = removeLineBreaksForLine(input.getValue()); | ||
if (!lineStripped.isEmpty() && lineStripped.charAt(0) == ' ') { | ||
return lineStripped.substring(1); | ||
} else { | ||
return lineStripped; | ||
} | ||
}) | ||
.collect(Collectors.joining("\n")); | ||
} | ||
|
||
private static String removeLineBreaksForLine(final String line) { | ||
return LINE_BREAK_PATTERN.matcher(line).replaceAll(""); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.spongepowered.configurate.loader.AbstractConfigurationLoader.CONFIGURATE_LINE_PATTERN; | ||
|
||
import org.checkerframework.checker.nullness.qual.Nullable; | ||
import org.spongepowered.configurate.CommentedConfigurationNodeIntermediary; | ||
import org.spongepowered.configurate.ConfigurationNode; | ||
import org.yaml.snakeyaml.DumperOptions; | ||
import org.yaml.snakeyaml.DumperOptions.FlowStyle; | ||
import org.yaml.snakeyaml.comments.CommentLine; | ||
import org.yaml.snakeyaml.comments.CommentType; | ||
import org.yaml.snakeyaml.nodes.MappingNode; | ||
import org.yaml.snakeyaml.nodes.Node; | ||
import org.yaml.snakeyaml.nodes.NodeTuple; | ||
import org.yaml.snakeyaml.nodes.SequenceNode; | ||
import org.yaml.snakeyaml.nodes.Tag; | ||
import org.yaml.snakeyaml.representer.Represent; | ||
import org.yaml.snakeyaml.representer.Representer; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
final class YamlRepresenter extends Representer { | ||
|
||
YamlRepresenter(final DumperOptions options) { | ||
super(options); | ||
multiRepresenters.put(ConfigurationNode.class, new ConfigurationNodeRepresent()); | ||
} | ||
|
||
private final class ConfigurationNodeRepresent implements Represent { | ||
@Override | ||
public Node representData(final Object nodeObject) { | ||
final ConfigurationNode node = (ConfigurationNode) nodeObject; | ||
|
||
final Node yamlNode; | ||
if (node.isMap()) { | ||
final List<NodeTuple> children = new ArrayList<>(); | ||
for (Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) { | ||
// SnakeYAML supports both key and value comments. Add the comments on the key | ||
final Node value = represent(ent.getValue()); | ||
final Node key = represent(String.valueOf(ent.getKey())); | ||
key.setBlockComments(value.getBlockComments()); | ||
value.setBlockComments(Collections.emptyList()); | ||
|
||
children.add(new NodeTuple(key, value)); | ||
} | ||
yamlNode = new MappingNode(Tag.MAP, children, FlowStyle.AUTO); | ||
} else if (node.isList()) { | ||
final List<Node> children = new ArrayList<>(); | ||
for (ConfigurationNode ent : node.childrenList()) { | ||
children.add(represent(ent)); | ||
} | ||
yamlNode = new SequenceNode(Tag.SEQ, children, FlowStyle.AUTO); | ||
} else { | ||
yamlNode = represent(node.rawScalar()); | ||
} | ||
|
||
if (node instanceof CommentedConfigurationNodeIntermediary<?>) { | ||
final @Nullable String nodeComment = ((CommentedConfigurationNodeIntermediary<?>) node).comment(); | ||
if (nodeComment != null) { | ||
yamlNode.setBlockComments( | ||
Arrays.stream(CONFIGURATE_LINE_PATTERN.split(nodeComment)) | ||
.map(this::commentLineFor) | ||
.collect(Collectors.toList()) | ||
); | ||
} | ||
} | ||
|
||
return yamlNode; | ||
} | ||
|
||
private CommentLine commentLineFor(final String comment) { | ||
// prepend a space before the comment: | ||
// before: #hello | ||
// after: # hello | ||
return new CommentLine(null, null, " " + comment, CommentType.BLOCK); | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a regression here:
load
returns null when the file is empty, whichConfigurationNode#from
can't receive.