fix(editor): add footnote url markdown preprocessor to avoid link node parsing (#11888)

Closes: [BS-3282](https://linear.app/affine-design/issue/BS-3282/预处理-footnote-definition-中的-url-避免-markdown-link-node-parsing)
This commit is contained in:
donteatfriedrice
2025-04-22 14:19:09 +00:00
parent c17c335f9b
commit bbdea71686
11 changed files with 200 additions and 16 deletions
@@ -1,13 +1,13 @@
import type { ExtensionType } from '@blocksuite/store';
import { BookmarkBlockHtmlAdapterExtension } from './html.js';
import { BookmarkBlockMarkdownAdapterExtension } from './markdown.js';
import { BookmarkBlockMarkdownAdapterExtensions } from './markdown/index.js';
import { BookmarkBlockNotionHtmlAdapterExtension } from './notion-html.js';
import { BookmarkBlockPlainTextAdapterExtension } from './plain-text.js';
export const BookmarkBlockAdapterExtensions: ExtensionType[] = [
BookmarkBlockHtmlAdapterExtension,
BookmarkBlockMarkdownAdapterExtension,
BookmarkBlockMarkdownAdapterExtensions,
BookmarkBlockNotionHtmlAdapterExtension,
BookmarkBlockPlainTextAdapterExtension,
];
].flat();
@@ -1,4 +1,4 @@
export * from './html.js';
export * from './markdown.js';
export * from './markdown/index.js';
export * from './notion-html.js';
export * from './plain-text.js';
@@ -0,0 +1,12 @@
import type { ExtensionType } from '@blocksuite/store';
import { BookmarkBlockMarkdownAdapterExtension } from './markdown.js';
import { BookmarkBlockMarkdownPreprocessorExtension } from './preprocessor.js';
export * from './markdown.js';
export * from './preprocessor.js';
export const BookmarkBlockMarkdownAdapterExtensions: ExtensionType[] = [
BookmarkBlockMarkdownPreprocessorExtension,
BookmarkBlockMarkdownAdapterExtension,
];
@@ -0,0 +1,67 @@
import {
type MarkdownAdapterPreprocessor,
MarkdownPreprocessorExtension,
} from '@blocksuite/affine-shared/adapters';
// Check if a URL is already encoded with encodeURIComponent
function isEncoded(uri: string): boolean {
try {
// If decoding produces a different result than the original,
// then the URI contains encoded characters
return uri !== decodeURIComponent(uri);
} catch {
// If decoding fails, the URI contains invalid percent-encoding
return true;
}
}
// Format footnote definition with consistent spacing
function formatFootnoteDefinition(reference: string, data: object): string {
return `[^${reference}]: ${JSON.stringify(data)}`;
}
/**
* Preprocessor for footnote url
* We should encode url in footnote definition to avoid markdown link parsing
*
* Example of footnote definition:
* [^ref]: {"type":"url","url":"https://example.com"}
*/
export function footnoteUrlPreprocessor(content: string): string {
// Match footnote definitions with any reference (not just numbers)
// Format: [^reference]: {json_content}
return content.replace(
/\[\^([^\]]+)\]:\s*({[\s\S]*?})/g,
(match, reference, jsonContent) => {
try {
const footnoteData = JSON.parse(jsonContent.trim());
// If footnoteData is not an object or doesn't have url, return original content
// If the url is already encoded, return original content
if (
typeof footnoteData !== 'object' ||
!footnoteData.url ||
isEncoded(footnoteData.url)
) {
return match;
}
return formatFootnoteDefinition(reference, {
...footnoteData,
url: encodeURIComponent(footnoteData.url),
});
} catch {
// Keep original content if JSON parsing fails
return match;
}
}
);
}
const bookmarkBlockPreprocessor: MarkdownAdapterPreprocessor = {
name: 'bookmark-block',
levels: ['block', 'slice', 'doc'],
preprocess: footnoteUrlPreprocessor,
};
export const BookmarkBlockMarkdownPreprocessorExtension =
MarkdownPreprocessorExtension(bookmarkBlockPreprocessor);