diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index fc17a529f..cdaebe2ff 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -313,7 +313,134 @@ export default {
const newChildren = Array.isArray(children)
? [...children].reverse().map(processItemReverse).reverse()
: children
- return {newChildren}
+ const attrs = getAttrs(opener)
+ const newAttrs = { ...attrs }
+ const fullAttrs = getAttrs(opener, () => true)
+ const classname = fullAttrs['class']
+ const isMFM = classname?.startsWith('mfm-')
+ if (isMFM) {
+ const mfmOperator = /^mfm-(\w+)$/.exec(classname)?.[1]
+ newAttrs['class'] = 'mfm'
+ newAttrs['data-mfm-operator'] = mfmOperator
+ switch(mfmOperator) {
+ case 'position': {
+ const x = fullAttrs['data-mfm-x'].replace(/\.+$/,'') || 0
+ const y = fullAttrs['data-mfm-y'].replace(/\.+$/,'') || 0
+ newAttrs.style = [
+ 'transform:',
+ `translate(calc(${x} * (var(--emoji-size) / 2)), `,
+ `calc(${y} * (var(--emoji-size) / 2)))`,
+ ].join(' ')
+ break
+ }
+ case 'scale': {
+ const x = fullAttrs['data-mfm-x'].replace(/\.+$/,'') || 1
+ const y = fullAttrs['data-mfm-y'].replace(/\.+$/,'') || 1
+ newAttrs.style = [
+ 'transform:',
+ `scale(${x}, ${y})`,
+ ].join(' ')
+ break
+ }
+ case 'rotate': {
+ const deg = fullAttrs['data-mfm-deg'] || 0
+ newAttrs.style = [
+ `transform: rotate(${deg}deg)`,
+ 'transform-origin: center',
+ ].join(';')
+ break
+ }
+ case 'bg': {
+ const color = fullAttrs['data-mfm-color'] || 0
+ newAttrs.style = [
+ `background-color: #${color}`,
+ ].join(' ')
+ break
+ }
+ case 'fg': {
+ const color = fullAttrs['data-mfm-color'] || 0
+ newAttrs.style = [
+ `color: #${color}`,
+ ].join(';')
+ break
+ }
+ case 'spin': {
+ const speed = fullAttrs['data-mfm-speed'] || '1s'
+ const delay = fullAttrs['data-mfm-delay'] || 0
+ const left = fullAttrs['data-mfm-left'] != null
+ const alternate = fullAttrs['data-mfm-alternate'] != null
+ const y = fullAttrs['data-mfm-y'] != null
+ const x = fullAttrs['data-mfm-x'] != null
+
+ const anim = [
+ x ? 'mfm-spinX' : null,
+ y ? 'mfm-spinY' : null,
+ 'mfm-spin'
+ ].filter(a => a)[0]
+
+ const direction = [
+ alternate ? 'alternate' : null,
+ left ? 'reverse' : null,
+ 'normal',
+ ].filter(a => a)[0]
+
+ newAttrs.style = [
+ `animation-name: ${anim}`,
+ `animation-duration: ${speed}`,
+ 'animation-iteration-count: infinite',
+ `animation-delay: ${delay}`,
+ `animation-direction: ${direction}`,
+ 'animation-fill-mode: none',
+ 'animation-timing-function: linear',
+ ].join(';')
+ break
+ }
+ case 'flip': {
+ newAttrs.style = 'transform: scaleX(-1)'
+ break
+ }
+ case 'border': {
+ const width = fullAttrs['data-mfm-width'] || '0'
+ const style = fullAttrs['data-mfm-style'] || 'solid'
+ const color = fullAttrs['data-mfm-color'] || 'transparent'
+ const radius = fullAttrs['data-mfm-radius'] || '0'
+ const noclip = fullAttrs['data-mfm-noclip'] || false
+
+ newAttrs.style = [
+ `border: ${width} ${style} ${color}`,
+ `border-radius: ${radius}`,
+ `overflow: ${noclip ? 'visible' : 'clip'}`
+ ].join(';')
+ break
+ }
+ case 'tada':
+ case 'jelly':
+ case 'twitch':
+ case 'shake':
+ case 'jump':
+ case 'bounce': {
+ const speed = fullAttrs['data-mfm-speed'] || '1s'
+ const delay = fullAttrs['data-mfm-delay'] || 0
+
+ const rules = [
+ `animation-name: mfm-${mfmOperator}`,
+ `animation-duration: ${speed}`,
+ 'animation-iteration-count: infinite',
+ `animation-delay: ${delay}`,
+ 'animation-direction: normal',
+ 'animation-fill-mode: none',
+ 'animation-timing-function: linear',
+ ].join(';')
+ newAttrs.style = rules
+ break
+ }
+ default:
+ console.log(mfmOperator, opener)
+ console.log(mfmOperator)
+ break
+ }
+ }
+ return {newChildren}
} else {
return
}
diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss
index 33effc623..f048e529e 100644
--- a/src/components/rich_content/rich_content.scss
+++ b/src/components/rich_content/rich_content.scss
@@ -1,6 +1,17 @@
.RichContent {
font-family: var(--font);
+ .mfm {
+ display: inline-block;
+ font-size: calc(var(--emoji-size) / 2);
+
+ .emoji {
+ /* Misskey's emoji width knows no bounds */
+ /* stylelint-disable-next-line declaration-no-important */
+ max-width: unset !important;
+ }
+ }
+
&.-faint {
color: var(--text);
/* stylelint-disable declaration-no-important */
@@ -106,3 +117,289 @@ a .RichContent {
/* stylelint-disable-next-line declaration-no-important */
color: var(--link) !important;
}
+
+@keyframes mfm-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes mfm-spinX {
+ 0% {
+ transform: perspective(8em) rotateX(0);
+ }
+
+ 100% {
+ transform: perspective(8em) rotateX(360deg);
+ }
+}
+
+@keyframes mfm-spinY {
+ 0% {
+ transform: perspective(8em) rotateY(0);
+ }
+
+ 100% {
+ transform: perspective(8em) rotateY(360deg);
+ }
+}
+
+@keyframes mfm-jump {
+ 0% {
+ transform: translateY(0)
+ }
+
+ 25% {
+ transform: translateY(-1em)
+ }
+
+ 50% {
+ transform: translateY(0)
+ }
+
+ 75% {
+ transform: translateY(-0.5em)
+ }
+
+ 100% {
+ transform: translateY(0)
+ }
+}
+
+@keyframes mfm-bounce {
+ 0% {
+ transform: translateY(0) scale(1)
+ }
+
+ 25% {
+ transform: translateY(-16px) scale(1)
+ }
+
+ 50% {
+ transform: translateY(0) scale(1)
+ }
+
+ 75% {
+ transform: translateY(0) scale(1.5,.75)
+ }
+
+ 100% {
+ transform: translateY(0) scale(1)
+ }
+}
+
+@keyframes mfm-twitch {
+ 0% {
+ transform: translate(7px,-2px)
+ }
+
+ 5% {
+ transform: translate(-3px,1px)
+ }
+
+ 10% {
+ transform: translate(-7px,-1px)
+ }
+
+ 15% {
+ transform: translateY(-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)
+ }
+
+ 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: translateY(-1px) rotate(-10deg)
+ }
+
+ 10% {
+ transform: translate(1px,-3px) rotate(0)
+ }
+
+ 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: translateY(-1px)rotate(-12deg)
+ }
+
+ 50% {
+ transform: translate(1px,2px)rotate(10deg)
+ }
+
+ 55% {
+ transform: translateY(-3px)rotate(8deg)
+ }
+
+ 60% {
+ transform: translate(1px,-1px)rotate(8deg)
+ }
+
+ 65% {
+ transform: translateY(-1px)rotate(-7deg)
+ }
+
+ 70% {
+ transform: translate(-1px,-3px)rotate(6deg)
+ }
+
+ 75% {
+ transform: translateY(-2px)rotate(4deg)
+ }
+
+ 80% {
+ transform: translate(-2px,-1px)rotate(3deg)
+ }
+
+ 85% {
+ transform: translate(1px,-3px)rotate(-10deg)
+ }
+
+ 90% {
+ transform: translate(1px)rotate(3deg)
+ }
+
+ 95% {
+ transform: translate(-2px)rotate(-3deg)
+ }
+
+ 100% {
+ transform: translate(2px,1px)rotate(2deg)
+ }
+}
+
+@keyframes mfm-rubberBand {
+ 0% {
+ transform: scale(1)
+ }
+
+ 30% {
+ transform: scale(1.25,.75)
+ }
+
+ 40% {
+ transform: scale(.75,1.25)
+ }
+
+ 50% {
+ transform: scale(1.15,.85)
+ }
+
+ 65% {
+ transform: scale(.95,1.05)
+ }
+
+ 75% {
+ transform: scale(1.05,.95)
+ }
+
+ 100% {
+ transform: scale(1)
+ }
+}
+
+@keyframes mfm-rainbow {
+ 0% {
+ filter: hue-rotate()contrast(150%)saturate(150%)
+ }
+
+ 100% {
+ filter: hue-rotate(360deg)contrast(150%)saturate(150%)
+ }
+}