memi-board 0.4.6 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -73
- package/dist/composables/useMemiBoardAuth.d.ts +15 -3
- package/dist/composables/useMemiBoardModeration.d.ts +3 -1
- package/dist/composables/useMemiBoardStorage.d.ts +5 -1
- package/dist/config.d.ts +7 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +498 -141
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/CommentForm.vue +13 -2
- package/dist/runtime/components/Detail.vue +19 -5
- package/dist/runtime/components/Editor.vue +215 -27
- package/dist/types.d.ts +13 -1
- package/dist/utils/compressImage.d.ts +7 -0
- package/dist/utils/extractEditorImageUrls.d.ts +5 -0
- package/dist/utils/moderation-prompt.d.ts +10 -1
- package/dist/utils/moderation-strike.d.ts +14 -0
- package/dist/utils/renderMarkdown.d.ts +10 -0
- package/dist/utils/storagePath.d.ts +7 -0
- package/docs/firebase-setup.md +106 -0
- package/docs/firestore.rules.example +97 -0
- package/docs/storage.rules.example +46 -0
- package/package.json +11 -2
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { computed as e, reactive as t, ref as n, watch as r } from "vue";
|
|
2
2
|
import { useCollection as i, useCurrentUser as a, useDocument as o, useFirebaseApp as s, useFirebaseAuth as c, useFirestore as l } from "vuefire";
|
|
3
|
-
import { collection as u, deleteDoc as d, deleteField as f, doc as p, getDoc as m, getDocs as h, increment as g, limit as _, orderBy as v, query as y, serverTimestamp as b, setDoc as x, startAfter as S, updateDoc as C, where as
|
|
4
|
-
import { GoogleAuthProvider as
|
|
5
|
-
import { deleteObject as
|
|
6
|
-
import { GoogleAIBackend as
|
|
3
|
+
import { collection as u, deleteDoc as d, deleteField as f, doc as p, getDoc as m, getDocs as h, increment as g, limit as _, orderBy as v, query as y, serverTimestamp as b, setDoc as x, startAfter as S, updateDoc as C, where as ee, writeBatch as w } from "firebase/firestore";
|
|
4
|
+
import { GoogleAuthProvider as T, OAuthProvider as te, createUserWithEmailAndPassword as ne, signInWithEmailAndPassword as re, signInWithPopup as ie, signOut as ae, updateProfile as oe } from "firebase/auth";
|
|
5
|
+
import { deleteObject as E, getDownloadURL as D, getStorage as O, listAll as k, ref as A, uploadBytes as j, uploadBytesResumable as M } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as N, Schema as P, getAI as se, getGenerativeModel as ce } from "firebase/ai";
|
|
7
7
|
//#region src/config.ts
|
|
8
|
-
var
|
|
9
|
-
function
|
|
8
|
+
var F = "__MEMI_BOARD_CONFIG__";
|
|
9
|
+
function I() {
|
|
10
10
|
let e = globalThis;
|
|
11
|
-
return e[
|
|
11
|
+
return e[F] || (e[F] = t({
|
|
12
12
|
collectionPrefix: "memiBoard",
|
|
13
13
|
auth: { providers: ["google", "apple"] },
|
|
14
14
|
moderation: {
|
|
@@ -16,113 +16,267 @@ function V() {
|
|
|
16
16
|
model: "gemini-3.5-flash-lite",
|
|
17
17
|
onError: "allow",
|
|
18
18
|
moderateImages: !1,
|
|
19
|
-
localBlocklist: []
|
|
19
|
+
localBlocklist: [],
|
|
20
|
+
blockBanThreshold: 3,
|
|
21
|
+
blockBanDecayMs: 864e5
|
|
20
22
|
}
|
|
21
|
-
})), e[
|
|
23
|
+
})), e[F];
|
|
22
24
|
}
|
|
23
|
-
function
|
|
24
|
-
let t =
|
|
25
|
+
function L(e) {
|
|
26
|
+
let t = I();
|
|
25
27
|
e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
|
|
26
28
|
}
|
|
27
|
-
function
|
|
28
|
-
return
|
|
29
|
+
function R() {
|
|
30
|
+
return I();
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/utils/moderation-strike.ts
|
|
34
|
+
var le = 3, z = 864e5;
|
|
35
|
+
function B(e, t, n = Date.now(), r = z) {
|
|
36
|
+
let i = Math.max(0, Math.floor(Number(e) || 0));
|
|
37
|
+
if (i <= 0) return 0;
|
|
38
|
+
let a = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
39
|
+
if (a == null || n <= a) return i;
|
|
40
|
+
let o = Math.floor((n - a) / r);
|
|
41
|
+
return Math.max(0, i - o);
|
|
42
|
+
}
|
|
43
|
+
function V(e, t, n = Date.now(), r = 3, i = z) {
|
|
44
|
+
let a = Math.max(0, Math.floor(Number(e) || 0));
|
|
45
|
+
if (a < r) return null;
|
|
46
|
+
let o = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
47
|
+
if (o == null) return n;
|
|
48
|
+
let s = o + (a - (r - 1)) * i;
|
|
49
|
+
return s > n ? s : null;
|
|
50
|
+
}
|
|
51
|
+
function H(e, t, n = Date.now(), r = 3, i = z) {
|
|
52
|
+
return V(e, t, n, r, i) != null;
|
|
53
|
+
}
|
|
54
|
+
function U(e, t = Date.now()) {
|
|
55
|
+
let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
|
|
56
|
+
if (r <= 0) return "곧 해제";
|
|
57
|
+
if (r < 24) return `약 ${r}시간 후 해제`;
|
|
58
|
+
let i = Math.floor(r / 24), a = r % 24;
|
|
59
|
+
return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
|
|
60
|
+
}
|
|
61
|
+
function ue(e) {
|
|
62
|
+
if (e == null) return null;
|
|
63
|
+
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
64
|
+
if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
|
|
65
|
+
if (typeof e == "object" && e && "seconds" in e) {
|
|
66
|
+
let t = Number(e.seconds);
|
|
67
|
+
if (Number.isFinite(t)) return t * 1e3;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
29
70
|
}
|
|
30
71
|
//#endregion
|
|
31
72
|
//#region src/composables/useMemiBoardAuth.ts
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
73
|
+
var W = "__MEMI_BOARD_AUTH_STATE__";
|
|
74
|
+
function de() {
|
|
75
|
+
let e = globalThis;
|
|
76
|
+
return e[W] || (e[W] = {
|
|
77
|
+
role: n("user"),
|
|
78
|
+
rolePending: n(!0),
|
|
79
|
+
rawBlockCount: n(0),
|
|
80
|
+
blockAtMs: n(null),
|
|
81
|
+
watchStarted: !1
|
|
82
|
+
}), e[W];
|
|
83
|
+
}
|
|
84
|
+
function G() {
|
|
85
|
+
let t = R(), n = de(), i = l(), o = c(), s = a(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: h } = n;
|
|
86
|
+
function _() {
|
|
87
|
+
let e = t.moderation?.blockBanThreshold;
|
|
88
|
+
return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
|
|
89
|
+
}
|
|
90
|
+
function v() {
|
|
91
|
+
let e = t.moderation?.blockBanDecayMs;
|
|
92
|
+
return typeof e == "number" && e > 0 ? e : z;
|
|
93
|
+
}
|
|
94
|
+
function y(e) {
|
|
35
95
|
return p(i, `${t.collectionPrefix}Users`, e);
|
|
36
96
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
97
|
+
function S(e) {
|
|
98
|
+
if (!e) {
|
|
99
|
+
f.value = 0, h.value = null;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
|
|
103
|
+
}
|
|
104
|
+
async function ee(e) {
|
|
105
|
+
let t = y(e.uid), n = await m(t);
|
|
106
|
+
if (!n.exists()) return await x(t, {
|
|
107
|
+
role: "user",
|
|
40
108
|
displayName: e.displayName,
|
|
41
109
|
updatedAt: b()
|
|
42
|
-
}),
|
|
43
|
-
|
|
110
|
+
}), S({ moderationBlockCount: 0 }), "user";
|
|
111
|
+
let r = n.data();
|
|
112
|
+
return S(r), await C(t, {
|
|
44
113
|
displayName: e.displayName,
|
|
45
114
|
updatedAt: b()
|
|
46
|
-
}), "user"
|
|
115
|
+
}), r.role ?? "user";
|
|
116
|
+
}
|
|
117
|
+
async function w() {
|
|
118
|
+
let e = s.value?.uid;
|
|
119
|
+
if (!e) {
|
|
120
|
+
S(void 0);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let t = await m(y(e));
|
|
124
|
+
S(t.exists() ? t.data() : void 0);
|
|
47
125
|
}
|
|
48
|
-
r(s, async (e) => {
|
|
126
|
+
n.watchStarted || (n.watchStarted = !0, r(s, async (e) => {
|
|
49
127
|
if (d.value = !0, !e) {
|
|
50
|
-
u.value = "user", d.value = !1;
|
|
128
|
+
u.value = "user", S(void 0), d.value = !1;
|
|
51
129
|
return;
|
|
52
130
|
}
|
|
53
131
|
try {
|
|
54
|
-
u.value = await
|
|
132
|
+
u.value = await ee(e);
|
|
55
133
|
} catch {
|
|
56
134
|
u.value = "user";
|
|
57
135
|
} finally {
|
|
58
136
|
d.value = !1;
|
|
59
137
|
}
|
|
60
|
-
}, { immediate: !0 });
|
|
61
|
-
let
|
|
62
|
-
|
|
138
|
+
}, { immediate: !0 }));
|
|
139
|
+
let E = e(() => !!s.value), D = e(() => u.value === "admin"), O = e(() => B(f.value, h.value, Date.now(), v())), k = e(() => {
|
|
140
|
+
let e = _();
|
|
141
|
+
return e <= 0 ? !1 : H(f.value, h.value, Date.now(), e, v());
|
|
142
|
+
}), A = e(() => {
|
|
143
|
+
let e = _();
|
|
144
|
+
if (e <= 0) return null;
|
|
145
|
+
let t = V(f.value, h.value, Date.now(), e, v());
|
|
146
|
+
return t == null ? null : U(t);
|
|
147
|
+
}), j = e(() => {
|
|
148
|
+
if (!k.value) return null;
|
|
149
|
+
let e = A.value;
|
|
150
|
+
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
151
|
+
});
|
|
152
|
+
async function M() {
|
|
153
|
+
let e = s.value?.uid, t = _();
|
|
154
|
+
if (!e || t <= 0) return {
|
|
155
|
+
effectiveCount: 0,
|
|
156
|
+
restricted: !1,
|
|
157
|
+
messageSuffix: ""
|
|
158
|
+
};
|
|
159
|
+
let n = y(e), r = await m(n);
|
|
160
|
+
r.exists() ? await C(n, {
|
|
161
|
+
moderationBlockCount: g(1),
|
|
162
|
+
moderationBlockAt: b(),
|
|
163
|
+
updatedAt: b()
|
|
164
|
+
}) : await x(n, {
|
|
165
|
+
role: "user",
|
|
166
|
+
displayName: s.value?.displayName ?? null,
|
|
167
|
+
moderationBlockCount: 1,
|
|
168
|
+
moderationBlockAt: b(),
|
|
169
|
+
updatedAt: b()
|
|
170
|
+
});
|
|
171
|
+
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
|
|
172
|
+
f.value = i, h.value = a;
|
|
173
|
+
let o = B(i, a, a, v()), c = H(i, a, a, t, v()), l = ` (경고 ${o}/${t})`;
|
|
174
|
+
if (c) {
|
|
175
|
+
let e = V(i, a, a, t, v()), n = e == null ? null : U(e, a);
|
|
176
|
+
l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
effectiveCount: o,
|
|
180
|
+
restricted: c,
|
|
181
|
+
messageSuffix: l
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function N() {
|
|
63
185
|
if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
|
|
64
186
|
return o;
|
|
65
187
|
}
|
|
66
|
-
async function
|
|
67
|
-
return
|
|
188
|
+
async function P() {
|
|
189
|
+
return ie(N(), new T());
|
|
68
190
|
}
|
|
69
|
-
async function
|
|
70
|
-
let e = new
|
|
71
|
-
return e.addScope("email"), e.addScope("name"),
|
|
191
|
+
async function se() {
|
|
192
|
+
let e = new te("apple.com");
|
|
193
|
+
return e.addScope("email"), e.addScope("name"), ie(N(), e);
|
|
72
194
|
}
|
|
73
|
-
async function
|
|
74
|
-
return
|
|
195
|
+
async function ce(e, t) {
|
|
196
|
+
return re(N(), e, t);
|
|
75
197
|
}
|
|
76
|
-
async function
|
|
77
|
-
let r = await
|
|
78
|
-
return n && await
|
|
198
|
+
async function F(e, t, n) {
|
|
199
|
+
let r = await ne(N(), e, t);
|
|
200
|
+
return n && await oe(r.user, { displayName: n }), r;
|
|
79
201
|
}
|
|
80
|
-
async function
|
|
81
|
-
await
|
|
202
|
+
async function I() {
|
|
203
|
+
await ae(N());
|
|
82
204
|
}
|
|
83
|
-
function
|
|
84
|
-
return
|
|
205
|
+
function L(e) {
|
|
206
|
+
return E.value && (s.value?.uid === e.authorUid || D.value);
|
|
85
207
|
}
|
|
86
|
-
function
|
|
87
|
-
return
|
|
208
|
+
function le(e) {
|
|
209
|
+
return E.value && (s.value?.uid === e.authorUid || D.value);
|
|
88
210
|
}
|
|
89
211
|
return {
|
|
90
212
|
user: s,
|
|
91
|
-
isSignedIn:
|
|
92
|
-
isAdmin:
|
|
213
|
+
isSignedIn: E,
|
|
214
|
+
isAdmin: D,
|
|
93
215
|
rolePending: d,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
216
|
+
moderationBlockCount: O,
|
|
217
|
+
isWriteRestricted: k,
|
|
218
|
+
restrictedUntilLabel: A,
|
|
219
|
+
restrictedMessage: j,
|
|
220
|
+
signInWithGoogle: P,
|
|
221
|
+
signInWithApple: se,
|
|
222
|
+
signInWithEmail: ce,
|
|
223
|
+
signUpWithEmail: F,
|
|
224
|
+
signOut: I,
|
|
225
|
+
canEdit: L,
|
|
226
|
+
canDelete: L,
|
|
227
|
+
canDeleteComment: le,
|
|
228
|
+
recordContentModerationBlock: M,
|
|
229
|
+
refreshBoardUser: w
|
|
102
230
|
};
|
|
103
231
|
}
|
|
104
232
|
//#endregion
|
|
105
233
|
//#region src/utils/slugify.ts
|
|
106
|
-
function
|
|
234
|
+
function K(e) {
|
|
107
235
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
108
236
|
}
|
|
109
|
-
function
|
|
237
|
+
function fe(e, t = 60) {
|
|
110
238
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
111
239
|
}
|
|
112
240
|
//#endregion
|
|
241
|
+
//#region src/utils/extractEditorImageUrls.ts
|
|
242
|
+
function pe(e) {
|
|
243
|
+
if (!e) return [];
|
|
244
|
+
let t = /* @__PURE__ */ new Set();
|
|
245
|
+
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
246
|
+
for (let n of e.matchAll(/<img[^>]*\ssrc=["'](https?:\/\/[^"']+)["']/gi)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
247
|
+
return [...t];
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/utils/storagePath.ts
|
|
251
|
+
function me(e) {
|
|
252
|
+
try {
|
|
253
|
+
let t = e.match(/\/o\/([^?]+)/);
|
|
254
|
+
return t?.[1] ? decodeURIComponent(t[1]) : null;
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function q(e) {
|
|
260
|
+
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
113
263
|
//#region src/composables/useMemiBoardPosts.ts
|
|
114
|
-
async function
|
|
115
|
-
let t = await
|
|
264
|
+
async function J(e) {
|
|
265
|
+
let t = await k(e).catch(() => ({
|
|
116
266
|
items: [],
|
|
117
267
|
prefixes: []
|
|
118
268
|
}));
|
|
119
|
-
await Promise.all([...t.items.map((e) =>
|
|
269
|
+
await Promise.all([...t.items.map((e) => E(e)), ...t.prefixes.map((e) => J(e))]);
|
|
270
|
+
}
|
|
271
|
+
async function he(e, t) {
|
|
272
|
+
let n = [...new Set([...t].filter(Boolean))];
|
|
273
|
+
await Promise.all(n.map((t) => E(A(e, t)).catch(() => {})));
|
|
120
274
|
}
|
|
121
|
-
function
|
|
122
|
-
let e =
|
|
275
|
+
function ge() {
|
|
276
|
+
let e = R(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
|
|
123
277
|
async function g(e = {}) {
|
|
124
278
|
let t = e.pageSize ?? 20, n = [];
|
|
125
|
-
e.category && n.push(
|
|
279
|
+
e.category && n.push(ee("category", "==", e.category)), n.push(v("createdAt", "desc")), e.cursor && n.push(S(e.cursor)), n.push(_(t + 1));
|
|
126
280
|
let r = await h(y(i(), ...n)), a = r.docs.slice(0, t);
|
|
127
281
|
return {
|
|
128
282
|
posts: a.map((e) => ({
|
|
@@ -133,7 +287,7 @@ function q() {
|
|
|
133
287
|
hasMore: r.docs.length > t
|
|
134
288
|
};
|
|
135
289
|
}
|
|
136
|
-
async function
|
|
290
|
+
async function T(e) {
|
|
137
291
|
let [t, n] = await Promise.all([m(a(e)), m(o(e))]);
|
|
138
292
|
return t.exists() ? {
|
|
139
293
|
id: t.id,
|
|
@@ -141,8 +295,8 @@ function q() {
|
|
|
141
295
|
content: n.exists() ? n.data().content ?? "" : ""
|
|
142
296
|
} : null;
|
|
143
297
|
}
|
|
144
|
-
async function
|
|
145
|
-
let t =
|
|
298
|
+
async function te(e) {
|
|
299
|
+
let t = K(e.title) || "post", n = t, r = 1;
|
|
146
300
|
for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
|
|
147
301
|
return await Promise.all([x(a(n), {
|
|
148
302
|
title: e.title,
|
|
@@ -159,7 +313,7 @@ function q() {
|
|
|
159
313
|
updatedAt: b()
|
|
160
314
|
}), x(o(n), { content: e.content })]), n;
|
|
161
315
|
}
|
|
162
|
-
async function
|
|
316
|
+
async function ne(e, t) {
|
|
163
317
|
await Promise.all([C(a(e), {
|
|
164
318
|
title: t.title,
|
|
165
319
|
tags: t.tags ?? [],
|
|
@@ -168,29 +322,49 @@ function q() {
|
|
|
168
322
|
updatedAt: b()
|
|
169
323
|
}), x(o(e), { content: t.content }, { merge: !0 })]);
|
|
170
324
|
}
|
|
171
|
-
async function
|
|
172
|
-
let i = await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
325
|
+
async function re(e) {
|
|
326
|
+
let [i, s] = await Promise.all([m(a(e)), m(o(e))]);
|
|
327
|
+
if (!i.exists()) return;
|
|
328
|
+
let l = i.data(), u = s.exists() ? String(s.data().content ?? "") : "", f = await h(c(e));
|
|
329
|
+
for (let e = 0; e < f.docs.length; e += 450) {
|
|
330
|
+
let n = w(t);
|
|
331
|
+
f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
176
332
|
}
|
|
177
|
-
let
|
|
178
|
-
|
|
333
|
+
let p = O(n), g = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
|
|
334
|
+
for (let t of l.attachments ?? []) if (t?.path) {
|
|
335
|
+
g.add(t.path);
|
|
336
|
+
let n = q(t.path);
|
|
337
|
+
n && n !== e && _.add(n);
|
|
338
|
+
}
|
|
339
|
+
for (let t of pe(u)) {
|
|
340
|
+
let n = me(t);
|
|
341
|
+
if (!n) continue;
|
|
342
|
+
g.add(n);
|
|
343
|
+
let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
344
|
+
r !== n && r.includes("/images/thumbnails/") && g.add(r);
|
|
345
|
+
let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
346
|
+
i && !n.includes("/thumbnails/") && g.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
347
|
+
let a = q(n);
|
|
348
|
+
a && a !== e && _.add(a);
|
|
349
|
+
}
|
|
350
|
+
await d(o(e)), await J(A(p, `${r()}/posts/${e}`));
|
|
351
|
+
for (let e of _) await J(A(p, `${r()}/posts/${e}`));
|
|
352
|
+
await he(p, g), await d(a(e));
|
|
179
353
|
}
|
|
180
354
|
return {
|
|
181
355
|
getPosts: g,
|
|
182
|
-
getPost:
|
|
183
|
-
createPost:
|
|
184
|
-
updatePost:
|
|
185
|
-
deletePost:
|
|
356
|
+
getPost: T,
|
|
357
|
+
createPost: te,
|
|
358
|
+
updatePost: ne,
|
|
359
|
+
deletePost: re
|
|
186
360
|
};
|
|
187
361
|
}
|
|
188
362
|
//#endregion
|
|
189
363
|
//#region src/composables/useMemiBoardComments.ts
|
|
190
|
-
function
|
|
191
|
-
let n =
|
|
364
|
+
function _e(t) {
|
|
365
|
+
let n = R(), r = l(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => y(u(r, `${a()}Posts`, o.value, "comments"), v("createdAt", "asc"))), c = i(s);
|
|
192
366
|
async function d(e) {
|
|
193
|
-
let t =
|
|
367
|
+
let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
|
|
194
368
|
t.set(i, {
|
|
195
369
|
postId: o.value,
|
|
196
370
|
body: e.body,
|
|
@@ -202,7 +376,7 @@ function J(t) {
|
|
|
202
376
|
}), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
|
|
203
377
|
}
|
|
204
378
|
async function f(e) {
|
|
205
|
-
let t =
|
|
379
|
+
let t = w(r), n = a();
|
|
206
380
|
t.delete(p(r, `${n}Posts`, o.value, "comments", e)), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
|
|
207
381
|
}
|
|
208
382
|
return {
|
|
@@ -213,7 +387,25 @@ function J(t) {
|
|
|
213
387
|
}
|
|
214
388
|
//#endregion
|
|
215
389
|
//#region src/utils/moderation-prompt.ts
|
|
216
|
-
var
|
|
390
|
+
var ve = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", ye = {
|
|
391
|
+
abuse: "욕설·비속어·모욕",
|
|
392
|
+
spam: "스팸·광고",
|
|
393
|
+
adult: "선정적·성인 내용",
|
|
394
|
+
violence: "폭력·위협",
|
|
395
|
+
other: "부적절한 내용",
|
|
396
|
+
none: ""
|
|
397
|
+
};
|
|
398
|
+
function Y(e) {
|
|
399
|
+
if (e.via === "local" && e.localHit) {
|
|
400
|
+
let t = e.localHit.trim();
|
|
401
|
+
return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
|
|
402
|
+
}
|
|
403
|
+
let t = (e.reason || "").trim();
|
|
404
|
+
if (t) return t;
|
|
405
|
+
let n = e.category && e.category !== "none" ? ye[e.category] || e.category : "";
|
|
406
|
+
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
407
|
+
}
|
|
408
|
+
var be = [
|
|
217
409
|
"씨발",
|
|
218
410
|
"시발",
|
|
219
411
|
"시이발",
|
|
@@ -239,10 +431,10 @@ var Y = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터
|
|
|
239
431
|
"bitch",
|
|
240
432
|
"asshole"
|
|
241
433
|
];
|
|
242
|
-
function
|
|
434
|
+
function xe(e) {
|
|
243
435
|
return new RegExp(e.join("|"), "i");
|
|
244
436
|
}
|
|
245
|
-
var
|
|
437
|
+
var Se = /* @__PURE__ */ new Set([
|
|
246
438
|
"none",
|
|
247
439
|
"abuse",
|
|
248
440
|
"spam",
|
|
@@ -250,7 +442,7 @@ var oe = /* @__PURE__ */ new Set([
|
|
|
250
442
|
"violence",
|
|
251
443
|
"other"
|
|
252
444
|
]);
|
|
253
|
-
function
|
|
445
|
+
function Ce(e) {
|
|
254
446
|
let t = e.trim();
|
|
255
447
|
if (!t) return null;
|
|
256
448
|
let n;
|
|
@@ -268,7 +460,7 @@ function X(e) {
|
|
|
268
460
|
if (typeof n != "object" || !n) return null;
|
|
269
461
|
let r = n;
|
|
270
462
|
if (typeof r.flagged != "boolean") return null;
|
|
271
|
-
let i = typeof r.category == "string" &&
|
|
463
|
+
let i = typeof r.category == "string" && Se.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
272
464
|
return {
|
|
273
465
|
flagged: r.flagged,
|
|
274
466
|
category: i,
|
|
@@ -277,43 +469,48 @@ function X(e) {
|
|
|
277
469
|
}
|
|
278
470
|
//#endregion
|
|
279
471
|
//#region src/composables/useMemiBoardModeration.ts
|
|
280
|
-
var
|
|
472
|
+
var X = {
|
|
281
473
|
flagged: !1,
|
|
282
474
|
category: "none",
|
|
283
475
|
reason: "",
|
|
284
476
|
via: "empty"
|
|
285
477
|
};
|
|
286
|
-
function
|
|
287
|
-
let e =
|
|
288
|
-
function
|
|
289
|
-
|
|
478
|
+
function we() {
|
|
479
|
+
let e = R().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = xe([...be, ...e.localBlocklist ?? []]);
|
|
480
|
+
function o(e) {
|
|
481
|
+
let t = e.match(a);
|
|
482
|
+
return t ? {
|
|
290
483
|
flagged: !0,
|
|
291
484
|
category: "abuse",
|
|
292
|
-
reason:
|
|
485
|
+
reason: Y({
|
|
486
|
+
via: "local",
|
|
487
|
+
localHit: t[0] || "",
|
|
488
|
+
category: "abuse"
|
|
489
|
+
}),
|
|
293
490
|
via: "local"
|
|
294
491
|
} : null;
|
|
295
492
|
}
|
|
296
|
-
function
|
|
297
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
298
|
-
backend: new
|
|
493
|
+
function c() {
|
|
494
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = se(t, {
|
|
495
|
+
backend: new N(),
|
|
299
496
|
useLimitedUseAppCheckTokens: r
|
|
300
497
|
});
|
|
301
|
-
return
|
|
498
|
+
return ce(i, {
|
|
302
499
|
model: n,
|
|
303
|
-
systemInstruction:
|
|
500
|
+
systemInstruction: ve,
|
|
304
501
|
generationConfig: {
|
|
305
502
|
temperature: 0,
|
|
306
503
|
maxOutputTokens: 256,
|
|
307
504
|
responseMimeType: "application/json",
|
|
308
|
-
responseSchema:
|
|
309
|
-
flagged:
|
|
310
|
-
category:
|
|
311
|
-
reason:
|
|
505
|
+
responseSchema: P.object({ properties: {
|
|
506
|
+
flagged: P.boolean(),
|
|
507
|
+
category: P.string(),
|
|
508
|
+
reason: P.string()
|
|
312
509
|
} })
|
|
313
510
|
}
|
|
314
511
|
});
|
|
315
512
|
}
|
|
316
|
-
function
|
|
513
|
+
function l() {
|
|
317
514
|
return e.onError === "block" ? {
|
|
318
515
|
flagged: !0,
|
|
319
516
|
category: "other",
|
|
@@ -328,64 +525,129 @@ function se() {
|
|
|
328
525
|
via: "ai-error-allow"
|
|
329
526
|
};
|
|
330
527
|
}
|
|
331
|
-
|
|
332
|
-
let n =
|
|
333
|
-
|
|
334
|
-
|
|
528
|
+
function u(e, t = "") {
|
|
529
|
+
let n = Y({
|
|
530
|
+
reason: e.reason,
|
|
531
|
+
category: e.category,
|
|
532
|
+
via: e.via
|
|
533
|
+
}), r = t ? `${n}${t}` : n;
|
|
534
|
+
return {
|
|
535
|
+
...e,
|
|
536
|
+
reason: r
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async function d(e) {
|
|
540
|
+
if (!e.flagged || e.error || e.via !== "local" && e.via !== "ai") return e;
|
|
541
|
+
try {
|
|
542
|
+
let { messageSuffix: t } = await i();
|
|
543
|
+
return u(e, t);
|
|
544
|
+
} catch (t) {
|
|
545
|
+
return console.warn("[memi-board] record moderation strike failed", t instanceof Error ? t.message : t), u(e);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function f(t) {
|
|
549
|
+
if (n.value) return {
|
|
550
|
+
flagged: !0,
|
|
551
|
+
category: "other",
|
|
552
|
+
reason: r.value || "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.",
|
|
553
|
+
via: "restricted"
|
|
554
|
+
};
|
|
555
|
+
let i = t.trim();
|
|
556
|
+
if (!i) return {
|
|
557
|
+
...X,
|
|
335
558
|
via: "empty"
|
|
336
559
|
};
|
|
337
|
-
let
|
|
338
|
-
if (
|
|
560
|
+
let a = o(i);
|
|
561
|
+
if (a) return d(a);
|
|
339
562
|
if (e.enabled === !1) return {
|
|
340
|
-
...
|
|
563
|
+
...X,
|
|
341
564
|
via: "disabled"
|
|
342
565
|
};
|
|
343
566
|
try {
|
|
344
|
-
let e =
|
|
345
|
-
|
|
567
|
+
let e = Ce((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
568
|
+
if (!e) return l();
|
|
569
|
+
let t = {
|
|
346
570
|
flagged: e.flagged,
|
|
347
571
|
category: e.category,
|
|
348
|
-
reason:
|
|
572
|
+
reason: Y({
|
|
573
|
+
reason: e.reason,
|
|
574
|
+
category: e.category,
|
|
575
|
+
via: "ai"
|
|
576
|
+
}),
|
|
349
577
|
via: "ai"
|
|
350
|
-
}
|
|
578
|
+
};
|
|
579
|
+
return t.flagged ? d(t) : t;
|
|
351
580
|
} catch (e) {
|
|
352
|
-
return console.warn("[memi-board] moderation AI failed", e instanceof Error ? e.message : e),
|
|
581
|
+
return console.warn("[memi-board] moderation AI failed", e instanceof Error ? e.message : e), l();
|
|
353
582
|
}
|
|
354
583
|
}
|
|
355
|
-
async function
|
|
584
|
+
async function p(t) {
|
|
585
|
+
if (n.value) return {
|
|
586
|
+
flagged: !0,
|
|
587
|
+
category: "other",
|
|
588
|
+
reason: r.value || "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.",
|
|
589
|
+
via: "restricted"
|
|
590
|
+
};
|
|
356
591
|
if (e.enabled === !1 || !e.moderateImages) return {
|
|
357
|
-
...
|
|
592
|
+
...X,
|
|
358
593
|
via: "disabled"
|
|
359
594
|
};
|
|
360
595
|
try {
|
|
361
596
|
let e = await new Promise((e, n) => {
|
|
362
597
|
let r = new FileReader();
|
|
363
598
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
364
|
-
}), n =
|
|
599
|
+
}), n = Ce((await c().generateContent([{ inlineData: {
|
|
365
600
|
mimeType: t.type || "image/jpeg",
|
|
366
601
|
data: e
|
|
367
602
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
368
|
-
|
|
603
|
+
if (!n) return l();
|
|
604
|
+
let r = {
|
|
369
605
|
flagged: n.flagged,
|
|
370
606
|
category: n.category,
|
|
371
|
-
reason:
|
|
607
|
+
reason: Y({
|
|
608
|
+
reason: n.reason,
|
|
609
|
+
category: n.category,
|
|
610
|
+
via: "ai"
|
|
611
|
+
}),
|
|
372
612
|
via: "ai"
|
|
373
|
-
}
|
|
613
|
+
};
|
|
614
|
+
return r.flagged ? d(r) : r;
|
|
374
615
|
} catch (e) {
|
|
375
|
-
return console.warn("[memi-board] moderation AI image failed", e instanceof Error ? e.message : e),
|
|
616
|
+
return console.warn("[memi-board] moderation AI image failed", e instanceof Error ? e.message : e), l();
|
|
376
617
|
}
|
|
377
618
|
}
|
|
378
619
|
return {
|
|
379
|
-
checkText:
|
|
380
|
-
checkImage:
|
|
620
|
+
checkText: f,
|
|
621
|
+
checkImage: p
|
|
381
622
|
};
|
|
382
623
|
}
|
|
383
624
|
//#endregion
|
|
625
|
+
//#region src/utils/compressImage.ts
|
|
626
|
+
async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
627
|
+
return new Promise((r, i) => {
|
|
628
|
+
let a = new Image(), o = URL.createObjectURL(e);
|
|
629
|
+
a.onload = () => {
|
|
630
|
+
URL.revokeObjectURL(o);
|
|
631
|
+
let e = Math.min(1, t / a.naturalWidth), s = document.createElement("canvas");
|
|
632
|
+
s.width = Math.round(a.naturalWidth * e), s.height = Math.round(a.naturalHeight * e);
|
|
633
|
+
let c = s.getContext("2d");
|
|
634
|
+
if (!c) {
|
|
635
|
+
i(/* @__PURE__ */ Error("Canvas를 사용할 수 없습니다"));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
c.drawImage(a, 0, 0, s.width, s.height), s.toBlob((e) => e ? r(e) : i(/* @__PURE__ */ Error("압축 실패")), "image/jpeg", n);
|
|
639
|
+
}, a.onerror = () => {
|
|
640
|
+
URL.revokeObjectURL(o), i(/* @__PURE__ */ Error("이미지 로드 실패"));
|
|
641
|
+
}, a.src = o;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
//#endregion
|
|
384
645
|
//#region src/composables/useMemiBoardStorage.ts
|
|
385
|
-
|
|
386
|
-
|
|
646
|
+
var Ee = 5242880;
|
|
647
|
+
function De() {
|
|
648
|
+
let e = R(), t = s(), n = () => e.collectionPrefix;
|
|
387
649
|
function r(e, r, i) {
|
|
388
|
-
let a =
|
|
650
|
+
let a = O(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = A(a, o), c = M(s, e, {
|
|
389
651
|
contentType: e.type || "application/octet-stream",
|
|
390
652
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
391
653
|
});
|
|
@@ -394,7 +656,7 @@ function Q() {
|
|
|
394
656
|
c.on("state_changed", (e) => {
|
|
395
657
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
396
658
|
}, n, async () => {
|
|
397
|
-
let n = await
|
|
659
|
+
let n = await D(s);
|
|
398
660
|
t({
|
|
399
661
|
name: e.name,
|
|
400
662
|
url: n,
|
|
@@ -408,17 +670,39 @@ function Q() {
|
|
|
408
670
|
};
|
|
409
671
|
}
|
|
410
672
|
async function i(e) {
|
|
411
|
-
let n =
|
|
412
|
-
await
|
|
673
|
+
let n = O(t);
|
|
674
|
+
await E(A(n, e.path)).catch(() => {});
|
|
675
|
+
}
|
|
676
|
+
async function a(e, r) {
|
|
677
|
+
if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
678
|
+
if (e.size > 5242880) throw Error(`이미지는 ${Ee / 1024 / 1024}MB 이하여야 합니다.`);
|
|
679
|
+
let i = O(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = A(i, l);
|
|
680
|
+
await j(u, e, { contentType: e.type || "image/png" });
|
|
681
|
+
let d = await D(u), f = await Te(e, {
|
|
682
|
+
maxWidth: 400,
|
|
683
|
+
quality: .8
|
|
684
|
+
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = A(i, p);
|
|
685
|
+
return await j(m, f, { contentType: "image/jpeg" }), {
|
|
686
|
+
originalUrl: d,
|
|
687
|
+
originalPath: l,
|
|
688
|
+
thumbnailUrl: await D(m),
|
|
689
|
+
thumbnailPath: p
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async function o(e) {
|
|
693
|
+
let n = O(t);
|
|
694
|
+
await Promise.all([E(A(n, e.originalPath)).catch(() => {}), E(A(n, e.thumbnailPath)).catch(() => {})]);
|
|
413
695
|
}
|
|
414
696
|
return {
|
|
415
697
|
uploadAttachment: r,
|
|
416
|
-
deleteAttachment: i
|
|
698
|
+
deleteAttachment: i,
|
|
699
|
+
uploadEditorImage: a,
|
|
700
|
+
deleteEditorImage: o
|
|
417
701
|
};
|
|
418
702
|
}
|
|
419
703
|
//#endregion
|
|
420
704
|
//#region src/composables/useMemiBoardSettings.ts
|
|
421
|
-
var
|
|
705
|
+
var Z = [
|
|
422
706
|
{
|
|
423
707
|
id: "free",
|
|
424
708
|
label: "자유"
|
|
@@ -432,10 +716,10 @@ var $ = [
|
|
|
432
716
|
label: "질문"
|
|
433
717
|
}
|
|
434
718
|
];
|
|
435
|
-
function
|
|
436
|
-
let t =
|
|
719
|
+
function Oe() {
|
|
720
|
+
let t = R(), n = l(), { isSignedIn: i, isAdmin: a } = G(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
|
|
437
721
|
let e = c.value?.categories;
|
|
438
|
-
return e && e.length > 0 ? e :
|
|
722
|
+
return e && e.length > 0 ? e : Z;
|
|
439
723
|
});
|
|
440
724
|
function f(e) {
|
|
441
725
|
if (e) return d.value.find((t) => t.id === e)?.label ?? e;
|
|
@@ -444,7 +728,7 @@ function ce() {
|
|
|
444
728
|
if (!i.value) return;
|
|
445
729
|
let e = s.value, t = await m(e).catch(() => null);
|
|
446
730
|
!t || t.exists() || await x(e, {
|
|
447
|
-
categories:
|
|
731
|
+
categories: Z,
|
|
448
732
|
updatedAt: b()
|
|
449
733
|
});
|
|
450
734
|
}
|
|
@@ -453,9 +737,9 @@ function ce() {
|
|
|
453
737
|
if (!t) throw Error("카테고리 이름을 입력해 주세요.");
|
|
454
738
|
if (!i.value) throw Error("로그인이 필요합니다.");
|
|
455
739
|
await h();
|
|
456
|
-
let n = d.value.find((e) => e.label === t || e.id ===
|
|
740
|
+
let n = d.value.find((e) => e.label === t || e.id === K(t));
|
|
457
741
|
if (n) return n.id;
|
|
458
|
-
let r =
|
|
742
|
+
let r = K(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
|
|
459
743
|
if (a.has(r)) {
|
|
460
744
|
let e = 2;
|
|
461
745
|
for (; a.has(`${r}-${e}`);) e++;
|
|
@@ -485,12 +769,12 @@ function ce() {
|
|
|
485
769
|
settingsPending: u,
|
|
486
770
|
ensureSettings: h,
|
|
487
771
|
addCategory: g,
|
|
488
|
-
DEFAULT_CATEGORIES:
|
|
772
|
+
DEFAULT_CATEGORIES: Z
|
|
489
773
|
};
|
|
490
774
|
}
|
|
491
775
|
//#endregion
|
|
492
776
|
//#region src/data/versionHistory.ts
|
|
493
|
-
var
|
|
777
|
+
var ke = [
|
|
494
778
|
{
|
|
495
779
|
version: "0.2.2",
|
|
496
780
|
date: "2026-07-31",
|
|
@@ -517,10 +801,83 @@ var le = [
|
|
|
517
801
|
];
|
|
518
802
|
//#endregion
|
|
519
803
|
//#region src/utils/formatDate.ts
|
|
520
|
-
function
|
|
804
|
+
function Ae(e) {
|
|
521
805
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
522
806
|
}
|
|
523
807
|
//#endregion
|
|
524
|
-
|
|
808
|
+
//#region src/utils/renderMarkdown.ts
|
|
809
|
+
function Q(e) {
|
|
810
|
+
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
811
|
+
}
|
|
812
|
+
function $(e) {
|
|
813
|
+
let t = e;
|
|
814
|
+
return t = t.replace(/`([^`]+)`/g, "<code>$1</code>"), t = t.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, "<img src=\"$2\" alt=\"$1\" class=\"max-w-full h-auto rounded-lg my-3\" loading=\"lazy\" />"), t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary underline\">$1</a>"), t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"), t = t.replace(/\*([^*]+)\*/g, "<em>$1</em>"), t = t.replace(/~~([^~]+)~~/g, "<del>$1</del>"), t;
|
|
815
|
+
}
|
|
816
|
+
function je(e) {
|
|
817
|
+
if (!e?.trim()) return "";
|
|
818
|
+
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
819
|
+
function c() {
|
|
820
|
+
if (!o || !s.length) {
|
|
821
|
+
o = null, s = [];
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
let e = o;
|
|
825
|
+
n.push(`<${e}>${s.map((e) => `<li>${$(e)}</li>`).join("")}</${e}>`), o = null, s = [];
|
|
826
|
+
}
|
|
827
|
+
for (; r < t.length;) {
|
|
828
|
+
let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
|
|
829
|
+
if (l) {
|
|
830
|
+
i ? (n.push(`<pre class="overflow-x-auto rounded-lg bg-muted p-3 text-sm"><code>${a.join("\n")}</code></pre>`), a = [], i = !1) : (c(), i = !0, l[1]), r++;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (i) {
|
|
834
|
+
a.push(Q(e)), r++;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(e)) {
|
|
838
|
+
c(), n.push("<hr class=\"my-4 border-default\" />"), r++;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
let u = e.match(/^(#{1,3})\s+(.+)$/);
|
|
842
|
+
if (u) {
|
|
843
|
+
c();
|
|
844
|
+
let e = u[1].length;
|
|
845
|
+
n.push(`<h${e} class="font-semibold mt-4 mb-2">${$(Q(u[2]))}</h${e}>`), r++;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (e.startsWith("> ")) {
|
|
849
|
+
c();
|
|
850
|
+
let e = [];
|
|
851
|
+
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(Q((t[r] ?? "").slice(2))), r++;
|
|
852
|
+
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${$(e.join("<br />"))}</blockquote>`);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
let d = e.match(/^[-*+]\s+(.+)$/);
|
|
856
|
+
if (d) {
|
|
857
|
+
o && o !== "ul" && c(), o = "ul", s.push(Q(d[1])), r++;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
let f = e.match(/^\d+\.\s+(.+)$/);
|
|
861
|
+
if (f) {
|
|
862
|
+
o && o !== "ol" && c(), o = "ol", s.push(Q(f[1])), r++;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (!e.trim()) {
|
|
866
|
+
c(), r++;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
c();
|
|
870
|
+
let p = [];
|
|
871
|
+
for (; r < t.length;) {
|
|
872
|
+
let e = t[r] ?? "";
|
|
873
|
+
if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
|
|
874
|
+
p.push(Q(e)), r++;
|
|
875
|
+
}
|
|
876
|
+
n.push(`<p class="my-2 leading-relaxed">${$(p.join("<br />"))}</p>`);
|
|
877
|
+
}
|
|
878
|
+
return c(), i && a.length && n.push(`<pre class="overflow-x-auto rounded-lg bg-muted p-3 text-sm"><code>${a.join("\n")}</code></pre>`), n.join("\n");
|
|
879
|
+
}
|
|
880
|
+
//#endregion
|
|
881
|
+
export { z as DEFAULT_BLOCK_BAN_DECAY_MS, le as DEFAULT_BLOCK_BAN_THRESHOLD, Z as DEFAULT_CATEGORIES, Ee as EDITOR_IMAGE_MAX_BYTES, Te as compressImage, L as configureMemiBoard, B as effectiveModerationBlockCount, pe as extractEditorImageUrls, Ae as formatDate, U as formatRestrictedUntilLabel, H as isModerationWriteRestricted, V as moderationWriteRestrictedUntilMs, q as postNamespaceFromStoragePath, je as renderMarkdownToHtml, me as storagePathFromDownloadUrl, G as useMemiBoardAuth, _e as useMemiBoardComments, R as useMemiBoardConfig, we as useMemiBoardModeration, ge as useMemiBoardPosts, Oe as useMemiBoardSettings, De as useMemiBoardStorage, ke as versionHistory };
|
|
525
882
|
|
|
526
883
|
//# sourceMappingURL=index.js.map
|