mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-14 03:02:31 +00:00
add mfm support
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
import { isoDate, longDate, stampDate } from '$lib/util/time'
|
||||
import { extractYouTubeVideoIds } from '$lib/util/youtube'
|
||||
import Avatar from '../common/Avatar.svelte'
|
||||
import RichText from '../common/RichText.svelte'
|
||||
import MfmContent from '../common/MfmContent.svelte'
|
||||
import Attachments from './Attachments.svelte'
|
||||
import PollView from './PollView.svelte'
|
||||
import PreviewCardView from './PreviewCardView.svelte'
|
||||
@@ -181,7 +181,7 @@
|
||||
{#if entry.spoiler_text}
|
||||
<details class="content-warning">
|
||||
<summary class="content-warning-summary">{entry.spoiler_text}</summary>
|
||||
<RichText
|
||||
<MfmContent
|
||||
html={entry.content}
|
||||
emojis={entry.emojis}
|
||||
mentions={entry.mentions}
|
||||
@@ -194,7 +194,7 @@
|
||||
<YouTubeEmbeds videoIds={youtubeVideoIds} sensitive={entry.sensitive} />
|
||||
</details>
|
||||
{:else}
|
||||
<RichText
|
||||
<MfmContent
|
||||
html={entry.content}
|
||||
emojis={entry.emojis}
|
||||
mentions={entry.mentions}
|
||||
|
||||
@@ -48,3 +48,21 @@ describe('BlogEntry YouTube embeds', () => {
|
||||
expect(view.getByTitle('YouTube video')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('BlogEntry MFM rendering', () => {
|
||||
it('renders the Pleroma API MFM HTML inside a backend-free blog entry', () => {
|
||||
const view = renderEntry({
|
||||
content:
|
||||
'<p>It <span class="mfm-spin" data-mfm-speed="2s" data-mfm-left>works</span></p>',
|
||||
})
|
||||
|
||||
const effect = view.getByText('works')
|
||||
expect(effect).toHaveClass('mfm', '-pause')
|
||||
expect(effect).toHaveAttribute('data-mfm-operator', 'spin')
|
||||
expect(effect).toHaveStyle({
|
||||
animationName: 'mfm-spin',
|
||||
animationDuration: '2s',
|
||||
animationDirection: 'reverse',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Status HTML renderer with Pleroma-FE-compatible MFM effects.
|
||||
*
|
||||
* This consumes the server-rendered FEP-c16b spans found in `status.content`;
|
||||
* it intentionally does not parse raw `$[...]` source syntax.
|
||||
*/
|
||||
import type { CustomEmoji, StatusMention, StatusTag } from '$lib/api/types'
|
||||
import { renderMfmHtml } from '$lib/util/mfm'
|
||||
|
||||
interface Props {
|
||||
html: string | null | undefined
|
||||
emojis?: CustomEmoji[]
|
||||
mentions?: StatusMention[]
|
||||
tags?: StatusTag[]
|
||||
inline?: boolean
|
||||
class?: string
|
||||
lang?: string | null
|
||||
/** Pleroma-FE defaults to pausing animation until hover. */
|
||||
pause?: boolean
|
||||
scale?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
html,
|
||||
emojis,
|
||||
mentions,
|
||||
tags,
|
||||
inline = false,
|
||||
class: extraClass = '',
|
||||
lang,
|
||||
pause = true,
|
||||
scale = false,
|
||||
}: Props = $props()
|
||||
|
||||
const rendered = $derived(
|
||||
renderMfmHtml(html, { emojis, mentions, tags, inline, pause, scale }),
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if rendered}
|
||||
<div
|
||||
class="rich-text {extraClass}"
|
||||
class:rich-text--inline={inline}
|
||||
lang={lang ?? undefined}
|
||||
dir="auto"
|
||||
>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized in renderMfmHtml -->
|
||||
{@html rendered}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import MfmContent from './MfmContent.svelte'
|
||||
|
||||
function mfm(view: { getByText(text: string): HTMLElement }, text: string): HTMLElement {
|
||||
const element = view.getByText(text)
|
||||
expect(element).toHaveClass('mfm')
|
||||
return element
|
||||
}
|
||||
|
||||
describe('MfmContent', () => {
|
||||
it('renders Pleroma server-produced MFM with its paused-until-hover default', () => {
|
||||
const view = render(MfmContent, {
|
||||
props: {
|
||||
html: '<p>Hello <span class="mfm-spin" data-mfm-speed="0.5s" data-mfm-alternate>world</span></p>',
|
||||
},
|
||||
})
|
||||
|
||||
const element = mfm(view, 'world')
|
||||
expect(element).toHaveClass('-pause')
|
||||
expect(element).toHaveAttribute('data-mfm-operator', 'spin')
|
||||
expect(element.style.animationName).toBe('mfm-spin')
|
||||
expect(element.style.animationDuration).toBe('0.5s')
|
||||
expect(element.style.animationDirection).toBe('alternate')
|
||||
expect(element.style.animationIterationCount).toBe('infinite')
|
||||
})
|
||||
|
||||
it('supports Pleroma-FE pause and scale options without a backend', () => {
|
||||
const view = render(MfmContent, {
|
||||
props: {
|
||||
html: '<span class="mfm-x2">large</span>',
|
||||
pause: false,
|
||||
scale: true,
|
||||
},
|
||||
})
|
||||
|
||||
const element = mfm(view, 'large')
|
||||
expect(element).not.toHaveClass('-pause')
|
||||
expect(element).toHaveClass('-scale')
|
||||
expect(element).toHaveAttribute('data-mfm-operator', 'x2')
|
||||
})
|
||||
|
||||
it('preserves RichText sanitizing, custom emoji, and local link rewriting', () => {
|
||||
const view = render(MfmContent, {
|
||||
props: {
|
||||
html: '<script>bad()</script><p><a href="https://example.test/@alice">@alice</a> :party:</p>',
|
||||
mentions: [
|
||||
{
|
||||
id: 'account-1',
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
url: 'https://example.test/@alice',
|
||||
},
|
||||
],
|
||||
emojis: [
|
||||
{
|
||||
shortcode: 'party',
|
||||
url: 'https://example.test/emoji/party.png',
|
||||
static_url: 'https://example.test/emoji/party.png',
|
||||
visible_in_picker: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(view.container.querySelector('script')).not.toBeInTheDocument()
|
||||
expect(view.getByRole('link', { name: '@alice' })).toHaveAttribute('href', '#/@alice')
|
||||
expect(view.getByRole('img', { name: ':party:' })).toHaveAttribute(
|
||||
'src',
|
||||
'https://example.test/emoji/party.png',
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user