import { defineComponent, h } from "vue"; import * as mfm from "mfm-js"; import type { VNode } from "vue"; import MkUrl from "@/components/global/MkUrl.vue"; import MkLink from "@/components/MkLink.vue"; import MkMention from "@/components/MkMention.vue"; import MkEmoji from "@/components/global/MkEmoji.vue"; import { concat } from "@/scripts/array"; import MkFormula from "@/components/MkFormula.vue"; import MkCode from "@/components/MkCode.vue"; import MkGoogle from "@/components/MkGoogle.vue"; import MkSparkle from "@/components/MkSparkle.vue"; import MkA from "@/components/global/MkA.vue"; import { host } from "@/config"; import { MFM_TAGS } from "@/scripts/mfm-tags"; import { reducedMotion } from "@/scripts/reduced-motion"; export default defineComponent({ props: { text: { type: String, required: true, }, plain: { type: Boolean, default: false, }, nowrap: { type: Boolean, default: false, }, author: { type: Object, default: null, }, i: { type: Object, default: null, }, customEmojis: { required: false, }, isNote: { type: Boolean, default: true, }, }, render() { if (this.text == null || this.text === "") return; const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text, { fnNameList: MFM_TAGS, }); const validTime = (t: string | null | undefined) => { if (t == null) return null; return t.match(/^[0-9.]+s$/) ? t : null; }; const genEl = (ast: mfm.MfmNode[]) => concat( ast.map((token): VNode[] => { switch (token.type) { case "text": { const text = token.props.text.replace(/(\r\n|\n|\r)/g, "\n"); if (!this.plain) { const res = []; for (const t of text.split("\n")) { res.push(h("br")); res.push(t); } res.shift(); return res; } else { return [text.replace(/\n/g, " ")]; } } case "bold": { return [h("b", genEl(token.children))]; } case "strike": { return [h("del", genEl(token.children))]; } case "italic": { return h( "i", { style: "font-style: oblique;", }, genEl(token.children), ); } case "fn": { // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる let style; switch (token.props.name) { case "tada": { const speed = validTime(token.props.args.speed) || "1s"; style = `font-size: 150%;${ this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : "" }`; break; } case "jelly": { const speed = validTime(token.props.args.speed) || "1s"; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ""; break; } case "twitch": { const speed = validTime(token.props.args.speed) || "0.5s"; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-twitch ${speed} ease infinite;` : ""; break; } case "shake": { const speed = validTime(token.props.args.speed) || "0.5s"; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-shake ${speed} ease infinite;` : ""; break; } case "spin": { const direction = token.props.args.left ? "reverse" : token.props.args.alternate ? "alternate" : "normal"; const anime = token.props.args.x ? "mfm-spinX" : token.props.args.y ? "mfm-spinY" : "mfm-spin"; const speed = validTime(token.props.args.speed) || "1.5s"; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ""; break; } case "jump": { const speed = validTime(token.props.args.speed) || "0.75s"; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-jump ${speed} linear infinite;` : ""; break; } case "bounce": { const speed = validTime(token.props.args.speed) || "0.75s"; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ""; break; } case "rainbow": { const speed = validTime(token.props.args.speed) || "1s"; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rainbow ${speed} linear infinite;` : ""; break; } case "sparkle": { if (!(this.$store.state.animatedMfm || reducedMotion())) { return genEl(token.children); } return h(MkSparkle, {}, genEl(token.children)); } case "flip": { const transform = token.props.args.h && token.props.args.v ? "scale(-1, -1)" : token.props.args.v ? "scaleY(-1)" : "scaleX(-1)"; style = `transform: ${transform};`; break; } case "x2": { return h( "span", { class: "mfm-x2", }, genEl(token.children), ); } case "x3": { return h( "span", { class: "mfm-x3", }, genEl(token.children), ); } case "x4": { return h( "span", { class: "mfm-x4", }, genEl(token.children), ); } case "font": { const family = token.props.args.serif ? "serif" : token.props.args.monospace ? "monospace" : token.props.args.cursive ? "cursive" : token.props.args.fantasy ? "fantasy" : token.props.args.emoji ? "emoji" : token.props.args.math ? "math" : null; if (family) style = `font-family: ${family};`; break; } case "blur": { return h( "span", { class: "_mfm_blur_", }, genEl(token.children), ); } case "rotate": { const rotate = token.props.args.x ? "perspective(128px) rotateX" : token.props.args.y ? "perspective(128px) rotateY" : "rotate"; const degrees = parseInt(token.props.args.deg) || "90"; style = `transform: ${rotate}(${degrees}deg); transform-origin: center center;`; break; } } if (style == null) { return h("span", {}, [ "$[", token.props.name, " ", ...genEl(token.children), "]", ]); } else { return h( "span", { style: `display: inline-block;${style}`, }, genEl(token.children), ); } } case "small": { return [ h( "small", { style: "opacity: 0.7;", }, genEl(token.children), ), ]; } case "center": { return [ h( "div", { style: "text-align:center;", }, genEl(token.children), ), ]; } case "url": { return [ h(MkUrl, { key: Math.random(), url: token.props.url, rel: "nofollow noopener", }), ]; } case "link": { return [ h( MkLink, { key: Math.random(), url: token.props.url, rel: "nofollow noopener", }, genEl(token.children), ), ]; } case "mention": { return [ h(MkMention, { key: Math.random(), host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, username: token.props.username, }), ]; } case "hashtag": { return [ h( MkA, { key: Math.random(), to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent( token.props.hashtag, )}`, style: "color:var(--hashtag);", }, `#${token.props.hashtag}`, ), ]; } case "blockCode": { return [ h(MkCode, { key: Math.random(), code: token.props.code, lang: token.props.lang, }), ]; } case "inlineCode": { return [ h(MkCode, { key: Math.random(), code: token.props.code, inline: true, }), ]; } case "quote": { if (!this.nowrap) { return [ h( "div", { class: "quote", }, genEl(token.children), ), ]; } else { return [ h( "span", { class: "quote", }, genEl(token.children), ), ]; } } case "emojiCode": { return [ h(MkEmoji, { key: Math.random(), emoji: `:${token.props.name}:`, customEmojis: this.customEmojis, normal: this.plain, }), ]; } case "unicodeEmoji": { return [ h(MkEmoji, { key: Math.random(), emoji: token.props.emoji, customEmojis: this.customEmojis, normal: this.plain, }), ]; } case "mathInline": { return [ h(MkFormula, { key: Math.random(), formula: token.props.formula, block: false, }), ]; } case "mathBlock": { return [ h(MkFormula, { key: Math.random(), formula: token.props.formula, block: true, }), ]; } case "search": { return [ h(MkGoogle, { key: Math.random(), q: token.props.query, }), ]; } case "plain": { return [h("span", genEl(token.children))]; } default: { console.error("unrecognized ast type:", token.type); return []; } } }), ); // Parse ast to DOM return h("span", genEl(ast)); }, });