Skip to content

Commit e2fac9c

Browse files
committed
refactor(theme): extract shared utilities and add type safety
- Add sdk/src/types/theme.d.ts with Appearance, ThemeCSSVariable, ThemeGroup types - Add sdk/src/core/theme-loader.ts with loadThemePaths(), buildThemeChoices() - Add sdk/src/core/theme-utils.ts with resolveOpacity(), extractAppearance() - Add sdk/src/core/theme-validation.ts with validateThemeCSS() - Refactor theme-selector.ts and new-theme.ts to use shared utilities - Reduce code duplication (~90 lines removed)
1 parent 42dcb24 commit e2fac9c

File tree

9 files changed

+736
-235
lines changed

9 files changed

+736
-235
lines changed

src/app/paste-snippet.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ if (args?.length > 0 && snippet.includes("$0")) {
1111
snippet = snippet.replaceAll("$0", args?.shift());
1212
}
1313

14-
// Find ${selection} and replace with selected text
15-
if (snippet.includes("$SELECTION")) {
14+
// Support both $SELECTION and $SELECTED_TEXT for consistency
15+
if (snippet.includes("$SELECTION") || snippet.includes("$SELECTED_TEXT")) {
1616
let selectedText = await getSelectedText()
1717
snippet = snippet.replaceAll("$SELECTION", selectedText)
18+
snippet = snippet.replaceAll("$SELECTED_TEXT", selectedText)
1819
}
1920

2021
if (snippet.includes("$CLIPBOARD")) {

src/cli/new-theme.ts

Lines changed: 26 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,104 +10,31 @@ Create a new theme based on the current theme
1010
// Pass: true
1111
// Keyword: nt
1212

13+
import { loadThemePaths, buildThemeChoices, replaceThemeName, slugifyThemeName } from "../core/theme-loader.js"
14+
1315
await ensureDir(kenvPath("themes"))
1416

15-
let customThemePaths = await globby([
16-
kenvPath("themes", "*.css").replaceAll("\\", "/"),
17-
])
18-
19-
let themePaths = await globby([
20-
kitPath("themes", "*.css").replaceAll("\\", "/"),
21-
`!${kitPath("themes", "script-kit*.css").replaceAll(
22-
"\\",
23-
"/"
24-
)}`,
25-
])
26-
27-
let defaultThemePaths = await globby([
28-
kitPath("themes", "script-kit*.css").replaceAll(
29-
"\\",
30-
"/"
31-
),
32-
])
33-
34-
let guide = await readFile(kitPath("GUIDE.md"), "utf-8")
35-
36-
let themes = []
37-
38-
for await (let themePath of defaultThemePaths) {
39-
let css = await readFile(themePath, "utf-8")
40-
let themeName = path.basename(themePath)
41-
let theme = {
42-
group: "Default",
43-
name: themeName,
44-
description: themePath,
45-
value: themePath,
46-
enter: "Apply Theme",
47-
preview: async () => {
48-
setScriptTheme(css)
49-
50-
return md(
51-
`# Preview of ${themeName}
52-
53-
${guide}`
54-
)
55-
},
56-
}
57-
58-
themes.push(theme)
59-
}
60-
61-
for await (let cssPath of themePaths) {
62-
let css = await readFile(cssPath, "utf-8")
63-
let themeName = path.basename(cssPath)
64-
let theme = {
65-
group: "Built-in",
66-
name: themeName,
67-
description: cssPath,
68-
value: cssPath,
69-
enter: "Apply Theme",
70-
preview: async () => {
71-
setScriptTheme(css)
72-
73-
return md(
74-
`# Preview of ${themeName}
75-
76-
${guide}`
77-
)
78-
},
79-
}
80-
81-
themes.push(theme)
82-
}
83-
84-
for await (let cssPath of customThemePaths) {
85-
let css = await readFile(cssPath, "utf-8")
86-
let themeName = path.basename(cssPath)
87-
let theme = {
88-
group: "Custom",
89-
name: themeName,
90-
description: cssPath,
91-
value: cssPath,
92-
enter: "Apply Theme",
93-
preview: async () => {
94-
setScriptTheme(css)
95-
96-
return md(
97-
`# Preview of ${themeName}
98-
99-
${guide}`
100-
)
101-
},
102-
}
103-
themes.push(theme)
104-
}
17+
// Load all theme paths using shared utility
18+
const paths = await loadThemePaths(kenvPath, kitPath)
19+
20+
// Load preview content
21+
const guide = await readFile(kitPath("GUIDE.md"), "utf-8")
22+
23+
// Build theme choices using shared utility
24+
let themes = await buildThemeChoices({
25+
paths,
26+
readFile,
27+
pathModule: path,
28+
setScriptTheme,
29+
md,
30+
previewContent: guide,
31+
})
10532

10633
themes = groupChoices(themes, {
10734
order: ["Default", "Custom", "Built-in"],
108-
})
35+
}) as typeof themes
10936

110-
let cssPath = await arg(
37+
const cssPath = await arg(
11138
{
11239
placeholder: "Select a Theme to Copy",
11340
hint: "Base New Theme on...",
@@ -121,19 +48,15 @@ let cssPath = await arg(
12148

12249
let theme = await readFile(cssPath, "utf-8")
12350

124-
let name = arg?.pass || (await arg("Theme Name"))
51+
const name = arg?.pass || (await arg("Theme Name"))
12552

126-
// convert name to dashed lowercase
127-
let dashed = name
128-
.replace(/[^a-zA-Z0-9]/g, "-")
129-
.toLowerCase()
53+
// Convert name to filename slug using shared utility
54+
const dashed = slugifyThemeName(name)
13055

131-
// Replace the --name variable with the new theme name
132-
theme = theme.replace(
133-
/--name: ".*";/g,
134-
`--name: "${name}";`
135-
)
136-
let themePath = kenvPath("themes", `${dashed}.css`)
56+
// Replace the --name variable with the new theme name using shared utility
57+
theme = replaceThemeName(theme, name)
58+
59+
const themePath = kenvPath("themes", `${dashed}.css`)
13760
await writeFile(themePath, theme)
13861
await cli("set-env-var", "KIT_THEME_LIGHT", themePath)
13962
await cli("set-env-var", "KIT_THEME_DARK", themePath)

src/core/snippets.ts

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import path from "node:path";
33
import { globby } from "globby";
44
import type { Metadata, Snippet } from '../types/index.js'
55
import { kenvPath } from './resolvers.js'
6-
import { escapeHTML, getKenvFromPath, getMetadata, postprocessMetadata, checkDbAndInvalidateCache } from './utils.js'
6+
import { escapeHTML, getKenvFromPath } from './utils.js'
7+
import { parseSnippetMetadata } from './metadata-parser.js'
78

89
interface SnippetFileCacheEntry {
910
snippetObject: Partial<Snippet>;
@@ -33,23 +34,19 @@ export let parseSnippets = async (): Promise<Snippet[]> => {
3334
}
3435

3536
let contents = await readFile(s, 'utf8')
36-
let { metadata, snippet } = getSnippet(contents)
37-
let formattedSnippet = escapeHTML(snippet)
37+
// Use unified metadata parser for consistency with App
38+
const { metadata, snippetBody, snippetKey, postfix } = parseSnippetMetadata(contents)
3839

39-
let expand = (metadata?.expand || metadata?.snippet || '').trim()
40-
let postfix = false
41-
if (expand.startsWith('*')) {
42-
postfix = true
43-
expand = expand.slice(1)
44-
}
40+
// Get raw expand value for tag (preserves * prefix for postfix snippets)
41+
const rawExpand = (metadata?.snippet || metadata?.expand || '') as string
4542

4643
const newSnippetChoice: Partial<Snippet> = {
4744
...metadata,
4845
filePath: s,
4946
name: metadata?.name || path.basename(s),
50-
tag: metadata?.snippet || '',
47+
tag: rawExpand,
5148
description: s,
52-
text: snippet.trim(),
49+
text: snippetBody.trim(),
5350
preview: `<div class="p-4">
5451
<style>
5552
p{
@@ -58,16 +55,16 @@ export let parseSnippets = async (): Promise<Snippet[]> => {
5855
li{
5956
margin-bottom: .25rem;
6057
}
61-
58+
6259
</style>
63-
${snippet.trim()}
60+
${escapeHTML(snippetBody.trim())}
6461
</div>`,
6562
group: 'Snippets',
6663
kenv: getKenvFromPath(s),
67-
value: snippet.trim(),
68-
expand,
69-
postfix: postfix ? true : false,
70-
snippetKey: expand,
64+
value: snippetBody.trim(),
65+
expand: snippetKey,
66+
postfix,
67+
snippetKey,
7168
}
7269

7370
snippetCache.set(s, { snippetObject: newSnippetChoice, mtimeMs: currentMtimeMs });
@@ -82,27 +79,17 @@ export let parseSnippets = async (): Promise<Snippet[]> => {
8279
return snippetChoices as Snippet[]
8380
}
8481

85-
const snippetRegex = /(?<=^(?:(?:\/\/)|#)\s{0,2})([\w-]+)(?::)(.*)/
82+
/**
83+
* Parse snippet contents and extract metadata and body.
84+
* Uses unified parseSnippetMetadata for consistency with App.
85+
* @deprecated Use parseSnippetMetadata directly for full result including warnings
86+
*/
8687
export let getSnippet = (
8788
contents: string
8889
): {
8990
metadata: Metadata
9091
snippet: string
9192
} => {
92-
let lines = contents.split('\n')
93-
let metadata = postprocessMetadata(getMetadata(contents), contents) as Metadata
94-
delete (metadata as any).type
95-
let contentStartIndex = lines.length
96-
97-
for (let i = 0; i < lines.length; i++) {
98-
let line = lines[i]
99-
let match = line.match(snippetRegex)
100-
101-
if (!match) {
102-
contentStartIndex = i
103-
break
104-
}
105-
}
106-
let snippet = lines.slice(contentStartIndex).join('\n')
107-
return { metadata, snippet }
93+
const { metadata, snippetBody } = parseSnippetMetadata(contents)
94+
return { metadata: metadata as Metadata, snippet: snippetBody }
10895
}

0 commit comments

Comments
 (0)