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.
Files changed (55) hide show
  1. package/dist/danx.es.js +25284 -23176
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +133 -120
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +4 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Buttons/ActionButton.vue +11 -3
  9. package/src/components/Utility/Code/CodeViewer.vue +219 -0
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +34 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +53 -0
  12. package/src/components/Utility/Code/LanguageBadge.vue +122 -0
  13. package/src/components/Utility/Code/MarkdownContent.vue +405 -0
  14. package/src/components/Utility/Code/index.ts +5 -0
  15. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +134 -38
  16. package/src/components/Utility/Files/CarouselHeader.vue +24 -0
  17. package/src/components/Utility/Files/FileMetadataDialog.vue +69 -0
  18. package/src/components/Utility/Files/FilePreview.vue +118 -166
  19. package/src/components/Utility/Files/index.ts +1 -0
  20. package/src/components/Utility/index.ts +1 -0
  21. package/src/composables/index.ts +5 -0
  22. package/src/composables/useCodeFormat.ts +199 -0
  23. package/src/composables/useCodeViewerCollapse.ts +125 -0
  24. package/src/composables/useCodeViewerEditor.ts +420 -0
  25. package/src/composables/useFilePreview.ts +119 -0
  26. package/src/composables/useTranscodeLoader.ts +68 -0
  27. package/src/helpers/formats/highlightSyntax.ts +327 -0
  28. package/src/helpers/formats/index.ts +3 -1
  29. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  30. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  31. package/src/helpers/formats/markdown/index.ts +85 -0
  32. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  33. package/src/helpers/formats/markdown/render/index.ts +92 -0
  34. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  35. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  36. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  37. package/src/helpers/formats/markdown/state.ts +58 -0
  38. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  39. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  40. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  41. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  42. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  43. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  44. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  45. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  46. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  47. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  48. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  49. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  50. package/src/helpers/formats/markdown/types.ts +63 -0
  51. package/src/styles/danx.scss +4 -0
  52. package/src/styles/themes/danx/code.scss +158 -0
  53. package/src/styles/themes/danx/index.scss +2 -0
  54. package/src/styles/themes/danx/markdown.scss +241 -0
  55. 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.progress : ((transcodingStatus?.progress || 0) / 100)"
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 { computed, ComputedRef, onMounted, ref, watch } from "vue";
167
- import { danxOptions } from "../../../config";
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 * as fileHelpers from "../../../helpers/filePreviewHelpers";
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
- src?: string;
186
- file?: UploadedFile;
187
- relatedFiles?: UploadedFile[];
188
- missingIcon?: any;
189
- showFilename?: boolean;
190
- downloadButtonClass?: string;
191
- imageFit?: "cover" | "contain" | "fill" | "none" | "scale-down";
192
- downloadable?: boolean;
193
- removable?: boolean;
194
- disabled?: boolean;
195
- square?: boolean;
196
- btnSize?: "xs" | "sm" | "md" | "lg";
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
- src: "",
203
- file: null,
204
- relatedFiles: null,
205
- missingIcon: ImageIcon,
206
- downloadButtonClass: "bg-blue-600 text-white",
207
- imageFit: "cover",
208
- downloadable: false,
209
- removable: false,
210
- disabled: false,
211
- square: false,
212
- btnSize: "sm"
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 isLoadingTranscodes = ref(false);
218
- const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
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
- const isUploading = computed(() => !props.file || props.file?.progress !== undefined);
261
+ // Computed
262
+ const isUploading = computed(() => props.file && props.file?.progress !== undefined);
235
263
  const statusMessage = computed(() => isUploading.value ? "Uploading..." : transcodingStatus.value?.message);
236
- const hasTranscodes = computed(() => (props.file?.transcodes?.length || 0) > 0);
237
- const previewableFiles: ComputedRef<(UploadedFile | null)[] | null> = computed(() => {
238
- return props.relatedFiles?.length > 0 ? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl) : [computedImage.value];
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
- return a.id === b.id ||
243
- [b.url, b.optimized?.url, b.thumb?.url].includes(a.url) ||
244
- [a.url, a.optimized?.url, a.thumb?.url].includes(b.url);
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
- if (!isConfirmingRemove.value) {
279
- isConfirmingRemove.value = true;
280
- setTimeout(() => {
281
- isConfirmingRemove.value = false;
282
- }, 2000);
283
- } else {
284
- emit("remove");
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
- // For external links (Google Docs, etc.), open directly in new tab
290
- if (computedImage.value && isExternalLinkFile(computedImage.value)) {
291
- window.open(computedImage.value.url, "_blank");
292
- return;
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
- position: absolute;
349
- bottom: 1.5em;
350
- right: 1em;
351
- z-index: 1;
300
+ position: absolute;
301
+ bottom: 1.5em;
302
+ right: 1em;
303
+ z-index: 1;
352
304
  }
353
305
 
354
306
  .play-button {
355
- position: absolute;
356
- top: 0;
357
- left: 0;
358
- display: flex;
359
- justify-content: center;
360
- align-items: center;
361
- width: 100%;
362
- height: 100%;
363
- pointer-events: none;
364
- @apply text-blue-200;
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";
@@ -1,4 +1,5 @@
1
1
  export * from "./Buttons";
2
+ export * from "./Code";
2
3
  export * from "./Controls";
3
4
  export * from "./Dialogs";
4
5
  export * from "./Files";
@@ -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
+ }