Marks are inline formatting styles applied to text (bold, italic, links, colors, etc.).
Unlike blocks which define document structure, marks define how text within blocks is styled. Multiple marks can be applied to the same text range.
OpenBlock includes these marks:
| Mark | Description | Attributes |
|---|---|---|
bold |
Bold text | - |
italic |
Italic text | - |
underline |
Underlined text | - |
strikethrough |
Strikethrough text | - |
code |
Inline code | - |
link |
Hyperlink | href, title |
textColor |
Text color | color |
backgroundColor |
Background highlight | color |
import type { MarkSpec } from 'prosemirror-model';
export const highlightMark: MarkSpec = {
// Attributes for this mark
attrs: {
color: { default: 'yellow' },
},
// How to parse from HTML (for copy/paste)
parseDOM: [
{
tag: 'mark',
getAttrs: (dom) => {
const element = dom as HTMLElement;
return {
color: element.style.backgroundColor || 'yellow',
};
},
},
{
style: 'background-color',
getAttrs: (value) => {
return { color: value };
},
},
],
// How to render to HTML
toDOM: (mark) => {
return [
'mark',
{
style: `background-color: ${mark.attrs.color}`,
},
0, // 0 means "render content here"
];
},
};export const subscriptMark: MarkSpec = {
parseDOM: [{ tag: 'sub' }],
toDOM: () => ['sub', 0],
};
export const superscriptMark: MarkSpec = {
parseDOM: [{ tag: 'sup' }],
toDOM: () => ['sup', 0],
};export const kbdMark: MarkSpec = {
parseDOM: [{ tag: 'kbd' }],
toDOM: () => [
'kbd',
{
class: 'keyboard-shortcut',
style: 'font-family: monospace; padding: 2px 4px; background: #f4f4f4; border-radius: 3px;',
},
0,
],
};Modify createSchema.ts:
import { highlightMark, subscriptMark, superscriptMark } from './marks';
export const DEFAULT_MARKS = {
// ... existing marks
highlight: highlightMark,
subscript: subscriptMark,
superscript: superscriptMark,
};In Editor.ts, add convenience methods:
/** Toggle highlight on the current selection. */
toggleHighlight(color: string = 'yellow'): boolean {
return this.pm.toggleMark('highlight', { color });
}
/** Set highlight color on the current selection. */
setHighlight(color: string): void {
this.pm.addMark('highlight', { color });
}
/** Remove highlight from the current selection. */
removeHighlight(): void {
this.pm.removeMark('highlight');
}Marks can exclude other marks. For example, you might want code to exclude other formatting:
export const codeMark: MarkSpec = {
// Exclude all other marks when code is applied
excludes: '_', // '_' means "all marks"
parseDOM: [{ tag: 'code' }],
toDOM: () => ['code', 0],
};Or exclude specific marks:
export const subscriptMark: MarkSpec = {
// Can't have both subscript and superscript
excludes: 'superscript',
parseDOM: [{ tag: 'sub' }],
toDOM: () => ['sub', 0],
};Group marks for easier management:
export const boldMark: MarkSpec = {
group: 'textStyle',
parseDOM: [{ tag: 'strong' }, { tag: 'b' }],
toDOM: () => ['strong', 0],
};
export const italicMark: MarkSpec = {
group: 'textStyle',
parseDOM: [{ tag: 'em' }, { tag: 'i' }],
toDOM: () => ['em', 0],
};In your plugins configuration:
import { keymap } from 'prosemirror-keymap';
import { toggleMark } from 'prosemirror-commands';
const shortcuts = keymap({
'Mod-Shift-h': toggleMark(schema.marks.highlight),
'Mod-,': toggleMark(schema.marks.subscript),
'Mod-.': toggleMark(schema.marks.superscript),
});Modify BubbleMenu.tsx to include your mark:
// Add button for highlight
<button
className={`ob-bubble-menu-btn ${isMarkActive('highlight') ? 'ob-bubble-menu-btn--active' : ''}`}
onClick={() => editor.toggleHighlight()}
title="Highlight"
>
<HighlightIcon />
</button>Add styles in editor.css:
/* Highlight */
.openblock-editor mark {
padding: 0.1em 0.2em;
border-radius: 2px;
}
/* Keyboard shortcut */
.openblock-editor kbd {
font-family: var(--ob-font-mono);
font-size: 0.85em;
padding: 2px 6px;
background: hsl(var(--ob-muted));
border: 1px solid hsl(var(--ob-border));
border-radius: 4px;
box-shadow: 0 1px 0 hsl(var(--ob-border));
}
/* Subscript & Superscript */
.openblock-editor sub,
.openblock-editor sup {
font-size: 0.75em;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.openblock-editor sup {
top: -0.5em;
}
.openblock-editor sub {
bottom: -0.25em;
}In your Block type definition:
interface StyledText {
type: 'text';
text: string;
styles: {
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
code?: boolean;
textColor?: string;
backgroundColor?: string;
highlight?: string; // Add your custom marks
subscript?: boolean;
superscript?: boolean;
};
}const editor = new OpenBlockEditor({
initialContent: [
{
id: '1',
type: 'paragraph',
props: {},
content: [
{ type: 'text', text: 'Normal text ', styles: {} },
{ type: 'text', text: 'highlighted', styles: { highlight: 'yellow' } },
{ type: 'text', text: ' and ', styles: {} },
{ type: 'text', text: 'H', styles: { subscript: true } },
{ type: 'text', text: '2', styles: {} },
{ type: 'text', text: 'O', styles: { subscript: true } },
],
},
],
});- Keep marks simple — Marks should only style text, not change structure
- Use attributes sparingly — Simple marks (bold, italic) don't need attributes
- Handle copy/paste — Ensure
parseDOMhandles pasted content correctly - Test exclusivity — Make sure excluded marks behave correctly
- Add keyboard shortcuts — Users expect shortcuts for common formatting