parse method
Parses the given source
string into a Map<String, dynamic>
.
Implementations must:
- Convert the raw string into structured key-value pairs.
- Return a valid map representation.
- Throw a FormatException if parsing fails due to invalid syntax or unexpected input.
Example:
final config = parser.parse('{"debug": true}');
print(config['debug']); // true
Implementation
@override
Map<String, dynamic> parse(String source) {
try {
final lines = source.split('\n');
final root = <String, dynamic>{};
// containerStack holds either Map<String,dynamic> or List<dynamic>
final List<dynamic> containerStack = [root];
final List<int> indentStack = [0];
for (var i = 0; i < lines.length; i++) {
var originalLine = lines[i];
var line = originalLine.replaceFirst(RegExp(r'\r$'), '').trimRight();
if (line.trim().isEmpty) continue;
final trimmed = line.trim();
if (trimmed.startsWith('#')) continue; // comment
final indent = _getIndentation(originalLine);
// pop containers until current indent is < stack top indent
while (indentStack.length > 0 && indent <= indentStack.last && indentStack.length > 1) {
indentStack.removeLast();
containerStack.removeLast();
}
final currentContainer = containerStack.last;
// List item
if (trimmed.startsWith('-')) {
final itemText = trimmed.length > 1 ? trimmed.substring(1).trim() : '';
// Ensure we have a List to append to. If currentContainer is Map, attempt to attach list to last key.
List<dynamic>? targetList;
if (currentContainer is List) {
targetList = currentContainer;
} else if (currentContainer is Map<String, dynamic>) {
if (currentContainer.isEmpty) {
// nothing to attach list to — create an anonymous list? skip
// This case shouldn't normally occur for well-formed YAML that uses key: followed by - items
targetList = <dynamic>[];
// We cannot assign to a key, so append to root? fallback:
(containerStack.first as Map)[
'__anonymous_list_${indentStack.length}_${i}'] = targetList;
} else {
// assume last key stores the list
final lastKey = currentContainer.keys.last;
var existing = currentContainer[lastKey];
if (existing is! List) {
existing = <dynamic>[];
currentContainer[lastKey] = existing;
}
targetList = existing;
}
} else {
// unknown container type - skip
continue;
}
// Process itemText
if (itemText.isEmpty) {
// item is a nested map (block sequence)
final newMap = <String, dynamic>{};
targetList.add(newMap);
// push newMap as current container with indent greater than '-' line
containerStack.add(newMap);
indentStack.add(indent + 1);
} else if (itemText.contains(':')) {
// inline map item: "- key: value"
final colonIdx = itemText.indexOf(':');
final k = itemText.substring(0, colonIdx).trim();
final vstr = itemText.substring(colonIdx + 1).trim();
final entryMap = <String, dynamic>{k: _parseValue(vstr)};
targetList.add(entryMap);
} else {
// scalar item
targetList.add(_parseValue(itemText));
}
continue;
}
// Key: value or key: (empty => nested structure)
if (trimmed.contains(':')) {
final colonIndex = trimmed.indexOf(':');
final key = trimmed.substring(0, colonIndex).trim();
final valueStr = trimmed.substring(colonIndex + 1).trim();
if (currentContainer is! Map<String, dynamic>) {
// if we are inside a List with a Map last element, use that map
if (currentContainer is List && currentContainer.isNotEmpty && currentContainer.last is Map<String, dynamic>) {
final parentMap = currentContainer.last as Map<String, dynamic>;
if (valueStr.isEmpty) {
// decide whether to create a list or map by looking ahead
final next = _peekNextNonEmptyLine(lines, i);
if (next != null && next.indent > indent && next.trim.startsWith('-')) {
final list = <dynamic>[];
parentMap[key] = list;
containerStack.add(list);
indentStack.add(indent + 1);
} else {
final nested = <String, dynamic>{};
parentMap[key] = nested;
containerStack.add(nested);
indentStack.add(indent + 1);
}
} else {
parentMap[key] = _parseValue(valueStr);
}
continue;
} else {
// Unexpected: current container is not a map - create a map and attach? fallback to root
if (containerStack.first is Map<String, dynamic>) {
(containerStack.first as Map)[key] = valueStr.isEmpty ? <String, dynamic>{} : _parseValue(valueStr);
if (valueStr.isEmpty) {
containerStack.add((containerStack.first as Map)[key]);
indentStack.add(indent + 1);
}
continue;
}
}
}
// currentContainer is a Map<String,dynamic>
final map = currentContainer as Map<String, dynamic>;
if (valueStr.isEmpty) {
// Need to look ahead to decide if this key maps to a List or Map
final next = _peekNextNonEmptyLine(lines, i);
if (next != null && next.indent > indent && next.trim.startsWith('-')) {
// create a list
final list = <dynamic>[];
map[key] = list;
containerStack.add(list);
indentStack.add(indent + 1);
} else {
// create a nested map
final nested = <String, dynamic>{};
map[key] = nested;
containerStack.add(nested);
indentStack.add(indent + 1);
}
} else if (valueStr.startsWith('[') && valueStr.endsWith(']')) {
map[key] = _parseInlineArray(valueStr);
} else if (valueStr.startsWith('{') && valueStr.endsWith('}')) {
map[key] = _parseInlineObject(valueStr);
} else {
map[key] = _parseValue(valueStr);
}
continue;
}
// If we get here: a line without ":" and not starting with "-". We treat as scalar or ignore.
// For robustness, try to attach to last map if possible.
if (currentContainer is Map<String, dynamic> && currentContainer.isNotEmpty) {
final lastKey = currentContainer.keys.last;
// append/overwrite? Usually unexpected - we set as lastKey's value if it's a Map and previously empty.
final lastVal = currentContainer[lastKey];
if (lastVal is Map<String, dynamic> && lastVal.isEmpty) {
// treat this line as "key: value" missing colon; skip in strict parser
// ignore
} else if (lastVal is List) {
// could be something like a list item without '-'; ignore or add
}
}
}
return root;
} catch (e) {
throw ParserException('Failed to parse YAML: $e');
}
}