(add) mfm preview

This commit is contained in:
kakkokari-gtyih 2023-07-12 12:57:32 +09:00
parent 1125dd7923
commit 447024f3c1
13 changed files with 1089 additions and 19 deletions

199
assets/css/mfm.scss Normal file
View File

@ -0,0 +1,199 @@
@keyframes blink {
0% { opacity: 1; transform: scale(1); }
30% { opacity: 1; transform: scale(1); }
90% { opacity: 0; transform: scale(0.5); }
}
@keyframes tada {
from {
transform: scale3d(1, 1, 1);
}
10%,
20% {
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
}
30%,
50%,
70%,
90% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
}
40%,
60%,
80% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
}
to {
transform: scale3d(1, 1, 1);
}
}
._anime_bounce {
will-change: transform;
animation: bounce ease 0.7s;
animation-iteration-count: 1;
transform-origin: 50% 50%;
}
._anime_bounce_ready {
will-change: transform;
transform: scaleX(0.90) scaleY(0.90) ;
}
._anime_bounce_standBy {
transition: transform 0.1s ease;
}
@keyframes bounce {
0% {
transform: scaleX(0.90) scaleY(0.90) ;
}
19% {
transform: scaleX(1.10) scaleY(1.10) ;
}
48% {
transform: scaleX(0.95) scaleY(0.95) ;
}
100% {
transform: scaleX(1.00) scaleY(1.00) ;
}
}
// MFM -----------------------------
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s;
&:hover {
filter: blur(0px);
}
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {
--mfm-zoom-size: 600%;
}
.mfm-x2, .mfm-x3, .mfm-x4 {
font-size: var(--mfm-zoom-size);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* disabled */
font-size: 100%;
}
}
}
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes mfm-spinX {
0% { transform: perspective(128px) rotateX(0deg); }
100% { transform: perspective(128px) rotateX(360deg); }
}
@keyframes mfm-spinY {
0% { transform: perspective(128px) rotateY(0deg); }
100% { transform: perspective(128px) rotateY(360deg); }
}
@keyframes mfm-jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes mfm-bounce {
0% { transform: translateY(0) scale(1, 1); }
25% { transform: translateY(-16px) scale(1, 1); }
50% { transform: translateY(0) scale(1, 1); }
75% { transform: translateY(0) scale(1.5, 0.75); }
100% { transform: translateY(0) scale(1, 1); }
}
// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
// let css = '';
// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
@keyframes mfm-twitch {
0% { transform: translate(7px, -2px) }
5% { transform: translate(-3px, 1px) }
10% { transform: translate(-7px, -1px) }
15% { transform: translate(0px, -1px) }
20% { transform: translate(-8px, 6px) }
25% { transform: translate(-4px, -3px) }
30% { transform: translate(-4px, -6px) }
35% { transform: translate(-8px, -8px) }
40% { transform: translate(4px, 6px) }
45% { transform: translate(-3px, 1px) }
50% { transform: translate(2px, -10px) }
55% { transform: translate(-7px, 0px) }
60% { transform: translate(-2px, 4px) }
65% { transform: translate(3px, -8px) }
70% { transform: translate(6px, 7px) }
75% { transform: translate(-7px, -2px) }
80% { transform: translate(-7px, -8px) }
85% { transform: translate(9px, 3px) }
90% { transform: translate(-3px, -2px) }
95% { transform: translate(-10px, 2px) }
100% { transform: translate(-2px, -6px) }
}
// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
// let css = '';
// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
@keyframes mfm-shake {
0% { transform: translate(-3px, -1px) rotate(-8deg) }
5% { transform: translate(0px, -1px) rotate(-10deg) }
10% { transform: translate(1px, -3px) rotate(0deg) }
15% { transform: translate(1px, 1px) rotate(11deg) }
20% { transform: translate(-2px, 1px) rotate(1deg) }
25% { transform: translate(-1px, -2px) rotate(-2deg) }
30% { transform: translate(-1px, 2px) rotate(-3deg) }
35% { transform: translate(2px, 1px) rotate(6deg) }
40% { transform: translate(-2px, -3px) rotate(-9deg) }
45% { transform: translate(0px, -1px) rotate(-12deg) }
50% { transform: translate(1px, 2px) rotate(10deg) }
55% { transform: translate(0px, -3px) rotate(8deg) }
60% { transform: translate(1px, -1px) rotate(8deg) }
65% { transform: translate(0px, -1px) rotate(-7deg) }
70% { transform: translate(-1px, -3px) rotate(6deg) }
75% { transform: translate(0px, -2px) rotate(4deg) }
80% { transform: translate(-2px, -1px) rotate(3deg) }
85% { transform: translate(1px, -3px) rotate(-10deg) }
90% { transform: translate(1px, 0px) rotate(3deg) }
95% { transform: translate(-2px, 0px) rotate(-3deg) }
100% { transform: translate(2px, 1px) rotate(2deg) }
}
@keyframes mfm-rubberBand {
from { transform: scale3d(1, 1, 1); }
30% { transform: scale3d(1.25, 0.75, 1); }
40% { transform: scale3d(0.75, 1.25, 1); }
50% { transform: scale3d(1.15, 0.85, 1); }
65% { transform: scale3d(0.95, 1.05, 1); }
75% { transform: scale3d(1.05, 0.95, 1); }
to { transform: scale3d(1, 1, 1); }
}
@keyframes mfm-rainbow {
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
}

View File

@ -0,0 +1,13 @@
<template>
<div class="rounded-lg border border-slate-200 dark:border-slate-800 p-6 mfm-root mb-4">
<MkMfm :text="text" />
</div>
</template>
<script setup lang="ts">
import MkMfm from '@/components/mk/Mfm';
defineProps<{
text: string;
}>();
</script>

View File

@ -21,7 +21,6 @@ let realTarget = props.target;
try {
const url = new URL(props.href);
console.log(url);
if (!url.hostname || rootDomain.hostname === url.hostname) {
realHref = localePath(realHref);
}

View File

@ -0,0 +1,151 @@
<script setup lang="ts">
import type { PropType } from "vue";
const props = defineProps({
links: {
type: Array as PropType<any>,
default: () => [],
},
level: {
type: Number,
default: 0,
},
max: {
type: Number,
default: null,
},
parent: {
type: Object as PropType<any>,
default: null,
},
});
const route = useRoute();
const collapsedMap = useState(
`docus-docs-aside-collapse-map-${props.parent?._path || "/"}`,
() => {
if (props.level === 0) {
return {};
}
return (props.links as any[])
.filter((link) => !!link.children)
.reduce((map, link) => {
map[link._path] = true;
return map;
}, {});
}
);
const isActive = (link: any) => {
return route.path === link._path;
};
const isCollapsed = (link: any) => {
if (link.children) {
// Directory has been toggled manually, use its state
if (typeof collapsedMap.value[link._path] !== "undefined") {
return collapsedMap.value[link._path];
}
// Check if aside.collapsed has been set in YML
if ([true, false].includes(link?.aside?.collapsed)) {
return link.aside.collapsed;
}
// Return value grabbed from the link
if (link?.collapsed) {
return link?.collapsed;
}
}
return false;
};
const toggleCollapse = (link: any) =>
(collapsedMap.value[link._path] = !isCollapsed(link));
const hasNesting = computed(() =>
props.links.some((link: any) => link.children)
);
</script>
<template>
<ul class="docs-aside-tree">
<li
v-for="link in links"
:key="link._path"
:class="{
'has-parent-icon': parent?.icon,
'has-children': level > 0 && link.children,
bordered: level > 0 || !hasNesting,
active: isActive(link),
}"
>
<button
v-if="link.children"
class="title-collapsible-button"
@click="toggleCollapse(link)"
>
<span class="content">
<Icon
v-if="link?.navigation?.icon || link.icon"
:name="link?.navigation?.icon || link.icon"
class="icon"
/>
<span>{{
link?.navigation?.title || link.title || link._path
}}</span>
</span>
<span>
<Icon
:name="
isCollapsed(link)
? 'lucide:chevrons-up-down'
: 'lucide:chevrons-down-up'
"
class="collapsible-icon"
/>
</span>
</button>
<NuxtLink
v-else
:to="link.redirect ? link.redirect : link._path"
class="link"
:exact="link.exact"
:class="{
padded: level > 0 || !hasNesting,
active: isActive(link),
}"
>
<span class="content">
<Icon
v-if="link?.navigation?.icon || link.icon"
:name="link?.navigation?.icon || link.icon"
class="icon"
/>
<span>{{
link?.navigation?.title || link.title || link._path
}}</span>
</span>
</NuxtLink>
<DocsAsideTree
v-show="!isCollapsed(link)"
v-if="
link.children?.length && (max === null || level + 1 < max)
"
:links="link.children"
:level="level + 1"
:parent="link"
:max="max"
class="recursive"
/>
</li>
</ul>
</template>
<style scoped lang="ts">
</style>

54
components/mk/Google.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<div :class="$style.root">
<input v-model="query" :class="$style.input" type="search" :placeholder="q">
<button :class="$style.button" @click="search"><SearchIco /> 検索</button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import SearchIco from 'bi/search.svg';
const props = defineProps<{
q: string;
}>();
const query = ref(props.q);
const search = () => {
window.open(`https://www.google.com/search?q=${query.value}`, '_blank');
};
</script>
<style lang="scss" module>
.root {
display: flex;
margin: 8px 0;
}
.input {
@apply border-slate-200;
flex-shrink: 1;
padding: 10px;
width: 100%;
height: 40px;
font-size: 16px;
border: solid 1px;
border-radius: 4px 0 0 4px;
-webkit-appearance: textfield;
}
.button {
@apply border-slate-200;
flex-shrink: 0;
margin: 0;
padding: 0 16px;
border: solid 1px;
border-left: none;
border-radius: 0 4px 4px 0;
&:active {
box-shadow: 0 2px 4px rgba(#000, 0.15) inset;
}
}
</style>

333
components/mk/Mfm.ts Normal file
View File

@ -0,0 +1,333 @@
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 ProseAVue from '@/components/content/ProseA.vue';
const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
export default function(props: {
text: string;
plain?: boolean;
nowrap?: boolean;
isNote?: boolean;
emojiUrls?: string[];
rootScale?: number;
}) {
const isNote = props.isNote !== undefined ? props.isNote : true;
if (props.text == null || props.text === '') return;
const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
const useAnim = true;
/**
* Gen Vue Elements from MFM AST
* @param ast MFM AST
* @param scale How times large the text is
*/
const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
switch (token.type) {
case 'text': {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!props.plain) {
const res: (VNode | string)[] = [];
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, scale))];
}
case 'strike': {
return [h('del', genEl(token.children, scale))];
}
case 'italic': {
return h('i', {
style: 'font-style: oblique;',
}, genEl(token.children, scale));
}
case 'fn': {
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
let style;
switch (token.props.name) {
case 'tada': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
break;
}
case 'jelly': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
break;
}
case 'twitch': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
break;
}
case 'shake': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
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';
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
break;
}
case 'jump': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
break;
}
case 'bounce': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
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, scale * 2));
}
case 'x3': {
return h('span', {
class: 'mfm-x3',
}, genEl(token.children, scale * 3));
}
case 'x4': {
return h('span', {
class: 'mfm-x4',
}, genEl(token.children, scale * 4));
}
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: '_mfm_blur_',
}, genEl(token.children, scale));
}
case 'rainbow': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
if (!useAnim) {
return genEl(token.children, scale);
}
return h(MkSparkle, {}, genEl(token.children, scale));
}
case 'rotate': {
const degrees = parseFloat(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 '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});`;
scale = scale * Math.max(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;
}
}
if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
} else {
return h('span', {
style: 'display: inline-block; ' + style,
}, genEl(token.children, scale));
}
}
case 'small': {
return [h('small', {
style: 'opacity: 0.7;',
}, genEl(token.children, scale))];
}
case 'center': {
return [h('div', {
style: 'text-align:center;',
}, genEl(token.children, scale))];
}
case 'url': {
return [h(ProseAVue, {
key: Math.random(),
href: token.props.url,
rel: 'nofollow noopener',
}, token.props.url)];
}
case 'link': {
return [h(NuxtLink, {
key: Math.random(),
to: token.props.url,
rel: 'nofollow noopener',
}, genEl(token.children, scale))];
}
case 'mention': {
return [h(MkMention, {
key: Math.random(),
host: (token.props.host) || host,
username: token.props.username,
})];
}
case 'hashtag': {
return [h(NuxtLink, {
key: Math.random(),
to: `https://misskey.io/tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:rgb(255, 145, 86);',
}, `#${token.props.hashtag}`)];
}
case 'quote': {
if (!props.nowrap) {
return [h('div', {
style: QUOTE_STYLE,
}, genEl(token.children, scale))];
} else {
return [h('span', {
style: QUOTE_STYLE,
}, genEl(token.children, scale))];
}
}
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,
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)];
}
case 'mathInline': {
return [h('code', token.props.formula)];
}
case 'mathBlock': {
return [h('code', token.props.formula)];
}
case 'search': {
return [h(MkGoogle, {
key: Math.random(),
q: token.props.query,
})];
}
case 'plain': {
return [h('span', genEl(token.children, scale))];
}
default: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
console.error('unrecognized ast type:', (token as any).type);
return [];
}
}
}).flat(Infinity) as (VNode | string)[];
return h('span', {
// https://codeday.me/jp/qa/20190424/690106.html
style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
}, genEl(ast, props.rootScale ?? 1));
}

135
components/mk/Sparkle.vue Normal file
View File

@ -0,0 +1,135 @@
<template>
<span :class="$style.root">
<span ref="el" style="display: inline-block">
<slot></slot>
</span>
<!-- なぜか path に対する key が機能しないため
<svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg">
<path v-for="particle in particles" :key="particle.id" style="transform-origin: center; transform-box: fill-box;"
:transform="`translate(${particle.x} ${particle.y})`"
:fill="particle.color"
d="M29.427,2.011C29.721,0.83 30.782,0 32,0C33.218,0 34.279,0.83 34.573,2.011L39.455,21.646C39.629,22.347 39.991,22.987 40.502,23.498C41.013,24.009 41.653,24.371 42.354,24.545L61.989,29.427C63.17,29.721 64,30.782 64,32C64,33.218 63.17,34.279 61.989,34.573L42.354,39.455C41.653,39.629 41.013,39.991 40.502,40.502C39.991,41.013 39.629,41.653 39.455,42.354L34.573,61.989C34.279,63.17 33.218,64 32,64C30.782,64 29.721,63.17 29.427,61.989L24.545,42.354C24.371,41.653 24.009,41.013 23.498,40.502C22.987,39.991 22.347,39.629 21.646,39.455L2.011,34.573C0.83,34.279 0,33.218 0,32C0,30.782 0.83,29.721 2.011,29.427L21.646,24.545C22.347,24.371 22.987,24.009 23.498,23.498C24.009,22.987 24.371,22.347 24.545,21.646L29.427,2.011Z"
>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 0 0"
to="360 0 0"
:dur="`${particle.dur}ms`"
repeatCount="indefinite"
additive="sum"
/>
<animateTransform
attributeName="transform"
attributeType="XML"
type="scale"
:values="`0; ${particle.size}; 0`"
:dur="`${particle.dur}ms`"
repeatCount="indefinite"
additive="sum"
/>
</path>
</svg>
-->
<!-- MFMで上位レイヤーに表示されるためリンクをクリックできるようにstyleにpointer-events: none;を付与 -->
<svg
v-for="particle in particles"
:key="particle.id"
:width="width"
:height="height"
:viewBox="`0 0 ${width} ${height}`"
xmlns="http://www.w3.org/2000/svg"
style="
position: absolute;
top: -32px;
left: -32px;
pointer-events: none;
"
>
<path
style="transform-origin: center; transform-box: fill-box"
:transform="`translate(${particle.x} ${particle.y})`"
:fill="particle.color"
d="M29.427,2.011C29.721,0.83 30.782,0 32,0C33.218,0 34.279,0.83 34.573,2.011L39.455,21.646C39.629,22.347 39.991,22.987 40.502,23.498C41.013,24.009 41.653,24.371 42.354,24.545L61.989,29.427C63.17,29.721 64,30.782 64,32C64,33.218 63.17,34.279 61.989,34.573L42.354,39.455C41.653,39.629 41.013,39.991 40.502,40.502C39.991,41.013 39.629,41.653 39.455,42.354L34.573,61.989C34.279,63.17 33.218,64 32,64C30.782,64 29.721,63.17 29.427,61.989L24.545,42.354C24.371,41.653 24.009,41.013 23.498,40.502C22.987,39.991 22.347,39.629 21.646,39.455L2.011,34.573C0.83,34.279 0,33.218 0,32C0,30.782 0.83,29.721 2.011,29.427L21.646,24.545C22.347,24.371 22.987,24.009 23.498,23.498C24.009,22.987 24.371,22.347 24.545,21.646L29.427,2.011Z"
>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 0 0"
to="360 0 0"
:dur="`${particle.dur}ms`"
repeatCount="1"
additive="sum"
/>
<animateTransform
attributeName="transform"
attributeType="XML"
type="scale"
:values="`0; ${particle.size}; 0`"
:dur="`${particle.dur}ms`"
repeatCount="1"
additive="sum"
/>
</path>
</svg>
</span>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, shallowRef } from "vue";
const particles = ref([]);
const el = shallowRef<HTMLElement>();
const width = ref(0);
const height = ref(0);
const colors = ["#FF1493", "#00FFFF", "#FFE202", "#FFE202", "#FFE202"];
let stop = false;
let ro: ResizeObserver | undefined;
onMounted(() => {
ro = new ResizeObserver((entries, observer) => {
width.value = el.value?.offsetWidth + 64;
height.value = el.value?.offsetHeight + 64;
});
ro.observe(el.value);
const add = () => {
if (stop) return;
const x = Math.random() * (width.value - 64);
const y = Math.random() * (height.value - 64);
const sizeFactor = Math.random();
const particle = {
id: Math.random().toString(),
x,
y,
size: 0.2 + (sizeFactor / 10) * 3,
dur: 1000 + sizeFactor * 1000,
color: colors[Math.floor(Math.random() * colors.length)],
};
particles.value.push(particle);
window.setTimeout(() => {
particles.value = particles.value.filter(
(x) => x.id !== particle.id
);
}, particle.dur - 100);
window.setTimeout(() => {
add();
}, 500 + Math.random() * 500);
};
add();
});
onUnmounted(() => {
if (ro) ro.disconnect();
stop = true;
});
</script>
<style lang="scss" module>
.root {
position: relative;
display: inline-block;
}
</style>

View File

@ -22,10 +22,10 @@ MFMは、Markup language For Misskeyの略で、Misskeyの様々な場所で使
メンションについての詳細は[こちら](./mention.md)を参照してください。
:::
```:no-line-numbers
```
@alice
```
```:no-line-numbers
```
@alice@example.com
```
@ -35,65 +35,79 @@ MFMは、Markup language For Misskeyの略で、Misskeyの様々な場所で使
ハッシュタグについての詳細は[こちら](./hashtag.md)を参照してください。
:::
```:no-line-numbers
```
#misskey
```
<MfmPreview text="#misskey"></MfmPreview>
### URL
URLを示すことができます。
```:no-line-numbers
```
https://example.com
```
<MfmPreview text="https://example.com"></MfmPreview>
### リンク
文章の特定の範囲を、URLに紐づけることができます。
```:no-line-numbers
```
[example link](https://example.com)
```
<MfmPreview text="[example link](https://example.com)"></MfmPreview>
### カスタム絵文字
コロンでカスタム絵文字名を囲むと、カスタム絵文字を表示させることができます。
:::tip
カスタム絵文字についての詳細は[こちら](./custom-emoji.md)を参照してください。
:::
```:no-line-numbers
```
:misskey:
```
### 太字
文字を太く表示して強調することができます。
```:no-line-numbers
```
**太字**
```
<MfmPreview text="**太字**"></MfmPreview>
### 目立たなくする
内容を小さく・薄く表示させることができます。
```:no-line-numbers
```
<small>MisskeyでFediverseの世界が広がります</small>
```
<MfmPreview text="<small>MisskeyでFediverseの世界が広がります</small>"></MfmPreview>
### 引用
内容が引用であることを示すことができます。
```:no-line-numbers
```
> MisskeyでFediverseの世界が広がります
```
<MfmPreview text="> MisskeyでFediverseの世界が広がります"></MfmPreview>
### 中央寄せ
内容を中央寄せで表示させることができます。
```:no-line-numbers
```
<center>MisskeyでFediverseの世界が広がります</center>
```
<MfmPreview text="<center>MisskeyでFediverseの世界が広がります</center>"></MfmPreview>
### コード(インライン)
プログラムなどのコードをインラインでシンタックスハイライトします。
```:no-line-numbers
```
`<: "Hello, world!"`
```
### コード(ブロック)
複数行のプログラムなどのコードをブロックでシンタックスハイライトします。
```:no-line-numbers
```
~ (#i, 100) {
<: ? ((i % 15) = 0) "FizzBuzz"
.? ((i % 3) = 0) "Fizz"
@ -104,33 +118,189 @@ https://example.com
### 反転
内容を上下または左右に反転させます。
```:no-line-numbers
```
$[flip MisskeyでFediverseの世界が広がります]
$[flip.v MisskeyでFediverseの世界が広がります]
$[flip.h,v MisskeyでFediverseの世界が広がります]
```
<MfmPreview text="$[flip MisskeyでFediverseの世界が広がります]
$[flip.v MisskeyでFediverseの世界が広がります]
$[flip.h,v MisskeyでFediverseの世界が広がります]"></MfmPreview>
### フォント
内容のフォントを指定することができます。
```:no-line-numbers
```
$[font.serif MisskeyでFediverseの世界が広がります]
$[font.monospace MisskeyでFediverseの世界が広がります]
$[font.cursive MisskeyでFediverseの世界が広がります]
$[font.fantasy MisskeyでFediverseの世界が広がります]
```
<MfmPreview text="$[font.serif MisskeyでFediverseの世界が広がります]
$[font.monospace MisskeyでFediverseの世界が広がります]
$[font.cursive MisskeyでFediverseの世界が広がります]
$[font.fantasy MisskeyでFediverseの世界が広がります]"></MfmPreview>
### ぼかし
内容をぼかすことができます。ポインターを上に乗せるとはっきり見えるようになります。
```:no-line-numbers
```
$[blur MisskeyでFediverseの世界が広がります]
```
<MfmPreview text="$[blur MisskeyでFediverseの世界が広がります]"></MfmPreview>
### 検索
検索ボックスを表示できます。
```
misskey 検索
```
<MfmPreview text="misskey 検索"></MfmPreview>
### 文字色・背景色
文字色と背景色を変更することができます。
3,4,6桁のカラーコードで色を表現します。
```
$[fg.color=f00 赤字]
$[bg.color=ff0 黄背景]
```
<MfmPreview text="$[fg.color=f00 赤字]
$[bg.color=ff0 黄背景]"></MfmPreview>
### 角度変更
指定した角度で回転させます。
```
$[rotate.deg=30 misskey]
```
<MfmPreview text="$[rotate.deg=30 misskey]"></MfmPreview>
### 位置変更
位置をずらすことができます。
```
😏$[position.x=0.8,y=0.5 🍮]😀
```
<MfmPreview text="😏$[position.x=0.8,y=0.5 🍮]😀"></MfmPreview>
### 拡大
文字を引き延ばして表示します。
```
$[scale.x=4,y=2 🍮]
```
<MfmPreview text="$[scale.x=4,y=2 🍮]"></MfmPreview>
```
$[x2 x2]
$[x3 x3]
$[x4 x4]
```
<MfmPreview text="$[x2 x2]
$[x3 x3]
$[x4 x4]"></MfmPreview>
### アニメーション(びよんびよん)
```
$[jelly 🍮] $[jelly.speed=5s 🍮]
```
<MfmPreview text="$[x2 $[jelly 🍮] $[jelly.speed=5s 🍮]]"></MfmPreview>
### アニメーション(じゃーん)
```
$[tada 🍮] $[tada.speed=5s 🍮]
```
<MfmPreview text="$[x2 $[tada 🍮] $[tada.speed=5s 🍮]]"></MfmPreview>
### アニメーション(ジャンプ)
```
$[jump 🍮] $[jump.speed=5s 🍮]
```
<MfmPreview text="$[x2 $[jump 🍮] $[jump.speed=5s 🍮]]"></MfmPreview>
### アニメーション(バウンド)
```
$[bounce 🍮] $[bounce.speed=5s 🍮]
```
<MfmPreview text="$[x2 $[bounce 🍮] $[bounce.speed=5s 🍮]]"></MfmPreview>
### アニメーション(回転)
```
$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]
$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]
$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]
$[spin.speed=5s 🍮]
```
<MfmPreview text="$[x2 $[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]
$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]
$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]
$[spin.speed=5s 🍮]]"></MfmPreview>
### アニメーション(ぶるぶる)
```
$[shake 🍮] $[shake.speed=5s 🍮]
```
<MfmPreview text="$[x2 $[shake 🍮] $[shake.speed=5s 🍮]]"></MfmPreview>
### アニメーション(ブレ)
```
$[twitch 🍮] $[twitch.speed=5s 🍮]
```
<MfmPreview text="$[x2 $[twitch 🍮] $[twitch.speed=5s 🍮]]"></MfmPreview>
### レインボー
```
$[rainbow 🍮] $[rainbow.speed=5s 🍮]
$[rainbow 色なし文字]
$[rainbow $[fg.color=f0f 色付き文字]]
```
<MfmPreview text="$[rainbow 🍮] $[rainbow.speed=5s 🍮]
$[rainbow 色なし文字]
$[rainbow $[fg.color=f0f 色付き文字]]"></MfmPreview>
### キラキラ
```
$[sparkle 🍮]
```
<MfmPreview text="$[x2 $[sparkle 🍮]]"></MfmPreview>
### プレーン
内側の構文を全て無効にします。
```:no-line-numbers
```
<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>
```
<MfmPreview text="<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>"></MfmPreview>
## 開発者向け情報
MFMのパーサー実装はライブラリとして公開されており、簡単にクライアントにMFMを組み込むことが可能です。
- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - JavaScriptパーサー実装

View File

@ -23,6 +23,7 @@ export default defineNuxtConfig({
}
},
css: [
"@/assets/css/mfm.scss",
"github-markdown-css/github-markdown.css",
"@/assets/css/tailwind.css",
"@/assets/css/bootstrap-forms.scss",

View File

@ -21,6 +21,7 @@
"bootstrap-icons": "^1.10.5",
"github-markdown-css": "^5.2.0",
"meshline": "^3.1.6",
"mfm-js": "^0.23.3",
"nuxt": "^3.6.2",
"postcss": "^8.4.25",
"sass": "^1.63.6",

View File

@ -2,7 +2,7 @@
<div class="relative container mx-auto max-w-screen-xl p-6 lg:py-0 grid docs-root pb-12">
<div class="hidden lg:block">
<div class="sticky top-16 h-[calc(100vh-4rem)] overflow-y-scroll border-r border-slate-200 dark:border-slate-700 py-6 pr-6">
<DocsAsideTree :links="navigation" />
</div>
</div>
<div class="lg:p-6 w-full overflow-x-hidden">
@ -41,6 +41,7 @@ const currentLocaleISO = () => {
}
const { data } = await useAsyncData(`blog-${locale.value}-${slugs.join('-')}`, () => queryContent(`/${locale.value}/docs/${slugs.join('/')}`).findOne());
const { navigation } = await useAsyncData('navigation', () => fetchContentNavigation());
route.meta.title = data.value?.title;
</script>

View File

@ -18,7 +18,7 @@
<IndexDonation />
<IndexSponsors />
</main>
<GFooter class="relative bg-transparent dark:bg-transparent" />
<GFooter class="relative !bg-transparent dark:!bg-transparent" />
</div>
</template>

View File

@ -41,6 +41,9 @@ devDependencies:
meshline:
specifier: ^3.1.6
version: 3.1.6(three@0.154.0)
mfm-js:
specifier: ^0.23.3
version: 0.23.3
nuxt:
specifier: ^3.6.2
version: 3.6.2(@types/node@18.0.0)(sass@1.63.6)(typescript@5.1.6)
@ -3897,6 +3900,12 @@ packages:
three: 0.154.0
dev: true
/mfm-js@0.23.3:
resolution: {integrity: sha512-o8scYmbey6rMUmWAlT3k3ntt6khaCLdxlmHhAWV5wTTMj2OK1atQvZfRUq0SIVm1Jig08qlZg/ps71xUqrScNA==}
dependencies:
twemoji-parser: 14.0.0
dev: true
/micromark-core-commonmark@1.1.0:
resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==}
dependencies:
@ -5873,6 +5882,10 @@ packages:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
dev: true
/twemoji-parser@14.0.0:
resolution: {integrity: sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==}
dev: true
/type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}