feat(editor): replace format bar with selection appearance controls
This commit is contained in:
1 parent
19aab631ec
commit
5abc99b89e
15 files changed
+634
-130
No files matched your search
@@ -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
|
||||
},
|
||||
}],
|
||||
}
|
||||
Reference in new issue
Block a user