Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@
<div
class="editor-wrapper"
:class="[{ typewriter: typewriter, focus: focus, source: sourceCode }]"
:style="{
lineHeight: lineHeight,
fontSize: `${fontSize}px`,
'font-family': editorFontFamily
? `${editorFontFamily}, ${defaultFontFamily}`
: `${defaultFontFamily}`
}"
:dir="textDirection"
>
<div
Expand Down Expand Up @@ -122,7 +115,7 @@ import { exportStyledHTML, type HeaderFooterPart } from '@/util/exportHtml'
import { applyCursor, isIndexCursor } from '@/util/cursor'
import EditorSearch from '../search/index.vue'
import bus from '@/bus'
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
import { DEFAULT_EDITOR_FONT_FAMILY, DEFAULT_CODE_FONT_FAMILY } from '@/config'
import notice from '@/services/notification'
import Printer from '@/services/printService'
import { SpellcheckerLanguageCommand } from '@/commands'
Expand All @@ -132,7 +125,7 @@ import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
import { guessClipboardFilePath } from '@/util/clipboard'
import { getCssForOptions, getHtmlToc, type PdfCssOptions, type HtmlTocOptions } from '@/util/pdf'
import { resolveTocHeadingElement } from '@/util/tocNavigation'
import { addCommonStyle, setEditorWidth, setWrapCodeBlocks } from '@/util/theme'
import { addCommonStyle, setEditorWidth } from '@/util/theme'
import { usePreferencesStore } from '@/store/preferences'
import { useEditorStore } from '@/store/editor'
import { useProjectStore } from '@/store/project'
Expand Down Expand Up @@ -267,6 +260,9 @@ const { projectTree } = storeToRefs(projectStore)

// Component state
const defaultFontFamily = DEFAULT_EDITOR_FONT_FAMILY
const resolveEditorFont = (family: string): string =>
family ? `${family}, ${defaultFontFamily}` : defaultFontFamily
const resolveCodeFont = (family: string): string => `${family}, ${DEFAULT_CODE_FONT_FAMILY}`
const selectionChange = ref<unknown>(null)
const editor = ref<MuyaInstance>(null)
const isShowClose = ref(false)
Expand Down Expand Up @@ -552,13 +548,19 @@ watch(focus, (value) => {

watch(fontSize, (value, oldValue) => {
if (value !== oldValue && editor.value) {
editor.value.setFont({ fontSize: value })
editor.value.setOptions({ fontSize: value })
}
})

watch(lineHeight, (value, oldValue) => {
if (value !== oldValue && editor.value) {
editor.value.setFont({ lineHeight: value })
editor.value.setOptions({ lineHeight: value })
}
})

watch(editorFontFamily, (value, oldValue) => {
if (value !== oldValue && editor.value) {
editor.value.setOptions({ editorFontFamily: resolveEditorFont(value) })
}
})

Expand All @@ -572,7 +574,7 @@ watch(preferLooseListItem, (value, oldValue) => {

watch(tabSize, (value, oldValue) => {
if (value !== oldValue && editor.value) {
editor.value.setTabSize(value)
editor.value.setOptions({ tabSize: value })
}
})

Expand Down Expand Up @@ -660,8 +662,8 @@ watch(editorLineWidth, (value, oldValue) => {
})

watch(wrapCodeBlocks, (value, oldValue) => {
if (value !== oldValue) {
setWrapCodeBlocks(value)
if (value !== oldValue && editor.value) {
editor.value.setOptions({ wrapCodeBlocks: value })
}
})

Expand Down Expand Up @@ -714,7 +716,9 @@ watch(autoCheck, (value, oldValue) => {
})

watch(codeFontSize, (value, oldValue) => {
if (value !== oldValue) {
if (value !== oldValue && editor.value) {
editor.value.setOptions({ codeFontSize: value })
// Source-mode CodeMirror is a separate surface muya doesn't own.
addCommonStyle({
codeFontSize: value,
codeFontFamily: codeFontFamily.value,
Expand All @@ -730,7 +734,9 @@ watch(codeBlockLineNumbers, (value, oldValue) => {
})

watch(codeFontFamily, (value, oldValue) => {
if (value !== oldValue) {
if (value !== oldValue && editor.value) {
editor.value.setOptions({ codeFontFamily: resolveCodeFont(value) })
// Source-mode CodeMirror is a separate surface muya doesn't own.
addCommonStyle({
codeFontSize: codeFontSize.value,
codeFontFamily: value,
Expand Down Expand Up @@ -1695,6 +1701,10 @@ onMounted(() => {
tabSize: tabSize.value,
fontSize: fontSize.value,
lineHeight: lineHeight.value,
editorFontFamily: resolveEditorFont(editorFontFamily.value),
codeFontSize: codeFontSize.value,
codeFontFamily: resolveCodeFont(codeFontFamily.value),
wrapCodeBlocks: wrapCodeBlocks.value,
codeBlockLineNumbers: codeBlockLineNumbers.value,
listIndentation: listIndentation.value,
frontmatterType: frontmatterType.value,
Expand Down Expand Up @@ -1917,7 +1927,6 @@ onMounted(() => {

document.addEventListener('keyup', keyup)

setWrapCodeBlocks(wrapCodeBlocks.value)
setEditorWidth(editorLineWidth.value)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ const handleDeleteClick = (selectedItem: CustomDictionaryWord): void => {
.pref-spellchecker .el-table tr {
background: var(--editorBgColor);
}
/* Element Plus colours table cells with its own grey --el-text-color-regular,
which the app never themes, so the custom-dictionary words rendered as
low-contrast grey on every theme. Use the theme's editor text colour. */
.pref-spellchecker .el-table,
.pref-spellchecker .el-table th.el-table__cell,
.pref-spellchecker .el-table td.el-table__cell {
color: var(--editorColor);
}
.pref-spellchecker .el-table th.el-table__cell.is-leaf,
.pref-spellchecker .el-table th,
.pref-spellchecker .el-table td {
Expand All @@ -240,11 +248,14 @@ const handleDeleteClick = (selectedItem: CustomDictionaryWord): void => {
.pref-spellchecker .el-table__fixed::before {
background: var(--tableBorderColor);
}
.pref-spellchecker .el-table__body tr.hover-row.current-row > td,
.pref-spellchecker .el-table__body tr.hover-row.el-table__row--striped.current-row > td,
.pref-spellchecker .el-table__body tr.hover-row.el-table__row--striped > td,
.pref-spellchecker .el-table__body tr.hover-row > td {
background: var(--selectionColor);
/* Theme Element Plus's table colour variables so the active theme is honoured
instead of EP's light defaults: the hovered row (--el-fill-color-light, a
near-white bar that hides the text) and the header background
(--el-fill-color-blank / white, which left the fixed "Options" column header
a white block on dark themes). */
.pref-spellchecker .el-table {
--el-table-row-hover-bg-color: var(--selectionColor);
--el-table-header-bg-color: var(--editorBgColor);
}
.pref-spellchecker .el-table .el-table__cell {
padding: 2px 0;
Expand Down
28 changes: 1 addition & 27 deletions packages/desktop/src/renderer/src/util/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,26 +197,6 @@ export const addThemeStyle = (theme: string): void => {
}
}

export const setWrapCodeBlocks = (value: boolean): void => {
const CODE_WRAP_STYLE_ID = 'ag-code-wrap'
let result = ''
if (value) {
result =
'.mu-code-block .mu-code { display: block; white-space: pre-wrap; word-break: break-word; overflow: hidden; }'
} else {
result =
'.mu-code-block .mu-code { display: block; white-space: pre; word-break: break-word; overflow: auto; }'
}
let styleEle = document.querySelector(`#${CODE_WRAP_STYLE_ID}`) as HTMLStyleElement | null
if (!styleEle) {
styleEle = document.createElement('style')
styleEle.setAttribute('id', CODE_WRAP_STYLE_ID)
document.head.appendChild(styleEle)
}

styleEle.innerHTML = result
}

export const setEditorWidth = (value: string): void => {
const EDITOR_WIDTH_STYLE_ID = 'editor-width'
let result = ''
Expand Down Expand Up @@ -256,13 +236,7 @@ export const addCommonStyle = (options: CommonStyleOptions): void => {
}

sheet.innerHTML = `${scrollbarStyle}
span code,
td code,
th code,
code,
code[class*="language-"],
.CodeMirror,
.mu-code-block {
.CodeMirror {
font-family: ${codeFontFamily}, ${DEFAULT_CODE_FONT_FAMILY};
font-size: ${codeFontSize}px;
}
Expand Down
20 changes: 8 additions & 12 deletions packages/desktop/test/e2e/code-block-wrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@ import { launchWithMarkdown } from './helpers'
// Post-migration (muyajs -> @muyajs/core) coverage backfill for the
// "Wrap Code Blocks" and "Code Block Line Numbers" editor preferences.
//
// The wrap preference is implemented as an injected <style id="ag-code-wrap">
// (packages/desktop/src/renderer/src/util/theme.ts::setWrapCodeBlocks). Issue
// #4421 changed the selector from the legacy DOM class to
// `.mu-code-block .mu-code`. If that selector ever stops matching the rendered
// @muyajs/core code DOM (`pre.mu-code-block > code.mu-code`), the style silently
// no-ops — the computed white-space below would never flip. These tests prove
// the selector still matches the live engine DOM and that the round-trip
// (renderer -> main store -> broadcast -> Pinia watcher -> setWrapCodeBlocks /
// muya.setOptions(forceRender)) takes effect live.
// The wrap preference is implemented via muya.setOptions({ wrapCodeBlocks }),
// which toggles a .mu-code-wrap class on the editor root. Muya's stylesheet
// then applies `white-space: pre-wrap` to .mu-code-wrap .mu-code-block .mu-code.
// These tests prove the round-trip (renderer -> main store -> broadcast ->
// Pinia watcher -> muya.setOptions) takes effect live.
//
// The actual visual wrapping / horizontal scroll behaviour is not asserted here
// (that remains manual); we assert the load-bearing computed style + class.
Expand Down Expand Up @@ -58,8 +54,8 @@ test.describe('Code block wrap + line-numbers preferences', () => {

// Item 99: wrap preference toggles white-space on .mu-code-block .mu-code.
test('wrapCodeBlocks toggles computed white-space on .mu-code-block .mu-code', async() => {
// Default preference is wrapCodeBlocks: false, so on boot setWrapCodeBlocks
// injects `white-space: pre`. Establish the baseline first.
// Default preference is wrapCodeBlocks: false, so the editor root has no
// .mu-code-wrap class and white-space is pre. Establish the baseline first.
await expect.poll(() => readWhiteSpace(page), { timeout: 10000 }).toBe('pre')

// Enable wrapping -> selector should resolve to `pre-wrap`.
Expand Down Expand Up @@ -113,7 +109,7 @@ test.describe('Code block wrap + line-numbers preferences', () => {
{ timeout: 10000 }
)

// The newly-rendered .mu-code must still pick up the #ag-code-wrap style.
// The newly-rendered .mu-code must still pick up the muya wrap CSS.
await expect.poll(() => readWhiteSpace(page), { timeout: 10000 }).toBe('pre-wrap')

// Restore defaults so the suite leaves no global preference state behind.
Expand Down
51 changes: 9 additions & 42 deletions packages/desktop/test/unit/specs/editor-theme-style.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ vi.hoisted(() => {
w.window.path ??= { sep: '/' }
})

import { addCommonStyle, setWrapCodeBlocks, setEditorWidth } from '@/util/theme'
import { addCommonStyle, setEditorWidth } from '@/util/theme'
import { COMMON_STYLE_ID, DEFAULT_CODE_FONT_FAMILY } from '@/config'

const styleHtml = (id: string) =>
Expand All @@ -23,26 +23,28 @@ describe('theme.ts style injection helpers', () => {
document.body.className = ''
})

// Items 196, 210 — addCommonStyle injects code font onto the engine .mu-code-block.
// Items 196, 210 — addCommonStyle injects code font onto the source-mode .CodeMirror selector.
describe('addCommonStyle', () => {
it('injects code font-family/size onto the .mu-code-block selector list', () => {
it('injects code font-family/size onto the .CodeMirror selector', () => {
addCommonStyle({ codeFontFamily: 'Courier New', codeFontSize: 24 })

const css = styleHtml(COMMON_STYLE_ID)
// selector list targets the engine block class
expect(css).toContain('.mu-code-block')
// selector targets source-mode CodeMirror only (muya owns code-block font via --mu-code-* vars)
expect(css).toContain('.CodeMirror')
expect(css).not.toContain('.mu-code-block')
// family is the requested font followed by the default fallback chain
expect(css).toContain(`font-family: Courier New, ${DEFAULT_CODE_FONT_FAMILY};`)
// size is the numeric value suffixed with px
expect(css).toContain('font-size: 24px;')
})

it('targets the engine .mu-code-block class (not a legacy ag-* class)', () => {
it('targets only .CodeMirror (no legacy ag-* or mu-code-block classes)', () => {
addCommonStyle({ codeFontFamily: 'Courier New', codeFontSize: 14 })

const css = styleHtml(COMMON_STYLE_ID)
expect(css).toContain('.mu-code-block')
expect(css).toContain('.CodeMirror')
expect(css).not.toContain('.ag-code-block')
expect(css).not.toContain('.mu-code-block')
})

it('prepends the webkit scrollbar hide rule when hideScrollbar is true', () => {
Expand Down Expand Up @@ -71,41 +73,6 @@ describe('theme.ts style injection helpers', () => {
})
})

// Item 197 — setWrapCodeBlocks toggles pre-wrap vs pre on .mu-code-block .mu-code.
describe('setWrapCodeBlocks', () => {
const WRAP_STYLE_ID = 'ag-code-wrap'

it('uses white-space: pre-wrap and overflow: hidden when wrapping is enabled', () => {
setWrapCodeBlocks(true)

const css = styleHtml(WRAP_STYLE_ID)
expect(css).toContain('.mu-code-block .mu-code {')
expect(css).toContain('white-space: pre-wrap;')
expect(css).toContain('overflow: hidden;')
})

it('uses white-space: pre and overflow: auto when wrapping is disabled', () => {
setWrapCodeBlocks(false)

const css = styleHtml(WRAP_STYLE_ID)
expect(css).toContain('.mu-code-block .mu-code {')
expect(css).toContain('white-space: pre;')
expect(css).toContain('overflow: auto;')
// the disabled rule must not leak the wrapped values
expect(css).not.toContain('white-space: pre-wrap;')
})

it('replaces the rule in the same single style element when toggled true -> false', () => {
setWrapCodeBlocks(true)
setWrapCodeBlocks(false)

expect(styleCount(WRAP_STYLE_ID)).toBe(1)
const css = styleHtml(WRAP_STYLE_ID)
expect(css).toContain('white-space: pre;')
expect(css).not.toContain('white-space: pre-wrap;')
})
})

// Item 209 — setEditorWidth validates input and injects --editorAreaWidth override.
describe('setEditorWidth', () => {
const WIDTH_STYLE_ID = 'editor-width'
Expand Down
23 changes: 23 additions & 0 deletions packages/muya/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,29 @@ Each subfolder is a floating tool/menu (inline format toolbar, image tools, para

`src/index.ts` is the published entrypoint. The `exports` map in `package.json` points `.` at `./src/index.ts` during development and `./lib/es/index.js` after publish — keep this file the single export hub.

### Appearance contract (typography)

muya renders its own content's typography from two equivalent inputs — pass
options, or override the CSS custom properties directly (pure-CSS theming).
The variables are set on the editor root (`.mu-editor`) and consumed by the
bundled stylesheets; each has a default baked into the CSS, so passing nothing
renders the standalone defaults.

| Option (`IMuyaOptions`) | CSS variable | Default | Applies to |
|---|---|---|---|
| `fontSize` (number, px) | `--mu-font-size` | `16px` | `.mu-editor` base text |
| `lineHeight` (number) | `--mu-line-height` | `1.6` | `.mu-editor` base text |
| `editorFontFamily` (string) | `--mu-font-family` | Open Sans stack | `.mu-editor` base text |
| `codeFontSize` (number, px) | `--mu-code-font-size` | `90%` | `.mu-code-block` only |
| `codeFontFamily` (string) | `--mu-code-font-family` | DejaVu Sans Mono stack | `.mu-code-block` only |
| `wrapCodeBlocks` (boolean) | — (`.mu-code-wrap` root class) | off (`pre`) | code-block line wrapping |

Inline code (`code.mu-inline-rule`) is deliberately NOT driven by these — it
keeps its relative `0.8em` / mono sizing. Editor column width
(`--editor-area-width`) and the colour palette (`--editor-color-*`) are
separate, pre-existing contracts owned by the host. All runtime changes go
through `muya.setOptions({...})`.

## Conventions enforced by tooling

- **ESLint** (`eslint.config.mjs`, antfu base) adds:
Expand Down
Loading
Loading