quasar-ui-danx 0.4.95 → 0.5.0
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/danx.es.js +25284 -23176
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +133 -120
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Buttons/ActionButton.vue +11 -3
- package/src/components/Utility/Code/CodeViewer.vue +219 -0
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
- package/src/components/Utility/Code/LanguageBadge.vue +122 -0
- package/src/components/Utility/Code/MarkdownContent.vue +405 -0
- package/src/components/Utility/Code/index.ts +5 -0
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
- package/src/components/Utility/Files/CarouselHeader.vue +24 -0
- package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
- package/src/components/Utility/Files/FilePreview.vue +118 -166
- package/src/components/Utility/Files/index.ts +1 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/useCodeFormat.ts +199 -0
- package/src/composables/useCodeViewerCollapse.ts +125 -0
- package/src/composables/useCodeViewerEditor.ts +420 -0
- package/src/composables/useFilePreview.ts +119 -0
- package/src/composables/useTranscodeLoader.ts +68 -0
- package/src/helpers/formats/highlightSyntax.ts +327 -0
- package/src/helpers/formats/index.ts +3 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- package/src/helpers/formats/markdown/index.ts +85 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +4 -0
- package/src/styles/themes/danx/code.scss +158 -0
- package/src/styles/themes/danx/index.scss +2 -0
- package/src/styles/themes/danx/markdown.scss +241 -0
- package/src/styles/themes/danx/scrollbar.scss +125 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<InfoDialog
|
|
3
|
+
title="File Metadata"
|
|
4
|
+
:hide-done="true"
|
|
5
|
+
done-text="Close"
|
|
6
|
+
content-class="w-[80vw] h-[80vh] max-w-none"
|
|
7
|
+
@close="$emit('close')"
|
|
8
|
+
>
|
|
9
|
+
<div class="file-metadata-container h-full flex flex-col">
|
|
10
|
+
<!-- File info header -->
|
|
11
|
+
<div class="bg-sky-50 rounded-lg p-4 mb-4 flex-shrink-0">
|
|
12
|
+
<h4 class="text-lg font-semibold text-gray-900 mb-2">
|
|
13
|
+
{{ filename || 'Unnamed File' }}
|
|
14
|
+
</h4>
|
|
15
|
+
<div v-if="mimeType" class="text-sm text-gray-600">
|
|
16
|
+
Type: {{ mimeType }}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Metadata section -->
|
|
21
|
+
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden flex-1 flex flex-col min-h-0">
|
|
22
|
+
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200 flex-shrink-0 flex items-center justify-between">
|
|
23
|
+
<h4 class="text-base font-medium text-gray-900">
|
|
24
|
+
Metadata
|
|
25
|
+
</h4>
|
|
26
|
+
<QBtn
|
|
27
|
+
v-if="showDockButton"
|
|
28
|
+
flat
|
|
29
|
+
dense
|
|
30
|
+
round
|
|
31
|
+
size="sm"
|
|
32
|
+
class="text-gray-500 hover:text-gray-700 hover:bg-gray-200"
|
|
33
|
+
@click="$emit('dock')"
|
|
34
|
+
>
|
|
35
|
+
<DockSideIcon class="w-4 h-4" />
|
|
36
|
+
<QTooltip>Dock to side</QTooltip>
|
|
37
|
+
</QBtn>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="p-4 flex-1 min-h-0 flex flex-col">
|
|
40
|
+
<CodeViewer
|
|
41
|
+
:model-value="metadata"
|
|
42
|
+
:readonly="true"
|
|
43
|
+
format="yaml"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</InfoDialog>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup lang="ts">
|
|
52
|
+
import { FaSolidTableColumns as DockSideIcon } from "danx-icon";
|
|
53
|
+
import { CodeViewer } from "../Code";
|
|
54
|
+
import { InfoDialog } from "../Dialogs";
|
|
55
|
+
|
|
56
|
+
withDefaults(defineProps<{
|
|
57
|
+
filename: string;
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
metadata: Record<string, unknown>;
|
|
60
|
+
showDockButton?: boolean;
|
|
61
|
+
}>(), {
|
|
62
|
+
showDockButton: false
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
defineEmits<{
|
|
66
|
+
close: [];
|
|
67
|
+
dock: [];
|
|
68
|
+
}>();
|
|
69
|
+
</script>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
>
|
|
75
75
|
<QLinearProgress
|
|
76
76
|
:key="'progress-' + isUploading ? 'uploading' : 'transcoding'"
|
|
77
|
-
:value="isUploading ? file
|
|
77
|
+
:value="isUploading ? (file?.progress || 0) : ((transcodingStatus?.progress || 0) / 100)"
|
|
78
78
|
size="36px"
|
|
79
79
|
:color="isUploading ? 'green-800' : 'blue-800'"
|
|
80
80
|
:animation-speed="transcodingStatus?.estimate_ms || 3000"
|
|
@@ -107,6 +107,22 @@
|
|
|
107
107
|
</template>
|
|
108
108
|
|
|
109
109
|
<div class="absolute top-1 right-1 flex items-center flex-nowrap justify-between space-x-1 transition-all opacity-0 group-hover:opacity-100">
|
|
110
|
+
<QBtn
|
|
111
|
+
v-if="hasMetadata"
|
|
112
|
+
:size="btnSize"
|
|
113
|
+
class="dx-file-preview-metadata bg-purple-700 text-white opacity-70 hover:opacity-100 py-1 px-2 relative"
|
|
114
|
+
@click.stop="showMetadataDialog = true"
|
|
115
|
+
>
|
|
116
|
+
<div class="flex items-center flex-nowrap gap-1">
|
|
117
|
+
<MetaIcon class="w-4 h-4" />
|
|
118
|
+
<QBadge
|
|
119
|
+
class="bg-purple-900 text-purple-200"
|
|
120
|
+
:label="metadataKeyCount"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<QTooltip>View Metadata</QTooltip>
|
|
124
|
+
</QBtn>
|
|
125
|
+
|
|
110
126
|
<QBtn
|
|
111
127
|
v-if="hasTranscodes"
|
|
112
128
|
:size="btnSize"
|
|
@@ -152,6 +168,14 @@
|
|
|
152
168
|
</QBtn>
|
|
153
169
|
</div>
|
|
154
170
|
|
|
171
|
+
<FileMetadataDialog
|
|
172
|
+
v-if="showMetadataDialog"
|
|
173
|
+
:filename="filename"
|
|
174
|
+
:mime-type="mimeType"
|
|
175
|
+
:metadata="filteredMetadata"
|
|
176
|
+
@close="showMetadataDialog = false"
|
|
177
|
+
/>
|
|
178
|
+
|
|
155
179
|
<FullScreenCarouselDialog
|
|
156
180
|
v-if="showPreview && !disabled && previewableFiles"
|
|
157
181
|
:files="previewableFiles"
|
|
@@ -163,204 +187,132 @@
|
|
|
163
187
|
|
|
164
188
|
<script setup lang="ts">
|
|
165
189
|
import { DocumentTextIcon as TextFileIcon, DownloadIcon, FilmIcon, PlayIcon } from "@heroicons/vue/outline";
|
|
166
|
-
import {
|
|
167
|
-
import {
|
|
190
|
+
import { FaSolidBarcode as MetaIcon } from "danx-icon";
|
|
191
|
+
import { computed, ref, toRef } from "vue";
|
|
192
|
+
import { useFilePreview } from "../../../composables/useFilePreview";
|
|
193
|
+
import { useTranscodeLoader } from "../../../composables/useTranscodeLoader";
|
|
168
194
|
import { download, uniqueBy } from "../../../helpers";
|
|
169
|
-
import
|
|
170
|
-
import { getMimeType, getOptimizedUrl, isExternalLinkFile } from "../../../helpers/filePreviewHelpers";
|
|
195
|
+
import { isExternalLinkFile } from "../../../helpers/filePreviewHelpers";
|
|
171
196
|
import { GoogleDocsIcon, ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
|
|
172
197
|
import { UploadedFile } from "../../../types";
|
|
173
198
|
import { FullScreenCarouselDialog } from "../Dialogs";
|
|
174
|
-
|
|
175
|
-
export interface FileTranscode {
|
|
176
|
-
status: "Complete" | "Pending" | "In Progress";
|
|
177
|
-
progress: number;
|
|
178
|
-
estimate_ms: number;
|
|
179
|
-
started_at: string;
|
|
180
|
-
completed_at: string;
|
|
181
|
-
message?: string;
|
|
182
|
-
}
|
|
199
|
+
import FileMetadataDialog from "./FileMetadataDialog.vue";
|
|
183
200
|
|
|
184
201
|
export interface FilePreviewProps {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
src?: string;
|
|
203
|
+
file?: UploadedFile;
|
|
204
|
+
relatedFiles?: UploadedFile[];
|
|
205
|
+
missingIcon?: any;
|
|
206
|
+
showFilename?: boolean;
|
|
207
|
+
downloadButtonClass?: string;
|
|
208
|
+
imageFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
|
|
209
|
+
downloadable?: boolean;
|
|
210
|
+
removable?: boolean;
|
|
211
|
+
disabled?: boolean;
|
|
212
|
+
square?: boolean;
|
|
213
|
+
btnSize?: "xs" | "sm" | "md" | "lg";
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
const emit = defineEmits(["remove"]);
|
|
200
217
|
|
|
201
218
|
const props = withDefaults(defineProps<FilePreviewProps>(), {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
src: "",
|
|
220
|
+
file: null,
|
|
221
|
+
relatedFiles: null,
|
|
222
|
+
missingIcon: ImageIcon,
|
|
223
|
+
downloadButtonClass: "bg-blue-600 text-white",
|
|
224
|
+
imageFit: "cover",
|
|
225
|
+
downloadable: false,
|
|
226
|
+
removable: false,
|
|
227
|
+
disabled: false,
|
|
228
|
+
square: false,
|
|
229
|
+
btnSize: "sm"
|
|
213
230
|
});
|
|
214
231
|
|
|
232
|
+
// Use composables for file preview logic
|
|
233
|
+
const fileRef = toRef(props, "file");
|
|
234
|
+
const srcRef = toRef(props, "src");
|
|
235
|
+
|
|
236
|
+
const {
|
|
237
|
+
computedImage,
|
|
238
|
+
filename,
|
|
239
|
+
mimeType,
|
|
240
|
+
isVideo,
|
|
241
|
+
isPdf,
|
|
242
|
+
isExternalLink,
|
|
243
|
+
previewUrl,
|
|
244
|
+
thumbUrl,
|
|
245
|
+
isPreviewable,
|
|
246
|
+
hasMetadata,
|
|
247
|
+
metadataKeyCount,
|
|
248
|
+
filteredMetadata,
|
|
249
|
+
hasTranscodes,
|
|
250
|
+
transcodingStatus
|
|
251
|
+
} = useFilePreview({ file: fileRef, src: srcRef });
|
|
215
252
|
|
|
253
|
+
// Load transcodes automatically
|
|
254
|
+
useTranscodeLoader({ file: fileRef });
|
|
255
|
+
|
|
256
|
+
// Local state
|
|
216
257
|
const showPreview = ref(false);
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
if (props.file) {
|
|
220
|
-
return props.file;
|
|
221
|
-
} else if (props.src) {
|
|
222
|
-
return {
|
|
223
|
-
id: props.src,
|
|
224
|
-
url: props.src,
|
|
225
|
-
type: "image/" + props.src.split(".").pop()?.toLowerCase(),
|
|
226
|
-
name: "",
|
|
227
|
-
size: 0,
|
|
228
|
-
__type: "BrowserFile"
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
});
|
|
258
|
+
const showMetadataDialog = ref(false);
|
|
259
|
+
const isConfirmingRemove = ref(false);
|
|
233
260
|
|
|
234
|
-
|
|
261
|
+
// Computed
|
|
262
|
+
const isUploading = computed(() => props.file && props.file?.progress !== undefined);
|
|
235
263
|
const statusMessage = computed(() => isUploading.value ? "Uploading..." : transcodingStatus.value?.message);
|
|
236
|
-
|
|
237
|
-
const previewableFiles
|
|
238
|
-
|
|
264
|
+
|
|
265
|
+
const previewableFiles = computed(() => {
|
|
266
|
+
return props.relatedFiles?.length > 0
|
|
267
|
+
? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl)
|
|
268
|
+
: [computedImage.value];
|
|
239
269
|
});
|
|
240
270
|
|
|
271
|
+
// Helpers
|
|
241
272
|
function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
return a.id === b.id ||
|
|
274
|
+
[b.url, b.optimized?.url, b.thumb?.url].includes(a.url) ||
|
|
275
|
+
[a.url, a.optimized?.url, a.thumb?.url].includes(b.url);
|
|
245
276
|
}
|
|
246
277
|
|
|
247
|
-
const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
|
|
248
|
-
const mimeType = computed(() => computedImage.value ? getMimeType(computedImage.value) : "");
|
|
249
|
-
const isImage = computed(() => computedImage.value ? fileHelpers.isImage(computedImage.value) : false);
|
|
250
|
-
const isVideo = computed(() => computedImage.value ? fileHelpers.isVideo(computedImage.value) : false);
|
|
251
|
-
const isPdf = computed(() => computedImage.value ? fileHelpers.isPdf(computedImage.value) : false);
|
|
252
|
-
const isExternalLink = computed(() => computedImage.value ? isExternalLinkFile(computedImage.value) : false);
|
|
253
|
-
const previewUrl = computed(() => computedImage.value ? getOptimizedUrl(computedImage.value) : "");
|
|
254
|
-
const thumbUrl = computed(() => computedImage.value?.thumb?.url || "");
|
|
255
|
-
const isPreviewable = computed(() => {
|
|
256
|
-
return !!thumbUrl.value || isVideo.value || isImage.value;
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Resolve the active transcoding operation if there is one, otherwise return null
|
|
261
|
-
*/
|
|
262
|
-
const transcodingStatus = computed(() => {
|
|
263
|
-
let status = null;
|
|
264
|
-
const metaTranscodes: FileTranscode[] = props.file?.meta?.transcodes || [];
|
|
265
|
-
|
|
266
|
-
for (let transcodeName of Object.keys(metaTranscodes)) {
|
|
267
|
-
const transcode = metaTranscodes[transcodeName];
|
|
268
|
-
if (!["Complete", "Timeout"].includes(transcode?.status)) {
|
|
269
|
-
return { ...transcode, message: `${transcodeName} ${transcode.status}` };
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return status;
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const isConfirmingRemove = ref(false);
|
|
277
278
|
function onRemove() {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
279
|
+
if (!isConfirmingRemove.value) {
|
|
280
|
+
isConfirmingRemove.value = true;
|
|
281
|
+
setTimeout(() => {
|
|
282
|
+
isConfirmingRemove.value = false;
|
|
283
|
+
}, 2000);
|
|
284
|
+
} else {
|
|
285
|
+
emit("remove");
|
|
286
|
+
}
|
|
286
287
|
}
|
|
287
288
|
|
|
288
289
|
function onShowPreview() {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
showPreview.value = true;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Check if transcodes need to be loaded for the current file
|
|
299
|
-
*/
|
|
300
|
-
function shouldLoadTranscodes(): boolean {
|
|
301
|
-
if (!props.file?.id) return false;
|
|
302
|
-
if (isLoadingTranscodes.value) return false;
|
|
303
|
-
if (!danxOptions.value.fileUpload?.refreshFile) return false;
|
|
304
|
-
|
|
305
|
-
// Only load if transcodes is explicitly null, undefined, or an empty array
|
|
306
|
-
const transcodes = props.file.transcodes;
|
|
307
|
-
return transcodes === null || transcodes === undefined || (Array.isArray(transcodes) && transcodes.length === 0);
|
|
290
|
+
if (computedImage.value && isExternalLinkFile(computedImage.value)) {
|
|
291
|
+
window.open(computedImage.value.url, "_blank");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
showPreview.value = true;
|
|
308
295
|
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Load transcodes for the current file
|
|
312
|
-
*/
|
|
313
|
-
async function loadTranscodes() {
|
|
314
|
-
if (!shouldLoadTranscodes()) return;
|
|
315
|
-
|
|
316
|
-
isLoadingTranscodes.value = true;
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
const refreshFile = danxOptions.value.fileUpload.refreshFile;
|
|
320
|
-
if (refreshFile && props.file?.id) {
|
|
321
|
-
const refreshedFile = await refreshFile(props.file.id);
|
|
322
|
-
|
|
323
|
-
// Update the file object with the loaded transcodes
|
|
324
|
-
if (refreshedFile.transcodes && props.file) {
|
|
325
|
-
props.file.transcodes = refreshedFile.transcodes;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
} catch (error) {
|
|
329
|
-
console.error("Failed to load transcodes:", error);
|
|
330
|
-
} finally {
|
|
331
|
-
isLoadingTranscodes.value = false;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Load transcodes when component mounts
|
|
336
|
-
onMounted(() => {
|
|
337
|
-
loadTranscodes();
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// Watch for file changes and reload transcodes if needed
|
|
341
|
-
watch(() => props.file?.id, () => {
|
|
342
|
-
loadTranscodes();
|
|
343
|
-
});
|
|
344
296
|
</script>
|
|
345
297
|
|
|
346
298
|
<style module="cls" lang="scss">
|
|
347
299
|
.action-button {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
300
|
+
position: absolute;
|
|
301
|
+
bottom: 1.5em;
|
|
302
|
+
right: 1em;
|
|
303
|
+
z-index: 1;
|
|
352
304
|
}
|
|
353
305
|
|
|
354
306
|
.play-button {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
307
|
+
position: absolute;
|
|
308
|
+
top: 0;
|
|
309
|
+
left: 0;
|
|
310
|
+
display: flex;
|
|
311
|
+
justify-content: center;
|
|
312
|
+
align-items: center;
|
|
313
|
+
width: 100%;
|
|
314
|
+
height: 100%;
|
|
315
|
+
pointer-events: none;
|
|
316
|
+
@apply text-blue-200;
|
|
365
317
|
}
|
|
366
318
|
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as CarouselHeader } from "./CarouselHeader.vue";
|
|
2
|
+
export { default as FileMetadataDialog } from "./FileMetadataDialog.vue";
|
|
2
3
|
export { default as FilePreview } from "./FilePreview.vue";
|
|
3
4
|
export { default as FileRenderer } from "./FileRenderer.vue";
|
|
4
5
|
export { default as SvgImg } from "./SvgImg.vue";
|
package/src/composables/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
export * from "./useCodeFormat";
|
|
2
|
+
export * from "./useCodeViewerCollapse";
|
|
3
|
+
export * from "./useCodeViewerEditor";
|
|
1
4
|
export * from "./useFileNavigation";
|
|
5
|
+
export * from "./useFilePreview";
|
|
2
6
|
export * from "./useKeyboardNavigation";
|
|
3
7
|
export * from "./useThumbnailScroll";
|
|
8
|
+
export * from "./useTranscodeLoader";
|
|
4
9
|
export * from "./useVirtualCarousel";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { computed, ref, Ref } from "vue";
|
|
2
|
+
import { parse as parseYAML, stringify as yamlStringify } from "yaml";
|
|
3
|
+
import { fJSON, parseMarkdownJSON, parseMarkdownYAML } from "../helpers/formats/parsers";
|
|
4
|
+
|
|
5
|
+
export type CodeFormat = "json" | "yaml" | "text" | "markdown";
|
|
6
|
+
|
|
7
|
+
export interface UseCodeFormatOptions {
|
|
8
|
+
initialFormat?: CodeFormat;
|
|
9
|
+
initialValue?: object | string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ValidationError {
|
|
13
|
+
message: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
column?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseCodeFormatReturn {
|
|
19
|
+
// State
|
|
20
|
+
format: Ref<CodeFormat>;
|
|
21
|
+
rawContent: Ref<string>;
|
|
22
|
+
|
|
23
|
+
// Computed
|
|
24
|
+
parsedValue: Ref<object | null>;
|
|
25
|
+
formattedContent: Ref<string>;
|
|
26
|
+
isValid: Ref<boolean>;
|
|
27
|
+
|
|
28
|
+
// Methods
|
|
29
|
+
setFormat: (format: CodeFormat) => void;
|
|
30
|
+
setContent: (content: string) => void;
|
|
31
|
+
setValue: (value: object | string | null) => void;
|
|
32
|
+
parse: (content: string) => object | null;
|
|
33
|
+
formatValue: (value: object | null, targetFormat?: CodeFormat) => string;
|
|
34
|
+
validate: (content: string, targetFormat?: CodeFormat) => boolean;
|
|
35
|
+
validateWithError: (content: string, targetFormat?: CodeFormat) => ValidationError | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useCodeFormat(options: UseCodeFormatOptions = {}): UseCodeFormatReturn {
|
|
39
|
+
const format = ref<CodeFormat>(options.initialFormat ?? "yaml");
|
|
40
|
+
const rawContent = ref("");
|
|
41
|
+
|
|
42
|
+
// Parse any string (JSON or YAML) to object
|
|
43
|
+
function parseContent(content: string): object | null {
|
|
44
|
+
if (!content) return null;
|
|
45
|
+
|
|
46
|
+
// Try JSON first
|
|
47
|
+
const jsonResult = parseMarkdownJSON(content);
|
|
48
|
+
if (jsonResult !== false && jsonResult !== null) return jsonResult;
|
|
49
|
+
|
|
50
|
+
// Try YAML
|
|
51
|
+
const yamlResult = parseMarkdownYAML(content);
|
|
52
|
+
if (yamlResult !== false && yamlResult !== null) return yamlResult;
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Format object to string in specified format
|
|
58
|
+
function formatValueToString(value: object | string | null, targetFormat: CodeFormat = format.value): string {
|
|
59
|
+
if (!value) return "";
|
|
60
|
+
|
|
61
|
+
// Text and markdown formats - just return as-is
|
|
62
|
+
if (targetFormat === "text" || targetFormat === "markdown") {
|
|
63
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const obj = typeof value === "string" ? parseContent(value) : value;
|
|
68
|
+
if (!obj) return typeof value === "string" ? value : "";
|
|
69
|
+
|
|
70
|
+
if (targetFormat === "json") {
|
|
71
|
+
const formatted = fJSON(obj);
|
|
72
|
+
return typeof formatted === "string" ? formatted : JSON.stringify(obj, null, 2);
|
|
73
|
+
} else {
|
|
74
|
+
return yamlStringify(obj as object);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate string content for a format
|
|
82
|
+
function validateContent(content: string, targetFormat: CodeFormat = format.value): boolean {
|
|
83
|
+
if (!content) return true;
|
|
84
|
+
|
|
85
|
+
// Text and markdown formats are always valid
|
|
86
|
+
if (targetFormat === "text" || targetFormat === "markdown") return true;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (targetFormat === "json") {
|
|
90
|
+
JSON.parse(content);
|
|
91
|
+
} else {
|
|
92
|
+
parseYAML(content);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate and return error details if invalid
|
|
101
|
+
function validateContentWithError(content: string, targetFormat: CodeFormat = format.value): ValidationError | null {
|
|
102
|
+
if (!content) return null;
|
|
103
|
+
|
|
104
|
+
// Text and markdown formats are always valid
|
|
105
|
+
if (targetFormat === "text" || targetFormat === "markdown") return null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
if (targetFormat === "json") {
|
|
109
|
+
JSON.parse(content);
|
|
110
|
+
} else {
|
|
111
|
+
parseYAML(content);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
} catch (e: unknown) {
|
|
115
|
+
const error = e as Error & { linePos?: { line: number; col: number }[] };
|
|
116
|
+
let line: number | undefined;
|
|
117
|
+
let column: number | undefined;
|
|
118
|
+
|
|
119
|
+
// YAML errors from 'yaml' library have linePos
|
|
120
|
+
if (error.linePos && error.linePos[0]) {
|
|
121
|
+
line = error.linePos[0].line;
|
|
122
|
+
column = error.linePos[0].col;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// JSON parse errors - try to extract position from message
|
|
126
|
+
if (targetFormat === "json" && error.message) {
|
|
127
|
+
const posMatch = error.message.match(/position\s+(\d+)/i);
|
|
128
|
+
if (posMatch) {
|
|
129
|
+
const pos = parseInt(posMatch[1], 10);
|
|
130
|
+
// Convert position to line number
|
|
131
|
+
const lines = content.substring(0, pos).split("\n");
|
|
132
|
+
line = lines.length;
|
|
133
|
+
column = lines[lines.length - 1].length + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
message: error.message || "Invalid syntax",
|
|
139
|
+
line,
|
|
140
|
+
column
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Initialize with value if provided
|
|
146
|
+
if (options.initialValue) {
|
|
147
|
+
rawContent.value = formatValueToString(options.initialValue, format.value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Computed: parsed object from raw content
|
|
151
|
+
const parsedValue = computed(() => parseContent(rawContent.value));
|
|
152
|
+
|
|
153
|
+
// Computed: formatted string
|
|
154
|
+
// For text and markdown formats, return rawContent directly without parsing
|
|
155
|
+
const formattedContent = computed(() => {
|
|
156
|
+
if (format.value === "text" || format.value === "markdown") {
|
|
157
|
+
return rawContent.value;
|
|
158
|
+
}
|
|
159
|
+
return formatValueToString(parsedValue.value, format.value);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Computed: is current content valid
|
|
163
|
+
const isValid = computed(() => validateContent(rawContent.value, format.value));
|
|
164
|
+
|
|
165
|
+
// Methods
|
|
166
|
+
function setFormat(newFormat: CodeFormat) {
|
|
167
|
+
if (format.value === newFormat) return;
|
|
168
|
+
|
|
169
|
+
// Convert content to new format
|
|
170
|
+
const obj = parsedValue.value;
|
|
171
|
+
format.value = newFormat;
|
|
172
|
+
if (obj) {
|
|
173
|
+
rawContent.value = formatValueToString(obj, newFormat);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setContent(content: string) {
|
|
178
|
+
rawContent.value = content;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function setValue(value: object | string | null) {
|
|
182
|
+
rawContent.value = formatValueToString(value, format.value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
format,
|
|
187
|
+
rawContent,
|
|
188
|
+
parsedValue,
|
|
189
|
+
formattedContent,
|
|
190
|
+
isValid,
|
|
191
|
+
setFormat,
|
|
192
|
+
setContent,
|
|
193
|
+
setValue,
|
|
194
|
+
parse: parseContent,
|
|
195
|
+
formatValue: formatValueToString,
|
|
196
|
+
validate: validateContent,
|
|
197
|
+
validateWithError: validateContentWithError
|
|
198
|
+
};
|
|
199
|
+
}
|