feat(editor): replace format bar with selection appearance controls

This commit is contained in:
eezd committed 2026-10-24 18:11:57 +08:00
1 parent 19aab631ec
commit 5abc99b89e
15 files changed
+634 -130

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
import type { MarkdownConfig } from '@lezer/markdown'
import { CODE_FONTS, TEXT_FONTS } from '../themes/typography'
export const INLINE_FONTS = { ...TEXT_FONTS, mono: CODE_FONTS.jetbrains }
export const HOLLOW = '0.2px currentColor'
export const SHADOW = '1px 1px 2px currentColor'
export type InlineStyles = Partial<Record<'color' | 'background-color' | 'font-family' | 'font-size' | '-webkit-text-stroke' | '-webkit-text-fill-color' | 'text-shadow', string>>
/** 笔记是外部输入;仅允许工具栏实际生成的样式,不开放通用 HTML/CSS。 */
export function parseInlineStyles(value: string): InlineStyles | null {
const result: InlineStyles = {}
for (const declaration of value.split(';')) {
if (!declaration.trim()) continue
const colon = declaration.indexOf(':')
if (colon < 0) return null
const key = declaration.slice(0, colon).trim()
const val = declaration.slice(colon + 1).trim()
switch (key) {
case 'color': case 'background-color':
if (!/^#[\da-f]{6}$/i.test(val)) return null
result[key] = val.toLowerCase(); break
case 'font-family':
if (!Object.values(INLINE_FONTS).some(font => font.css === val)) return null
result[key] = val; break
case 'font-size': {
const match = /^(\d+(?:\.\d+)?)(px|em)$/.exec(val)
if (!match || !(match[2] === 'px' ? +match[1] >= 9 && +match[1] <= 72 : +match[1] >= .56 && +match[1] <= 4.5)) return null
result[key] = val; break
}
case '-webkit-text-stroke':
if (val !== HOLLOW) return null
result[key] = val; break
case '-webkit-text-fill-color':
if (val !== 'transparent') return null
result[key] = val; break
case 'text-shadow':
if (val !== SHADOW) return null
result[key] = val; break
default: return null
}
}
return Object.keys(result).length ? result : null
}
export function inlineStyleText(styles: InlineStyles): string {
return Object.entries(styles).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}: ${value}`).join('; ')
}
export function stylesFromTag(tag: string): InlineStyles | null {
const match = /^<span\s+style="([^"<>]*)"\s*>$/.exec(tag)
return match ? parseInlineStyles(match[1]) : null
}
const underline = { resolve: 'Underline', mark: 'UnderlineMark' }
const appearance = { resolve: 'InlineStyle', mark: 'InlineStyleMark' }
export const inlineStyleSyntax: MarkdownConfig = {
defineNodes: ['Underline', 'UnderlineMark', 'InlineStyle', 'InlineStyleMark'],
parseInline: [{
name: 'InlineAppearance', before: 'HTMLTag',
parse(cx, next, pos) {
if (next !== 60) return -1
const tag = /^<[^<>\n]*>/.exec(cx.slice(pos, cx.end))?.[0]
if (!tag) return -1
if (tag === '<u>') return cx.addDelimiter(underline, pos, pos + tag.length, true, false)
if (tag === '</u>') return cx.addDelimiter(underline, pos, pos + tag.length, false, true)
if (stylesFromTag(tag)) return cx.addDelimiter(appearance, pos, pos + tag.length, true, false)
if (tag === '</span>') return cx.addDelimiter(appearance, pos, pos + tag.length, false, true)
return -1
},
}],
}