Meintenable MFM Renderer Component
MFMをレンダリングするコンポーネント 各文法ごとにコンポーネント化されているのでメンテナンスしやすい気がする。
Vue 3 と React(18・19)で使える。React 版は mfm-renderer/react から読み込む(React で使う)。
プレビュー: Vue 版・React 版・Vue と React を比べる
Installation
npm から入れるか、GitHub のタグを指定して入れる。GitHub から入れたときは、インストール時に prepare でビルドされる。v0.3.0 より後の版を GitHub から入れるには、ビルドに使う Vite 8 のために Node.js 20.19 以降が要る。
npm install mfm-renderer
npm install github:fruitriin/mfm-renderer#v0.4.0
main.js (main.ts) 的なところで createApp() しているところに .use(Mfm) を足す
import { createApp } from 'vue'
import App from './App.vue'
import Mfm from "mfm-renderer"
import "mfm-renderer/style.css"
createApp(App).use(Mfm).mount('#app')
vue は peerDependencies なので、利用側で入れておく。React 版だけを使うときは vue は要らない。
版ごとの使い方
.use(Mfm) でグローバル登録されるコンポーネントの名前が、v0.3.0 で変わった。MfmRenderer だけを使うなら、どちらの版でも書き方は同じである。
| v0.3.0 以降 | v0.2.x | |
|---|---|---|
| 文字列の MFM を描画する | MfmRenderer |
MfmRenderer |
| 解析済みの AST を描画する | MfmComponent |
MfmComponent |
| 構文ごとのコンポーネント | MfmBold・MfmLink・MfmFg など(Mfm 接頭辞付き) |
Bold・Link・Fg など(接頭辞なし) |
v0.3.0 以降
npm install github:fruitriin/mfm-renderer#v0.4.0
<script setup lang="ts">
import { parse } from 'mfm-js'
const tokens = parse('**太字** と $[x2 大きい文字]')
</script>
<template>
<!-- ふつうはこれだけでよい -->
<MfmRenderer text="Hello **mfm**" />
<!-- 解析済みの AST を描画する -->
<MfmComponent :tokens="tokens" />
<!-- 構文ごとのコンポーネントを直接使う。名前には Mfm 接頭辞が付く -->
<MfmBold :children="tokens[0].children" />
</template>
構文ごとのコンポーネントに接頭辞が付いたので、利用側の Link や Search などのコンポーネントと名前が衝突しない。
v0.2.x
npm install github:fruitriin/mfm-renderer#v0.2.1
<template>
<MfmRenderer text="Hello **mfm**" />
<MfmComponent :tokens="tokens" />
<!-- 構文ごとのコンポーネントは接頭辞なし -->
<Bold :children="tokens[0].children" />
</template>
v0.2.x では構文ごとのコンポーネントを Bold・Link・Search などの名前でグローバル登録する。利用側が同じ名前のコンポーネントを登録すると、登録した順番によってどちらかが上書きされる。
後述の MfmRenderer の props のうち text 以外(nyaize・advancedMfm など)は、v0.2.1 で追加した。v0.2.0 では使えない。
v0.2.x から v0.3.0 に上げるとき
MfmRendererとMfmComponentだけを使っているなら、書き換えは要らない- 構文ごとのコンポーネントを名前で使っているなら、名前の頭に
Mfmを付ける(<Bold>→<MfmBold>、<X2>→<MfmX2>) - 名前の衝突を避けるために、利用側のコンポーネントの名前を変えていた場合は、元に戻してよい
使い方
コンポーネントの中で MfmRenderer コンポーネントを使う
<MfmRenderer text="Hello **mfm**" @click-ev="(ev) => console.log(ev)" />
MfmRenderer は、Misskey の MFM コンポーネントと同じ次の props を受け取る。
| props | 既定値 | せつめい |
|---|---|---|
text |
(必須) | 描画する MFM |
plain |
false |
テキスト・絵文字・プレーンだけを解析し、改行を空白にする。絵文字は文字の大きさに合わせる(ユーザー名など向け) |
nowrap |
false |
1 行に収め、はみ出たら省略する |
rootScale |
1 |
文字の倍率の初期値。x2〜x4 と scale で掛け合わされ、2.5 以上になったカスタム絵文字には original-size クラスが付く |
nyaize |
false |
'respect' のとき、author.isCat が true ならにゃー化する(Misskey と同じく、true ではにゃー化しない)。引用とリンクの中はにゃー化しない |
author |
null |
投稿者。今は isCat だけを使う |
advancedMfm |
true |
false のときは x2〜x4・position・scale を効かせず、アニメーションも止める |
animatedMfm |
true |
false のときはアニメーションを止める。rainbow は静的な虹色、sparkle は中身だけで表示する |
emojiStyle |
'native' |
Unicode の絵文字の描き方。'twemoji' にすると Twemoji の画像で、Misskey の既定の見た目(高さ 1.25em)になる。カスタム絵文字(高さ 2em)との大きさの差も Misskey と同じになる |
twemojiBase |
jsDelivr の @discordapp/twemoji@16.0.1/dist/svg |
Twemoji の SVG を置いている場所。自分のサーバーに置くときに変える |
にゃー化には misskey-js の nyaize を使い、必要になったときだけ読み込む。読み込めない環境(UMD を script タグで使う場合など)では、にゃー化しない。
$[clickable.ev=名前 ...]が押されると、clickEvイベントで名前を渡す- メンションのアバターとカスタム絵文字は、
provide('domain', 'misskey.io')とprovide('emojis', { 名前: { url } })で渡す(ref や computed でもよい)。domainがないときは、アバターを取りに行かない - コードブロックのハイライトには
shikiを使う。読み込めない環境(UMD を script タグで使う場合など)では、ハイライトせずに表示する - 色は Misskey のテーマと同じ CSS 変数(
--MI_THEME-fg・--MI_THEME-accent・--MI_THEME-divider・--MI_THEME-mention・--MI_THEME-hashtag)を使う。なければ 2024.10 より前の名前(--fgなど)を使う
React で使う
react と react-dom を入れておく(18 か 19)。vue は要らない。
npm install github:fruitriin/mfm-renderer#v0.4.0 react react-dom
import { MfmRenderer } from 'mfm-renderer/react'
import 'mfm-renderer/react/style.css'
const emojis = { blobcat: { url: 'https://example.com/blobcat.png' } }
export function Note({ text }: { text: string }) {
return <MfmRenderer text={text} domain="misskey.io" emojis={emojis} onClickEv={(ev) => console.log(ev)} />
}
- Vue 版の
.use()のようなグローバル登録はしない。使う場所で import する mfm-renderer/reactは ES Modules だけで配布する(requireでは読み込めない)。UMD 版もない- サーバー側で描画(SSR)しても使える。ただし
$[unixtime]の表示は描画した時点の時計とロケールで決まるので、サーバーとブラウザで食い違うことがある。ハイドレーションの不一致の警告は出さない - props は Vue 版の
MfmRendererと同じ(plain・nowrap・rootScale・nyaize・author・advancedMfm・animatedMfm・emojiStyle・twemojiBase)。Vue 版の@click-evはonClickEvになる domainとemojisは props で渡す。Vue 版のprovide('domain')・provide('emojis')に当たる- たくさんの投稿で同じものを使うときは、
MfmProviderで外側から一度だけ渡せる。MfmRendererの props で渡したものが優先される
import { MfmProvider, MfmRenderer } from 'mfm-renderer/react'
<MfmProvider domain="misskey.io" emojis={emojis}>
{notes.map((note) => <MfmRenderer key={note.id} text={note.text} />)}
</MfmProvider>
- 構文ごとのコンポーネントは
componentsで差し替えられる。キーは接頭辞なしの名前(Link・Mention・X2など)である。$[foo ...]のように、新しい関数を足すこともできる
import type { MfmLink } from 'mfm-js'
import type { MfmSyntaxProps } from 'mfm-renderer/react'
function MyLink({ token }: MfmSyntaxProps<MfmLink['props']>) {
return <a href={token?.url} target="_blank" rel="noopener noreferrer">{token?.url}</a>
}
<MfmRenderer text={text} components={{ Link: MyLink }} />
components・emojisをインラインのオブジェクトで書くと、描画のたびに作り直され、中身がすべて描画し直される。たくさんの投稿に使うときは、モジュールに置くかuseMemoで作って渡す差し替えるコンポーネントには、位置(何番目のノードか)を key にして描画する。state を持つコンポーネントは、
textが変わると前の入力の state が残ることがあるので、tokenの値から作った key で state を作り直すMfmSyntaxPropsのtokenの型は、指定しないとanyになる。構文ごとに型が違うためである。上の例のように mfm-js の型(MfmLink['props']など)を指定すると型が付く構文ごとのコンポーネントや、解析済みの AST を描画する
MfmComponentも import できる(MfmBold・MfmX2など、Vue 版の登録名と同じ)
進み具合
構文 (Syntax)
| ファイル名 | 説明 | 進捗 |
|---|---|---|
| Blockcode.vue | 複数行のコード | Shiki でハイライト |
| Bold.vue | 太字 | 完成 |
| EmojiCode.vue | カスタム絵文字 | 完成 |
| InlineCode.vue | 一行のコード | 完成 |
| Mention.vue | メンション | 動作OK(最適化の余地あり) |
| Search.vue | 検索 | 完成 |
| Text.vue | テキスト | 完成 |
| Center.vue | 中央寄せ | 完成 |
| Fn.vue | なんか色々 | 後述 |
| Italic.vue | 斜体 | 完成 |
| Plain.vue | プレーン | 完成 |
| Small.vue | 小文字 | 完成 |
| UnicodeEmoji.vue | Unicode絵文字 | 暫定実装(Twemoji等への置き換え推奨) |
| Hashtag.vue | ハッシュタグ | 完成 |
| Link.vue | リンク構文 | 動作OK(silentオプション未実装) |
| Quote.vue | 引用 | 完成 |
| Strike.vue | 打ち消し線 | 完成 |
| Url.vue | URL | 完成 |
Fn
$[nantoka text] のやつ
色・スタイル
| コンポーネント | せつめい | 進み具合 |
|---|---|---|
| fg | 文字色 | 完成 |
| bg | 背景色 | 完成 |
| blur | ぼかし | 完成 |
| font | フォント | 完成 |
| border | ボーダー | 完成 |
変形
| コンポーネント | せつめい | 進み具合 |
|---|---|---|
| rotate | 回転 | 完成 |
| scale | 拡大縮小 | 完成 |
| position | 位置調整 | 完成 |
| flip | 反転 | 完成 |
| x2 | 2倍 | 完成 |
| x3 | 3倍 | 完成 |
| x4 | 4倍 | 完成 |
アニメーション
| コンポーネント | せつめい | 進み具合 |
|---|---|---|
| spin | 回転 | speed, delay, x/y, left/alternate対応済み |
| jump | ジャンプ | speed, delay対応済み |
| bounce | バウンス | speed, delay対応済み |
| shake | シェイク | speed, delay対応済み |
| twitch | けいれん | speed, delay対応済み |
| tada | タダー | speed, delay対応済み |
| jelly | ゼリー | speed, delay対応済み |
| rainbow | レインボー | speed, delay対応済み |
| sparkle | キラキラ | speed, delay対応済み(独自実装) |
その他
| コンポーネント | せつめい | 進み具合 |
|---|---|---|
| ruby | ルビ(ふりがな) | 完成 |
| unixtime | 日時と相対時間 | 完成 |
| clickable | クリックイベント | 完成(clickEv イベント) |
今後の改善候補
- Mention.vue の最適化(デバウンス/キャッシュ)
- Unicode 絵文字の置き換え(Twemoji 等)
- Link.vue の silent オプション実装