lutaml-jsonschema 0.1.14 → 0.1.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 105f1b74c4e82552c08ad6a406ab2da66979e1bad38b93a0d654a547868275d6
4
- data.tar.gz: 25b463827ef7593125b58a781f4d3cb60c3a8861b2736b2590879ec957d0b6f4
3
+ metadata.gz: a01871af7f67b7fac28c6c32abf7397d48af9c14cc95004849310f8f1b3d13cc
4
+ data.tar.gz: 119580a2cacbf4374811f05765074442fa06d25e733f81b65aec40f18a1069b0
5
5
  SHA512:
6
- metadata.gz: 9e9e80db8d96d09d0da8990e71f8b637cb7e11bb7a0debed104d56eb71add45d4f14c3933286f07333f5194ee5addcfcb21dabc3c22a426919a16b8896a0e5a1
7
- data.tar.gz: 8bdbe14abce145c588ff46c2efec874d9f7a7098e74a615442929821d93f8646f871b43eb3a04da4f4114a1dd6fddbfbd9009ee6000786462afe10891529eeb6
6
+ metadata.gz: 17e159091bff17f2d15ec5aefac90e6bff786a3df02677cb40bb24f635e4f2fc3785173d9336d5f7e5636a230cb1c1a21ade16d4d3155841d7ddcd3ae7d3207d
7
+ data.tar.gz: d3c1630021ee7690da9dff15293668fef1dc464c26493f7347d726c6e976f946c1e89057180023eb6aff4851ed011f9894ef6475b1118dfd823f4ce258bba69e
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-05-10 22:34:24 UTC using RuboCop version 1.86.1.
3
+ # on 2026-05-13 07:41:21 UTC using RuboCop version 1.86.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -168,7 +168,8 @@
168
168
  <td class="constraint-key">Enum</td>
169
169
  <td>
170
170
  <div class="enum-values-list">
171
- <span v-for="e in propertyItem.enum" :key="e" class="enum-value-chip">{{ e }}</span>
171
+ <span v-for="e in visibleEnumValues(propertyItem.name, propertyItem.enum)" :key="e" class="enum-value-chip">{{ e }}</span>
172
+ <button v-if="propertyItem.enum.length > 8 && !detailEnumExpanded.has(propertyItem.name)" class="enum-more-btn" @click="toggleDetailEnum(propertyItem.name)">+{{ propertyItem.enum.length - 8 }} more</button>
172
173
  </div>
173
174
  </td>
174
175
  </tr>
@@ -260,12 +261,15 @@
260
261
  <div v-if="examples.length" class="example-group">
261
262
  <h4 class="example-label">Provided Examples</h4>
262
263
  <div v-for="(ex, idx) in examples" :key="idx" class="example-block">
263
- <pre class="example-pre"><code>{{ formatExample(ex) }}</code></pre>
264
+ <div v-if="isComplexExample(ex)" class="example-collapsible" @click="toggleExampleCollapse($event)" @keydown="handleExampleKey($event)">
265
+ <pre class="example-pre" v-html="renderExampleHtml(ex)"></pre>
266
+ </div>
267
+ <pre v-else class="example-pre"><code>{{ formatExample(ex) }}</code></pre>
264
268
  </div>
265
269
  </div>
266
270
  <div v-if="generatedExample" class="example-group">
267
271
  <h4 class="example-label">Generated Example</h4>
268
- <pre class="example-pre example-generated"><code>{{ generatedExample }}</code></pre>
272
+ <pre class="example-pre example-generated" v-html="renderExampleHtml(generatedExample)"></pre>
269
273
  </div>
270
274
  <p v-if="!examples.length && !generatedExample" class="text-muted">No examples available.</p>
271
275
  </div>
@@ -281,10 +285,11 @@
281
285
  </template>
282
286
 
283
287
  <script setup lang="ts">
284
- import { computed, ref, onMounted, onUnmounted } from 'vue'
288
+ import { computed, ref, reactive, onMounted, onUnmounted } from 'vue'
285
289
  import { useSchemaStore, type SelectedItem } from '../stores/schemaStore'
286
290
  import { useUiStore } from '../stores/uiStore'
287
291
  import { renderInlineMarkdown } from '../composables/useMarkdownLite'
292
+ import { jsonToCollapsibleHtml } from '../composables/useJsonViewer'
288
293
  import type { SpaProperty } from '../types'
289
294
 
290
295
  const schemaStore = useSchemaStore()
@@ -292,6 +297,17 @@ const uiStore = useUiStore()
292
297
 
293
298
  const panelRef = ref<HTMLElement | null>(null)
294
299
  const closeBtnRef = ref<HTMLButtonElement | null>(null)
300
+ const detailEnumExpanded = reactive(new Set<string>())
301
+
302
+ function visibleEnumValues(name: string, enums: string[]): string[] {
303
+ if (detailEnumExpanded.has(name) || enums.length <= 8) return enums
304
+ return enums.slice(0, 8)
305
+ }
306
+
307
+ function toggleDetailEnum(name: string) {
308
+ if (detailEnumExpanded.has(name)) detailEnumExpanded.delete(name)
309
+ else detailEnumExpanded.add(name)
310
+ }
295
311
 
296
312
  onMounted(() => {
297
313
  closeBtnRef.value?.focus()
@@ -514,6 +530,56 @@ function navigateToProperty(name: string) {
514
530
  uiStore.closeDetailPanel()
515
531
  schemaStore.selectProperty(name)
516
532
  }
533
+
534
+ function isComplexExample(value: unknown): boolean {
535
+ if (typeof value === 'string') {
536
+ try { return typeof JSON.parse(value) === 'object' } catch { return false }
537
+ }
538
+ return typeof value === 'object' && value !== null
539
+ }
540
+
541
+ function renderExampleHtml(value: unknown): string {
542
+ let parsed: unknown
543
+ if (typeof value === 'string') {
544
+ try { parsed = JSON.parse(value) } catch { return escapeHtml(value) }
545
+ } else {
546
+ parsed = value
547
+ }
548
+ try {
549
+ return jsonToCollapsibleHtml(parsed, 3)
550
+ } catch {
551
+ return escapeHtml(typeof parsed === 'string' ? parsed : JSON.stringify(parsed, null, 2))
552
+ }
553
+ }
554
+
555
+ function escapeHtml(t: string): string {
556
+ return t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
557
+ }
558
+
559
+ function toggleExampleCollapse(event: MouseEvent) {
560
+ const target = event.target as HTMLElement
561
+ if (!target.classList.contains('jv-toggle')) return
562
+ const parent = target.parentElement
563
+ if (!parent) return
564
+ const children = parent.querySelector('.jv-children')
565
+ if (!children) return
566
+ const isCollapsed = children.classList.contains('jv-collapsed')
567
+ if (isCollapsed) {
568
+ children.classList.remove('jv-collapsed')
569
+ target.setAttribute('aria-label', 'collapse')
570
+ } else {
571
+ children.classList.add('jv-collapsed')
572
+ target.setAttribute('aria-label', 'expand')
573
+ }
574
+ }
575
+
576
+ function handleExampleKey(event: KeyboardEvent) {
577
+ if (event.key !== 'Enter' && event.key !== ' ') return
578
+ const target = event.target as HTMLElement
579
+ if (!target.classList.contains('jv-toggle')) return
580
+ event.preventDefault()
581
+ toggleExampleCollapse(event as unknown as MouseEvent)
582
+ }
517
583
  </script>
518
584
 
519
585
  <style scoped>
@@ -820,6 +886,20 @@ function navigateToProperty(name: string) {
820
886
  color: var(--text-primary);
821
887
  }
822
888
 
889
+ .enum-more-btn {
890
+ font-size: 11px;
891
+ color: var(--color-primary);
892
+ background: none;
893
+ border: none;
894
+ cursor: pointer;
895
+ padding: 1px 4px;
896
+ font-family: var(--font-mono);
897
+ }
898
+
899
+ .enum-more-btn:hover {
900
+ text-decoration: underline;
901
+ }
902
+
823
903
  .constraint-table td:first-child {
824
904
  font-weight: 500;
825
905
  color: var(--text-secondary);
@@ -896,6 +976,103 @@ function navigateToProperty(name: string) {
896
976
  border-color: var(--color-primary-alpha);
897
977
  }
898
978
 
979
+ .example-collapsible {
980
+ cursor: default;
981
+ }
982
+
983
+ .example-pre :deep(ul) {
984
+ list-style: none;
985
+ padding-left: var(--space-4);
986
+ margin: 0;
987
+ }
988
+
989
+ .example-pre :deep(li) {
990
+ margin: 0;
991
+ }
992
+
993
+ .example-pre :deep(.jv-toggle) {
994
+ background: none;
995
+ border: 1px solid var(--border-medium);
996
+ border-radius: 2px;
997
+ cursor: pointer;
998
+ width: 14px;
999
+ height: 14px;
1000
+ font-size: 9px;
1001
+ line-height: 1;
1002
+ display: inline-flex;
1003
+ align-items: center;
1004
+ justify-content: center;
1005
+ color: var(--text-muted);
1006
+ padding: 0;
1007
+ margin-right: 4px;
1008
+ vertical-align: middle;
1009
+ }
1010
+
1011
+ .example-pre :deep(.jv-toggle:hover) {
1012
+ background: var(--bg-hover);
1013
+ }
1014
+
1015
+ .example-pre :deep(.jv-toggle[aria-label="expand"])::after { content: '+'; }
1016
+ .example-pre :deep(.jv-toggle[aria-label="collapse"])::after { content: '−'; }
1017
+
1018
+ .example-pre :deep(.jv-ellipsis) {
1019
+ display: none;
1020
+ color: var(--text-muted);
1021
+ font-size: var(--text-xs);
1022
+ font-style: italic;
1023
+ margin-left: 4px;
1024
+ }
1025
+
1026
+ .example-pre :deep(.jv-children.jv-collapsed) {
1027
+ display: none;
1028
+ }
1029
+
1030
+ .example-pre :deep(.jv-children.jv-collapsed + .jv-ellipsis) {
1031
+ display: inline;
1032
+ }
1033
+
1034
+ .example-pre :deep(.jv-key) {
1035
+ color: var(--color-primary-dark);
1036
+ font-weight: 500;
1037
+ }
1038
+
1039
+ .example-pre :deep(.jv-punct) {
1040
+ color: var(--text-muted);
1041
+ }
1042
+
1043
+ .example-pre :deep(.jv-string) {
1044
+ color: var(--color-green);
1045
+ }
1046
+
1047
+ .example-pre :deep(.jv-number) {
1048
+ color: var(--color-orange);
1049
+ }
1050
+
1051
+ .example-pre :deep(.jv-boolean) {
1052
+ color: var(--color-accent);
1053
+ }
1054
+
1055
+ .example-pre :deep(.jv-null) {
1056
+ color: var(--text-muted);
1057
+ font-style: italic;
1058
+ }
1059
+
1060
+ .example-pre :deep(.jv-link) {
1061
+ color: var(--color-green);
1062
+ text-decoration: underline;
1063
+ }
1064
+
1065
+ .example-pre :deep(.jv-row:hover) {
1066
+ background: var(--bg-hover);
1067
+ border-radius: 2px;
1068
+ }
1069
+
1070
+ :root[data-theme="dark"] .example-pre :deep(.jv-key) { color: var(--color-primary-light); }
1071
+ :root[data-theme="dark"] .example-pre :deep(.jv-string) { color: var(--color-teal); }
1072
+ :root[data-theme="dark"] .example-pre :deep(.jv-number) { color: var(--color-accent-light); }
1073
+ :root[data-theme="dark"] .example-pre :deep(.jv-boolean) { color: var(--color-primary-light); }
1074
+ :root[data-theme="dark"] .example-pre :deep(.jv-toggle) { border-color: rgba(255, 255, 255, 0.2); }
1075
+
899
1076
  .panel-content :deep(.md-code) {
900
1077
  font-family: var(--font-mono);
901
1078
  font-size: inherit;
@@ -914,6 +1091,37 @@ function navigateToProperty(name: string) {
914
1091
  text-decoration: underline;
915
1092
  }
916
1093
 
1094
+ .panel-content :deep(.md-heading) {
1095
+ font-weight: 600;
1096
+ margin: var(--space-2) 0 var(--space-1);
1097
+ color: var(--text-primary);
1098
+ }
1099
+
1100
+ .panel-content :deep(.md-list) {
1101
+ margin: var(--space-1) 0;
1102
+ padding-left: var(--space-5);
1103
+ font-size: inherit;
1104
+ }
1105
+
1106
+ .panel-content :deep(.md-list li) {
1107
+ margin-bottom: 2px;
1108
+ }
1109
+
1110
+ .panel-content :deep(.md-pre) {
1111
+ background: var(--bg-secondary);
1112
+ border: 1px solid var(--border-light);
1113
+ border-radius: var(--radius-sm);
1114
+ padding: var(--space-2);
1115
+ margin: var(--space-2) 0;
1116
+ overflow-x: auto;
1117
+ font-size: var(--text-sm);
1118
+ }
1119
+
1120
+ .panel-content :deep(.md-pre code) {
1121
+ font-family: var(--font-mono);
1122
+ font-size: inherit;
1123
+ }
1124
+
917
1125
  /* Dark mode overrides */
918
1126
  :root[data-theme="dark"] .detail-panel {
919
1127
  border-left: 1px solid rgba(255, 255, 255, 0.08);
@@ -33,9 +33,11 @@
33
33
  :aria-label="`Include ${field.prop.name}`"
34
34
  @change="toggleField(field, ($event.target as HTMLInputElement).checked)"
35
35
  />
36
- <button class="field-name" :class="{ dimmed: !field.included, deprecated: field.prop.deprecated }" :aria-label="`View details for ${field.prop.name}`" @click="openPropertyDetail(field.prop)">
36
+ <span class="field-bullet" aria-hidden="true"></span>
37
+ <button class="field-name" :class="{ dimmed: !field.included, deprecated: field.prop.deprecated, expandable: !!field.resolvedDef }" :aria-label="field.resolvedDef ? `${field.expanded ? 'Collapse' : 'Expand'} ${field.prop.name}` : `View details for ${field.prop.name}`" @click="field.resolvedDef && field.included ? (field.expanded = !field.expanded) : openPropertyDetail(field.prop)">
37
38
  <span v-if="field.prop.title && field.prop.title !== field.prop.name" class="field-human-title">{{ field.prop.title }}</span>
38
39
  <span class="font-mono">{{ field.prop.name }}</span>
40
+ <span v-if="field.resolvedDef" class="field-expand-icon" :class="{ expanded: field.expanded }">&#9654;</span>
39
41
  </button>
40
42
  <span class="field-type-badge" :class="typeBadgeClass(field.prop)">{{ displayType(field.prop, field.resolvedDef?.title || field.resolvedDef?.name) }}</span>
41
43
  <span v-if="field.prop.title && field.prop.title !== field.prop.name" class="field-title-badge">{{ field.prop.title }}</span>
@@ -247,7 +249,7 @@
247
249
  </button>
248
250
  </div>
249
251
  </div>
250
- <pre ref="jsonBlockRef" class="json-block" @click="handleJsonClick" @dblclick="selectJsonBlock" v-html="highlightedJson"></pre>
252
+ <pre ref="jsonBlockRef" class="json-block" @click="handleJsonClick" @keydown="handleJsonKey" @dblclick="selectJsonBlock" v-html="highlightedJson"></pre>
251
253
  <div v-if="!hasIncludedFields" class="json-empty-hint">
252
254
  <span class="text-muted">Check fields to build JSON</span>
253
255
  </div>
@@ -433,6 +435,19 @@ function collapseAllJson() {
433
435
  function handleJsonClick(event: MouseEvent) {
434
436
  const target = event.target as HTMLElement
435
437
  if (!target.classList.contains('jv-toggle')) return
438
+ toggleJsonNode(target)
439
+ }
440
+
441
+ function handleJsonKey(event: KeyboardEvent) {
442
+ const target = event.target as HTMLElement
443
+ if (!target.classList.contains('jv-toggle')) return
444
+ if (event.key === 'Enter' || event.key === ' ') {
445
+ event.preventDefault()
446
+ toggleJsonNode(target)
447
+ }
448
+ }
449
+
450
+ function toggleJsonNode(target: HTMLElement) {
436
451
  const parent = target.parentElement
437
452
  if (!parent) return
438
453
  const children = parent.querySelector('.jv-children')
@@ -628,6 +643,23 @@ async function copyJson() {
628
643
  cursor: default;
629
644
  }
630
645
 
646
+ .field-bullet {
647
+ width: 4px;
648
+ height: 4px;
649
+ border-radius: 50%;
650
+ background: var(--border-medium);
651
+ flex-shrink: 0;
652
+ margin-left: -2px;
653
+ }
654
+
655
+ .field-bullet + .field-name.dimmed {
656
+ opacity: 0.35;
657
+ }
658
+
659
+ .field-bullet + .field-name.dimmed + .field-bullet {
660
+ opacity: 0.35;
661
+ }
662
+
631
663
  .field-name {
632
664
  font-weight: 600;
633
665
  font-size: var(--text-sm);
@@ -666,6 +698,21 @@ async function copyJson() {
666
698
  opacity: 0.7;
667
699
  }
668
700
 
701
+ .field-name.expandable .font-mono:hover {
702
+ color: var(--color-primary);
703
+ }
704
+
705
+ .field-expand-icon {
706
+ font-size: 8px;
707
+ color: var(--text-muted);
708
+ transition: transform var(--transition-fast);
709
+ margin-left: 2px;
710
+ }
711
+
712
+ .field-expand-icon.expanded {
713
+ transform: rotate(90deg);
714
+ }
715
+
669
716
  .field-type-badge {
670
717
  font-size: 11px;
671
718
  font-weight: 500;
@@ -937,6 +984,38 @@ async function copyJson() {
937
984
  text-decoration: underline;
938
985
  }
939
986
 
987
+ .field-desc :deep(.md-heading) {
988
+ font-weight: 600;
989
+ margin: var(--space-2) 0 var(--space-1);
990
+ color: var(--text-primary);
991
+ font-size: inherit;
992
+ }
993
+
994
+ .field-desc :deep(.md-list) {
995
+ margin: var(--space-1) 0;
996
+ padding-left: var(--space-5);
997
+ font-size: inherit;
998
+ }
999
+
1000
+ .field-desc :deep(.md-list li) {
1001
+ margin-bottom: 2px;
1002
+ }
1003
+
1004
+ .field-desc :deep(.md-pre) {
1005
+ background: var(--bg-secondary);
1006
+ border: 1px solid var(--border-light);
1007
+ border-radius: var(--radius-sm);
1008
+ padding: var(--space-2);
1009
+ margin: var(--space-2) 0;
1010
+ overflow-x: auto;
1011
+ font-size: var(--text-xs);
1012
+ }
1013
+
1014
+ .field-desc :deep(.md-pre code) {
1015
+ font-family: var(--font-mono);
1016
+ font-size: inherit;
1017
+ }
1018
+
940
1019
  .field-constraints {
941
1020
  display: flex;
942
1021
  flex-wrap: wrap;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Lightweight inline Markdown renderer for schema descriptions.
3
- * Handles: **bold**, *italic*, `code`, [link](url), ```fenced code blocks```
3
+ * Handles: **bold**, *italic*, `code`, [link](url), ```fenced code blocks```,
4
+ * bullet lists (-, *, +), numbered lists (1.), headings (###).
4
5
  */
5
6
  export function renderInlineMarkdown(text: string): string {
6
7
  if (!text) return ''
@@ -8,12 +9,82 @@ export function renderInlineMarkdown(text: string): string {
8
9
 
9
10
  // Fenced code blocks: ```lang\n...\n``` (must run before inline code)
10
11
  html = html.replace(/```[\w]*\n([\s\S]*?)```/g, (_match, code: string) => {
11
- return `<pre class="md-pre"><code>${code.trim()}</code></pre>`
12
+ return `\x00FENCED${code.trim()}\x00ENDFENCED`
12
13
  })
13
14
 
14
- // Line breaks: \n <br> (after fenced code blocks are extracted)
15
+ // Split into lines for block-level processing
16
+ const lines = html.split('\n')
17
+ const result: string[] = []
18
+ let inUl = false
19
+ let inOl = false
20
+
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const line = lines[i]
23
+
24
+ // Skip fenced code block internals (already escaped)
25
+ if (line.includes('\x00FENCED') || line.includes('\x00ENDFENCED')) {
26
+ if (inUl) { result.push('</ul>'); inUl = false }
27
+ if (inOl) { result.push('</ol>'); inOl = false }
28
+ result.push(line)
29
+ continue
30
+ }
31
+
32
+ // Heading: ### text
33
+ const headingMatch = line.match(/^(#{1,4})\s+(.+)$/)
34
+ if (headingMatch) {
35
+ if (inUl) { result.push('</ul>'); inUl = false }
36
+ if (inOl) { result.push('</ol>'); inOl = false }
37
+ const level = headingMatch[1].length
38
+ result.push(`<h${level + 2} class="md-heading">${headingMatch[2]}</h${level + 2}>`)
39
+ continue
40
+ }
41
+
42
+ // Bullet list: -, *, + followed by space
43
+ const ulMatch = line.match(/^(\s*)([-*+])\s+(.+)$/)
44
+ if (ulMatch) {
45
+ if (inOl) { result.push('</ol>'); inOl = false }
46
+ if (!inUl) { result.push('<ul class="md-list">'); inUl = true }
47
+ result.push(`<li>${applyInline(ulMatch[3])}</li>`)
48
+ continue
49
+ }
50
+
51
+ // Numbered list: 1. text
52
+ const olMatch = line.match(/^(\s*)\d+\.\s+(.+)$/)
53
+ if (olMatch) {
54
+ if (inUl) { result.push('</ul>'); inUl = false }
55
+ if (!inOl) { result.push('<ol class="md-list">'); inOl = true }
56
+ result.push(`<li>${applyInline(olMatch[2])}</li>`)
57
+ continue
58
+ }
59
+
60
+ // Close list if non-list line encountered
61
+ if (inUl) { result.push('</ul>'); inUl = false }
62
+ if (inOl) { result.push('</ol>'); inOl = false }
63
+
64
+ // Regular line
65
+ result.push(line)
66
+ }
67
+
68
+ if (inUl) result.push('</ul>')
69
+ if (inOl) result.push('</ol>')
70
+
71
+ html = result.join('\n')
72
+
73
+ // Restore fenced code blocks
74
+ html = html.replace(/\x00FENCED([\s\S]*?)\x00ENDFENCED/g, (_match, code: string) => {
75
+ return `<pre class="md-pre"><code>${code}</code></pre>`
76
+ })
77
+
78
+ // Line breaks: \n → <br> (after block elements are formed)
15
79
  html = html.replace(/\n/g, '<br>')
16
80
 
81
+ // Apply inline formatting to the whole output
82
+ html = applyInline(html)
83
+
84
+ return html
85
+ }
86
+
87
+ function applyInline(html: string): string {
17
88
  // Links: [text](url)
18
89
  html = html.replace(
19
90
  /\[([^\]]+)\]\(([^)]+)\)/g,
@@ -27,7 +98,6 @@ export function renderInlineMarkdown(text: string): string {
27
98
  html = html.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '<em>$1</em>')
28
99
  // Code: `text`
29
100
  html = html.replace(/`([^`]+)`/g, '<code class="md-code">$1</code>')
30
-
31
101
  return html
32
102
  }
33
103
 
@@ -187,15 +187,18 @@
187
187
  <div v-if="viewMode === 'source'" class="schema-section">
188
188
  <div v-if="schemaStore.selectedSchema.sourceJson" class="source-viewer">
189
189
  <div class="source-toolbar">
190
- <span class="text-muted">{{ sourceLineCount }} lines</span>
191
- <button class="btn btn-ghost btn-sm copy-btn-wrap" @click="copySource">
192
- Copy
193
- <span v-if="sourceCopied" class="copy-tooltip">Copied!</span>
194
- </button>
190
+ <span class="text-muted">Source JSON Schema</span>
191
+ <div class="source-toolbar-actions">
192
+ <button class="btn btn-ghost btn-sm copy-btn-wrap" @click="copySource">
193
+ Copy
194
+ <span v-if="sourceCopied" class="copy-tooltip">Copied!</span>
195
+ </button>
196
+ <button class="btn btn-ghost btn-sm" @click="expandSourceAll">Expand all</button>
197
+ <button class="btn btn-ghost btn-sm" @click="collapseSourceAll">Collapse all</button>
198
+ </div>
195
199
  </div>
196
200
  <div class="source-code-wrapper">
197
- <div class="source-lines" aria-hidden="true"><span v-for="n in sourceLineCount" :key="n" :class="{ 'source-line-active': n === activeSourceLine }" @click="activeSourceLine = n">{{ n }}</span></div>
198
- <pre class="source-pre" @dblclick="selectSourceBlock"><code v-html="highlightedSource"></code></pre>
201
+ <pre ref="sourcePreRef" class="source-pre" @click="handleSourceClick" @keydown="handleSourceKey" @dblclick="selectSourceBlock"><code v-html="highlightedSource"></code></pre>
199
202
  </div>
200
203
  </div>
201
204
  <div v-else class="source-empty">
@@ -208,7 +211,10 @@
208
211
  <div v-else class="landing-page">
209
212
  <div class="landing-header">
210
213
  <div>
211
- <h1>{{ schemaStore.metadata?.title || 'JSON Schema Documentation' }}</h1>
214
+ <div class="landing-title-row">
215
+ <h1>{{ schemaStore.metadata?.title || 'JSON Schema Documentation' }}</h1>
216
+ <span v-if="schemaStore.metadata?.version" class="version-badge">v{{ schemaStore.metadata.version }}</span>
217
+ </div>
212
218
  <p v-if="schemaStore.metadata?.description" class="landing-description">{{ schemaStore.metadata.description }}</p>
213
219
  <div class="landing-subtitle">
214
220
  <span>{{ schemaStore.schemaCounts.schemas }} schemas</span>
@@ -216,6 +222,10 @@
216
222
  <span>{{ schemaStore.schemaCounts.properties }} properties</span>
217
223
  <span class="separator">&middot;</span>
218
224
  <span>{{ schemaStore.schemaCounts.definitions }} definitions</span>
225
+ <template v-if="schemaStore.metadata?.baseUrl">
226
+ <span class="separator">&middot;</span>
227
+ <a :href="schemaStore.metadata.baseUrl" target="_blank" rel="noopener" class="landing-base-url">{{ schemaStore.metadata.baseUrl }}</a>
228
+ </template>
219
229
  </div>
220
230
  <div v-if="schemaStore.schemas.length > 3" class="landing-search">
221
231
  <input
@@ -294,6 +304,7 @@ import { useUiStore } from '../stores/uiStore'
294
304
  import SchemaBuilder from '../components/SchemaBuilder.vue'
295
305
  import { downloadFile } from '../composables/useDownload'
296
306
  import { renderInlineMarkdown } from '../composables/useMarkdownLite'
307
+ import { jsonToCollapsibleHtml } from '../composables/useJsonViewer'
297
308
  import { copyToClipboard } from '../composables/useClipboard'
298
309
  import type { SpaSchema, SpaProperty } from '../types'
299
310
 
@@ -305,6 +316,7 @@ const sourceCopied = ref(false)
305
316
  const activeSourceLine = ref(-1)
306
317
  const landingSearch = ref('')
307
318
  const linkCopied = ref(false)
319
+ const sourcePreRef = ref<HTMLElement | null>(null)
308
320
 
309
321
  const selectedDefinitionTitle = computed(() => {
310
322
  const name = schemaStore.selectedDefinitionName
@@ -348,7 +360,11 @@ function collapseAllDefs() {
348
360
  const highlightedSource = computed(() => {
349
361
  const src = schemaStore.selectedSchema?.sourceJson
350
362
  if (!src) return ''
351
- return syntaxHighlight(src)
363
+ try {
364
+ return jsonToCollapsibleHtml(JSON.parse(src), 2)
365
+ } catch {
366
+ return syntaxHighlight(src)
367
+ }
352
368
  })
353
369
 
354
370
  const sourceLineCount = computed(() => {
@@ -392,7 +408,7 @@ function syntaxHighlight(json: string): string {
392
408
  }
393
409
 
394
410
  function selectSourceBlock() {
395
- const el = document.querySelector('.source-pre') as HTMLElement | null
411
+ const el = sourcePreRef.value
396
412
  if (!el) return
397
413
  const range = document.createRange()
398
414
  range.selectNodeContents(el)
@@ -403,6 +419,57 @@ function selectSourceBlock() {
403
419
  }
404
420
  }
405
421
 
422
+ function handleSourceClick(event: MouseEvent) {
423
+ const target = event.target as HTMLElement
424
+ if (!target.classList.contains('jv-toggle')) return
425
+ toggleSourceNode(target)
426
+ }
427
+
428
+ function handleSourceKey(event: KeyboardEvent) {
429
+ const target = event.target as HTMLElement
430
+ if (!target.classList.contains('jv-toggle')) return
431
+ if (event.key === 'Enter' || event.key === ' ') {
432
+ event.preventDefault()
433
+ toggleSourceNode(target)
434
+ }
435
+ }
436
+
437
+ function toggleSourceNode(target: HTMLElement) {
438
+ const parent = target.parentElement
439
+ if (!parent) return
440
+ const children = parent.querySelector('.jv-children')
441
+ if (!children) return
442
+ const isCollapsed = children.classList.contains('jv-collapsed')
443
+ if (isCollapsed) {
444
+ children.classList.remove('jv-collapsed')
445
+ target.setAttribute('aria-label', 'collapse')
446
+ } else {
447
+ children.classList.add('jv-collapsed')
448
+ target.setAttribute('aria-label', 'expand')
449
+ }
450
+ }
451
+
452
+ function expandSourceAll() {
453
+ const container = sourcePreRef.value
454
+ if (!container) return
455
+ container.querySelectorAll('.jv-collapsed').forEach(el => el.classList.remove('jv-collapsed'))
456
+ container.querySelectorAll('.jv-toggle').forEach(btn => btn.setAttribute('aria-label', 'collapse'))
457
+ }
458
+
459
+ function collapseSourceAll() {
460
+ const container = sourcePreRef.value
461
+ if (!container) return
462
+ const children = container.querySelectorAll('.jv-children')
463
+ children.forEach((el, i) => {
464
+ if (i === 0) return
465
+ el.classList.add('jv-collapsed')
466
+ })
467
+ container.querySelectorAll('.jv-toggle').forEach((btn, i) => {
468
+ if (i === 0) return
469
+ btn.setAttribute('aria-label', 'expand')
470
+ })
471
+ }
472
+
406
473
  function selectSchema(name: string) {
407
474
  schemaStore.selectSchema(name)
408
475
  }
@@ -814,6 +881,11 @@ watch(() => schemaStore.selectedItemKey, (key) => {
814
881
  font-size: var(--text-xs);
815
882
  }
816
883
 
884
+ .source-toolbar-actions {
885
+ display: flex;
886
+ gap: var(--space-2);
887
+ }
888
+
817
889
  /* Copy button tooltip */
818
890
  .copy-btn-wrap {
819
891
  position: relative;
@@ -852,56 +924,112 @@ watch(() => schemaStore.selectedItemKey, (key) => {
852
924
  }
853
925
 
854
926
  .source-code-wrapper {
855
- display: flex;
856
927
  max-height: 70vh;
857
928
  overflow: auto;
858
929
  }
859
930
 
860
- .source-lines {
861
- display: flex;
862
- flex-direction: column;
863
- padding: var(--space-4) var(--space-2) var(--space-4) var(--space-3);
864
- text-align: right;
865
- user-select: none;
866
- flex-shrink: 0;
867
- background: var(--bg-secondary);
868
- border-right: 1px solid var(--border-light);
931
+ .source-pre :deep(.json-key) { color: var(--color-primary-dark); }
932
+ .source-pre :deep(.json-string) { color: var(--color-green); }
933
+ .source-pre :deep(.json-number) { color: var(--color-orange); }
934
+ .source-pre :deep(.json-boolean) { color: var(--color-accent); }
935
+ .source-pre :deep(.json-null) { color: var(--text-muted); }
936
+
937
+ .source-pre :deep(.json-url) {
938
+ color: var(--color-green);
939
+ text-decoration: underline;
940
+ text-decoration-style: dotted;
941
+ }
942
+ .source-pre :deep(.json-url:hover) {
943
+ text-decoration-style: solid;
869
944
  }
870
945
 
871
- .source-lines span {
872
- font-family: var(--font-mono);
873
- font-size: var(--text-sm);
874
- line-height: var(--leading-relaxed);
946
+ /* Collapsible JSON in source viewer */
947
+ .source-pre :deep(ul) {
948
+ list-style: none;
949
+ padding-left: var(--space-4);
950
+ margin: 0;
951
+ }
952
+
953
+ .source-pre :deep(li) {
954
+ margin: 0;
955
+ }
956
+
957
+ .source-pre :deep(.jv-toggle) {
958
+ background: none;
959
+ border: 1px solid var(--border-medium);
960
+ border-radius: 2px;
961
+ cursor: pointer;
962
+ width: 14px;
963
+ height: 14px;
964
+ font-size: 9px;
965
+ line-height: 1;
966
+ display: inline-flex;
967
+ align-items: center;
968
+ justify-content: center;
875
969
  color: var(--text-muted);
876
- display: block;
877
- padding: 0 4px;
970
+ padding: 0;
971
+ margin-right: 4px;
972
+ vertical-align: middle;
973
+ transition: background var(--transition-fast);
878
974
  }
879
975
 
880
- .source-lines span:hover {
976
+ .source-pre :deep(.jv-toggle:hover) {
881
977
  background: var(--bg-hover);
882
- border-radius: 2px;
883
978
  }
884
979
 
885
- .source-lines span.source-line-active {
886
- background: var(--color-primary-alpha);
887
- color: var(--color-primary);
888
- border-radius: 2px;
889
- font-weight: 600;
980
+ .source-pre :deep(.jv-toggle[aria-label="expand"])::after { content: '+'; }
981
+ .source-pre :deep(.jv-toggle[aria-label="collapse"])::after { content: '−'; }
982
+
983
+ .source-pre :deep(.jv-ellipsis) {
984
+ display: none;
985
+ color: var(--text-muted);
986
+ font-size: var(--text-xs);
987
+ font-style: italic;
988
+ margin-left: 4px;
890
989
  }
891
990
 
892
- .source-pre :deep(.json-key) { color: var(--color-primary-dark); }
893
- .source-pre :deep(.json-string) { color: var(--color-green); }
894
- .source-pre :deep(.json-number) { color: var(--color-orange); }
895
- .source-pre :deep(.json-boolean) { color: var(--color-accent); }
896
- .source-pre :deep(.json-null) { color: var(--text-muted); }
991
+ .source-pre :deep(.jv-children.jv-collapsed) {
992
+ display: none;
993
+ }
897
994
 
898
- .source-pre :deep(.json-url) {
995
+ .source-pre :deep(.jv-children.jv-collapsed + .jv-ellipsis) {
996
+ display: inline;
997
+ }
998
+
999
+ .source-pre :deep(.jv-key) {
1000
+ color: var(--color-primary-dark);
1001
+ font-weight: 500;
1002
+ }
1003
+
1004
+ .source-pre :deep(.jv-punct) {
1005
+ color: var(--text-muted);
1006
+ }
1007
+
1008
+ .source-pre :deep(.jv-string) {
1009
+ color: var(--color-green);
1010
+ }
1011
+
1012
+ .source-pre :deep(.jv-number) {
1013
+ color: var(--color-orange);
1014
+ }
1015
+
1016
+ .source-pre :deep(.jv-boolean) {
1017
+ color: var(--color-accent);
1018
+ }
1019
+
1020
+ .source-pre :deep(.jv-null) {
1021
+ color: var(--text-muted);
1022
+ font-style: italic;
1023
+ }
1024
+
1025
+ .source-pre :deep(.jv-link) {
899
1026
  color: var(--color-green);
900
1027
  text-decoration: underline;
901
- text-decoration-style: dotted;
902
1028
  }
903
- .source-pre :deep(.json-url:hover) {
904
- text-decoration-style: solid;
1029
+
1030
+ .source-pre :deep(.jv-row:hover) {
1031
+ background: var(--bg-hover);
1032
+ border-radius: 2px;
905
1033
  }
906
1034
 
907
1035
  :root[data-theme="dark"] .source-pre :deep(.json-key) { color: var(--color-primary-light); }
@@ -910,23 +1038,18 @@ watch(() => schemaStore.selectedItemKey, (key) => {
910
1038
  :root[data-theme="dark"] .source-pre :deep(.json-boolean) { color: var(--color-primary-light); }
911
1039
  :root[data-theme="dark"] .source-pre :deep(.json-null) { color: var(--text-muted); }
912
1040
 
1041
+ :root[data-theme="dark"] .source-pre :deep(.jv-key) { color: var(--color-primary-light); }
1042
+ :root[data-theme="dark"] .source-pre :deep(.jv-string) { color: var(--color-teal); }
1043
+ :root[data-theme="dark"] .source-pre :deep(.jv-number) { color: var(--color-accent-light); }
1044
+ :root[data-theme="dark"] .source-pre :deep(.jv-boolean) { color: var(--color-primary-light); }
1045
+ :root[data-theme="dark"] .source-pre :deep(.jv-null) { color: var(--text-muted); }
1046
+ :root[data-theme="dark"] .source-pre :deep(.jv-toggle) { border-color: rgba(255, 255, 255, 0.2); }
1047
+ :root[data-theme="dark"] .source-pre :deep(.jv-punct) { color: var(--panel-dark-muted); }
1048
+ :root[data-theme="dark"] .source-pre :deep(.jv-row:hover) { background: rgba(255, 255, 255, 0.06); }
1049
+
913
1050
  :root[data-theme="dark"] .source-code-wrapper {
914
1051
  background: var(--panel-dark-bg);
915
1052
  }
916
- :root[data-theme="dark"] .source-lines {
917
- background: rgba(0, 0, 0, 0.15);
918
- border-right-color: rgba(255, 255, 255, 0.08);
919
- }
920
- :root[data-theme="dark"] .source-lines span {
921
- color: var(--panel-dark-muted);
922
- }
923
- :root[data-theme="dark"] .source-lines span:hover {
924
- background: rgba(255, 255, 255, 0.06);
925
- }
926
- :root[data-theme="dark"] .source-lines span.source-line-active {
927
- background: rgba(91, 156, 212, 0.2);
928
- color: var(--color-primary-light);
929
- }
930
1053
  :root[data-theme="dark"] .source-pre {
931
1054
  color: var(--panel-dark-text);
932
1055
  }
@@ -1039,6 +1162,48 @@ watch(() => schemaStore.selectedItemKey, (key) => {
1039
1162
  text-decoration: underline;
1040
1163
  }
1041
1164
 
1165
+ .def-card-desc :deep(.md-heading),
1166
+ .def-body-desc :deep(.md-heading),
1167
+ .schema-desc :deep(.md-heading) {
1168
+ font-weight: 600;
1169
+ margin: var(--space-2) 0 var(--space-1);
1170
+ color: var(--text-primary);
1171
+ font-size: inherit;
1172
+ }
1173
+
1174
+ .def-card-desc :deep(.md-list),
1175
+ .def-body-desc :deep(.md-list),
1176
+ .schema-desc :deep(.md-list) {
1177
+ margin: var(--space-1) 0;
1178
+ padding-left: var(--space-5);
1179
+ font-size: inherit;
1180
+ }
1181
+
1182
+ .def-card-desc :deep(.md-list li),
1183
+ .def-body-desc :deep(.md-list li),
1184
+ .schema-desc :deep(.md-list li) {
1185
+ margin-bottom: 2px;
1186
+ }
1187
+
1188
+ .def-card-desc :deep(.md-pre),
1189
+ .def-body-desc :deep(.md-pre),
1190
+ .schema-desc :deep(.md-pre) {
1191
+ background: var(--bg-secondary);
1192
+ border: 1px solid var(--border-light);
1193
+ border-radius: var(--radius-sm);
1194
+ padding: var(--space-2);
1195
+ margin: var(--space-2) 0;
1196
+ overflow-x: auto;
1197
+ font-size: var(--text-xs);
1198
+ }
1199
+
1200
+ .def-card-desc :deep(.md-pre code),
1201
+ .def-body-desc :deep(.md-pre code),
1202
+ .schema-desc :deep(.md-pre code) {
1203
+ font-family: var(--font-mono);
1204
+ font-size: inherit;
1205
+ }
1206
+
1042
1207
  .def-card-info {
1043
1208
  padding: 0 var(--space-4) var(--space-3);
1044
1209
  margin-top: calc(var(--space-1) * -1);
@@ -1349,6 +1514,27 @@ watch(() => schemaStore.selectedItemKey, (key) => {
1349
1514
  margin-bottom: var(--space-2);
1350
1515
  }
1351
1516
 
1517
+ .landing-title-row {
1518
+ display: flex;
1519
+ align-items: center;
1520
+ gap: var(--space-3);
1521
+ }
1522
+
1523
+ .landing-title-row h1 {
1524
+ margin-bottom: 0;
1525
+ }
1526
+
1527
+ .version-badge {
1528
+ font-size: var(--text-sm);
1529
+ font-weight: 500;
1530
+ color: var(--text-muted);
1531
+ background: var(--bg-secondary);
1532
+ padding: 2px 8px;
1533
+ border-radius: var(--radius-md);
1534
+ border: 1px solid var(--border-light);
1535
+ font-family: var(--font-mono);
1536
+ }
1537
+
1352
1538
  .landing-description {
1353
1539
  font-size: var(--text-base);
1354
1540
  color: var(--text-secondary);
@@ -1411,6 +1597,17 @@ watch(() => schemaStore.selectedItemKey, (key) => {
1411
1597
  margin: 0 var(--space-1);
1412
1598
  }
1413
1599
 
1600
+ .landing-base-url {
1601
+ color: var(--color-primary);
1602
+ text-decoration: none;
1603
+ font-size: var(--text-sm);
1604
+ font-family: var(--font-mono);
1605
+ }
1606
+
1607
+ .landing-base-url:hover {
1608
+ text-decoration: underline;
1609
+ }
1610
+
1414
1611
  .schema-grid {
1415
1612
  display: grid;
1416
1613
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -1484,6 +1681,16 @@ watch(() => schemaStore.selectedItemKey, (key) => {
1484
1681
  text-decoration: underline;
1485
1682
  }
1486
1683
 
1684
+ .schema-card-desc :deep(.md-list) {
1685
+ margin: var(--space-1) 0;
1686
+ padding-left: var(--space-5);
1687
+ font-size: inherit;
1688
+ }
1689
+
1690
+ .schema-card-desc :deep(.md-pre) {
1691
+ display: none;
1692
+ }
1693
+
1487
1694
  .schema-card-meta {
1488
1695
  margin-bottom: var(--space-2);
1489
1696
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Jsonschema
5
- VERSION = "0.1.14"
5
+ VERSION = "0.1.15"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-jsonschema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14
4
+ version: 0.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-10 00:00:00.000000000 Z
11
+ date: 2026-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json