initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
This commit is contained in:
commit
951dd8d26b
175 changed files with 50041 additions and 0 deletions
165
internal/tokenengine/json.go
Normal file
165
internal/tokenengine/json.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package tokenengine
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/urpi/go-bananas/internal/domain"
|
||||
)
|
||||
|
||||
// ResolvedTheme is the JSON payload served to runtime consumers. It carries
|
||||
// both a flat variable map (what a browser or a design tool wants) and the
|
||||
// nested resolved document (what an application that maps tokens onto component
|
||||
// props wants), so consumers never have to choose.
|
||||
type ResolvedTheme struct {
|
||||
Theme ThemeRef `json:"theme"`
|
||||
Tokens domain.TokenDoc `json:"tokens"`
|
||||
Variable map[string]string `json:"variables"`
|
||||
Meta domain.ThemeMeta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// ThemeRef is the identity block repeated in every runtime payload.
|
||||
type ThemeRef struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Version int64 `json:"version"`
|
||||
Appearance domain.Appearance `json:"appearance"`
|
||||
DefaultAppearance domain.Appearance `json:"defaultAppearance"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// JSONPayload builds the runtime JSON payload for one appearance.
|
||||
func JSONPayload(theme *domain.Theme, appearance domain.Appearance) ResolvedTheme {
|
||||
if appearance == "" {
|
||||
appearance = theme.DefaultAppearance
|
||||
}
|
||||
doc := Resolve(theme, appearance)
|
||||
return ResolvedTheme{
|
||||
Theme: ThemeRef{
|
||||
Slug: theme.Slug,
|
||||
Name: theme.Name,
|
||||
Description: theme.Description,
|
||||
Version: theme.Version,
|
||||
Appearance: appearance,
|
||||
DefaultAppearance: theme.DefaultAppearance,
|
||||
UpdatedAt: theme.UpdatedAt.UTC().Format(domain.TimeFormat),
|
||||
},
|
||||
Tokens: resolvedDocument(doc),
|
||||
Variable: doc.Variables(),
|
||||
Meta: theme.Meta,
|
||||
}
|
||||
}
|
||||
|
||||
// resolvedDocument renders the resolved tokens back into nested form, keeping
|
||||
// the DTCG `$type`/`$value` shape so that the output can be fed straight into
|
||||
// any DTCG-aware tool.
|
||||
func resolvedDocument(doc *Doc) domain.TokenDoc {
|
||||
out := make(domain.TokenDoc, len(Groups)+1)
|
||||
for _, t := range doc.Tokens {
|
||||
insertResolved(out, strings.Split(t.Path, "."), t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// insertResolved places a token into the nested output document, creating the
|
||||
// intermediate group objects on the way down.
|
||||
func insertResolved(root domain.TokenDoc, segments []string, t FlatToken) {
|
||||
current := root
|
||||
for _, seg := range segments[:len(segments)-1] {
|
||||
child, ok := current[seg].(map[string]any)
|
||||
if !ok {
|
||||
child = map[string]any{}
|
||||
current[seg] = child
|
||||
}
|
||||
// Re-widen so that the next iteration can index it as a TokenDoc.
|
||||
current = child
|
||||
}
|
||||
leaf := map[string]any{ValueKey: t.Value, TypeKey: string(t.Kind)}
|
||||
if t.Description != "" {
|
||||
leaf[DescKey] = t.Description
|
||||
}
|
||||
current[segments[len(segments)-1]] = leaf
|
||||
}
|
||||
|
||||
// ExportDocument produces a portable, self-describing token document for a
|
||||
// theme. Unlike the runtime payload it keeps the appearance overrides intact,
|
||||
// because the point of an export is that it can be re-imported losslessly.
|
||||
func ExportDocument(theme *domain.Theme) domain.TokenDoc {
|
||||
out := domain.TokenDoc{SchemaKey: SchemaID}
|
||||
merged := make(domain.TokenDoc, len(theme.Tokens)+1)
|
||||
for k, v := range theme.Tokens {
|
||||
merged[k] = v
|
||||
}
|
||||
if _, ok := merged[SchemaKey]; !ok {
|
||||
out[SchemaKey] = SchemaID
|
||||
}
|
||||
delete(merged, SchemaKey)
|
||||
for k, v := range merged {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ManifestEntry describes one theme in the runtime manifest: the small payload
|
||||
// a loader needs in order to build a theme picker without fetching every theme.
|
||||
type ManifestEntry struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DefaultAppearance domain.Appearance `json:"defaultAppearance"`
|
||||
Version int64 `json:"version"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
CSS string `json:"css"`
|
||||
Tokens string `json:"tokens"`
|
||||
}
|
||||
|
||||
// Manifest is the discovery document served from the runtime API.
|
||||
type Manifest struct {
|
||||
// APIVersion is the runtime contract version. Consumers should refuse to
|
||||
// load a manifest whose major version they do not understand.
|
||||
APIVersion string `json:"apiVersion"`
|
||||
// Default is the slug applied when a consumer does not choose.
|
||||
Default string `json:"default,omitempty"`
|
||||
// BaseURL is the absolute runtime base the entries are relative to.
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Themes []ManifestEntry `json:"themes"`
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// RuntimeAPIVersion is the current runtime contract version.
|
||||
const RuntimeAPIVersion = "1"
|
||||
|
||||
// Tags extracts theme tags from the opaque meta block. Tags are the one piece
|
||||
// of meta the engine understands, because the manifest needs them.
|
||||
func Tags(meta domain.ThemeMeta) []string {
|
||||
raw, ok := meta["tags"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
switch t := raw.(type) {
|
||||
case []string:
|
||||
return t
|
||||
case []any:
|
||||
out := make([]string, 0, len(t))
|
||||
for _, v := range t {
|
||||
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
|
||||
out = append(out, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
case string:
|
||||
parts := strings.Split(t, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if s := strings.TrimSpace(p); s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue