vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Derive theme metadata from the stylesheet file name
d222b0cf5d23444ea5df9429ec7d3690407dd00c · Keenan Nemetz · 1d ago · parent f7a34d87
Derive theme metadata from the stylesheet file name profile/themes/index.json is gone. Each profile/themes/<id>.css is self-describing: the file name is the id and the label (acme-ocean.css becomes Acme Ocean), and the rules are scoped to that id. The loader discovers the files, validates the names, and sorts them by label. Remove the manifest from the starter repository, its README, the Admin Configuration path table, and the configuration docs. An old index.json in an existing repository is ignored.
forge/instancerepo/instancerepo_test.go +13−24
| @@ -53,17 +53,16 @@ func TestLoadPicksOnlyTheFixedPaths(t *testing.T) { | |||
| 53 | 53 | } | |
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | func TestLoadReadsThemesInManifestOrder(t *testing.T) { | ||
| 56 | func TestLoadReadsThemesFromFileNames(t *testing.T) { | ||
| 57 | 57 | t.Parallel() | |
| 58 | 58 | g, commit := seed(t, map[string][]byte{ | |
| 59 | "profile/custom.css": []byte(":root[data-theme] { --radius: 2px; }"), | ||
| 60 | "profile/themes/index.json": []byte(`{"themes":[ | ||
| 61 | {"id":"acme-ocean","name":"Acme Ocean"}, | ||
| 62 | {"id":"acme-paper","name":"Acme Paper"} | ||
| 63 | ]}`), | ||
| 64 | "profile/themes/acme-ocean.css": []byte(`:root[data-theme="acme-ocean"] { color-scheme: dark; }`), | ||
| 59 | "profile/custom.css": []byte(":root[data-theme] { --radius: 2px; }"), | ||
| 65 | 60 | "profile/themes/acme-paper.css": []byte(`:root[data-theme="acme-paper"] { color-scheme: light; }`), | |
| 66 | "profile/themes/unlisted.css": []byte("ignored"), | ||
| 61 | "profile/themes/acme-ocean.css": []byte(`:root[data-theme="acme-ocean"] { color-scheme: dark; }`), | ||
| 62 | // An old manifest from before the file-name convention is ignored, and | ||
| 63 | // a non-CSS file under the directory is not a theme. | ||
| 64 | "profile/themes/index.json": []byte(`{"themes":[{"id":"gone","name":"Gone"}]}`), | ||
| 65 | "profile/themes/notes.txt": []byte("ignored"), | ||
| 67 | 66 | }) | |
| 68 | 67 | snap, err := load(g, commit) | |
| 69 | 68 | if err != nil { | |
| @@ -84,30 +83,20 @@ func TestLoadReadsThemesInManifestOrder(t *testing.T) { | |||
| 84 | 83 | } | |
| 85 | 84 | } | |
| 86 | 85 | ||
| 87 | func TestLoadRejectsABadThemeManifest(t *testing.T) { | ||
| 86 | func TestLoadRejectsABadThemeFileName(t *testing.T) { | ||
| 88 | 87 | t.Parallel() | |
| 89 | 88 | cases := map[string]map[string][]byte{ | |
| 90 | "invalid json": { | ||
| 91 | "profile/themes/index.json": []byte(`{"themes":[}`), | ||
| 92 | }, | ||
| 93 | "duplicate id": { | ||
| 94 | "profile/themes/index.json": []byte(`{"themes":[{"id":"acme","name":"A"},{"id":"acme","name":"B"}]}`), | ||
| 95 | "profile/themes/acme.css": []byte("body {}"), | ||
| 96 | }, | ||
| 97 | 89 | "reserved id": { | |
| 98 | "profile/themes/index.json": []byte(`{"themes":[{"id":"everforest-dark","name":"Not Everforest"}]}`), | ||
| 99 | 90 | "profile/themes/everforest-dark.css": []byte("body {}"), | |
| 100 | 91 | }, | |
| 101 | "missing stylesheet": { | ||
| 102 | "profile/themes/index.json": []byte(`{"themes":[{"id":"acme","name":"Acme"}]}`), | ||
| 92 | "system id": { | ||
| 93 | "profile/themes/system.css": []byte("body {}"), | ||
| 103 | 94 | }, | |
| 104 | 95 | "invalid id": { | |
| 105 | "profile/themes/index.json": []byte(`{"themes":[{"id":"Acme Ocean","name":"Acme"}]}`), | ||
| 106 | 96 | "profile/themes/Acme Ocean.css": []byte("body {}"), | |
| 107 | 97 | }, | |
| 108 | "empty name": { | ||
| 109 | "profile/themes/index.json": []byte(`{"themes":[{"id":"acme","name":" "}]}`), | ||
| 110 | "profile/themes/acme.css": []byte("body {}"), | ||
| 98 | "uppercase id": { | ||
| 99 | "profile/themes/Acme.css": []byte("body {}"), | ||
| 111 | 100 | }, | |
| 112 | 101 | } | |
| 113 | 102 | for name, files := range cases { | |
| @@ -116,7 +105,7 @@ func TestLoadRejectsABadThemeManifest(t *testing.T) { | |||
| 116 | 105 | t.Parallel() | |
| 117 | 106 | g, commit := seed(t, files) | |
| 118 | 107 | if _, err := load(g, commit); err == nil { | |
| 119 | t.Fatal("load accepted a bad theme manifest") | ||
| 108 | t.Fatal("load accepted a bad theme file name") | ||
| 120 | 109 | } | |
| 121 | 110 | }) | |
| 122 | 111 | } | |