clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / internal / enrich / enrichers / stdicons / stdicons.go
stdicons.go 5.2K
1// Package stdicons resolves the standard icon library a collection may name2// instead of shipping artwork of its own.3//4// A translator that reads a source with no icons can still say what a5// collection is a collection of, by naming a glyph from a standard set:6// atlas.icon.std = "maki/monument". This package turns that promise into bytes.7// The named glyph lands in the volume's icon set under a name that spells its8// provenance -- std--maki-monument.svg -- so a bundle listing reads honestly and9// a source's own artwork can never be shadowed by it. The application never10// learns this package exists: a resolved standard icon is one more asset in a11// bundle's icons tree, tinted and haloed like any glyph a source shipped.12//13// It runs after a merge, which is the whole reason it is an enricher rather14// than a step inside a translator: a collection folded in from another reading15// resolves its declaration too.16//17// # Where silence is not the rule18//19// Every other enricher in this lane prefers silence to a plausible claim. This20// one refuses instead: a declaration the library cannot answer fails the build.21// The difference is who made the promise. A membership join guesses about the22// world; a standard-icon declaration was written by a translator author, in23// this repository, and an unanswerable one is a typo that should be heard about24// while it is one table edit old rather than quietly dropped.25//26// Only the names some translator actually speaks are vendored. The embedded set27// is the vocabulary.28package stdicons2930import (31 "embed"32 "fmt"33 "log/slog"34 "sort"35 "strings"3637 "github.com/FelineStateMachine/atlas/format/semconv"38 "github.com/FelineStateMachine/atlas/internal/enrich"39 "github.com/FelineStateMachine/atlas/internal/logging"40)4142//go:embed standard/maki/*.svg standard/maki/LICENSE.txt43var standard embed.FS4445// Name is what curation queues this enricher as.46const Name = "stdicons"4748// Enricher resolves standard icon declarations.49type Enricher struct{}5051// New builds the enricher.52func New() *Enricher { return &Enricher{} }5354func (*Enricher) Name() string { return Name }5556// Declares is empty: this enricher writes artwork, not attributes. The kind of57// artwork a collection ends up carrying is declared by composition, which is58// the step that knows whether the asset actually travelled.59func (*Enricher) Declares() []string { return nil }6061// Enrich resolves every declaration a collection makes and does not already62// answer with artwork of its own.63func (e *Enricher) Enrich(v *enrich.Volume, ctx enrich.Context) (enrich.Contribution, error) {64 out := enrich.Contribution{Enricher: Name, Volume: v.Slug}65 log := ctx.Log66 if log == nil {67 log = slog.New(slog.DiscardHandler)68 }69 log = log.With(logging.Op(Name), logging.Volume(v.Slug))7071 carried := make(map[string]bool)72 for _, world := range v.Worlds {73 for _, collection := range world.Collections {74 if collection.Kind != enrich.KindPoint {75 continue76 }77 ref := collection.Attrs[semconv.KeyIconStd]78 // A source's own artwork wins the slot: the declaration is what a79 // collection wears when it has nothing.80 if ref == "" || collection.Icon != "" || collection.IconAsset != "" {81 continue82 }83 data, asset, err := Standard(ref)84 if err != nil {85 return enrich.Contribution{}, fmt.Errorf(86 "world %s collection %q: %w", world.Slug, collection.Title, err)87 }88 key := strings.TrimSuffix(asset, ".svg")89 if !carried[key] {90 carried[key] = true91 out.Ops = append(out.Ops, enrich.Op{92 Kind: enrich.OpAddAsset,93 Asset: &enrich.Icon{Key: key, File: asset, Data: data},94 })95 }96 out.Ops = append(out.Ops, enrich.Op{97 Kind: enrich.OpSetIcon,98 World: world.Slug,99 Collection: collection.ID,100 Key: key,101 Value: asset,102 })103 log.Debug("standard icon resolved", logging.World(world.Slug),104 "collection", collection.Title, "icon", ref)105 }106 }107 if len(carried) > 0 {108 log.Info("standard icons resolved", "assets", len(carried))109 }110 return out, nil111}112113// Standard resolves a set/name reference to the icon's bytes and the asset name114// it ships under.115func Standard(ref string) ([]byte, string, error) {116 set, name, found := strings.Cut(ref, "/")117 if !found || set != "maki" {118 return nil, "", fmt.Errorf("standard icon %q is not in a vendored set", ref)119 }120 data, err := standard.ReadFile("standard/maki/" + name + ".svg")121 if err != nil {122 return nil, "", fmt.Errorf("standard icon %q is not vendored", ref)123 }124 return data, "std--maki-" + name + ".svg", nil125}126127// Vocabulary lists the references the vendored set answers, in sorted order. It128// is what a translator author checks a new declaration against, and what the129// test that holds the embed to the documented set reads.130func Vocabulary() []string {131 entries, err := standard.ReadDir("standard/maki")132 if err != nil {133 return nil134 }135 var out []string136 for _, entry := range entries {137 name, found := strings.CutSuffix(entry.Name(), ".svg")138 if !found {139 continue140 }141 out = append(out, "maki/"+name)142 }143 // The directory hands its entries back in file-name order, which is not the144 // order these read in: a hyphen sorts before a dot, so circle-stroked.svg145 // comes before circle.svg and the references come out shuffled.146 sort.Strings(out)147 return out148}