feat(web): highlight Markdown source and switch themes from the toolbar
Light auto theme now colors headings, marks, quotes, and links so source mode is readable. The editor toolbar can pick the same Markdown themes as settings.
This commit is contained in:
1 parent
a10267b0d6
commit
a9cd05e960
9 files changed
+102
-10
No files matched your search
@@ -6,6 +6,7 @@
|
||||
"dependencies": {
|
||||
"@antv/x6": "3.1.8",
|
||||
"@codemirror/lang-markdown": "^6.5.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@edgeever/client": "workspace:*",
|
||||
"@floating-ui/react-dom": "^2.1.8",
|
||||
"@fontsource-variable/noto-sans-sc": "^5.2.8",
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { CODE_BLOCK_LANGUAGES, getCodeBlockLanguageValue } from "@/lib/code-block";
|
||||
import { EditorTableMenu } from "@/components/EditorTableMenu";
|
||||
import { wrapIndentedParagraphInList } from "@/lib/editor-shortcuts";
|
||||
import { MARKDOWN_THEME_PREFERENCES, useMarkdownTheme } from "@/components/ThemeProvider";
|
||||
|
||||
const EditorToolbarButton = ({
|
||||
active = false,
|
||||
@@ -159,6 +160,7 @@ export const EditorToolbar = ({
|
||||
externalLinkActive?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { markdownThemePreference, setMarkdownTheme } = useMarkdownTheme();
|
||||
const controlsRef = useRef<HTMLDivElement>(null);
|
||||
const [expanded, setExpanded] = useState(readEditorToolbarExpandedPreference);
|
||||
const [hasOverflow, setHasOverflow] = useState(false);
|
||||
@@ -366,7 +368,24 @@ export const EditorToolbar = ({
|
||||
</>
|
||||
)}
|
||||
{markdownMode ? (
|
||||
<span className="shrink-0 text-xs text-slate-500">{t("editorToolbar.markdownSource")}</span>
|
||||
<Select
|
||||
value={markdownThemePreference}
|
||||
onValueChange={(value) => setMarkdownTheme(value as typeof markdownThemePreference)}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("editorToolbar.markdownTheme")}
|
||||
className="h-8 w-[11.5rem] shrink-0 whitespace-nowrap border-slate-200 bg-card text-xs text-slate-800 [&>span]:truncate [&>span]:whitespace-nowrap"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-card border border-slate-200 rounded-md py-1 shadow-md">
|
||||
{MARKDOWN_THEME_PREFERENCES.map((theme) => (
|
||||
<SelectItem key={theme} value={theme}>
|
||||
{t(`settings.markdownThemes.${theme}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<>
|
||||
<Select
|
||||
|
||||
@@ -87,6 +87,14 @@ export const MARKDOWN_THEME_NAMES = [
|
||||
export type MarkdownThemeName = (typeof MARKDOWN_THEME_NAMES)[number];
|
||||
export const MARKDOWN_THEME_PREFERENCES = ["auto", ...MARKDOWN_THEME_NAMES] as const;
|
||||
export type MarkdownThemePreference = (typeof MARKDOWN_THEME_PREFERENCES)[number];
|
||||
export const MARKDOWN_LIGHT_THEME_NAMES = [
|
||||
"github-light",
|
||||
"solarized-light",
|
||||
"xcode-light",
|
||||
"duotone-light",
|
||||
] as const satisfies readonly MarkdownThemeName[];
|
||||
export const isMarkdownLightTheme = (theme: MarkdownThemeName): boolean =>
|
||||
(MARKDOWN_LIGHT_THEME_NAMES as readonly MarkdownThemeName[]).includes(theme);
|
||||
|
||||
export const EDITOR_THEME_NAMES = [
|
||||
"default",
|
||||
|
||||
@@ -6,7 +6,7 @@ import CodeMirror, {
|
||||
} from "@uiw/react-codemirror";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import {
|
||||
githubLight,
|
||||
githubLightInit,
|
||||
githubDark,
|
||||
atomone,
|
||||
tokyoNight,
|
||||
@@ -14,17 +14,18 @@ import {
|
||||
dracula,
|
||||
nord,
|
||||
monokai,
|
||||
solarizedLight,
|
||||
solarizedLightInit,
|
||||
solarizedDark,
|
||||
vscodeDark,
|
||||
xcodeLight,
|
||||
xcodeLightInit,
|
||||
sublime,
|
||||
duotoneLight,
|
||||
duotoneLightInit,
|
||||
duotoneDark,
|
||||
gruvboxDark,
|
||||
} from "@uiw/codemirror-themes-all";
|
||||
import type { MarkdownThemeName } from "../ThemeProvider";
|
||||
import { getAiSlashCommandStart } from "@/lib/editor-shortcuts";
|
||||
import { lightMarkdownHighlightStyles } from "@/lib/markdown-source-highlight";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface MarkdownSourceEditorRef {
|
||||
@@ -49,7 +50,7 @@ export interface MarkdownSourceEditorProps {
|
||||
}
|
||||
|
||||
export const CODE_MIRROR_THEME_MAP: Record<MarkdownThemeName, Extension> = {
|
||||
"github-light": githubLight,
|
||||
"github-light": githubLightInit({ styles: lightMarkdownHighlightStyles }),
|
||||
"github-dark": githubDark,
|
||||
"one-dark": atomone,
|
||||
"tokyo-night": tokyoNight,
|
||||
@@ -57,12 +58,12 @@ export const CODE_MIRROR_THEME_MAP: Record<MarkdownThemeName, Extension> = {
|
||||
dracula,
|
||||
nord,
|
||||
monokai,
|
||||
"solarized-light": solarizedLight,
|
||||
"solarized-light": solarizedLightInit({ styles: lightMarkdownHighlightStyles }),
|
||||
"solarized-dark": solarizedDark,
|
||||
"vscode-dark": vscodeDark,
|
||||
"xcode-light": xcodeLight,
|
||||
"xcode-light": xcodeLightInit({ styles: lightMarkdownHighlightStyles }),
|
||||
sublime,
|
||||
"duotone-light": duotoneLight,
|
||||
"duotone-light": duotoneLightInit({ styles: lightMarkdownHighlightStyles }),
|
||||
"duotone-dark": duotoneDark,
|
||||
"gruvbox-dark": gruvboxDark,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { tags as t } from "@lezer/highlight";
|
||||
|
||||
/**
|
||||
* Appended to light CodeMirror themes so Markdown source tokens stay
|
||||
* distinct. Later rules in the same HighlightStyle win, which is why these
|
||||
* go through `*Init({ styles })` instead of a second highlighter.
|
||||
*/
|
||||
export const lightMarkdownHighlightStyles = [
|
||||
{ tag: t.heading1, color: "#0550ae", fontWeight: "700" },
|
||||
{ tag: t.heading2, color: "#0550ae", fontWeight: "700" },
|
||||
{ tag: t.heading3, color: "#0a3069", fontWeight: "700" },
|
||||
{ tag: t.heading4, color: "#0f766e", fontWeight: "700" },
|
||||
{ tag: t.heading5, color: "#0f766e", fontWeight: "700" },
|
||||
{ tag: t.heading6, color: "#0f766e", fontWeight: "700" },
|
||||
{ tag: t.heading, color: "#0550ae", fontWeight: "700" },
|
||||
{ tag: t.processingInstruction, color: "#64748b" },
|
||||
{ tag: t.meta, color: "#64748b" },
|
||||
{ tag: t.comment, color: "#64748b" },
|
||||
{ tag: t.strong, color: "#0f172a", fontWeight: "700" },
|
||||
{ tag: t.emphasis, color: "#9f1239", fontStyle: "italic" },
|
||||
{ tag: t.strikethrough, color: "#64748b", textDecoration: "line-through" },
|
||||
{ tag: t.link, color: "#0969da" },
|
||||
{ tag: t.url, color: "#0969da", textDecoration: "underline" },
|
||||
{ tag: t.monospace, color: "#be185d" },
|
||||
{ tag: t.quote, color: "#15803d" },
|
||||
{ tag: t.atom, color: "#0f766e", fontWeight: "700" },
|
||||
{ tag: t.contentSeparator, color: "#94a3b8" },
|
||||
{ tag: t.labelName, color: "#7c3aed" },
|
||||
{ tag: t.string, color: "#0f766e" },
|
||||
{ tag: t.escape, color: "#b45309" },
|
||||
{ tag: t.character, color: "#b45309" },
|
||||
];
|
||||
@@ -1,11 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
getStoredMarkdownTheme,
|
||||
isMarkdownLightTheme,
|
||||
MARKDOWN_LIGHT_THEME_NAMES,
|
||||
MARKDOWN_THEME_NAMES,
|
||||
MARKDOWN_THEME_PREFERENCES,
|
||||
resolveMarkdownTheme,
|
||||
} from "../components/ThemeProvider";
|
||||
import { CODE_MIRROR_THEME_MAP } from "../components/editor/MarkdownSourceEditor";
|
||||
import { lightMarkdownHighlightStyles } from "./markdown-source-highlight";
|
||||
|
||||
describe("markdown theme contracts", () => {
|
||||
test("automatic Markdown themes follow the resolved appearance", () => {
|
||||
@@ -34,4 +38,28 @@ describe("markdown theme contracts", () => {
|
||||
expect(extension).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("light markdown themes receive an extra syntax highlight layer", () => {
|
||||
const editorSource = readFileSync(new URL("../components/editor/MarkdownSourceEditor.tsx", import.meta.url), "utf8");
|
||||
expect(lightMarkdownHighlightStyles.length).toBeGreaterThan(0);
|
||||
expect(editorSource).toContain("lightMarkdownHighlightStyles");
|
||||
for (const theme of MARKDOWN_LIGHT_THEME_NAMES) {
|
||||
expect(MARKDOWN_THEME_NAMES).toContain(theme);
|
||||
expect(isMarkdownLightTheme(theme)).toBe(true);
|
||||
expect(CODE_MIRROR_THEME_MAP[theme]).toBeDefined();
|
||||
expect(editorSource).toContain(`"${theme}":`);
|
||||
}
|
||||
expect(isMarkdownLightTheme("tokyo-night")).toBe(false);
|
||||
expect(editorSource).toContain("githubLightInit({ styles: lightMarkdownHighlightStyles })");
|
||||
expect(editorSource).toContain("xcodeLightInit({ styles: lightMarkdownHighlightStyles })");
|
||||
});
|
||||
|
||||
test("the editor toolbar can switch Markdown source themes", () => {
|
||||
const toolbar = readFileSync(new URL("../components/EditorToolbar.tsx", import.meta.url), "utf8");
|
||||
expect(toolbar).toContain("useMarkdownTheme");
|
||||
expect(toolbar).toContain("MARKDOWN_THEME_PREFERENCES");
|
||||
expect(toolbar).toContain("markdownThemePreference");
|
||||
expect(toolbar).toContain('t("editorToolbar.markdownTheme")');
|
||||
expect(toolbar).not.toContain('t("editorToolbar.markdownSource")');
|
||||
});
|
||||
});
|
||||
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "@edgeever/desktop",
|
||||
"version": "1.59.0",
|
||||
"version": "1.67.0",
|
||||
"dependencies": {
|
||||
"@edgeever/public-network": "workspace:*",
|
||||
"croner": "^10.0.1",
|
||||
@@ -217,6 +217,7 @@
|
||||
"@edgeever/client": "workspace:*",
|
||||
"@floating-ui/react-dom": "^2.1.8",
|
||||
"@fontsource-variable/noto-sans-sc": "^5.2.8",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@tiptap/extension-code-block-lowlight": "3.31.0",
|
||||
"@tiptap/pm": "3.31.0",
|
||||
"@uiw/codemirror-themes-all": "^4.25.11",
|
||||
|
||||
@@ -1879,6 +1879,7 @@ export const enUS = {
|
||||
markdown: "Switch to Markdown source",
|
||||
richText: "Switch to rich text",
|
||||
markdownSource: "Markdown source",
|
||||
markdownTheme: "Markdown source theme",
|
||||
switchToMarkdown: "Switch to Markdown mode",
|
||||
switchToRichText: "Switch to rich text mode",
|
||||
attachment: "Upload attachment",
|
||||
|
||||
@@ -1877,6 +1877,7 @@ export const zhCN = {
|
||||
markdown: "切换到 Markdown 源码",
|
||||
richText: "切换到富文本编辑",
|
||||
markdownSource: "Markdown 源码",
|
||||
markdownTheme: "Markdown 源码主题",
|
||||
switchToMarkdown: "切换到 Markdown 模式",
|
||||
switchToRichText: "切换到富文本模式",
|
||||
attachment: "上传附件",
|
||||
|
||||
Reference in new issue
Block a user