Frontend: Switched to MMM rendering where possible
ci/woodpecker/push/ociImagePush Pipeline was successful
Details
ci/woodpecker/push/ociImagePush Pipeline was successful
Details
This commit is contained in:
parent
7ad46191ec
commit
76c4d0267f
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<MkLink class="mention" :url="url" @click.stop>
|
||||
<span class="icon">matrix /</span>
|
||||
<span class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
<span class="host">:{{ toUnicode(host) }}</span>
|
||||
</span>
|
||||
</MkLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toUnicode } from "punycode";
|
||||
import MkLink from "@/components/MkLink.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
username: string;
|
||||
host: string;
|
||||
}>();
|
||||
|
||||
const url = computed(
|
||||
() => `https://matrix.to/#/@${props.username}:${props.host}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mention {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 2px 8px 2px 2px;
|
||||
margin-block: 2px;
|
||||
border-radius: 999px;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--mention);
|
||||
isolation: isolate;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: var(--mention);
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin: 0 0.2em 0 0.35em;
|
||||
border-radius: 100%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .main > .host {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -60,6 +60,7 @@
|
|||
>
|
||||
<Mfm
|
||||
v-if="user.description"
|
||||
:mm="user.description_mm"
|
||||
:text="user.description"
|
||||
:author="user"
|
||||
:i="$i"
|
||||
|
@ -86,6 +87,7 @@
|
|||
</dt>
|
||||
<dd class="value">
|
||||
<Mfm
|
||||
:mm="field.value_mm"
|
||||
:text="field.value"
|
||||
:author="user"
|
||||
:i="$i"
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
<Mfm
|
||||
v-if="note.cw != ''"
|
||||
class="text"
|
||||
:mm="magMaybeProperty(note, 'cw_mm')"
|
||||
:text="note.cw"
|
||||
:author="note.user"
|
||||
:i="$i"
|
||||
|
@ -126,6 +127,7 @@
|
|||
</template>
|
||||
<Mfm
|
||||
v-if="note.text"
|
||||
:mm="magMaybeProperty(note, 'text_mm')"
|
||||
:text="note.text"
|
||||
:author="note.user"
|
||||
:i="$i"
|
||||
|
@ -236,7 +238,7 @@ import { extractMfmWithAnimation } from "@/scripts/extract-mfm";
|
|||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
import { $i } from "@/account";
|
||||
import { magTransProperty } from "@/scripts-mag/mag-util";
|
||||
import { magMaybeProperty, magTransProperty } from "@/scripts-mag/mag-util";
|
||||
import { packed } from "magnetar-common";
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<MfmCore
|
||||
:mm="mm"
|
||||
:text="text"
|
||||
:plain="plain"
|
||||
:nowrap="nowrap"
|
||||
|
@ -9,19 +10,23 @@
|
|||
class="mfm-object"
|
||||
:class="{
|
||||
nowrap,
|
||||
advancedMfm: defaultStore.state.advancedMfm || advancedMfm,
|
||||
advancedMfm: advancedMfmStore || advancedMfm,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MfmCore from "@/components/mfm";
|
||||
import { defaultStore } from "@/store";
|
||||
import MfmCore from "@/components/mfm.vue";
|
||||
import * as Misskey from "calckey-js";
|
||||
import { packed } from "magnetar-common";
|
||||
import { defaultStore } from "@/store";
|
||||
import { ref } from "vue";
|
||||
|
||||
const advancedMfmStore = ref(defaultStore.state.advancedMfm);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mm?: string;
|
||||
text: string;
|
||||
plain?: boolean;
|
||||
nowrap?: boolean;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<Mfm
|
||||
:class="$style.root"
|
||||
:mm="magMaybeProperty(user, 'display_name_mm')"
|
||||
:text="magTransUsername(user)"
|
||||
:plain="true"
|
||||
:nowrap="nowrap"
|
||||
|
@ -11,7 +12,7 @@
|
|||
<script lang="ts" setup>
|
||||
import * as misskey from "calckey-js";
|
||||
import { packed } from "magnetar-common";
|
||||
import { magTransUsername } from "@/scripts-mag/mag-util";
|
||||
import { magMaybeProperty, magTransUsername } from "@/scripts-mag/mag-util";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
|
@ -1,654 +0,0 @@
|
|||
import type { VNode } from "vue";
|
||||
import { defineComponent, h } from "vue";
|
||||
import * as mfm from "mfm-js";
|
||||
import MkUrl from "@/components/global/MkUrl.vue";
|
||||
import MkLink from "@/components/MkLink.vue";
|
||||
import MagMention from "@/components/MagMention.vue";
|
||||
import { concat } from "@/scripts/array";
|
||||
import MkFormula from "@/components/MkFormula.vue";
|
||||
import MkCode from "@/components/MkCode.vue";
|
||||
import MkSparkle from "@/components/MkSparkle.vue";
|
||||
import MkA from "@/components/global/MkA.vue";
|
||||
import { host } from "@/config";
|
||||
import { reducedMotion } from "@/scripts/reduced-motion";
|
||||
import MagEmoji from "@/components/global/MagEmoji.vue";
|
||||
import {
|
||||
magConvertReaction,
|
||||
magIsMissingEmoji,
|
||||
magReactionEquals,
|
||||
magTransProperty,
|
||||
} from "@/scripts-mag/mag-util";
|
||||
|
||||
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 isPlain = this.plain;
|
||||
|
||||
const ast = (isPlain ? mfm.parseSimple : mfm.parse)(this.text);
|
||||
|
||||
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;
|
||||
};
|
||||
// const validEase = (e: string | null | undefined) => {
|
||||
// if (e == null) return null;
|
||||
// return e.match(/(steps)?\(-?[0-9.]+,-?[0-9.]+,-?[0-9.]+,-?[0-9.]+\)/)
|
||||
// ? (e.startsWith("steps") ? e : "cubic-bezier" + e)
|
||||
// : null
|
||||
// }
|
||||
|
||||
const genEl = (ast: mfm.MfmNode[]) =>
|
||||
concat(
|
||||
ast.map((token, index): 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: string;
|
||||
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";
|
||||
// const ease = validEase(token.props.args.ease) || "linear";
|
||||
style = `font-size: 150%; animation: tada ${speed} ${delay} linear ${loop} both;`;
|
||||
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;`;
|
||||
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};`;
|
||||
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};`;
|
||||
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};`;
|
||||
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};`;
|
||||
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;`;
|
||||
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};`;
|
||||
break;
|
||||
}
|
||||
case "sparkle": {
|
||||
if (reducedMotion()) {
|
||||
return genEl(token.children);
|
||||
}
|
||||
return h(
|
||||
MkSparkle,
|
||||
{},
|
||||
genEl(token.children)
|
||||
);
|
||||
}
|
||||
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};`;
|
||||
break;
|
||||
}
|
||||
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: "_blur_text",
|
||||
},
|
||||
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;
|
||||
}
|
||||
case "crop": {
|
||||
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}%);`;
|
||||
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;
|
||||
}
|
||||
case "small": {
|
||||
return h(
|
||||
"small",
|
||||
{
|
||||
style: "opacity: 0.7;",
|
||||
},
|
||||
genEl(token.children)
|
||||
);
|
||||
}
|
||||
case "center": {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
style: "text-align: center;",
|
||||
},
|
||||
genEl(token.children)
|
||||
);
|
||||
}
|
||||
}
|
||||
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(MagMention, {
|
||||
key: Math.random(),
|
||||
username: token.props.username,
|
||||
host:
|
||||
(token.props.host == null &&
|
||||
this.author &&
|
||||
this.author.host != null
|
||||
? this.author.host
|
||||
: token.props.host) || host,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
case "hashtag": {
|
||||
return [
|
||||
h(
|
||||
MkA,
|
||||
{
|
||||
key: Math.random(),
|
||||
to: `/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("blockquote", genEl(token.children))];
|
||||
} else {
|
||||
return [
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
class: "quote",
|
||||
},
|
||||
genEl(token.children)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
case "emojiCode": {
|
||||
const shortcode = `:${token.props.name}:`;
|
||||
const emoji = magConvertReaction(
|
||||
shortcode,
|
||||
(name, host) =>
|
||||
this.customEmojis.find((e) =>
|
||||
magReactionEquals(
|
||||
magConvertReaction(
|
||||
`:${magTransProperty(
|
||||
e,
|
||||
"shortcode",
|
||||
"name"
|
||||
)}:`
|
||||
),
|
||||
{ name, host, url: null! }
|
||||
)
|
||||
)?.url ?? null
|
||||
);
|
||||
|
||||
if (magIsMissingEmoji(emoji)) {
|
||||
return [shortcode];
|
||||
}
|
||||
|
||||
return [
|
||||
h(MagEmoji, {
|
||||
key: Math.random(),
|
||||
emoji,
|
||||
normal: this.plain,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
case "unicodeEmoji": {
|
||||
return [
|
||||
h(MagEmoji, {
|
||||
key: Math.random(),
|
||||
emoji: token.props.emoji,
|
||||
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": {
|
||||
const sentinel = "#";
|
||||
let ast2 = (isPlain ? mfm.parseSimple : mfm.parse)(
|
||||
token.props.content + 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);
|
||||
}
|
||||
|
||||
let prefix = "\n";
|
||||
if (
|
||||
index === 0 ||
|
||||
[
|
||||
"blockCode",
|
||||
"center",
|
||||
"mathBlock",
|
||||
"quote",
|
||||
"search",
|
||||
].includes(ast[index - 1].type)
|
||||
) {
|
||||
prefix = "";
|
||||
}
|
||||
|
||||
return [prefix, ...genEl(ast2)];
|
||||
}
|
||||
|
||||
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));
|
||||
},
|
||||
});
|
|
@ -0,0 +1,697 @@
|
|||
<template v-slot="{ mfmTree }">
|
||||
<span>
|
||||
<component :is="mfmTree" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { h, shallowRef, VNodeArrayChildren, VNodeChild, watch } from "vue";
|
||||
import * as mfm from "mfm-js";
|
||||
import MkUrl from "@/components/global/MkUrl.vue";
|
||||
import MkLink from "@/components/MkLink.vue";
|
||||
import MagMention from "@/components/MagMention.vue";
|
||||
import MagMatrixMention from "@/components/MagMatrixMention.vue";
|
||||
import { concat } from "@/scripts/array";
|
||||
import MkFormula from "@/components/MkFormula.vue";
|
||||
import MkCode from "@/components/MkCode.vue";
|
||||
import MkSparkle from "@/components/MkSparkle.vue";
|
||||
import MkA from "@/components/global/MkA.vue";
|
||||
import { host } from "@/config";
|
||||
import { reducedMotion } from "@/scripts/reduced-motion";
|
||||
import MagEmoji from "@/components/global/MagEmoji.vue";
|
||||
import {
|
||||
magConvertReaction,
|
||||
magIsMissingEmoji,
|
||||
magReactionEquals,
|
||||
magTransProperty,
|
||||
} from "@/scripts-mag/mag-util";
|
||||
import * as Misskey from "calckey-js";
|
||||
import { packed } from "magnetar-common";
|
||||
import {
|
||||
magnetarMarkdownToMfm,
|
||||
MagnetarParseError,
|
||||
MagNode,
|
||||
parseMagnetarMarkdownXml,
|
||||
} from "@/scripts-mag/mmm-util";
|
||||
import { Result } from "@/types/result";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mm?: string;
|
||||
text: string;
|
||||
plain?: boolean;
|
||||
nowrap?: boolean;
|
||||
author?: any;
|
||||
customEmojis?: (packed.PackEmojiBase | Misskey.entities.CustomEmoji)[];
|
||||
isNote?: boolean;
|
||||
}>(),
|
||||
{
|
||||
plain: false,
|
||||
nowrap: false,
|
||||
author: null,
|
||||
isNote: true,
|
||||
}
|
||||
);
|
||||
|
||||
function render() {
|
||||
let ast: MagNode[];
|
||||
|
||||
let result: Result<MagNode[], MagnetarParseError> | null = null;
|
||||
if (props.mm) {
|
||||
result = parseMagnetarMarkdownXml(props.mm).flatMap(
|
||||
magnetarMarkdownToMfm
|
||||
);
|
||||
}
|
||||
|
||||
if (result && result.isOk()) {
|
||||
ast = result.unwrap();
|
||||
} else if (!props.text) {
|
||||
return h("span");
|
||||
} else {
|
||||
const isPlain = props.plain;
|
||||
ast = (isPlain ? mfm.parseSimple : mfm.parse)(props.text);
|
||||
}
|
||||
|
||||
const validTime = (t: string | boolean | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
if (typeof t === "boolean") return null;
|
||||
return t.match(/^[0-9.]+s$/) ? t : null;
|
||||
};
|
||||
|
||||
const validNumber = (n: string | boolean | null | undefined) => {
|
||||
if (n == null) return null;
|
||||
if (typeof n === "boolean") return null;
|
||||
const parsed = parseFloat(n);
|
||||
return !isNaN(parsed) && isFinite(parsed) && parsed > 0;
|
||||
};
|
||||
// const validEase = (e: string | null | undefined) => {
|
||||
// if (e == null) return null;
|
||||
// return e.match(/(steps)?\(-?[0-9.]+,-?[0-9.]+,-?[0-9.]+,-?[0-9.]+\)/)
|
||||
// ? (e.startsWith("steps") ? e : "cubic-bezier" + e)
|
||||
// : null
|
||||
// }
|
||||
|
||||
const genEl = (ast: MagNode[]) =>
|
||||
concat(
|
||||
ast.map((token, index): VNodeChild[] => {
|
||||
switch (token.type) {
|
||||
case "text": {
|
||||
const text = token.props.text.replace(
|
||||
/(\r\n|\n|\r)/g,
|
||||
"\n"
|
||||
);
|
||||
|
||||
if (!props.plain) {
|
||||
const res = [] as VNodeArrayChildren;
|
||||
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": {
|
||||
let style: string | null = null;
|
||||
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";
|
||||
// const ease = validEase(token.props.args.ease) || "linear";
|
||||
style = `font-size: 150%; animation: tada ${speed} ${delay} linear ${loop} both;`;
|
||||
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;`;
|
||||
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};`;
|
||||
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};`;
|
||||
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};`;
|
||||
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};`;
|
||||
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;`;
|
||||
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};`;
|
||||
break;
|
||||
}
|
||||
case "sparkle": {
|
||||
if (reducedMotion()) {
|
||||
return genEl(token.children);
|
||||
}
|
||||
return [
|
||||
h(MkSparkle, {}, genEl(token.children)),
|
||||
];
|
||||
}
|
||||
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};`;
|
||||
break;
|
||||
}
|
||||
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: "_blur_text",
|
||||
},
|
||||
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;
|
||||
}
|
||||
case "crop": {
|
||||
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}%);`;
|
||||
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;
|
||||
}
|
||||
case "small": {
|
||||
return [
|
||||
h(
|
||||
"small",
|
||||
{
|
||||
style: "opacity: 0.7;",
|
||||
},
|
||||
genEl(token.children)
|
||||
),
|
||||
];
|
||||
}
|
||||
case "center": {
|
||||
return [
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
style: "text-align: center;",
|
||||
},
|
||||
genEl(token.children)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
if (style) {
|
||||
return [
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
style: `display: inline-block;${style}`,
|
||||
},
|
||||
genEl(token.children)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
h("span", {}, [
|
||||
"$[",
|
||||
token.props.name,
|
||||
" ",
|
||||
...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(MagMention, {
|
||||
key: Math.random(),
|
||||
username: token.props.username,
|
||||
host:
|
||||
(token.props.host == null &&
|
||||
props.author &&
|
||||
props.author.host != null
|
||||
? props.author.host
|
||||
: token.props.host) || host,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
case "matrixMention": {
|
||||
return [
|
||||
h(MagMatrixMention, {
|
||||
key: Math.random(),
|
||||
username: token.props.username,
|
||||
host: token.props.host,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
case "hashtag": {
|
||||
return [
|
||||
h(
|
||||
MkA,
|
||||
{
|
||||
key: Math.random(),
|
||||
to: `/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 (!props.nowrap) {
|
||||
return [h("blockquote", genEl(token.children))];
|
||||
} else {
|
||||
return [
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
class: "quote",
|
||||
},
|
||||
genEl(token.children)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
case "emojiCode": {
|
||||
const shortcode = `:${token.props.name}:`;
|
||||
const emoji = magConvertReaction(
|
||||
shortcode,
|
||||
(name, host) =>
|
||||
props.customEmojis?.find((e) =>
|
||||
magReactionEquals(
|
||||
magConvertReaction(
|
||||
`:${magTransProperty(
|
||||
e,
|
||||
"shortcode",
|
||||
"name"
|
||||
)}:`
|
||||
),
|
||||
{ name, host, url: null! }
|
||||
)
|
||||
)?.url ?? null!
|
||||
);
|
||||
|
||||
if (magIsMissingEmoji(emoji)) {
|
||||
return [shortcode];
|
||||
}
|
||||
|
||||
return [
|
||||
h(MagEmoji, {
|
||||
key: Math.random(),
|
||||
emoji,
|
||||
normal: props.plain,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
case "unicodeEmoji": {
|
||||
return [
|
||||
h(MagEmoji, {
|
||||
key: Math.random(),
|
||||
emoji: token.props.emoji,
|
||||
normal: props.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": {
|
||||
const sentinel = "#";
|
||||
let ast2 = (props.plain ? mfm.parseSimple : mfm.parse)(
|
||||
token.props.content + sentinel
|
||||
);
|
||||
|
||||
const lastNode = ast2[ast2.length - 1];
|
||||
if (
|
||||
lastNode.type === "text" &&
|
||||
(
|
||||
lastNode.props as
|
||||
| mfm.MfmText["props"]
|
||||
| undefined
|
||||
)?.text?.endsWith(sentinel)
|
||||
) {
|
||||
lastNode.props.text = lastNode.props.text.slice(
|
||||
0,
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
let prefix = "\n";
|
||||
if (
|
||||
index === 0 ||
|
||||
[
|
||||
"blockCode",
|
||||
"center",
|
||||
"mathBlock",
|
||||
"quote",
|
||||
"search",
|
||||
].includes(ast[index - 1].type)
|
||||
) {
|
||||
prefix = "";
|
||||
}
|
||||
|
||||
return [prefix, ...genEl(ast2)];
|
||||
}
|
||||
|
||||
case "plain": {
|
||||
return [h("span", genEl(token.children))];
|
||||
}
|
||||
|
||||
default: {
|
||||
console.error(
|
||||
"unrecognized ast type:",
|
||||
(token as any)?.type
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return h("span", genEl(ast));
|
||||
}
|
||||
|
||||
const mfmTree = shallowRef<VNodeChild>(props.text);
|
||||
watch(
|
||||
() => props.text,
|
||||
() => {
|
||||
mfmTree.value = render();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -12,6 +12,7 @@ import "@phosphor-icons/web/fill";
|
|||
//#region account indexedDB migration
|
||||
import { set } from "@/scripts/idb-proxy";
|
||||
import {
|
||||
App,
|
||||
computed,
|
||||
createApp,
|
||||
defineAsyncComponent,
|
||||
|
@ -42,8 +43,6 @@ import { reactionPicker } from "@/scripts/reaction-picker";
|
|||
import { getUrlWithoutLoginId } from "@/scripts/login-id";
|
||||
import { getAccountFromId } from "@/scripts/get-account-from-id";
|
||||
|
||||
import { App } from "vue";
|
||||
|
||||
import Mfm from "./components/global/MkMisskeyFlavoredMarkdown.vue";
|
||||
import MkA from "./components/global/MkA.vue";
|
||||
import MkAcct from "./components/global/MkAcct.vue";
|
||||
|
|
|
@ -392,7 +392,7 @@ import { $i } from "@/account";
|
|||
import MkFollowApproveButton from "@/components/MkFollowApproveButton.vue";
|
||||
import MkUserName from "@/components/global/MkUserName.vue";
|
||||
import MkAvatar from "@/components/global/MkAvatar.vue";
|
||||
import Mfm from "@/components/mfm";
|
||||
import Mfm from "@/components/mfm.vue";
|
||||
import MkTime from "@/components/global/MkTime.vue";
|
||||
import MkA from "@/components/global/MkA.vue";
|
||||
import page from "@/components/page/page.vue";
|
||||
|
|
|
@ -33,6 +33,19 @@ export function magTransProperty<
|
|||
return x[keyB];
|
||||
}
|
||||
|
||||
export function magMaybeProperty<
|
||||
A extends Record<string, any>,
|
||||
AA extends keyof UnionToIntersection<A> & string
|
||||
>(x: A, keyA: AA): UnionIntersectionMerge<A>[AA] | undefined {
|
||||
const a = x[keyA];
|
||||
|
||||
if (typeof a !== "undefined") {
|
||||
return a;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function magTransMap<
|
||||
A extends Record<string, any>,
|
||||
AA extends keyof UnionIntersectionMerge<A> & string,
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
import { Err, Ok, Result } from "@/types/result";
|
||||
import * as mfm from "mfm-js";
|
||||
import { types } from "magnetar-common";
|
||||
|
||||
export type MagnetarParseError =
|
||||
| "XMLParseError"
|
||||
| "XMLParseException"
|
||||
| "InvalidRootNode";
|
||||
|
||||
export function parseMagnetarMarkdownXml(
|
||||
xml: types.MmXml
|
||||
): Result<XMLDocument, MagnetarParseError> {
|
||||
const parser = new DOMParser();
|
||||
const document = parser.parseFromString(
|
||||
xml,
|
||||
"application/xml"
|
||||
) as XMLDocument;
|
||||
const error = document.querySelector("parsererror");
|
||||
|
||||
if (error) {
|
||||
return Err("XMLParseError");
|
||||
}
|
||||
|
||||
if (document.documentElement.tagName !== "mmm") {
|
||||
return Err("InvalidRootNode");
|
||||
}
|
||||
|
||||
return Ok(document);
|
||||
}
|
||||
|
||||
export declare type MmmMatrixMention = {
|
||||
type: "matrixMention";
|
||||
props: {
|
||||
username: string;
|
||||
host: string;
|
||||
};
|
||||
children?: [];
|
||||
};
|
||||
|
||||
type MagnetarChildren<N extends { children?: any[] }> = {
|
||||
[T in keyof N]: T extends "children"
|
||||
? N[T] extends []
|
||||
? []
|
||||
: MagNode[]
|
||||
: N[T];
|
||||
};
|
||||
|
||||
export type MagNodeInline = mfm.MfmNode | MmmMatrixMention;
|
||||
export type MagNode = MagnetarChildren<mfm.MfmNode> | MmmMatrixMention;
|
||||
|
||||
const mkBase = <T>(type: T): { type: T; props: Record<string, unknown> } => ({
|
||||
type,
|
||||
props: {},
|
||||
});
|
||||
|
||||
const mk = <T, P extends Record<string, unknown>, C>(
|
||||
type: T,
|
||||
props: P,
|
||||
children?: C
|
||||
): {
|
||||
type: T;
|
||||
props: P;
|
||||
children: C extends (infer I & {})[] ? I[] : [];
|
||||
} => {
|
||||
return {
|
||||
...mkBase(type),
|
||||
props,
|
||||
...(typeof children === "undefined" ? children : { children }),
|
||||
} as {
|
||||
type: T;
|
||||
props: P;
|
||||
children: C extends (infer I & {})[] ? I[] : [];
|
||||
};
|
||||
};
|
||||
|
||||
export function mapMmXmlNodeListToMfm(nodes: NodeListOf<ChildNode>): MagNode[] {
|
||||
return Array.from(nodes).map(mapMmXmlNodeToMfm);
|
||||
}
|
||||
|
||||
export function mapMmXmlNodeToMfm(node: Node): MagNode {
|
||||
switch (node.nodeType) {
|
||||
case Node.TEXT_NODE:
|
||||
return mfm.TEXT(node.textContent ?? "");
|
||||
case Node.ELEMENT_NODE: {
|
||||
const el = node as Element;
|
||||
switch (el.tagName) {
|
||||
case "quote":
|
||||
case "small":
|
||||
case "center":
|
||||
return mk(
|
||||
el.tagName,
|
||||
{},
|
||||
mapMmXmlNodeListToMfm(el.childNodes)
|
||||
);
|
||||
case "b":
|
||||
return mk("bold", {}, mapMmXmlNodeListToMfm(el.childNodes));
|
||||
case "i":
|
||||
return mk(
|
||||
"italic",
|
||||
{},
|
||||
mapMmXmlNodeListToMfm(el.childNodes)
|
||||
);
|
||||
case "s":
|
||||
return mk(
|
||||
"strike",
|
||||
{},
|
||||
mapMmXmlNodeListToMfm(el.childNodes)
|
||||
);
|
||||
case "inline-code":
|
||||
return mk("inlineCode", {
|
||||
code: el.textContent ?? "",
|
||||
});
|
||||
case "inline-math":
|
||||
return mk("mathInline", {
|
||||
formula: el.textContent ?? "",
|
||||
});
|
||||
case "a":
|
||||
const url = el.getAttribute("href");
|
||||
|
||||
if (!url) break;
|
||||
|
||||
return mk(
|
||||
"link",
|
||||
{
|
||||
url,
|
||||
silent: el.getAttribute("embed") !== "true",
|
||||
},
|
||||
mapMmXmlNodeListToMfm(el.childNodes)
|
||||
);
|
||||
case "code":
|
||||
return mk("blockCode", {
|
||||
code: el.textContent ?? "",
|
||||
lang: el.getAttribute("lang"),
|
||||
});
|
||||
case "math":
|
||||
return mk("mathBlock", {
|
||||
formula: el.textContent ?? "",
|
||||
});
|
||||
case "hashtag":
|
||||
if (!el.textContent) break;
|
||||
|
||||
return mk("hashtag", {
|
||||
hashtag: el.textContent,
|
||||
});
|
||||
case "function":
|
||||
return mk("mathBlock", {
|
||||
formula: el.textContent ?? "",
|
||||
});
|
||||
case "ue":
|
||||
if (!el.textContent) break;
|
||||
return mk("unicodeEmoji", {
|
||||
emoji: el.textContent,
|
||||
});
|
||||
case "ee":
|
||||
if (!el.textContent) break;
|
||||
return mk("emojiCode", {
|
||||
name: el.textContent,
|
||||
});
|
||||
case "mention":
|
||||
const username = el.getAttribute("name");
|
||||
const host = el.getAttribute("host");
|
||||
const type = el.getAttribute("type");
|
||||
if (!username || (type === "matrix_user" && !host)) break;
|
||||
|
||||
switch (type) {
|
||||
case "matrix_user":
|
||||
return mk("matrixMention", {
|
||||
username,
|
||||
host: host!,
|
||||
});
|
||||
default:
|
||||
return mk("mention", {
|
||||
username,
|
||||
host,
|
||||
acct: host
|
||||
? `@${username}@${host}`
|
||||
: `@${username}`,
|
||||
});
|
||||
}
|
||||
case "fn":
|
||||
const name = el.getAttribute("name") ?? "";
|
||||
const args = el
|
||||
.getAttributeNames()
|
||||
.filter((v) => v.startsWith("arg-"))
|
||||
.map((v) => [
|
||||
v.substring("arg-".length),
|
||||
el.getAttribute(v) || true,
|
||||
])
|
||||
.reduce<Record<string, string | true>>(
|
||||
(acc, [k, v]) => ({
|
||||
...acc,
|
||||
[k as string]: v as string | true,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
return mk(
|
||||
"fn",
|
||||
{
|
||||
name,
|
||||
args,
|
||||
},
|
||||
mapMmXmlNodeListToMfm(el.childNodes)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mfm.TEXT(node.textContent ?? "");
|
||||
}
|
||||
|
||||
export function magnetarMarkdownToMfm(
|
||||
doc: XMLDocument
|
||||
): Result<MagNode[], MagnetarParseError> {
|
||||
const el = doc.documentElement;
|
||||
|
||||
if (el.tagName !== "mmm") {
|
||||
return Err("InvalidRootNode");
|
||||
}
|
||||
|
||||
return Ok(mapMmXmlNodeListToMfm(el.childNodes));
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
export class ResultOk<T, E> {
|
||||
public ok: T;
|
||||
|
||||
constructor(value: T) {
|
||||
this.ok = value;
|
||||
}
|
||||
|
||||
public map<U>(mapper: (t: T) => U): Result<U, E> {
|
||||
return new ResultOk(mapper(this.ok));
|
||||
}
|
||||
|
||||
public mapError<UE>(mapper: (t: E) => UE): Result<T, UE> {
|
||||
return new ResultOk(this.ok);
|
||||
}
|
||||
|
||||
public flatMap<U, E2>(
|
||||
mapper: (t: T) => Result<U, [E] extends [E2] ? E2 : E>
|
||||
): Result<U, [E] extends [E2] ? E2 : E> {
|
||||
return mapper(this.ok);
|
||||
}
|
||||
|
||||
public errorInto<E2>(): Result<T, [E] extends [E2] ? E2 : E> {
|
||||
return this as Result<T, [E] extends [E2] ? E2 : E>;
|
||||
}
|
||||
|
||||
public isOk(): this is ResultOk<T, E> {
|
||||
return true;
|
||||
}
|
||||
|
||||
public isErr(): this is ResultErr<T, E> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public unwrap(): T {
|
||||
return this.ok;
|
||||
}
|
||||
}
|
||||
|
||||
export class ResultErr<T, E> {
|
||||
public error: E;
|
||||
|
||||
constructor(errorValue: E) {
|
||||
this.error = errorValue;
|
||||
}
|
||||
|
||||
public map<U>(_mapper: (t: T) => U): Result<T, E> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public mapError<UE>(mapper: (t: E) => UE): Result<T, UE> {
|
||||
return new ResultErr(mapper(this.error));
|
||||
}
|
||||
|
||||
public flatMap<U, E2>(
|
||||
_mapper: (t: T) => Result<U, [E] extends [E2] ? E2 : E>
|
||||
): Result<U, [E] extends [E2] ? E2 : E> {
|
||||
return Err<[E] extends [E2] ? E2 : E, U>(
|
||||
this.error as [E] extends [E2] ? E2 : E
|
||||
);
|
||||
}
|
||||
|
||||
public errorInto<E2>(): Result<T, [E] extends [E2] ? E2 : E> {
|
||||
return this as Result<T, [E] extends [E2] ? E2 : E>;
|
||||
}
|
||||
|
||||
public isOk(): this is ResultOk<T, E> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public isErr(): this is ResultOk<T, E> {
|
||||
return true;
|
||||
}
|
||||
|
||||
public unwrap(): never {
|
||||
throw this.error;
|
||||
}
|
||||
}
|
||||
|
||||
export function Ok<T, E = never>(value: T): Result<T, E> {
|
||||
return new ResultOk(value);
|
||||
}
|
||||
|
||||
export function Err<E, T = never>(errorValue: E): Result<T, E> {
|
||||
return new ResultErr(errorValue);
|
||||
}
|
||||
|
||||
export type Result<T, E> = ResultOk<T, E> | ResultErr<T, E>;
|
Loading…
Reference in New Issue