class_name YamlParser ## Shared YAML parser — common subset used by ui_strings.gd and checklist_evaluator.gd. ## ## Handles: nested sections (maps), arrays of dict items (- key: val), typed values. ## Returns a hierarchical Dictionary. Use flatten() to convert to dotted-key format ## (as UIStrings._parse_yaml() requires). ## ## Limitations: single-line values only; no YAML anchors/aliases; no flow syntax. ## String values: quotes stripped. Booleans, ints, and floats are type-inferred. ## ## Spec ref: #560 (Sprint 20 — unify duplicate YAML parsers), D-030 (testability). ## Parse YAML text into a hierarchical Dictionary. ## Nested sections become nested dicts. Array items (- key: val) become Arrays. ## Values are type-inferred: bool, int, float, or String. static func parse(text: String) -> Dictionary: var root: Dictionary = {} # Stack: [{indent: int, key: String}] — path of open section headers var stack: Array = [] # Array state var current_array: Variant = null # Array being built, or null var current_item: Variant = null # Dict being built for current array item, or null var array_parent_indent: int = -1 # indent of the "key:" line that owns the array for raw_line in text.split("\n"): var stripped := raw_line.strip_edges(false, true) if stripped.is_empty() or stripped.strip_edges().begins_with("#"): continue var indent: int = raw_line.length() - raw_line.lstrip(" ").length() var content: String = stripped.strip_edges() # --- Array item (- key: value) --- if content.begins_with("- "): # First item: convert parent section's {} placeholder to [] if current_array == null and stack.size() > 0: var parent := _node_at(root, stack, true) var arr_key: String = stack.back()["key"] var new_arr: Array = [] parent[arr_key] = new_arr current_array = new_arr array_parent_indent = stack.back()["indent"] # Flush previous item and start a new one if current_item != null: current_array.append(current_item) current_item = {} var rest: String = content.substr(2).strip_edges() var colon: int = rest.find(":") if colon >= 0: var k: String = rest.substr(0, colon).strip_edges() var v: String = rest.substr(colon + 1).strip_edges() current_item[k] = _parse_value(v) continue # --- Continuation line within current array item --- if current_array != null and indent > array_parent_indent: var colon: int = content.find(":") if colon >= 0 and current_item != null: var k: String = content.substr(0, colon).strip_edges() var v: String = content.substr(colon + 1).strip_edges() current_item[k] = _parse_value(v) continue # --- End of array (indent has returned to array level or above) --- if current_array != null: if current_item != null: current_array.append(current_item) current_item = null current_array = null array_parent_indent = -1 if stack.size() > 0: stack.pop_back() # pop the array-owning key # --- Regular key: value or section header --- var colon: int = content.find(":") if colon < 0: continue var key: String = content.substr(0, colon).strip_edges() var val_str: String = content.substr(colon + 1).strip_edges() # Pop sections at the same or deeper indent (we're back at a shallower level) while stack.size() > 0 and stack.back()["indent"] >= indent: stack.pop_back() var node: Dictionary = _node_at(root, stack, false) if val_str.is_empty() or val_str.begins_with("#"): # Section header — create nested dict (may become Array if - items follow) node[key] = {} stack.push_back({"indent": indent, "key": key}) else: node[key] = _parse_value(val_str) # Flush the last array item if the file ended inside an array if current_array != null and current_item != null: current_array.append(current_item) return root ## Convenience: parse text and flatten to dotted-key format in one call. ## Used by UIStrings._parse_yaml() — equivalent to flatten(parse(text)). static func parse_flat(text: String) -> Dictionary: return flatten(parse(text)) ## Flatten a hierarchical dict to dotted-key format (for UIStrings compatibility). ## {"a": {"b": "v"}} → {"a.b": "v"} ## Arrays are skipped — dotted-key format does not represent them. ## All values are converted to String (UIStrings stores display text, not typed data). static func flatten(d: Dictionary, prefix: String = "") -> Dictionary: var result: Dictionary = {} for k in d: var full_key: String = (prefix + "." if not prefix.is_empty() else "") + str(k) var v = d[k] if v is Dictionary: result.merge(flatten(v, full_key)) elif not v is Array: result[full_key] = str(v) return result ## Parse a single YAML value string into a typed GDScript value. ## Strips inline comments, handles quoted strings, infers bool/int/float/String. static func _parse_value(val: String) -> Variant: if val.is_empty(): return "" # Strip inline comment outside quotes if not val.begins_with('"'): var comment_pos: int = val.find(" #") if comment_pos >= 0: val = val.substr(0, comment_pos).strip_edges() # Quoted string — extract content between quotes if val.begins_with('"'): var end_quote: int = val.find('"', 1) if end_quote > 0: return val.substr(1, end_quote - 1) return val.substr(1) # Boolean if val == "true": return true if val == "false": return false # Float (must have decimal point) if val.contains(".") and val.is_valid_float(): return val.to_float() # Integer if val.is_valid_int(): return val.to_int() # Plain string return val ## Navigate root following the stack key path. ## parent=true: navigate one level less (returns the parent node, not the leaf). static func _node_at(root: Dictionary, stack: Array, parent: bool) -> Dictionary: var node: Dictionary = root var depth: int = stack.size() - (1 if parent else 0) for i in range(depth): var k: String = stack[i]["key"] if node.has(k) and node[k] is Dictionary: node = node[k] else: break return node