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 @@ + + + + + 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;