codex-terminal-themes 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +23 -0
- package/LICENSE +21 -0
- package/LICENSE.txt +21 -0
- package/README.md +169 -0
- package/SECURITY.md +5 -0
- package/SUPPORT.md +5 -0
- package/bin/codex-terminal-themes.mjs +12 -0
- package/docs/app.js +1393 -0
- package/docs/assets/theme-gallery-header.png +0 -0
- package/docs/favicon.svg +10 -0
- package/docs/index.html +240 -0
- package/docs/preview.svg +24 -0
- package/docs/site-data.json +51078 -0
- package/docs/styles.css +780 -0
- package/metadata/README.md +71 -0
- package/metadata/themes.json +16185 -0
- package/metadata/themes.schema.json +218 -0
- package/package.json +109 -0
- package/src/cli.mjs +1763 -0
- package/stylelint.config.mjs +12 -0
- package/themes/80s.tmTheme +229 -0
- package/themes/Active4D.tmTheme +407 -0
- package/themes/All Hallow's Eve Custom.tmTheme +275 -0
- package/themes/All Hallow's Eve.tmTheme +277 -0
- package/themes/All Hallows Eve.tmTheme +277 -0
- package/themes/Amy.tmTheme +559 -0
- package/themes/Artic Fall.tmTheme +275 -0
- package/themes/BBEdit.tmTheme +439 -0
- package/themes/Bespin.tmTheme +516 -0
- package/themes/Black Pearl II.tmTheme +496 -0
- package/themes/Black Pearl.tmTheme +400 -0
- package/themes/Blackboard 2.tmTheme +348 -0
- package/themes/Blackboard Black.tmTheme +350 -0
- package/themes/Blackboard.tmTheme +348 -0
- package/themes/Blueberry Jelly.tmTheme +242 -0
- package/themes/Bluesy.tmTheme +242 -0
- package/themes/Blurb 2.tmTheme +229 -0
- package/themes/Blurb.tmTheme +229 -0
- package/themes/Bongzilla 2.tmTheme +223 -0
- package/themes/Bongzilla.tmTheme +223 -0
- package/themes/Brightly Dark.tmTheme +242 -0
- package/themes/Brilliance Black.tmTheme +2619 -0
- package/themes/Brilliance Dull.tmTheme +2243 -0
- package/themes/Bromozoid.tmTheme +227 -0
- package/themes/CSSEdit.tmTheme +203 -0
- package/themes/Classic Modified.tmTheme +469 -0
- package/themes/Clouds Midnight.tmTheme +361 -0
- package/themes/Clouds of Ruby.tmTheme +649 -0
- package/themes/Clouds.tmTheme +348 -0
- package/themes/Cloudy Fields.tmTheme +240 -0
- package/themes/Coal Graal.tmTheme +282 -0
- package/themes/Cobalt.tmTheme +561 -0
- package/themes/Coda.tmTheme +317 -0
- package/themes/Colorful.tmTheme +307 -0
- package/themes/Cool Glow.tmTheme +234 -0
- package/themes/Corona.tmTheme +290 -0
- package/themes/Cowabunga.tmTheme +242 -0
- package/themes/DanBurst.tmTheme +665 -0
- package/themes/Daniel Fischer.tmTheme +627 -0
- package/themes/Dark Ocean.tmTheme +242 -0
- package/themes/DarkNeon.tmTheme +818 -0
- package/themes/Dawn.tmTheme +437 -0
- package/themes/DawnCustom.tmTheme +443 -0
- package/themes/Django (Smoothy).tmTheme +453 -0
- package/themes/Django Blues.tmTheme +182 -0
- package/themes/Django Extended.tmTheme +495 -0
- package/themes/Django.tmTheme +436 -0
- package/themes/Dobdark.tmTheme +615 -0
- package/themes/Dominion Day.tmTheme +562 -0
- package/themes/Doo-Daa.tmTheme +242 -0
- package/themes/Drankin Purp.tmTheme +227 -0
- package/themes/Dreamweaver_Blackbam_Aptana303.tmTheme +980 -0
- package/themes/Easy on my Eyes There Buddy.tmTheme +227 -0
- package/themes/Eiffel.tmTheme +435 -0
- package/themes/Emacs Strict.tmTheme +241 -0
- package/themes/Emacs.tmTheme +241 -0
- package/themes/Epic Blue.tmTheme +320 -0
- package/themes/Erebus.tmTheme +467 -0
- package/themes/Espresso Libre.tmTheme +402 -0
- package/themes/Espresso Tutti.tmTheme +392 -0
- package/themes/Espresso.tmTheme +329 -0
- package/themes/Fade to Grey.tmTheme +308 -0
- package/themes/Fluidvision.tmTheme +443 -0
- package/themes/ForLaTeX.tmTheme +214 -0
- package/themes/Freckle.tmTheme +279 -0
- package/themes/Friendship Bracelet.tmTheme +303 -0
- package/themes/GaGaGaGroovy.tmTheme +227 -0
- package/themes/Gangrene.tmTheme +242 -0
- package/themes/GitHub.tmTheme +653 -0
- package/themes/GlitterBomb.tmTheme +387 -0
- package/themes/Halloween Night.tmTheme +303 -0
- package/themes/Happy happy joy joy.tmTheme +841 -0
- package/themes/Happydeluxe.tmTheme +184 -0
- package/themes/HelvectorLight.tmTheme +557 -0
- package/themes/Humane.tmTheme +220 -0
- package/themes/IDLE.tmTheme +235 -0
- package/themes/IR_Black.tmTheme +810 -0
- package/themes/IR_White.tmTheme +792 -0
- package/themes/Jane Fonda, Baby Redux.tmTheme +264 -0
- package/themes/Johnny.tmTheme +798 -0
- package/themes/Juicy.tmTheme +250 -0
- package/themes/Kuroir Theme.tmTheme +707 -0
- package/themes/LAZY.tmTheme +291 -0
- package/themes/Lowlight.tmTheme +605 -0
- package/themes/Mac Classic.tmTheme +476 -0
- package/themes/Made of Code.tmTheme +695 -0
- package/themes/MagicWB (Amiga).tmTheme +376 -0
- package/themes/Malibu Nights.tmTheme +257 -0
- package/themes/Menage A Trois.tmTheme +881 -0
- package/themes/Merbivore Soft.tmTheme +285 -0
- package/themes/Merbivore.tmTheme +285 -0
- package/themes/Midnight.tmTheme +321 -0
- package/themes/Mmm Sandy.tmTheme +227 -0
- package/themes/Monokai.tmTheme +289 -0
- package/themes/MultiMarkdown.tmTheme +183 -0
- package/themes/Mustang.tmTheme +339 -0
- package/themes/Neopro Inverted.tmTheme +328 -0
- package/themes/Neopro.tmTheme +330 -0
- package/themes/Nice One.tmTheme +222 -0
- package/themes/No Way.tmTheme +255 -0
- package/themes/Overcast.tmTheme +659 -0
- package/themes/Pastels on Dark.tmTheme +703 -0
- package/themes/Pastie.tmTheme +321 -0
- package/themes/Peridinkle.tmTheme +240 -0
- package/themes/Play!.tmTheme +736 -0
- package/themes/Puss.tmTheme +227 -0
- package/themes/Putty.tmTheme +275 -0
- package/themes/Quail.tmTheme +257 -0
- package/themes/RDark.tmTheme +235 -0
- package/themes/Rails Envy.tmTheme +299 -0
- package/themes/Railscasts 2.tmTheme +368 -0
- package/themes/Railscasts.tmTheme +278 -0
- package/themes/Resesif.tmTheme +298 -0
- package/themes/Ringo.tmTheme +240 -0
- package/themes/Ruby Blue.tmTheme +366 -0
- package/themes/RubyRobot.tmTheme +250 -0
- package/themes/Ryan Light.tmTheme +232 -0
- package/themes/Seafoam.tmTheme +242 -0
- package/themes/Sidewalk Chalk.tmTheme +276 -0
- package/themes/Sin City (that yellow bastard).tmTheme +585 -0
- package/themes/Slate.tmTheme +436 -0
- package/themes/Slush & Poppies.tmTheme +336 -0
- package/themes/Slush and Poppies.tmTheme +336 -0
- package/themes/Smokey Morning.tmTheme +229 -0
- package/themes/Smoothy original.tmTheme +623 -0
- package/themes/Smoothy.tmTheme +623 -0
- package/themes/Solarized (dark).tmTheme +2051 -0
- package/themes/Solarized-dark.tmTheme +312 -0
- package/themes/Solarized-light.tmTheme +305 -0
- package/themes/Sometheme.tmTheme +240 -0
- package/themes/SoylentTheme.tmTheme +353 -0
- package/themes/SpaceCadet.tmTheme +212 -0
- package/themes/Spectacular.tmTheme +436 -0
- package/themes/Starlight.tmTheme +857 -0
- package/themes/Stoneship Bright.tmTheme +348 -0
- package/themes/Stoneship.tmTheme +361 -0
- package/themes/Summer Camp Mod.tmTheme +229 -0
- package/themes/Summer Camp.tmTheme +229 -0
- package/themes/Summery Drink.tmTheme +242 -0
- package/themes/Sunburst.tmTheme +665 -0
- package/themes/Swyphs II.tmTheme +306 -0
- package/themes/Tango Bright.tmTheme +1 -0
- package/themes/Tango.tmTheme +450 -0
- package/themes/Teenage Dream.tmTheme +242 -0
- package/themes/Texari.tmTheme +727 -0
- package/themes/Text Ex Machina.tmTheme +295 -0
- package/themes/Tomorrow-Night-Blue.tmTheme +175 -0
- package/themes/Tomorrow-Night-Eighties.tmTheme +175 -0
- package/themes/Tomorrow-Night.tmTheme +175 -0
- package/themes/Tomorrow.tmTheme +349 -0
- package/themes/ToyChest.tmTheme +503 -0
- package/themes/TravisJeffery.tmTheme +1261 -0
- package/themes/Tubster.tmTheme +280 -0
- package/themes/Twilight BG FG.tmTheme +1000 -0
- package/themes/Twilight.tmTheme +516 -0
- package/themes/TwilightMod.tmTheme +529 -0
- package/themes/Two Days Ago.tmTheme +242 -0
- package/themes/Vibrant Fin.tmTheme +447 -0
- package/themes/Vibrant Ink.tmTheme +447 -0
- package/themes/Vibrant Scala.tmTheme +292 -0
- package/themes/Vibrant Tango.tmTheme +438 -0
- package/themes/Wandering.tmTheme +681 -0
- package/themes/Whimsy in Blue.tmTheme +240 -0
- package/themes/Why/342/200/231s Poignant.tmTheme" +191 -0
- package/themes/Windows XP.tmTheme +350 -0
- package/themes/Witch.tmTheme +282 -0
- package/themes/Yurple.tmTheme +227 -0
- package/themes/ZZZ.tmTheme +131 -0
- package/themes/Zachstronaut.tmTheme +381 -0
- package/themes/Zenburnesque.tmTheme +343 -0
- package/themes/[ Argonaut ].tmTheme +387 -0
- package/themes/barf.tmTheme +254 -0
- package/themes/converted-vscode-AmoledShinyBlack.tmTheme +528 -0
- package/themes/converted-vscode-AmoledShinyBlack2.tmTheme +609 -0
- package/themes/converted-vscode-AmoledShinyBlack3.tmTheme +683 -0
- package/themes/converted-vscode-AmoledShinyBlack4.tmTheme +896 -0
- package/themes/converted-vscode-AmoledShinyBlack5.tmTheme +1023 -0
- package/themes/converted-vscode-AmoledShinyBlack6.tmTheme +2092 -0
- package/themes/eclips3.media (ECLM).tmTheme +294 -0
- package/themes/evin.tmTheme +253 -0
- package/themes/fake.tmTheme +669 -0
- package/themes/fapfap.tmTheme +508 -0
- package/themes/helloKitty.tmTheme +293 -0
- package/themes/iLife 05.tmTheme +619 -0
- package/themes/iPlastic.tmTheme +397 -0
- package/themes/idleFingers.tmTheme +380 -0
- package/themes/krTheme.tmTheme +551 -0
- package/themes/mark.james.name.tmTheme +1117 -0
- package/themes/minimal Theme.tmTheme +551 -0
- package/themes/mint.tmTheme +617 -0
- package/themes/mintBlue Dark.tmTheme +653 -0
- package/themes/mintBlue.tmTheme +655 -0
- package/themes/modifiedPastels.tmTheme +745 -0
- package/themes/monoindustrial.tmTheme +451 -0
- package/themes/my-theme-blackboard.tmTheme +363 -0
- package/themes/my-theme-classic.tmTheme +465 -0
- package/themes/nppGLua.tmTheme +293 -0
- package/themes/reST testing theme.tmTheme +554 -0
- package/themes/rose-pine-dawn.tmTheme +329 -0
- package/themes/rose-pine-moon.tmTheme +329 -0
- package/themes/rose-pine.tmTheme +329 -0
- package/themes/ryan-light.tmTheme +232 -0
- package/themes/wut.tmTheme +255 -0
- package/tools/build-pages-site.mjs +465 -0
- package/tools/generate-theme-metadata.mjs +680 -0
- package/tools/serve-pages-site.mjs +196 -0
- package/tools/validate-themes.mjs +272 -0
- package/types/index.d.ts +49 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { SyntaxValidator } from "fast-xml-validator";
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
const rootDirectory = process.cwd();
|
|
7
|
+
const metadataDirectory = path.join(rootDirectory, "metadata");
|
|
8
|
+
const manifestPath = path.join(metadataDirectory, "themes.json");
|
|
9
|
+
const schemaPath = path.join(metadataDirectory, "themes.schema.json");
|
|
10
|
+
const themeDirectory = path.join(rootDirectory, "themes");
|
|
11
|
+
const shouldCheck = process.argv.includes("--check");
|
|
12
|
+
const shouldWrite = process.argv.includes("--write");
|
|
13
|
+
|
|
14
|
+
const parser = new XMLParser({
|
|
15
|
+
ignoreAttributes: false,
|
|
16
|
+
preserveOrder: true,
|
|
17
|
+
trimValues: false,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* readonly background: string | null;
|
|
23
|
+
* readonly caret: string | null;
|
|
24
|
+
* readonly foreground: string | null;
|
|
25
|
+
* readonly invisibles: string | null;
|
|
26
|
+
* readonly lineHighlight: string | null;
|
|
27
|
+
* readonly selection: string | null;
|
|
28
|
+
* }} ThemeColors
|
|
29
|
+
*
|
|
30
|
+
* @typedef {{
|
|
31
|
+
* readonly colorReferences: number;
|
|
32
|
+
* readonly scopedSettings: number;
|
|
33
|
+
* readonly settings: number;
|
|
34
|
+
* readonly uniqueScopes: number;
|
|
35
|
+
* }} ThemeStatistics
|
|
36
|
+
*
|
|
37
|
+
* @typedef {{
|
|
38
|
+
* readonly appearance: "dark" | "light" | "unknown";
|
|
39
|
+
* readonly author: string | null;
|
|
40
|
+
* readonly colorSpace: string | null;
|
|
41
|
+
* readonly colors: ThemeColors;
|
|
42
|
+
* readonly fileName: string;
|
|
43
|
+
* readonly id: string;
|
|
44
|
+
* readonly name: string;
|
|
45
|
+
* readonly path: string;
|
|
46
|
+
* readonly scopes: readonly string[];
|
|
47
|
+
* readonly semanticClass: string | null;
|
|
48
|
+
* readonly statistics: ThemeStatistics;
|
|
49
|
+
* readonly uuid: string;
|
|
50
|
+
* }} ThemeMetadata
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {readonly ThemeMetadata[]} themes
|
|
55
|
+
*
|
|
56
|
+
* @returns {Record<string, readonly string[]>}
|
|
57
|
+
*/
|
|
58
|
+
function buildDuplicateUuidGroups(themes) {
|
|
59
|
+
/** @type {Map<string, string[]>} */
|
|
60
|
+
const groupedPaths = new Map();
|
|
61
|
+
|
|
62
|
+
for (const theme of themes) {
|
|
63
|
+
const existingPaths = groupedPaths.get(theme.uuid) ?? [];
|
|
64
|
+
existingPaths.push(theme.path);
|
|
65
|
+
groupedPaths.set(theme.uuid, existingPaths);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @type {Record<string, readonly string[]>} */
|
|
69
|
+
const duplicateGroups = {};
|
|
70
|
+
|
|
71
|
+
for (const [uuid, paths] of groupedPaths) {
|
|
72
|
+
if (paths.length > 1) {
|
|
73
|
+
duplicateGroups[uuid] = paths.toSorted((left, right) =>
|
|
74
|
+
left.localeCompare(right)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return Object.fromEntries(
|
|
80
|
+
Object.entries(duplicateGroups).toSorted(([left], [right]) =>
|
|
81
|
+
left.localeCompare(right)
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
88
|
+
*/
|
|
89
|
+
async function buildManifest() {
|
|
90
|
+
const filePaths = await getThemeFiles(themeDirectory);
|
|
91
|
+
const themes = await parseThemes(filePaths);
|
|
92
|
+
const duplicateUuidGroups = buildDuplicateUuidGroups(themes);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
$schema: "./themes.schema.json",
|
|
96
|
+
consumers: [
|
|
97
|
+
"bat",
|
|
98
|
+
"Codex terminal",
|
|
99
|
+
"TextMate-compatible syntax highlighters",
|
|
100
|
+
],
|
|
101
|
+
description:
|
|
102
|
+
"Generated metadata for the TextMate themes in this repository.",
|
|
103
|
+
duplicateUuidGroups,
|
|
104
|
+
generatedBy: "npm run metadata:write",
|
|
105
|
+
name: "codex-terminal-themes",
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
themeCount: themes.length,
|
|
108
|
+
themeDirectory: "themes",
|
|
109
|
+
themes,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} color
|
|
115
|
+
*
|
|
116
|
+
* @returns {number | null}
|
|
117
|
+
*/
|
|
118
|
+
function calculateLuminance(color) {
|
|
119
|
+
const parsedColor = parseColor(color);
|
|
120
|
+
|
|
121
|
+
if (parsedColor === null) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const [
|
|
126
|
+
red,
|
|
127
|
+
green,
|
|
128
|
+
blue,
|
|
129
|
+
] = parsedColor.map((channel) => {
|
|
130
|
+
const normalizedChannel = channel / 255;
|
|
131
|
+
return normalizedChannel <= 0.039_28
|
|
132
|
+
? normalizedChannel / 12.92
|
|
133
|
+
: ((normalizedChannel + 0.055) / 1.055) ** 2.4;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} fileName
|
|
141
|
+
*
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
function createThemeId(fileName) {
|
|
145
|
+
const baseName = fileName.replace(/\.tmTheme$/v, "");
|
|
146
|
+
const id = baseName
|
|
147
|
+
.normalize("NFKD")
|
|
148
|
+
.replaceAll(/[^\dA-Za-z]+/gv, "-")
|
|
149
|
+
.replaceAll(/^-|-$/gv, "")
|
|
150
|
+
.toLowerCase();
|
|
151
|
+
|
|
152
|
+
return id.length > 0 ? id : "theme";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {string | null} background
|
|
157
|
+
*
|
|
158
|
+
* @returns {"dark" | "light" | "unknown"}
|
|
159
|
+
*/
|
|
160
|
+
function detectAppearance(background) {
|
|
161
|
+
if (background === null) {
|
|
162
|
+
return "unknown";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const luminance = calculateLuminance(background);
|
|
166
|
+
|
|
167
|
+
if (luminance === null) {
|
|
168
|
+
return "unknown";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return luminance < 0.5 ? "dark" : "light";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {Record<string, unknown>} record
|
|
176
|
+
* @param {string} key
|
|
177
|
+
*
|
|
178
|
+
* @returns {readonly unknown[] | undefined}
|
|
179
|
+
*/
|
|
180
|
+
function getArrayProperty(record, key) {
|
|
181
|
+
const value = record[key];
|
|
182
|
+
return isUnknownArray(value) ? value : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} manifestJson
|
|
187
|
+
*
|
|
188
|
+
* @returns {Promise<number>}
|
|
189
|
+
*/
|
|
190
|
+
async function getCheckExitCode(manifestJson) {
|
|
191
|
+
const existingManifest = await readExistingManifest();
|
|
192
|
+
|
|
193
|
+
if (existingManifest === manifestJson) {
|
|
194
|
+
process.stdout.write("Theme metadata manifest is up to date.\n");
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
"Theme metadata manifest is stale. Run `npm run metadata:write`.\n"
|
|
200
|
+
);
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {ReadonlyMap<string, unknown>} settings
|
|
206
|
+
* @param {string} key
|
|
207
|
+
*
|
|
208
|
+
* @returns {string | null}
|
|
209
|
+
*/
|
|
210
|
+
function getColorValue(settings, key) {
|
|
211
|
+
const value = settings.get(key);
|
|
212
|
+
const color = getStringValue(value);
|
|
213
|
+
return color ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {readonly unknown[]} dictChildren
|
|
218
|
+
*
|
|
219
|
+
* @returns {ReadonlyMap<string, unknown>}
|
|
220
|
+
*/
|
|
221
|
+
function getDictionaryEntries(dictChildren) {
|
|
222
|
+
/** @type {Map<string, unknown>} */
|
|
223
|
+
const entries = new Map();
|
|
224
|
+
/** @type {string | undefined} */
|
|
225
|
+
let currentKey;
|
|
226
|
+
|
|
227
|
+
for (const child of dictChildren) {
|
|
228
|
+
if (hasOwnRecordKey(child, "key")) {
|
|
229
|
+
currentKey = getTextNodeValue(child.key);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (currentKey !== undefined && !hasOwnRecordKey(child, "#text")) {
|
|
234
|
+
entries.set(currentKey, child);
|
|
235
|
+
currentKey = undefined;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return entries;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {ReadonlyMap<string, unknown>} entries
|
|
244
|
+
* @param {string} key
|
|
245
|
+
*
|
|
246
|
+
* @returns {ReadonlyMap<string, unknown>}
|
|
247
|
+
*/
|
|
248
|
+
function getDictionaryValue(entries, key) {
|
|
249
|
+
const value = entries.get(key);
|
|
250
|
+
|
|
251
|
+
if (!isRecord(value)) {
|
|
252
|
+
return new Map();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const dictChildren = getArrayProperty(value, "dict");
|
|
256
|
+
return dictChildren === undefined
|
|
257
|
+
? new Map()
|
|
258
|
+
: getDictionaryEntries(dictChildren);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {Record<string, unknown>} manifest
|
|
263
|
+
*
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
function getManifestJson(manifest) {
|
|
267
|
+
return `${JSON.stringify(manifest, null, 4)}\n`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {unknown} value
|
|
272
|
+
*
|
|
273
|
+
* @returns {string | undefined}
|
|
274
|
+
*/
|
|
275
|
+
function getStringValue(value) {
|
|
276
|
+
if (!isRecord(value)) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return getTextNodeValue(value.string);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @param {unknown} value
|
|
285
|
+
*
|
|
286
|
+
* @returns {string | undefined}
|
|
287
|
+
*/
|
|
288
|
+
function getTextNodeValue(value) {
|
|
289
|
+
if (!isUnknownArray(value)) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const firstValue = value[0];
|
|
294
|
+
if (!isRecord(firstValue)) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const textValue = firstValue["#text"];
|
|
299
|
+
return typeof textValue === "string" ? textValue : undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {readonly unknown[]} settingsArray
|
|
304
|
+
*
|
|
305
|
+
* @returns {ThemeColors}
|
|
306
|
+
*/
|
|
307
|
+
function getThemeColors(settingsArray) {
|
|
308
|
+
const firstSettingsItem = settingsArray.find((item) =>
|
|
309
|
+
hasOwnRecordKey(item, "dict")
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (!isRecord(firstSettingsItem)) {
|
|
313
|
+
return {
|
|
314
|
+
background: null,
|
|
315
|
+
caret: null,
|
|
316
|
+
foreground: null,
|
|
317
|
+
invisibles: null,
|
|
318
|
+
lineHighlight: null,
|
|
319
|
+
selection: null,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const firstSettingsDict = getArrayProperty(firstSettingsItem, "dict");
|
|
324
|
+
const firstSettingsEntries =
|
|
325
|
+
firstSettingsDict === undefined
|
|
326
|
+
? new Map()
|
|
327
|
+
: getDictionaryEntries(firstSettingsDict);
|
|
328
|
+
const globalSettings = getDictionaryValue(firstSettingsEntries, "settings");
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
background: getColorValue(globalSettings, "background"),
|
|
332
|
+
caret: getColorValue(globalSettings, "caret"),
|
|
333
|
+
foreground: getColorValue(globalSettings, "foreground"),
|
|
334
|
+
invisibles: getColorValue(globalSettings, "invisibles"),
|
|
335
|
+
lineHighlight: getColorValue(globalSettings, "lineHighlight"),
|
|
336
|
+
selection: getColorValue(globalSettings, "selection"),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @param {string} directory
|
|
342
|
+
*
|
|
343
|
+
* @returns {Promise<readonly string[]>}
|
|
344
|
+
*/
|
|
345
|
+
async function getThemeFiles(directory) {
|
|
346
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- The generator intentionally reads the repo-local themes directory.
|
|
347
|
+
const directoryEntries = await readdir(directory, { withFileTypes: true });
|
|
348
|
+
|
|
349
|
+
return directoryEntries
|
|
350
|
+
.filter(
|
|
351
|
+
(directoryEntry) =>
|
|
352
|
+
directoryEntry.isFile() &&
|
|
353
|
+
directoryEntry.name.endsWith(".tmTheme")
|
|
354
|
+
)
|
|
355
|
+
.map((directoryEntry) => directoryEntry.name)
|
|
356
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
357
|
+
.map((fileName) => path.join(directory, fileName));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @param {readonly unknown[]} settingsArray
|
|
362
|
+
*
|
|
363
|
+
* @returns {readonly string[]}
|
|
364
|
+
*/
|
|
365
|
+
function getThemeScopes(settingsArray) {
|
|
366
|
+
return settingsArray
|
|
367
|
+
.flatMap((item) => {
|
|
368
|
+
if (!hasOwnRecordKey(item, "dict")) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const dictChildren = getArrayProperty(item, "dict");
|
|
373
|
+
const entries =
|
|
374
|
+
dictChildren === undefined
|
|
375
|
+
? new Map()
|
|
376
|
+
: getDictionaryEntries(dictChildren);
|
|
377
|
+
const scope = getStringValue(entries.get("scope"));
|
|
378
|
+
|
|
379
|
+
if (scope === undefined) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return scope
|
|
384
|
+
.split(",")
|
|
385
|
+
.map((scopePart) => scopePart.trim())
|
|
386
|
+
.filter((scopePart) => scopePart.length > 0);
|
|
387
|
+
})
|
|
388
|
+
.toSorted((left, right) => left.localeCompare(right));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @param {ReadonlyMap<string, unknown>} topLevelEntries
|
|
393
|
+
*
|
|
394
|
+
* @returns {readonly unknown[]}
|
|
395
|
+
*/
|
|
396
|
+
function getThemeSettings(topLevelEntries) {
|
|
397
|
+
const settingsNode = topLevelEntries.get("settings");
|
|
398
|
+
|
|
399
|
+
if (!isRecord(settingsNode)) {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return getArrayProperty(settingsNode, "array") ?? [];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {readonly unknown[]} settingsArray
|
|
408
|
+
*
|
|
409
|
+
* @returns {ThemeStatistics}
|
|
410
|
+
*/
|
|
411
|
+
function getThemeStatistics(settingsArray) {
|
|
412
|
+
const settingsCount = settingsArray.filter((item) =>
|
|
413
|
+
hasOwnRecordKey(item, "dict")
|
|
414
|
+
).length;
|
|
415
|
+
const scopes = getThemeScopes(settingsArray);
|
|
416
|
+
const colorReferences = settingsArray
|
|
417
|
+
.filter((item) => hasOwnRecordKey(item, "dict"))
|
|
418
|
+
.flatMap((item) => {
|
|
419
|
+
if (!isRecord(item)) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const dictChildren = getArrayProperty(item, "dict");
|
|
424
|
+
const entries =
|
|
425
|
+
dictChildren === undefined
|
|
426
|
+
? new Map()
|
|
427
|
+
: getDictionaryEntries(dictChildren);
|
|
428
|
+
const settings = getDictionaryValue(entries, "settings");
|
|
429
|
+
|
|
430
|
+
return [...settings.values()].filter((value) => {
|
|
431
|
+
const stringValue = getStringValue(value);
|
|
432
|
+
return stringValue === undefined
|
|
433
|
+
? false
|
|
434
|
+
: parseColor(stringValue) !== null;
|
|
435
|
+
});
|
|
436
|
+
}).length;
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
colorReferences,
|
|
440
|
+
scopedSettings: scopes.length,
|
|
441
|
+
settings: settingsCount,
|
|
442
|
+
uniqueScopes: new Set(scopes).size,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* @param {unknown} parsedDocument
|
|
448
|
+
*
|
|
449
|
+
* @returns {ReadonlyMap<string, unknown>}
|
|
450
|
+
*/
|
|
451
|
+
function getTopLevelDictionary(parsedDocument) {
|
|
452
|
+
if (!isUnknownArray(parsedDocument)) {
|
|
453
|
+
return new Map();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const plistNode = parsedDocument.find((node) =>
|
|
457
|
+
hasOwnRecordKey(node, "plist")
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
if (!isRecord(plistNode)) {
|
|
461
|
+
return new Map();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const plistChildren = getArrayProperty(plistNode, "plist");
|
|
465
|
+
if (plistChildren === undefined) {
|
|
466
|
+
return new Map();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const dictNode = plistChildren.find((node) =>
|
|
470
|
+
hasOwnRecordKey(node, "dict")
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if (!isRecord(dictNode)) {
|
|
474
|
+
return new Map();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const dictChildren = getArrayProperty(dictNode, "dict");
|
|
478
|
+
return dictChildren === undefined
|
|
479
|
+
? new Map()
|
|
480
|
+
: getDictionaryEntries(dictChildren);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* @param {unknown} value
|
|
485
|
+
* @param {string} key
|
|
486
|
+
*
|
|
487
|
+
* @returns {value is Record<string, unknown>}
|
|
488
|
+
*/
|
|
489
|
+
function hasOwnRecordKey(value, key) {
|
|
490
|
+
return isRecord(value) && Object.hasOwn(value, key);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @param {unknown} value
|
|
495
|
+
*
|
|
496
|
+
* @returns {value is Record<string, unknown>}
|
|
497
|
+
*/
|
|
498
|
+
function isRecord(value) {
|
|
499
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* @param {unknown} value
|
|
504
|
+
*
|
|
505
|
+
* @returns {value is readonly unknown[]}
|
|
506
|
+
*/
|
|
507
|
+
function isUnknownArray(value) {
|
|
508
|
+
return Array.isArray(value);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @returns {Promise<number>}
|
|
513
|
+
*/
|
|
514
|
+
async function main() {
|
|
515
|
+
if (shouldCheck === shouldWrite) {
|
|
516
|
+
process.stderr.write("Specify exactly one of --check or --write.\n");
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const manifest = await buildManifest();
|
|
521
|
+
const manifestJson = getManifestJson(manifest);
|
|
522
|
+
|
|
523
|
+
if (shouldCheck) {
|
|
524
|
+
return getCheckExitCode(manifestJson);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await writeManifest(manifestJson);
|
|
528
|
+
process.stdout.write("Wrote metadata/themes.json.\n");
|
|
529
|
+
return 0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* @param {string} color
|
|
534
|
+
*
|
|
535
|
+
* @returns {readonly [number, number, number] | null}
|
|
536
|
+
*/
|
|
537
|
+
function parseColor(color) {
|
|
538
|
+
const normalizedColor = color.trim();
|
|
539
|
+
const hexMatch = /^#(?<hex>[\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/iv.exec(
|
|
540
|
+
normalizedColor
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
if (hexMatch?.groups === undefined) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const { hex } = hexMatch.groups;
|
|
548
|
+
|
|
549
|
+
if (hex.length === 3) {
|
|
550
|
+
return [
|
|
551
|
+
Number.parseInt(`${hex[0]}${hex[0]}`, 16),
|
|
552
|
+
Number.parseInt(`${hex[1]}${hex[1]}`, 16),
|
|
553
|
+
Number.parseInt(`${hex[2]}${hex[2]}`, 16),
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return [
|
|
558
|
+
Number.parseInt(hex.slice(0, 2), 16),
|
|
559
|
+
Number.parseInt(hex.slice(2, 4), 16),
|
|
560
|
+
Number.parseInt(hex.slice(4, 6), 16),
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* @param {string} filePath
|
|
566
|
+
*
|
|
567
|
+
* @returns {Promise<ThemeMetadata>}
|
|
568
|
+
*/
|
|
569
|
+
async function parseThemeFile(filePath) {
|
|
570
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Theme paths come from the repo-local themes directory listing.
|
|
571
|
+
const text = await readFile(filePath, "utf8");
|
|
572
|
+
const xmlValidation = SyntaxValidator.validate(text);
|
|
573
|
+
|
|
574
|
+
if (xmlValidation !== true) {
|
|
575
|
+
const { col, line, msg } = xmlValidation.err;
|
|
576
|
+
throw new Error(
|
|
577
|
+
`${filePath}: XML parse error at ${line}:${col}: ${msg}`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const parsedDocument = /** @type {unknown} */ (parser.parse(text));
|
|
582
|
+
const topLevelEntries = getTopLevelDictionary(parsedDocument);
|
|
583
|
+
const name = getStringValue(topLevelEntries.get("name"));
|
|
584
|
+
const uuid = getStringValue(topLevelEntries.get("uuid"));
|
|
585
|
+
|
|
586
|
+
if (name === undefined || uuid === undefined) {
|
|
587
|
+
throw new Error(`${filePath}: missing top-level name or uuid`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const settingsArray = getThemeSettings(topLevelEntries);
|
|
591
|
+
const colors = getThemeColors(settingsArray);
|
|
592
|
+
const fileName = path.basename(filePath);
|
|
593
|
+
const relativePath = path
|
|
594
|
+
.relative(rootDirectory, filePath)
|
|
595
|
+
.replaceAll(path.sep, "/");
|
|
596
|
+
const scopes = [...new Set(getThemeScopes(settingsArray))];
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
appearance: detectAppearance(colors.background),
|
|
600
|
+
author: getStringValue(topLevelEntries.get("author")) ?? null,
|
|
601
|
+
colors,
|
|
602
|
+
colorSpace: getStringValue(topLevelEntries.get("colorSpace")) ?? null,
|
|
603
|
+
fileName,
|
|
604
|
+
id: createThemeId(fileName),
|
|
605
|
+
name,
|
|
606
|
+
path: relativePath,
|
|
607
|
+
scopes,
|
|
608
|
+
semanticClass:
|
|
609
|
+
getStringValue(topLevelEntries.get("semanticClass")) ?? null,
|
|
610
|
+
statistics: getThemeStatistics(settingsArray),
|
|
611
|
+
uuid,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @param {readonly string[]} filePaths
|
|
617
|
+
*
|
|
618
|
+
* @returns {Promise<readonly ThemeMetadata[]>}
|
|
619
|
+
*/
|
|
620
|
+
async function parseThemes(filePaths) {
|
|
621
|
+
const themes = await Promise.all(
|
|
622
|
+
filePaths.map((filePath) => parseThemeFile(filePath))
|
|
623
|
+
);
|
|
624
|
+
/** @type {Map<string, number>} */
|
|
625
|
+
const idCounts = new Map();
|
|
626
|
+
|
|
627
|
+
return themes.map((theme) => {
|
|
628
|
+
const idCount = idCounts.get(theme.id) ?? 0;
|
|
629
|
+
idCounts.set(theme.id, idCount + 1);
|
|
630
|
+
|
|
631
|
+
if (idCount === 0) {
|
|
632
|
+
return theme;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
...theme,
|
|
637
|
+
id: `${theme.id}-${idCount + 1}`,
|
|
638
|
+
};
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* @returns {Promise<string>}
|
|
644
|
+
*/
|
|
645
|
+
async function readExistingManifest() {
|
|
646
|
+
try {
|
|
647
|
+
return await readFile(manifestPath, "utf8");
|
|
648
|
+
} catch {
|
|
649
|
+
return "";
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* @returns {Promise<void>}
|
|
655
|
+
*/
|
|
656
|
+
async function run() {
|
|
657
|
+
try {
|
|
658
|
+
process.exitCode = await main();
|
|
659
|
+
} catch (error) {
|
|
660
|
+
process.stderr.write(`${String(error)}\n`);
|
|
661
|
+
process.exitCode = 1;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* @param {string} manifestJson
|
|
667
|
+
*
|
|
668
|
+
* @returns {Promise<void>}
|
|
669
|
+
*/
|
|
670
|
+
async function writeManifest(manifestJson) {
|
|
671
|
+
await mkdir(metadataDirectory, { recursive: true });
|
|
672
|
+
|
|
673
|
+
await writeFile(manifestPath, manifestJson, "utf8");
|
|
674
|
+
process.stdout.write(
|
|
675
|
+
`Schema location: ${path.relative(rootDirectory, schemaPath)}\n`
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await -- This published-module config also enforces n/no-top-level-await.
|
|
680
|
+
void run();
|