import Setting from '@joplin/lib/models/Setting'; import { processImagesInPastedHtml, processPastedHtml } from './resourceHandling'; import markupLanguageUtils from '@joplin/lib/markupLanguageUtils'; import HtmlToMd from '@joplin/lib/HtmlToMd'; import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types'; import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; const createTestMarkupConverters = () => { const markupToHtml: MarkupToHtmlHandler = async (markupLanguage, markup, options) => { const conv = markupLanguageUtils.newMarkupToHtml({}, { resourceBaseUrl: `file://${Setting.value('resourceDir')}/`, customCss: '', }); return conv.render(markupLanguage, markup, {}, options); }; const htmlToMd: HtmlToMarkdownHandler = async (_markupLanguage, html, _originalCss) => { const conv = new HtmlToMd(); return conv.parse(html); }; return { markupToHtml, htmlToMd }; }; describe('resourceHandling', () => { it('should sanitize pasted HTML', async () => { Setting.setConstant('resourceDir', '/home/.config/joplin/resources'); const testCases = [ ['Test: ', 'Test: '], ['test', 'test'], ['test', 'test'], ['evil.pdf', 'evil.pdf'], ['evil()', ''], ['', ''], [ '', '', ], ]; for (const [html, expected] of testCases) { expect(await processPastedHtml(html, null, null)).toBe(expected); } }); it('should clean up pasted HTML', async () => { const { markupToHtml, htmlToMd } = createTestMarkupConverters(); const testCases = [ ['

Hello

World

', '

Hello

\n

World

\n'], ['', ''], ]; for (const [html, expected] of testCases) { expect(await processPastedHtml(html, htmlToMd, markupToHtml)).toBe(expected); } }); it('should preserve images pasted from the resource directory', async () => { const { markupToHtml, htmlToMd } = createTestMarkupConverters(); // All images in the resource directory should be preserved. const html = `test`; expect(await processPastedHtml(html, htmlToMd, markupToHtml)).toBe(html); }); it('should normalize HTML-encoded newlines in image alt attributes', async () => { // Word encodes newlines in alt text as HTML entities. These must be // normalized to spaces before Turndown processes the HTML, otherwise // node.outerHTML (returned verbatim for images with width/height) embeds // literal newlines that break Markdown raw HTML block parsing. const resourceSrc = `file://${encodeURI(Setting.value('resourceDir'))}/resource.png`; const testCases: [string, string][] = [ // HTML entity newlines (Word clipboard format: = LF) [ `A screenshot

AI-generated content.`, `A screenshot AI-generated content.`, ], // Literal newlines in the raw HTML attribute value [ `hello\nworld`, `hello world`, ], ]; for (const [html, expected] of testCases) { expect(await processPastedHtml(html, null, null)).toBe(expected); } }); it('should render Word-pasted images with newlines in alt as img elements, not broken text', async () => { // When Word pastes an image with width/height attributes and in the alt, // Turndown returns node.outerHTML verbatim (preserveImageTagsWithSize=true). // Without normalization, literal newlines inside the Markdown raw HTML block // would terminate the block early, causing the to render as plain text. const { markupToHtml, htmlToMd } = createTestMarkupConverters(); const resourceSrc = `file://${encodeURI(Setting.value('resourceDir'))}/resource.png`; const testCases = [ // Word-style: width/height present, alt has entities `A screenshot

AI-generated content.`, // Multiple consecutive newline entities collapsed to single space `line1

line2`, ]; for (const html of testCases) { const result = await processPastedHtml(html, htmlToMd, markupToHtml); // The image must be rendered as an element, not as escaped/broken text expect(result).toContain('`; const result = await processImagesInPastedHtml(html, { useInternalUrls }); expect(result).toMatch(expectMatch); expect(result).not.toContain(expectAbsent); expect(result).not.toContain('data:'); }); });