import Foundation import Yams /// One top-level frontmatter entry: what YAML made of it plus the text as written. public struct FrontmatterField: Sendable, Equatable { public let key: String public let value: YAMLValue public let rawValue: String } /// The result of reading a typed field. `malformed` is what keeps strict fields (`schema`, /// `order`) from being silently coerced and lenient fields (colors, icons, `width`) from /// erroring — the loader decides which reaction each one gets. public enum FieldValue: Sendable, Equatable { case missing case valid(Value) case malformed(raw: String) public var value: Value? { if case let .valid(value) = self { return value } return nil } public var isMissing: Bool { if case .missing = self { return true } return false } public var isMalformed: Bool { if case .malformed = self { return true } return false } /// The value as written, for a malformed field. public var rawText: String? { if case let .malformed(raw) = self { return raw } return nil } } extension FrontmatterDocument { // MARK: - Strict (structure — the loader fails fast on `.malformed`) public var schema: FieldValue { read(FrontmatterKeys.schema) { if case let .int(value) = $0 { value } else { nil } } } public var order: FieldValue { read(FrontmatterKeys.order) { switch $0 { case let .int(value): Double(value) case let .double(value): value default: nil } } } // MARK: - Lenient (a malformed value is preserved and simply not used) public var title: FieldValue { read(FrontmatterKeys.title, Self.string) } public var background: FieldValue { read(FrontmatterKeys.background, Self.string) } public var icon: FieldValue { read(FrontmatterKeys.icon, Self.string) } public var iconColor: FieldValue { read(FrontmatterKeys.iconColor, Self.string) } /// Width multiplier; anything but an integer ≥ 1 is malformed and renders as the default 1. public var width: FieldValue { read(FrontmatterKeys.width) { if case let .int(value) = $0, value >= 1 { value } else { nil } } } public var created: FieldValue { read(FrontmatterKeys.created, Self.date) } public var modified: FieldValue { read(FrontmatterKeys.modified, Self.date) } public var deleted: FieldValue { read(FrontmatterKeys.deleted, Self.date) } /// Schema-owned, not an unknown key: the app clears it on every app-mediated write. public var modifiedBy: FieldValue { read(FrontmatterKeys.modifiedBy, Self.string) } // MARK: - private func read(_ key: String, _ transform: (YAMLValue) -> Value?) -> FieldValue { guard let value = value(for: key) else { return .missing } if case .null = value { return .missing } if let typed = transform(value) { return .valid(typed) } return .malformed(raw: rawValue(for: key) ?? value.description) } private static func string(_ value: YAMLValue) -> String? { if case let .string(text) = value { return text } return nil } /// A quoted timestamp reads the same as an unquoted one — same YAML 1.1 timestamp grammar. private static func date(_ value: YAMLValue) -> Date? { switch value { case let .date(date): date case let .string(text): Date.construct(from: Node.Scalar(text)) default: nil } } }