From 59603afc9482b57ee618c22b7281c884d610a04c Mon Sep 17 00:00:00 2001 From: eezd Date: Sun, 25 Oct 2026 16:03:08 +0800 Subject: [PATCH] fix(editor): unify text and blank line heights Apply configured line height to the editor scroller and remove independent paragraph spacing. Update settings, typography preview, and regression coverage. --- web/src/app/SettingsDialog.vue | 1 - web/src/app/app.css | 3 ++- web/src/app/settingsCatalog.ts | 1 - web/src/app/workspace.ts | 2 +- web/src/editor/livepreview.ts | 3 --- web/src/themes/base.css | 4 +--- web/src/themes/typography.test.ts | 15 +++++++++++++++ web/src/themes/typography.ts | 7 +++---- 8 files changed, 22 insertions(+), 14 deletions(-) diff --git a/web/src/app/SettingsDialog.vue b/web/src/app/SettingsDialog.vue index fef3bcc..84b2717 100644 --- a/web/src/app/SettingsDialog.vue +++ b/web/src/app/SettingsDialog.vue @@ -181,7 +181,6 @@ async function changePassword() {
{{ settings.lineHeight.toFixed(2) }} - {{ settings.paragraphSpacing }}px
排版预览

让文字自由呼吸

用简洁的记录,连接每一个想法。Markdown 文件始终属于你。

真实内容,自然排版,专注当下的灵感。

const idea = 'Hello, RawMark'

编辑行为

diff --git a/web/src/app/app.css b/web/src/app/app.css index 1b20d4e..26de7ce 100644 --- a/web/src/app/app.css +++ b/web/src/app/app.css @@ -465,7 +465,8 @@ button { .rm-settings :is(.rm-setting, .rm-settings-details, .rm-settings-form):focus { outline: 2px solid var(--rm-accent); outline-offset: -2px; border-radius: 6px; } .rm-typography-preview { margin: 8px 0 0; padding: 16px; border: 1px solid var(--rm-divider); border-radius: 8px; background: var(--rm-sidebar-bg); font: var(--rm-font-size)/var(--rm-line-height) var(--rm-font-editor, var(--rm-font-text)); overflow-wrap: anywhere; } .rm-typography-preview h2 { margin-top: 0; font-family: var(--rm-font-heading); font-size: 1.5em; } -.rm-typography-preview p { margin-bottom: var(--rm-paragraph-spacing); } +.rm-typography-preview p { margin: 0; } +.rm-typography-preview p + p { margin-top: 1lh; } .rm-typography-preview code { font-family: var(--rm-font-mono); font-size: .9em; color: var(--rm-inline-code-text); } .rm-settings .rm-share-note, .rm-settings .rm-appearance-note { margin: 12px 0; overflow-wrap: anywhere; } .rm-settings .rm-share-form { flex-wrap: wrap; padding: 16px 0 0; } diff --git a/web/src/app/settingsCatalog.ts b/web/src/app/settingsCatalog.ts index bedbb8a..fbf6891 100644 --- a/web/src/app/settingsCatalog.ts +++ b/web/src/app/settingsCatalog.ts @@ -36,7 +36,6 @@ export const SETTING_ITEMS = { 'font-size': { section: 'editor', title: '编辑器字号', description: '调整正文与代码的基础字号,不影响界面字号。', keywords: '正文 大小' }, 'content-width': { section: 'editor', title: '正文宽度', description: '限制正文最大宽度,或使用可用空间的全宽。', keywords: '窄 标准 宽 全宽' }, 'line-height': { section: 'editor', title: '正文行高', description: '调整正文各行之间的距离。', keywords: '行距 排版' }, - 'paragraph-spacing': { section: 'editor', title: '段落间距', description: '调整相邻段落之间的距离。', keywords: '段距 排版' }, 'selection-toolbar': { section: 'editor', title: '选中文本浮动工具栏', description: '选中文字后显示常用格式操作。', keywords: '格式 编辑' }, 'spellcheck': { section: 'editor', title: '拼写检查', description: '使用浏览器的拼写检查,检查语言取决于浏览器设置。', keywords: '语言 拼写' }, 'tab-size': { section: 'editor', title: '制表符宽度', description: '仅调整 Tab 字符的显示宽度,不改写正文;Tab 键仍按两个空格缩进。', keywords: 'Tab 缩进 空格' }, diff --git a/web/src/app/workspace.ts b/web/src/app/workspace.ts index ef0ee48..da9f103 100644 --- a/web/src/app/workspace.ts +++ b/web/src/app/workspace.ts @@ -1038,7 +1038,7 @@ export async function startWorkspace() { ) // 字号、正文宽度变化后行高随之改变,编辑器需要重新测量 watch( - () => [settings.uiFont, settings.uiFontSize, settings.fontSize, settings.contentWidth, settings.sidebarWidth, settings.textFont, settings.codeFont, settings.lineHeight, settings.paragraphSpacing], + () => [settings.uiFont, settings.uiFontSize, settings.fontSize, settings.contentWidth, settings.sidebarWidth, settings.textFont, settings.codeFont, settings.lineHeight], () => view?.requestMeasure(), ) watch( diff --git a/web/src/editor/livepreview.ts b/web/src/editor/livepreview.ts index 69cf251..61172bc 100644 --- a/web/src/editor/livepreview.ts +++ b/web/src/editor/livepreview.ts @@ -125,9 +125,6 @@ function buildDecorations(view: EditorView): DecorationSet { return } switch (name) { - case 'Paragraph': - if (!node.parent || !['ListItem', 'Blockquote'].includes(node.parent.name)) out.push(lineDeco('cm-rm-paragraph-end').range(doc.lineAt(node.to).from)) - return case 'HeaderMark': { if (isActive(node.from)) return const parent = node.parent! diff --git a/web/src/themes/base.css b/web/src/themes/base.css index a511ffd..9a36961 100644 --- a/web/src/themes/base.css +++ b/web/src/themes/base.css @@ -30,7 +30,6 @@ --rm-header-height: calc(var(--rm-ui-font-size) + 48px); --rm-line-height: 1.75; --rm-content-width: 760px; - --rm-paragraph-spacing: 16px; --rm-glass-opacity: 0.75; --rm-bg: #faf8ff; --rm-sidebar-bg: #f2f3ff; @@ -114,7 +113,6 @@ button, input, select, textarea { font: inherit; } .cm-editor { font-size: var(--rm-font-size); - line-height: var(--rm-line-height); background: var(--rm-bg); color: var(--rm-text); } @@ -123,6 +121,7 @@ button, input, select, textarea { font: inherit; } } .cm-editor .cm-scroller { font-family: var(--rm-font-editor, var(--rm-font-text)); + line-height: var(--rm-line-height); } .cm-editor .cm-content { max-width: var(--rm-content-width); @@ -348,7 +347,6 @@ button, input, select, textarea { font: inherit; } .cm-rm-code-lang { font-family: var(--rm-font-mono); } .cm-line.cm-rm-code-begin::before { border-top: 1px solid var(--rm-divider); } .cm-line.cm-rm-code-end::before { border-bottom: 1px solid var(--rm-divider); } -.cm-line.cm-rm-paragraph-end { padding-bottom: var(--rm-paragraph-spacing); } @media (max-width: 767px) { .cm-line.cm-rm-h1 { font-size: 1.75em; } } diff --git a/web/src/themes/typography.test.ts b/web/src/themes/typography.test.ts index cf103f7..6daddf6 100644 --- a/web/src/themes/typography.test.ts +++ b/web/src/themes/typography.test.ts @@ -9,6 +9,21 @@ describe('界面与正文排版设置', () => { expect(sanitizeDisplaySettings({ fontSize: 24 })).toMatchObject({ uiFontSize: 14, fontSize: 24 }) }) + it('旧段间距设置不再进入正文排版,保留已设置的字号和行距', () => { + const display = sanitizeDisplaySettings({ paragraphSpacing: 40, fontSize: 24, lineHeight: 2 }) + expect(display).not.toHaveProperty('paragraphSpacing') + applyDisplaySettings(display) + expect(document.documentElement.style.getPropertyValue('--rm-font-size')).toBe('24px') + expect(document.documentElement.style.getPropertyValue('--rm-line-height')).toBe('2') + }) + + it.each([1.2, 1.75, 2.5])('保留并应用允许的正文行距 %s', lineHeight => { + const display = sanitizeDisplaySettings({ lineHeight }) + expect(display.lineHeight).toBe(lineHeight) + applyDisplaySettings(display) + expect(document.documentElement.style.getPropertyValue('--rm-line-height')).toBe(String(lineHeight)) + }) + it.each([14, 16, 20])('接受范围内的整数界面字号 %s', uiFontSize => { expect(sanitizeDisplaySettings({ uiFontSize }).uiFontSize).toBe(uiFontSize) }) diff --git a/web/src/themes/typography.ts b/web/src/themes/typography.ts index 1ef80e6..1cdbf52 100644 --- a/web/src/themes/typography.ts +++ b/web/src/themes/typography.ts @@ -13,10 +13,10 @@ export const CODE_FONTS = { } export const UI_FONT_SIZE = { min: 14, max: 20, default: 14 } export const FONT_PRESETS: Record = { ...TEXT_FONTS, jetbrains: CODE_FONTS.jetbrains } -export const DISPLAY_DEFAULTS = { uiFont: 'maple', uiFontSize: UI_FONT_SIZE.default, fontSize: 16, contentWidth: 760, textFont: 'maple', codeFont: 'maple', lineHeight: 1.75, paragraphSpacing: 16, glassOpacity: .75 } +export const DISPLAY_DEFAULTS = { uiFont: 'maple', uiFontSize: UI_FONT_SIZE.default, fontSize: 16, contentWidth: 760, textFont: 'maple', codeFont: 'maple', lineHeight: 1.75, glassOpacity: .75 } export type DisplaySettings = typeof DISPLAY_DEFAULTS export function sanitizeDisplaySettings(raw: Record, customIDs: string[] = [], hiddenFonts: string[] = []): DisplaySettings { - const ranged = (key: 'lineHeight' | 'paragraphSpacing' | 'glassOpacity', min: number, max: number) => typeof raw[key] === 'number' && Number.isFinite(raw[key]) && raw[key] >= min && raw[key] <= max ? raw[key] : DISPLAY_DEFAULTS[key] + const ranged = (key: 'lineHeight' | 'glassOpacity', min: number, max: number) => typeof raw[key] === 'number' && Number.isFinite(raw[key]) && raw[key] >= min && raw[key] <= max ? raw[key] : DISPLAY_DEFAULTS[key] const font = (value: unknown) => typeof value === 'string' && !hiddenFonts.includes(value) && (Object.hasOwn(FONT_PRESETS, value) || customIDs.includes(value)) ? value : 'maple' return { uiFont: font(raw.uiFont), @@ -25,7 +25,7 @@ export function sanitizeDisplaySettings(raw: Record, customIDs: contentWidth: typeof raw.contentWidth === 'number' && [0, 680, 760, 960].includes(raw.contentWidth) ? raw.contentWidth : 760, textFont: font(raw.textFont), codeFont: font(raw.codeFont), - lineHeight: ranged('lineHeight', 1.2, 2.5), paragraphSpacing: ranged('paragraphSpacing', 0, 40), glassOpacity: ranged('glassOpacity', .4, 1), + lineHeight: ranged('lineHeight', 1.2, 2.5), glassOpacity: ranged('glassOpacity', .4, 1), } } export function applyDisplaySettings(settings: DisplaySettings) { @@ -38,6 +38,5 @@ export function applyDisplaySettings(settings: DisplaySettings) { root.setProperty('--rm-font-editor', css(settings.textFont)) root.setProperty('--rm-font-mono', css(settings.codeFont, true)) root.setProperty('--rm-line-height', String(settings.lineHeight)) - root.setProperty('--rm-paragraph-spacing', settings.paragraphSpacing + 'px') root.setProperty('--rm-glass-opacity', String(settings.glassOpacity)) }