mktcms 0.1.14 → 0.1.16
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/dist/module.json +1 -1
- package/dist/module.mjs +5 -0
- package/dist/runtime/app/components/content/editor/csv.d.vue.ts +10 -0
- package/dist/runtime/app/components/content/editor/csv.vue +235 -0
- package/dist/runtime/app/components/content/editor/csv.vue.d.ts +10 -0
- package/dist/runtime/app/components/content/{editor.vue → editor/index.vue} +10 -4
- package/dist/runtime/app/components/content/editor/markdown.d.vue.ts +10 -0
- package/dist/runtime/app/components/content/editor/markdown.vue +69 -0
- package/dist/runtime/app/components/content/editor/markdown.vue.d.ts +10 -0
- package/dist/runtime/app/pages/admin/edit/[path].vue +1 -1
- package/dist/runtime/server/api/admin/content/[path].d.ts +2 -0
- package/dist/runtime/server/api/admin/content/[path].js +25 -0
- package/dist/runtime/server/api/content/[path].d.ts +1 -1
- package/dist/runtime/server/api/content/[path].js +42 -2
- package/package.json +3 -1
- /package/dist/runtime/app/components/content/{editor.d.vue.ts → editor/index.d.vue.ts} +0 -0
- /package/dist/runtime/app/components/content/{editor.vue.d.ts → editor/index.vue.d.ts} +0 -0
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -41,6 +41,11 @@ const module$1 = defineNuxtModule({
|
|
|
41
41
|
route: "/api/admin/logout",
|
|
42
42
|
handler: resolver.resolve("./runtime/server/api/admin/logout")
|
|
43
43
|
});
|
|
44
|
+
addServerHandler({
|
|
45
|
+
route: "/api/admin/content/:path",
|
|
46
|
+
method: "get",
|
|
47
|
+
handler: resolver.resolve("./runtime/server/api/admin/content/[path]")
|
|
48
|
+
});
|
|
44
49
|
addServerHandler({
|
|
45
50
|
route: "/api/admin/content/:path",
|
|
46
51
|
method: "post",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type __VLS_ModelProps = {
|
|
2
|
+
'content'?: string;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
|
+
"update:content": (value: string) => any;
|
|
6
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
7
|
+
"onUpdate:content"?: ((value: string) => any) | undefined;
|
|
8
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
const content = defineModel("content", { type: String, ...{ default: "" } });
|
|
4
|
+
function parseSemicolonCsv(text) {
|
|
5
|
+
const normalized = (text ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
6
|
+
if (!normalized.trim()) {
|
|
7
|
+
return { headers: [], rows: [] };
|
|
8
|
+
}
|
|
9
|
+
const rows2 = [];
|
|
10
|
+
let row = [];
|
|
11
|
+
let field = "";
|
|
12
|
+
let inQuotes = false;
|
|
13
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
14
|
+
const ch = normalized[i];
|
|
15
|
+
if (inQuotes) {
|
|
16
|
+
if (ch === '"') {
|
|
17
|
+
const next = normalized[i + 1];
|
|
18
|
+
if (next === '"') {
|
|
19
|
+
field += '"';
|
|
20
|
+
i++;
|
|
21
|
+
} else {
|
|
22
|
+
inQuotes = false;
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
field += ch;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (ch === '"') {
|
|
30
|
+
inQuotes = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ch === ";") {
|
|
34
|
+
row.push(field);
|
|
35
|
+
field = "";
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === "\n") {
|
|
39
|
+
row.push(field);
|
|
40
|
+
field = "";
|
|
41
|
+
if (!(row.length === 1 && row[0] === "" && i === normalized.length - 1)) {
|
|
42
|
+
rows2.push(row);
|
|
43
|
+
}
|
|
44
|
+
row = [];
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
field += ch;
|
|
48
|
+
}
|
|
49
|
+
row.push(field);
|
|
50
|
+
rows2.push(row);
|
|
51
|
+
const headers2 = rows2[0] ?? [];
|
|
52
|
+
const dataRows = rows2.slice(1);
|
|
53
|
+
const columnCount2 = Math.max(headers2.length, ...dataRows.map((r) => r.length), 0);
|
|
54
|
+
const paddedHeaders = Array.from({ length: columnCount2 }, (_, i) => headers2[i] ?? "");
|
|
55
|
+
const paddedRows = dataRows.map((r) => Array.from({ length: columnCount2 }, (_, i) => r[i] ?? ""));
|
|
56
|
+
return { headers: paddedHeaders, rows: paddedRows };
|
|
57
|
+
}
|
|
58
|
+
function escapeSemicolonCsvField(value) {
|
|
59
|
+
const v = value ?? "";
|
|
60
|
+
if (/[";\n]/.test(v)) {
|
|
61
|
+
return `"${v.replace(/"/g, '""')}"`;
|
|
62
|
+
}
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
function serializeSemicolonCsv(grid) {
|
|
66
|
+
const allRows = [grid.headers, ...grid.rows];
|
|
67
|
+
return allRows.map((r) => r.map(escapeSemicolonCsvField).join(";")).join("\n");
|
|
68
|
+
}
|
|
69
|
+
const headers = ref([]);
|
|
70
|
+
const rows = ref([]);
|
|
71
|
+
const columnCount = computed(() => headers.value.length);
|
|
72
|
+
const tableColspan = computed(() => Math.max(headers.value.length, 1) + 1);
|
|
73
|
+
let isApplyingFromContent = false;
|
|
74
|
+
let isApplyingToContent = false;
|
|
75
|
+
function applyFromContent() {
|
|
76
|
+
isApplyingFromContent = true;
|
|
77
|
+
try {
|
|
78
|
+
const grid = parseSemicolonCsv(content.value);
|
|
79
|
+
headers.value = grid.headers;
|
|
80
|
+
rows.value = grid.rows;
|
|
81
|
+
} finally {
|
|
82
|
+
isApplyingFromContent = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function applyToContent() {
|
|
86
|
+
if (isApplyingFromContent) return;
|
|
87
|
+
isApplyingToContent = true;
|
|
88
|
+
try {
|
|
89
|
+
content.value = serializeSemicolonCsv({ headers: headers.value, rows: rows.value });
|
|
90
|
+
} finally {
|
|
91
|
+
isApplyingToContent = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
watch(
|
|
95
|
+
() => content.value,
|
|
96
|
+
() => {
|
|
97
|
+
if (isApplyingToContent) return;
|
|
98
|
+
applyFromContent();
|
|
99
|
+
},
|
|
100
|
+
{ immediate: true }
|
|
101
|
+
);
|
|
102
|
+
watch(
|
|
103
|
+
() => [headers.value, rows.value],
|
|
104
|
+
() => applyToContent(),
|
|
105
|
+
{ deep: true }
|
|
106
|
+
);
|
|
107
|
+
function insertRow(atIndex) {
|
|
108
|
+
if (columnCount.value === 0) return;
|
|
109
|
+
const clamped = Math.max(0, Math.min(atIndex, rows.value.length));
|
|
110
|
+
rows.value.splice(clamped, 0, Array.from({ length: columnCount.value }, () => ""));
|
|
111
|
+
}
|
|
112
|
+
function moveRowUp(index) {
|
|
113
|
+
if (index <= 0) return;
|
|
114
|
+
const tmp = rows.value[index - 1];
|
|
115
|
+
rows.value[index - 1] = rows.value[index];
|
|
116
|
+
rows.value[index] = tmp;
|
|
117
|
+
}
|
|
118
|
+
function moveRowDown(index) {
|
|
119
|
+
if (index < 0 || index >= rows.value.length - 1) return;
|
|
120
|
+
const tmp = rows.value[index + 1];
|
|
121
|
+
rows.value[index + 1] = rows.value[index];
|
|
122
|
+
rows.value[index] = tmp;
|
|
123
|
+
}
|
|
124
|
+
function removeRow(index) {
|
|
125
|
+
rows.value.splice(index, 1);
|
|
126
|
+
}
|
|
127
|
+
function onCellInput(rowIndex, colIndex, value) {
|
|
128
|
+
const row = rows.value[rowIndex];
|
|
129
|
+
if (!row) return;
|
|
130
|
+
row[colIndex] = value;
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<template>
|
|
135
|
+
<div style="width: 100%;">
|
|
136
|
+
<div style="display: flex; gap: 8px; margin-bottom: 10px; align-items: center;">
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
:disabled="headers.length === 0"
|
|
140
|
+
@click="insertRow(0)"
|
|
141
|
+
>
|
|
142
|
+
+ Zeile (oben)
|
|
143
|
+
</button>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
:disabled="headers.length === 0"
|
|
147
|
+
@click="insertRow(rows.length)"
|
|
148
|
+
>
|
|
149
|
+
+ Zeile (unten)
|
|
150
|
+
</button>
|
|
151
|
+
<span
|
|
152
|
+
v-if="headers.length === 0"
|
|
153
|
+
style="opacity: 0.7;"
|
|
154
|
+
>
|
|
155
|
+
Keine Kopfzeile gefunden. Bitte eine CSV mit Kopfzeile bereitstellen, um Zeilen zu bearbeiten.
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div style="overflow: auto; border: 1px solid #d0d0d0;">
|
|
160
|
+
<table style="width: 100%; border-collapse: collapse; min-width: 520px;">
|
|
161
|
+
<thead>
|
|
162
|
+
<tr>
|
|
163
|
+
<th
|
|
164
|
+
v-for="(h, colIndex) in headers"
|
|
165
|
+
:key="colIndex"
|
|
166
|
+
style="text-align: left; padding: 8px; border-bottom: 1px solid #d0d0d0; background: #f6f6f6; position: sticky; top: 0;"
|
|
167
|
+
title="Kopfzeile (nicht editierbar)"
|
|
168
|
+
>
|
|
169
|
+
<span>{{ h }}</span>
|
|
170
|
+
</th>
|
|
171
|
+
<th style="width: 1%; white-space: nowrap; border-bottom: 1px solid #d0d0d0; background: #f6f6f6; position: sticky; top: 0;" />
|
|
172
|
+
</tr>
|
|
173
|
+
</thead>
|
|
174
|
+
|
|
175
|
+
<tbody>
|
|
176
|
+
<template
|
|
177
|
+
v-for="(r, rowIndex) in rows"
|
|
178
|
+
:key="rowIndex"
|
|
179
|
+
>
|
|
180
|
+
<tr>
|
|
181
|
+
<td
|
|
182
|
+
v-for="(cell, colIndex) in r"
|
|
183
|
+
:key="colIndex"
|
|
184
|
+
style="padding: 6px; border-bottom: 1px solid #eee;"
|
|
185
|
+
>
|
|
186
|
+
<input
|
|
187
|
+
:value="cell"
|
|
188
|
+
type="text"
|
|
189
|
+
style="width: 100%; box-sizing: border-box; padding: 6px; border: 1px solid #cfcfcf;"
|
|
190
|
+
@input="onCellInput(rowIndex, colIndex, $event.target.value)"
|
|
191
|
+
>
|
|
192
|
+
</td>
|
|
193
|
+
<td style="padding: 6px; border-bottom: 1px solid #eee; white-space: nowrap;">
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
:disabled="rowIndex === 0"
|
|
197
|
+
@click="moveRowUp(rowIndex)"
|
|
198
|
+
>
|
|
199
|
+
↑
|
|
200
|
+
</button>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
:disabled="rowIndex === rows.length - 1"
|
|
204
|
+
@click="moveRowDown(rowIndex)"
|
|
205
|
+
>
|
|
206
|
+
↓
|
|
207
|
+
</button>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
@click="removeRow(rowIndex)"
|
|
211
|
+
>
|
|
212
|
+
Löschen
|
|
213
|
+
</button>
|
|
214
|
+
</td>
|
|
215
|
+
</tr>
|
|
216
|
+
|
|
217
|
+
<tr>
|
|
218
|
+
<td
|
|
219
|
+
:colspan="tableColspan"
|
|
220
|
+
style="padding: 6px; border-bottom: 1px solid #eee; background: #fafafa;"
|
|
221
|
+
>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
@click="insertRow(rowIndex + 1)"
|
|
225
|
+
>
|
|
226
|
+
+ Zeile hier einfügen
|
|
227
|
+
</button>
|
|
228
|
+
</td>
|
|
229
|
+
</tr>
|
|
230
|
+
</template>
|
|
231
|
+
</tbody>
|
|
232
|
+
</table>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type __VLS_ModelProps = {
|
|
2
|
+
'content'?: string;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
|
+
"update:content": (value: string) => any;
|
|
6
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
7
|
+
"onUpdate:content"?: ((value: string) => any) | undefined;
|
|
8
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useFetch, useRoute } from "#app";
|
|
3
|
+
import Csv from "./csv.vue";
|
|
4
|
+
import Markdown from "./markdown.vue";
|
|
3
5
|
const path = useRoute().params.path || "";
|
|
4
6
|
const pathParts = path.split(":");
|
|
5
|
-
const { data: content } = await useFetch(`/api/content/${path}`);
|
|
7
|
+
const { data: content } = await useFetch(`/api/admin/content/${path}`);
|
|
6
8
|
async function saveContent() {
|
|
7
9
|
await $fetch(`/api/admin/content/${path}`, {
|
|
8
10
|
method: "POST",
|
|
@@ -27,9 +29,13 @@ async function saveContent() {
|
|
|
27
29
|
</div>
|
|
28
30
|
|
|
29
31
|
<div v-if="content !== void 0">
|
|
30
|
-
<
|
|
31
|
-
v-
|
|
32
|
-
|
|
32
|
+
<Markdown
|
|
33
|
+
v-if="path.endsWith('.md')"
|
|
34
|
+
v-model:content="content"
|
|
35
|
+
/>
|
|
36
|
+
<Csv
|
|
37
|
+
v-else-if="path.endsWith('.csv')"
|
|
38
|
+
v-model:content="content"
|
|
33
39
|
/>
|
|
34
40
|
<button
|
|
35
41
|
style="margin-top: 10px;"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type __VLS_ModelProps = {
|
|
2
|
+
'content'?: string;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
|
+
"update:content": (value: string) => any;
|
|
6
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
7
|
+
"onUpdate:content"?: ((value: string) => any) | undefined;
|
|
8
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
const content = defineModel("content", { type: String, ...{ default: "" } });
|
|
5
|
+
const mode = ref("edit");
|
|
6
|
+
function escapeHtml(value) {
|
|
7
|
+
return (value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8
|
+
}
|
|
9
|
+
function isSafeHref(href) {
|
|
10
|
+
const h = (href ?? "").trim();
|
|
11
|
+
if (!h) return false;
|
|
12
|
+
if (h.startsWith("#") || h.startsWith("/") || h.startsWith("./") || h.startsWith("../")) return true;
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(h);
|
|
15
|
+
return ["http:", "https:", "mailto:", "tel:"].includes(url.protocol);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const renderedHtml = computed(() => {
|
|
21
|
+
const renderer = new marked.Renderer();
|
|
22
|
+
renderer.html = (token) => escapeHtml(token.text);
|
|
23
|
+
renderer.link = function(token) {
|
|
24
|
+
const safe = token.href && isSafeHref(token.href) ? token.href : "";
|
|
25
|
+
const text = this.parser.parseInline(token.tokens);
|
|
26
|
+
if (!safe) return text;
|
|
27
|
+
const t = token.title ? ` title="${escapeHtml(token.title)}"` : "";
|
|
28
|
+
const target = safe.startsWith("http") ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
29
|
+
return `<a href="${escapeHtml(safe)}"${t}${target}>${text}</a>`;
|
|
30
|
+
};
|
|
31
|
+
renderer.image = function(token) {
|
|
32
|
+
const safe = token.href && isSafeHref(token.href) ? token.href : "";
|
|
33
|
+
if (!safe) return escapeHtml(token.text);
|
|
34
|
+
const alt = escapeHtml(token.text);
|
|
35
|
+
const t = token.title ? ` title="${escapeHtml(token.title)}"` : "";
|
|
36
|
+
return `<img src="${escapeHtml(safe)}" alt="${alt}"${t} />`;
|
|
37
|
+
};
|
|
38
|
+
return marked.parse(content.value ?? "", {
|
|
39
|
+
renderer,
|
|
40
|
+
gfm: true,
|
|
41
|
+
breaks: true
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<div style="width: 100%;">
|
|
48
|
+
<div style="display: flex; gap: 8px; margin-bottom: 10px; align-items: center;">
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
@click="mode = mode === 'edit' ? 'preview' : 'edit'"
|
|
52
|
+
>
|
|
53
|
+
{{ mode === "edit" ? "Vorschau" : "Bearbeiten" }}
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<textarea
|
|
58
|
+
v-if="mode === 'edit'"
|
|
59
|
+
v-model="content"
|
|
60
|
+
style="width: 100%; height: 400px; resize: vertical; border: 1px solid #d0d0d0; padding: 10px; box-sizing: border-box;"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
v-else
|
|
65
|
+
style="width: 100%; min-height: 400px; border: 1px solid #d0d0d0; padding: 10px; box-sizing: border-box; overflow: auto;"
|
|
66
|
+
v-html="renderedHtml"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type __VLS_ModelProps = {
|
|
2
|
+
'content'?: string;
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
|
+
"update:content": (value: string) => any;
|
|
6
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
7
|
+
"onUpdate:content"?: ((value: string) => any) | undefined;
|
|
8
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import Admin from "../../../components/admin.vue";
|
|
3
3
|
import Header from "../../../components/header.vue";
|
|
4
|
-
import Editor from "../../../components/content/editor.vue";
|
|
4
|
+
import Editor from "../../../components/content/editor/index.vue";
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createError, defineEventHandler, getValidatedRouterParams } from "h3";
|
|
3
|
+
import { useRuntimeConfig, useStorage } from "nitropack/runtime";
|
|
4
|
+
const paramsSchema = z.object({
|
|
5
|
+
path: z.string().min(1)
|
|
6
|
+
});
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const { path } = await getValidatedRouterParams(event, (params) => paramsSchema.parse(params));
|
|
9
|
+
const { mktcms: { s3Prefix } } = useRuntimeConfig();
|
|
10
|
+
const fullPath = s3Prefix + ":" + path;
|
|
11
|
+
const storage = useStorage("content");
|
|
12
|
+
const file = await storage.getItem(fullPath);
|
|
13
|
+
if (!file) {
|
|
14
|
+
const fallbackStorage = useStorage("fallback");
|
|
15
|
+
const fallbackFile = await fallbackStorage.getItem(fullPath);
|
|
16
|
+
if (fallbackFile) {
|
|
17
|
+
return fallbackFile;
|
|
18
|
+
}
|
|
19
|
+
throw createError({
|
|
20
|
+
statusCode: 404,
|
|
21
|
+
statusMessage: "File not found"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return file;
|
|
25
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createError, defineEventHandler, getValidatedRouterParams } from "h3";
|
|
3
3
|
import { useRuntimeConfig, useStorage } from "nitropack/runtime";
|
|
4
|
+
import { parse } from "csv-parse/sync";
|
|
5
|
+
import { marked } from "marked";
|
|
6
|
+
function parsedFile(fullPath, file) {
|
|
7
|
+
if (fullPath.endsWith(".json") && typeof file === "string") {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(file);
|
|
10
|
+
} catch {
|
|
11
|
+
throw createError({
|
|
12
|
+
statusCode: 500,
|
|
13
|
+
statusMessage: "Invalid JSON file"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (fullPath.endsWith(".csv") && typeof file === "string") {
|
|
18
|
+
try {
|
|
19
|
+
return parse(file, {
|
|
20
|
+
columns: true,
|
|
21
|
+
skip_empty_lines: true,
|
|
22
|
+
delimiter: ";"
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
throw createError({
|
|
26
|
+
statusCode: 500,
|
|
27
|
+
statusMessage: "Invalid CSV file"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (fullPath.endsWith(".md") && typeof file === "string") {
|
|
32
|
+
try {
|
|
33
|
+
const html = marked.parse(file);
|
|
34
|
+
return html;
|
|
35
|
+
} catch {
|
|
36
|
+
throw createError({
|
|
37
|
+
statusCode: 500,
|
|
38
|
+
statusMessage: "Invalid Markdown file"
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return file;
|
|
43
|
+
}
|
|
4
44
|
const paramsSchema = z.object({
|
|
5
45
|
path: z.string().min(1)
|
|
6
46
|
});
|
|
@@ -14,12 +54,12 @@ export default defineEventHandler(async (event) => {
|
|
|
14
54
|
const fallbackStorage = useStorage("fallback");
|
|
15
55
|
const fallbackFile = await fallbackStorage.getItem(fullPath);
|
|
16
56
|
if (fallbackFile) {
|
|
17
|
-
return fallbackFile;
|
|
57
|
+
return parsedFile(fullPath, fallbackFile);
|
|
18
58
|
}
|
|
19
59
|
throw createError({
|
|
20
60
|
statusCode: 404,
|
|
21
61
|
statusMessage: "File not found"
|
|
22
62
|
});
|
|
23
63
|
}
|
|
24
|
-
return file;
|
|
64
|
+
return parsedFile(fullPath, file);
|
|
25
65
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mktcms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Simple CMS module for Nuxt",
|
|
5
5
|
"repository": "mktcode/mktcms",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@nuxt/kit": "^4.2.2",
|
|
39
39
|
"aws4fetch": "^1.0.20",
|
|
40
|
+
"csv-parse": "^6.1.0",
|
|
40
41
|
"defu": "^6.1.4",
|
|
42
|
+
"marked": "^17.0.1",
|
|
41
43
|
"nodemailer": "^7.0.12",
|
|
42
44
|
"zod": "^4.3.5"
|
|
43
45
|
},
|
|
File without changes
|
|
File without changes
|