quasar-ui-danx 0.4.94 → 0.4.99
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 +24432 -22819
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +130 -119
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- 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 +251 -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 +124 -162
- 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/filePreviewHelpers.ts +31 -0
- package/src/helpers/formats/highlightSyntax.ts +327 -0
- package/src/helpers/formats/index.ts +3 -1
- package/src/helpers/formats/renderMarkdown.ts +338 -0
- package/src/helpers/objectStore.ts +10 -2
- package/src/styles/danx.scss +3 -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 +145 -0
- package/src/styles/themes/danx/scrollbar.scss +125 -0
- package/src/svg/GoogleDocsIcon.vue +88 -0
- package/src/svg/index.ts +1 -0
|
@@ -37,8 +37,12 @@
|
|
|
37
37
|
v-else
|
|
38
38
|
class="flex items-center justify-center h-full"
|
|
39
39
|
>
|
|
40
|
+
<GoogleDocsIcon
|
|
41
|
+
v-if="isExternalLink"
|
|
42
|
+
class="h-3/4"
|
|
43
|
+
/>
|
|
40
44
|
<PdfIcon
|
|
41
|
-
v-if="isPdf"
|
|
45
|
+
v-else-if="isPdf"
|
|
42
46
|
class="w-3/4"
|
|
43
47
|
/>
|
|
44
48
|
<TextFileIcon
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
>
|
|
71
75
|
<QLinearProgress
|
|
72
76
|
:key="'progress-' + isUploading ? 'uploading' : 'transcoding'"
|
|
73
|
-
:value="isUploading ? file
|
|
77
|
+
:value="isUploading ? (file?.progress || 0) : ((transcodingStatus?.progress || 0) / 100)"
|
|
74
78
|
size="36px"
|
|
75
79
|
:color="isUploading ? 'green-800' : 'blue-800'"
|
|
76
80
|
:animation-speed="transcodingStatus?.estimate_ms || 3000"
|
|
@@ -103,6 +107,22 @@
|
|
|
103
107
|
</template>
|
|
104
108
|
|
|
105
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
|
+
|
|
106
126
|
<QBtn
|
|
107
127
|
v-if="hasTranscodes"
|
|
108
128
|
:size="btnSize"
|
|
@@ -148,6 +168,14 @@
|
|
|
148
168
|
</QBtn>
|
|
149
169
|
</div>
|
|
150
170
|
|
|
171
|
+
<FileMetadataDialog
|
|
172
|
+
v-if="showMetadataDialog"
|
|
173
|
+
:filename="filename"
|
|
174
|
+
:mime-type="mimeType"
|
|
175
|
+
:metadata="filteredMetadata"
|
|
176
|
+
@close="showMetadataDialog = false"
|
|
177
|
+
/>
|
|
178
|
+
|
|
151
179
|
<FullScreenCarouselDialog
|
|
152
180
|
v-if="showPreview && !disabled && previewableFiles"
|
|
153
181
|
:files="previewableFiles"
|
|
@@ -159,198 +187,132 @@
|
|
|
159
187
|
|
|
160
188
|
<script setup lang="ts">
|
|
161
189
|
import { DocumentTextIcon as TextFileIcon, DownloadIcon, FilmIcon, PlayIcon } from "@heroicons/vue/outline";
|
|
162
|
-
import {
|
|
163
|
-
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";
|
|
164
194
|
import { download, uniqueBy } from "../../../helpers";
|
|
165
|
-
import
|
|
166
|
-
import {
|
|
167
|
-
import { ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
|
|
195
|
+
import { isExternalLinkFile } from "../../../helpers/filePreviewHelpers";
|
|
196
|
+
import { GoogleDocsIcon, ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
|
|
168
197
|
import { UploadedFile } from "../../../types";
|
|
169
198
|
import { FullScreenCarouselDialog } from "../Dialogs";
|
|
170
|
-
|
|
171
|
-
export interface FileTranscode {
|
|
172
|
-
status: "Complete" | "Pending" | "In Progress";
|
|
173
|
-
progress: number;
|
|
174
|
-
estimate_ms: number;
|
|
175
|
-
started_at: string;
|
|
176
|
-
completed_at: string;
|
|
177
|
-
message?: string;
|
|
178
|
-
}
|
|
199
|
+
import FileMetadataDialog from "./FileMetadataDialog.vue";
|
|
179
200
|
|
|
180
201
|
export interface FilePreviewProps {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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";
|
|
193
214
|
}
|
|
194
215
|
|
|
195
216
|
const emit = defineEmits(["remove"]);
|
|
196
217
|
|
|
197
218
|
const props = withDefaults(defineProps<FilePreviewProps>(), {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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"
|
|
209
230
|
});
|
|
210
231
|
|
|
232
|
+
// Use composables for file preview logic
|
|
233
|
+
const fileRef = toRef(props, "file");
|
|
234
|
+
const srcRef = toRef(props, "src");
|
|
211
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 });
|
|
252
|
+
|
|
253
|
+
// Load transcodes automatically
|
|
254
|
+
useTranscodeLoader({ file: fileRef });
|
|
255
|
+
|
|
256
|
+
// Local state
|
|
212
257
|
const showPreview = ref(false);
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
if (props.file) {
|
|
216
|
-
return props.file;
|
|
217
|
-
} else if (props.src) {
|
|
218
|
-
return {
|
|
219
|
-
id: props.src,
|
|
220
|
-
url: props.src,
|
|
221
|
-
type: "image/" + props.src.split(".").pop()?.toLowerCase(),
|
|
222
|
-
name: "",
|
|
223
|
-
size: 0,
|
|
224
|
-
__type: "BrowserFile"
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
});
|
|
258
|
+
const showMetadataDialog = ref(false);
|
|
259
|
+
const isConfirmingRemove = ref(false);
|
|
229
260
|
|
|
230
|
-
|
|
261
|
+
// Computed
|
|
262
|
+
const isUploading = computed(() => props.file && props.file?.progress !== undefined);
|
|
231
263
|
const statusMessage = computed(() => isUploading.value ? "Uploading..." : transcodingStatus.value?.message);
|
|
232
|
-
|
|
233
|
-
const previewableFiles
|
|
234
|
-
|
|
264
|
+
|
|
265
|
+
const previewableFiles = computed(() => {
|
|
266
|
+
return props.relatedFiles?.length > 0
|
|
267
|
+
? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl)
|
|
268
|
+
: [computedImage.value];
|
|
235
269
|
});
|
|
236
270
|
|
|
271
|
+
// Helpers
|
|
237
272
|
function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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);
|
|
241
276
|
}
|
|
242
277
|
|
|
243
|
-
const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
|
|
244
|
-
const mimeType = computed(() => computedImage.value ? getMimeType(computedImage.value) : "");
|
|
245
|
-
const isImage = computed(() => computedImage.value ? fileHelpers.isImage(computedImage.value) : false);
|
|
246
|
-
const isVideo = computed(() => computedImage.value ? fileHelpers.isVideo(computedImage.value) : false);
|
|
247
|
-
const isPdf = computed(() => computedImage.value ? fileHelpers.isPdf(computedImage.value) : false);
|
|
248
|
-
const previewUrl = computed(() => computedImage.value ? getOptimizedUrl(computedImage.value) : "");
|
|
249
|
-
const thumbUrl = computed(() => computedImage.value?.thumb?.url || "");
|
|
250
|
-
const isPreviewable = computed(() => {
|
|
251
|
-
return !!thumbUrl.value || isVideo.value || isImage.value;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Resolve the active transcoding operation if there is one, otherwise return null
|
|
256
|
-
*/
|
|
257
|
-
const transcodingStatus = computed(() => {
|
|
258
|
-
let status = null;
|
|
259
|
-
const metaTranscodes: FileTranscode[] = props.file?.meta?.transcodes || [];
|
|
260
|
-
|
|
261
|
-
for (let transcodeName of Object.keys(metaTranscodes)) {
|
|
262
|
-
const transcode = metaTranscodes[transcodeName];
|
|
263
|
-
if (!["Complete", "Timeout"].includes(transcode?.status)) {
|
|
264
|
-
return { ...transcode, message: `${transcodeName} ${transcode.status}` };
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return status;
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const isConfirmingRemove = ref(false);
|
|
272
278
|
function onRemove() {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
if (!isConfirmingRemove.value) {
|
|
280
|
+
isConfirmingRemove.value = true;
|
|
281
|
+
setTimeout(() => {
|
|
282
|
+
isConfirmingRemove.value = false;
|
|
283
|
+
}, 2000);
|
|
284
|
+
} else {
|
|
285
|
+
emit("remove");
|
|
286
|
+
}
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
function onShowPreview() {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
*/
|
|
290
|
-
function shouldLoadTranscodes(): boolean {
|
|
291
|
-
if (!props.file?.id) return false;
|
|
292
|
-
if (isLoadingTranscodes.value) return false;
|
|
293
|
-
if (!danxOptions.value.fileUpload?.refreshFile) return false;
|
|
294
|
-
|
|
295
|
-
// Only load if transcodes is explicitly null, undefined, or an empty array
|
|
296
|
-
const transcodes = props.file.transcodes;
|
|
297
|
-
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;
|
|
298
295
|
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Load transcodes for the current file
|
|
302
|
-
*/
|
|
303
|
-
async function loadTranscodes() {
|
|
304
|
-
if (!shouldLoadTranscodes()) return;
|
|
305
|
-
|
|
306
|
-
isLoadingTranscodes.value = true;
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
const refreshFile = danxOptions.value.fileUpload.refreshFile;
|
|
310
|
-
if (refreshFile && props.file?.id) {
|
|
311
|
-
const refreshedFile = await refreshFile(props.file.id);
|
|
312
|
-
|
|
313
|
-
// Update the file object with the loaded transcodes
|
|
314
|
-
if (refreshedFile.transcodes && props.file) {
|
|
315
|
-
props.file.transcodes = refreshedFile.transcodes;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
} catch (error) {
|
|
319
|
-
console.error("Failed to load transcodes:", error);
|
|
320
|
-
} finally {
|
|
321
|
-
isLoadingTranscodes.value = false;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Load transcodes when component mounts
|
|
326
|
-
onMounted(() => {
|
|
327
|
-
loadTranscodes();
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// Watch for file changes and reload transcodes if needed
|
|
331
|
-
watch(() => props.file?.id, () => {
|
|
332
|
-
loadTranscodes();
|
|
333
|
-
});
|
|
334
296
|
</script>
|
|
335
297
|
|
|
336
298
|
<style module="cls" lang="scss">
|
|
337
299
|
.action-button {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
300
|
+
position: absolute;
|
|
301
|
+
bottom: 1.5em;
|
|
302
|
+
right: 1em;
|
|
303
|
+
z-index: 1;
|
|
342
304
|
}
|
|
343
305
|
|
|
344
306
|
.play-button {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
|
355
317
|
}
|
|
356
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
|
+
}
|