From c7d7fd5fdd0485369a3441ed0f0ba91019cd322f Mon Sep 17 00:00:00 2001 From: Oneric Date: Mon, 13 Apr 2026 00:00:00 +0000 Subject: [PATCH] rich_content: overhaul spacing around mentions The old code was inconsistent about when the mention line state and remembered spacing were reset as well as failing to add necessary whitespace if a mention line was immediately succeeded by a plain text node. The latter was kludgily fixd for a particular special case with the last-child exception to whitespace trimming. (However this could en up to retaining superfluous undesireable space in other cases) This led to sometimes space being added "back" several times notably leading to one extra, empty line in blockquote elements succeeding mentions with whitespace. This stalled the otherwise unrelated https://akkoma.dev/AkkomaGang/akkoma-fe/pulls/412 Now we always reset the mention chain and remembered spacing together and also add back spacing in front of plain text nodes if appropriate obsoleting the last-child exception. --- CHANGELOG.md | 1 + src/components/rich_content/rich_content.jsx | 30 ++++++---- .../specs/components/rich_content.spec.js | 60 ++++++++++++++++++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 974b0e9b..b67e868c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - fix notifications on mobile - fix attachment display for remotes not federating any MIME type indicators if they still indicate a generic type. This applies to e.g. bridgy +- fixed some spacing issues after mentions ### Changed - reworked rich content (anything with custom emoji or not pure plaintext) parsing; diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 2631b0c5..ca63dfba 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -208,6 +208,11 @@ class RichContentParser { /> } + #breakMentionChain() { + this.#currentMentions = [] + this.#lastSpacing = '' + } + #maybePushTextNode(acc, text, start, end) { if (end > start && start >= 0) { acc.push(text.slice(start, end)) @@ -250,26 +255,28 @@ class RichContentParser { } #processTextNode(textNode) { - const text = textNode.textContent; + let text = textNode.textContent; const isEmpty = text.trim() === '' if (isEmpty) { // No idea why the old code did that; in-source linebreaks have no special meaning in HTML. // Maybe a quirk of the cursed old NIH parser, but doesn't make any sense to me now. - //if (text.includes('\n')) this.#currentMentions = []; + //if (text.includes('\n')) this.#breakMentionChain(); - // don't include spaces between consecutive mentions. - // we'll include them in MentionsLine. - // However, do preserve _trailing_ whitespace, in case the original source - // placed the spacing between mentions and following elements within the mention context. - if (this.#currentMentions.length > 0 && textNode.parentNode?.lastChild !== textNode) { + // Don't include spaces between consecutive mentions. Since Mentions are inserted into + // the preceeding MentionLine element, we’ll end up with superfluous whitespace otherwise. + if (this.#currentMentions.length > 0) { this.#lastSpacing = text textNode.textContent = '' } return [textNode.textContent] } - this.#currentMentions = [] + if (this.#lastSpacing && !text.match(/^\s/)) { + text = this.#lastSpacing + text + } + + this.#breakMentionChain() return this.#processTextForEmoji(text) } @@ -319,7 +326,7 @@ class RichContentParser { } // mention chain no longer consecutive - this.#currentMentions = [] + this.#breakMentionChain() // Hashtags if ( @@ -352,10 +359,11 @@ class RichContentParser { switch (Tag) { case 'BR': - this.#currentMentions = [] + this.#breakMentionChain() return [
] case 'IMG': + this.#breakMentionChain() return [mentionsLinePadding, this.#renderImage(node)] case 'A': @@ -370,6 +378,8 @@ class RichContentParser { break } + this.#breakMentionChain() + const pchilds = [...node.childNodes].map(n => this.#processNode(n)) const attrs = this.#nodeAttributeMap(node) return [mentionsLinePadding, { pchilds }] diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js index 968c4123..f67dab85 100644 --- a/test/unit/specs/components/rich_content.spec.js +++ b/test/unit/specs/components/rich_content.spec.js @@ -499,8 +499,8 @@ describe('RichContent', () => { '', '', // v-if placeholder, mentionsline's extra mentions and stuff '', - ' ', '', + ' ', 'Testing' ].join('') @@ -518,6 +518,64 @@ describe('RichContent', () => { expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected)) }) + it.skip('does not add back whitespace twice or too late', () => { + // This test is skipped due to what appears to be a bug in Vue’s test-utils + // refer to https://akkoma.dev/AkkomaGang/akkoma-fe/pulls/503 for details + // (if upgrading Vue/test-utils ideally check if the test now works and enable if if it does) + const html_mention = [ + '', + '', + '@user', + '', + '' + ].join('') + + const html = [ + '

', + html_mention, + ' ', + html_mention, + ' ', + '

', + '

aa
bb

' + ].join('') + + const expected_mentionlink = [ + '', + '', + '', + '@user', + '', + '', + '' + ].join('') + + const expected = [ + '

', + '', + expected_mentionlink, + expected_mentionlink, + '', + '', + '

', + ' ', + '

aa
bb

' + ].join('') + + const wrapper = mount(RichContent, { + global, + props: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(expected)) + }) + it('rich contents of a link are handled properly', () => { const html = [ '

',