mirror of
https://iceshrimp.dev/Crimekillz/jointrashposs.git
synced 2024-11-22 08:53:49 +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 {
|
||||
0% { opacity: 1; transform: scale(1); }
|
||||
30% { opacity: 1; transform: scale(1); }
|
||||
|
@ -127,28 +127,42 @@ html.light .markdown-body {
|
||||
@apply font-content-sans;
|
||||
}
|
||||
|
||||
.markdown-body h1 > a[href^='#'],
|
||||
.markdown-body h2 > a[href^='#'],
|
||||
.markdown-body h3 > a[href^='#'],
|
||||
.markdown-body h4 > a[href^='#'],
|
||||
.markdown-body h5 > a[href^='#'],
|
||||
.markdown-body h6 > a[href^='#'] {
|
||||
.markdown-body > h1 > a[href^='#'],
|
||||
.markdown-body :not(.ignore) h1 > a[href^='#'],
|
||||
.markdown-body > h2 > a[href^='#'],
|
||||
.markdown-body :not(.ignore) h2 > a[href^='#'],
|
||||
.markdown-body > h3 > 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);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown-body h1 > a[href^='#']:hover,
|
||||
.markdown-body h2 > a[href^='#']:hover,
|
||||
.markdown-body h3 > a[href^='#']:hover,
|
||||
.markdown-body h4 > a[href^='#']:hover,
|
||||
.markdown-body h5 > a[href^='#']:hover,
|
||||
.markdown-body h6 > a[href^='#']:hover {
|
||||
.markdown-body > h1 > a[href^='#']:hover,
|
||||
.markdown-body :not(.ignore) h1 > a[href^='#']:hover,
|
||||
.markdown-body > h2 > a[href^='#']:hover,
|
||||
.markdown-body :not(.ignore) h2 > a[href^='#']:hover,
|
||||
.markdown-body > h3 > 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;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
.markdown-body > ul ,
|
||||
.markdown-body :not(.ignore) ul {
|
||||
list-style: disc;
|
||||
}
|
||||
.markdown-body ol {
|
||||
.markdown-body > ol ,
|
||||
.markdown-body :not(.ignore) ol {
|
||||
list-style: decimal;
|
||||
}
|
@ -1,10 +1,19 @@
|
||||
import { FunctionalComponent } from "nuxt/dist/app/compat/capi";
|
||||
import GHIcon from "bi/github.svg";
|
||||
|
||||
export type NavSection = {
|
||||
/** セクションタイトル 翻訳キー */
|
||||
title: string;
|
||||
/** アイテム */
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
/** ナビゲーションバー アイテム */
|
||||
type NavItem = {
|
||||
export type NavItem = {
|
||||
/** 翻訳キー */
|
||||
i18n: string;
|
||||
/** 説明文 翻訳キー */
|
||||
description?: 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 { parseURL } from 'ufo';
|
||||
|
||||
/**
|
||||
* オブジェクトのパス文字列からオブジェクトの内部を参照
|
||||
* @param o オブジェクト
|
||||
* @param s パス
|
||||
* @returns パスの先にあるもの
|
||||
*/
|
||||
export function resolveObjPath(o: object, s: string): any {
|
||||
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||
s = s.replace(/^\./, ''); // strip a leading dot
|
||||
@ -15,33 +22,33 @@ export function resolveObjPath(o: object, s: string): any {
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* URLがドメイン内部かどうかを判別
|
||||
* @param link 判別したいURL
|
||||
* @param base ローカルの基準となるドメイン
|
||||
*/
|
||||
export function isLocalPath(link: string, base?: string): boolean {
|
||||
let baseUrl;
|
||||
|
||||
if (base) {
|
||||
baseUrl = base;
|
||||
} else {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
baseUrl = runtimeConfig.public.baseUrl;
|
||||
}
|
||||
const rootDomain = new URL(baseUrl);
|
||||
try {
|
||||
const url = new URL(link);
|
||||
if (!url.hostname || rootDomain.hostname === url.hostname) {
|
||||
return true;
|
||||
} else if (rootDomain.hostname !== url.hostname) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch(error) {
|
||||
if(link !== '') {
|
||||
return true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const rootDomain = parseURL(base);
|
||||
const url = parseURL(link);
|
||||
|
||||
return (!url.host || rootDomain.host === url.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* ナビゲーションObjectを合致する条件まで深掘り
|
||||
* @param obj ナビゲーションObject
|
||||
* @param condition 深掘りを停止する条件
|
||||
* @returns 深掘りしたナビゲーションObject
|
||||
*/
|
||||
export const findDeepObject = (obj: NavItem, condition: (v: NavItem) => boolean): NavItem | null => {
|
||||
if (condition(obj)) {
|
||||
return obj;
|
||||
|
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<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>
|
||||
<MkMfm :text="text" />
|
||||
<div class="ignore" :class="$style.mfmRoot">
|
||||
<MkMfm :text="text" baseHost="misskey.io" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -12,3 +14,9 @@ defineProps<{
|
||||
text: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.mfmRoot img {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
@ -8,7 +8,7 @@
|
||||
</GNuxtLink>
|
||||
</div>
|
||||
<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']"
|
||||
>
|
||||
<li v-for="item in NavData.center">
|
||||
|
@ -8,7 +8,8 @@
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
/**
|
||||
@ -26,11 +27,8 @@ const realHref = computed(() => {
|
||||
const rhf = rawProps.to ?? rawProps.href;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
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 * as mfm from 'mfm-js';
|
||||
import MkGoogle from '@/components/mk/Google.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';
|
||||
|
||||
const QUOTE_STYLE = `
|
||||
@ -21,6 +28,7 @@ export default function(props: {
|
||||
isNote?: boolean;
|
||||
emojiUrls?: string[];
|
||||
rootScale?: number;
|
||||
baseHost?: string;
|
||||
}) {
|
||||
const isNote = props.isNote !== undefined ? props.isNote : true;
|
||||
|
||||
@ -226,22 +234,26 @@ export default function(props: {
|
||||
return [h(ProseAVue, {
|
||||
key: Math.random(),
|
||||
href: token.props.url,
|
||||
target: '_blank',
|
||||
rel: 'nofollow noopener',
|
||||
}, token.props.url)];
|
||||
}
|
||||
|
||||
case 'link': {
|
||||
return [h(ProseAVue, {
|
||||
return [h(NuxtLink, {
|
||||
key: Math.random(),
|
||||
to: token.props.url,
|
||||
target: '_blank',
|
||||
rel: 'nofollow noopener',
|
||||
}, genEl(token.children, scale))];
|
||||
}
|
||||
|
||||
case 'mention': {
|
||||
//@ts-ignore
|
||||
return [h(MkMention, {
|
||||
key: Math.random(),
|
||||
host: (token.props.host) || host,
|
||||
host: (token.props.host) ?? props.baseHost,
|
||||
localHost: props.baseHost,
|
||||
username: token.props.username,
|
||||
})];
|
||||
}
|
||||
@ -249,7 +261,7 @@ export default function(props: {
|
||||
case 'hashtag': {
|
||||
return [h(NuxtLink, {
|
||||
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);',
|
||||
}, `#${token.props.hashtag}`)];
|
||||
}
|
||||
@ -268,34 +280,20 @@ export default function(props: {
|
||||
|
||||
case 'emojiCode': {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (props.author?.host == null) {
|
||||
return [h(MkCustomEmoji, {
|
||||
key: Math.random(),
|
||||
name: token.props.name,
|
||||
normal: props.plain,
|
||||
host: null,
|
||||
host: props.baseHost,
|
||||
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': {
|
||||
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': {
|
||||
@ -306,6 +304,16 @@ export default function(props: {
|
||||
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': {
|
||||
return [h(MkGoogle, {
|
||||
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
|
||||
@ -75,6 +80,8 @@ https://example.com
|
||||
: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:
|
||||
title: "ツール集"
|
||||
index: "ツール集 ホーム画面"
|
||||
description: "Misskey向けの便利ツールを公開中!"
|
||||
menuToggle: "メニュー"
|
||||
_forUsers:
|
||||
title: "Misskeyユーザー向け"
|
||||
|
||||
_mfmPlayground:
|
||||
title: "MFMお試しコーナー"
|
||||
description: "MFMを自由に練習できます!Misskeyの投稿画面・ノートの画面を再現!"
|
||||
preview: "プレビュー"
|
||||
disclaimer: "ここに表示される通りに描画されるとは限りません。コードのシンタックスハイライトには対応していません。"
|
||||
mfm: "MFM"
|
||||
character: "{0} 文字"
|
||||
domain: "表示を再現するサーバー"
|
||||
noteIt: "ノート"
|
||||
clearEmojiCache: "絵文字のキャッシュを削除"
|
||||
|
||||
_api:
|
||||
_permissions:
|
||||
|
@ -23,8 +23,8 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
css: [
|
||||
"@/assets/css/mfm.scss",
|
||||
"github-markdown-css/github-markdown.css",
|
||||
"@/assets/css/mfm.scss",
|
||||
"@/assets/css/tailwind.css",
|
||||
"@/assets/css/bootstrap-forms.scss",
|
||||
],
|
||||
|
@ -17,23 +17,19 @@
|
||||
</GHero>
|
||||
<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">
|
||||
<GLocalNav :items="[
|
||||
{
|
||||
name: $t('_tools._forUsers.title'),
|
||||
anchor: '#forUsers',
|
||||
},
|
||||
]" />
|
||||
<section>
|
||||
<GLocalNav :items="sections.map((v) => ({
|
||||
name: $t(v.title),
|
||||
anchor: '#' + v.title.replaceAll('.', '_'),
|
||||
}))" />
|
||||
<section :id="section.title.replaceAll('.', '_')" v-for="section in sections">
|
||||
<h2 class="text-2xl lg:text-3xl font-bold mb-4">
|
||||
{{ $t(`_tools._forUsers.title`) }}
|
||||
{{ $t(section.title) }}
|
||||
</h2>
|
||||
<GLinks :wide="true" :items="[
|
||||
{
|
||||
to: localePath('/tools/mfm-playground/'),
|
||||
title: $t('_mfmPlayground.title'),
|
||||
description: $t('_mfmPlayground.description'),
|
||||
}
|
||||
]" />
|
||||
<GLinks :wide="true" :items="section.items.map((e) => ({
|
||||
title: $t(e.i18n),
|
||||
description: $t(e.description),
|
||||
to: localePath(e.to),
|
||||
}))" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@ -41,6 +37,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import sections from '@/assets/data/toolsNav';
|
||||
|
||||
const localePath = useLocalePath();
|
||||
</script>
|
||||
|
||||
|
@ -3,23 +3,80 @@
|
||||
<h1 class='text-2xl lg:text-3xl font-bold mb-4'>
|
||||
{{ $t(`_mfmPlayground.title`) }}
|
||||
</h1>
|
||||
<div class='rounded-lg grid grid-cols-2'>
|
||||
WIP
|
||||
<div class='rounded-lg grid md:grid-cols-2 gap-4'>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import SendIco from 'bi/send-fill.svg';
|
||||
import { parseURL, withQuery } from 'ufo';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'slim',
|
||||
layout: 'tools',
|
||||
});
|
||||
|
||||
const mfmText = ref<string>();
|
||||
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>
|
||||
|
||||
<style module>
|
||||
.mfmRoot {
|
||||
@apply rounded-lg p-6 border break-words overflow-hidden;
|
||||
font-family: Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user