1 // Ged2Web converts a GEDCOM file into a SPA webpage that can be simply added to any existing website 2 package main // import "vimagination.zapto.org/ged2web" 3 4 import ( 5 "flag" 6 "fmt" 7 "io" 8 "os" 9 "strconv" 10 "strings" 11 12 "vimagination.zapto.org/gedcom" 13 "vimagination.zapto.org/rwcount" 14 ) 15 16 func main() { 17 if err := run(); err != nil { 18 fmt.Fprintln(os.Stderr, err) 19 os.Exit(1) 20 } 21 } 22 23 type idMap map[gedcom.Xref]uint64 24 25 func (i idMap) GetID(ref gedcom.Xref) uint64 { 26 id, ok := i[ref] 27 if !ok { 28 id = uint64(len(i)) + 1 29 i[ref] = id 30 } 31 return id 32 } 33 34 type data []string 35 36 type gedcomData []data 37 38 func (g *gedcomData) Set(id uint64, data data) { 39 if min := id + 1; min >= uint64(len(*g)) { 40 if min >= uint64(cap(*g)) { 41 h := make(gedcomData, min, min*2) 42 copy(h, *g) 43 *g = h 44 } else { 45 *g = (*g)[:min] 46 } 47 } 48 (*g)[id] = data 49 } 50 51 func (g gedcomData) WriteTo(w io.Writer) { 52 for n, d := range g { 53 if n == 0 { 54 io.WriteString(w, "[") 55 } else { 56 io.WriteString(w, ",[") 57 } 58 for m, p := range d { 59 if m > 0 { 60 io.WriteString(w, ",") 61 } 62 io.WriteString(w, p) 63 } 64 io.WriteString(w, "]") 65 } 66 } 67 68 func run() error { 69 var ( 70 err error 71 input = flag.String("i", "-", "gedcom file") 72 output = flag.String("o", "-", "output js file") 73 html = flag.Bool("h", false, "create full HTML file") 74 module = flag.Bool("m", false, "just create gecom module") 75 f = os.Stdin 76 w = os.Stdout 77 ) 78 flag.Parse() 79 if *input != "-" { 80 if f, err = os.Open(*input); err != nil { 81 return fmt.Errorf("error opening input file (%s): %w", *input, err) 82 } 83 } 84 indis, fams, err := processGedcom(f) 85 f.Close() 86 if err != nil { 87 return err 88 } 89 if *output != "-" { 90 if w, err = os.Create(*output); err != nil { 91 return fmt.Errorf("error creating output file (%s): %w", *output, err) 92 } 93 } 94 wr := &rwcount.Writer{Writer: w} 95 if *html { 96 io.WriteString(wr, htmlStart) 97 io.WriteString(wr, jsStart) 98 } else if *module { 99 io.WriteString(wr, modStart) 100 } else { 101 io.WriteString(wr, jsStart) 102 } 103 indis.WriteTo(wr) 104 if *module { 105 io.WriteString(wr, modMid) 106 } else { 107 io.WriteString(wr, jsMid) 108 } 109 fams.WriteTo(wr) 110 if *html { 111 io.WriteString(wr, jsEnd) 112 io.WriteString(wr, htmlEnd) 113 } else if *module { 114 io.WriteString(wr, modEnd) 115 } else { 116 io.WriteString(wr, jsEnd) 117 } 118 if wr.Err != nil { 119 return fmt.Errorf("error writing to output file (%s): %w", *output, wr.Err) 120 } 121 if err := w.Close(); err != nil { 122 return fmt.Errorf("error closing output file (%s): %w", *output, err) 123 } 124 return nil 125 } 126 127 func processGedcom(f io.Reader) (gedcomData, gedcomData, error) { 128 indiIDs := make(idMap) 129 famIDs := make(idMap) 130 indis := gedcomData{data{"", "", "", "", "0", "0"}} 131 fams := gedcomData{data{"0", "0"}} 132 r := gedcom.NewReader(f, gedcom.AllowMissingRequired, gedcom.IgnoreInvalidValue, gedcom.AllowUnknownCharset, gedcom.AllowTerminatorsInValue, gedcom.AllowWrongLength, gedcom.AllowInvalidEscape, gedcom.AllowInvalidChars) 133 for { 134 record, err := r.Record() 135 if err != nil { 136 if err == io.EOF { 137 break 138 } 139 return nil, nil, fmt.Errorf("error reading GEDCOM record: %w", err) 140 } 141 switch t := record.(type) { 142 case *gedcom.Individual: 143 person := append(make(data, 4, 6+len(t.SpouseOf)), "0", "0") 144 if len(t.PersonalNameStructure) > 0 { 145 name := strings.Split(string(t.PersonalNameStructure[0].NamePersonal), "/") 146 var firstName, lastName string 147 if t.Death.Date == "" { 148 firstName = strings.Split(name[0], " ")[0] 149 } else { 150 firstName = strings.TrimSpace(name[0]) 151 } 152 if len(name) > 1 { 153 lastName = strings.TrimSpace(name[1]) 154 } 155 person[0] = strconv.Quote(firstName) 156 person[1] = strconv.Quote(lastName) 157 } 158 if t.Death.Date != "" { 159 person[2] = strconv.Quote(strings.TrimSpace(string(t.Birth.Date))) 160 person[3] = strconv.Quote(strings.TrimSpace(string(t.Death.Date))) 161 } 162 switch t.Gender { 163 case "F", "f", "Female", "FEMALE", "female": 164 person[4] = "2" 165 case "M", "m", "Male", "MALE", "male": 166 person[4] = "1" 167 } 168 if len(t.ChildOf) > 0 { 169 person[5] = strconv.FormatUint(famIDs.GetID(t.ChildOf[0].ID), 10) 170 } 171 for _, spouse := range t.SpouseOf { 172 person = append(person, strconv.FormatUint(famIDs.GetID(spouse.ID), 10)) 173 } 174 indis.Set(indiIDs.GetID(t.ID), person) 175 case *gedcom.Family: 176 family := append(make(data, 0, 2+len(t.Children)), "0", "0") 177 if t.Husband != "" { 178 family[0] = strconv.FormatUint(indiIDs.GetID(t.Husband), 10) 179 } 180 if t.Wife != "" { 181 family[1] = strconv.FormatUint(indiIDs.GetID(t.Wife), 10) 182 } 183 for _, child := range t.Children { 184 family = append(family, strconv.FormatUint(indiIDs.GetID(child), 10)) 185 } 186 fams.Set(famIDs.GetID(t.ID), family) 187 } 188 } 189 return indis, fams, nil 190 } 191