summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/scripts')
-rw-r--r--packages/frontend/src/scripts/clone.ts4
-rw-r--r--packages/frontend/src/scripts/code-highlighter.ts68
-rw-r--r--packages/frontend/src/scripts/merge.ts31
-rw-r--r--packages/frontend/src/scripts/theme.ts10
4 files changed, 107 insertions, 6 deletions
diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts
index ac38faefaa..6d3a1c8c79 100644
--- a/packages/frontend/src/scripts/clone.ts
+++ b/packages/frontend/src/scripts/clone.ts
@@ -8,13 +8,13 @@
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
-type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
+export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === 'object') {
if (x === null) return x;
if (Array.isArray(x)) return x.map(deepClone) as T;
- const obj = {} as Record<string, Cloneable>;
+ const obj = {} as Record<string | number | symbol, Cloneable>;
for (const [k, v] of Object.entries(x)) {
obj[k] = v === undefined ? undefined : deepClone(v);
}
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index bc05ec94d5..b11dfed41a 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -1,9 +1,51 @@
+import { bundledThemesInfo } from 'shiki';
import { getHighlighterCore, loadWasm } from 'shiki/core';
import darkPlus from 'shiki/themes/dark-plus.mjs';
-import type { Highlighter, LanguageRegistration } from 'shiki';
+import { unique } from './array.js';
+import { deepClone } from './clone.js';
+import { deepMerge } from './merge.js';
+import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
+import { ColdDeviceStorage } from '@/store.js';
+import lightTheme from '@/themes/_light.json5';
+import darkTheme from '@/themes/_dark.json5';
let _highlighter: Highlighter | null = null;
+export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
+export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
+export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
+ const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
+
+ if (theme.base) {
+ const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
+ if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
+ }
+
+ if (theme.codeHighlighter) {
+ let _res: ThemeRegistration = {};
+ if (theme.codeHighlighter.base === '_none_') {
+ _res = deepClone(theme.codeHighlighter.overrides);
+ } else {
+ const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
+ _res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
+ }
+ if (_res.name == null) {
+ _res.name = theme.id;
+ }
+ _res.type = mode;
+
+ if (getName) {
+ return _res.name;
+ }
+ return _res;
+ }
+
+ if (getName) {
+ return 'dark-plus';
+ }
+ return darkPlus;
+}
+
export async function getHighlighter(): Promise<Highlighter> {
if (!_highlighter) {
return await initHighlighter();
@@ -13,11 +55,17 @@ export async function getHighlighter(): Promise<Highlighter> {
export async function initHighlighter() {
const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
-
+
await loadWasm(import('shiki/onig.wasm?init'));
+ // テーマの重複を消す
+ const themes = unique([
+ darkPlus,
+ ...(await Promise.all([getTheme('light'), getTheme('dark')])),
+ ]);
+
const highlighter = await getHighlighterCore({
- themes: [darkPlus],
+ themes,
langs: [
import('shiki/langs/javascript.mjs'),
{
@@ -27,6 +75,20 @@ export async function initHighlighter() {
],
});
+ ColdDeviceStorage.watch('lightTheme', async () => {
+ const newTheme = await getTheme('light');
+ if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+ highlighter.loadTheme(newTheme);
+ }
+ });
+
+ ColdDeviceStorage.watch('darkTheme', async () => {
+ const newTheme = await getTheme('dark');
+ if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+ highlighter.loadTheme(newTheme);
+ }
+ });
+
_highlighter = highlighter;
return highlighter;
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
new file mode 100644
index 0000000000..60097051fa
--- /dev/null
+++ b/packages/frontend/src/scripts/merge.ts
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { deepClone } from './clone.js';
+import type { Cloneable } from './clone.js';
+
+function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/**
+ * valueにないキーをdefからもらう(再帰的)\
+ * nullはそのまま、undefinedはdefの値
+ **/
+export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: X, def: X): X {
+ if (isPureObject(value) && isPureObject(def)) {
+ const result = deepClone(value as Cloneable) as X;
+ for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
+ if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
+ result[k] = v;
+ } else if (isPureObject(v) && isPureObject(result[k])) {
+ const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record<string | number | symbol, unknown>;
+ result[k] = deepMerge<typeof v>(child, v);
+ }
+ }
+ return result;
+ }
+ return value;
+}
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index 21ef85fe7a..d3bd9ba4bc 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -6,6 +6,7 @@
import { ref } from 'vue';
import tinycolor from 'tinycolor2';
import { deepClone } from './clone.js';
+import type { BuiltinTheme } from 'shiki';
import { globalEvents } from '@/events.js';
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
@@ -18,6 +19,13 @@ export type Theme = {
desc?: string;
base?: 'dark' | 'light';
props: Record<string, string>;
+ codeHighlighter?: {
+ base: BuiltinTheme;
+ overrides?: Record<string, any>;
+ } | {
+ base: '_none_';
+ overrides: Record<string, any>;
+ };
};
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
@@ -53,7 +61,7 @@ export const getBuiltinThemesRef = () => {
return builtinThemes;
};
-let timeout = null;
+let timeout: number | null = null;
export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout);