feat(editor): add favicon, title, description support for footnote url reference (#11924)

Closes: [BS-3272](https://linear.app/affine-design/issue/BS-3272/footnote-适配-exa-api-返回结果)

## What's Changed

Add link preview data support (favicon, title, description) for URL references in footnotes:
- Store and display URL preview data in footnotes
- Add encoding/decoding support for favicon URLs
- Optimize link preview by using existing preview data when available
This commit is contained in:
donteatfriedrice
2025-04-23 11:57:25 +00:00
parent ff133d1267
commit db5eadb72a
7 changed files with 121 additions and 31 deletions
@@ -50,4 +50,28 @@ describe('footnoteUrlPreprocessor', () => {
'[^ref]: {"type":"url","url":"https%3A%2F%2Fexample.com%2Fpath%20with%20spaces%3Fparam%3Dvalue%26another%3Dparam"}';
expect(footnoteUrlPreprocessor(input)).toBe(expected);
});
it('should encode unencoded favicon URLs', () => {
const input =
'[^ref]: {"type":"url","url":"https://example.com","favicon":"https://example.com/icon.png"}';
const expected =
'[^ref]: {"type":"url","url":"https%3A%2F%2Fexample.com","favicon":"https%3A%2F%2Fexample.com%2Ficon.png"}';
expect(footnoteUrlPreprocessor(input)).toBe(expected);
});
it('should not encode already encoded favicon URLs', () => {
const input =
'[^ref]: {"type":"url","url":"https://example.com","favicon":"https%3A%2F%2Fexample.com%2Ficon.png"}';
const expected =
'[^ref]: {"type":"url","url":"https%3A%2F%2Fexample.com","favicon":"https%3A%2F%2Fexample.com%2Ficon.png"}';
expect(footnoteUrlPreprocessor(input)).toBe(expected);
});
it('should handle both URL and icon encoding in the same footnote', () => {
const input =
'[^ref]: {"type":"url","url":"https://example.com?param=value","favicon":"https://example.com/icon.png?size=large"}';
const expected =
'[^ref]: {"type":"url","url":"https%3A%2F%2Fexample.com%3Fparam%3Dvalue","favicon":"https%3A%2F%2Fexample.com%2Ficon.png%3Fsize%3Dlarge"}';
expect(footnoteUrlPreprocessor(input)).toBe(expected);
});
});
@@ -35,20 +35,41 @@ export function footnoteUrlPreprocessor(content: string): string {
(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)
) {
// Basic validation checks
if (typeof footnoteData !== 'object') {
return match;
}
return formatFootnoteDefinition(reference, {
if (!footnoteData.url) {
return match;
}
// Check if URLs are already encoded
const isUrlEncoded = isEncoded(footnoteData.url);
const hasIcon = !!footnoteData.favicon;
const isIconEncoded = hasIcon && isEncoded(footnoteData.favicon);
// If both URL and icon (if present) are already encoded, return original
if (isUrlEncoded && (!hasIcon || isIconEncoded)) {
return match;
}
// Create processed data with encoded URLs
const processedData = {
...footnoteData,
url: encodeURIComponent(footnoteData.url),
});
url: isUrlEncoded
? footnoteData.url
: encodeURIComponent(footnoteData.url),
};
// Add encoded favicon if present
if (hasIcon) {
processedData.favicon = isIconEncoded
? footnoteData.favicon
: encodeURIComponent(footnoteData.favicon);
}
return formatFootnoteDefinition(reference, processedData);
} catch {
// Keep original content if JSON parsing fails
return match;