Skip to content

Commit f2a9952

Browse files
committed
feat(plugin-themes): object-only register API + safe CM theme fallback/validation
1 parent e2f2385 commit f2a9952

File tree

5 files changed

+206
-14
lines changed

5 files changed

+206
-14
lines changed

src/cm/themes/index.js

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EditorState } from "@codemirror/state";
12
import { oneDark } from "@codemirror/theme-one-dark";
23
import aura, { config as auraConfig } from "./aura";
34
import dracula, { config as draculaConfig } from "./dracula";
@@ -32,17 +33,93 @@ const oneDarkConfig = {
3233
};
3334

3435
const themes = new Map();
36+
const warnedInvalidThemes = new Set();
37+
38+
function normalizeExtensions(value, target = []) {
39+
if (Array.isArray(value)) {
40+
value.forEach((item) => normalizeExtensions(item, target));
41+
return target;
42+
}
43+
44+
if (value !== null && value !== undefined) {
45+
target.push(value);
46+
}
47+
48+
return target;
49+
}
50+
51+
function toExtensionGetter(getExtension) {
52+
if (typeof getExtension === "function") {
53+
return () => normalizeExtensions(getExtension());
54+
}
55+
56+
return () => normalizeExtensions(getExtension);
57+
}
58+
59+
function logInvalidThemeOnce(themeId, error, reason = "") {
60+
if (warnedInvalidThemes.has(themeId)) return;
61+
warnedInvalidThemes.add(themeId);
62+
const message = reason
63+
? `[editorThemes] Theme '${themeId}' is invalid: ${reason}`
64+
: `[editorThemes] Theme '${themeId}' is invalid.`;
65+
console.error(message, error);
66+
}
67+
68+
function validateThemeExtensions(themeId, extensions) {
69+
if (!extensions.length) {
70+
logInvalidThemeOnce(themeId, null, "no extensions were returned");
71+
return false;
72+
}
73+
74+
try {
75+
// Validate against Acode's own CodeMirror instance.
76+
EditorState.create({ doc: "", extensions });
77+
return true;
78+
} catch (error) {
79+
logInvalidThemeOnce(themeId, error);
80+
return false;
81+
}
82+
}
83+
84+
function resolveThemeEntryExtensions(theme, fallbackExtensions) {
85+
const fallback = fallbackExtensions.length
86+
? [...fallbackExtensions]
87+
: [oneDark];
88+
89+
if (!theme) return fallback;
90+
91+
try {
92+
const resolved = normalizeExtensions(theme.getExtension?.());
93+
if (!validateThemeExtensions(theme.id, resolved)) {
94+
return fallback;
95+
}
96+
return resolved;
97+
} catch (error) {
98+
logInvalidThemeOnce(theme.id, error);
99+
return fallback;
100+
}
101+
}
35102

36103
export function addTheme(id, caption, isDark, getExtension, config = null) {
37-
const key = String(id).toLowerCase();
38-
if (themes.has(key)) return;
39-
themes.set(key, {
104+
const key = String(id || "")
105+
.trim()
106+
.toLowerCase();
107+
if (!key || themes.has(key)) return false;
108+
109+
const theme = {
40110
id: key,
41111
caption: caption || id,
42112
isDark: !!isDark,
43-
getExtension,
113+
getExtension: toExtensionGetter(getExtension),
44114
config: config || null,
45-
});
115+
};
116+
117+
if (!validateThemeExtensions(key, theme.getExtension())) {
118+
return false;
119+
}
120+
121+
themes.set(key, theme);
122+
return true;
46123
}
47124

48125
export function getThemes() {
@@ -60,6 +137,13 @@ export function getThemeConfig(id) {
60137
return theme?.config || oneDarkConfig;
61138
}
62139

140+
export function getThemeExtensions(id, fallback = [oneDark]) {
141+
const fallbackExtensions = normalizeExtensions(fallback);
142+
const theme =
143+
getThemeById(id) || getThemeById(String(id || "").replace(/-/g, "_"));
144+
return resolveThemeEntryExtensions(theme, fallbackExtensions);
145+
}
146+
63147
export function removeTheme(id) {
64148
if (!id) return;
65149
themes.delete(String(id).toLowerCase());
@@ -142,6 +226,7 @@ export default {
142226
getThemes,
143227
getThemeById,
144228
getThemeConfig,
229+
getThemeExtensions,
145230
addTheme,
146231
removeTheme,
147232
};

src/lib/acode.js

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import fsOperation from "fileSystem";
22
import sidebarApps from "sidebarApps";
3+
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
4+
import { EditorView } from "@codemirror/view";
35
import ajax from "@deadlyjack/ajax";
6+
import { tags } from "@lezer/highlight";
47
import {
58
getRegisteredCommands as listRegisteredCommands,
69
refreshCommandKeymap,
@@ -103,14 +106,111 @@ export default class Acode {
103106
};
104107

105108
// CodeMirror editor theme API for plugins
109+
const normalizeThemeSpec = (spec) => {
110+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
111+
console.warn(
112+
"[editorThemes] register(spec) expects an object: { id, caption?, dark?, getExtension|extensions|extension|theme, config? }",
113+
);
114+
return null;
115+
}
116+
117+
const id = spec.id || spec.name;
118+
if (!id) {
119+
console.warn("[editorThemes] register(spec) requires a valid `id`.");
120+
return null;
121+
}
122+
123+
const extensionSource =
124+
spec.getExtension || spec.extensions || spec.extension || spec.theme;
125+
if (extensionSource === undefined || extensionSource === null) {
126+
console.warn(
127+
`[editorThemes] register('${id}') requires extensions via getExtension/extensions/extension/theme.`,
128+
);
129+
return null;
130+
}
131+
132+
return {
133+
id,
134+
caption: spec.caption || spec.label || id,
135+
isDark: spec.isDark ?? spec.dark ?? false,
136+
getExtension:
137+
typeof extensionSource === "function"
138+
? extensionSource
139+
: () => extensionSource,
140+
config: spec.config ?? null,
141+
};
142+
};
143+
144+
const createHighlightStyle = (spec) => {
145+
if (!spec) return null;
146+
if (Array.isArray(spec)) return HighlightStyle.define(spec);
147+
return spec;
148+
};
149+
150+
const createTheme = ({
151+
styles,
152+
dark = false,
153+
highlightStyle,
154+
extensions = [],
155+
} = {}) => {
156+
const ext = [];
157+
158+
if (styles && typeof styles === "object") {
159+
ext.push(EditorView.theme(styles, { dark: !!dark }));
160+
}
161+
162+
const resolvedHighlight = createHighlightStyle(highlightStyle);
163+
if (resolvedHighlight) {
164+
ext.push(syntaxHighlighting(resolvedHighlight));
165+
}
166+
167+
if (Array.isArray(extensions)) {
168+
ext.push(...extensions);
169+
} else if (extensions) {
170+
ext.push(extensions);
171+
}
172+
173+
return ext;
174+
};
175+
106176
const editorThemesModule = {
107-
register: (id, caption, isDark, getExtension, config = null) =>
108-
cmThemeRegistry.addTheme(id, caption, isDark, getExtension, config),
177+
/**
178+
* Register a CodeMirror theme from plugin code.
179+
* @param {{
180+
* id: string,
181+
* caption?: string,
182+
* dark?: boolean,
183+
* getExtension?: Function,
184+
* extensions?: unknown,
185+
* config?: object
186+
* }} spec
187+
* `isDark`, `extension`, and `theme` are accepted aliases for compatibility.
188+
* @returns {boolean}
189+
*/
190+
register: (spec) => {
191+
const resolved = normalizeThemeSpec(spec);
192+
if (!resolved) return false;
193+
return cmThemeRegistry.addTheme(
194+
resolved.id,
195+
resolved.caption,
196+
resolved.isDark,
197+
resolved.getExtension,
198+
resolved.config,
199+
);
200+
},
109201
unregister: (id) => cmThemeRegistry.removeTheme(id),
110202
list: () => cmThemeRegistry.getThemes(),
111203
apply: (id) => editorManager?.editor?.setTheme?.(id),
112204
get: (id) => cmThemeRegistry.getThemeById(id),
113205
getConfig: (id) => cmThemeRegistry.getThemeConfig(id),
206+
createTheme,
207+
createHighlightStyle,
208+
cm: {
209+
EditorView,
210+
HighlightStyle,
211+
syntaxHighlighting,
212+
tags,
213+
},
114214
};
115215

116216
const sidebarAppsModule = {

src/lib/editorManager.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import {
6262
} from "cm/editorUtils";
6363
import indentGuides from "cm/indentGuides";
6464
import rainbowBrackets from "cm/rainbowBrackets";
65-
import themeRegistry, { getThemeById, getThemes } from "cm/themes";
65+
import { getThemeExtensions } from "cm/themes";
6666
import list from "components/collapsableList";
6767
import quickTools from "components/quickTools";
6868
import ScrollBar from "components/scrollbar";
@@ -778,8 +778,7 @@ async function EditorManager($header, $body) {
778778
editor.setTheme = function (themeId) {
779779
try {
780780
const id = String(themeId || "");
781-
const theme = getThemeById(id) || getThemeById(id.replace(/-/g, "_"));
782-
const ext = theme?.getExtension?.() || [oneDark];
781+
const ext = getThemeExtensions(id, [oneDark]);
783782
editor.dispatch({ effects: themeCompartment.reconfigure(ext) });
784783
return true;
785784
} catch (_) {

src/pages/themeSetting/themeSetting.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { javascript } from "@codemirror/lang-javascript";
33
// For CodeMirror preview
44
import { EditorState } from "@codemirror/state";
55
import { oneDark } from "@codemirror/theme-one-dark";
6-
import { getThemeById, getThemes } from "cm/themes";
6+
import { getThemeExtensions, getThemes } from "cm/themes";
77
import { basicSetup, EditorView } from "codemirror";
88
import Page from "components/page";
99
import searchBar from "components/searchbar";
@@ -35,7 +35,7 @@ export default function () {
3535
cmPreview.destroy();
3636
cmPreview = null;
3737
}
38-
const theme = getThemeById(themeId)?.getExtension?.() || [oneDark];
38+
const theme = getThemeExtensions(themeId, [oneDark]);
3939
const fixedHeightTheme = EditorView.theme({
4040
"&": { height: "100%", flex: "1 1 auto" },
4141
".cm-scroller": { height: "100%", overflow: "auto" },
@@ -213,7 +213,14 @@ export default function () {
213213
);
214214
return;
215215
}
216-
editorManager.editor.setTheme(theme);
216+
const ok = editorManager.editor.setTheme(theme);
217+
if (!ok) {
218+
alert(
219+
"Invalid theme",
220+
"This editor theme is not compatible with Acode's CodeMirror runtime.",
221+
);
222+
return;
223+
}
217224
if (cmPreview) createPreview(theme);
218225
appSettings.update(
219226
{

src/palettes/changeEditorTheme/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function generateHints() {
2222

2323
function onselect(themeId) {
2424
if (!themeId) return;
25-
editorManager.editor.setTheme(themeId);
25+
const ok = editorManager.editor.setTheme(themeId);
26+
if (!ok) return;
2627
appSettings.update({ editorTheme: themeId }, false);
2728
}

0 commit comments

Comments
 (0)