mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08:00
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:
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { footnoteUrlPreprocessor } from '../../adapters/markdown/preprocessor';
|
||||
|
||||
describe('footnoteUrlPreprocessor', () => {
|
||||
it('should encode unencoded URLs in footnote definitions', () => {
|
||||
const input =
|
||||
'[^ref]: {"type":"url","url":"https://example.com?param=value"}';
|
||||
const expected =
|
||||
'[^ref]: {"type":"url","url":"https%3A%2F%2Fexample.com%3Fparam%3Dvalue"}';
|
||||
expect(footnoteUrlPreprocessor(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not encode already encoded URLs', () => {
|
||||
const input = '[^ref]: {"type":"url","url":"https%3A%2F%2Fexample.com"}';
|
||||
expect(footnoteUrlPreprocessor(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle invalid JSON content', () => {
|
||||
const input = '[^ref]: {"invalid json"}';
|
||||
expect(footnoteUrlPreprocessor(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle non-object footnote data', () => {
|
||||
const input = '[^ref]: "not an object"';
|
||||
expect(footnoteUrlPreprocessor(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle footnote data without url property', () => {
|
||||
const input = '[^ref]: {"type":"url"}';
|
||||
expect(footnoteUrlPreprocessor(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle multiple footnote definitions', () => {
|
||||
const input = `
|
||||
[^ref1]: {"type":"url","url":"https://example1.com"}
|
||||
[^ref2]: {"type":"url","url":"https://example2.com"}
|
||||
`.trim();
|
||||
const expected = `
|
||||
[^ref1]: {"type":"url","url":"https%3A%2F%2Fexample1.com"}
|
||||
[^ref2]: {"type":"url","url":"https%3A%2F%2Fexample2.com"}
|
||||
`.trim();
|
||||
expect(footnoteUrlPreprocessor(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle special characters in URLs', () => {
|
||||
const input =
|
||||
'[^ref]: {"type":"url","url":"https://example.com/path with spaces?param=value&another=param"}';
|
||||
const expected =
|
||||
'[^ref]: {"type":"url","url":"https%3A%2F%2Fexample.com%2Fpath%20with%20spaces%3Fparam%3Dvalue%26another%3Dparam"}';
|
||||
expect(footnoteUrlPreprocessor(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user