Add portable callout page blocks
This commit is contained in:
parent
0f048d9eb2
commit
edf46c6733
@ -80,6 +80,68 @@
|
|||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tolaria-callout {
|
||||||
|
--tolaria-callout-accent: #2563eb;
|
||||||
|
--tolaria-callout-bg: color-mix(in srgb, #2563eb 9%, var(--bg-primary));
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 4px minmax(0, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.45rem 0;
|
||||||
|
padding: 11px 13px 11px 0;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--tolaria-callout-accent) 28%, var(--border));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--tolaria-callout-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout[data-kind="success"] {
|
||||||
|
--tolaria-callout-accent: #168a48;
|
||||||
|
--tolaria-callout-bg: color-mix(in srgb, #168a48 10%, var(--bg-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout[data-kind="warning"] {
|
||||||
|
--tolaria-callout-accent: #b7791f;
|
||||||
|
--tolaria-callout-bg: color-mix(in srgb, #b7791f 13%, var(--bg-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout[data-kind="danger"] {
|
||||||
|
--tolaria-callout-accent: #dc2626;
|
||||||
|
--tolaria-callout-bg: color-mix(in srgb, #dc2626 10%, var(--bg-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout__marker {
|
||||||
|
width: 4px;
|
||||||
|
min-height: 100%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--tolaria-callout-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout__body {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout__label {
|
||||||
|
color: var(--tolaria-callout-accent);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout__content {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tolaria-callout__content:empty::before {
|
||||||
|
color: var(--text-muted);
|
||||||
|
content: 'Write a note';
|
||||||
|
}
|
||||||
|
|
||||||
/* Breadcrumb bar: border can still react to the data attribute, but the
|
/* Breadcrumb bar: border can still react to the data attribute, but the
|
||||||
breadcrumb filename/title stays visible at all times. */
|
breadcrumb filename/title stays visible at all times. */
|
||||||
.breadcrumb-bar {
|
.breadcrumb-bar {
|
||||||
|
|||||||
@ -25,6 +25,11 @@ import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown
|
|||||||
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
|
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
|
||||||
import { MARKDOWN_HIGHLIGHT_STYLE } from '../utils/markdownHighlightMarkdown'
|
import { MARKDOWN_HIGHLIGHT_STYLE } from '../utils/markdownHighlightMarkdown'
|
||||||
import { GUANGHU_PORTAL_BLOCK_TYPE } from '../utils/guanghuPortalMarkdown'
|
import { GUANGHU_PORTAL_BLOCK_TYPE } from '../utils/guanghuPortalMarkdown'
|
||||||
|
import {
|
||||||
|
calloutKind,
|
||||||
|
TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
type TolariaCalloutKind,
|
||||||
|
} from '../utils/calloutMarkdown'
|
||||||
import type { VaultEntry } from '../types'
|
import type { VaultEntry } from '../types'
|
||||||
import { createTolariaCodeBlockOptions } from './codeBlockOptions'
|
import { createTolariaCodeBlockOptions } from './codeBlockOptions'
|
||||||
import { NoteTitleIcon } from './NoteTitleIcon'
|
import { NoteTitleIcon } from './NoteTitleIcon'
|
||||||
@ -350,6 +355,45 @@ const GuanghuPortalBlock = createReactBlockSpec(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CALLOUT_LABELS: Record<TolariaCalloutKind, string> = {
|
||||||
|
danger: 'Danger',
|
||||||
|
info: 'Info',
|
||||||
|
success: 'Success',
|
||||||
|
warning: 'Warning',
|
||||||
|
}
|
||||||
|
|
||||||
|
function TolariaCalloutRenderer({
|
||||||
|
block,
|
||||||
|
contentRef,
|
||||||
|
}: {
|
||||||
|
block: { props: { kind?: string } }
|
||||||
|
contentRef: (node: HTMLElement | null) => void
|
||||||
|
}) {
|
||||||
|
const kind = calloutKind(block.props.kind)
|
||||||
|
return (
|
||||||
|
<aside className="tolaria-callout" data-kind={kind}>
|
||||||
|
<span className="tolaria-callout__marker" aria-hidden="true" />
|
||||||
|
<div className="tolaria-callout__body">
|
||||||
|
<span className="tolaria-callout__label">{CALLOUT_LABELS[kind]}</span>
|
||||||
|
<div ref={contentRef} className="tolaria-callout__content" />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TolariaCalloutBlock = createReactBlockSpec(
|
||||||
|
{
|
||||||
|
type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
propSchema: {
|
||||||
|
kind: { default: 'info' },
|
||||||
|
},
|
||||||
|
content: 'inline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
render: TolariaCalloutRenderer,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export function mediaBlockPropsForPreviewRuntime<T extends MediaBlockPreviewProps>(
|
export function mediaBlockPropsForPreviewRuntime<T extends MediaBlockPreviewProps>(
|
||||||
props: T,
|
props: T,
|
||||||
externalMediaPreview: boolean,
|
externalMediaPreview: boolean,
|
||||||
@ -450,6 +494,7 @@ const codeBlock = createCodeBlockSpec(createTolariaCodeBlockOptions())
|
|||||||
const audioBlock = AudioBlockSpec()
|
const audioBlock = AudioBlockSpec()
|
||||||
const mathBlock = MathBlock()
|
const mathBlock = MathBlock()
|
||||||
const guanghuPortalBlock = GuanghuPortalBlock()
|
const guanghuPortalBlock = GuanghuPortalBlock()
|
||||||
|
const tolariaCalloutBlock = TolariaCalloutBlock()
|
||||||
const mermaidBlock = MermaidBlock()
|
const mermaidBlock = MermaidBlock()
|
||||||
const tldrawBlock = TldrawBlock()
|
const tldrawBlock = TldrawBlock()
|
||||||
const videoBlock = VideoBlockSpec()
|
const videoBlock = VideoBlockSpec()
|
||||||
@ -485,6 +530,7 @@ export const schema = BlockNoteSchema.create({
|
|||||||
blockSpecs: {
|
blockSpecs: {
|
||||||
audio: audioBlock,
|
audio: audioBlock,
|
||||||
guanghuPortalBlock,
|
guanghuPortalBlock,
|
||||||
|
[TOLARIA_CALLOUT_BLOCK_TYPE]: tolariaCalloutBlock,
|
||||||
mathBlock,
|
mathBlock,
|
||||||
mermaidBlock,
|
mermaidBlock,
|
||||||
tldrawBlock,
|
tldrawBlock,
|
||||||
|
|||||||
@ -7,7 +7,9 @@ vi.mock('../lib/telemetry', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
addItemsAfterKey,
|
||||||
addItemsToMediaGroup,
|
addItemsToMediaGroup,
|
||||||
|
createCalloutSlashMenuItems,
|
||||||
createMathSlashMenuItem,
|
createMathSlashMenuItem,
|
||||||
filterTolariaFormattingToolbarItems,
|
filterTolariaFormattingToolbarItems,
|
||||||
filterTolariaSlashMenuItems,
|
filterTolariaSlashMenuItems,
|
||||||
@ -18,6 +20,7 @@ import {
|
|||||||
import { trackEvent } from '../lib/telemetry'
|
import { trackEvent } from '../lib/telemetry'
|
||||||
import { MATH_BLOCK_TYPE } from '../utils/mathMarkdown'
|
import { MATH_BLOCK_TYPE } from '../utils/mathMarkdown'
|
||||||
import { mermaidFenceSource } from '../utils/mermaidMarkdown'
|
import { mermaidFenceSource } from '../utils/mermaidMarkdown'
|
||||||
|
import { TOLARIA_CALLOUT_BLOCK_TYPE } from '../utils/calloutMarkdown'
|
||||||
|
|
||||||
describe('tolariaEditorFormatting', () => {
|
describe('tolariaEditorFormatting', () => {
|
||||||
it('keeps the markdown-safe toolbar controls and block type select', () => {
|
it('keeps the markdown-safe toolbar controls and block type select', () => {
|
||||||
@ -73,6 +76,7 @@ describe('tolariaEditorFormatting', () => {
|
|||||||
{ key: 'heading_4', title: 'Heading 4', onItemClick: () => {} },
|
{ key: 'heading_4', title: 'Heading 4', onItemClick: () => {} },
|
||||||
{ key: 'bullet_list', title: 'Bullet List', subtext: 'Default list copy', onItemClick: () => {} },
|
{ key: 'bullet_list', title: 'Bullet List', subtext: 'Default list copy', onItemClick: () => {} },
|
||||||
{ key: 'code_block', title: 'Code Block', subtext: 'Default code copy', onItemClick: () => {} },
|
{ key: 'code_block', title: 'Code Block', subtext: 'Default code copy', onItemClick: () => {} },
|
||||||
|
{ key: 'tolaria_callout_info', title: 'Info callout', onItemClick: () => {} },
|
||||||
{ key: 'heading_5', title: 'Heading 5', onItemClick: () => {} },
|
{ key: 'heading_5', title: 'Heading 5', onItemClick: () => {} },
|
||||||
{ key: 'heading_6', title: 'Heading 6', onItemClick: () => {} },
|
{ key: 'heading_6', title: 'Heading 6', onItemClick: () => {} },
|
||||||
] satisfies TolariaSlashMenuTestItem[])
|
] satisfies TolariaSlashMenuTestItem[])
|
||||||
@ -82,12 +86,14 @@ describe('tolariaEditorFormatting', () => {
|
|||||||
'heading_4',
|
'heading_4',
|
||||||
'bullet_list',
|
'bullet_list',
|
||||||
'code_block',
|
'code_block',
|
||||||
|
'tolaria_callout_info',
|
||||||
])
|
])
|
||||||
expect(items.map((item) => item.subtext)).toEqual([
|
expect(items.map((item) => item.subtext)).toEqual([
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -224,6 +230,58 @@ describe('tolariaEditorFormatting', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('places callout commands after quote blocks', () => {
|
||||||
|
type TolariaSlashMenuTestItem = {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
group: string
|
||||||
|
onItemClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = addItemsAfterKey([
|
||||||
|
{ key: 'paragraph', title: 'Paragraph', group: 'Basic blocks', onItemClick: () => {} },
|
||||||
|
{ key: 'quote', title: 'Quote', group: 'Basic blocks', onItemClick: () => {} },
|
||||||
|
{ key: 'code_block', title: 'Code Block', group: 'Basic blocks', onItemClick: () => {} },
|
||||||
|
] satisfies TolariaSlashMenuTestItem[], 'quote', [
|
||||||
|
{ key: 'tolaria_callout_info', title: 'Info callout', group: 'Basic blocks', onItemClick: () => {} },
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(items.map(item => item.key)).toEqual([
|
||||||
|
'paragraph',
|
||||||
|
'quote',
|
||||||
|
'tolaria_callout_info',
|
||||||
|
'code_block',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps callout slash commands searchable and inserts an editable callout block', async () => {
|
||||||
|
const block = { id: 'active-block' }
|
||||||
|
const editor = {
|
||||||
|
getTextCursorPosition: () => ({ block }),
|
||||||
|
replaceBlocks: () => {},
|
||||||
|
}
|
||||||
|
const replaceBlocks = vi.spyOn(editor, 'replaceBlocks')
|
||||||
|
const items = createCalloutSlashMenuItems(editor as never)
|
||||||
|
const warningItem = items.find(item => item.key === 'tolaria_callout_warning')
|
||||||
|
|
||||||
|
expect(warningItem).toEqual(expect.objectContaining({
|
||||||
|
title: 'Warning callout',
|
||||||
|
aliases: expect.arrayContaining(['warning', '警告']),
|
||||||
|
}))
|
||||||
|
|
||||||
|
warningItem?.onItemClick()
|
||||||
|
|
||||||
|
expect(replaceBlocks).toHaveBeenCalledWith([block], [{
|
||||||
|
type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
props: { kind: 'warning' },
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: 'Warning callout',
|
||||||
|
styles: {},
|
||||||
|
}],
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
it('creates a math slash command with a default display equation', () => {
|
it('creates a math slash command with a default display equation', () => {
|
||||||
const block = { id: 'active-block' }
|
const block = { id: 'active-block' }
|
||||||
const editor = {
|
const editor = {
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import {
|
|||||||
import { createElement, type ReactElement } from 'react'
|
import { createElement, type ReactElement } from 'react'
|
||||||
import {
|
import {
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
|
CheckCircle,
|
||||||
File,
|
File,
|
||||||
FlowArrow,
|
FlowArrow,
|
||||||
ImageSquare,
|
ImageSquare,
|
||||||
|
Info,
|
||||||
ListBullets,
|
ListBullets,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
ListNumbers,
|
ListNumbers,
|
||||||
@ -26,6 +28,8 @@ import {
|
|||||||
TextHFour,
|
TextHFour,
|
||||||
TextHFive,
|
TextHFive,
|
||||||
TextHSix,
|
TextHSix,
|
||||||
|
Warning,
|
||||||
|
XCircle,
|
||||||
Video,
|
Video,
|
||||||
type Icon as PhosphorIcon,
|
type Icon as PhosphorIcon,
|
||||||
} from '@phosphor-icons/react'
|
} from '@phosphor-icons/react'
|
||||||
@ -38,6 +42,10 @@ import {
|
|||||||
import { MATH_BLOCK_TYPE } from '../utils/mathMarkdown'
|
import { MATH_BLOCK_TYPE } from '../utils/mathMarkdown'
|
||||||
import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown'
|
import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown'
|
||||||
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
|
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
|
||||||
|
import {
|
||||||
|
TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
type TolariaCalloutKind,
|
||||||
|
} from '../utils/calloutMarkdown'
|
||||||
|
|
||||||
type TolariaSlashMenuItem = DefaultReactSuggestionItem & { key: string }
|
type TolariaSlashMenuItem = DefaultReactSuggestionItem & { key: string }
|
||||||
type TolariaBlockTypeSelectItem = RichEditorBlockTypeDefinition & {
|
type TolariaBlockTypeSelectItem = RichEditorBlockTypeDefinition & {
|
||||||
@ -55,6 +63,12 @@ type BlockSlashMenuItemConfig = {
|
|||||||
title: string
|
title: string
|
||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
type CalloutSlashMenuItemConfig = {
|
||||||
|
aliases: string[]
|
||||||
|
key: string
|
||||||
|
kind: TolariaCalloutKind
|
||||||
|
title: string
|
||||||
|
}
|
||||||
type TolariaSlashMenuLabels = {
|
type TolariaSlashMenuLabels = {
|
||||||
mathTitle: string
|
mathTitle: string
|
||||||
}
|
}
|
||||||
@ -115,6 +129,10 @@ const TOLARIA_SLASH_MENU_ICONS: Partial<Record<string, PhosphorIcon>> = {
|
|||||||
numbered_list: ListNumbers,
|
numbered_list: ListNumbers,
|
||||||
paragraph: Paragraph,
|
paragraph: Paragraph,
|
||||||
quote: Quotes,
|
quote: Quotes,
|
||||||
|
tolaria_callout_danger: XCircle,
|
||||||
|
tolaria_callout_info: Info,
|
||||||
|
tolaria_callout_success: CheckCircle,
|
||||||
|
tolaria_callout_warning: Warning,
|
||||||
table: Table,
|
table: Table,
|
||||||
toggle_heading: TextHOne,
|
toggle_heading: TextHOne,
|
||||||
toggle_heading_2: TextHTwo,
|
toggle_heading_2: TextHTwo,
|
||||||
@ -164,6 +182,64 @@ function createMermaidSlashMenuItem(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCalloutSlashMenuItem(
|
||||||
|
editor: Parameters<typeof getDefaultReactSlashMenuItems>[0],
|
||||||
|
config: CalloutSlashMenuItemConfig,
|
||||||
|
): TolariaSlashMenuItem {
|
||||||
|
const blockEditor = editor as unknown as SlashInsertEditor
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: config.key,
|
||||||
|
title: config.title,
|
||||||
|
aliases: config.aliases,
|
||||||
|
group: 'Basic blocks',
|
||||||
|
onItemClick: () => {
|
||||||
|
const block = blockEditor.getTextCursorPosition().block
|
||||||
|
blockEditor.replaceBlocks([block], [{
|
||||||
|
type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
props: { kind: config.kind },
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: config.title,
|
||||||
|
styles: {},
|
||||||
|
}],
|
||||||
|
}])
|
||||||
|
trackEvent('editor_callout_slash_command_used', { kind: config.kind })
|
||||||
|
},
|
||||||
|
} as TolariaSlashMenuItem
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCalloutSlashMenuItems(
|
||||||
|
editor: Parameters<typeof getDefaultReactSlashMenuItems>[0],
|
||||||
|
): TolariaSlashMenuItem[] {
|
||||||
|
return [
|
||||||
|
createCalloutSlashMenuItem(editor, {
|
||||||
|
key: 'tolaria_callout_info',
|
||||||
|
title: 'Info callout',
|
||||||
|
aliases: ['callout', 'note', 'info', 'tip', '提示', '高亮'],
|
||||||
|
kind: 'info',
|
||||||
|
}),
|
||||||
|
createCalloutSlashMenuItem(editor, {
|
||||||
|
key: 'tolaria_callout_success',
|
||||||
|
title: 'Success callout',
|
||||||
|
aliases: ['callout', 'success', 'done', 'ok', '完成', '成功'],
|
||||||
|
kind: 'success',
|
||||||
|
}),
|
||||||
|
createCalloutSlashMenuItem(editor, {
|
||||||
|
key: 'tolaria_callout_warning',
|
||||||
|
title: 'Warning callout',
|
||||||
|
aliases: ['callout', 'warning', 'alert', 'todo', '警告', '注意'],
|
||||||
|
kind: 'warning',
|
||||||
|
}),
|
||||||
|
createCalloutSlashMenuItem(editor, {
|
||||||
|
key: 'tolaria_callout_danger',
|
||||||
|
title: 'Danger callout',
|
||||||
|
aliases: ['callout', 'danger', 'risk', 'error', '风险', '危险'],
|
||||||
|
kind: 'danger',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export function createMathSlashMenuItem(
|
export function createMathSlashMenuItem(
|
||||||
editor: Parameters<typeof getDefaultReactSlashMenuItems>[0],
|
editor: Parameters<typeof getDefaultReactSlashMenuItems>[0],
|
||||||
labels: TolariaSlashMenuLabels = { mathTitle: 'Math' },
|
labels: TolariaSlashMenuLabels = { mathTitle: 'Math' },
|
||||||
@ -218,6 +294,23 @@ export function addItemsToMediaGroup(
|
|||||||
return nextItems
|
return nextItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addItemsAfterKey(
|
||||||
|
items: TolariaSlashMenuItem[],
|
||||||
|
insertAfterKey: string,
|
||||||
|
insertedItems: TolariaSlashMenuItem[],
|
||||||
|
): TolariaSlashMenuItem[] {
|
||||||
|
const nextItems = [...items]
|
||||||
|
const insertIndex = nextItems.findIndex((item) => item.key === insertAfterKey)
|
||||||
|
|
||||||
|
if (insertIndex === -1) {
|
||||||
|
nextItems.unshift(...insertedItems)
|
||||||
|
return nextItems
|
||||||
|
}
|
||||||
|
|
||||||
|
nextItems.splice(insertIndex + 1, 0, ...insertedItems)
|
||||||
|
return nextItems
|
||||||
|
}
|
||||||
|
|
||||||
function createTolariaSlashMenuIcon(Icon: PhosphorIcon) {
|
function createTolariaSlashMenuIcon(Icon: PhosphorIcon) {
|
||||||
return createElement(
|
return createElement(
|
||||||
'span',
|
'span',
|
||||||
@ -273,8 +366,14 @@ export function getTolariaSlashMenuItems(
|
|||||||
query: string,
|
query: string,
|
||||||
labels?: TolariaSlashMenuLabels,
|
labels?: TolariaSlashMenuLabels,
|
||||||
) {
|
) {
|
||||||
|
const defaultItems = getDefaultReactSlashMenuItems(editor) as TolariaSlashMenuItem[]
|
||||||
|
const itemsWithCallouts = addItemsAfterKey(
|
||||||
|
defaultItems,
|
||||||
|
'quote',
|
||||||
|
createCalloutSlashMenuItems(editor),
|
||||||
|
)
|
||||||
const items = addItemsToMediaGroup(
|
const items = addItemsToMediaGroup(
|
||||||
getDefaultReactSlashMenuItems(editor) as TolariaSlashMenuItem[],
|
itemsWithCallouts,
|
||||||
[
|
[
|
||||||
createMermaidSlashMenuItem(editor),
|
createMermaidSlashMenuItem(editor),
|
||||||
createMathSlashMenuItem(editor, labels),
|
createMathSlashMenuItem(editor, labels),
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
serializeBlockNoteMarkdown,
|
serializeBlockNoteMarkdown,
|
||||||
type DirectMarkdownCapableSerializer,
|
type DirectMarkdownCapableSerializer,
|
||||||
} from './blockNoteDirectMarkdown'
|
} from './blockNoteDirectMarkdown'
|
||||||
|
import { TOLARIA_CALLOUT_BLOCK_TYPE } from './calloutMarkdown'
|
||||||
import { serializeRichEditorBodyToMarkdown } from './richEditorMarkdown'
|
import { serializeRichEditorBodyToMarkdown } from './richEditorMarkdown'
|
||||||
|
|
||||||
function makeEditor(document: unknown[]): DirectMarkdownCapableSerializer & { document: unknown[] } {
|
function makeEditor(document: unknown[]): DirectMarkdownCapableSerializer & { document: unknown[] } {
|
||||||
@ -117,6 +118,17 @@ describe('BlockNote direct Markdown serialization', () => {
|
|||||||
].join('\n'))
|
].join('\n'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('serializes Tolaria callout blocks as portable Markdown callouts', () => {
|
||||||
|
expect(blocksToMarkdownDirect([
|
||||||
|
{
|
||||||
|
type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
props: { kind: 'success' },
|
||||||
|
content: [{ type: 'text', text: 'Package is ready', styles: {} }],
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]).markdown).toBe('> [!success] Package is ready')
|
||||||
|
})
|
||||||
|
|
||||||
it('caches unchanged block objects across rich-editor body serialization', () => {
|
it('caches unchanged block objects across rich-editor body serialization', () => {
|
||||||
const block = {
|
const block = {
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { calloutKind, TOLARIA_CALLOUT_BLOCK_TYPE } from './calloutMarkdown'
|
||||||
|
|
||||||
interface TextStyles {
|
interface TextStyles {
|
||||||
[style: string]: string | boolean | undefined
|
[style: string]: string | boolean | undefined
|
||||||
}
|
}
|
||||||
@ -228,6 +230,15 @@ function quoteMarkdown(block: BlockLike): string {
|
|||||||
return text.split('\n').map(line => `> ${line}`).join('\n')
|
return text.split('\n').map(line => `> ${line}`).join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calloutMarkdown(block: BlockLike): string {
|
||||||
|
const kind = calloutKind(block.props?.kind)
|
||||||
|
const text = serializeInlineContent(contentArray(block.content)) || 'Write a note'
|
||||||
|
return `[!${kind}] ${text}`
|
||||||
|
.split('\n')
|
||||||
|
.map(line => `> ${line}`)
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
function tableCellMarkdown(cell: TableCellValue): string {
|
function tableCellMarkdown(cell: TableCellValue): string {
|
||||||
const text = typeof cell === 'string'
|
const text = typeof cell === 'string'
|
||||||
? cell
|
? cell
|
||||||
@ -273,6 +284,7 @@ const BLOCK_MARKDOWN_HANDLERS: Record<string, BlockMarkdownHandler> = {
|
|||||||
heading: headingMarkdown,
|
heading: headingMarkdown,
|
||||||
quote: quoteMarkdown,
|
quote: quoteMarkdown,
|
||||||
table: tableMarkdown,
|
table: tableMarkdown,
|
||||||
|
[TOLARIA_CALLOUT_BLOCK_TYPE]: calloutMarkdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockMarkdownWithoutChildren(block: BlockLike, context: SerializeContext): string | null {
|
function blockMarkdownWithoutChildren(block: BlockLike, context: SerializeContext): string | null {
|
||||||
|
|||||||
70
src/utils/calloutMarkdown.ts
Normal file
70
src/utils/calloutMarkdown.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import type { BlockLike } from './durableMarkdownBlocks'
|
||||||
|
|
||||||
|
export const TOLARIA_CALLOUT_BLOCK_TYPE = 'tolariaCallout'
|
||||||
|
|
||||||
|
export const TOLARIA_CALLOUT_KINDS = ['info', 'success', 'warning', 'danger'] as const
|
||||||
|
|
||||||
|
export type TolariaCalloutKind = typeof TOLARIA_CALLOUT_KINDS[number]
|
||||||
|
|
||||||
|
const CALLOUT_MARKER_RE = /^\[!(info|success|warning|danger)\]\s*(.*)$/iu
|
||||||
|
|
||||||
|
function isCalloutKind(value: unknown): value is TolariaCalloutKind {
|
||||||
|
return typeof value === 'string' && TOLARIA_CALLOUT_KINDS.includes(value as TolariaCalloutKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTextOnlyContent(content: BlockLike['content']): string | null {
|
||||||
|
if (!Array.isArray(content) || content.length === 0) return null
|
||||||
|
|
||||||
|
let text = ''
|
||||||
|
for (const item of content) {
|
||||||
|
if (item.type !== 'text' || typeof item.text !== 'string') return null
|
||||||
|
text += item.text
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function calloutMatchFromQuote(block: BlockLike) {
|
||||||
|
if (block.type !== 'quote') return null
|
||||||
|
|
||||||
|
const text = readTextOnlyContent(block.content)
|
||||||
|
if (text === null) return null
|
||||||
|
|
||||||
|
const match = CALLOUT_MARKER_RE.exec(text)
|
||||||
|
if (!match) return null
|
||||||
|
|
||||||
|
const kind = match[1]?.toLowerCase()
|
||||||
|
if (!isCalloutKind(kind)) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
text: match[2] ?? '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function injectTolariaCalloutBlocks(blocks: unknown[]): unknown[] {
|
||||||
|
return (blocks as BlockLike[]).map((block) => {
|
||||||
|
const match = calloutMatchFromQuote(block)
|
||||||
|
const children = Array.isArray(block.children)
|
||||||
|
? injectTolariaCalloutBlocks(block.children) as BlockLike[]
|
||||||
|
: block.children
|
||||||
|
|
||||||
|
if (!match) return { ...block, children }
|
||||||
|
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
props: {
|
||||||
|
...(block.props ?? {}),
|
||||||
|
kind: match.kind,
|
||||||
|
},
|
||||||
|
content: match.text
|
||||||
|
? [{ type: 'text', text: match.text, styles: {} }]
|
||||||
|
: [],
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calloutKind(value: unknown): TolariaCalloutKind {
|
||||||
|
return isCalloutKind(value) ? value : 'info'
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import {
|
|||||||
preProcessDurableEditorMarkdown,
|
preProcessDurableEditorMarkdown,
|
||||||
serializeDurableEditorBlocks,
|
serializeDurableEditorBlocks,
|
||||||
} from './editorDurableMarkdown'
|
} from './editorDurableMarkdown'
|
||||||
|
import { TOLARIA_CALLOUT_BLOCK_TYPE } from './calloutMarkdown'
|
||||||
import { MERMAID_BLOCK_TYPE } from './mermaidMarkdown'
|
import { MERMAID_BLOCK_TYPE } from './mermaidMarkdown'
|
||||||
import { TLDRAW_BLOCK_TYPE } from './tldrawMarkdown'
|
import { TLDRAW_BLOCK_TYPE } from './tldrawMarkdown'
|
||||||
|
|
||||||
@ -70,4 +71,20 @@ describe('editor durable markdown blocks', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('restores Markdown callouts as Tolaria page blocks', () => {
|
||||||
|
const blocks = injectDurableEditorMarkdownBlocks([
|
||||||
|
{
|
||||||
|
type: 'quote',
|
||||||
|
content: [{ type: 'text', text: '[!warning] Check signing before sharing.', styles: {} }],
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]) as Array<{ type: string; props?: Record<string, string>; content?: Array<{ text?: string }> }>
|
||||||
|
|
||||||
|
expect(blocks[0]).toMatchObject({
|
||||||
|
type: TOLARIA_CALLOUT_BLOCK_TYPE,
|
||||||
|
props: { kind: 'warning' },
|
||||||
|
content: [{ text: 'Check signing before sharing.' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { serializeMathAwareBlocks } from './mathMarkdown'
|
|||||||
import { mermaidMarkdownCodec } from './mermaidMarkdown'
|
import { mermaidMarkdownCodec } from './mermaidMarkdown'
|
||||||
import { tldrawMarkdownCodec } from './tldrawMarkdown'
|
import { tldrawMarkdownCodec } from './tldrawMarkdown'
|
||||||
import { guanghuPortalMarkdownCodec } from './guanghuPortalMarkdown'
|
import { guanghuPortalMarkdownCodec } from './guanghuPortalMarkdown'
|
||||||
|
import { injectTolariaCalloutBlocks } from './calloutMarkdown'
|
||||||
|
|
||||||
const EDITOR_DURABLE_MARKDOWN_CODECS = [
|
const EDITOR_DURABLE_MARKDOWN_CODECS = [
|
||||||
guanghuPortalMarkdownCodec,
|
guanghuPortalMarkdownCodec,
|
||||||
@ -36,7 +37,7 @@ export function injectDurableEditorMarkdownBlocks(blocks: unknown[]): unknown[]
|
|||||||
blocks,
|
blocks,
|
||||||
codecs: EDITOR_DURABLE_MARKDOWN_CODECS,
|
codecs: EDITOR_DURABLE_MARKDOWN_CODECS,
|
||||||
})
|
})
|
||||||
return injectFileAttachmentBlocks(withDurableBlocks)
|
return injectTolariaCalloutBlocks(injectFileAttachmentBlocks(withDurableBlocks))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeDurableEditorBlocks(
|
export function serializeDurableEditorBlocks(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user