Files
edentext/src/lib/editor/extensions/characterStyle.ts
T

65 lines
2.4 KiB
TypeScript

import { Mark, mergeAttributes } from '@tiptap/core';
// A named character style on a run (LibreOffice style:family="text" / Word w:rStyle):
// paragraphStyle.ts one level down. Rendered as data-char-style for the generated
// document stylesheet; direct marks are inline styles and still win.
declare module '@tiptap/core' {
interface Commands<ReturnType> {
characterStyle: {
setCharacterStyle: (name: string) => ReturnType;
unsetCharacterStyle: () => ReturnType;
};
}
}
export const CharacterStyle = Mark.create({
name: 'charStyle',
addAttributes() {
return {
name: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute('data-char-style') || null,
renderHTML: (attributes: Record<string, unknown>) =>
attributes.name ? { 'data-char-style': String(attributes.name) } : {},
},
};
},
parseHTML() {
return [{ tag: 'span[data-char-style]' }];
},
renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes(HTMLAttributes), 0];
},
addCommands() {
return {
setCharacterStyle: (name: string) => ({ commands }) => commands.setMark(this.name, { name }),
unsetCharacterStyle: () => ({ commands }) => commands.unsetMark(this.name),
};
},
});
// The character style covering the selection, or null when there is none / it is mixed.
export function activeCharacterStyle(state: {
selection: { from: number; to: number; empty: boolean; $head: { marks(): readonly { type: { name: string }; attrs: Record<string, unknown> }[] } };
storedMarks?: readonly { type: { name: string }; attrs: Record<string, unknown> }[] | null;
doc: { nodesBetween(from: number, to: number, f: (node: { isText: boolean; marks: readonly { type: { name: string }; attrs: Record<string, unknown> }[] }) => void): void };
}): string | null {
const nameOf = (marks: readonly { type: { name: string }; attrs: Record<string, unknown> }[]) =>
(marks.find(m => m.type.name === 'charStyle')?.attrs.name as string | undefined) ?? null;
if (state.selection.empty) return nameOf(state.storedMarks ?? state.selection.$head.marks());
let found: string | null | undefined;
let mixed = false;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
if (mixed || !node.isText) return;
const name = nameOf(node.marks);
if (found === undefined) found = name;
else if (found !== name) mixed = true;
});
return mixed ? null : found ?? null;
}