Use FEP-c16b: Formatting MFM functions #410

Open
ilja wants to merge 10 commits from ilja/akkoma-fe:use_fep-c16b_formatting_mfm_functions into develop
9 changed files with 503 additions and 265 deletions

View file

@ -6,7 +6,6 @@
<title>Akkoma</title> <title>Akkoma</title>
<link rel="stylesheet" href="/static/font/tiresias.css"> <link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css"> <link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css"> <link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder"> <link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta--> <!--server-generated-meta-->

View file

@ -1,4 +1,4 @@
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4'] const MFM_TAGS = ['bg', 'blur', 'bounce', 'center', 'fg', 'flip', 'font', 'jelly', 'jump', 'position', 'rainbow', 'rotate', 'scale', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true })) .map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
/** /**

View file

@ -191,6 +191,24 @@ export default {
if (this.handleLinks && attrs?.['class']?.includes?.('h-card')) { if (this.handleLinks && attrs?.['class']?.includes?.('h-card')) {
return ['', children.map(processItem), ''] return ['', children.map(processItem), '']
} }
// Turn data-mfm- attributes into a string for the `style` attribute
// If they have a value different than `true`, they need to be added to `style`
// e.g. `attrs={'data-mfm-some': '1deg', 'data-mfm-thing': '5s'}` => "--mfm-some: 1deg;--mfm-thing: 5s;"
// Note that we only add the value to `style` when they contain only letters, numbers, dot, or minus signs
// At the moment of writing, this should be enough for legitimite purposes and reduces the chance of injection by using special characters
// There is a special case for the `color` value, who is provided without `#`, but requires this in the `style` attribute
let mfm_style = Object.keys(attrs).filter(
(key) => key.startsWith('data-mfm-') && attrs[key] !== true && /^[a-zA-Z0-9.\-]*$/.test(attrs[key])
).map(
(key) => '--mfm-' + key.substr(9) + (key === 'data-mfm-color' ? ': #' : ': ') + attrs[key] + ';'
).reduce((a,v) => a+v, '')
if (mfm_style !== '') {
return [
opener.slice(0,-1) + ' style="' + mfm_style + '">',
children.map(processItem),
closer
]
}
} }
if (children !== undefined) { if (children !== undefined) {

View file

@ -3,6 +3,7 @@
.StatusBody { .StatusBody {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
.translation { .translation {
border: 1px solid var(--accent, $fallback--link); border: 1px solid var(--accent, $fallback--link);
@ -23,24 +24,6 @@
transition: 0.05s; transition: 0.05s;
} }
._mfm_x2_ {
.emoji {
height: 100px;
}
}
._mfm_x3_ {
.emoji {
height: 150px;
}
}
._mfm_x4_ {
.emoji {
height: 200px;
}
}
.attachments { .attachments {
margin-top: 0.5em; margin-top: 0.5em;
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="StatusBody" class="StatusBody"
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }" :class="{ '-compact': compact }"
> >
<div class="body"> <div class="body">
<div <div

View file

@ -0,0 +1,423 @@
/**
* "FEP-c16b: Formatting MFM functions" attributes that Akkoma supports
*/
.StatusContent:not(.mfm-disabled) {
/* The following are the non-animated MFM */
.mfm-center {
display: block;
text-align: center;
}
.mfm-flip {
display: inline-block;
transform: scaleX(-1);
}
.mfm-flip[data-mfm-v] {
transform: scaleY(-1);
}
.mfm-flip[data-mfm-v][data-mfm-h] {
transform: scale(-1, -1);
}
.mfm-font[data-mfm-serif] {
font-family: serif;
}
.mfm-font[data-mfm-monospace] {
font-family: monospace;
}
.mfm-font[data-mfm-cursive] {
font-family: cursive;
}
.mfm-font[data-mfm-fantasy] {
font-family: fantasy;
}
.mfm-font[data-mfm-emoji] {
font-family: emoji;
}
.mfm-font[data-mfm-math] {
font-family: math;
}
.mfm-blur {
filter: blur(6px);
transition: filter 0.3s;
&:hover {
filter: blur(0);
}
}
.mfm-rotate {
display: inline-block;
transform: rotate(calc(var(--mfm-deg, 90) * 1deg));
transform-origin: center center;
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {

I think the whole emoji size scaling setup is overly complicated. Just setting --mfm-emoji-zoom-size: 2.63; etc as here in all relevant cases and using this with a default of 1.0 directly in the base emoji height definition would achieve the same effect as now but avoid manually overriding height directly and --nested-base-emoji-size for some MFM effects.

However, while i appreciate the effort which went into preserving Akkoma’s old emoji scaling exactly (except for nested scaling which previously was a noop), this scaling behaviour is not compatible with Foundkey, iceshrimp and likely all other keys and imho leads to a jarring discrepancy as emoji get scaled noticeable more than text. Foundkey and IceShrimp set the emoji height to 2em instead of a fixed pixel value which means they’ll get automatically be scaled together with and by the same amount as text without any emoji-specific scaling logic. Imho we should adopt this too; improving portability and simplifying our logic at the same time.

I think the whole emoji size scaling setup is overly complicated. Just setting `--mfm-emoji-zoom-size: 2.63;` etc as here in all relevant cases and using this with a default of `1.0` directly in the base emoji `height` definition would achieve the same effect as now but avoid manually overriding `height` directly and `--nested-base-emoji-size` for some MFM effects. However, while i appreciate the effort which went into preserving Akkoma’s old emoji scaling exactly *(except for nested scaling which previously was a noop)*, this scaling behaviour is not compatible with Foundkey, iceshrimp and likely all other keys and imho leads to a jarring discrepancy as emoji get scaled noticeable more than text. Foundkey and IceShrimp set the emoji height to `2em` instead of a fixed pixel value which means they’ll get automatically be scaled together with and by the same amount as text without any emoji-specific scaling logic. Imho we should adopt this too; improving portability and simplifying our logic at the same time.
Outdated
Review

set the emoji height to 2em instead of a fixed pixel value

Yes! I didn't understand why I needed extra CSS for emoji while Foundey didn't ^^' Using em works perfectly, thx!

One problem I saw with this, as you say, *key uses 2em. But Akkoma uses 38px, which corresponds to ~2.9px. So that means that we'd have smaller emoji than what people are used to on Akkoma-fe. I see two options

  • We set emoji at 2em, and people have to live with it (personally I prefer them a bit bigger)
  • We keep 38px for emoji, but when a zoom (x2, x3, x4, tada) is used, we override to --emoji-size: 2em;. In normal situations, emoji will be 38px as they are now. When a zoom is used, the emoji will be e.g. 2em * 200%. For tada, it will start at 2em, so smaller than "normal", but then it grows bigger bc that's what tada does.

When I try, this looks OK for me. It may cause things to be slightly off for things like mfmArt, but if we want to be completely compatible with the most advanced mfmArt, we have to change a lot more I'm afraid. So the question then becomes how far we want to go with that.

I now pushed a new commit with the second option, because it looks best to me, but it can still be changed if needed.

> set the emoji height to 2em instead of a fixed pixel value Yes! I didn't understand why I needed extra CSS for emoji while Foundey didn't ^^' Using `em` works perfectly, thx! One problem I saw with this, as you say, *key uses 2em. But Akkoma uses 38px, which corresponds to ~2.9px. So that means that we'd have smaller emoji than what people are used to on Akkoma-fe. I see two options * We set emoji at 2em, and people have to live with it (personally I prefer them a bit bigger) * We keep 38px for emoji, but when a zoom (x2, x3, x4, tada) is used, we override to `--emoji-size: 2em;`. In normal situations, emoji will be 38px as they are now. When a zoom is used, the emoji will be e.g. `2em * 200%`. For tada, it will start at `2em`, so smaller than "normal", but then it grows bigger bc that's what tada does. When I try, this looks OK for me. It may cause things to be slightly off for things like mfmArt, but if we want to be completely compatible with the most advanced mfmArt, we have to change a lot more I'm afraid. So the question then becomes how far we want to go with that. I now pushed a new commit with the second option, because it looks best to me, but it can still be changed if needed.

i was thinking of continuing to retain the current base size for non-MFM posts, but use 2em for MFM posts only.
This way scaling remains striaghtforward and we’re more compatible while the majority of posts (not-MFM) doesn't change at all

i was thinking of continuing to retain the current base size for non-MFM posts, but use `2em` for MFM posts only. This way scaling remains striaghtforward and we’re more compatible while the majority of posts (not-MFM) doesn't change at all
Outdated
Review

That one has crossed my mind too, but also has its problems (and worse imo).

From a code-complexity perspective; We now need a way for css to know what posts are mfm and not. It's doable, we can add a class somewhere with what content they were written in (or at least that it's MFM). But that's more complexity than what I did now (just one extra line in the css that already handles the zoom), so not an advantage in that regard, on the contrary.

It also means that input method now has an effect on the end result. But input method should purely be a client thing imo, transforming input into a proper representation. With MFM we couldn't do that, hence the work that this PR is part of. Bringing back the requirenment for knowing the input method and doing special handling goes against the "raison d'être" of this work for me.

There's also the fact that it may not be obvious to people why a custom emoji is smaller in some cases and bigger in others. Not all posts who are marked as having MFM are always MfmArt. So in those cases you'd have a perfectly normal-looking post, but suddenly the emoji have a different size than what people expect. This is internally inconsistent and I fear this will bring more confusion.

That one has crossed my mind too, but also has its problems (and worse imo). From a code-complexity perspective; We now need a way for css to know what posts are mfm and not. It's doable, we can add a class somewhere with what content they were written in (or at least that it's MFM). But that's more complexity than what I did now (just one extra line in the css that already handles the zoom), so not an advantage in that regard, on the contrary. It also means that input method now has an effect on the end result. But input method should purely be a client thing imo, transforming input into a proper representation. With MFM we couldn't do that, hence the work that this PR is part of. Bringing back the requirenment for knowing the input method and doing special handling goes against the "raison d'être" of this work for me. There's also the fact that it may not be obvious to people why a custom emoji is smaller in some cases and bigger in others. Not all posts who are marked as having MFM are always MfmArt. So in those cases you'd have a perfectly normal-looking post, but suddenly the emoji have a different size than what people expect. This is internally inconsistent and I fear this will bring more confusion.

From a code-complexity perspective; We now need a way for css to know what posts are mfm and not

If it wasn’t dropped as part of this change we already have that via the .mfm class. In fact this change is part of the client-side overrides I’m applying atm to make MFM post behave better.

current MFM CSS overrides for reference
    /* Slow default down until Akkoma parses parameters again */
    ._mfm_spin_ {
        animation: mfm-spin 2.0s linear infinite !important;
    }
    
    /* Nested Tags use calc(var(--mfm-zoom-size) / 2 + 50%)
     * With zoom-size being:
     *  - x4: 600%
     *  - x3: 400%
     *  - x2: 200%
     */
    ._mfm_x2_ ._mfm_x4_,
    ._mfm_x3_ ._mfm_x4_,
    ._mfm_x4_ ._mfm_x4_ {
        font-size: 350%;
    }
    ._mfm_x2_ ._mfm_x3_,
    ._mfm_x3_ ._mfm_x3_,
    ._mfm_x4_ ._mfm_x3_ {
        font-size: 250%;
    }
    ._mfm_x2_ ._mfm_x2_,
    ._mfm_x3_ ._mfm_x2_,
    ._mfm_x4_ ._mfm_x2_ {
        font-size: 150%;
    }
    
    .mfm .emoji /*img*/ {
        height: 2em !important;
        vertical-align: middle;/**/
    }

It also means that input method now has an effect on the end result.

Input methods already have an effect on the end result, e.g. MFM is rendered as an inline block, line spacing differs between MFM,Markdown and BBCode,plain text, plain text quotes get coloured while in Markdown and MFM it gets shifted, set in faint text /Or italics atm) and a bar is added, etc

Each input method is a different typeset language and to get the same result across different input methods, different input is required (if possible at all).
MFM in particular comes with stricter expectations on what constitutes a correct display and is defined by *key instances. One of those requirements being 2em-sized emoji.

> From a code-complexity perspective; We now need a way for css to know what posts are mfm and not If it wasn’t dropped as part of this change we already have that via the `.mfm` class. In fact this change is part of the client-side overrides I’m applying atm to make MFM post behave better. <details> <summary>current MFM CSS overrides for reference</summary> ```css /* Slow default down until Akkoma parses parameters again */ ._mfm_spin_ { animation: mfm-spin 2.0s linear infinite !important; } /* Nested Tags use calc(var(--mfm-zoom-size) / 2 + 50%) * With zoom-size being: * - x4: 600% * - x3: 400% * - x2: 200% */ ._mfm_x2_ ._mfm_x4_, ._mfm_x3_ ._mfm_x4_, ._mfm_x4_ ._mfm_x4_ { font-size: 350%; } ._mfm_x2_ ._mfm_x3_, ._mfm_x3_ ._mfm_x3_, ._mfm_x4_ ._mfm_x3_ { font-size: 250%; } ._mfm_x2_ ._mfm_x2_, ._mfm_x3_ ._mfm_x2_, ._mfm_x4_ ._mfm_x2_ { font-size: 150%; } .mfm .emoji /*img*/ { height: 2em !important; vertical-align: middle;/**/ } ``` </details> > It also means that input method now has an effect on the end result. Input methods already have an effect on the end result, e.g. MFM is rendered as an inline block, line spacing differs between MFM,Markdown and BBCode,plain text, plain text quotes get coloured while in Markdown and MFM it gets shifted, set in faint text /Or italics atm) and a bar is added, etc Each input method is a different typeset language and to get the same result across different input methods, different input is required *(if possible at all)*. MFM in particular comes with stricter expectations on what constitutes a *correct* display and is defined by \*key instances. One of those requirements being `2em`-sized emoji.
Outdated
Review

I should maybe rephrase myself. We get an object who has a content field. It also has a source field. Currently we use the source field, and that causes problems. This whole excercise here is about making it so that the content can be used without requiring the source field.

Afaik, the differences you mention don't have anything to do with the source (i.e. stated input method) field at all, they are purely derived from the content. The only exception is MFM, and that's what this is trying to fix.

MFM in particular comes with stricter expectations on what constitutes a correct display and is defined by *key instances. One of those requirements being 2em-sized emoji.

I certainly do not have the expectation of emoji to suddenly look different, and I very much believe it will only cause more confusion. Emoji are federated just like any one else federates them, so there's no special expectation there. There's is also no specific mention of 2em on https://misskey-hub.net/en/docs/for-users/features/mfm/

Besides, if that's really the case to make, that the content should look like it does on Misskey; how far should this be taken? Because there's more than just "emoji are 2em size". Should the text font be different? Should the spacings be different? Should we render the spacing differend? Heck, should the width of the content be changed? And so on... If yes, then we dissagree on a very fundamental level which I do not see how to resolve.

I should maybe rephrase myself. We get an object who has a `content` field. It also has a `source` field. Currently we use the `source` field, and that causes problems. This whole excercise here is about making it so that the `content` can be used without requiring the `source` field. Afaik, the differences you mention don't have anything to do with the `source` (i.e. stated input method) field at all, they are purely derived from the content. The only exception is MFM, and that's what this is trying to fix. > MFM in particular comes with stricter expectations on what constitutes a correct display and is defined by *key instances. One of those requirements being 2em-sized emoji. I certainly do not have the expectation of emoji to suddenly look different, and I very much believe it will only cause more confusion. Emoji are federated just like any one else federates them, so there's no special expectation there. There's is also no specific mention of `2em` on https://misskey-hub.net/en/docs/for-users/features/mfm/ Besides, if that's really the case to make, that the content should look like it does on Misskey; how far should this be taken? Because there's more than just "emoji are `2em` size". Should the text font be different? Should the spacings be different? Should we render the spacing differend? Heck, should the width of the content be changed? And so on... If yes, then we dissagree on a very fundamental level which I do not see how to resolve.
Outdated
Review

Maybe a more important question; Do you consider this a blocking issue? If not, I'd like to keep it for now, as I do consider it a strict improvement regardless, and then see what people think.

Maybe a more important question; Do you consider this a blocking issue? If not, I'd like to keep it for now, as I do consider it a strict improvement regardless, and then see what people think.
--mfm-zoom-size: 600%;
}
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
.emoji {
--emoji-size: 2em;
}
font-size: var(--mfm-zoom-size);
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
/* disabled */
font-size: 100%;
}
}
}
.mfm-position {
display: inline-block;
transform: translate(calc(var(--mfm-x, 0) * 1em), calc(var(--mfm-y, 0) * 1em));
}
.mfm-scale {
display: inline-block;
transform: scale(var(--mfm-x, 1), var(--mfm-y, 1));
}
.mfm-fg {
color: var(--mfm-color, #f00);
}
.mfm-bg {
background-color: var(--mfm-color, #0f0);
}
/* The following are the animated MFM */
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
* So either StatusContent does not have this class,
* or it has the class and we are hovering over StatusContent
*/
&:not(.mfm-hover:not(:hover)) {
.mfm-jelly {
display: inline-block;
animation: mfm-rubberBand var(--mfm-speed, 1s) linear infinite both;
}
.mfm-twitch {
display: inline-block;
animation: mfm-twitch var(--mfm-speed, 0.5s) ease infinite;
}
.mfm-shake {
display: inline-block;
animation: mfm-shake var(--mfm-speed, 0.5s) ease infinite;
}
.mfm-spin {
display: inline-block;
animation: mfm-spin var(--mfm-speed, 1.5s) linear infinite;
}
.mfm-spin[data-mfm-y] {
animation-name: mfm-spinY;
}
.mfm-spin[data-mfm-x] {
animation-name: mfm-spinX;
}
.mfm-spin[data-mfm-alternate] {
animation-direction: alternate;
}
.mfm-spin[data-mfm-left] {
animation-direction: reverse;
}
.mfm-jump {
display: inline-block;
animation: mfm-jump var(--mfm-speed, 0.75s) linear infinite;
}
.mfm-bounce {
display: inline-block;
animation: mfm-bounce var(--mfm-speed, 0.75s) linear infinite;
transform-origin: center bottom;
}
.mfm-rainbow {
animation: mfm-rainbow var(--mfm-speed, 1s) linear infinite;
}
.mfm-tada {
display: inline-block;
animation: mfm-tada var(--mfm-speed, 1s) linear infinite both;
--mfm-zoom-size: 150%;
}
}
/* animation keyframes */
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes mfm-spinX {
0% { transform: perspective(128px) rotateX(0deg); }
100% { transform: perspective(128px) rotateX(360deg); }
}
@keyframes mfm-spinY {
0% { transform: perspective(128px) rotateY(0deg); }
100% { transform: perspective(128px) rotateY(360deg); }
}
@keyframes mfm-jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes mfm-bounce {
0% { transform: translateY(0) scale(1, 1); }
25% { transform: translateY(-16px) scale(1, 1); }
50% { transform: translateY(0) scale(1, 1); }
75% { transform: translateY(0) scale(1.5, 0.75); }
100% { transform: translateY(0) scale(1, 1); }
}
@keyframes mfm-twitch {
0% { transform: translate(7px, -2px); }
5% { transform: translate(-3px, 1px); }
10% { transform: translate(-7px, -1px); }
15% { transform: translate(0, -1px); }
20% { transform: translate(-8px, 6px); }
25% { transform: translate(-4px, -3px); }
30% { transform: translate(-4px, -6px); }
35% { transform: translate(-8px, -8px); }
40% { transform: translate(4px, 6px); }
45% { transform: translate(-3px, 1px); }
50% { transform: translate(2px, -10px); }
55% { transform: translate(-7px, 0); }
60% { transform: translate(-2px, 4px); }
65% { transform: translate(3px, -8px); }
70% { transform: translate(6px, 7px); }
75% { transform: translate(-7px, -2px); }
80% { transform: translate(-7px, -8px); }
85% { transform: translate(9px, 3px); }
90% { transform: translate(-3px, -2px); }
95% { transform: translate(-10px, 2px); }
100% { transform: translate(-2px, -6px); }
}
@keyframes mfm-shake {
0% { transform: translate(-3px, -1px) rotate(-8deg); }
5% { transform: translate(0, -1px) rotate(-10deg); }
10% { transform: translate(1px, -3px) rotate(0deg); }
15% { transform: translate(1px, 1px) rotate(11deg); }
20% { transform: translate(-2px, 1px) rotate(1deg); }
25% { transform: translate(-1px, -2px) rotate(-2deg); }
30% { transform: translate(-1px, 2px) rotate(-3deg); }
35% { transform: translate(2px, 1px) rotate(6deg); }
40% { transform: translate(-2px, -3px) rotate(-9deg); }
45% { transform: translate(0, -1px) rotate(-12deg); }
50% { transform: translate(1px, 2px) rotate(10deg); }
55% { transform: translate(0, -3px) rotate(8deg); }
60% { transform: translate(1px, -1px) rotate(8deg); }
65% { transform: translate(0, -1px) rotate(-7deg); }
70% { transform: translate(-1px, -3px) rotate(6deg); }
75% { transform: translate(0, -2px) rotate(4deg); }
80% { transform: translate(-2px, -1px) rotate(3deg); }
85% { transform: translate(1px, -3px) rotate(-10deg); }
90% { transform: translate(1px, 0) rotate(3deg); }
95% { transform: translate(-2px, 0) rotate(-3deg); }
100% { transform: translate(2px, 1px) rotate(2deg); }
}
@keyframes mfm-rubberBand {
0% { transform: scale3d(1, 1, 1); }
30% { transform: scale3d(1.25, 0.75, 1); }
40% { transform: scale3d(0.75, 1.25, 1); }
50% { transform: scale3d(1.15, 0.85, 1); }
65% { transform: scale3d(0.95, 1.05, 1); }
75% { transform: scale3d(1.05, 0.95, 1); }
100% { transform: scale3d(1, 1, 1); }
}
@keyframes mfm-rainbow {
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
}
@keyframes mfm-tada {
0%,
100% { transform: scale3d(1, 1, 1); }
10%,
20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
30%,
50%,
70%,
90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
40%,
60%,
80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
}
/**
* Legacy MFM
* This is for backwards compatibility with posts formatted on Akkoma before support for FEP-c16b
* Note that it uses the keyframes as defined above for the FEP-c16b compatible MFM representation
*/
.mfm {
display: inline-block;
}
/* The following are the legacy non-animated MFM */
._mfm_flip_[data-h][data-v] {
transform: scale(-1, -1);
}
._mfm_flip_[data-v] {
transform: scaleY(-1);
}
._mfm_flip_:not([data-v]) {
transform: scaleX(-1);
}
._mfm_x2_ {
font-size: 200%;
}
._mfm_x3_ {
font-size: 400%;
}
._mfm_x4_ {
font-size: 600%;
}
._mfm_x2_ {
.emoji {
height: 100px;
}
}
._mfm_x3_ {
.emoji {
height: 150px;
}
}
._mfm_x4_ {
.emoji {
height: 200px;
}
}
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s;
}
._mfm_blur_:hover {
filter: blur(0);
}
._mfm_rotate_ {
transform: rotate(90deg);
transform-origin: center center;
}
/* The following are the legacy animated MFM */
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
* So either StatusContent does not have this class,
* or it has the class and we are hovering over StatusContent
*/
&:not(.mfm-hover:not(:hover)) {
._mfm_tada_ {
font-size: 150%;
animation: mfm-tada 1s linear infinite both;
}
._mfm_jelly_ {
animation: mfm-rubberBand 1s linear infinite both;
}
._mfm_twitch_ {
animation: mfm-twitch 0.5s ease infinite;
}
._mfm_shake_ {
animation: mfm-shake 0.5s ease infinite;
}
._mfm_spin_ {
animation: mfm-spin 0.5s linear infinite;
}
._mfm_spin_[data-x] {
animation-name: mfm-spinX;
}
._mfm_spin_[data-y] {
animation-name: mfm-spinY;
}
._mfm_spin_[left] {
animation-direction: reverse;
}
._mfm_spin_[alternate] {
animation-direction: alternate;
}
._mfm_jump_ {
animation: mfm-jump 0.75s linear infinite;
}
._mfm_bounce_ {
animation: mfm-bounce 0.75s linear infinite;
transform-origin: center bottom;
}
._mfm_rainbow_ {
animation: mfm-rainbow 1s linear infinite;
}
}
}

View file

@ -64,6 +64,7 @@
</template> </template>
<script src="./status_content.js"></script> <script src="./status_content.js"></script>
<style lang="scss" src="./mfm.scss" />
<style lang="scss"> <style lang="scss">
.StatusContent { .StatusContent {
flex: 1; flex: 1;
@ -75,23 +76,6 @@
height: 50px; height: 50px;
} }
} }
&.mfm-hover:not(:hover) {
.mfm {
animation: none !important;
}
}
&.mfm-disabled {
span {
font-size: 100% !important;
}
.mfm {
animation: none !important;
}
.emoji {
height: 32px !important;
}
}
} }
.quote-inline, .quote-inline,

View file

@ -1,227 +0,0 @@
.mfm {
display: inline-block;
}
._mfm_tada_ {
font-size: 150%;
animation: mfm-tada 1s linear infinite both;
}
._mfm_jelly_ {
animation: mfm-jelly 1s linear infinite both;
}
._mfm_twitch_ {
animation: mfm-twitch 0.5s ease infinite;
}
._mfm_shake_ {
animation: mfm-shake 0.5s ease infinite;
}
._mfm_spin_ {
animation: mfm-spin 0.5s linear infinite;
}
._mfm_spin_[data-x] {
animation-name: mfm-spinX;
}
._mfm_spin_[data-y] {
animation-name: mfm-spinY;
}
._mfm_spin_[left] {
animation-direction: reverse;
}
._mfm_spin_[alternate] {
animation-direction: alternate;
}
._mfm_jump_ {
animation: mfm-jump 0.75s linear infinite;
}
._mfm_bounce_ {
animation: mfm-bounce 0.75s linear infinite;
transform-origin: center bottom;
}
._mfm_flip_[data-h][data-v] {
transform: scale(-1, -1);
}
._mfm_flip_[data-v] {
transform: scaleY(-1);
}
._mfm_flip_:not([data-v]) {
transform: scaleX(-1);
}
._mfm_x2_ {
font-size: 200%;
}
._mfm_x3_ {
font-size: 400%;
}
._mfm_x4_ {
font-size: 600%;
}
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s
}
._mfm_blur_:hover {
filter: blur(0px);
}
._mfm_rainbow_ {
animation: mfm-rainbow 1s linear infinite;
}
._mfm_rotate_ {
transform: rotate(90deg);
transform-origin: center center;
}
/* sparkle */
@keyframes mfm-tada {
from {
transform: scale3d(1, 1, 1);
}
10%,
20% {
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
}
30%,
50%,
70%,
90% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
}
40%,
60%,
80% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
}
to {
transform: scale3d(1, 1, 1);
}
}
@keyframes bounce {
0% {
transform: scaleX(0.9) scaleY(0.9);
}
19% {
transform: scaleX(1.1) scaleY(1.1);
}
48% {
transform: scaleX(0.95) scaleY(0.95);
}
100% {
transform: scaleX(1) scaleY(1);
}
}
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes mfm-spinX {
0% { transform: perspective(128px) rotateX(0deg); }
100% { transform: perspective(128px) rotateX(360deg); }
}
@keyframes mfm-spinY {
0% { transform: perspective(128px) rotateY(0deg); }
100% { transform: perspective(128px) rotateY(360deg); }
}
@keyframes mfm-jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes mfm-bounce {
0% { transform: translateY(0) scale(1, 1); }
25% { transform: translateY(-16px) scale(1, 1); }
50% { transform: translateY(0) scale(1, 1); }
75% { transform: translateY(0) scale(1.5, 0.75); }
100% { transform: translateY(0) scale(1, 1); }
}
@keyframes mfm-twitch {
0% { transform: translate(7px, -2px); }
5% { transform: translate(-3px, 1px); }
10% { transform: translate(-7px, -1px); }
15% { transform: translate(0, -1px); }
20% { transform: translate(-8px, 6px); }
25% { transform: translate(-4px, -3px); }
30% { transform: translate(-4px, -6px); }
35% { transform: translate(-8px, -8px); }
40% { transform: translate(4px, 6px); }
45% { transform: translate(-3px, 1px); }
50% { transform: translate(2px, -10px); }
55% { transform: translate(-7px, 0); }
60% { transform: translate(-2px, 4px); }
65% { transform: translate(3px, -8px); }
70% { transform: translate(6px, 7px); }
75% { transform: translate(-7px, -2px); }
80% { transform: translate(-7px, -8px); }
85% { transform: translate(9px, 3px); }
90% { transform: translate(-3px, -2px); }
95% { transform: translate(-10px, 2px); }
100% { transform: translate(-2px, -6px); }
}
@keyframes mfm-shake {
0% { transform: translate(-3px, -1px) rotate(-8deg); }
5% { transform: translate(0, -1px) rotate(-10deg); }
10% { transform: translate(1px, -3px) rotate(0deg); }
15% { transform: translate(1px, 1px) rotate(11deg); }
20% { transform: translate(-2px, 1px) rotate(1deg); }
25% { transform: translate(-1px, -2px) rotate(-2deg); }
30% { transform: translate(-1px, 2px) rotate(-3deg); }
35% { transform: translate(2px, 1px) rotate(6deg); }
40% { transform: translate(-2px, -3px) rotate(-9deg); }
45% { transform: translate(0, -1px) rotate(-12deg); }
50% { transform: translate(1px, 2px) rotate(10deg); }
55% { transform: translate(0, -3px) rotate(8deg); }
60% { transform: translate(1px, -1px) rotate(8deg); }
65% { transform: translate(0, -1px) rotate(-7deg); }
70% { transform: translate(-1px, -3px) rotate(6deg); }
75% { transform: translate(0, -2px) rotate(4deg); }
80% { transform: translate(-2px, -1px) rotate(3deg); }
85% { transform: translate(1px, -3px) rotate(-10deg); }
90% { transform: translate(1px, 0) rotate(3deg); }
95% { transform: translate(-2px, 0) rotate(-3deg); }
100% { transform: translate(2px, 1px) rotate(2deg); }
}
@keyframes mfm-jelly {
from { transform: scale3d(1, 1, 1); }
30% { transform: scale3d(1.25, 0.75, 1); }
40% { transform: scale3d(0.75, 1.25, 1); }
50% { transform: scale3d(1.15, 0.85, 1); }
65% { transform: scale3d(0.95, 1.05, 1); }
75% { transform: scale3d(1.05, 0.95, 1); }
to { transform: scale3d(1, 1, 1); }
}
@keyframes mfm-rainbow {
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
}

View file

@ -40,6 +40,64 @@ describe('RichContent', () => {
expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(html)) expect(wrapper.html().replace(/\n/g, '')).to.eql(compwrap(html))
}) })
it('it adds a # to the MFM color style value', () => {
const html_ok = '<span class="mfm-fg" data-mfm-color="fff">this text is not white</span>'
const expected_ok = '<span class="mfm-fg" data-mfm-color="fff" style="--mfm-color: #fff;">this text is not white</span>'
const wrapper_ok = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_ok
}
})
expect(wrapper_ok.html()).to.eql(compwrap(expected_ok))
})
it('does not allow injection through MFM data- attributes', () => {
const html_ok = '<span class="mfm-spin" data-mfm-speed="-0.2s">brrr</span>'
const expected_ok = '<span class="mfm-spin" data-mfm-speed="-0.2s" style="--mfm-speed: -0.2s;">brrr</span>'
const wrapper_ok = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_ok
}
})
const html_nok1 = '<span class="mfm-spin" data-mfm-speed="<">brrr</span>'
const wrapper_nok1 = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_nok1
}
})
const html_nok2 = '<span class="mfm-spin" data-mfm-speed="\\">brrr</span>'
const wrapper_nok2 = shallowMount(RichContent, {
global,
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html: html_nok2
}
})
expect(wrapper_ok.html()).to.eql(compwrap(expected_ok))
expect(wrapper_nok1.html()).to.eql(compwrap(html_nok1))
expect(wrapper_nok2.html()).to.eql(compwrap(html_nok2))
})
it('unescapes everything as needed', () => { it('unescapes everything as needed', () => {
const html = [ const html = [
p('Testing &#39;em all'), p('Testing &#39;em all'),