diff --git a/Sources/Gedcom/AddressStructure.swift b/Sources/Gedcom/AddressStructure.swift index 0dd6f9d..c7fa71c 100644 --- a/Sources/Gedcom/AddressStructure.swift +++ b/Sources/Gedcom/AddressStructure.swift @@ -27,7 +27,8 @@ n ADDR {1:1} g7:ADDR +1 CTRY {0:1} g7:CTRY */ -public class AddressStructure : RecordProtocol { +public class AddressStructure : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var address: String public var adr1: String? public var adr2: String? @@ -56,7 +57,7 @@ public class AddressStructure : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -90,6 +91,12 @@ public class AddressStructure : RecordProtocol { record.children.append(Record(level: 1, tag: "CTRY", value: country)) } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } diff --git a/Sources/Gedcom/Dates.swift b/Sources/Gedcom/Dates.swift index 8dd6981..21c0a47 100644 --- a/Sources/Gedcom/Dates.swift +++ b/Sources/Gedcom/Dates.swift @@ -24,7 +24,8 @@ +1 <> {0:M} */ -public class DateTime : RecordProtocol { +public class DateTime : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: String = "" public var parsedDate: GedcomDate? { GedcomDateParser.parse(date) } public var time: String? @@ -50,7 +51,7 @@ public class DateTime : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -67,11 +68,15 @@ public class DateTime : RecordProtocol { let timeRecord = Record(level: 1, tag: "TIME", value: time) record.children.append(timeRecord) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class DateTimeExact : RecordProtocol { +public class DateTimeExact : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: String = "" public var parsedDate: GedcomDate? { GedcomDateParser.parse(date) } public var time: String? @@ -95,7 +100,7 @@ public class DateTimeExact : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -112,11 +117,15 @@ public class DateTimeExact : RecordProtocol { let timeRecord = Record(level: 1, tag: "TIME", value: time) record.children.append(timeRecord) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class DateValue : RecordProtocol { +public class DateValue : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: String = "" public var parsedDate: GedcomDate? { GedcomDateParser.parse(date) } public var time: String? @@ -135,7 +144,7 @@ public class DateValue : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -155,11 +164,15 @@ public class DateValue : RecordProtocol { let phraseRecord = Record(level: 1, tag: "PHRASE", value: phrase) record.children.append(phraseRecord) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class DatePeriod : RecordProtocol { +public class DatePeriod : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: String = "" public var parsedDate: GedcomDate? { GedcomDateParser.parse(date) } public var time: String? @@ -178,7 +191,7 @@ public class DatePeriod : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -198,12 +211,16 @@ public class DatePeriod : RecordProtocol { let phraseRecord = Record(level: 1, tag: "PHRASE", value: phrase) record.children.append(phraseRecord) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class CreationDate : RecordProtocol { +public class CreationDate : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: DateTime = DateTime() nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ "DATE" : \CreationDate.date, @@ -217,7 +234,7 @@ public class CreationDate : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -231,10 +248,14 @@ public class CreationDate : RecordProtocol { let record = Record(level: 0, tag: "CREA") let exportedDate = date.export() record.children.append(exportedDate) + for node in extensions { + record.children.append(node.export()) + } return record } } -public class ChangeDate : RecordProtocol { +public class ChangeDate : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: DateTime = DateTime() public var notes: [NoteStructure] = [] @@ -252,7 +273,7 @@ public class ChangeDate : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -272,6 +293,9 @@ public class ChangeDate : RecordProtocol { let exportedNote = note.export() record.children.append(exportedNote) } + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/Family.swift b/Sources/Gedcom/Family.swift index e8cb1c7..b2a928b 100644 --- a/Sources/Gedcom/Family.swift +++ b/Sources/Gedcom/Family.swift @@ -17,7 +17,8 @@ // import Foundation -public class LdsSpouseSealing : RecordProtocol { +public class LdsSpouseSealing : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: DateValue? public var temple: String? public var place: PlaceStructure? @@ -58,7 +59,7 @@ public class LdsSpouseSealing : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -102,12 +103,19 @@ public class LdsSpouseSealing : RecordProtocol { record.children += [citation.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class SpouseAge : RecordProtocol { +public class SpouseAge : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] var kind: String public var age: Age @@ -126,7 +134,7 @@ public class SpouseAge : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -141,6 +149,12 @@ public class SpouseAge : RecordProtocol { record.children += [age.export()] + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -152,7 +166,8 @@ public enum FamilyAttributeKind : String { case FACT } -public class FamilyAttribute : RecordProtocol { +public class FamilyAttribute : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: FamilyAttributeKind public var text: String? public var type: String? @@ -230,7 +245,7 @@ public class FamilyAttribute : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -339,6 +354,12 @@ public class FamilyAttribute : RecordProtocol { record.children += [Record(level: 1, tag: "UID", value: uuid.uuidString.lowercased())] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -357,7 +378,8 @@ public enum FamilyEventKind : String { case EVEN } -public class NonFamilyEventStructure : RecordProtocol { +public class NonFamilyEventStructure : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: FamilyEventKind public var date: DatePeriod? public var notes: [NoteStructure] = [] @@ -390,7 +412,7 @@ public class NonFamilyEventStructure : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -419,11 +441,18 @@ public class NonFamilyEventStructure : RecordProtocol { record.children += [citation.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class FamilyEvent : RecordProtocol { +public class FamilyEvent : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: FamilyEventKind public var text: String? public var occured: Bool // Text == Y @@ -541,7 +570,7 @@ public class FamilyEvent : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -660,12 +689,19 @@ public class FamilyEvent : RecordProtocol { record.children += [Record(level: 1, tag: "UID", value: uuid.uuidString.lowercased())] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class Family : RecordProtocol { +public class Family : RecordProtocol, GedcomExtensionContainer { public var xref: String + public var extensions: [GedcomExtensionNode] = [] public var restrictions: [Restriction] = [] public var attributes: [FamilyAttribute] = [] @@ -742,7 +778,7 @@ public class Family : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -850,7 +886,14 @@ public class Family : RecordProtocol { record.children += [creationDate.export()] } + for node in extensions { + record.children.append(node.export()) + } + record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/GedcomExtension.swift b/Sources/Gedcom/GedcomExtension.swift new file mode 100644 index 0000000..7343fd8 --- /dev/null +++ b/Sources/Gedcom/GedcomExtension.swift @@ -0,0 +1,121 @@ +// +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2026 Mattias Holm +// +// 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. +// + +import Foundation + +public struct GedcomExtensionNode: Codable, Equatable { + public var level: Int + public var xref: String? + public var tag: String + public var value: String? + public var children: [GedcomExtensionNode] + + public init(level: Int = 0, + xref: String? = nil, + tag: String, + value: String? = nil, + children: [GedcomExtensionNode] = []) { + self.level = level + self.xref = xref + self.tag = tag + self.value = value + self.children = children + } + + init(record: Record) { + self.level = record.line.level + self.xref = record.line.xref + self.tag = record.line.tag + self.value = record.line.value + self.children = record.children.map(GedcomExtensionNode.init(record:)) + } + + func export() -> Record { + let record = Record(level: level, xref: xref, tag: tag, value: value) + record.children = children.map { $0.export() } + return record + } + + public var extensionTags: Set { + var tags = tag.hasPrefix("_") ? Set([tag]) : Set() + for child in children { + tags.formUnion(child.extensionTags) + } + return tags + } + + public var flattened: [GedcomExtensionNode] { + [self] + children.flatMap(\.flattened) + } +} + +public protocol GedcomExtensionContainer { + var extensions: [GedcomExtensionNode] { get set } +} + +public extension GedcomExtensionContainer { + func extensions(tag: String) -> [GedcomExtensionNode] { + extensions.filter { $0.tag == tag } + } + + func firstExtension(tag: String) -> GedcomExtensionNode? { + extensions.first { $0.tag == tag } + } + + var extensionTags: Set { + extensions.reduce(into: Set()) { tags, node in + tags.formUnion(node.extensionTags) + } + } +} + +public enum GedcomExtensionURI { + public static let synthesizedBase = "https://openorbit.org/gedcom/extensions" + + public static func synthesized(tag: String, source: String?) -> URL { + let slug = source.flatMap(vendorSlug) + let namespace = slug.map { "vendor-\($0)" } ?? "unknown" + return URL(string: "\(synthesizedBase)/\(namespace)/\(tag)")! + } + + public static func vendorSlug(from source: String) -> String? { + let lowercased = source.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + var result = "" + var previousWasSeparator = false + + for scalar in lowercased.unicodeScalars { + let value = scalar.value + let isLetter = value >= 97 && value <= 122 + let isDigit = value >= 48 && value <= 57 + + if isLetter || isDigit { + result.unicodeScalars.append(scalar) + previousWasSeparator = false + } else if !previousWasSeparator && !result.isEmpty { + result.append("-") + previousWasSeparator = true + } + } + + while result.last == "-" { + result.removeLast() + } + + return result.isEmpty ? nil : result + } +} diff --git a/Sources/Gedcom/GedcomFile.swift b/Sources/Gedcom/GedcomFile.swift index 4d51300..d855dc2 100644 --- a/Sources/Gedcom/GedcomFile.swift +++ b/Sources/Gedcom/GedcomFile.swift @@ -33,6 +33,7 @@ public class GedcomFile { public var sharedNoteRecords: [SharedNote] = [] public var sourceRecords: [Source] = [] public var submitterRecords: [Submitter] = [] + public var extensionRecords: [GedcomExtensionNode] = [] public var familyRecordsMap: [String: Family] = [:] public var individualRecordsMap: [String: Individual] = [:] @@ -154,6 +155,9 @@ public class GedcomFile { var mutableSelf = self for record in recordLines { guard let kp = Self.keys[record.line.tag] else { + if record.line.tag != "TRLR" { + extensionRecords.append(GedcomExtensionNode(record: record)) + } continue } if let wkp = kp as? WritableKeyPath { @@ -198,10 +202,63 @@ public class GedcomFile { } } - public func exportContent() -> String { - var records: [Record] = [] + public var extensionSchema: [String: URL] { + header.schema?.tags ?? [:] + } - records += [header.export()] + public func uri(forExtensionTag tag: String) -> URL? { + header.schema?.uri(forExtensionTag: tag) + } + + public func extensionNodes(tag: String) -> [GedcomExtensionNode] { + extensionNodes(tag: tag, in: [header.export()] + exportBodyRecords()) + } + + public func extensionNodes(uri: URL) -> [GedcomExtensionNode] { + guard let tag = header.schema?.extensionTag(forURI: uri) else { return [] } + return extensionNodes(tag: tag) + } + + public func discoveredExtensionTags() -> Set { + extensionTags(in: [header.export()] + exportBodyRecords()) + } + + private func extensionNodes(tag: String, in records: [Record]) -> [GedcomExtensionNode] { + records.flatMap { extensionNodes(tag: tag, in: $0) } + } + + private func extensionNodes(tag: String, in record: Record) -> [GedcomExtensionNode] { + var nodes: [GedcomExtensionNode] = record.line.tag == tag ? [GedcomExtensionNode(record: record)] : [] + for child in record.children { + nodes += extensionNodes(tag: tag, in: child) + } + return nodes + } + + private func extensionTags(in records: [Record]) -> Set { + records.reduce(into: Set()) { tags, record in + tags.formUnion(extensionTags(in: record)) + } + } + + private func extensionTags(in record: Record) -> Set { + var tags = record.line.tag.hasPrefix("_") ? Set([record.line.tag]) : Set() + for child in record.children { + tags.formUnion(extensionTags(in: child)) + } + return tags + } + + private func ensureExtensionSchemaDeclarations(for tags: Set) { + guard !tags.isEmpty else { return } + if header.schema == nil { + header.schema = Schema() + } + header.schema?.ensureDeclarations(for: tags, source: header.source?.source) + } + + private func exportBodyRecords() -> [Record] { + var records: [Record] = [] for fam in familyRecords { records += [fam.export()] @@ -231,6 +288,22 @@ public class GedcomFile { records += [submitter.export()] } + for node in extensionRecords { + records += [node.export()] + } + + return records + } + + public func exportContent() -> String { + let bodyRecords = exportBodyRecords() + ensureExtensionSchemaDeclarations(for: extensionTags(in: [header.export()] + bodyRecords)) + + var records: [Record] = [] + + records += [header.export()] + records += bodyRecords + records += [Record(level: 0, tag: "TRLR")] var result = "" diff --git a/Sources/Gedcom/Header.swift b/Sources/Gedcom/Header.swift index b360cf6..c099694 100644 --- a/Sources/Gedcom/Header.swift +++ b/Sources/Gedcom/Header.swift @@ -16,7 +16,8 @@ // limitations under the License. // import Foundation -public class Gedc : RecordProtocol { +public class Gedc : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var vers: String = "7.0" nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ @@ -30,7 +31,7 @@ public class Gedc : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -43,13 +44,31 @@ public class Gedc : RecordProtocol { func export() -> Record { let record = Record(level: 0, tag: "GEDC") record.children.append(Record(level: 1, tag: "VERS", value: vers)) + for node in extensions { + record.children.append(node.export()) + } return record } } -public class Schema : RecordProtocol { +public class Schema : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var tags: [String: URL] = [:] + + public func uri(forExtensionTag tag: String) -> URL? { + tags[tag] + } + + public func extensionTag(forURI uri: URL) -> String? { + tags.first { $0.value == uri }?.key + } + + public func ensureDeclarations(for discoveredTags: Set, source: String?) { + for tag in discoveredTags.sorted() where tag.hasPrefix("_") && tags[tag] == nil { + tags[tag] = GedcomExtensionURI.synthesized(tag: tag, source: source) + } + } nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ "TAG" : \Schema.tags, ] @@ -61,7 +80,7 @@ public class Schema : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -89,12 +108,16 @@ public class Schema : RecordProtocol { for key in tags.keys.sorted() { record.children.append(Record(level: 1, tag: "TAG", value: key + " " + tags[key]!.absoluteString)) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class HeaderPlace : RecordProtocol { +public class HeaderPlace : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var form: [String] = [] nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ "FORM" : \HeaderPlace.form, @@ -108,7 +131,7 @@ public class HeaderPlace : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -126,11 +149,15 @@ public class HeaderPlace : RecordProtocol { return acc + ((acc.count > 1) ? ", " : "") + token } record.children.append(Record(level: 1, tag: "FORM", value: formString)) + for node in extensions { + record.children.append(node.export()) + } return record } } -public class HeaderSourceCorporation : RecordProtocol { +public class HeaderSourceCorporation : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var corporation: String = "" public var address: AddressStructure? public var phone: [String] = [] @@ -156,7 +183,7 @@ public class HeaderSourceCorporation : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -190,11 +217,15 @@ public class HeaderSourceCorporation : RecordProtocol { for www in www { record.children.append(Record(level: 1, tag: "WWW", value: www.absoluteString)) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class HeaderSourceData : RecordProtocol { +public class HeaderSourceData : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var data: String = "" public var date: DateTimeExact? public var copyright: String? @@ -215,7 +246,7 @@ public class HeaderSourceData : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -239,11 +270,15 @@ public class HeaderSourceData : RecordProtocol { let copyrightRecord = Record(level: 1, tag: "COPR", value: copyright) record.children.append(copyrightRecord) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class HeaderSource : RecordProtocol { +public class HeaderSource : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var source: String = "" public var version: String? public var name: String? @@ -266,7 +301,7 @@ public class HeaderSource : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -298,14 +333,18 @@ public class HeaderSource : RecordProtocol { let child = data.export() record.children.append(child) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class Header : RecordProtocol { +public class Header : RecordProtocol, GedcomExtensionContainer { public var gedc: Gedc public var schema : Schema? public var source: HeaderSource? + public var extensions: [GedcomExtensionNode] = [] public var date: DateTimeExact? public var destination: String? @@ -340,7 +379,7 @@ public class Header : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -406,7 +445,14 @@ public class Header : RecordProtocol { record.children.append(note) } + for node in extensions { + record.children.append(node.export()) + } + record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/IdentifierStructure.swift b/Sources/Gedcom/IdentifierStructure.swift index 10cf79d..1db9ee9 100644 --- a/Sources/Gedcom/IdentifierStructure.swift +++ b/Sources/Gedcom/IdentifierStructure.swift @@ -17,7 +17,8 @@ // import Foundation -public class REFN : RecordProtocol { +public class REFN : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var refn: String = "" public var type: String? @@ -35,7 +36,7 @@ public class REFN : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -50,11 +51,15 @@ public class REFN : RecordProtocol { if let type { record.children.append(Record(level: 1, tag: "TYPE", value: type)) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class UID : RecordProtocol { +public class UID : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var uid: UUID init(ident: String) { @@ -67,11 +72,15 @@ public class UID : RecordProtocol { func export() -> Record { let record = Record(level: 0, tag: "UID", value: uid.uuidString.lowercased()) + for node in extensions { + record.children.append(node.export()) + } return record } } -public class EXID : RecordProtocol { +public class EXID : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var exid: String = "" public var type: String? @@ -90,7 +99,7 @@ public class EXID : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -106,6 +115,9 @@ public class EXID : RecordProtocol { if let type { record.children.append(Record(level: 1, tag: "TYPE", value: type)) } + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/Individual.swift b/Sources/Gedcom/Individual.swift index 38a4772..bdf11fe 100644 --- a/Sources/Gedcom/Individual.swift +++ b/Sources/Gedcom/Individual.swift @@ -17,7 +17,8 @@ // import Foundation -public class Age : RecordProtocol { +public class Age : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var age: String public var parsedAge: GedcomAge? { GedcomAgeParser.parse(age) } public var phrase: String? @@ -41,7 +42,7 @@ public class Age : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -58,6 +59,12 @@ public class Age : RecordProtocol { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -79,7 +86,8 @@ public enum IndividualAttributeKind : String { case FACT = "FACT" } -public class IndividualAttributeStructure : RecordProtocol { +public class IndividualAttributeStructure : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: IndividualAttributeKind public var text: String? public var type: String? @@ -184,7 +192,7 @@ public class IndividualAttributeStructure : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -290,6 +298,12 @@ public class IndividualAttributeStructure : RecordProtocol { record.children += [Record(level: 1, tag: "UID", value: uuid.uuidString.lowercased())] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -320,7 +334,8 @@ public enum IndividualEventKind : String { case EVEN = "EVEN" } -public class NonEventStructure : RecordProtocol { +public class NonEventStructure : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: IndividualEventKind public var date: DatePeriod? public var notes: [NoteStructure] = [] @@ -353,7 +368,7 @@ public class NonEventStructure : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -382,11 +397,18 @@ public class NonEventStructure : RecordProtocol { record.children += [citation.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class IndividualEvent : RecordProtocol { +public class IndividualEvent : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: IndividualEventKind public var text: String? public var occurred: Bool // Text == Y @@ -505,7 +527,7 @@ public class IndividualEvent : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -626,6 +648,12 @@ public class IndividualEvent : RecordProtocol { record.children += [Record(level: 1, tag: "UID", value: uuid.uuidString.lowercased())] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -653,7 +681,8 @@ public enum LdsOrdinanceStatusKind : String { case UNCLEARED } -public class LdsOrdinanceStatus : RecordProtocol { +public class LdsOrdinanceStatus : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: LdsOrdinanceStatusKind public var date: DateTime @@ -673,7 +702,7 @@ public class LdsOrdinanceStatus : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -686,11 +715,15 @@ public class LdsOrdinanceStatus : RecordProtocol { func export() -> Record { let record = Record(level: 0, tag: "STAT", value: kind.rawValue) record.children += [date.export()] + for node in extensions { + record.children.append(node.export()) + } return record } } -public class LdsIndividualOrdinance : RecordProtocol { +public class LdsIndividualOrdinance : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: LdsIndividualOrdinanceKind public var date: DateValue? @@ -746,7 +779,7 @@ public class LdsIndividualOrdinance : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -795,6 +828,15 @@ public class LdsIndividualOrdinance : RecordProtocol { } + for node in extensions { + + + record.children.append(node.export()) + + + } + + return record } } @@ -809,7 +851,8 @@ public enum NameTypeKind : String { case OTHER = "OTHER" } -public class NameType : RecordProtocol { +public class NameType : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: NameTypeKind public var phrase: String? @@ -827,7 +870,7 @@ public class NameType : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -844,6 +887,12 @@ public class NameType : RecordProtocol { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -876,7 +925,8 @@ extension PersonalNamePiece { } } -public class PersonalNameTranslation : RecordProtocol { +public class PersonalNameTranslation : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var name: String public var lang: String = "" public var namePieces : [PersonalNamePiece] = [] @@ -903,7 +953,7 @@ public class PersonalNameTranslation : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -938,11 +988,18 @@ public class PersonalNameTranslation : RecordProtocol { record.children += [namePiece.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class PersonalName : RecordProtocol { +public class PersonalName : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var name: String public var parsedName: GedcomPersonalName? @@ -987,7 +1044,7 @@ public class PersonalName : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1043,6 +1100,12 @@ public class PersonalName : RecordProtocol { record.children += [citation.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -1056,7 +1119,8 @@ public enum PedigreeKind : String { case OTHER // TODO: Needs payload } -public class Pedigree : RecordProtocol { +public class Pedigree : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: PedigreeKind public var phrase: String? @@ -1075,7 +1139,7 @@ public class Pedigree : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1090,6 +1154,9 @@ public class Pedigree : RecordProtocol { if let phrase { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + record.children.append(node.export()) + } return record } } @@ -1104,7 +1171,8 @@ public enum AdoptionKind : String { case WIFE case BOTH } -public class FamilyChildAdoptionKind : RecordProtocol { +public class FamilyChildAdoptionKind : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: AdoptionKind public var phrase: String? @@ -1121,7 +1189,7 @@ public class FamilyChildAdoptionKind : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1136,11 +1204,15 @@ public class FamilyChildAdoptionKind : RecordProtocol { if let phrase { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class FamilyChildAdoption : RecordProtocol { +public class FamilyChildAdoption : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var adoption: FamilyChildAdoptionKind? @@ -1157,7 +1229,7 @@ public class FamilyChildAdoption : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1172,11 +1244,15 @@ public class FamilyChildAdoption : RecordProtocol { if let adoption { record.children += [adoption.export()] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class ChildStatus : RecordProtocol { +public class ChildStatus : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: ChildStatusKind public var phrase: String? @@ -1193,7 +1269,7 @@ public class ChildStatus : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1208,11 +1284,15 @@ public class ChildStatus : RecordProtocol { if let phrase { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class FamilyChild : RecordProtocol { +public class FamilyChild : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var pedigree: Pedigree? public var status: ChildStatus? @@ -1241,7 +1321,7 @@ public class FamilyChild : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1268,10 +1348,14 @@ public class FamilyChild : RecordProtocol { for note in notes { record.children += [note.export()] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class FamilySpouse : RecordProtocol { +public class FamilySpouse : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var notes: [NoteStructure] = [] @@ -1291,7 +1375,7 @@ public class FamilySpouse : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1306,6 +1390,9 @@ public class FamilySpouse : RecordProtocol { for note in notes { record.children += [note.export()] } + for node in extensions { + record.children.append(node.export()) + } return record } } @@ -1328,7 +1415,8 @@ public enum RoleKind : String { case OTHER } -public class Role : RecordProtocol { +public class Role : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: RoleKind public var phrase: String? @@ -1346,7 +1434,7 @@ public class Role : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1361,11 +1449,15 @@ public class Role : RecordProtocol { if let phrase = phrase { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class AssoiciationStructure : RecordProtocol { +public class AssoiciationStructure : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var phrase: String? public var role: Role? @@ -1397,7 +1489,7 @@ public class AssoiciationStructure : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1432,10 +1524,17 @@ public class AssoiciationStructure : RecordProtocol { record.children += [citation.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class PhraseRef : RecordProtocol { +public class PhraseRef : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var tag: String public var xref: String public var phrase: String? @@ -1456,7 +1555,7 @@ public class PhraseRef : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1473,6 +1572,12 @@ public class PhraseRef : RecordProtocol { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -1484,8 +1589,9 @@ public enum Sex : String { case unknown = "U" } -public class Individual : RecordProtocol { +public class Individual : RecordProtocol, GedcomExtensionContainer { public var xref: String + public var extensions: [GedcomExtensionNode] = [] public var restrictions: [Restriction] = [] public var names: [PersonalName] = [] @@ -1598,7 +1704,7 @@ public class Individual : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -1733,7 +1839,14 @@ public class Individual : RecordProtocol { record.children += [creationDate.export()] } + for node in extensions { + record.children.append(node.export()) + } + record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/Multimedia.swift b/Sources/Gedcom/Multimedia.swift index 98fac25..abf2245 100644 --- a/Sources/Gedcom/Multimedia.swift +++ b/Sources/Gedcom/Multimedia.swift @@ -15,7 +15,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // -public class Crop : RecordProtocol { +public class Crop : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var top: Int? public var left: Int? public var height: Int? @@ -39,7 +40,7 @@ public class Crop : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -64,10 +65,17 @@ public class Crop : RecordProtocol { record.children.append(Record(level: 1, tag: "WIDTH", value: "\(width)")) } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class MultimediaLink : RecordProtocol { +public class MultimediaLink : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var crop: Crop? public var title: String? @@ -89,7 +97,7 @@ public class MultimediaLink : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -111,11 +119,18 @@ public class MultimediaLink : RecordProtocol { record.children.append(Record(level: 1, tag: "TITL", value: title)) } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class FileTranslation : RecordProtocol { +public class FileTranslation : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var path: String = "" public var form: String = "" @@ -135,7 +150,7 @@ public class FileTranslation : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -148,11 +163,15 @@ public class FileTranslation : RecordProtocol { func export() -> Record { let record = Record(level: 0, tag: "TRAN", value: path) record.children += [Record(level: 1, tag: "FORM", value: form)] + for node in extensions { + record.children.append(node.export()) + } return record } } -public class MultimediaFileForm : RecordProtocol { +public class MultimediaFileForm : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var form: String = "" public var medium: Medium? nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ @@ -169,7 +188,7 @@ public class MultimediaFileForm : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -185,10 +204,17 @@ public class MultimediaFileForm : RecordProtocol { record.children += [medium.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class MultimediaFile : RecordProtocol { +public class MultimediaFile : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var path: String public var form: MultimediaFileForm = MultimediaFileForm(form: "") @@ -212,7 +238,7 @@ public class MultimediaFile : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -241,10 +267,17 @@ public class MultimediaFile : RecordProtocol { record.children += [translation.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class Multimedia : RecordProtocol { +public class Multimedia : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var restrictions: [Restriction] = [] @@ -278,7 +311,7 @@ public class Multimedia : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -335,6 +368,9 @@ public class Multimedia : RecordProtocol { } record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/Note.swift b/Sources/Gedcom/Note.swift index dc79b11..da30500 100644 --- a/Sources/Gedcom/Note.swift +++ b/Sources/Gedcom/Note.swift @@ -16,7 +16,8 @@ // limitations under the License. // -public class Translation : RecordProtocol { +public class Translation : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var text: String public var mimeType: String? public var lang: String? @@ -39,7 +40,7 @@ public class Translation : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -60,11 +61,15 @@ public class Translation : RecordProtocol { let langRecord = Record(level: 1, tag: "LANG", value: lang) record.children.append(langRecord) } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class Note : RecordProtocol { +public class Note : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var text: String = "" public var mimeType: String? public var lang: String? @@ -91,7 +96,7 @@ public class Note : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -129,11 +134,18 @@ public class Note : RecordProtocol { record.children.append(citationRecord) } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class SNoteRef : RecordProtocol { +public class SNoteRef : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String required convenience init(record: Record) throws { diff --git a/Sources/Gedcom/PlaceStructure.swift b/Sources/Gedcom/PlaceStructure.swift index e63686f..ac98ed9 100644 --- a/Sources/Gedcom/PlaceStructure.swift +++ b/Sources/Gedcom/PlaceStructure.swift @@ -16,7 +16,8 @@ // limitations under the License. // -public class PlaceTranslation : RecordProtocol { +public class PlaceTranslation : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var place: [String] = [] public var lang: String = "" @@ -38,7 +39,7 @@ public class PlaceTranslation : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -51,11 +52,15 @@ public class PlaceTranslation : RecordProtocol { func export() -> Record { let record = Record(level: 0, tag: "TRAN", value: place.joined(separator: ", ")) record.children += [Record(level: 1, tag: "LANG", value: lang)] + for node in extensions { + record.children.append(node.export()) + } return record } } -public class PlaceCoordinates : RecordProtocol { +public class PlaceCoordinates : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var lat: Double = Double.nan public var lon: Double = Double.nan @@ -73,7 +78,7 @@ public class PlaceCoordinates : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -105,11 +110,15 @@ public class PlaceCoordinates : RecordProtocol { Record(level: 1, tag: "LATI", value: "\(lat >= 0 ? "N\(lat)" : "S\(lat * -1)")"), Record(level: 1, tag: "LONG", value: "\(lon >= 0 ? "E\(lon)" : "W\(lon * -1)")") ] + for node in extensions { + record.children.append(node.export()) + } return record } } -public class PlaceStructure : RecordProtocol { +public class PlaceStructure : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var place: [String] = [] public var form: [String] = [] public var lang: String? @@ -145,7 +154,7 @@ public class PlaceStructure : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -193,6 +202,12 @@ public class PlaceStructure : RecordProtocol { record.children += [note.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } diff --git a/Sources/Gedcom/Repository.swift b/Sources/Gedcom/Repository.swift index 29e17b9..06bd692 100644 --- a/Sources/Gedcom/Repository.swift +++ b/Sources/Gedcom/Repository.swift @@ -18,7 +18,8 @@ import Foundation -public class Repository : RecordProtocol { +public class Repository : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var name: String = "" public var address: AddressStructure? @@ -58,7 +59,7 @@ public class Repository : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -121,6 +122,9 @@ public class Repository : RecordProtocol { } record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/SharedNote.swift b/Sources/Gedcom/SharedNote.swift index eb1e746..60264af 100644 --- a/Sources/Gedcom/SharedNote.swift +++ b/Sources/Gedcom/SharedNote.swift @@ -30,7 +30,8 @@ */ -public class SharedNote : RecordProtocol { +public class SharedNote : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var text: String = "" public var mimeType: String? @@ -68,7 +69,7 @@ public class SharedNote : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -120,6 +121,9 @@ public class SharedNote : RecordProtocol { } record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/SourceRecord.swift b/Sources/Gedcom/SourceRecord.swift index eba8406..bf8fd0d 100644 --- a/Sources/Gedcom/SourceRecord.swift +++ b/Sources/Gedcom/SourceRecord.swift @@ -16,7 +16,8 @@ // limitations under the License. // -public class SourceCitationData : RecordProtocol { +public class SourceCitationData : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: DateValue? public var text: [SourceText] = [] @@ -32,7 +33,7 @@ public class SourceCitationData : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -51,10 +52,14 @@ public class SourceCitationData : RecordProtocol { for text in self.text { record.children += [text.export()] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class SourceEventRole : RecordProtocol { +public class SourceEventRole : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var role: String public var phrase: String? @@ -71,7 +76,7 @@ public class SourceEventRole : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -85,11 +90,15 @@ public class SourceEventRole : RecordProtocol { if let phrase { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class SourceEventData : RecordProtocol { +public class SourceEventData : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var event: String public var phrase: String? public var role: SourceEventRole? @@ -109,7 +118,7 @@ public class SourceEventData : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -130,11 +139,18 @@ public class SourceEventData : RecordProtocol { record.children += [role.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class SourceCitation : RecordProtocol { +public class SourceCitation : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var page: String? public var data: SourceCitationData? @@ -175,7 +191,7 @@ public class SourceCitation : RecordProtocol { var mutableSelf = self for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -217,11 +233,18 @@ public class SourceCitation : RecordProtocol { record.children.append(note.export()) } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class SourceDataEventPeriod : RecordProtocol { +public class SourceDataEventPeriod : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var date: String public var phrase: String? nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ @@ -238,7 +261,7 @@ public class SourceDataEventPeriod : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -254,11 +277,15 @@ public class SourceDataEventPeriod : RecordProtocol { if let phrase { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class SourceDataEvents : RecordProtocol { +public class SourceDataEvents : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var eventTypes: [String] = [] public var period: SourceDataEventPeriod? public var place: PlaceStructure? @@ -279,7 +306,7 @@ public class SourceDataEvents : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -301,11 +328,18 @@ public class SourceDataEvents : RecordProtocol { record.children += [place.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class SourceData : RecordProtocol { +public class SourceData : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var events: [SourceDataEvents] = [] public var agency: String? public var notes: [NoteStructure] = [] @@ -324,7 +358,7 @@ public class SourceData : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -353,6 +387,12 @@ public class SourceData : RecordProtocol { record.children += [note.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } @@ -374,7 +414,8 @@ public enum MediumKind : String { case OTHER // A value not listed here; should have a PHRASE substructure } -public class Medium : RecordProtocol { +public class Medium : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var kind: MediumKind public var phrase: String? nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ @@ -393,7 +434,7 @@ public class Medium : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -408,11 +449,15 @@ public class Medium : RecordProtocol { if let phrase = phrase { record.children += [Record(level: 1, tag: "PHRASE", value: phrase)] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class CallNumber : RecordProtocol { +public class CallNumber : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var callNumber: String public var medium: Medium? nonisolated(unsafe) static let keys : [String:AnyKeyPath] = [ @@ -428,7 +473,7 @@ public class CallNumber : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -443,11 +488,15 @@ public class CallNumber : RecordProtocol { if let medium{ record.children += [medium.export()] } + for node in extensions { + record.children.append(node.export()) + } return record } } -public class SourceRepositoryCitation : RecordProtocol { +public class SourceRepositoryCitation : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var notes: [NoteStructure] = [] public var callNumbers: [CallNumber] = [] @@ -468,7 +517,7 @@ public class SourceRepositoryCitation : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -490,11 +539,18 @@ public class SourceRepositoryCitation : RecordProtocol { record.children += [callNumber.export()] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class SourceText : RecordProtocol { +public class SourceText : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var text: String public var mimeType: String? public var lang: String? @@ -514,7 +570,7 @@ public class SourceText : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } if let wkp = kp as? WritableKeyPath { @@ -533,11 +589,18 @@ public class SourceText : RecordProtocol { record.children += [Record(level: 0, tag: "LANG", value: lang)] } + for node in extensions { + + record.children.append(node.export()) + + } + return record } } -public class Source : RecordProtocol { +public class Source : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String public var data: SourceData? public var author: String? @@ -584,7 +647,7 @@ public class Source : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -657,6 +720,9 @@ public class Source : RecordProtocol { } record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Sources/Gedcom/SubmitterReord.swift b/Sources/Gedcom/SubmitterReord.swift index 2928a25..33073ab 100644 --- a/Sources/Gedcom/SubmitterReord.swift +++ b/Sources/Gedcom/SubmitterReord.swift @@ -17,7 +17,8 @@ // import Foundation -public class Submitter : RecordProtocol { +public class Submitter : RecordProtocol, GedcomExtensionContainer { + public var extensions: [GedcomExtensionNode] = [] public var xref: String = "" public var name: String = "" public var address: AddressStructure? @@ -62,7 +63,7 @@ public class Submitter : RecordProtocol { for child in record.children { guard let kp = Self.keys[child.line.tag] else { - // throw GedcomError.badRecord + extensions.append(GedcomExtensionNode(record: child)) continue } @@ -138,6 +139,9 @@ public class Submitter : RecordProtocol { } record.setLevel(0) + for node in extensions { + record.children.append(node.export()) + } return record } } diff --git a/Tests/GedcomTests/GedcomImprovementsTests.swift b/Tests/GedcomTests/GedcomImprovementsTests.swift index 7e4c66d..6da5f3e 100644 --- a/Tests/GedcomTests/GedcomImprovementsTests.swift +++ b/Tests/GedcomTests/GedcomImprovementsTests.swift @@ -1,5 +1,6 @@ import Testing +import Foundation @testable import Gedcom @Suite struct ReproductionTaskDefault { @@ -75,6 +76,79 @@ import Testing #expect(age.phrase == "363.5 days rounded down") } + @Test func testExtensionPreservationAndSchemaLookup() throws { + let content = """ +0 HEAD +1 GEDC +2 VERS 7.0 +1 SOUR Family Tree Maker +1 SCHMA +2 TAG _LOC https://example.com/LocationRecord +0 @I1@ INDI +1 NAME John /Doe/ +1 BIRT +2 DATE 1 JAN 1900 +2 _EVENTEXT Event extension payload +1 _LOC @L1@ +2 _NOTE Nested extension payload +1 FOO Future standard-looking payload +0 @L1@ _LOC +1 NAME Paris +0 TRLR +""" + let ged = try loadGedcom(content) + + #expect(ged.uri(forExtensionTag: "_LOC") == URL(string: "https://example.com/LocationRecord")!) + #expect(ged.individualRecords.count == 1) + #expect(ged.individualRecords[0].extensions.count == 2) + #expect(ged.individualRecords[0].extensions[0].tag == "_LOC") + #expect(ged.individualRecords[0].extensions[0].value == "@L1@") + #expect(ged.individualRecords[0].extensions[0].children[0].tag == "_NOTE") + #expect(ged.individualRecords[0].extensions[1].tag == "FOO") + #expect(ged.individualRecords[0].events.first?.extensions.first?.tag == "_EVENTEXT") + #expect(ged.extensionRecords.first?.xref == "@L1@") + #expect(ged.extensionRecords.first?.tag == "_LOC") + #expect(ged.extensionNodes(tag: "_NOTE").first?.value == "Nested extension payload") + #expect(ged.extensionNodes(tag: "_EVENTEXT").first?.value == "Event extension payload") + + let exported = ged.exportContent() + #expect(exported.contains("2 TAG _LOC https://example.com/LocationRecord\n")) + #expect(exported.contains("2 TAG _NOTE https://openorbit.org/gedcom/extensions/vendor-family-tree-maker/_NOTE\n")) + #expect(exported.contains("2 TAG _EVENTEXT https://openorbit.org/gedcom/extensions/vendor-family-tree-maker/_EVENTEXT\n")) + #expect(exported.contains("2 _EVENTEXT Event extension payload\n")) + #expect(exported.contains("1 _LOC @L1@\n2 _NOTE Nested extension payload\n")) + #expect(exported.contains("1 FOO Future standard-looking payload\n")) + #expect(exported.contains("0 @L1@ _LOC\n1 NAME Paris\n")) + } + + @Test func testUndeclaredExtensionUsesUnknownNamespaceWithoutHeaderSource() throws { + let content = """ +0 HEAD +1 GEDC +2 VERS 7.0 +0 @I1@ INDI +1 _FOO Bar +0 TRLR +""" + let ged = try loadGedcom(content) + + #expect(ged.header.schema == nil) + + let exported = ged.exportContent() + #expect(exported.contains("1 SCHMA\n2 TAG _FOO https://openorbit.org/gedcom/extensions/unknown/_FOO\n")) + #expect(exported.contains("1 _FOO Bar\n")) + #expect(ged.uri(forExtensionTag: "_FOO") == URL(string: "https://openorbit.org/gedcom/extensions/unknown/_FOO")!) + } + + private func loadGedcom(_ content: String) throws -> GedcomFile { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("ged") + try content.write(to: url, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: url) } + return try GedcomFile(withFile: url) + } + /* @Test func testNameSlashHandling() { // This name has no slashes, so surname should be nil