diff --git a/fe_calckey/frontend/client/src/components/MagMatrixMention.vue b/fe_calckey/frontend/client/src/components/MagMatrixMention.vue
new file mode 100644
index 0000000..a477c1e
--- /dev/null
+++ b/fe_calckey/frontend/client/src/components/MagMatrixMention.vue
@@ -0,0 +1,59 @@
+
+
+ matrix /
+
+ @{{ username }}
+ :{{ toUnicode(host) }}
+
+
+
+
+
+
+
diff --git a/fe_calckey/frontend/client/src/components/MagUserPreview.vue b/fe_calckey/frontend/client/src/components/MagUserPreview.vue
index 8d95425..fe0c0b3 100644
--- a/fe_calckey/frontend/client/src/components/MagUserPreview.vue
+++ b/fe_calckey/frontend/client/src/components/MagUserPreview.vue
@@ -60,6 +60,7 @@
>
diff --git a/fe_calckey/frontend/client/src/init.ts b/fe_calckey/frontend/client/src/init.ts
index 4641858..b97f2cd 100644
--- a/fe_calckey/frontend/client/src/init.ts
+++ b/fe_calckey/frontend/client/src/init.ts
@@ -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";
diff --git a/fe_calckey/frontend/client/src/pages/user/home.vue b/fe_calckey/frontend/client/src/pages/user/home.vue
index bd19089..aee7423 100644
--- a/fe_calckey/frontend/client/src/pages/user/home.vue
+++ b/fe_calckey/frontend/client/src/pages/user/home.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";
diff --git a/fe_calckey/frontend/client/src/scripts-mag/mag-util.ts b/fe_calckey/frontend/client/src/scripts-mag/mag-util.ts
index d62dd05..226ec91 100644
--- a/fe_calckey/frontend/client/src/scripts-mag/mag-util.ts
+++ b/fe_calckey/frontend/client/src/scripts-mag/mag-util.ts
@@ -33,6 +33,19 @@ export function magTransProperty<
return x[keyB];
}
+export function magMaybeProperty<
+ A extends Record,
+ AA extends keyof UnionToIntersection & string
+>(x: A, keyA: AA): UnionIntersectionMerge[AA] | undefined {
+ const a = x[keyA];
+
+ if (typeof a !== "undefined") {
+ return a;
+ }
+
+ return undefined;
+}
+
export function magTransMap<
A extends Record,
AA extends keyof UnionIntersectionMerge & string,
diff --git a/fe_calckey/frontend/client/src/scripts-mag/mmm-util.ts b/fe_calckey/frontend/client/src/scripts-mag/mmm-util.ts
new file mode 100644
index 0000000..8b7c9c6
--- /dev/null
+++ b/fe_calckey/frontend/client/src/scripts-mag/mmm-util.ts
@@ -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 {
+ 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 = {
+ [T in keyof N]: T extends "children"
+ ? N[T] extends []
+ ? []
+ : MagNode[]
+ : N[T];
+};
+
+export type MagNodeInline = mfm.MfmNode | MmmMatrixMention;
+export type MagNode = MagnetarChildren | MmmMatrixMention;
+
+const mkBase = (type: T): { type: T; props: Record } => ({
+ type,
+ props: {},
+});
+
+const mk = , 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): 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>(
+ (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 {
+ const el = doc.documentElement;
+
+ if (el.tagName !== "mmm") {
+ return Err("InvalidRootNode");
+ }
+
+ return Ok(mapMmXmlNodeListToMfm(el.childNodes));
+}
diff --git a/fe_calckey/frontend/client/src/types/result.ts b/fe_calckey/frontend/client/src/types/result.ts
new file mode 100644
index 0000000..23f29dc
--- /dev/null
+++ b/fe_calckey/frontend/client/src/types/result.ts
@@ -0,0 +1,87 @@
+export class ResultOk {
+ public ok: T;
+
+ constructor(value: T) {
+ this.ok = value;
+ }
+
+ public map(mapper: (t: T) => U): Result {
+ return new ResultOk(mapper(this.ok));
+ }
+
+ public mapError(mapper: (t: E) => UE): Result {
+ return new ResultOk(this.ok);
+ }
+
+ public flatMap(
+ mapper: (t: T) => Result
+ ): Result {
+ return mapper(this.ok);
+ }
+
+ public errorInto(): Result {
+ return this as Result;
+ }
+
+ public isOk(): this is ResultOk {
+ return true;
+ }
+
+ public isErr(): this is ResultErr {
+ return false;
+ }
+
+ public unwrap(): T {
+ return this.ok;
+ }
+}
+
+export class ResultErr {
+ public error: E;
+
+ constructor(errorValue: E) {
+ this.error = errorValue;
+ }
+
+ public map(_mapper: (t: T) => U): Result {
+ return this;
+ }
+
+ public mapError(mapper: (t: E) => UE): Result {
+ return new ResultErr(mapper(this.error));
+ }
+
+ public flatMap(
+ _mapper: (t: T) => Result
+ ): Result {
+ return Err<[E] extends [E2] ? E2 : E, U>(
+ this.error as [E] extends [E2] ? E2 : E
+ );
+ }
+
+ public errorInto(): Result {
+ return this as Result;
+ }
+
+ public isOk(): this is ResultOk {
+ return false;
+ }
+
+ public isErr(): this is ResultOk {
+ return true;
+ }
+
+ public unwrap(): never {
+ throw this.error;
+ }
+}
+
+export function Ok(value: T): Result {
+ return new ResultOk(value);
+}
+
+export function Err(errorValue: E): Result {
+ return new ResultErr(errorValue);
+}
+
+export type Result = ResultOk | ResultErr;