package xmlmodels import ( "fmt" "html/template" "log/slog" "maps" "path/filepath" "slices" "strconv" "strings" "sync" gitpkg "github.com/Theodor-Springmann-Stiftung/lenz-web/git" "github.com/Theodor-Springmann-Stiftung/lenz-web/xmlparsing" ) const ( REFERENCES_PATH = "data/xml/references.xml" LETTERS_PATH = "data/xml/briefe.xml" META_PATH = "data/xml/meta.xml" TRADITIONS_PATH = "data/xml/traditions.xml" ) type Library struct { Commit gitpkg.Commit Persons *xmlparsing.XMLParser[PersonDef] Places *xmlparsing.XMLParser[LocationDef] AppDefs *xmlparsing.XMLParser[AppDef] Letters *xmlparsing.XMLParser[Letter] Traditions *xmlparsing.XMLParser[Tradition] Metas *xmlparsing.XMLParser[Meta] cache sync.Map } func (l *Library) String() string { sb := strings.Builder{} sb.WriteString("Persons: ") sb.WriteString(strconv.Itoa(l.Persons.Count())) sb.WriteString("\n") sb.WriteString("Places: ") sb.WriteString(strconv.Itoa(l.Places.Count())) sb.WriteString("\n") sb.WriteString("AppDefs: ") sb.WriteString(strconv.Itoa(l.AppDefs.Count())) sb.WriteString("\n") sb.WriteString("Letters: ") sb.WriteString(strconv.Itoa(l.Letters.Count())) filter := func(item Letter) bool { return len(item.Hands) > 0 } hands := 0 for l := range l.Letters.Filter(filter) { hands += 1 sb.WriteString("\n") sb.WriteString(strconv.Itoa(l.Letter) + ": ") sb.WriteString(strconv.Itoa(len(l.Hands)) + " hands, No " + strconv.Itoa(hands)) } sb.WriteString("\n") sb.WriteString("Traditions: ") sb.WriteString(strconv.Itoa(l.Traditions.Count())) sb.WriteString("\n") sb.WriteString("Metas: ") sb.WriteString(strconv.Itoa(l.Metas.Count())) sb.WriteString("\n") return sb.String() } func NewLibrary(baseDir string, commit *gitpkg.Commit) (*Library, error) { lib := &Library{ Persons: xmlparsing.NewXMLParser[PersonDef](), Places: xmlparsing.NewXMLParser[LocationDef](), AppDefs: xmlparsing.NewXMLParser[AppDef](), Letters: xmlparsing.NewXMLParser[Letter](), Traditions: xmlparsing.NewXMLParser[Tradition](), Metas: xmlparsing.NewXMLParser[Meta](), } if commit != nil { lib.Commit = *commit } if err := lib.parse(baseDir); err != nil { return nil, err } return lib, nil } func (l *Library) parse(baseDir string) error { l.cache.Clear() wg := sync.WaitGroup{} failedPaths := make([]string, 0) failedMu := sync.Mutex{} parse := func(fn func() error, path string, label string) { wg.Add(1) go func() { defer wg.Done() if err := fn(); err != nil { failedMu.Lock() slog.Error("Failed to serialize "+label+":", "error", err) failedPaths = append(failedPaths, filepath.Join(baseDir, path)) failedMu.Unlock() } }() } // References must be ready before dependent documents (hands etc.) resolve correctly. parse(func() error { return l.Persons.Serialize(&PersonDefs{}, filepath.Join(baseDir, REFERENCES_PATH)) }, REFERENCES_PATH, "persons") parse(func() error { return l.Places.Serialize(&LocationDefs{}, filepath.Join(baseDir, REFERENCES_PATH)) }, REFERENCES_PATH, "places") parse(func() error { return l.AppDefs.Serialize(&AppDefs{}, filepath.Join(baseDir, REFERENCES_PATH)) }, REFERENCES_PATH, "appdefs") wg.Wait() // Remaining documents can be parsed once references are available. parse(func() error { return l.Letters.Serialize(&DocumentsRoot{}, filepath.Join(baseDir, LETTERS_PATH)) }, LETTERS_PATH, "letters") parse(func() error { return l.Traditions.Serialize(&TraditionsRoot{}, filepath.Join(baseDir, TRADITIONS_PATH)) }, TRADITIONS_PATH, "traditions") parse(func() error { return l.Metas.Serialize(&MetaRoot{}, filepath.Join(baseDir, META_PATH)) }, META_PATH, "meta") wg.Wait() if len(failedPaths) > 0 { return fmt.Errorf("parsing encountered errors: failed paths: %v", failedPaths) } return nil } type NextPrev struct { Next, Prev *Meta } func (l *Library) NextPrev(meta *Meta) *NextPrev { year := meta.Earliest().Sort().Year years, yearmap := l.Years() var next, prev *Meta for i, y := range yearmap[year] { if y.Letter == meta.Letter { if i > 0 { prev = &yearmap[year][i-1] } else { index := slices.Index(years, year) if index > 0 { prev = &yearmap[years[index-1]][len(yearmap[years[index-1]])-1] } } if i < len(yearmap[year])-1 { next = &yearmap[year][i+1] } else { index := slices.Index(years, year) if index < len(years)-1 { next = &yearmap[years[index+1]][0] } } break } } return &NextPrev{Next: next, Prev: prev} } func (l *Library) Years() ([]int, map[int][]Meta) { if years, ok := l.cache.Load("years"); ok { if yearmap, ok := l.cache.Load("yearmap"); ok { return years.([]int), yearmap.(map[int][]Meta) } } mapYears := make(map[int][]Meta) for item := range l.Metas.Iterate() { earliest := item.Earliest() if earliest != nil { mapYears[earliest.Sort().Year] = append(mapYears[earliest.Sort().Year], item) } } ret := slices.Collect(maps.Keys(mapYears)) slices.Sort(ret) for _, items := range mapYears { slices.SortFunc(items, func(a, b Meta) int { return a.Earliest().Sort().Compare(b.Earliest().Sort()) }) } l.cache.Store("years", ret) l.cache.Store("yearmap", mapYears) return ret, mapYears } func (l *Library) LettersForYear(year int) (ret []Meta) { for l := range l.Metas.Filter(func(item Meta) bool { return item.Earliest().Sort().Year == year }) { ret = append(ret, l) } return } func (l *Library) Person(id int) (ret *PersonDef) { ret = l.Persons.Item(id) return } func (l *Library) App(id int) (ret *AppDef) { ret = l.AppDefs.Item(id) return } func (l *Library) Place(id int) (ret *LocationDef) { ret = l.Places.Item(id) return } func (l *Library) Tradition(letter int) (ret []App) { item := l.Traditions.Item(letter) if item == nil { return []App{} } return item.Apps } func (l *Library) GetPersons(id []int) (ret []*PersonDef) { for _, i := range id { ret = append(ret, l.Person(i)) } return } func (l *Library) GetPlaces(id []int) (ret []*LocationDef) { for _, i := range id { ret = append(ret, l.Place(i)) } return } func (l *Library) FuncMap() template.FuncMap { return template.FuncMap{ "Person": l.Person, "Place": l.Place, "Persons": l.GetPersons, "Places": l.GetPlaces, "App": l.App, "Tradition": l.Tradition, } }