iceshrimp/packages/client/src/components/mfm.ts

546 lines
14 KiB
TypeScript
Raw Normal View History

2023-01-13 13:40:33 +09:00
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 { reducedMotion } from "@/scripts/reduced-motion";
2023-04-07 14:28:44 +09:00
import { defaultStore } from "@/store";
Migrate to Vue3 (#6587) * Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
2020-10-17 20:12:00 +09:00
export default defineComponent({
props: {
text: {
type: String,
2022-06-23 21:46:15 +09:00
required: true,
},
2019-07-06 00:46:00 +09:00
plain: {
type: Boolean,
2022-06-23 21:46:15 +09:00
default: false,
},
2019-07-06 00:46:00 +09:00
nowrap: {
type: Boolean,
2022-06-23 21:46:15 +09:00
default: false,
},
author: {
type: Object,
2022-06-23 21:46:15 +09:00
default: null,
},
i: {
type: Object,
2022-06-23 21:46:15 +09:00
default: null,
},
customEmojis: {
required: false,
},
isNote: {
type: Boolean,
2022-06-23 21:46:15 +09:00
default: true,
},
},
Migrate to Vue3 (#6587) * Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
2020-10-17 20:12:00 +09:00
render() {
2023-01-13 13:40:33 +09:00
if (this.text == null || this.text === "") return;
const isPlain = this.plain;
const ast = (isPlain ? mfm.parseSimple : mfm.parse)(this.text);
2021-01-03 12:51:50 +09:00
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
const validNumber = (n: string | null | undefined) => {
if (n == null) return null;
const parsed = parseFloat(n);
return !isNaN(parsed) && isFinite(parsed) && parsed > 0;
};
2023-06-06 04:28:42 +09:00
// const validEase = (e: string | null | undefined) => {
// if (e == null) return null;
// return e.match(/(steps)?\(-?[0-9.]+,-?[0-9.]+,-?[0-9.]+,-?[0-9.]+\)/)
2023-06-06 04:28:42 +09:00
// ? (e.startsWith("steps") ? e : "cubic-bezier" + e)
// : null
// }
2021-01-03 12:51:50 +09:00
2023-01-13 13:40:33 +09:00
const genEl = (ast: mfm.MfmNode[]) =>
concat(
ast.map((token, index): VNode[] => {
2023-01-13 13:40:33 +09:00
switch (token.type) {
case "text": {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, "\n");
2023-01-13 13:40:33 +09:00
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, " ")];
}
2021-04-15 12:37:58 +09:00
}
2018-08-03 23:27:37 +09:00
2023-01-13 13:40:33 +09:00
case "bold": {
return [h("b", genEl(token.children))];
}
2023-01-13 13:40:33 +09:00
case "strike": {
return [h("del", genEl(token.children))];
2020-11-08 17:08:51 +09:00
}
2023-01-13 13:40:33 +09:00
case "italic": {
return h(
"i",
{
style: "font-style: oblique;",
},
genEl(token.children),
);
2022-11-30 11:40:12 +09:00
}
2023-01-13 13:40:33 +09:00
case "fn": {
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
let style;
switch (token.props.name) {
case "tada": {
const speed = validTime(token.props.args.speed) || "1s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
2023-06-06 04:28:42 +09:00
// const ease = validEase(token.props.args.ease) || "linear";
style = `font-size: 150%; animation: tada ${speed} ${delay} linear ${loop} both;`;
2023-01-13 13:40:33 +09:00
break;
}
case "jelly": {
const speed = validTime(token.props.args.speed) || "1s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: mfm-rubberBand ${speed} ${delay} linear ${loop} both;`;
2023-01-13 13:40:33 +09:00
break;
}
case "twitch": {
const speed = validTime(token.props.args.speed) || "0.5s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: mfm-twitch ${speed} ${delay} ease ${loop};`;
2023-01-13 13:40:33 +09:00
break;
}
case "shake": {
const speed = validTime(token.props.args.speed) || "0.5s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: mfm-shake ${speed} ${delay} ease ${loop};`;
2023-01-13 13:40:33 +09:00
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";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: ${anime} ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`;
2023-01-13 13:40:33 +09:00
break;
}
case "jump": {
const speed = validTime(token.props.args.speed) || "0.75s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: mfm-jump ${speed} ${delay} linear ${loop};`;
2023-01-13 13:40:33 +09:00
break;
}
case "bounce": {
const speed = validTime(token.props.args.speed) || "0.75s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: mfm-bounce ${speed} ${delay} linear ${loop}; transform-origin: center bottom;`;
2023-01-13 13:40:33 +09:00
break;
}
case "rainbow": {
const speed = validTime(token.props.args.speed) || "1s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: mfm-rainbow ${speed} ${delay} linear ${loop};`;
2023-01-13 13:40:33 +09:00
break;
}
case "sparkle": {
if (reducedMotion()) {
2023-01-13 13:40:33 +09:00
return genEl(token.children);
}
return h(MkSparkle, {}, genEl(token.children));
}
2023-05-23 09:26:27 +09:00
case "fade": {
const direction = token.props.args.out
? "alternate-reverse"
: "alternate";
const speed = validTime(token.props.args.speed) || "1.5s";
const delay = validTime(token.props.args.delay) || "0s";
const loop = validNumber(token.props.args.loop) || "infinite";
style = `animation: mfm-fade ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`;
2023-05-23 09:26:27 +09:00
break;
}
2023-01-13 13:40:33 +09:00
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",
{
2023-04-07 14:28:44 +09:00
class: "_blur_text",
2023-01-13 13:40:33 +09:00
},
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;
}
case "position": {
const x = parseFloat(token.props.args.x ?? "0");
const y = parseFloat(token.props.args.y ?? "0");
style = `transform: translateX(${x}em) translateY(${y}em);`;
break;
}
2023-05-28 14:19:57 +09:00
case "crop": {
2023-05-29 02:31:06 +09:00
const top = parseFloat(token.props.args.top ?? "0");
const right = parseFloat(token.props.args.right ?? "0");
const bottom = parseFloat(token.props.args.bottom ?? "0");
const left = parseFloat(token.props.args.left ?? "0");
style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`;
2023-05-28 14:19:57 +09:00
break;
}
case "scale": {
const x = Math.min(parseFloat(token.props.args.x ?? "1"), 5);
const y = Math.min(parseFloat(token.props.args.y ?? "1"), 5);
style = `transform: scale(${x}, ${y});`;
break;
}
case "fg": {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
style = `color: #${color};`;
break;
}
case "bg": {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
style = `background-color: #${color};`;
break;
}
2023-01-13 13:40:33 +09:00
}
if (style == null) {
return h("span", {}, [
"$[",
token.props.name,
" ",
...genEl(token.children),
"]",
]);
} else {
return h(
"span",
{
style: `display: inline-block;${style}`,
},
genEl(token.children),
);
2022-11-30 11:40:12 +09:00
}
}
2023-01-13 13:40:33 +09:00
case "small": {
return [
h(
"small",
{
style: "opacity: 0.7;",
},
genEl(token.children),
),
];
}
2018-12-05 20:11:54 +09:00
2023-01-13 13:40:33 +09:00
case "center": {
return [
h(
"div",
{
style: "text-align:center;",
},
genEl(token.children),
),
];
}
2018-11-25 13:36:40 +09:00
2023-01-13 13:40:33 +09:00
case "url": {
return [
h(MkUrl, {
key: Math.random(),
url: token.props.url,
rel: "nofollow noopener",
}),
];
}
2023-01-13 13:40:33 +09:00
case "link": {
return [
h(
MkLink,
{
key: Math.random(),
url: token.props.url,
rel: "nofollow noopener",
},
genEl(token.children),
),
];
}
2023-01-13 13:40:33 +09:00
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,
}),
];
}
2023-01-13 13:40:33 +09:00
case "hashtag": {
return [
h(
MkA,
{
key: Math.random(),
2023-04-20 13:00:01 +09:00
to: `/tags/${encodeURIComponent(token.props.hashtag)}`,
2023-01-13 13:40:33 +09:00
style: "color:var(--hashtag);",
},
`#${token.props.hashtag}`,
),
];
}
2023-01-13 13:40:33 +09:00
case "blockCode": {
return [
h(MkCode, {
key: Math.random(),
code: token.props.code,
lang: token.props.lang,
}),
];
}
2023-01-13 13:40:33 +09:00
case "inlineCode": {
return [
h(MkCode, {
key: Math.random(),
code: token.props.code,
inline: true,
}),
];
}
2023-01-13 13:40:33 +09:00
case "quote": {
if (!this.nowrap) {
2023-02-11 08:41:19 +09:00
return [h("blockquote", genEl(token.children))];
2023-01-13 13:40:33 +09:00
} else {
return [
h(
"span",
{
class: "quote",
},
genEl(token.children),
),
];
}
}
2023-01-13 13:40:33 +09:00
case "emojiCode": {
return [
h(MkEmoji, {
key: Math.random(),
emoji: `:${token.props.name}:`,
customEmojis: this.customEmojis,
normal: this.plain,
}),
];
}
2023-01-13 13:40:33 +09:00
case "unicodeEmoji": {
return [
h(MkEmoji, {
key: Math.random(),
emoji: token.props.emoji,
customEmojis: this.customEmojis,
normal: this.plain,
}),
];
}
2023-01-13 13:40:33 +09:00
case "mathInline": {
return [
h(MkFormula, {
key: Math.random(),
formula: token.props.formula,
block: false,
}),
];
}
2023-01-13 13:40:33 +09:00
case "mathBlock": {
return [
h(MkFormula, {
key: Math.random(),
formula: token.props.formula,
block: true,
}),
];
}
2018-11-16 17:03:52 +09:00
2023-01-13 13:40:33 +09:00
case "search": {
// Disable "search" keyword
// (see the issue #9816 on Codeberg)
if (token.props.content.slice(-6).toLowerCase() === "search") {
const sentinel = "#";
let ast2 = (isPlain ? mfm.parseSimple : mfm.parse)(
token.props.content.slice(0, -6) + sentinel,
);
if (
ast2[ast2.length - 1].type === "text" &&
ast2[ast2.length - 1].props.text.endsWith(sentinel)
) {
ast2[ast2.length - 1].props.text = ast2[
ast2.length - 1
].props.text.slice(0, -1);
} else {
// I don't think this scope is reachable
console.warn(
"Something went wrong while parsing MFM. Please send a bug report, if possible.",
);
}
let prefix = "\n";
if (
index === 0 ||
[
"blockCode",
"center",
"mathBlock",
"quote",
"search",
].includes(ast[index - 1].type)
) {
prefix = "";
}
return [
prefix,
...genEl(ast2),
`${token.props.content.slice(-6)}\n`,
];
}
2023-01-13 13:40:33 +09:00
return [
h(MkGoogle, {
key: Math.random(),
q: token.props.query,
}),
];
}
2018-04-21 18:59:16 +09:00
2023-01-13 13:40:33 +09:00
case "plain": {
return [h("span", genEl(token.children))];
}
2022-07-12 12:03:38 +09:00
2023-01-13 13:40:33 +09:00
default: {
console.error("unrecognized ast type:", token.type);
2018-09-12 03:32:47 +09:00
2023-01-13 13:40:33 +09:00
return [];
}
}
}),
);
// Parse ast to DOM
2023-01-13 13:40:33 +09:00
return h("span", genEl(ast));
2022-06-23 21:46:15 +09:00
},
});