mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-02-12 05:42:39 +00:00
Fix: Image was not rendering correctly in description editor.
This commit is contained in:
parent
7d74efa501
commit
8571c0a7a4
@ -10,11 +10,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Editor } from '@tiptap/react';
|
||||
import {
|
||||
formatValueBasedOnContent,
|
||||
getHtmlStringFromMarkdownString,
|
||||
getTextFromHtmlString,
|
||||
isHTMLString,
|
||||
setEditorContent,
|
||||
transformImgTagsToFileAttachment,
|
||||
} from './BlockEditorUtils';
|
||||
|
||||
describe('getTextFromHtmlString', () => {
|
||||
@ -189,3 +192,617 @@ please contact the support team <test@test.com>.
|
||||
expect(isHTMLString(mixedContent)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Mock EditorState for testing
|
||||
jest.mock('@tiptap/pm/state', () => ({
|
||||
EditorState: {
|
||||
create: jest.fn(() => ({})),
|
||||
},
|
||||
PluginKey: jest.fn().mockImplementation(() => ({})),
|
||||
}));
|
||||
|
||||
// Mock Editor for testing setEditorContent
|
||||
const mockEditor = {
|
||||
commands: {
|
||||
setContent: jest.fn(),
|
||||
},
|
||||
state: {
|
||||
doc: {},
|
||||
plugins: [],
|
||||
schema: {},
|
||||
selection: {},
|
||||
storedMarks: null,
|
||||
},
|
||||
view: {
|
||||
updateState: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
describe('Image transformation in setEditorContent', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('transformImgTagsToFileAttachment', () => {
|
||||
it('should return original string when no img tags present', () => {
|
||||
const htmlString = '<p>Hello world</p>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
expect(mockEditor.commands.setContent).toHaveBeenCalledWith(htmlString);
|
||||
});
|
||||
|
||||
it('should transform simple img tag to file attachment div', () => {
|
||||
const htmlString =
|
||||
'<p><img src="https://example.com/image.jpg" alt="Test image" title="Test title"></p>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
// Verify that setContent was called with the transformed HTML
|
||||
expect(mockEditor.commands.setContent).toHaveBeenCalled();
|
||||
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
// Check that the img tag was transformed to file-attachment div
|
||||
expect(transformedHtml).toContain('data-type="file-attachment"');
|
||||
expect(transformedHtml).toContain(
|
||||
'data-url="https://example.com/image.jpg"'
|
||||
);
|
||||
expect(transformedHtml).toContain('data-filename="Test title"');
|
||||
expect(transformedHtml).toContain('data-mimetype="image"');
|
||||
expect(transformedHtml).toContain('data-is-image="true"');
|
||||
expect(transformedHtml).toContain('data-alt="Test image"');
|
||||
expect(transformedHtml).not.toContain('<img');
|
||||
});
|
||||
|
||||
it('should transform img tag with only src attribute', () => {
|
||||
const htmlString = '<p><img src="https://example.com/image.jpg"></p>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
expect(transformedHtml).toContain(
|
||||
'data-url="https://example.com/image.jpg"'
|
||||
);
|
||||
expect(transformedHtml).toContain('data-filename="image"');
|
||||
expect(transformedHtml).not.toContain('data-alt=""');
|
||||
expect(transformedHtml).not.toContain('<img');
|
||||
});
|
||||
|
||||
it('should transform multiple img tags in same content', () => {
|
||||
const htmlString =
|
||||
'<p><img src="https://example.com/image1.jpg" alt="Image 1"><img src="https://example.com/image2.png" alt="Image 2" title="Second image"></p>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
expect(transformedHtml).toContain(
|
||||
'data-url="https://example.com/image1.jpg"'
|
||||
);
|
||||
expect(transformedHtml).toContain('data-filename="Image 1"');
|
||||
expect(transformedHtml).toContain(
|
||||
'data-url="https://example.com/image2.png"'
|
||||
);
|
||||
expect(transformedHtml).toContain('data-filename="Second image"');
|
||||
expect(transformedHtml).not.toContain('<img');
|
||||
// Should have two file-attachment divs
|
||||
expect(
|
||||
transformedHtml.match(/data-type="file-attachment"/g) || []
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle base64 image sources', () => {
|
||||
const base64Src =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
||||
const htmlString = `<p><img src="${base64Src}" alt="Base64 image"></p>`;
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
expect(transformedHtml).toContain(`data-url="${base64Src}"`);
|
||||
expect(transformedHtml).toContain('data-filename="Base64 image"');
|
||||
expect(transformedHtml).not.toContain('<img');
|
||||
});
|
||||
|
||||
it('should skip img tags without src attribute', () => {
|
||||
const htmlString = '<p><img alt="No source"></p>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
// Should remain unchanged since img has no src
|
||||
expect(transformedHtml).toContain('<img alt="No source">');
|
||||
expect(transformedHtml).not.toContain('data-type="file-attachment"');
|
||||
});
|
||||
|
||||
it('should handle complex HTML with nested img tags', () => {
|
||||
const htmlString =
|
||||
'<div><p><img src="https://placebear.com/g/200/200"></p></div>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
expect(transformedHtml).toContain(
|
||||
'data-url="https://placebear.com/g/200/200"'
|
||||
);
|
||||
expect(transformedHtml).toContain('data-filename="image"');
|
||||
expect(transformedHtml).toContain('<div><p>');
|
||||
expect(transformedHtml).not.toContain('<img');
|
||||
});
|
||||
|
||||
it('should preserve other HTML content while transforming images', () => {
|
||||
const htmlString =
|
||||
'<div><h1>Title</h1><p><img src="https://example.com/test.jpg" alt="Test"></p><p>More content</p></div>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, htmlString);
|
||||
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
expect(transformedHtml).toContain('<h1>Title</h1>');
|
||||
expect(transformedHtml).toContain('<p>More content</p>');
|
||||
expect(transformedHtml).toContain('data-type="file-attachment"');
|
||||
expect(transformedHtml).toContain(
|
||||
'data-url="https://example.com/test.jpg"'
|
||||
);
|
||||
expect(transformedHtml).not.toContain('<img');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEditorContent integration', () => {
|
||||
it('should call editor.commands.setContent with transformed HTML', () => {
|
||||
const content = '<p>Hello world</p>';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, content);
|
||||
|
||||
expect(mockEditor.commands.setContent).toHaveBeenCalledWith(
|
||||
'<p>Hello world</p>'
|
||||
);
|
||||
expect(mockEditor.view.updateState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle markdown content conversion', () => {
|
||||
const markdownContent = '**Bold text**';
|
||||
|
||||
setEditorContent(mockEditor as unknown as Editor, markdownContent);
|
||||
|
||||
expect(mockEditor.commands.setContent).toHaveBeenCalled();
|
||||
expect(mockEditor.view.updateState).toHaveBeenCalled();
|
||||
|
||||
// Content should be converted from markdown to HTML
|
||||
const transformedHtml = mockEditor.commands.setContent.mock.calls[0][0];
|
||||
|
||||
expect(transformedHtml).toContain('<strong>Bold text</strong>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformImgTagsToFileAttachment', () => {
|
||||
describe('Basic Functionality', () => {
|
||||
it('should return original string when no img tags present', () => {
|
||||
const testCases = [
|
||||
'<p>Hello world</p>',
|
||||
'<div><h1>Title</h1><p>Content</p></div>',
|
||||
'<ul><li>List item</li></ul>',
|
||||
'',
|
||||
'Plain text without HTML',
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
const result = transformImgTagsToFileAttachment(testCase);
|
||||
|
||||
expect(result).toBe(testCase);
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect img tags correctly', () => {
|
||||
const htmlWithImg = '<p><img src="test.jpg" alt="test"></p>';
|
||||
const htmlWithoutImg = '<p>No images here</p>';
|
||||
|
||||
expect(htmlWithImg).toContain('<img');
|
||||
expect(htmlWithoutImg).not.toContain('<img');
|
||||
});
|
||||
|
||||
it('should handle empty and null input gracefully', () => {
|
||||
const edgeCases = ['', ' ', '\n\t', null, undefined];
|
||||
|
||||
edgeCases.forEach((input) => {
|
||||
expect(() =>
|
||||
transformImgTagsToFileAttachment(input || '')
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid input types gracefully', () => {
|
||||
const invalidInputs = [123, {}, [], true, false];
|
||||
|
||||
invalidInputs.forEach((input) => {
|
||||
expect(() =>
|
||||
transformImgTagsToFileAttachment(input as string)
|
||||
).not.toThrow();
|
||||
|
||||
const result = transformImgTagsToFileAttachment(input as string);
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
// Should return empty string or string representation for non-string inputs
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Attribute Handling', () => {
|
||||
it('should validate filename priority logic', () => {
|
||||
// Test the priority: title > alt > 'image'
|
||||
const testCases = [
|
||||
{ title: 'Title Text', alt: 'Alt Text', expected: 'Title Text' },
|
||||
{ title: '', alt: 'Alt Text', expected: 'Alt Text' },
|
||||
{ title: '', alt: '', expected: 'image' },
|
||||
{ title: null, alt: 'Alt Text', expected: 'Alt Text' },
|
||||
{ title: undefined, alt: '', expected: 'image' },
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
const filename = testCase.title || testCase.alt || 'image';
|
||||
|
||||
expect(filename).toBe(testCase.expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate expected data attributes structure', () => {
|
||||
const expectedAttributes = [
|
||||
'data-type',
|
||||
'data-url',
|
||||
'data-filename',
|
||||
'data-mimetype',
|
||||
'data-uploading',
|
||||
'data-upload-progress',
|
||||
'data-is-image',
|
||||
];
|
||||
|
||||
expectedAttributes.forEach((attr) => {
|
||||
expect(attr.startsWith('data-')).toBe(true);
|
||||
expect(typeof attr).toBe('string');
|
||||
});
|
||||
|
||||
// Test attribute values
|
||||
const sampleValues = {
|
||||
'data-type': 'file-attachment',
|
||||
'data-mimetype': 'image',
|
||||
'data-uploading': 'false',
|
||||
'data-upload-progress': '0',
|
||||
'data-is-image': 'true',
|
||||
};
|
||||
|
||||
Object.entries(sampleValues).forEach(([, value]) => {
|
||||
expect(typeof value).toBe('string');
|
||||
expect(value.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate attribute assignment patterns', () => {
|
||||
// Test conditional attribute logic
|
||||
const altValues = ['', 'Valid alt text', null, undefined];
|
||||
|
||||
altValues.forEach((alt) => {
|
||||
const shouldSetAlt = !!(alt && alt.trim && alt.trim().length > 0);
|
||||
|
||||
expect(typeof shouldSetAlt).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL Handling', () => {
|
||||
it('should validate base64 image detection', () => {
|
||||
const base64Examples = [
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
|
||||
// eslint-disable-next-line max-len
|
||||
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRwfDx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAhEQACAQIHAQAAAAAAAAAAAAABAgADBAUREiExQVFhkf/aAAwDAQACEQMRAD8A0s91CKWOBIgECogDALzEb3PttHb0zKO8B6sSMEckE5hPBhHQ96L6Cxl5o8XPGJ6lF68VVc2ZJNTXVr0sE09+bAE4I4gg4POD1O9fTpZ1rPtXJm9xCx02HXIkE9Dm1yCXQ0PdnX1rnLqrPUNYSWcmIqCpR5YogBGNOv8Ao',
|
||||
];
|
||||
|
||||
base64Examples.forEach((base64) => {
|
||||
expect(base64.startsWith('data:image/')).toBe(true);
|
||||
expect(base64).toContain('base64,');
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate URL parsing scenarios', () => {
|
||||
const urlScenarios = [
|
||||
{ url: 'https://example.com/test.jpg', valid: true },
|
||||
{ url: '/relative/path.png', valid: true },
|
||||
{ url: 'data:image/png;base64,abc', valid: true },
|
||||
{ url: '', valid: false },
|
||||
{ url: null, valid: false },
|
||||
];
|
||||
|
||||
urlScenarios.forEach((scenario) => {
|
||||
const isValid = !!(scenario.url && scenario.url.length > 0);
|
||||
|
||||
expect(isValid).toBe(scenario.valid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenMetadata Migration Scenarios', () => {
|
||||
it('should recognize legacy OpenMetadata format patterns', () => {
|
||||
// Based on actual migration scenario
|
||||
const oldFormat = '<p><img src="https://placebear.com/g/200/200"></p>';
|
||||
const newFormatPattern = 'data-type="file-attachment"';
|
||||
|
||||
expect(oldFormat).toContain('<p>');
|
||||
expect(oldFormat).toContain('<img');
|
||||
expect(oldFormat).toContain('src=');
|
||||
expect(oldFormat).toContain('placebear.com');
|
||||
|
||||
// New format validation
|
||||
expect(newFormatPattern).toContain('data-type');
|
||||
expect(newFormatPattern).toContain('file-attachment');
|
||||
});
|
||||
|
||||
it('should handle real OpenMetadata migration scenario', () => {
|
||||
// Actual data from the user's example
|
||||
const oldOpenMetadataImage =
|
||||
'<p><img src="https://placebear.com/g/200/200"></p>';
|
||||
|
||||
expect(oldOpenMetadataImage).toContain('placebear.com');
|
||||
expect(oldOpenMetadataImage).toContain('<p><img');
|
||||
|
||||
// Test that function doesn't crash with real data
|
||||
expect(() =>
|
||||
transformImgTagsToFileAttachment(oldOpenMetadataImage)
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with markdown-converted HTML', () => {
|
||||
// Simulate HTML that would come from markdown conversion
|
||||
const markdownHtml =
|
||||
'<p><img src="https://example.com/test.jpg" alt="Test Image"></p>';
|
||||
|
||||
expect(markdownHtml).toContain('<img');
|
||||
expect(markdownHtml).toContain('src=');
|
||||
expect(markdownHtml).toContain('alt=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
it('should handle malformed HTML gracefully', () => {
|
||||
const malformedCases = [
|
||||
'<img>', // No src
|
||||
'<img src="">', // Empty src
|
||||
'<img src="test.jpg"', // Unclosed tag
|
||||
'<<img src="test.jpg">>', // Double brackets
|
||||
'<img src=test.jpg>', // Unquoted src
|
||||
];
|
||||
|
||||
malformedCases.forEach((html) => {
|
||||
// These should not cause errors in processing
|
||||
expect(() => html.includes('<img')).not.toThrow();
|
||||
expect(() => transformImgTagsToFileAttachment(html)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple images in content', () => {
|
||||
const multipleImages =
|
||||
'<p><img src="img1.jpg"><img src="img2.jpg"><img src="img3.jpg"></p>';
|
||||
const imgCount = (multipleImages.match(/<img/g) || []).length;
|
||||
|
||||
expect(imgCount).toBe(3);
|
||||
expect(multipleImages).toContain('<img');
|
||||
});
|
||||
|
||||
it('should preserve non-image content', () => {
|
||||
const mixedContent =
|
||||
'<h1>Title</h1><p>Text content</p><img src="test.jpg"><p>More text</p>';
|
||||
|
||||
expect(mixedContent).toContain('<h1>');
|
||||
expect(mixedContent).toContain('<p>');
|
||||
expect(mixedContent).toContain('<img');
|
||||
});
|
||||
|
||||
it('should handle special characters in attributes', () => {
|
||||
const specialChars = `<img src="https://example.com/image.jpg?param=value&other=123" alt="Image with "quotes" & symbols" title="Title with 'apostrophes'">`;
|
||||
|
||||
expect(() =>
|
||||
transformImgTagsToFileAttachment(specialChars)
|
||||
).not.toThrow();
|
||||
expect(specialChars).toContain('"');
|
||||
expect(specialChars).toContain('&');
|
||||
});
|
||||
|
||||
it('should handle complex nested HTML structures', () => {
|
||||
const complexHtml = `
|
||||
<div class="container">
|
||||
<article>
|
||||
<header><h1>Title</h1></header>
|
||||
<section>
|
||||
<p>Introduction text with <strong>bold</strong> content.</p>
|
||||
<div class="image-container">
|
||||
<img src="https://example.com/main.jpg" alt="Main Image" title="Main"/>
|
||||
<figcaption>Image caption</figcaption>
|
||||
</div>
|
||||
<ul>
|
||||
<li>Item 1 with <img src="icon1.png" alt="Icon"> inline image</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>Quote with <img src="quote-img.jpg"> embedded image</p>
|
||||
</blockquote>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
|
||||
expect(() => transformImgTagsToFileAttachment(complexHtml)).not.toThrow();
|
||||
expect(complexHtml).toContain('<img');
|
||||
|
||||
// Count images in complex HTML
|
||||
const imgCount = (complexHtml.match(/<img/g) || []).length;
|
||||
|
||||
expect(imgCount).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Tests', () => {
|
||||
it('should handle large content efficiently', () => {
|
||||
const largeContent =
|
||||
'<div>'.repeat(100) + '<img src="test.jpg">' + '</div>'.repeat(100);
|
||||
|
||||
expect(largeContent.length).toBeGreaterThan(1000);
|
||||
expect(largeContent).toContain('<img');
|
||||
|
||||
// Should not timeout or crash
|
||||
const start = Date.now();
|
||||
transformImgTagsToFileAttachment(largeContent);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
expect(duration).toBeLessThan(1000); // Should complete in under 1 second
|
||||
});
|
||||
|
||||
it('should validate performance with many images', () => {
|
||||
const manyImages = Array.from(
|
||||
{ length: 50 },
|
||||
(_, i) =>
|
||||
`<img src="https://example.com/image${i}.jpg" alt="Image ${i}">`
|
||||
).join('');
|
||||
|
||||
const start = Date.now();
|
||||
transformImgTagsToFileAttachment(manyImages);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
expect(duration).toBeLessThan(2000); // Should handle 50 images quickly
|
||||
expect(manyImages.match(/<img/g) || []).toHaveLength(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Type Validation', () => {
|
||||
it('should ensure consistent data types', () => {
|
||||
const stringAttributes = [
|
||||
'file-attachment',
|
||||
'image',
|
||||
'false',
|
||||
'0',
|
||||
'true',
|
||||
];
|
||||
|
||||
stringAttributes.forEach((attr) => {
|
||||
expect(typeof attr).toBe('string');
|
||||
expect(attr.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security & Robustness', () => {
|
||||
it('should handle extremely large content without timeout', () => {
|
||||
const largeHtml =
|
||||
'<div>'.repeat(1000) +
|
||||
Array.from({ length: 10 }, (_, i) => `<img src="image${i}.jpg">`).join(
|
||||
''
|
||||
) +
|
||||
'</div>'.repeat(1000);
|
||||
|
||||
const start = Date.now();
|
||||
const result = transformImgTagsToFileAttachment(largeHtml);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
|
||||
// Verify transformation occurred
|
||||
const attachmentCount = (
|
||||
result.match(/data-type="file-attachment"/g) || []
|
||||
).length;
|
||||
|
||||
expect(attachmentCount).toBe(10);
|
||||
});
|
||||
|
||||
it('should maintain consistent behavior with whitespace variations', () => {
|
||||
const variations = [
|
||||
'<img src="test.jpg" alt="test">',
|
||||
'<img src="test.jpg" alt="test" >',
|
||||
'<img\nsrc="test.jpg"\nalt="test">',
|
||||
'<img\tsrc="test.jpg"\talt="test">',
|
||||
// Note: '< img' with space is invalid HTML, so we skip that case
|
||||
];
|
||||
|
||||
variations.forEach((html) => {
|
||||
const result = transformImgTagsToFileAttachment(html);
|
||||
|
||||
expect(result).toContain('data-type="file-attachment"');
|
||||
expect(result).toContain('data-url="test.jpg"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world Edge Cases', () => {
|
||||
it('should handle mixed content with various elements', () => {
|
||||
const mixedContent = `
|
||||
<div class="content">
|
||||
<h1>Article Title</h1>
|
||||
<p>Some introductory text with <strong>bold</strong> content.</p>
|
||||
<img src="header-image.jpg" alt="Header" title="Main header image">
|
||||
<blockquote>
|
||||
<p>This is a quote with an <img src="quote-icon.png" alt="quote"> icon.</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>List item 1</li>
|
||||
<li>Item with image: <img src="list-image.gif" alt="list item"></li>
|
||||
</ul>
|
||||
<p>Final paragraph with embedded <img src="inline.jpg" alt="inline"> image.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const result = transformImgTagsToFileAttachment(mixedContent);
|
||||
|
||||
// Should preserve structure
|
||||
expect(result).toContain('<h1>Article Title</h1>');
|
||||
expect(result).toContain('<blockquote>');
|
||||
expect(result).toContain('<ul>');
|
||||
|
||||
// Should transform all images
|
||||
expect(result.match(/data-type="file-attachment"/g) || []).toHaveLength(
|
||||
4
|
||||
);
|
||||
expect(result).not.toContain('<img');
|
||||
|
||||
// Should preserve alt attributes
|
||||
expect(result).toContain('data-alt="Header"');
|
||||
expect(result).toContain('data-alt="quote"');
|
||||
expect(result).toContain('data-alt="inline"');
|
||||
});
|
||||
|
||||
it('should handle OpenMetadata specific scenarios', () => {
|
||||
// Test scenarios specific to OpenMetadata migration
|
||||
const scenarios = [
|
||||
{
|
||||
name: 'placebear.com images',
|
||||
html: '<p><img src="https://placebear.com/g/200/200"></p>',
|
||||
expected: 'data-url="https://placebear.com/g/200/200"',
|
||||
},
|
||||
{
|
||||
name: 'base64 images from drag-drop',
|
||||
html: '<p><img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gA7Q1JFQVRP" alt="Uploaded"></p>',
|
||||
expected:
|
||||
'data-url="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gA7Q1JFQVRP"',
|
||||
},
|
||||
{
|
||||
name: 'images with special characters in filename',
|
||||
html: '<img src="test%20image%20(1).jpg" alt="Test Image (1)" title="Test Image (1)">',
|
||||
expected: 'data-filename="Test Image (1)"',
|
||||
},
|
||||
];
|
||||
|
||||
scenarios.forEach((scenario) => {
|
||||
const result = transformImgTagsToFileAttachment(scenario.html);
|
||||
|
||||
expect(result).toContain('data-type="file-attachment"');
|
||||
expect(result).not.toContain('<img');
|
||||
|
||||
expect(result).toContain(scenario.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
import { EditorState } from '@tiptap/pm/state';
|
||||
import { Editor } from '@tiptap/react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, isString } from 'lodash';
|
||||
import Showdown from 'showdown';
|
||||
import { ReactComponent as IconFormatAttachment } from '../assets/svg/ic-format-attachment.svg';
|
||||
import { ReactComponent as IconFormatAudio } from '../assets/svg/ic-format-audio.svg';
|
||||
@ -194,9 +194,54 @@ export const getHtmlStringFromMarkdownString = (content: string) => {
|
||||
* @param editor The editor instance
|
||||
* @param newContent The new content to set
|
||||
*/
|
||||
export const transformImgTagsToFileAttachment = (
|
||||
htmlString: string
|
||||
): string => {
|
||||
// Input validation - ensure we have a valid string
|
||||
if (!htmlString || !isString(htmlString)) {
|
||||
return String(htmlString || '');
|
||||
}
|
||||
|
||||
if (!htmlString.includes('<img')) {
|
||||
return htmlString;
|
||||
}
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlString;
|
||||
|
||||
const imgTags = tempDiv.querySelectorAll('img[src]');
|
||||
|
||||
imgTags.forEach((img) => {
|
||||
const src = img.getAttribute('src');
|
||||
const alt = img.getAttribute('alt') || '';
|
||||
const title = img.getAttribute('title') || '';
|
||||
|
||||
if (src) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.setAttribute('data-type', 'file-attachment');
|
||||
fileDiv.setAttribute('data-url', src);
|
||||
fileDiv.setAttribute('data-filename', title || alt || 'image');
|
||||
fileDiv.setAttribute('data-mimetype', 'image');
|
||||
fileDiv.setAttribute('data-uploading', 'false');
|
||||
fileDiv.setAttribute('data-upload-progress', '0');
|
||||
fileDiv.setAttribute('data-is-image', 'true');
|
||||
if (alt) {
|
||||
fileDiv.setAttribute('data-alt', alt);
|
||||
}
|
||||
|
||||
img.parentNode?.replaceChild(fileDiv, img);
|
||||
}
|
||||
});
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
};
|
||||
|
||||
export const setEditorContent = (editor: Editor, newContent: string) => {
|
||||
// Convert the markdown string to an HTML string
|
||||
const htmlString = getHtmlStringFromMarkdownString(newContent);
|
||||
let htmlString = getHtmlStringFromMarkdownString(newContent);
|
||||
|
||||
// Transform img tags to file-attachment divs before Tiptap processes them
|
||||
htmlString = transformImgTagsToFileAttachment(htmlString);
|
||||
|
||||
editor.commands.setContent(htmlString);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user