summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-02-06 15:03:07 +0900
committerGitHub <noreply@github.com>2024-02-06 15:03:07 +0900
commit16eccad49271b81282cdbd482d29e7a8bb048f4c (patch)
tree96f0b6f94f106de95ff8cb9af29b39b53fdeaece /packages
parent2024.2.0-beta.10 (diff)
downloadsharkey-16eccad49271b81282cdbd482d29e7a8bb048f4c.tar.gz
sharkey-16eccad49271b81282cdbd482d29e7a8bb048f4c.tar.bz2
sharkey-16eccad49271b81282cdbd482d29e7a8bb048f4c.zip
enhance(frontend): シンタックスハイライトにテーマを適用できるように (#13175)
* enhance(frontend): シンタックスハイライトにテーマを適用できるように * Update Changelog * こっちも * テーマの値がディープマージされるように * 常にテーマ設定に準じるように * テーマ更新時に新しいshikiテーマを読み込むように
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/src/components/MkCode.core.vue56
-rw-r--r--packages/frontend/src/components/MkCode.vue8
-rw-r--r--packages/frontend/src/components/MkCodeEditor.vue5
-rw-r--r--packages/frontend/src/components/MkCodeInline.vue3
-rw-r--r--packages/frontend/src/components/MkSelect.vue7
-rw-r--r--packages/frontend/src/pages/settings/theme.vue15
-rw-r--r--packages/frontend/src/pizzax.ts23
-rw-r--r--packages/frontend/src/router/main.ts4
-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
-rw-r--r--packages/frontend/src/store.ts1
-rw-r--r--packages/frontend/src/themes/_dark.json54
-rw-r--r--packages/frontend/src/themes/_light.json54
15 files changed, 201 insertions, 42 deletions
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index c655ff416e..68c50c4c69 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- eslint-disable vue/no-v-html -->
<template>
-<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
+<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { bundledLanguagesInfo } from 'shiki';
import type { BuiltinLanguage } from 'shiki';
-import { getHighlighter } from '@/scripts/code-highlighter.js';
+import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
+import { defaultStore } from '@/store.js';
const props = defineProps<{
code: string;
@@ -21,11 +22,23 @@ const props = defineProps<{
}>();
const highlighter = await getHighlighter();
-
+const darkMode = defaultStore.reactiveState.darkMode;
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
+
+const [lightThemeName, darkThemeName] = await Promise.all([
+ getTheme('light', true),
+ getTheme('dark', true),
+]);
+
const html = computed(() => highlighter.codeToHtml(props.code, {
lang: codeLang.value,
- theme: 'dark-plus',
+ themes: {
+ fallback: 'dark-plus',
+ light: lightThemeName,
+ dark: darkThemeName,
+ },
+ defaultColor: false,
+ cssVariablePrefix: '--shiki-',
}));
async function fetchLanguage(to: string): Promise<void> {
@@ -64,6 +77,15 @@ watch(() => props.lang, (to) => {
margin: .5em 0;
overflow: auto;
border-radius: 8px;
+ border: 1px solid var(--divider);
+
+ color: var(--shiki-fallback);
+ background-color: var(--shiki-fallback-bg);
+
+ & span {
+ color: var(--shiki-fallback);
+ background-color: var(--shiki-fallback-bg);
+ }
& pre,
& code {
@@ -71,6 +93,26 @@ watch(() => props.lang, (to) => {
}
}
+.light.codeBlockRoot :global(.shiki) {
+ color: var(--shiki-light);
+ background-color: var(--shiki-light-bg);
+
+ & span {
+ color: var(--shiki-light);
+ background-color: var(--shiki-light-bg);
+ }
+}
+
+.dark.codeBlockRoot :global(.shiki) {
+ color: var(--shiki-dark);
+ background-color: var(--shiki-dark-bg);
+
+ & span {
+ color: var(--shiki-dark);
+ background-color: var(--shiki-dark-bg);
+ }
+}
+
.codeBlockRoot.codeEditor {
min-width: 100%;
height: 100%;
@@ -79,6 +121,7 @@ watch(() => props.lang, (to) => {
padding: 12px;
margin: 0;
border-radius: 6px;
+ border: none;
min-height: 130px;
pointer-events: none;
min-width: calc(100% - 24px);
@@ -90,6 +133,11 @@ watch(() => props.lang, (to) => {
text-rendering: inherit;
text-transform: inherit;
white-space: pre;
+
+ & span {
+ display: inline-block;
+ min-height: 1em;
+ }
}
}
</style>
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index 251e6ade00..6c14738937 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -53,7 +53,6 @@ function copy() {
}
.codeBlockCopyButton {
- color: #D4D4D4;
position: absolute;
top: 8px;
right: 8px;
@@ -67,8 +66,7 @@ function copy() {
.codeBlockFallbackRoot {
display: block;
overflow-wrap: anywhere;
- color: #D4D4D4;
- background: #1E1E1E;
+ background: var(--bg);
padding: 1em;
margin: .5em 0;
overflow: auto;
@@ -93,8 +91,8 @@ function copy() {
border-radius: 8px;
padding: 24px;
margin-top: 4px;
- color: #D4D4D4;
- background: #1E1E1E;
+ color: var(--fg);
+ background: var(--bg);
}
.codePlaceholderContainer {
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index c8c3deb610..3cf8234e72 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -196,10 +196,11 @@ watch(v, newValue => {
resize: none;
text-align: left;
color: transparent;
- caret-color: rgb(225, 228, 232);
+ caret-color: var(--fg);
background-color: transparent;
border: 0;
border-radius: 6px;
+ box-sizing: border-box;
outline: 0;
min-width: calc(100% - 24px);
height: 100%;
@@ -210,6 +211,6 @@ watch(v, newValue => {
}
.textarea::selection {
- color: #fff;
+ color: var(--bg);
}
</style>
diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue
index 5340c1fd5f..6a9d97ab5a 100644
--- a/packages/frontend/src/components/MkCodeInline.vue
+++ b/packages/frontend/src/components/MkCodeInline.vue
@@ -18,8 +18,7 @@ const props = defineProps<{
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
- color: #D4D4D4;
- background: #1E1E1E;
+ background: var(--bg);
padding: .1em;
border-radius: .3em;
}
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 8dd2b9129d..3e8ff99387 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
- <MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
@@ -138,6 +138,7 @@ function show() {
active: computed(() => v.value === option.props?.value),
action: () => {
v.value = option.props?.value;
+ changed.value = true;
emit('changeByUser', v.value);
},
});
@@ -288,6 +289,10 @@ function show() {
padding-left: 6px;
}
+.save {
+ margin: 8px 0 0 0;
+}
+
.chevron {
transition: transform 0.1s ease-out;
}
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index dedac10270..1d6fec5290 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
import { fetchThemes, getThemes } from '@/theme-store.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
+import { unisonReload } from '@/scripts/unison-reload.js';
+import * as os from '@/os.js';
+
+async function reloadAsk() {
+ const { canceled } = await os.confirm({
+ type: 'info',
+ text: i18n.ts.reloadToApplySetting,
+ });
+ if (canceled) return;
+
+ unisonReload();
+}
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
@@ -124,6 +136,7 @@ const lightThemeId = computed({
}
},
});
+
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
@@ -141,7 +154,7 @@ watch(wallpaper, () => {
} else {
miLocalStorage.setItem('wallpaper', wallpaper.value);
}
- location.reload();
+ reloadAsk();
});
onActivated(() => {
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 68c36ca1b4..043b6efd73 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
import { defaultStore } from '@/store.js';
import { useStream } from '@/stream.js';
import { deepClone } from '@/scripts/clone.js';
+import { deepMerge } from '@/scripts/merge.js';
type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount';
@@ -84,29 +85,9 @@ export class Storage<T extends StateDef> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
- /**
- * valueにないキーをdefからもらう(再帰的)\
- * nullはそのまま、undefinedはdefの値
- **/
- private mergeObject<X>(value: X, def: X): X {
- if (this.isPureObject(value) && this.isPureObject(def)) {
- const result = structuredClone(value) 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 (this.isPureObject(v) && this.isPureObject(result[k])) {
- const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
- result[k] = this.mergeObject<typeof v>(child, v);
- }
- }
- return result;
- }
- return value;
- }
-
private mergeState<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
- const merged = this.mergeObject(value, def);
+ const merged = deepMerge(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts
index 5adb3f606f..c6a520e913 100644
--- a/packages/frontend/src/router/main.ts
+++ b/packages/frontend/src/router/main.ts
@@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter {
return this.supplier().resolve(path);
}
+ init(): void {
+ this.supplier().init();
+ }
+
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
return this.supplier().eventNames();
}
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);
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index afc35bb825..641a506679 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js';
+import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki';
import { Storage } from '@/pizzax.js';
import { hemisphere } from '@/scripts/intl-const.js';
diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5
index 3f5822977a..c82a956868 100644
--- a/packages/frontend/src/themes/_dark.json5
+++ b/packages/frontend/src/themes/_dark.json5
@@ -94,4 +94,8 @@
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
+
+ codeHighlighter: {
+ base: 'one-dark-pro',
+ },
}
diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5
index 6ebfcaafeb..63bc030916 100644
--- a/packages/frontend/src/themes/_light.json5
+++ b/packages/frontend/src/themes/_light.json5
@@ -94,4 +94,8 @@
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
+
+ codeHighlighter: {
+ base: 'catppuccin-latte',
+ },
}