memi-board 0.4.5 → 0.4.12
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 -2
- package/dist/composables/useMemiBoardStorage.d.ts +5 -1
- package/dist/config.d.ts +7 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +472 -212
- 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 +217 -71
- package/dist/types.d.ts +16 -4
- 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/docs/firebase-setup.md +106 -0
- package/docs/firestore.rules.example +97 -0
- package/docs/storage.rules.example +49 -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 E, signOut as ie, updateProfile as ae } from "firebase/auth";
|
|
5
|
+
import { deleteObject as D, getDownloadURL as O, getStorage as k, listAll as A, ref as j, uploadBytes as M, uploadBytesResumable as oe } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as N, Schema as P, getAI as F, getGenerativeModel as I } from "firebase/ai";
|
|
7
7
|
//#region src/config.ts
|
|
8
|
-
var
|
|
9
|
-
function
|
|
8
|
+
var L = "__MEMI_BOARD_CONFIG__";
|
|
9
|
+
function R() {
|
|
10
10
|
let e = globalThis;
|
|
11
|
-
return e[
|
|
11
|
+
return e[L] || (e[L] = t({
|
|
12
12
|
collectionPrefix: "memiBoard",
|
|
13
13
|
auth: { providers: ["google", "apple"] },
|
|
14
14
|
moderation: {
|
|
@@ -16,113 +16,241 @@ function B() {
|
|
|
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[L];
|
|
22
24
|
}
|
|
23
|
-
function
|
|
24
|
-
let t =
|
|
25
|
+
function z(e) {
|
|
26
|
+
let t = R();
|
|
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 B() {
|
|
30
|
+
return R();
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/utils/moderation-strike.ts
|
|
34
|
+
var se = 3, V = 864e5;
|
|
35
|
+
function H(e, t, n = Date.now(), r = V) {
|
|
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 U(e, t, n = Date.now(), r = 3, i = V) {
|
|
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 W(e, t, n = Date.now(), r = 3, i = V) {
|
|
52
|
+
return U(e, t, n, r, i) != null;
|
|
53
|
+
}
|
|
54
|
+
function G(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 ce(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 K = "__MEMI_BOARD_AUTH_STATE__";
|
|
74
|
+
function le() {
|
|
75
|
+
let e = globalThis;
|
|
76
|
+
return e[K] || (e[K] = {
|
|
77
|
+
role: n("user"),
|
|
78
|
+
rolePending: n(!0),
|
|
79
|
+
rawBlockCount: n(0),
|
|
80
|
+
blockAtMs: n(null),
|
|
81
|
+
watchStarted: !1
|
|
82
|
+
}), e[K];
|
|
83
|
+
}
|
|
84
|
+
function q() {
|
|
85
|
+
let t = B(), n = le(), 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 : V;
|
|
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 = ce(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 D = e(() => !!s.value), O = e(() => u.value === "admin"), k = e(() => H(f.value, h.value, Date.now(), v())), A = e(() => {
|
|
140
|
+
let e = _();
|
|
141
|
+
return e <= 0 ? !1 : W(f.value, h.value, Date.now(), e, v());
|
|
142
|
+
}), j = e(() => {
|
|
143
|
+
let e = _();
|
|
144
|
+
if (e <= 0) return null;
|
|
145
|
+
let t = U(f.value, h.value, Date.now(), e, v());
|
|
146
|
+
return t == null ? null : G(t);
|
|
147
|
+
}), M = e(() => {
|
|
148
|
+
if (!A.value) return null;
|
|
149
|
+
let e = j.value;
|
|
150
|
+
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
151
|
+
});
|
|
152
|
+
async function oe() {
|
|
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 = H(i, a, a, v()), c = W(i, a, a, t, v()), l = ` (경고 ${o}/${t})`;
|
|
174
|
+
if (c) {
|
|
175
|
+
let e = U(i, a, a, t, v()), n = e == null ? null : G(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 E(N(), new T());
|
|
68
190
|
}
|
|
69
|
-
async function
|
|
70
|
-
let e = new
|
|
71
|
-
return e.addScope("email"), e.addScope("name"),
|
|
191
|
+
async function F() {
|
|
192
|
+
let e = new te("apple.com");
|
|
193
|
+
return e.addScope("email"), e.addScope("name"), E(N(), e);
|
|
72
194
|
}
|
|
73
|
-
async function
|
|
74
|
-
return
|
|
195
|
+
async function I(e, t) {
|
|
196
|
+
return re(N(), e, t);
|
|
75
197
|
}
|
|
76
|
-
async function
|
|
77
|
-
let r = await
|
|
78
|
-
return n && await
|
|
198
|
+
async function L(e, t, n) {
|
|
199
|
+
let r = await ne(N(), e, t);
|
|
200
|
+
return n && await ae(r.user, { displayName: n }), r;
|
|
79
201
|
}
|
|
80
|
-
async function
|
|
81
|
-
await
|
|
202
|
+
async function R() {
|
|
203
|
+
await ie(N());
|
|
82
204
|
}
|
|
83
|
-
function
|
|
84
|
-
return
|
|
205
|
+
function z(e) {
|
|
206
|
+
return D.value && (s.value?.uid === e.authorUid || O.value);
|
|
85
207
|
}
|
|
86
|
-
function
|
|
87
|
-
return
|
|
208
|
+
function se(e) {
|
|
209
|
+
return D.value && (s.value?.uid === e.authorUid || O.value);
|
|
88
210
|
}
|
|
89
211
|
return {
|
|
90
212
|
user: s,
|
|
91
|
-
isSignedIn:
|
|
92
|
-
isAdmin:
|
|
213
|
+
isSignedIn: D,
|
|
214
|
+
isAdmin: O,
|
|
93
215
|
rolePending: d,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
216
|
+
moderationBlockCount: k,
|
|
217
|
+
isWriteRestricted: A,
|
|
218
|
+
restrictedUntilLabel: j,
|
|
219
|
+
restrictedMessage: M,
|
|
220
|
+
signInWithGoogle: P,
|
|
221
|
+
signInWithApple: F,
|
|
222
|
+
signInWithEmail: I,
|
|
223
|
+
signUpWithEmail: L,
|
|
224
|
+
signOut: R,
|
|
225
|
+
canEdit: z,
|
|
226
|
+
canDelete: z,
|
|
227
|
+
canDeleteComment: se,
|
|
228
|
+
recordContentModerationBlock: oe,
|
|
229
|
+
refreshBoardUser: w
|
|
102
230
|
};
|
|
103
231
|
}
|
|
104
232
|
//#endregion
|
|
105
233
|
//#region src/utils/slugify.ts
|
|
106
|
-
function
|
|
234
|
+
function J(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 ue(e, t = 60) {
|
|
110
238
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
111
239
|
}
|
|
112
240
|
//#endregion
|
|
113
241
|
//#region src/composables/useMemiBoardPosts.ts
|
|
114
|
-
async function
|
|
115
|
-
let t = await
|
|
242
|
+
async function de(e) {
|
|
243
|
+
let t = await A(e).catch(() => ({
|
|
116
244
|
items: [],
|
|
117
245
|
prefixes: []
|
|
118
246
|
}));
|
|
119
|
-
await Promise.all([...t.items.map((e) =>
|
|
247
|
+
await Promise.all([...t.items.map((e) => D(e)), ...t.prefixes.map((e) => de(e))]);
|
|
120
248
|
}
|
|
121
|
-
function
|
|
122
|
-
let e =
|
|
249
|
+
function fe() {
|
|
250
|
+
let e = B(), 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
251
|
async function g(e = {}) {
|
|
124
252
|
let t = e.pageSize ?? 20, n = [];
|
|
125
|
-
e.category && n.push(
|
|
253
|
+
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
254
|
let r = await h(y(i(), ...n)), a = r.docs.slice(0, t);
|
|
127
255
|
return {
|
|
128
256
|
posts: a.map((e) => ({
|
|
@@ -133,7 +261,7 @@ function K() {
|
|
|
133
261
|
hasMore: r.docs.length > t
|
|
134
262
|
};
|
|
135
263
|
}
|
|
136
|
-
async function
|
|
264
|
+
async function T(e) {
|
|
137
265
|
let [t, n] = await Promise.all([m(a(e)), m(o(e))]);
|
|
138
266
|
return t.exists() ? {
|
|
139
267
|
id: t.id,
|
|
@@ -141,8 +269,8 @@ function K() {
|
|
|
141
269
|
content: n.exists() ? n.data().content ?? "" : ""
|
|
142
270
|
} : null;
|
|
143
271
|
}
|
|
144
|
-
async function
|
|
145
|
-
let t =
|
|
272
|
+
async function te(e) {
|
|
273
|
+
let t = J(e.title) || "post", n = t, r = 1;
|
|
146
274
|
for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
|
|
147
275
|
return await Promise.all([x(a(n), {
|
|
148
276
|
title: e.title,
|
|
@@ -159,7 +287,7 @@ function K() {
|
|
|
159
287
|
updatedAt: b()
|
|
160
288
|
}), x(o(n), { content: e.content })]), n;
|
|
161
289
|
}
|
|
162
|
-
async function
|
|
290
|
+
async function ne(e, t) {
|
|
163
291
|
await Promise.all([C(a(e), {
|
|
164
292
|
title: t.title,
|
|
165
293
|
tags: t.tags ?? [],
|
|
@@ -168,29 +296,29 @@ function K() {
|
|
|
168
296
|
updatedAt: b()
|
|
169
297
|
}), x(o(e), { content: t.content }, { merge: !0 })]);
|
|
170
298
|
}
|
|
171
|
-
async function
|
|
299
|
+
async function re(e) {
|
|
172
300
|
let i = await h(c(e));
|
|
173
301
|
for (let e = 0; e < i.docs.length; e += 450) {
|
|
174
|
-
let n =
|
|
302
|
+
let n = w(t);
|
|
175
303
|
i.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
176
304
|
}
|
|
177
|
-
let s =
|
|
178
|
-
await d(o(e)), await
|
|
305
|
+
let s = k(n);
|
|
306
|
+
await d(o(e)), await de(j(s, `${r()}/posts/${e}`)), await d(a(e));
|
|
179
307
|
}
|
|
180
308
|
return {
|
|
181
309
|
getPosts: g,
|
|
182
|
-
getPost:
|
|
183
|
-
createPost:
|
|
184
|
-
updatePost:
|
|
185
|
-
deletePost:
|
|
310
|
+
getPost: T,
|
|
311
|
+
createPost: te,
|
|
312
|
+
updatePost: ne,
|
|
313
|
+
deletePost: re
|
|
186
314
|
};
|
|
187
315
|
}
|
|
188
316
|
//#endregion
|
|
189
317
|
//#region src/composables/useMemiBoardComments.ts
|
|
190
|
-
function
|
|
191
|
-
let n =
|
|
318
|
+
function pe(t) {
|
|
319
|
+
let n = B(), 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
320
|
async function d(e) {
|
|
193
|
-
let t =
|
|
321
|
+
let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
|
|
194
322
|
t.set(i, {
|
|
195
323
|
postId: o.value,
|
|
196
324
|
body: e.body,
|
|
@@ -202,7 +330,7 @@ function q(t) {
|
|
|
202
330
|
}), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
|
|
203
331
|
}
|
|
204
332
|
async function f(e) {
|
|
205
|
-
let t =
|
|
333
|
+
let t = w(r), n = a();
|
|
206
334
|
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
335
|
}
|
|
208
336
|
return {
|
|
@@ -213,7 +341,25 @@ function q(t) {
|
|
|
213
341
|
}
|
|
214
342
|
//#endregion
|
|
215
343
|
//#region src/utils/moderation-prompt.ts
|
|
216
|
-
var
|
|
344
|
+
var me = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", he = {
|
|
345
|
+
abuse: "욕설·비속어·모욕",
|
|
346
|
+
spam: "스팸·광고",
|
|
347
|
+
adult: "선정적·성인 내용",
|
|
348
|
+
violence: "폭력·위협",
|
|
349
|
+
other: "부적절한 내용",
|
|
350
|
+
none: ""
|
|
351
|
+
};
|
|
352
|
+
function Y(e) {
|
|
353
|
+
if (e.via === "local" && e.localHit) {
|
|
354
|
+
let t = e.localHit.trim();
|
|
355
|
+
return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
|
|
356
|
+
}
|
|
357
|
+
let t = (e.reason || "").trim();
|
|
358
|
+
if (t) return t;
|
|
359
|
+
let n = e.category && e.category !== "none" ? he[e.category] || e.category : "";
|
|
360
|
+
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
361
|
+
}
|
|
362
|
+
var ge = [
|
|
217
363
|
"씨발",
|
|
218
364
|
"시발",
|
|
219
365
|
"시이발",
|
|
@@ -239,10 +385,10 @@ var J = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터
|
|
|
239
385
|
"bitch",
|
|
240
386
|
"asshole"
|
|
241
387
|
];
|
|
242
|
-
function
|
|
388
|
+
function _e(e) {
|
|
243
389
|
return new RegExp(e.join("|"), "i");
|
|
244
390
|
}
|
|
245
|
-
var
|
|
391
|
+
var ve = /* @__PURE__ */ new Set([
|
|
246
392
|
"none",
|
|
247
393
|
"abuse",
|
|
248
394
|
"spam",
|
|
@@ -250,7 +396,7 @@ var se = /* @__PURE__ */ new Set([
|
|
|
250
396
|
"violence",
|
|
251
397
|
"other"
|
|
252
398
|
]);
|
|
253
|
-
function
|
|
399
|
+
function ye(e) {
|
|
254
400
|
let t = e.trim();
|
|
255
401
|
if (!t) return null;
|
|
256
402
|
let n;
|
|
@@ -268,7 +414,7 @@ function Y(e) {
|
|
|
268
414
|
if (typeof n != "object" || !n) return null;
|
|
269
415
|
let r = n;
|
|
270
416
|
if (typeof r.flagged != "boolean") return null;
|
|
271
|
-
let i = typeof r.category == "string" &&
|
|
417
|
+
let i = typeof r.category == "string" && ve.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
272
418
|
return {
|
|
273
419
|
flagged: r.flagged,
|
|
274
420
|
category: i,
|
|
@@ -283,48 +429,46 @@ var X = {
|
|
|
283
429
|
reason: "",
|
|
284
430
|
via: "empty"
|
|
285
431
|
};
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
function r(e) {
|
|
292
|
-
return e.match(n) ? {
|
|
432
|
+
function be() {
|
|
433
|
+
let e = B().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = q(), a = _e([...ge, ...e.localBlocklist ?? []]);
|
|
434
|
+
function o(e) {
|
|
435
|
+
let t = e.match(a);
|
|
436
|
+
return t ? {
|
|
293
437
|
flagged: !0,
|
|
294
438
|
category: "abuse",
|
|
295
|
-
reason:
|
|
439
|
+
reason: Y({
|
|
440
|
+
via: "local",
|
|
441
|
+
localHit: t[0] || "",
|
|
442
|
+
category: "abuse"
|
|
443
|
+
}),
|
|
296
444
|
via: "local"
|
|
297
445
|
} : null;
|
|
298
446
|
}
|
|
299
|
-
function
|
|
300
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
301
|
-
backend: new
|
|
447
|
+
function c() {
|
|
448
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = F(t, {
|
|
449
|
+
backend: new N(),
|
|
302
450
|
useLimitedUseAppCheckTokens: r
|
|
303
451
|
});
|
|
304
|
-
return {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
} })
|
|
319
|
-
}
|
|
320
|
-
})
|
|
321
|
-
};
|
|
452
|
+
return I(i, {
|
|
453
|
+
model: n,
|
|
454
|
+
systemInstruction: me,
|
|
455
|
+
generationConfig: {
|
|
456
|
+
temperature: 0,
|
|
457
|
+
maxOutputTokens: 256,
|
|
458
|
+
responseMimeType: "application/json",
|
|
459
|
+
responseSchema: P.object({ properties: {
|
|
460
|
+
flagged: P.boolean(),
|
|
461
|
+
category: P.string(),
|
|
462
|
+
reason: P.string()
|
|
463
|
+
} })
|
|
464
|
+
}
|
|
465
|
+
});
|
|
322
466
|
}
|
|
323
|
-
function
|
|
467
|
+
function l() {
|
|
324
468
|
return e.onError === "block" ? {
|
|
325
469
|
flagged: !0,
|
|
326
470
|
category: "other",
|
|
327
|
-
reason: "
|
|
471
|
+
reason: "내용 검토에 실패해 글을 등록할 수 없습니다. 잠시 후 다시 시도해 주세요.",
|
|
328
472
|
error: !0,
|
|
329
473
|
via: "ai-error-block"
|
|
330
474
|
} : {
|
|
@@ -335,76 +479,69 @@ function Q() {
|
|
|
335
479
|
via: "ai-error-allow"
|
|
336
480
|
};
|
|
337
481
|
}
|
|
338
|
-
|
|
339
|
-
let
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return Z("local-block", {
|
|
357
|
-
hit: e,
|
|
358
|
-
...s
|
|
359
|
-
}), Z("result", s), s;
|
|
360
|
-
}
|
|
361
|
-
if (Z("local-pass", { message: "로컬 금칙어 미매칭 → AI Logic 호출" }), e.enabled === !1) {
|
|
362
|
-
let e = {
|
|
363
|
-
...X,
|
|
364
|
-
via: "disabled"
|
|
365
|
-
};
|
|
366
|
-
return Z("result", e), e;
|
|
482
|
+
function u(e, t = "") {
|
|
483
|
+
let n = Y({
|
|
484
|
+
reason: e.reason,
|
|
485
|
+
category: e.category,
|
|
486
|
+
via: e.via
|
|
487
|
+
}), r = t ? `${n}${t}` : n;
|
|
488
|
+
return {
|
|
489
|
+
...e,
|
|
490
|
+
reason: r
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function d(e) {
|
|
494
|
+
if (!e.flagged || e.error || e.via !== "local" && e.via !== "ai") return e;
|
|
495
|
+
try {
|
|
496
|
+
let { messageSuffix: t } = await i();
|
|
497
|
+
return u(e, t);
|
|
498
|
+
} catch (t) {
|
|
499
|
+
return console.warn("[memi-board] record moderation strike failed", t instanceof Error ? t.message : t), u(e);
|
|
367
500
|
}
|
|
501
|
+
}
|
|
502
|
+
async function f(t) {
|
|
503
|
+
if (n.value) return {
|
|
504
|
+
flagged: !0,
|
|
505
|
+
category: "other",
|
|
506
|
+
reason: r.value || "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.",
|
|
507
|
+
via: "restricted"
|
|
508
|
+
};
|
|
509
|
+
let i = t.trim();
|
|
510
|
+
if (!i) return {
|
|
511
|
+
...X,
|
|
512
|
+
via: "empty"
|
|
513
|
+
};
|
|
514
|
+
let a = o(i);
|
|
515
|
+
if (a) return d(a);
|
|
516
|
+
if (e.enabled === !1) return {
|
|
517
|
+
...X,
|
|
518
|
+
via: "disabled"
|
|
519
|
+
};
|
|
368
520
|
try {
|
|
369
|
-
let
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
let u = Y(l);
|
|
380
|
-
if (!u) {
|
|
381
|
-
Z("ai-parse-fail", { raw: l.slice(0, 300) });
|
|
382
|
-
let e = a();
|
|
383
|
-
return Z("result", e), e;
|
|
384
|
-
}
|
|
385
|
-
let d = {
|
|
386
|
-
flagged: u.flagged,
|
|
387
|
-
category: u.category,
|
|
388
|
-
reason: u.reason,
|
|
521
|
+
let e = ye((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
522
|
+
if (!e) return l();
|
|
523
|
+
let t = {
|
|
524
|
+
flagged: e.flagged,
|
|
525
|
+
category: e.category,
|
|
526
|
+
reason: Y({
|
|
527
|
+
reason: e.reason,
|
|
528
|
+
category: e.category,
|
|
529
|
+
via: "ai"
|
|
530
|
+
}),
|
|
389
531
|
via: "ai"
|
|
390
532
|
};
|
|
391
|
-
return
|
|
392
|
-
...d,
|
|
393
|
-
ms: c
|
|
394
|
-
}), d;
|
|
533
|
+
return t.flagged ? d(t) : t;
|
|
395
534
|
} catch (e) {
|
|
396
|
-
|
|
397
|
-
Z("ai-error", {
|
|
398
|
-
message: e instanceof Error ? e.message : String(e),
|
|
399
|
-
name: e instanceof Error ? e.name : void 0,
|
|
400
|
-
code: t?.code,
|
|
401
|
-
cause: t?.cause instanceof Error ? t.cause.message : t?.cause
|
|
402
|
-
});
|
|
403
|
-
let n = a();
|
|
404
|
-
return Z("result", n), n;
|
|
535
|
+
return console.warn("[memi-board] moderation AI failed", e instanceof Error ? e.message : e), l();
|
|
405
536
|
}
|
|
406
537
|
}
|
|
407
|
-
async function
|
|
538
|
+
async function p(t) {
|
|
539
|
+
if (n.value) return {
|
|
540
|
+
flagged: !0,
|
|
541
|
+
category: "other",
|
|
542
|
+
reason: r.value || "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.",
|
|
543
|
+
via: "restricted"
|
|
544
|
+
};
|
|
408
545
|
if (e.enabled === !1 || !e.moderateImages) return {
|
|
409
546
|
...X,
|
|
410
547
|
via: "disabled"
|
|
@@ -413,39 +550,58 @@ function Q() {
|
|
|
413
550
|
let e = await new Promise((e, n) => {
|
|
414
551
|
let r = new FileReader();
|
|
415
552
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
416
|
-
}),
|
|
417
|
-
Z("ai-image-call", {
|
|
418
|
-
modelName: r,
|
|
419
|
-
type: t.type,
|
|
420
|
-
size: t.size
|
|
421
|
-
});
|
|
422
|
-
let o = (await n.generateContent([{ inlineData: {
|
|
553
|
+
}), n = ye((await c().generateContent([{ inlineData: {
|
|
423
554
|
mimeType: t.type || "image/jpeg",
|
|
424
555
|
data: e
|
|
425
|
-
} }, "이 이미지를 심사해 주세요."])).response.text()
|
|
426
|
-
if (!
|
|
427
|
-
let
|
|
428
|
-
flagged:
|
|
429
|
-
category:
|
|
430
|
-
reason:
|
|
556
|
+
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
557
|
+
if (!n) return l();
|
|
558
|
+
let r = {
|
|
559
|
+
flagged: n.flagged,
|
|
560
|
+
category: n.category,
|
|
561
|
+
reason: Y({
|
|
562
|
+
reason: n.reason,
|
|
563
|
+
category: n.category,
|
|
564
|
+
via: "ai"
|
|
565
|
+
}),
|
|
431
566
|
via: "ai"
|
|
432
567
|
};
|
|
433
|
-
return
|
|
568
|
+
return r.flagged ? d(r) : r;
|
|
434
569
|
} catch (e) {
|
|
435
|
-
return
|
|
570
|
+
return console.warn("[memi-board] moderation AI image failed", e instanceof Error ? e.message : e), l();
|
|
436
571
|
}
|
|
437
572
|
}
|
|
438
573
|
return {
|
|
439
|
-
checkText:
|
|
440
|
-
checkImage:
|
|
574
|
+
checkText: f,
|
|
575
|
+
checkImage: p
|
|
441
576
|
};
|
|
442
577
|
}
|
|
443
578
|
//#endregion
|
|
579
|
+
//#region src/utils/compressImage.ts
|
|
580
|
+
async function xe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
581
|
+
return new Promise((r, i) => {
|
|
582
|
+
let a = new Image(), o = URL.createObjectURL(e);
|
|
583
|
+
a.onload = () => {
|
|
584
|
+
URL.revokeObjectURL(o);
|
|
585
|
+
let e = Math.min(1, t / a.naturalWidth), s = document.createElement("canvas");
|
|
586
|
+
s.width = Math.round(a.naturalWidth * e), s.height = Math.round(a.naturalHeight * e);
|
|
587
|
+
let c = s.getContext("2d");
|
|
588
|
+
if (!c) {
|
|
589
|
+
i(/* @__PURE__ */ Error("Canvas를 사용할 수 없습니다"));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
c.drawImage(a, 0, 0, s.width, s.height), s.toBlob((e) => e ? r(e) : i(/* @__PURE__ */ Error("압축 실패")), "image/jpeg", n);
|
|
593
|
+
}, a.onerror = () => {
|
|
594
|
+
URL.revokeObjectURL(o), i(/* @__PURE__ */ Error("이미지 로드 실패"));
|
|
595
|
+
}, a.src = o;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
//#endregion
|
|
444
599
|
//#region src/composables/useMemiBoardStorage.ts
|
|
445
|
-
|
|
446
|
-
|
|
600
|
+
var Se = 5242880;
|
|
601
|
+
function Ce() {
|
|
602
|
+
let e = B(), t = s(), n = () => e.collectionPrefix;
|
|
447
603
|
function r(e, r, i) {
|
|
448
|
-
let a =
|
|
604
|
+
let a = k(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${ue(e.name)}`, s = j(a, o), c = oe(s, e, {
|
|
449
605
|
contentType: e.type || "application/octet-stream",
|
|
450
606
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
451
607
|
});
|
|
@@ -454,7 +610,7 @@ function ce() {
|
|
|
454
610
|
c.on("state_changed", (e) => {
|
|
455
611
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
456
612
|
}, n, async () => {
|
|
457
|
-
let n = await
|
|
613
|
+
let n = await O(s);
|
|
458
614
|
t({
|
|
459
615
|
name: e.name,
|
|
460
616
|
url: n,
|
|
@@ -468,17 +624,39 @@ function ce() {
|
|
|
468
624
|
};
|
|
469
625
|
}
|
|
470
626
|
async function i(e) {
|
|
471
|
-
let n =
|
|
472
|
-
await
|
|
627
|
+
let n = k(t);
|
|
628
|
+
await D(j(n, e.path)).catch(() => {});
|
|
629
|
+
}
|
|
630
|
+
async function a(e, r) {
|
|
631
|
+
if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
632
|
+
if (e.size > 5242880) throw Error(`이미지는 ${Se / 1024 / 1024}MB 이하여야 합니다.`);
|
|
633
|
+
let i = k(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = ue(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = j(i, l);
|
|
634
|
+
await M(u, e, { contentType: e.type || "image/png" });
|
|
635
|
+
let d = await O(u), f = await xe(e, {
|
|
636
|
+
maxWidth: 400,
|
|
637
|
+
quality: .8
|
|
638
|
+
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = j(i, p);
|
|
639
|
+
return await M(m, f, { contentType: "image/jpeg" }), {
|
|
640
|
+
originalUrl: d,
|
|
641
|
+
originalPath: l,
|
|
642
|
+
thumbnailUrl: await O(m),
|
|
643
|
+
thumbnailPath: p
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function o(e) {
|
|
647
|
+
let n = k(t);
|
|
648
|
+
await Promise.all([D(j(n, e.originalPath)).catch(() => {}), D(j(n, e.thumbnailPath)).catch(() => {})]);
|
|
473
649
|
}
|
|
474
650
|
return {
|
|
475
651
|
uploadAttachment: r,
|
|
476
|
-
deleteAttachment: i
|
|
652
|
+
deleteAttachment: i,
|
|
653
|
+
uploadEditorImage: a,
|
|
654
|
+
deleteEditorImage: o
|
|
477
655
|
};
|
|
478
656
|
}
|
|
479
657
|
//#endregion
|
|
480
658
|
//#region src/composables/useMemiBoardSettings.ts
|
|
481
|
-
var
|
|
659
|
+
var Z = [
|
|
482
660
|
{
|
|
483
661
|
id: "free",
|
|
484
662
|
label: "자유"
|
|
@@ -492,10 +670,10 @@ var $ = [
|
|
|
492
670
|
label: "질문"
|
|
493
671
|
}
|
|
494
672
|
];
|
|
495
|
-
function
|
|
496
|
-
let t =
|
|
673
|
+
function we() {
|
|
674
|
+
let t = B(), n = l(), { isSignedIn: i, isAdmin: a } = q(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
|
|
497
675
|
let e = c.value?.categories;
|
|
498
|
-
return e && e.length > 0 ? e :
|
|
676
|
+
return e && e.length > 0 ? e : Z;
|
|
499
677
|
});
|
|
500
678
|
function f(e) {
|
|
501
679
|
if (e) return d.value.find((t) => t.id === e)?.label ?? e;
|
|
@@ -504,7 +682,7 @@ function le() {
|
|
|
504
682
|
if (!i.value) return;
|
|
505
683
|
let e = s.value, t = await m(e).catch(() => null);
|
|
506
684
|
!t || t.exists() || await x(e, {
|
|
507
|
-
categories:
|
|
685
|
+
categories: Z,
|
|
508
686
|
updatedAt: b()
|
|
509
687
|
});
|
|
510
688
|
}
|
|
@@ -513,9 +691,9 @@ function le() {
|
|
|
513
691
|
if (!t) throw Error("카테고리 이름을 입력해 주세요.");
|
|
514
692
|
if (!i.value) throw Error("로그인이 필요합니다.");
|
|
515
693
|
await h();
|
|
516
|
-
let n = d.value.find((e) => e.label === t || e.id ===
|
|
694
|
+
let n = d.value.find((e) => e.label === t || e.id === J(t));
|
|
517
695
|
if (n) return n.id;
|
|
518
|
-
let r =
|
|
696
|
+
let r = J(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
|
|
519
697
|
if (a.has(r)) {
|
|
520
698
|
let e = 2;
|
|
521
699
|
for (; a.has(`${r}-${e}`);) e++;
|
|
@@ -545,12 +723,12 @@ function le() {
|
|
|
545
723
|
settingsPending: u,
|
|
546
724
|
ensureSettings: h,
|
|
547
725
|
addCategory: g,
|
|
548
|
-
DEFAULT_CATEGORIES:
|
|
726
|
+
DEFAULT_CATEGORIES: Z
|
|
549
727
|
};
|
|
550
728
|
}
|
|
551
729
|
//#endregion
|
|
552
730
|
//#region src/data/versionHistory.ts
|
|
553
|
-
var
|
|
731
|
+
var Te = [
|
|
554
732
|
{
|
|
555
733
|
version: "0.2.2",
|
|
556
734
|
date: "2026-07-31",
|
|
@@ -577,10 +755,92 @@ var ue = [
|
|
|
577
755
|
];
|
|
578
756
|
//#endregion
|
|
579
757
|
//#region src/utils/formatDate.ts
|
|
580
|
-
function
|
|
758
|
+
function Ee(e) {
|
|
581
759
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
582
760
|
}
|
|
583
761
|
//#endregion
|
|
584
|
-
|
|
762
|
+
//#region src/utils/extractEditorImageUrls.ts
|
|
763
|
+
function De(e) {
|
|
764
|
+
if (!e) return [];
|
|
765
|
+
let t = /* @__PURE__ */ new Set();
|
|
766
|
+
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
767
|
+
for (let n of e.matchAll(/<img[^>]*\ssrc=["'](https?:\/\/[^"']+)["']/gi)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
768
|
+
return [...t];
|
|
769
|
+
}
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/utils/renderMarkdown.ts
|
|
772
|
+
function Q(e) {
|
|
773
|
+
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
774
|
+
}
|
|
775
|
+
function $(e) {
|
|
776
|
+
let t = e;
|
|
777
|
+
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;
|
|
778
|
+
}
|
|
779
|
+
function Oe(e) {
|
|
780
|
+
if (!e?.trim()) return "";
|
|
781
|
+
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
782
|
+
function c() {
|
|
783
|
+
if (!o || !s.length) {
|
|
784
|
+
o = null, s = [];
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
let e = o;
|
|
788
|
+
n.push(`<${e}>${s.map((e) => `<li>${$(e)}</li>`).join("")}</${e}>`), o = null, s = [];
|
|
789
|
+
}
|
|
790
|
+
for (; r < t.length;) {
|
|
791
|
+
let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
|
|
792
|
+
if (l) {
|
|
793
|
+
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++;
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (i) {
|
|
797
|
+
a.push(Q(e)), r++;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(e)) {
|
|
801
|
+
c(), n.push("<hr class=\"my-4 border-default\" />"), r++;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
let u = e.match(/^(#{1,3})\s+(.+)$/);
|
|
805
|
+
if (u) {
|
|
806
|
+
c();
|
|
807
|
+
let e = u[1].length;
|
|
808
|
+
n.push(`<h${e} class="font-semibold mt-4 mb-2">${$(Q(u[2]))}</h${e}>`), r++;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (e.startsWith("> ")) {
|
|
812
|
+
c();
|
|
813
|
+
let e = [];
|
|
814
|
+
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(Q((t[r] ?? "").slice(2))), r++;
|
|
815
|
+
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${$(e.join("<br />"))}</blockquote>`);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
let d = e.match(/^[-*+]\s+(.+)$/);
|
|
819
|
+
if (d) {
|
|
820
|
+
o && o !== "ul" && c(), o = "ul", s.push(Q(d[1])), r++;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
let f = e.match(/^\d+\.\s+(.+)$/);
|
|
824
|
+
if (f) {
|
|
825
|
+
o && o !== "ol" && c(), o = "ol", s.push(Q(f[1])), r++;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (!e.trim()) {
|
|
829
|
+
c(), r++;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
c();
|
|
833
|
+
let p = [];
|
|
834
|
+
for (; r < t.length;) {
|
|
835
|
+
let e = t[r] ?? "";
|
|
836
|
+
if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
|
|
837
|
+
p.push(Q(e)), r++;
|
|
838
|
+
}
|
|
839
|
+
n.push(`<p class="my-2 leading-relaxed">${$(p.join("<br />"))}</p>`);
|
|
840
|
+
}
|
|
841
|
+
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");
|
|
842
|
+
}
|
|
843
|
+
//#endregion
|
|
844
|
+
export { V as DEFAULT_BLOCK_BAN_DECAY_MS, se as DEFAULT_BLOCK_BAN_THRESHOLD, Z as DEFAULT_CATEGORIES, Se as EDITOR_IMAGE_MAX_BYTES, xe as compressImage, z as configureMemiBoard, H as effectiveModerationBlockCount, De as extractEditorImageUrls, Ee as formatDate, G as formatRestrictedUntilLabel, W as isModerationWriteRestricted, U as moderationWriteRestrictedUntilMs, Oe as renderMarkdownToHtml, q as useMemiBoardAuth, pe as useMemiBoardComments, B as useMemiBoardConfig, be as useMemiBoardModeration, fe as useMemiBoardPosts, we as useMemiBoardSettings, Ce as useMemiBoardStorage, Te as versionHistory };
|
|
585
845
|
|
|
586
846
|
//# sourceMappingURL=index.js.map
|