YamlLine.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*/
package net.sourceforge.plantuml.yaml.parser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import net.sourceforge.plantuml.annotation.DuplicateCode;
public class YamlLine {
private final int indent;
private final String key;
private final String value;
private final List<String> values;
private final boolean listItem;
private final YamlLineType type;
public static YamlLine build(String line) {
int count = 0;
line = line.replaceAll("\t", " ");
while (count < line.length() && line.charAt(count) == ' ')
count++;
String trimmedLine = removeYamlComment(line.substring(count).trim());
if (trimmedLine.isEmpty())
return new YamlLine(YamlLineType.EMPTY_LINE, 0, null, null, null, false);
if (trimmedLine.equals("-"))
return new YamlLine(YamlLineType.PLAIN_DASH, count + 1, null, null, null, true);
final boolean listItem = trimmedLine.startsWith("- ");
if (listItem) {
count += 2;
trimmedLine = trimmedLine.substring(2);
}
final int colonIndex = trimmedLine.indexOf(':');
if (colonIndex == -1)
if (listItem)
return new YamlLine(YamlLineType.PLAIN_ELEMENT_LIST, count, null, unquote(trimmedLine), null, listItem);
else
return new YamlLine(YamlLineType.NO_KEY_ONLY_TEXT, count, null, unquote(trimmedLine), null, false);
final String rawKey = trimmedLine.substring(0, colonIndex).trim();
final String rawValue = trimmedLine.substring(colonIndex + 1).trim();
YamlLineType type = YamlLineType.KEY_AND_VALUE;
if (rawValue.isEmpty())
type = YamlLineType.KEY_ONLY;
else if (rawValue.equals("|"))
type = YamlLineType.KEY_AND_BLOCK_STYLE;
else if (rawValue.equals(">"))
type = YamlLineType.KEY_AND_FOLDED_STYLE;
else if (rawValue.startsWith("[") && rawValue.endsWith("]"))
return new YamlLine(YamlLineType.KEY_AND_FLOW_SEQUENCE, count, unquote(rawKey), null,
toList(rawValue.substring(1, rawValue.length() - 1)), listItem);
return new YamlLine(type, count, unquote(rawKey), unquote(rawValue), null, listItem);
}
private static List<String> toList(String rawValue) {
final List<String> result = new ArrayList<>();
final StringBuilder current = new StringBuilder();
// Zero if we are not in a quoted state or
// represents the quote character if we are in a quoted string
char inQuotedString = '\0';
// Indicates that the current field started with a quote
boolean fieldStartWithQuote = false;
for (int i = 0; i < rawValue.length(); i++) {
final char c = rawValue.charAt(i);
if (inQuotedString != '\0') {
// Processing a quoted string
if (c == '\\') {
// Handle escaping: append the next character as is if it exists
if (i + 1 < rawValue.length()) {
current.append(rawValue.charAt(i + 1));
i++; // Skip the escaped character
}
} else if (c == inQuotedString) {
// End of the quoted string
inQuotedString = '\0';
} else {
current.append(c);
}
} else {
// We are not in a quoted string.
// If the field contains only spaces and we encounter a quote,
// we consider that the field actually starts with a quote.
if (fieldStartWithQuote == false && current.toString().trim().isEmpty() && (c == '\'' || c == '"')) {
inQuotedString = c;
fieldStartWithQuote = true;
current.setLength(0); // Clear any preliminary spaces
} else if (c == ',') {
// The field separator is encountered.
// For a quoted field, keep the content as is,
// otherwise apply trim.
result.add(fieldStartWithQuote ? current.toString() : current.toString().trim());
// Reset for the next field
current.setLength(0);
fieldStartWithQuote = false;
} else if (c == '\\') {
// Handle escaping outside of quotes
if (i + 1 < rawValue.length()) {
current.append(rawValue.charAt(i + 1));
i++; // Skip the escaped character
}
} else {
current.append(c);
}
}
}
// Add the last field (even if it's empty)
result.add(fieldStartWithQuote ? current.toString() : current.toString().trim());
return result;
}
private YamlLine(YamlLineType type, int indent, String key, String value, List<String> values, boolean listItem) {
this.type = type;
this.indent = indent;
this.key = key;
this.value = value;
this.values = values;
this.listItem = listItem;
}
private static String unquote(String str) {
if (str == null || str.length() < 2)
return str;
final char first = str.charAt(0);
final char last = str.charAt(str.length() - 1);
if ((first == '"' && last == '"') || (first == '\'' && last == '\''))
return str.substring(1, str.length() - 1);
return str;
}
public int getIndent() {
return indent;
}
@DuplicateCode(reference = "YamlLines")
private static String removeYamlComment(String s) {
if (s == null || s.isEmpty())
return s;
char inQuoteChar = '\0';
if (s.charAt(0) == '#')
return "";
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if (c == '\'' || c == '"')
if (inQuoteChar == '\0')
inQuoteChar = c;
else if (c == inQuoteChar)
inQuoteChar = '\0';
if (inQuoteChar == '\0' && i < s.length() - 1 && c == ' ' && s.charAt(i + 1) == '#')
return s.substring(0, i);
}
return s;
}
public String getKey() {
return key;
}
public String getValue() {
if (type == YamlLineType.KEY_AND_VALUE || type == YamlLineType.KEY_AND_FLOW_SEQUENCE
|| type == YamlLineType.PLAIN_ELEMENT_LIST || type == YamlLineType.NO_KEY_ONLY_TEXT)
return value;
throw new IllegalStateException(type.name());
}
public boolean isListItem() {
return listItem;
}
public YamlLineType getType() {
return type;
}
public List<String> getValues() {
if (type == YamlLineType.KEY_AND_FLOW_SEQUENCE)
return Collections.unmodifiableList(values);
throw new IllegalStateException(type.name());
}
@Override
public String toString() {
return "YamlLine(" + type + " indent=" + indent + ", key=" + key + ", value=" + value + ")";
}
}