mirror of
https://iceshrimp.dev/Crimekillz/jointrashposs.git
synced 2024-11-25 10:19:07 +01:00
(Add) Tools: MFM Preview (#2)
* wip * (add) mfm preview * (fix) iroiro * (Fix) build error * (fix) something
This commit is contained in:
parent
dafeec6a98
commit
1f2b1aa58b
@ -1,3 +1,12 @@
|
|||||||
|
@charset "utf-8";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* MFM */
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
0% { opacity: 1; transform: scale(1); }
|
0% { opacity: 1; transform: scale(1); }
|
||||||
30% { opacity: 1; transform: scale(1); }
|
30% { opacity: 1; transform: scale(1); }
|
||||||
|
@ -127,28 +127,42 @@ html.light .markdown-body {
|
|||||||
@apply font-content-sans;
|
@apply font-content-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h1 > a[href^='#'],
|
.markdown-body > h1 > a[href^='#'],
|
||||||
.markdown-body h2 > a[href^='#'],
|
.markdown-body :not(.ignore) h1 > a[href^='#'],
|
||||||
.markdown-body h3 > a[href^='#'],
|
.markdown-body > h2 > a[href^='#'],
|
||||||
.markdown-body h4 > a[href^='#'],
|
.markdown-body :not(.ignore) h2 > a[href^='#'],
|
||||||
.markdown-body h5 > a[href^='#'],
|
.markdown-body > h3 > a[href^='#'],
|
||||||
.markdown-body h6 > a[href^='#'] {
|
.markdown-body :not(.ignore) h3 > a[href^='#'],
|
||||||
|
.markdown-body > h4 > a[href^='#'],
|
||||||
|
.markdown-body :not(.ignore) h4 > a[href^='#'],
|
||||||
|
.markdown-body > h5 > a[href^='#'],
|
||||||
|
.markdown-body :not(.ignore) h5 > a[href^='#'],
|
||||||
|
.markdown-body > h6 > a[href^='#'] ,
|
||||||
|
.markdown-body :not(.ignore) h6 > a[href^='#'] {
|
||||||
color: var(--color-fg-default);
|
color: var(--color-fg-default);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h1 > a[href^='#']:hover,
|
.markdown-body > h1 > a[href^='#']:hover,
|
||||||
.markdown-body h2 > a[href^='#']:hover,
|
.markdown-body :not(.ignore) h1 > a[href^='#']:hover,
|
||||||
.markdown-body h3 > a[href^='#']:hover,
|
.markdown-body > h2 > a[href^='#']:hover,
|
||||||
.markdown-body h4 > a[href^='#']:hover,
|
.markdown-body :not(.ignore) h2 > a[href^='#']:hover,
|
||||||
.markdown-body h5 > a[href^='#']:hover,
|
.markdown-body > h3 > a[href^='#']:hover,
|
||||||
.markdown-body h6 > a[href^='#']:hover {
|
.markdown-body :not(.ignore) h3 > a[href^='#']:hover,
|
||||||
|
.markdown-body > h4 > a[href^='#']:hover,
|
||||||
|
.markdown-body :not(.ignore) h4 > a[href^='#']:hover,
|
||||||
|
.markdown-body > h5 > a[href^='#']:hover,
|
||||||
|
.markdown-body :not(.ignore) h5 > a[href^='#']:hover,
|
||||||
|
.markdown-body > h6 > a[href^='#']:hover ,
|
||||||
|
.markdown-body :not(.ignore) h6 > a[href^='#']:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ul {
|
.markdown-body > ul ,
|
||||||
|
.markdown-body :not(.ignore) ul {
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
}
|
}
|
||||||
.markdown-body ol {
|
.markdown-body > ol ,
|
||||||
|
.markdown-body :not(.ignore) ol {
|
||||||
list-style: decimal;
|
list-style: decimal;
|
||||||
}
|
}
|
@ -1,10 +1,19 @@
|
|||||||
import { FunctionalComponent } from "nuxt/dist/app/compat/capi";
|
import { FunctionalComponent } from "nuxt/dist/app/compat/capi";
|
||||||
import GHIcon from "bi/github.svg";
|
import GHIcon from "bi/github.svg";
|
||||||
|
|
||||||
|
export type NavSection = {
|
||||||
|
/** セクションタイトル 翻訳キー */
|
||||||
|
title: string;
|
||||||
|
/** アイテム */
|
||||||
|
items: NavItem[];
|
||||||
|
};
|
||||||
|
|
||||||
/** ナビゲーションバー アイテム */
|
/** ナビゲーションバー アイテム */
|
||||||
type NavItem = {
|
export type NavItem = {
|
||||||
/** 翻訳キー */
|
/** 翻訳キー */
|
||||||
i18n: string;
|
i18n: string;
|
||||||
|
/** 説明文 翻訳キー */
|
||||||
|
description?: string;
|
||||||
/** リンク先 */
|
/** リンク先 */
|
||||||
to: string;
|
to: string;
|
||||||
} | {
|
} | {
|
||||||
|
14
assets/data/toolsNav.ts
Normal file
14
assets/data/toolsNav.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { NavSection } from "./nav";
|
||||||
|
|
||||||
|
export default <NavSection[]>[
|
||||||
|
{
|
||||||
|
title: "_tools._forUsers.title",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
i18n: "_mfmPlayground.title",
|
||||||
|
description: "_mfmPlayground.description",
|
||||||
|
to: "/tools/mfm-playground/",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
25
assets/js/mi/clone.ts
Normal file
25
assets/js/mi/clone.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// structredCloneが遅いため
|
||||||
|
// SEE: http://var.blog.jp/archives/86038606.html
|
||||||
|
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||||
|
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
||||||
|
|
||||||
|
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
||||||
|
|
||||||
|
export function deepClone<T extends Cloneable>(x: T): T {
|
||||||
|
if (typeof x === 'object') {
|
||||||
|
if (x === null) return x;
|
||||||
|
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||||
|
const obj = {} as Record<string, Cloneable>;
|
||||||
|
for (const [k, v] of Object.entries(x)) {
|
||||||
|
obj[k] = deepClone(v);
|
||||||
|
}
|
||||||
|
return obj as T;
|
||||||
|
} else {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
20
assets/js/mi/collapsed.ts
Normal file
20
assets/js/mi/collapsed.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
|
export function shouldCollapsed(note: Misskey.entities.Note): boolean {
|
||||||
|
const collapsed = note?.cw == null && note?.text != null && (
|
||||||
|
(note.text.includes('$[x2')) ||
|
||||||
|
(note.text.includes('$[x3')) ||
|
||||||
|
(note.text.includes('$[x4')) ||
|
||||||
|
(note.text.includes('$[scale')) ||
|
||||||
|
(note.text.split('\n').length > 9) ||
|
||||||
|
(note.text.length > 500) ||
|
||||||
|
(note.files.length >= 5)
|
||||||
|
);
|
||||||
|
|
||||||
|
return collapsed;
|
||||||
|
}
|
13
assets/js/mi/io-emojis.ts
Normal file
13
assets/js/mi/io-emojis.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { getIOEmoji } from './io-meta';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
export const customEmojisMap = new Map<string, Misskey.entities.CustomEmoji>();
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const emojis = await getIOEmoji();
|
||||||
|
|
||||||
|
for (const emoji of emojis.emojis) {
|
||||||
|
customEmojisMap.set(emoji.name, emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
23
assets/js/mi/io-media-proxy.ts
Normal file
23
assets/js/mi/io-media-proxy.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 各インスタンスから画像引っ張ってくるのがとても大変なのでio経由で出す */
|
||||||
|
import { withQuery } from "ufo";
|
||||||
|
|
||||||
|
export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
|
||||||
|
const localProxy = 'https://misskey.io/proxy';
|
||||||
|
|
||||||
|
if (imageUrl.startsWith('https://proxy.misskeyusercontent.com/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
|
||||||
|
// もう既にproxyっぽそうだったらurlを取り出す
|
||||||
|
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return withQuery(`${mustOrigin ? localProxy : 'https://proxy.misskeyusercontent.com'}/${type === 'preview' ? 'preview.webp' : 'image.webp'}`, {
|
||||||
|
url: imageUrl,
|
||||||
|
...(!noFallback ? { 'fallback': '1' } : {}),
|
||||||
|
...(type ? { [type]: '1' } : {}),
|
||||||
|
...(mustOrigin ? { origin: '1' } : {}),
|
||||||
|
});
|
||||||
|
}
|
33
assets/js/mi/io-meta.ts
Normal file
33
assets/js/mi/io-meta.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
|
export const getIOMeta = async (): Promise<Misskey.entities.LiteInstanceMetadata> => {
|
||||||
|
if (!process.client) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!sessionStorage.getItem('miHub_io_meta')) {
|
||||||
|
const meta = await fetch('https://misskey.io/api/meta');
|
||||||
|
|
||||||
|
const metaText = await meta.text();
|
||||||
|
sessionStorage.setItem('miHub_io_meta', metaText);
|
||||||
|
|
||||||
|
return JSON.parse(metaText);
|
||||||
|
} else {
|
||||||
|
return JSON.parse(sessionStorage.getItem('miHub_io_meta') ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIOEmoji = async (): Promise<{ emojis: Misskey.entities.CustomEmoji[] }> => {
|
||||||
|
if (!process.client) {
|
||||||
|
return { emojis: [] };
|
||||||
|
}
|
||||||
|
if (!localStorage.getItem('miHub_io_emoji')) {
|
||||||
|
const emoji = await fetch('https://misskey.io/api/emojis');
|
||||||
|
|
||||||
|
const emojiText = await emoji.text();
|
||||||
|
localStorage.setItem('miHub_io_emoji', emojiText);
|
||||||
|
|
||||||
|
return JSON.parse(emojiText);
|
||||||
|
} else {
|
||||||
|
return JSON.parse(localStorage.getItem('miHub_io_emoji') ?? '');
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,12 @@
|
|||||||
import type { NavItem } from '@nuxt/content/dist/runtime/types';
|
import type { NavItem } from '@nuxt/content/dist/runtime/types';
|
||||||
|
import { parseURL } from 'ufo';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* オブジェクトのパス文字列からオブジェクトの内部を参照
|
||||||
|
* @param o オブジェクト
|
||||||
|
* @param s パス
|
||||||
|
* @returns パスの先にあるもの
|
||||||
|
*/
|
||||||
export function resolveObjPath(o: object, s: string): any {
|
export function resolveObjPath(o: object, s: string): any {
|
||||||
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||||
s = s.replace(/^\./, ''); // strip a leading dot
|
s = s.replace(/^\./, ''); // strip a leading dot
|
||||||
@ -15,33 +22,33 @@ export function resolveObjPath(o: object, s: string): any {
|
|||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URLがドメイン内部かどうかを判別
|
||||||
|
* @param link 判別したいURL
|
||||||
|
* @param base ローカルの基準となるドメイン
|
||||||
|
*/
|
||||||
export function isLocalPath(link: string, base?: string): boolean {
|
export function isLocalPath(link: string, base?: string): boolean {
|
||||||
let baseUrl;
|
let baseUrl;
|
||||||
|
|
||||||
if (base) {
|
if (base) {
|
||||||
baseUrl = base;
|
baseUrl = base;
|
||||||
} else {
|
} else {
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
baseUrl = runtimeConfig.public.baseUrl;
|
baseUrl = runtimeConfig.public.baseUrl;
|
||||||
}
|
}
|
||||||
const rootDomain = new URL(baseUrl);
|
|
||||||
try {
|
const rootDomain = parseURL(base);
|
||||||
const url = new URL(link);
|
const url = parseURL(link);
|
||||||
if (!url.hostname || rootDomain.hostname === url.hostname) {
|
|
||||||
return true;
|
return (!url.host || rootDomain.host === url.host);
|
||||||
} else if (rootDomain.hostname !== url.hostname) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch(error) {
|
|
||||||
if(link !== '') {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ナビゲーションObjectを合致する条件まで深掘り
|
||||||
|
* @param obj ナビゲーションObject
|
||||||
|
* @param condition 深掘りを停止する条件
|
||||||
|
* @returns 深掘りしたナビゲーションObject
|
||||||
|
*/
|
||||||
export const findDeepObject = (obj: NavItem, condition: (v: NavItem) => boolean): NavItem | null => {
|
export const findDeepObject = (obj: NavItem, condition: (v: NavItem) => boolean): NavItem | null => {
|
||||||
if (condition(obj)) {
|
if (condition(obj)) {
|
||||||
return obj;
|
return obj;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-lg border border-slate-200 dark:border-slate-800 p-6 mfm-root mb-4 relative overflow-hidden">
|
<div class="rounded-lg border border-slate-200 dark:border-slate-800 p-6 mfm-root mb-4 relative overflow-hidden">
|
||||||
<div class="absolute top-0 left-0 px-1 py-0.5 text-xs bg-slate-200 dark:bg-slate-800 rounded-br-lg z-20">{{ $t('_content.preview') }}</div>
|
<div class="absolute top-0 left-0 px-1 py-0.5 text-xs bg-slate-200 dark:bg-slate-800 rounded-br-lg z-20">{{ $t('_content.preview') }}</div>
|
||||||
<MkMfm :text="text" />
|
<div class="ignore" :class="$style.mfmRoot">
|
||||||
|
<MkMfm :text="text" baseHost="misskey.io" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -12,3 +14,9 @@ defineProps<{
|
|||||||
text: string;
|
text: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.mfmRoot img {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
</style>
|
@ -8,7 +8,7 @@
|
|||||||
</GNuxtLink>
|
</GNuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
class="fixed z-[9902] top-16 right-0 text-right p-4 w-[80vw] sm:w-[50vw] bg-slate-300/90 dark:bg-slate-950/90 shadow-lg space-y-2 transition-[transform,border-radius] lg:transition-none lg:translate-x-0 lg:backdrop-blur-none lg:w-auto lg:rounded-none lg:shadow-none lg:space-y-0 lg:p-0 lg:relative lg:top-0 lg:right-auto lg:bg-transparent lg:col-span-4 lg:space-x-8 xl:space-x-10 lg:flex lg:justify-center"
|
class="fixed z-[9902] top-16 right-0 text-right p-4 w-[80vw] sm:w-[50vw] bg-slate-300/90 dark:bg-slate-950/90 shadow-lg space-y-2 transition-[transform,border-radius] lg:transition-none lg:translate-x-0 lg:backdrop-blur-none lg:w-auto lg:rounded-none lg:shadow-none lg:space-y-0 lg:p-0 lg:relative lg:top-0 lg:right-auto lg:bg-transparent dark:lg:bg-transparent lg:col-span-4 lg:space-x-8 xl:space-x-10 lg:flex lg:justify-center"
|
||||||
:class="[(scrollPos <= -40) ? 'rounded-bl-lg' : 'rounded-l-lg', navOpen ? 'translate-x-0' : 'translate-x-full']"
|
:class="[(scrollPos <= -40) ? 'rounded-bl-lg' : 'rounded-l-lg', navOpen ? 'translate-x-0' : 'translate-x-full']"
|
||||||
>
|
>
|
||||||
<li v-for="item in NavData.center">
|
<li v-for="item in NavData.center">
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { parseURL, cleanDoubleSlashes, withTrailingSlash } from 'ufo';
|
import { cleanDoubleSlashes, withTrailingSlash } from 'ufo';
|
||||||
|
import { isLocalPath } from '@/assets/js/misc';
|
||||||
import { RouteLocationRaw } from '#vue-router';
|
import { RouteLocationRaw } from '#vue-router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,11 +27,8 @@ const realHref = computed(() => {
|
|||||||
const rhf = rawProps.to ?? rawProps.href;
|
const rhf = rawProps.to ?? rawProps.href;
|
||||||
|
|
||||||
if (rhf && typeof rhf === 'string') {
|
if (rhf && typeof rhf === 'string') {
|
||||||
const runtimeConfig = useRuntimeConfig();
|
|
||||||
const rootDomain = parseURL(runtimeConfig.public.baseUrl);
|
|
||||||
const url = parseURL(rhf);
|
|
||||||
|
|
||||||
if (!url.host || rootDomain.host === url.host) {
|
if (isLocalPath(rhf)) {
|
||||||
return withTrailingSlash(cleanDoubleSlashes(rhf), true);
|
return withTrailingSlash(cleanDoubleSlashes(rhf), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
82
components/mk/CustomEmoji.vue
Normal file
82
components/mk/CustomEmoji.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span v-if="errored">:{{ customEmojiName }}:</span>
|
||||||
|
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { getProxiedImageUrl } from '@/assets/js/mi/io-media-proxy';
|
||||||
|
import { customEmojisMap } from '@/assets/js/mi/io-emojis';
|
||||||
|
import { parseURL } from 'ufo';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string;
|
||||||
|
normal?: boolean;
|
||||||
|
noStyle?: boolean;
|
||||||
|
host?: string | null;
|
||||||
|
url?: string;
|
||||||
|
useOriginalSize?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
|
||||||
|
const isLocal = computed(() => props.host === 'misskey.io' && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
||||||
|
|
||||||
|
const rawUrl = computed(() => {
|
||||||
|
if (props.url) {
|
||||||
|
return props.url;
|
||||||
|
}
|
||||||
|
if (isLocal.value) {
|
||||||
|
return customEmojisMap.get(customEmojiName.value)?.url ?? null;
|
||||||
|
}
|
||||||
|
return props.host !== 'misskey.io' ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = computed(() => {
|
||||||
|
if (rawUrl.value == null) return null;
|
||||||
|
|
||||||
|
const proxied =
|
||||||
|
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
|
||||||
|
? parseURL(rawUrl.value).host ? rawUrl.value : 'https://misskey.io' + rawUrl.value
|
||||||
|
: getProxiedImageUrl(
|
||||||
|
rawUrl.value,
|
||||||
|
props.useOriginalSize ? undefined : 'emoji',
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return proxied;
|
||||||
|
});
|
||||||
|
|
||||||
|
const alt = computed(() => `:${customEmojiName.value}:`);
|
||||||
|
const errored = ref(props.host == null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
display: inline;
|
||||||
|
height: 2em;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal {
|
||||||
|
display: inline;
|
||||||
|
height: 1.25em;
|
||||||
|
vertical-align: -0.25em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.noStyle {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
49
components/mk/Mention.vue
Normal file
49
components/mk/Mention.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GNuxtLink :class="$style.root" :to="url" target="_blank">
|
||||||
|
<img :class="$style.icon" :src="`https://${localHost}/avatar/@${username}@${host}`" alt="">
|
||||||
|
<span>
|
||||||
|
<span>@{{ username }}</span>
|
||||||
|
<span v-if="host != localHost" :class="$style.host">@{{ host }}</span>
|
||||||
|
</span>
|
||||||
|
</GNuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps<{
|
||||||
|
username: string;
|
||||||
|
host: string;
|
||||||
|
localHost: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const canonical = props.host === props.localHost ? `@${props.username}` : `@${props.username}@${props.host}`;
|
||||||
|
|
||||||
|
const url = `https://${props.localHost}/${canonical}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px 4px 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: rgb(134, 179, 0)!important;
|
||||||
|
background-color: rgba(134, 179, 0, 0.1)!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
object-fit: cover;
|
||||||
|
margin: 0 0.2em 0 0;
|
||||||
|
vertical-align: bottom;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,8 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
import { VNode, h } from 'vue';
|
import { VNode, h } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import MkGoogle from '@/components/mk/Google.vue';
|
import MkGoogle from '@/components/mk/Google.vue';
|
||||||
import MkSparkle from '@/components/mk/Sparkle.vue';
|
import MkSparkle from '@/components/mk/Sparkle.vue';
|
||||||
import NuxtLink from '@/components/g/NuxtLink';
|
import MkCustomEmoji from '@/components/mk/CustomEmoji.vue';
|
||||||
|
import MkMention from '@/components/mk/Mention.vue';
|
||||||
|
import NuxtLink from '@/components/g/NuxtLink.vue';
|
||||||
import ProseAVue from '@/components/content/ProseA.vue';
|
import ProseAVue from '@/components/content/ProseA.vue';
|
||||||
|
|
||||||
const QUOTE_STYLE = `
|
const QUOTE_STYLE = `
|
||||||
@ -21,6 +28,7 @@ export default function(props: {
|
|||||||
isNote?: boolean;
|
isNote?: boolean;
|
||||||
emojiUrls?: string[];
|
emojiUrls?: string[];
|
||||||
rootScale?: number;
|
rootScale?: number;
|
||||||
|
baseHost?: string;
|
||||||
}) {
|
}) {
|
||||||
const isNote = props.isNote !== undefined ? props.isNote : true;
|
const isNote = props.isNote !== undefined ? props.isNote : true;
|
||||||
|
|
||||||
@ -99,12 +107,12 @@ export default function(props: {
|
|||||||
case 'spin': {
|
case 'spin': {
|
||||||
const direction =
|
const direction =
|
||||||
token.props.args.left ? 'reverse' :
|
token.props.args.left ? 'reverse' :
|
||||||
token.props.args.alternate ? 'alternate' :
|
token.props.args.alternate ? 'alternate' :
|
||||||
'normal';
|
'normal';
|
||||||
const anime =
|
const anime =
|
||||||
token.props.args.x ? 'mfm-spinX' :
|
token.props.args.x ? 'mfm-spinX' :
|
||||||
token.props.args.y ? 'mfm-spinY' :
|
token.props.args.y ? 'mfm-spinY' :
|
||||||
'mfm-spin';
|
'mfm-spin';
|
||||||
const speed = validTime(token.props.args.speed) ?? '1.5s';
|
const speed = validTime(token.props.args.speed) ?? '1.5s';
|
||||||
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
||||||
break;
|
break;
|
||||||
@ -122,8 +130,8 @@ export default function(props: {
|
|||||||
case 'flip': {
|
case 'flip': {
|
||||||
const transform =
|
const transform =
|
||||||
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
|
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
|
||||||
token.props.args.v ? 'scaleY(-1)' :
|
token.props.args.v ? 'scaleY(-1)' :
|
||||||
'scaleX(-1)';
|
'scaleX(-1)';
|
||||||
style = `transform: ${transform};`;
|
style = `transform: ${transform};`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -145,12 +153,12 @@ export default function(props: {
|
|||||||
case 'font': {
|
case 'font': {
|
||||||
const family =
|
const family =
|
||||||
token.props.args.serif ? 'serif' :
|
token.props.args.serif ? 'serif' :
|
||||||
token.props.args.monospace ? 'monospace' :
|
token.props.args.monospace ? 'monospace' :
|
||||||
token.props.args.cursive ? 'cursive' :
|
token.props.args.cursive ? 'cursive' :
|
||||||
token.props.args.fantasy ? 'fantasy' :
|
token.props.args.fantasy ? 'fantasy' :
|
||||||
token.props.args.emoji ? 'emoji' :
|
token.props.args.emoji ? 'emoji' :
|
||||||
token.props.args.math ? 'math' :
|
token.props.args.math ? 'math' :
|
||||||
null;
|
null;
|
||||||
if (family) style = `font-family: ${family};`;
|
if (family) style = `font-family: ${family};`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -226,22 +234,26 @@ export default function(props: {
|
|||||||
return [h(ProseAVue, {
|
return [h(ProseAVue, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
href: token.props.url,
|
href: token.props.url,
|
||||||
|
target: '_blank',
|
||||||
rel: 'nofollow noopener',
|
rel: 'nofollow noopener',
|
||||||
}, token.props.url)];
|
}, token.props.url)];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'link': {
|
case 'link': {
|
||||||
return [h(ProseAVue, {
|
return [h(NuxtLink, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
to: token.props.url,
|
to: token.props.url,
|
||||||
|
target: '_blank',
|
||||||
rel: 'nofollow noopener',
|
rel: 'nofollow noopener',
|
||||||
}, genEl(token.children, scale))];
|
}, genEl(token.children, scale))];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'mention': {
|
case 'mention': {
|
||||||
|
//@ts-ignore
|
||||||
return [h(MkMention, {
|
return [h(MkMention, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
host: (token.props.host) || host,
|
host: (token.props.host) ?? props.baseHost,
|
||||||
|
localHost: props.baseHost,
|
||||||
username: token.props.username,
|
username: token.props.username,
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
@ -249,7 +261,7 @@ export default function(props: {
|
|||||||
case 'hashtag': {
|
case 'hashtag': {
|
||||||
return [h(NuxtLink, {
|
return [h(NuxtLink, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
to: `https://misskey.io/tags/${encodeURIComponent(token.props.hashtag)}`,
|
to: `https://${props.baseHost ?? 'misskey.io'}/tags/${encodeURIComponent(token.props.hashtag)}`,
|
||||||
style: 'color:rgb(255, 145, 86);',
|
style: 'color:rgb(255, 145, 86);',
|
||||||
}, `#${token.props.hashtag}`)];
|
}, `#${token.props.hashtag}`)];
|
||||||
}
|
}
|
||||||
@ -268,34 +280,20 @@ export default function(props: {
|
|||||||
|
|
||||||
case 'emojiCode': {
|
case 'emojiCode': {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (props.author?.host == null) {
|
return [h(MkCustomEmoji, {
|
||||||
return [h(MkCustomEmoji, {
|
key: Math.random(),
|
||||||
key: Math.random(),
|
name: token.props.name,
|
||||||
name: token.props.name,
|
normal: props.plain,
|
||||||
normal: props.plain,
|
host: props.baseHost,
|
||||||
host: null,
|
useOriginalSize: scale >= 2.5,
|
||||||
useOriginalSize: scale >= 2.5,
|
})];
|
||||||
})];
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
|
|
||||||
return [h('span', `:${token.props.name}:`)];
|
|
||||||
} else {
|
|
||||||
return [h(MkCustomEmoji, {
|
|
||||||
key: Math.random(),
|
|
||||||
name: token.props.name,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
|
|
||||||
normal: props.plain,
|
|
||||||
host: props.author.host,
|
|
||||||
useOriginalSize: scale >= 2.5,
|
|
||||||
})];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'unicodeEmoji': {
|
case 'unicodeEmoji': {
|
||||||
return [h('span', token.props.emoji)];
|
return [h('img', {
|
||||||
|
style: 'display:inline;height:1.25em;vertical-align:-.25em;',
|
||||||
|
src: `https://cdn.jsdelivr.net/gh/jdecked/twemoji@latest/assets/svg/${token.props.emoji.codePointAt(0)?.toString(16)}.svg`,
|
||||||
|
})];
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'mathInline': {
|
case 'mathInline': {
|
||||||
@ -306,6 +304,16 @@ export default function(props: {
|
|||||||
return [h('code', token.props.formula)];
|
return [h('code', token.props.formula)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'inlineCode': {
|
||||||
|
return [h('code', token.props.code)];
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'blockCode': {
|
||||||
|
return [h('pre', {
|
||||||
|
class: 'p-4 bg-gray-200/50 rounded',
|
||||||
|
}, h('code', token.props.code))];
|
||||||
|
}
|
||||||
|
|
||||||
case 'search': {
|
case 'search': {
|
||||||
return [h(MkGoogle, {
|
return [h(MkGoogle, {
|
||||||
key: Math.random(),
|
key: Math.random(),
|
||||||
|
44
components/tools/AsideNavSection.vue
Normal file
44
components/tools/AsideNavSection.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { NavSection } from '@/assets/data/nav';
|
||||||
|
import { isSamePath } from 'ufo';
|
||||||
|
import CaretRightFillIco from 'bi/caret-right-fill.svg';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
d: NavSection;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'toggleNav',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const navData = props.d;
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const isShown = ref(false);
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
|
||||||
|
router.afterEach((to, from, failure) => {
|
||||||
|
if (navData.items.some((e) => isSamePath(route.path, localePath(e.to)))) {
|
||||||
|
isShown.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (navData.items.some((e) => isSamePath(route.path, localePath(e.to)))) {
|
||||||
|
isShown.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleCollapse() {
|
||||||
|
isShown.value = !isShown.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<button @click="toggleCollapse()" class="hover:text-accent-600 hover:bg-accent-50 dark:hover:text-accent-100 dark:hover:bg-accent-800 px-4 py-2 rounded-r-full flex items-center text-start"><CaretRightFillIco :class="[{ 'rotate-90': (isShown) }, 'mr-1 h-3 w-3 transition-transform']" />{{ $t(navData.title) }}</button>
|
||||||
|
<div v-show="isShown">
|
||||||
|
<GNuxtLink v-for="item in navData.items" @click.native="emit('toggleNav')" :class="[isSamePath(route.path, localePath(item.to)) ? 'bg-accent-100 text-accent-600 dark:text-accent-100 dark:bg-accent-800 font-bold' : 'hover:text-accent-600 hover:bg-accent-50 dark:hover:text-accent-100 dark:hover:bg-accent-800', 'block pl-6 pr-4 py-2 rounded-r-full']" :to="localePath(item.to)">
|
||||||
|
{{ $t(item.i18n) }}
|
||||||
|
</GNuxtLink>
|
||||||
|
</div>
|
||||||
|
<hr class="mb-1 mt-2" />
|
||||||
|
</template>
|
@ -23,12 +23,17 @@ MFMは、Markup language For Misskeyの略で、Misskeyの様々な場所で使
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
```
|
```
|
||||||
@alice
|
@ai
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<MfmPreview text="@ai"></MfmPreview>
|
||||||
|
|
||||||
```
|
```
|
||||||
@alice@example.com
|
@repo@p1.a9z.dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<MfmPreview text="@repo@p1.a9z.dev"></MfmPreview>
|
||||||
|
|
||||||
### ハッシュタグ
|
### ハッシュタグ
|
||||||
ナンバーサイン + タグで、ハッシュタグを示すことができます。
|
ナンバーサイン + タグで、ハッシュタグを示すことができます。
|
||||||
:::tip
|
:::tip
|
||||||
@ -75,6 +80,8 @@ https://example.com
|
|||||||
:misskey:
|
:misskey:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<MfmPreview text=":misskey:"></MfmPreview>
|
||||||
|
|
||||||
### 太字
|
### 太字
|
||||||
文字を太く表示して強調することができます。
|
文字を太く表示して強調することができます。
|
||||||
```
|
```
|
||||||
|
61
layouts/tools.vue
Normal file
61
layouts/tools.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import sections from '@/assets/data/toolsNav';
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'toggleNav',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isNavOpen = ref<boolean>(false);
|
||||||
|
const isAsideNavOpen = ref<boolean>(false);
|
||||||
|
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
class: 'scroll-pt-20',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-white dark:bg-slate-950">
|
||||||
|
<GNav @toggleNav="isNavOpen = !isNavOpen" :is-open="isNavOpen" :slim="true" :disable-shadow="true" />
|
||||||
|
<div :class="$style.slimPageRoot" class="overflow-x-hidden">
|
||||||
|
<aside
|
||||||
|
class="w-80 lg:w-72 fixed top-0 mt-16 h-screen flex transition-transform lg:translate-x-0"
|
||||||
|
:class="[isAsideNavOpen ? 'translate-x-0' : '-translate-x-72']"
|
||||||
|
>
|
||||||
|
<nav class="border-r pr-2 py-5 flex flex-col w-72 overflow-y-scroll">
|
||||||
|
<NuxtLink @click.native="emit('toggleNav')" class="block pl-6 pr-4 py-2 rounded-r-full hover:text-accent-600 hover:bg-accent-50 dark:hover:text-accent-100 dark:hover:bg-accent-800" :to="localePath('/tools/')">
|
||||||
|
{{ $t('_tools.index') }}
|
||||||
|
</NuxtLink>
|
||||||
|
<hr class="mb-1 mt-2" />
|
||||||
|
<ToolsAsideNavSection :d="section" @toggleNav="emit('toggleNav')" v-for="section in sections" />
|
||||||
|
</nav>
|
||||||
|
<div class="lg:hidden">
|
||||||
|
<button @click="isAsideNavOpen = !isAsideNavOpen" class="bg-slate-300 dark:bg-slate-800 hover:bg-slate-400 dark:hover:bg-slate-700" :class="$style.toolsMenuToggle"><span>{{ $t('_tools.menuToggle') }}</span></button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="ml-8 lg:ml-72 lg:translate-x-0 transition-transform bg-slate-100 dark:bg-slate-900" :class="[isAsideNavOpen ? 'translate-x-72' : 'translate-x-0']">
|
||||||
|
<div :class="$style.slimPageRoot">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.slimPageRoot {
|
||||||
|
min-height: calc(100vh - 4rem);
|
||||||
|
}
|
||||||
|
.toolsMenuToggle {
|
||||||
|
@apply block mt-6 w-8 h-min py-2 rounded-r-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolsMenuToggle span {
|
||||||
|
@apply tracking-wide;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
}
|
||||||
|
</style>
|
@ -173,13 +173,22 @@ _links:
|
|||||||
|
|
||||||
_tools:
|
_tools:
|
||||||
title: "ツール集"
|
title: "ツール集"
|
||||||
|
index: "ツール集 ホーム画面"
|
||||||
description: "Misskey向けの便利ツールを公開中!"
|
description: "Misskey向けの便利ツールを公開中!"
|
||||||
|
menuToggle: "メニュー"
|
||||||
_forUsers:
|
_forUsers:
|
||||||
title: "Misskeyユーザー向け"
|
title: "Misskeyユーザー向け"
|
||||||
|
|
||||||
_mfmPlayground:
|
_mfmPlayground:
|
||||||
title: "MFMお試しコーナー"
|
title: "MFMお試しコーナー"
|
||||||
description: "MFMを自由に練習できます!Misskeyの投稿画面・ノートの画面を再現!"
|
description: "MFMを自由に練習できます!Misskeyの投稿画面・ノートの画面を再現!"
|
||||||
|
preview: "プレビュー"
|
||||||
|
disclaimer: "ここに表示される通りに描画されるとは限りません。コードのシンタックスハイライトには対応していません。"
|
||||||
|
mfm: "MFM"
|
||||||
|
character: "{0} 文字"
|
||||||
|
domain: "表示を再現するサーバー"
|
||||||
|
noteIt: "ノート"
|
||||||
|
clearEmojiCache: "絵文字のキャッシュを削除"
|
||||||
|
|
||||||
_api:
|
_api:
|
||||||
_permissions:
|
_permissions:
|
||||||
|
@ -23,8 +23,8 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
css: [
|
css: [
|
||||||
"@/assets/css/mfm.scss",
|
|
||||||
"github-markdown-css/github-markdown.css",
|
"github-markdown-css/github-markdown.css",
|
||||||
|
"@/assets/css/mfm.scss",
|
||||||
"@/assets/css/tailwind.css",
|
"@/assets/css/tailwind.css",
|
||||||
"@/assets/css/bootstrap-forms.scss",
|
"@/assets/css/bootstrap-forms.scss",
|
||||||
],
|
],
|
||||||
|
@ -17,23 +17,19 @@
|
|||||||
</GHero>
|
</GHero>
|
||||||
<div class="pb-12 lg:mt-12 pt-6 bg-white dark:bg-slate-950">
|
<div class="pb-12 lg:mt-12 pt-6 bg-white dark:bg-slate-950">
|
||||||
<div class="container mx-auto max-w-screen-xl px-6 space-y-6 lg:space-y-8">
|
<div class="container mx-auto max-w-screen-xl px-6 space-y-6 lg:space-y-8">
|
||||||
<GLocalNav :items="[
|
<GLocalNav :items="sections.map((v) => ({
|
||||||
{
|
name: $t(v.title),
|
||||||
name: $t('_tools._forUsers.title'),
|
anchor: '#' + v.title.replaceAll('.', '_'),
|
||||||
anchor: '#forUsers',
|
}))" />
|
||||||
},
|
<section :id="section.title.replaceAll('.', '_')" v-for="section in sections">
|
||||||
]" />
|
|
||||||
<section>
|
|
||||||
<h2 class="text-2xl lg:text-3xl font-bold mb-4">
|
<h2 class="text-2xl lg:text-3xl font-bold mb-4">
|
||||||
{{ $t(`_tools._forUsers.title`) }}
|
{{ $t(section.title) }}
|
||||||
</h2>
|
</h2>
|
||||||
<GLinks :wide="true" :items="[
|
<GLinks :wide="true" :items="section.items.map((e) => ({
|
||||||
{
|
title: $t(e.i18n),
|
||||||
to: localePath('/tools/mfm-playground/'),
|
description: $t(e.description),
|
||||||
title: $t('_mfmPlayground.title'),
|
to: localePath(e.to),
|
||||||
description: $t('_mfmPlayground.description'),
|
}))" />
|
||||||
}
|
|
||||||
]" />
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -41,6 +37,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import sections from '@/assets/data/toolsNav';
|
||||||
|
|
||||||
const localePath = useLocalePath();
|
const localePath = useLocalePath();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -3,23 +3,80 @@
|
|||||||
<h1 class='text-2xl lg:text-3xl font-bold mb-4'>
|
<h1 class='text-2xl lg:text-3xl font-bold mb-4'>
|
||||||
{{ $t(`_mfmPlayground.title`) }}
|
{{ $t(`_mfmPlayground.title`) }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class='rounded-lg grid grid-cols-2'>
|
<div class='rounded-lg grid md:grid-cols-2 gap-4'>
|
||||||
WIP
|
<div>
|
||||||
|
{{ $t('_mfmPlayground.preview') }}
|
||||||
|
<div :class="$style.mfmRoot" class="mb-2 bg-white dark:bg-[#212529] border-gray-200 dark:border-gray-600">
|
||||||
|
<MkMfm :text="mfmText ?? ''" :baseHost="mfmHost" />
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ $t('_mfmPlayground.disclaimer') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="flex">
|
||||||
|
<label for="mfmPlaygroundMFM">{{ $t('_mfmPlayground.mfm') }}</label>
|
||||||
|
<div
|
||||||
|
class="ml-auto"
|
||||||
|
:class="[(mfmText?.length ?? 0) >= 5000 && 'font-bold text-red-600']"
|
||||||
|
>
|
||||||
|
{{ $t('_mfmPlayground.character', [ (mfmText?.length ?? 0) ]) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
:rows="(mfmText || '').split('\n').length >= 8 ? (mfmText || '').split('\n').length + 5 : 10"
|
||||||
|
class="form-control"
|
||||||
|
id="mfmPlaygroundMFM"
|
||||||
|
v-model="mfmText"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="mfmPlaygroundDomain">{{ $t('_mfmPlayground.domain') }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="mfmPlaygroundDomain" v-model="mfmHost" />
|
||||||
|
<GNuxtLink :to="shareURL" target="_blank" class="btn btn-primary !text-white">{{ $t('_mfmPlayground.noteIt') }}<SendIco class="ml-1" /></GNuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="w-1/2 md:w-1/3 pr-2 col-form-label">
|
||||||
|
絵文字が表示されないとき
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 md:w-2/3">
|
||||||
|
<button @click="clearEmojiCache()" class="btn w-full btn-outline-primary hover:!text-white">{{ $t('_mfmPlayground.clearEmojiCache') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
|
import SendIco from 'bi/send-fill.svg';
|
||||||
|
import { parseURL, withQuery } from 'ufo';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'slim',
|
layout: 'tools',
|
||||||
});
|
});
|
||||||
|
|
||||||
const mfmText = ref<string>();
|
const mfmText = ref<string>();
|
||||||
const mfmHost = ref<string>('misskey.io');
|
const mfmHost = ref<string>('misskey.io');
|
||||||
|
const shareURL = computed(() => {
|
||||||
|
const domain = 'https://' + (parseURL(mfmHost.value).host ?? mfmHost.value) + '/share';
|
||||||
|
return withQuery(domain, { text: mfmText.value });
|
||||||
|
})
|
||||||
|
|
||||||
|
function clearEmojiCache() {
|
||||||
|
if (process.client) {
|
||||||
|
localStorage.clear();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
.mfmRoot {
|
.mfmRoot {
|
||||||
|
@apply rounded-lg p-6 border break-words overflow-hidden;
|
||||||
font-family: Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;
|
font-family: Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user