mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat(editor): support footnote adapter (#9844)
[BS-2373](https://linear.app/affine-design/issue/BS-2373/适配-footnote-adapter)
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.7",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"zod": "^3.23.8"
|
||||
|
||||
@@ -3,15 +3,19 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
import {
|
||||
DocNoteBlockHtmlAdapterExtension,
|
||||
EdgelessNoteBlockHtmlAdapterExtension,
|
||||
} from './html.js';
|
||||
} from './html';
|
||||
import {
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
EdgelessNoteBlockMarkdownAdapterExtension,
|
||||
} from './markdown.js';
|
||||
} from './markdown';
|
||||
import {
|
||||
DocNoteBlockPlainTextAdapterExtension,
|
||||
EdgelessNoteBlockPlainTextAdapterExtension,
|
||||
} from './plain-text.js';
|
||||
} from './plain-text';
|
||||
|
||||
export * from './html';
|
||||
export * from './markdown';
|
||||
export * from './plain-text';
|
||||
|
||||
export const DocNoteBlockAdapterExtensions: ExtensionType[] = [
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
|
||||
@@ -2,7 +2,35 @@ import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { FootnoteDefinition, Root } from 'mdast';
|
||||
|
||||
const isRootNode = (node: MarkdownAST): node is Root => node.type === 'root';
|
||||
const isFootnoteDefinitionNode = (
|
||||
node: MarkdownAST
|
||||
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
|
||||
|
||||
const createFootnoteDefinition = (
|
||||
identifier: string,
|
||||
content: string
|
||||
): MarkdownAST => ({
|
||||
type: 'footnoteDefinition',
|
||||
label: identifier,
|
||||
identifier,
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: content,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a markdown adapter matcher for note block.
|
||||
@@ -15,9 +43,36 @@ const createNoteBlockMarkdownAdapterMatcher = (
|
||||
displayModeToSkip: NoteDisplayMode
|
||||
): BlockMarkdownAdapterMatcher => ({
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
toMatch: o => isRootNode(o.node),
|
||||
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!isRootNode(o.node)) {
|
||||
return;
|
||||
}
|
||||
const noteAst = o.node;
|
||||
// Find all the footnoteDefinition in the noteAst
|
||||
const { configs } = context;
|
||||
noteAst.children.forEach(child => {
|
||||
if (isFootnoteDefinitionNode(child)) {
|
||||
const identifier = child.identifier;
|
||||
const definitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${identifier}`;
|
||||
// Get the text content of the footnoteDefinition
|
||||
const textContent = child.children
|
||||
.find(child => child.type === 'paragraph')
|
||||
?.children.find(child => child.type === 'text')?.value;
|
||||
if (textContent) {
|
||||
configs.set(definitionKey, textContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the footnoteDefinition node from the noteAst
|
||||
noteAst.children = noteAst.children.filter(
|
||||
child => !isFootnoteDefinitionNode(child)
|
||||
);
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const node = o.node;
|
||||
@@ -25,6 +80,33 @@ const createNoteBlockMarkdownAdapterMatcher = (
|
||||
context.walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
leave: (_, context) => {
|
||||
const { walkerContext, configs } = context;
|
||||
// Get all the footnote definitions config starts with FOOTNOTE_DEFINITION_PREFIX
|
||||
// And create footnoteDefinition AST node for each of them
|
||||
Array.from(configs.keys())
|
||||
.filter(key => key.startsWith(FOOTNOTE_DEFINITION_PREFIX))
|
||||
.forEach(key => {
|
||||
const hasFootnoteDefinition = !!walkerContext.getGlobalContext(key);
|
||||
// If the footnoteDefinition node is already in md ast, skip it
|
||||
// In markdown file, we only need to create footnoteDefinition once
|
||||
if (hasFootnoteDefinition) {
|
||||
return;
|
||||
}
|
||||
const definition = configs.get(key);
|
||||
const identifier = key.slice(FOOTNOTE_DEFINITION_PREFIX.length);
|
||||
if (definition && identifier) {
|
||||
walkerContext
|
||||
.openNode(
|
||||
createFootnoteDefinition(identifier, definition),
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
// Set the footnoteDefinition node as global context to avoid duplicate creation
|
||||
walkerContext.setGlobalContext(key, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './inline-delta';
|
||||
export * from './markdown-inline';
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
InlineDeltaToMarkdownAdapterExtension,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
@@ -146,6 +147,46 @@ export const latexDeltaToMarkdownAdapterMatcher =
|
||||
},
|
||||
});
|
||||
|
||||
export const footnoteReferenceDeltaToMarkdownAdapterMatcher =
|
||||
InlineDeltaToMarkdownAdapterExtension({
|
||||
name: 'footnote-reference',
|
||||
match: delta => !!delta.attributes?.footnote,
|
||||
toAST: (delta, context) => {
|
||||
const mdast: PhrasingContent = {
|
||||
type: 'text',
|
||||
value: delta.insert,
|
||||
};
|
||||
const footnote = delta.attributes?.footnote;
|
||||
if (!footnote) {
|
||||
return mdast;
|
||||
}
|
||||
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnote.label}`;
|
||||
const { configs } = context;
|
||||
// FootnoteReference should be paired with FootnoteDefinition
|
||||
// If the footnoteDefinition is not in the configs, set it to configs
|
||||
// We should add the footnoteDefinition markdown ast nodes to tree after all the footnoteReference markdown ast nodes are added
|
||||
if (!configs.has(footnoteDefinitionKey)) {
|
||||
// clone the footnote reference
|
||||
const clonedFootnoteReference = { ...footnote.reference };
|
||||
// If the footnote reference contains url, encode it
|
||||
if (clonedFootnoteReference.url) {
|
||||
clonedFootnoteReference.url = encodeURIComponent(
|
||||
clonedFootnoteReference.url
|
||||
);
|
||||
}
|
||||
configs.set(
|
||||
footnoteDefinitionKey,
|
||||
JSON.stringify(clonedFootnoteReference)
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: 'footnoteReference',
|
||||
label: footnote.label,
|
||||
identifier: footnote.label,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const InlineDeltaToMarkdownAdapterExtensions = [
|
||||
referenceDeltaToMarkdownAdapterMatcher,
|
||||
linkDeltaToMarkdownAdapterMatcher,
|
||||
@@ -154,4 +195,5 @@ export const InlineDeltaToMarkdownAdapterExtensions = [
|
||||
italicDeltaToMarkdownAdapterMatcher,
|
||||
strikeDeltaToMarkdownAdapterMatcher,
|
||||
latexDeltaToMarkdownAdapterMatcher,
|
||||
footnoteReferenceDeltaToMarkdownAdapterMatcher,
|
||||
];
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { MarkdownASTToDeltaExtension } from '@blocksuite/affine-shared/adapters';
|
||||
import { FootNoteReferenceParamsSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
MarkdownASTToDeltaExtension,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const markdownTextToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'text',
|
||||
@@ -138,6 +142,43 @@ export const markdownInlineMathToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
},
|
||||
});
|
||||
|
||||
export const markdownFootnoteReferenceToDeltaMatcher =
|
||||
MarkdownASTToDeltaExtension({
|
||||
name: 'footnote-reference',
|
||||
match: ast => ast.type === 'footnoteReference',
|
||||
toDelta: (ast, context) => {
|
||||
if (ast.type !== 'footnoteReference') {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const { configs } = context;
|
||||
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${ast.identifier}`;
|
||||
const footnoteDefinition = configs.get(footnoteDefinitionKey);
|
||||
if (!footnoteDefinition) {
|
||||
return [];
|
||||
}
|
||||
const footnoteDefinitionJson = JSON.parse(footnoteDefinition);
|
||||
// If the footnote definition contains url, decode it
|
||||
if (footnoteDefinitionJson.url) {
|
||||
footnoteDefinitionJson.url = decodeURIComponent(
|
||||
footnoteDefinitionJson.url
|
||||
);
|
||||
}
|
||||
const footnoteReference = FootNoteReferenceParamsSchema.parse(
|
||||
footnoteDefinitionJson
|
||||
);
|
||||
const footnote = {
|
||||
label: ast.identifier,
|
||||
reference: footnoteReference,
|
||||
};
|
||||
return [{ insert: ' ', attributes: { footnote } }];
|
||||
} catch (error) {
|
||||
console.error('Error parsing footnote reference', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const MarkdownInlineToDeltaAdapterExtensions = [
|
||||
markdownTextToDeltaMatcher,
|
||||
markdownInlineCodeToDeltaMatcher,
|
||||
@@ -147,4 +188,5 @@ export const MarkdownInlineToDeltaAdapterExtensions = [
|
||||
markdownLinkToDeltaMatcher,
|
||||
markdownInlineMathToDeltaMatcher,
|
||||
markdownListToDeltaMatcher,
|
||||
markdownFootnoteReferenceToDeltaMatcher,
|
||||
];
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export * from './adapters/extensions';
|
||||
export * from './adapters/html/html-inline';
|
||||
export * from './adapters/html/inline-delta';
|
||||
export * from './adapters/markdown/inline-delta';
|
||||
export * from './adapters/markdown/markdown-inline';
|
||||
export * from './adapters/markdown';
|
||||
export * from './adapters/notion-html/html-inline';
|
||||
export * from './adapters/plain-text/inline-delta';
|
||||
export * from './presets/affine-inline-specs';
|
||||
|
||||
@@ -29,10 +29,12 @@
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.mergewith": "^4.6.2",
|
||||
"mdast-util-gfm-autolink-literal": "^2.0.1",
|
||||
"mdast-util-gfm-footnote": "^2.0.0",
|
||||
"mdast-util-gfm-strikethrough": "^2.0.0",
|
||||
"mdast-util-gfm-table": "^2.0.0",
|
||||
"mdast-util-gfm-task-list-item": "^2.0.0",
|
||||
"micromark-extension-gfm-autolink-literal": "^2.1.0",
|
||||
"micromark-extension-gfm-footnote": "^2.1.0",
|
||||
"micromark-extension-gfm-strikethrough": "^2.1.0",
|
||||
"micromark-extension-gfm-table": "^2.1.0",
|
||||
"micromark-extension-gfm-task-list-item": "^2.1.0",
|
||||
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
BlockMarkdownAdapterMatcherIdentifier,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
InlineDeltaToMarkdownAdapterExtension,
|
||||
type InlineDeltaToMarkdownAdapterMatcher,
|
||||
InlineDeltaToMarkdownAdapterMatcherIdentifier,
|
||||
|
||||
@@ -4,9 +4,12 @@ MIT License
|
||||
Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
|
||||
|
||||
mdast-util-gfm-autolink-literal is from markdown only.
|
||||
mdast-util-gfm-footnote is not included.
|
||||
*/
|
||||
import { gfmAutolinkLiteralFromMarkdown } from 'mdast-util-gfm-autolink-literal';
|
||||
import {
|
||||
gfmFootnoteFromMarkdown,
|
||||
gfmFootnoteToMarkdown,
|
||||
} from 'mdast-util-gfm-footnote';
|
||||
import {
|
||||
gfmStrikethroughFromMarkdown,
|
||||
gfmStrikethroughToMarkdown,
|
||||
@@ -17,6 +20,7 @@ import {
|
||||
gfmTaskListItemToMarkdown,
|
||||
} from 'mdast-util-gfm-task-list-item';
|
||||
import { gfmAutolinkLiteral } from 'micromark-extension-gfm-autolink-literal';
|
||||
import { gfmFootnote } from 'micromark-extension-gfm-footnote';
|
||||
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough';
|
||||
import { gfmTable } from 'micromark-extension-gfm-table';
|
||||
import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item';
|
||||
@@ -29,6 +33,7 @@ export function gfm() {
|
||||
gfmStrikethrough(),
|
||||
gfmTable(),
|
||||
gfmTaskListItem(),
|
||||
gfmFootnote(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -38,6 +43,7 @@ function gfmFromMarkdown() {
|
||||
gfmTableFromMarkdown(),
|
||||
gfmTaskListItemFromMarkdown(),
|
||||
gfmAutolinkLiteralFromMarkdown(),
|
||||
gfmFootnoteFromMarkdown(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -47,6 +53,7 @@ function gfmToMarkdown() {
|
||||
gfmStrikethroughToMarkdown(),
|
||||
gfmTableToMarkdown(),
|
||||
gfmTaskListItemToMarkdown(),
|
||||
gfmFootnoteToMarkdown(),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,3 +15,5 @@ export const isMarkdownAST = (node: unknown): node is MarkdownAST =>
|
||||
!Array.isArray(node) &&
|
||||
'type' in (node as object) &&
|
||||
(node as MarkdownAST).type !== undefined;
|
||||
|
||||
export const FOOTNOTE_DEFINITION_PREFIX = 'footnoteDefinition:';
|
||||
|
||||
@@ -2350,6 +2350,109 @@ World!
|
||||
});
|
||||
expect(target.file).toBe(docMd);
|
||||
});
|
||||
|
||||
test('footnote', async () => {
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
id: 'block:vu6SK6WJpW',
|
||||
flavour: 'affine:page',
|
||||
props: {
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [],
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'block:Tk4gSPocAt',
|
||||
flavour: 'affine:surface',
|
||||
props: {
|
||||
elements: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
id: 'block:WfnS5ZDCJT',
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DefaultTheme.noteBackgrounColor,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'block:zxDyvrg1Mh',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'aaa',
|
||||
},
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
footnote: {
|
||||
label: '1',
|
||||
reference: {
|
||||
type: 'url',
|
||||
url: 'https://www.example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
footnote: {
|
||||
label: '2',
|
||||
reference: {
|
||||
type: 'doc',
|
||||
docId: 'deadbeef',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
footnote: {
|
||||
label: '3',
|
||||
reference: {
|
||||
type: 'attachment',
|
||||
blobId: 'abcdefg',
|
||||
fileName: 'test.txt',
|
||||
fileType: 'text/plain',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const markdown =
|
||||
'aaa[^1][^2][^3]\n\n[^1]: {"type":"url","url":"https%3A%2F%2Fwww.example.com"}\n\n[^2]: {"type":"doc","docId":"deadbeef"}\n\n[^3]: {"type":"attachment","blobId":"abcdefg","fileName":"test.txt","fileType":"text/plain"}\n';
|
||||
|
||||
const mdAdapter = new MarkdownAdapter(createJob(), provider);
|
||||
const target = await mdAdapter.fromBlockSnapshot({
|
||||
snapshot: blockSnapshot,
|
||||
});
|
||||
expect(target.file).toBe(markdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown to snapshot', () => {
|
||||
@@ -3858,4 +3961,85 @@ hhh
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('without footnote middleware', async () => {
|
||||
const markdown =
|
||||
'aaa[^1][^2][^3]\n\n[^1]: {"type":"url","url":"https%3A%2F%2Fwww.example.com"}\n\n[^2]: {"type":"doc","docId":"deadbeef"}\n\n[^3]: {"type":"attachment","blobId":"abcdefg","fileName":"test.txt","fileType":"text/plain"}\n';
|
||||
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[0]',
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DefaultTheme.noteBackgrounColor,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[1]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert: 'aaa',
|
||||
},
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
footnote: {
|
||||
label: '1',
|
||||
reference: {
|
||||
type: 'url',
|
||||
url: 'https://www.example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
footnote: {
|
||||
label: '2',
|
||||
reference: {
|
||||
type: 'doc',
|
||||
docId: 'deadbeef',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
footnote: {
|
||||
label: '3',
|
||||
reference: {
|
||||
type: 'attachment',
|
||||
blobId: 'abcdefg',
|
||||
fileName: 'test.txt',
|
||||
fileType: 'text/plain',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mdAdapter = new MarkdownAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
|
||||
file: markdown,
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,11 +13,14 @@ import {
|
||||
import { ImageBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-image';
|
||||
import { LatexBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-latex';
|
||||
import { ListBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-list';
|
||||
import { DocNoteBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-note';
|
||||
import { ParagraphBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-paragraph';
|
||||
|
||||
import { RootBlockMarkdownAdapterExtension } from '../../../root-block/adapters/markdown.js';
|
||||
|
||||
export const defaultBlockMarkdownAdapterMatchers = [
|
||||
RootBlockMarkdownAdapterExtension,
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
EmbedFigmaMarkdownAdapterExtension,
|
||||
EmbedGithubMarkdownAdapterExtension,
|
||||
EmbedLinkedDocMarkdownAdapterExtension,
|
||||
@@ -32,5 +35,4 @@ export const defaultBlockMarkdownAdapterMatchers = [
|
||||
DividerBlockMarkdownAdapterExtension,
|
||||
ImageBlockMarkdownAdapterExtension,
|
||||
LatexBlockMarkdownAdapterExtension,
|
||||
RootBlockMarkdownAdapterExtension,
|
||||
];
|
||||
|
||||
@@ -26,6 +26,7 @@ const escapedSnapshotAttributes = new Set([
|
||||
'"background"',
|
||||
'"LinkedPage"',
|
||||
'"elementIds"',
|
||||
'"attachment"',
|
||||
]);
|
||||
|
||||
function nanoidReplacementString(snapshotString: string) {
|
||||
|
||||
Reference in New Issue
Block a user