mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-30 22:39:13 +01:00
599 lines
20 KiB
TypeScript
599 lines
20 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import { parse } from 'acorn';
|
|
import { generate } from 'astring';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name.js';
|
|
import type * as estree from 'estree';
|
|
|
|
function parseExpression(code: string): estree.Expression {
|
|
const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program;
|
|
const statement = program.body[0] as estree.ExpressionStatement;
|
|
return statement.expression;
|
|
}
|
|
|
|
describe(normalizeClass.name, () => {
|
|
it('should normalize string', () => {
|
|
expect(normalizeClass(parseExpression('"a b c"'))).toBe('a b c');
|
|
});
|
|
it('should trim redundant spaces', () => {
|
|
expect(normalizeClass(parseExpression('" a b c "'))).toBe('a b c');
|
|
});
|
|
it('should ignore undefined', () => {
|
|
expect(normalizeClass(parseExpression('undefined'))).toBe('');
|
|
});
|
|
it('should ignore non string literals', () => {
|
|
expect(normalizeClass(parseExpression('0'))).toBe('');
|
|
expect(normalizeClass(parseExpression('true'))).toBe('');
|
|
expect(normalizeClass(parseExpression('null'))).toBe('');
|
|
expect(normalizeClass(parseExpression('/I.D/'))).toBe('');
|
|
});
|
|
it('should not normalize identifiers', () => {
|
|
expect(normalizeClass(parseExpression('EScape'))).toBeNull();
|
|
});
|
|
it('should normalize recursively array', () => {
|
|
expect(normalizeClass(parseExpression('["from", ...["Utopia"]]'))).toBe('from Utopia');
|
|
expect(normalizeClass(parseExpression('["from", ...[Utopia]]'))).toBeNull();
|
|
});
|
|
it('should normalize recursively template literal', () => {
|
|
expect(normalizeClass(parseExpression('`name ${"shiho"} code ${33}`'))).toBe('name shiho code');
|
|
expect(normalizeClass(parseExpression('`name ${shiho.name} code ${33}`'))).toBeNull();
|
|
});
|
|
it('should normalize recursively binary expression', () => {
|
|
expect(normalizeClass(parseExpression('"mirage" + "mirror"'))).toBe('miragemirror');
|
|
expect(normalizeClass(parseExpression('"mirage" + mirror'))).toBeNull();
|
|
});
|
|
it('should normalize recursively object expression', () => {
|
|
expect(normalizeClass(parseExpression('({ a: true, b: "c" })'))).toBe('a b');
|
|
expect(normalizeClass(parseExpression('({ a: false, b: "c" })'))).toBe('b');
|
|
expect(normalizeClass(parseExpression('({ a: true, b: c })'))).toBeNull();
|
|
expect(normalizeClass(parseExpression('({ a: true, b: "c", ...({ d: true }) })'))).toBe('a b d');
|
|
expect(normalizeClass(parseExpression('({ a: true, [b]: "c" })'))).toBeNull();
|
|
expect(normalizeClass(parseExpression('({ a: true, b: false, c: !false, d: !!0 })'))).toBe('a c');
|
|
});
|
|
});
|
|
|
|
it('Composition API (standard)', () => {
|
|
const ast = parse(`
|
|
import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
|
|
import { M as MkContainer } from './MkContainer-!~{03M}~.js';
|
|
import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
|
|
import './photoswipe-!~{003}~.js';
|
|
|
|
const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
|
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
__name: "index.photos",
|
|
props: {
|
|
user: {}
|
|
},
|
|
setup(__props) {
|
|
const props = __props;
|
|
let fetching = ref(true);
|
|
let images = ref([]);
|
|
function thumbnail(image) {
|
|
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
|
}
|
|
onMounted(() => {
|
|
const image = [
|
|
"image/jpeg",
|
|
"image/webp",
|
|
"image/avif",
|
|
"image/png",
|
|
"image/gif",
|
|
"image/apng",
|
|
"image/vnd.mozilla.apng"
|
|
];
|
|
api("users/notes", {
|
|
userId: props.user.id,
|
|
fileType: image,
|
|
limit: 10
|
|
}).then((notes) => {
|
|
for (const note of notes) {
|
|
for (const file of note.files) {
|
|
images.value.push({
|
|
note,
|
|
file
|
|
});
|
|
}
|
|
}
|
|
fetching.value = false;
|
|
});
|
|
});
|
|
return (_ctx, _cache) => {
|
|
const _component_MkLoading = resolveComponent("MkLoading");
|
|
const _component_MkA = resolveComponent("MkA");
|
|
return openBlock(), createBlock(MkContainer, {
|
|
"max-height": 300,
|
|
foldable: true
|
|
}, {
|
|
icon: withCtx(() => [
|
|
_hoisted_1
|
|
]),
|
|
header: withCtx(() => [
|
|
createTextVNode(toDisplayString(unref(i18n).ts.images), 1)
|
|
]),
|
|
default: withCtx(() => [
|
|
createBaseVNode("div", {
|
|
class: normalizeClass(_ctx.$style.root)
|
|
}, [
|
|
unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true),
|
|
!unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
|
|
key: 1,
|
|
class: normalizeClass(_ctx.$style.stream)
|
|
}, [
|
|
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => {
|
|
return openBlock(), createBlock(_component_MkA, {
|
|
key: image.note.id + image.file.id,
|
|
class: normalizeClass(_ctx.$style.img),
|
|
to: unref(notePage)(image.note)
|
|
}, {
|
|
default: withCtx(() => [
|
|
createVNode(ImgWithBlurhash, {
|
|
hash: image.file.blurhash,
|
|
src: thumbnail(image.file),
|
|
title: image.file.name
|
|
}, null, 8, ["hash", "src", "title"])
|
|
]),
|
|
_: 2
|
|
}, 1032, ["class", "to"]);
|
|
}), 128))
|
|
], 2)) : createCommentVNode("", true),
|
|
!unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
|
|
key: 2,
|
|
class: normalizeClass(_ctx.$style.empty)
|
|
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)
|
|
], 2)
|
|
]),
|
|
_: 1
|
|
});
|
|
};
|
|
}
|
|
});
|
|
|
|
const root = "xenMW";
|
|
const stream = "xaZzf";
|
|
const img = "xtA8t";
|
|
const empty = "xhYKj";
|
|
const style0 = {
|
|
root: root,
|
|
stream: stream,
|
|
img: img,
|
|
empty: empty
|
|
};
|
|
|
|
const cssModules = {
|
|
"$style": style0
|
|
};
|
|
const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
|
|
|
|
export { index_photos as default };
|
|
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
|
|
unwindCssModuleClassName(ast);
|
|
expect(generate(ast)).toBe(`
|
|
import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
|
|
import {M as MkContainer} from './MkContainer-!~{03M}~.js';
|
|
import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
|
|
import './photoswipe-!~{003}~.js';
|
|
const _hoisted_1 = createBaseVNode("i", {
|
|
class: "ti ti-photo"
|
|
}, null, -1);
|
|
const index_photos = defineComponent({
|
|
__name: "index.photos",
|
|
props: {
|
|
user: {}
|
|
},
|
|
setup(__props) {
|
|
const props = __props;
|
|
let fetching = ref(true);
|
|
let images = ref([]);
|
|
function thumbnail(image) {
|
|
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
|
|
}
|
|
onMounted(() => {
|
|
const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
|
|
api("users/notes", {
|
|
userId: props.user.id,
|
|
fileType: image,
|
|
limit: 10
|
|
}).then(notes => {
|
|
for (const note of notes) {
|
|
for (const file of note.files) {
|
|
images.value.push({
|
|
note,
|
|
file
|
|
});
|
|
}
|
|
}
|
|
fetching.value = false;
|
|
});
|
|
});
|
|
return (_ctx, _cache) => {
|
|
const _component_MkLoading = resolveComponent("MkLoading");
|
|
const _component_MkA = resolveComponent("MkA");
|
|
return (openBlock(), createBlock(MkContainer, {
|
|
"max-height": 300,
|
|
foldable: true
|
|
}, {
|
|
icon: withCtx(() => [_hoisted_1]),
|
|
header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]),
|
|
default: withCtx(() => [createBaseVNode("div", {
|
|
class: "xenMW"
|
|
}, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, {
|
|
key: 0
|
|
})) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
|
|
key: 1,
|
|
class: "xaZzf"
|
|
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => {
|
|
return (openBlock(), createBlock(_component_MkA, {
|
|
key: image.note.id + image.file.id,
|
|
class: "xtA8t",
|
|
to: unref(notePage)(image.note)
|
|
}, {
|
|
default: withCtx(() => [createVNode(ImgWithBlurhash, {
|
|
hash: image.file.blurhash,
|
|
src: thumbnail(image.file),
|
|
title: image.file.name
|
|
}, null, 8, ["hash", "src", "title"])]),
|
|
_: 2
|
|
}, 1032, ["class", "to"]));
|
|
}), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
|
|
key: 2,
|
|
class: "xhYKj"
|
|
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]),
|
|
_: 1
|
|
}));
|
|
};
|
|
}
|
|
});
|
|
const root = "xenMW";
|
|
const stream = "xaZzf";
|
|
const img = "xtA8t";
|
|
const empty = "xhYKj";
|
|
const style0 = {
|
|
root: root,
|
|
stream: stream,
|
|
img: img,
|
|
empty: empty
|
|
};
|
|
const cssModules = {
|
|
"$style": style0
|
|
};
|
|
export {index_photos as default};
|
|
`.slice(1));
|
|
});
|
|
|
|
it('Composition API (with `useCssModule()`)', () => {
|
|
const ast = parse(`
|
|
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
|
|
import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
|
|
|
|
function isDebuggerEnabled(id) {
|
|
try {
|
|
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function stackTraceInstances() {
|
|
let instance = getCurrentInstance();
|
|
const stack = [];
|
|
while (instance) {
|
|
stack.push(instance);
|
|
instance = instance.parent;
|
|
}
|
|
return stack;
|
|
}
|
|
|
|
const _sfc_main = defineComponent({
|
|
props: {
|
|
items: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
direction: {
|
|
type: String,
|
|
required: false,
|
|
default: "down"
|
|
},
|
|
reversed: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
noGap: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
ad: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
}
|
|
},
|
|
setup(props, { slots, expose }) {
|
|
const $style = useCssModule();
|
|
function getDateText(time) {
|
|
const date = new Date(time).getDate();
|
|
const month = new Date(time).getMonth() + 1;
|
|
return i18n.t("monthAndDay", {
|
|
month: month.toString(),
|
|
day: date.toString()
|
|
});
|
|
}
|
|
if (props.items.length === 0)
|
|
return;
|
|
const renderChildrenImpl = () => props.items.map((item, i) => {
|
|
if (!slots || !slots.default)
|
|
return;
|
|
const el = slots.default({
|
|
item
|
|
})[0];
|
|
if (el.key == null && item.id)
|
|
el.key = item.id;
|
|
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
|
|
const separator = h("div", {
|
|
class: $style["separator"],
|
|
key: item.id + ":separator"
|
|
}, h("p", {
|
|
class: $style["date"]
|
|
}, [
|
|
h("span", {
|
|
class: $style["date-1"]
|
|
}, [
|
|
h("i", {
|
|
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
|
|
}),
|
|
getDateText(item.createdAt)
|
|
]),
|
|
h("span", {
|
|
class: $style["date-2"]
|
|
}, [
|
|
getDateText(props.items[i + 1].createdAt),
|
|
h("i", {
|
|
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
|
|
})
|
|
])
|
|
]));
|
|
return [el, separator];
|
|
} else {
|
|
if (props.ad && item._shouldInsertAd_) {
|
|
return [h(MkAd, {
|
|
key: item.id + ":ad",
|
|
prefer: ["horizontal", "horizontal-big"]
|
|
}), el];
|
|
} else {
|
|
return el;
|
|
}
|
|
}
|
|
});
|
|
const renderChildren = () => {
|
|
const children = renderChildrenImpl();
|
|
if (isDebuggerEnabled(6864)) {
|
|
const nodes = children.flatMap((node) => node ?? []);
|
|
const keys = new Set(nodes.map((node) => node.key));
|
|
if (keys.size !== nodes.length) {
|
|
const id = crypto.randomUUID();
|
|
const instances = stackTraceInstances();
|
|
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
|
|
console.warn({ id, debugId: 6864, stack: instances });
|
|
}
|
|
}
|
|
return children;
|
|
};
|
|
function onBeforeLeave(el) {
|
|
el.style.top = \`\${el.offsetTop}px\`;
|
|
el.style.left = \`\${el.offsetLeft}px\`;
|
|
}
|
|
function onLeaveCanceled(el) {
|
|
el.style.top = "";
|
|
el.style.left = "";
|
|
}
|
|
return () => h(
|
|
defaultStore.state.animation ? TransitionGroup : "div",
|
|
{
|
|
class: {
|
|
[$style["date-separated-list"]]: true,
|
|
[$style["date-separated-list-nogap"]]: props.noGap,
|
|
[$style["reversed"]]: props.reversed,
|
|
[$style["direction-down"]]: props.direction === "down",
|
|
[$style["direction-up"]]: props.direction === "up"
|
|
},
|
|
...defaultStore.state.animation ? {
|
|
name: "list",
|
|
tag: "div",
|
|
onBeforeLeave,
|
|
onLeaveCanceled
|
|
} : {}
|
|
},
|
|
{ default: renderChildren }
|
|
);
|
|
}
|
|
});
|
|
|
|
const reversed = "xxiZh";
|
|
const separator = "xxeDx";
|
|
const date = "xxawD";
|
|
const style0 = {
|
|
"date-separated-list": "xfKPa",
|
|
"date-separated-list-nogap": "xf9zr",
|
|
"direction-up": "x7AeO",
|
|
"direction-down": "xBIqc",
|
|
reversed: reversed,
|
|
separator: separator,
|
|
date: date,
|
|
"date-1": "xwtmh",
|
|
"date-1-icon": "xsNPa",
|
|
"date-2": "x1xvw",
|
|
"date-2-icon": "x9ZiG"
|
|
};
|
|
|
|
const cssModules = {
|
|
"$style": style0
|
|
};
|
|
const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
|
|
|
|
export { MkDateSeparatedList as M };
|
|
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
|
|
unwindCssModuleClassName(ast);
|
|
expect(generate(ast)).toBe(`
|
|
import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
|
|
import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
|
|
function isDebuggerEnabled(id) {
|
|
try {
|
|
return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function stackTraceInstances() {
|
|
let instance = getCurrentInstance();
|
|
const stack = [];
|
|
while (instance) {
|
|
stack.push(instance);
|
|
instance = instance.parent;
|
|
}
|
|
return stack;
|
|
}
|
|
const _sfc_main = defineComponent({
|
|
props: {
|
|
items: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
direction: {
|
|
type: String,
|
|
required: false,
|
|
default: "down"
|
|
},
|
|
reversed: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
noGap: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
ad: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
}
|
|
},
|
|
setup(props, {slots, expose}) {
|
|
const $style = useCssModule();
|
|
function getDateText(time) {
|
|
const date = new Date(time).getDate();
|
|
const month = new Date(time).getMonth() + 1;
|
|
return i18n.t("monthAndDay", {
|
|
month: month.toString(),
|
|
day: date.toString()
|
|
});
|
|
}
|
|
if (props.items.length === 0) return;
|
|
const renderChildrenImpl = () => props.items.map((item, i) => {
|
|
if (!slots || !slots.default) return;
|
|
const el = slots.default({
|
|
item
|
|
})[0];
|
|
if (el.key == null && item.id) el.key = item.id;
|
|
if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) {
|
|
const separator = h("div", {
|
|
class: $style["separator"],
|
|
key: item.id + ":separator"
|
|
}, h("p", {
|
|
class: $style["date"]
|
|
}, [h("span", {
|
|
class: $style["date-1"]
|
|
}, [h("i", {
|
|
class: \`ti ti-chevron-up \${$style["date-1-icon"]}\`
|
|
}), getDateText(item.createdAt)]), h("span", {
|
|
class: $style["date-2"]
|
|
}, [getDateText(props.items[i + 1].createdAt), h("i", {
|
|
class: \`ti ti-chevron-down \${$style["date-2-icon"]}\`
|
|
})])]));
|
|
return [el, separator];
|
|
} else {
|
|
if (props.ad && item._shouldInsertAd_) {
|
|
return [h(MkAd, {
|
|
key: item.id + ":ad",
|
|
prefer: ["horizontal", "horizontal-big"]
|
|
}), el];
|
|
} else {
|
|
return el;
|
|
}
|
|
}
|
|
});
|
|
const renderChildren = () => {
|
|
const children = renderChildrenImpl();
|
|
if (isDebuggerEnabled(6864)) {
|
|
const nodes = children.flatMap(node => node ?? []);
|
|
const keys = new Set(nodes.map(node => node.key));
|
|
if (keys.size !== nodes.length) {
|
|
const id = crypto.randomUUID();
|
|
const instances = stackTraceInstances();
|
|
toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`));
|
|
console.warn({
|
|
id,
|
|
debugId: 6864,
|
|
stack: instances
|
|
});
|
|
}
|
|
}
|
|
return children;
|
|
};
|
|
function onBeforeLeave(el) {
|
|
el.style.top = \`\${el.offsetTop}px\`;
|
|
el.style.left = \`\${el.offsetLeft}px\`;
|
|
}
|
|
function onLeaveCanceled(el) {
|
|
el.style.top = "";
|
|
el.style.left = "";
|
|
}
|
|
return () => h(defaultStore.state.animation ? TransitionGroup : "div", {
|
|
class: {
|
|
[$style["date-separated-list"]]: true,
|
|
[$style["date-separated-list-nogap"]]: props.noGap,
|
|
[$style["reversed"]]: props.reversed,
|
|
[$style["direction-down"]]: props.direction === "down",
|
|
[$style["direction-up"]]: props.direction === "up"
|
|
},
|
|
...defaultStore.state.animation ? {
|
|
name: "list",
|
|
tag: "div",
|
|
onBeforeLeave,
|
|
onLeaveCanceled
|
|
} : {}
|
|
}, {
|
|
default: renderChildren
|
|
});
|
|
}
|
|
});
|
|
const reversed = "xxiZh";
|
|
const separator = "xxeDx";
|
|
const date = "xxawD";
|
|
const style0 = {
|
|
"date-separated-list": "xfKPa",
|
|
"date-separated-list-nogap": "xf9zr",
|
|
"direction-up": "x7AeO",
|
|
"direction-down": "xBIqc",
|
|
reversed: reversed,
|
|
separator: separator,
|
|
date: date,
|
|
"date-1": "xwtmh",
|
|
"date-1-icon": "xsNPa",
|
|
"date-2": "x1xvw",
|
|
"date-2-icon": "x9ZiG"
|
|
};
|
|
const cssModules = {
|
|
"$style": style0
|
|
};
|
|
const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
|
|
export {MkDateSeparatedList as M};
|
|
`.slice(1));
|
|
});
|