diff --git a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts index 1cc0e5b96..44eb8998f 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts @@ -6,7 +6,7 @@ import Resource from '@joplin/lib/models/Resource'; const bridge = require('@electron/remote').require('./bridge').default; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; import htmlUtils from '@joplin/lib/htmlUtils'; -import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils'; +import rendererHtmlUtils, { extractHtmlBody, removeWrappingParagraphAndTrailingEmptyElements } from '@joplin/renderer/htmlUtils'; import Logger from '@joplin/utils/Logger'; import { fileUriToPath } from '@joplin/utils/url'; import { MarkupLanguage } from '@joplin/renderer'; @@ -220,6 +220,13 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa if (htmlToMd && mdToHtml) { const md = await htmlToMd(MarkupLanguage.Markdown, html, ''); html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html; + + // When plugins that add to the end of rendered content are installed, bodyOnly can + // fail to remove the wrapping paragraph. This works around that issue by removing + // the wrapping paragraph in more cases. See issue #10061. + if (!md.trim().includes('\n')) { + html = removeWrappingParagraphAndTrailingEmptyElements(html); + } } return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(html, { diff --git a/packages/renderer/htmlUtils.test.ts b/packages/renderer/htmlUtils.test.ts index a91873596..251572b00 100644 --- a/packages/renderer/htmlUtils.test.ts +++ b/packages/renderer/htmlUtils.test.ts @@ -1,4 +1,4 @@ -import htmlUtils, { extractHtmlBody, htmlDocIsImageOnly } from './htmlUtils'; +import htmlUtils, { extractHtmlBody, htmlDocIsImageOnly, removeWrappingParagraphAndTrailingEmptyElements } from './htmlUtils'; describe('htmlUtils', () => { @@ -86,4 +86,14 @@ describe('htmlUtils', () => { } }); + it.each([ + ['

Test

', 'Test'], + ['

Testing

A test

', '

Testing

A test

'], + ['

Testing


', '

Testing


'], + ['

Testing

', '

Testing

'], + ['

Testing

', 'Testing'], + ['

is

\n', 'is\n'], + ])('should remove empty elements (case %#)', (before, expected) => { + expect(removeWrappingParagraphAndTrailingEmptyElements(before)).toBe(expected); + }); }); diff --git a/packages/renderer/htmlUtils.ts b/packages/renderer/htmlUtils.ts index 4b55ee223..ecc2e6995 100644 --- a/packages/renderer/htmlUtils.ts +++ b/packages/renderer/htmlUtils.ts @@ -424,6 +424,54 @@ export const extractHtmlBody = (html: string) => { return bodyFound ? output.join('') : html; }; +export const removeWrappingParagraphAndTrailingEmptyElements = (html: string) => { + if (!html.startsWith('

')) return html; + + const stack: string[] = []; + const output: string[] = []; + let inFirstParagraph = true; + let canSimplify = true; + + const parser = new htmlparser2.Parser({ + onopentag: (name: string, attrs: Record) => { + if (inFirstParagraph && stack.length > 0) { + output.push(makeHtmlTag(name, attrs)); + } else if (!inFirstParagraph && attrs.style) { + canSimplify = false; + } + + stack.push(name); + }, + ontext: (encodedText: string) => { + if (encodedText.trim() && !inFirstParagraph) { + canSimplify = false; + } else { + output.push(encodedText); + } + }, + onclosetag: (name: string) => { + stack.pop(); + if (stack.length === 0 && name === 'p') { + inFirstParagraph = false; + } else if (inFirstParagraph) { + if (isSelfClosingTag(name)) return; + output.push(``); + + // Many elements, even if empty, can still be visible. + // For example, an


. Don't simplify if these elements + // are present. + } else if (!['div', 'style', 'span'].includes(name)) { + canSimplify = false; + } + }, + }); + + parser.write(html); + parser.end(); + + return canSimplify ? output.join('') : html; +}; + export const htmlDocIsImageOnly = (html: string) => { let imageCount = 0; let nonImageFound = false;