1 // Package gedcom implements a parser to read genealogical data in a standard format 2 package gedcom // import "vimagination.zapto.org/gedcom" 3 4 import ( 5 "errors" 6 "io" 7 "strconv" 8 9 "vimagination.zapto.org/parser" 10 ) 11 12 type line struct { 13 level uint64 14 xrefID string 15 tag string 16 value string 17 } 18 19 // Reader reads Records from the underlying GEDCOM file 20 type Reader struct { 21 t *tokeniser 22 23 line line 24 err error 25 26 options options 27 28 peeked bool 29 done bool 30 31 hadHeader, hadRecord bool 32 } 33 34 // NewReader creates a new Reader, setting the given options 35 func NewReader(r io.Reader, opts ...Option) *Reader { 36 var o options 37 for _, opt := range opts { 38 opt(&o) 39 } 40 return &Reader{ 41 t: newTokeniser(r, o), 42 options: o, 43 } 44 } 45 46 func (r *Reader) readLine() { 47 if r.err != nil { 48 return 49 } 50 var t parser.Token 51 t, r.err = r.t.GetToken() 52 if r.err != nil { 53 return 54 } 55 if t.Type != tokenLevel { 56 if t.Type == parser.TokenDone { 57 r.done = true 58 return 59 } 60 r.err = ErrNotLevel 61 return 62 } 63 r.line.level, r.err = strconv.ParseUint(t.Data, 10, 64) 64 if r.err != nil { 65 return 66 } 67 t, r.err = r.t.GetToken() 68 if r.err != nil { 69 return 70 } 71 if t.Type == tokenXref { 72 r.line.xrefID = t.Data 73 t, r.err = r.t.GetToken() 74 if r.err != nil { 75 return 76 } 77 } else { 78 r.line.xrefID = "" 79 } 80 if t.Type != tokenTag { 81 r.err = ErrNotTag 82 return 83 } 84 r.line.tag = t.Data 85 t, r.err = r.t.GetToken() 86 if r.err != nil { 87 return 88 } 89 if t.Type == tokenEndLine { 90 r.line.value = "" 91 return 92 } 93 if t.Type != tokenLine && t.Type != tokenPointer { 94 r.err = ErrNotLine 95 return 96 } 97 r.line.value = t.Data 98 } 99 100 // Record returns a GEDCOM Record. 101 // Record types are: - 102 // *Header 103 // *Submission 104 // *Family 105 // *Invididual 106 // *MultimediaNote 107 // *Repository 108 // *Source 109 // *Submitter 110 // *Trailer 111 func (r *Reader) Record() (Record, error) { 112 if !r.peeked { 113 r.readLine() 114 r.peeked = true 115 } 116 if r.done { 117 return nil, io.EOF 118 } 119 if r.err != nil { 120 return nil, r.err 121 } 122 if !r.hadHeader { 123 if r.line.tag != "HEAD" { 124 r.peeked = false 125 return nil, ErrNoHeader 126 } 127 r.hadHeader = true 128 } else if !r.hadRecord { 129 switch r.line.tag { 130 case "FAM", "INDI", "OBJE", "NOTE", "REPO", "SOUR", "SUBN": 131 r.hadRecord = true 132 case "TRLR": 133 r.peeked = false 134 return nil, ErrNoRecords 135 } 136 } else if r.line.tag == "TRLR" { 137 r.peeked = false 138 return &Trailer{}, nil 139 } 140 141 lines := make([]line, 1, 32) 142 lines[0] = r.line 143 var lastlevel uint64 144 for { 145 if r.err != nil { 146 return nil, r.err 147 } 148 if r.line.level > lastlevel+1 { 149 return nil, ErrInvalidLevel 150 } 151 lastlevel = r.line.level 152 lines = append(lines, r.line) 153 r.readLine() 154 if r.line.level == 0 { 155 plines := parseLines(lines) 156 var record Record 157 switch lines[0].tag { 158 case "HEAD": 159 record = &Header{} 160 case "SUBM": 161 record = &SubmissionRecord{} 162 case "FAM": 163 record = &Family{} 164 case "INDI": 165 record = &Individual{} 166 case "OBJE": 167 record = &MultimediaRecord{} 168 case "NOTE": 169 record = &NoteRecord{} 170 case "REPO": 171 record = &RepositoryRecord{} 172 case "SOUR": 173 record = &SourceRecord{} 174 case "SUBN": 175 record = &SubmitterRecord{} 176 default: 177 if lines[0].tag[0] == '_' { 178 return plines, nil 179 } 180 return plines, ErrContext{"root", lines[0].tag, ErrUnknownTag} 181 } 182 err := record.parse(&plines, r.options) 183 if err != nil { 184 return nil, ErrContext{"root", lines[0].tag, err} 185 } 186 return record, nil 187 } 188 } 189 190 } 191 192 // Errors 193 var ( 194 ErrNoHeader = errors.New("no header") 195 ErrNoRecords = errors.New("no records") 196 ErrNotLevel = errors.New("not level token") 197 ErrNotTag = errors.New("not tag token") 198 ErrNotLine = errors.New("not line_value token") 199 ) 200