1+ <!DOCTYPE html>
2+ < html lang ="en ">
3+ < head >
4+ < meta charset ="UTF-8 ">
5+ < title > YAML Explorer</ title >
6+ < style >
7+ * {
8+ box-sizing : border-box;
9+ }
10+
11+ body {
12+ font-family : Helvetica, Arial, sans-serif;
13+ line-height : 1.4 ;
14+ margin : 0 ;
15+ padding : 20px ;
16+ }
17+
18+ textarea , input [type = "url" ] {
19+ width : 100% ;
20+ margin-bottom : 20px ;
21+ padding : 8px ;
22+ font-size : 16px ;
23+ font-family : monospace;
24+ border : 1px solid # ccc ;
25+ border-radius : 4px ;
26+ }
27+
28+ button {
29+ font-size : 16px ;
30+ padding : 0 20px ;
31+ background : # 0066cc ;
32+ color : white;
33+ border : none;
34+ border-radius : 4px ;
35+ cursor : pointer;
36+ min-width : 80px ;
37+ height : 37px ; /* Match input height */
38+ }
39+
40+ button : hover {
41+ background : # 0052a3 ;
42+ }
43+
44+ button : active {
45+ background : # 004080 ;
46+ }
47+
48+ textarea {
49+ height : 200px ;
50+ }
51+
52+ .container {
53+ max-width : 800px ;
54+ margin : 0 auto;
55+ }
56+
57+ .output {
58+ border : 1px solid # eee ;
59+ padding : 20px ;
60+ border-radius : 4px ;
61+ }
62+
63+ details {
64+ margin : 0.5em 0 ;
65+ padding-left : 20px ;
66+ }
67+
68+ summary {
69+ margin-left : -20px ;
70+ cursor : pointer;
71+ }
72+
73+ summary > span {
74+ color : # 666 ;
75+ font-size : 0.9em ;
76+ }
77+
78+ .expand-all {
79+ color : # 0066cc ;
80+ cursor : pointer;
81+ text-decoration : underline;
82+ font-size : 0.9em ;
83+ margin-left : 8px ;
84+ }
85+
86+ .key {
87+ color : # 0066cc ;
88+ font-weight : bold;
89+ }
90+
91+ .string {
92+ color : # 008000 ;
93+ }
94+
95+ .number {
96+ color : # ff6600 ;
97+ }
98+
99+ .boolean {
100+ color : # 9933cc ;
101+ }
102+
103+ .null {
104+ color : # 999 ;
105+ }
106+
107+ .error {
108+ color : red;
109+ margin : 1em 0 ;
110+ }
111+ </ style >
112+ </ head >
113+ < body >
114+ < div class ="container ">
115+ < h1 > YAML Explorer</ h1 >
116+ < div style ="display: flex; gap: 8px; ">
117+ < input type ="url " placeholder ="Optional: Enter URL to YAML file " style ="flex: 1; " />
118+ < button type ="button " style ="font-size: 16px; padding: 0 16px; "> Load</ button >
119+ </ div >
120+ < textarea placeholder ="Paste your YAML here... "> </ textarea >
121+ < div class ="output "> </ div >
122+ </ div >
123+
124+ < script type ="module ">
125+ import { load } from 'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.mjs'
126+
127+ const urlInput = document . querySelector ( 'input[type="url"]' )
128+ const textarea = document . querySelector ( 'textarea' )
129+ const output = document . querySelector ( '.output' )
130+
131+ // Create unique IDs for details elements to track state
132+ let detailsCounter = 0
133+
134+ function createValueSpan ( value ) {
135+ const span = document . createElement ( 'span' )
136+ if ( value === null ) {
137+ span . className = 'null'
138+ span . textContent = 'null'
139+ } else if ( typeof value === 'string' ) {
140+ span . className = 'string'
141+ span . textContent = `"${ value } "`
142+ } else if ( typeof value === 'number' ) {
143+ span . className = 'number'
144+ span . textContent = value
145+ } else if ( typeof value === 'boolean' ) {
146+ span . className = 'boolean'
147+ span . textContent = value
148+ }
149+ return span
150+ }
151+
152+ function createExpandAllButton ( parent ) {
153+ const button = document . createElement ( 'span' )
154+ button . className = 'expand-all'
155+ button . textContent = 'expand all'
156+ button . onclick = ( e ) => {
157+ e . preventDefault ( )
158+ e . stopPropagation ( )
159+ const allDetails = parent . querySelectorAll ( 'details' )
160+ allDetails . forEach ( d => d . open = true )
161+ updateUrlState ( )
162+ }
163+ return button
164+ }
165+
166+ function renderObject ( obj ) {
167+ if ( typeof obj !== 'object' || obj === null ) {
168+ return createValueSpan ( obj )
169+ }
170+
171+ const details = document . createElement ( 'details' )
172+ const detailsId = `d${ detailsCounter ++ } `
173+ details . dataset . id = detailsId
174+
175+ details . addEventListener ( 'toggle' , updateUrlState )
176+
177+ const summary = document . createElement ( 'summary' )
178+ const isArray = Array . isArray ( obj )
179+
180+ const text = document . createElement ( 'span' )
181+ text . textContent = isArray ?
182+ `Array (${ obj . length } items)` :
183+ `Object (${ Object . keys ( obj ) . length } properties)`
184+
185+ summary . appendChild ( text )
186+ summary . appendChild ( createExpandAllButton ( details ) )
187+ details . appendChild ( summary )
188+
189+ const items = isArray ? obj : Object . entries ( obj )
190+
191+ items . forEach ( ( item , index ) => {
192+ const div = document . createElement ( 'div' )
193+ if ( isArray ) {
194+ div . appendChild ( renderObject ( item ) )
195+ } else {
196+ const [ key , value ] = item
197+ const keySpan = document . createElement ( 'span' )
198+ keySpan . className = 'key'
199+ keySpan . textContent = `${ key } : `
200+ div . appendChild ( keySpan )
201+ div . appendChild ( renderObject ( value ) )
202+ }
203+ details . appendChild ( div )
204+ } )
205+
206+ return details
207+ }
208+
209+ function showError ( message ) {
210+ const error = document . createElement ( 'div' )
211+ error . className = 'error'
212+ error . textContent = message
213+ output . appendChild ( error )
214+ }
215+
216+ function updateUrlState ( ) {
217+ const url = urlInput . value
218+ const openDetails = [ ...document . querySelectorAll ( 'details[data-id]' ) ]
219+ . filter ( d => d . open )
220+ . map ( d => d . dataset . id )
221+
222+ const state = {
223+ url : url || undefined ,
224+ open : openDetails . length ? openDetails : undefined
225+ }
226+
227+ const fragment = '#' + btoa ( JSON . stringify ( state ) )
228+ window . history . replaceState ( null , '' , fragment )
229+ }
230+
231+ function parseUrlState ( ) {
232+ try {
233+ const fragment = window . location . hash . slice ( 1 )
234+ if ( ! fragment ) return { }
235+ return JSON . parse ( atob ( fragment ) )
236+ } catch ( e ) {
237+ console . warn ( 'Failed to parse URL state:' , e )
238+ return { }
239+ }
240+ }
241+
242+ async function loadYaml ( yaml ) {
243+ try {
244+ output . innerHTML = ''
245+ detailsCounter = 0
246+
247+ if ( ! yaml ) return
248+
249+ const data = load ( yaml )
250+ const tree = renderObject ( data )
251+ output . appendChild ( tree )
252+
253+ // Restore open state from URL if present
254+ const state = parseUrlState ( )
255+ if ( state . open ) {
256+ state . open . forEach ( id => {
257+ const details = document . querySelector ( `details[data-id="${ id } "]` )
258+ if ( details ) details . open = true
259+ } )
260+ }
261+ } catch ( err ) {
262+ showError ( `Error parsing YAML: ${ err . message } ` )
263+ }
264+ }
265+
266+ async function fetchAndLoadUrl ( url ) {
267+ if ( ! url ) return
268+
269+ try {
270+ const response = await fetch ( url )
271+ if ( ! response . ok ) {
272+ throw new Error ( `HTTP error! status: ${ response . status } ` )
273+ }
274+ const yaml = await response . text ( )
275+ textarea . value = yaml
276+ await loadYaml ( yaml )
277+ updateUrlState ( )
278+ } catch ( err ) {
279+ showError ( `Error fetching YAML: ${ err . message } ` )
280+ }
281+ }
282+
283+ textarea . addEventListener ( 'input' , ( ) => {
284+ loadYaml ( textarea . value . trim ( ) )
285+ updateUrlState ( )
286+ } )
287+
288+ const loadButton = document . querySelector ( 'button' )
289+ loadButton . addEventListener ( 'click' , ( ) => {
290+ fetchAndLoadUrl ( urlInput . value )
291+ } )
292+
293+ // Initial load from URL state
294+ const initialState = parseUrlState ( )
295+ if ( initialState . url ) {
296+ urlInput . value = initialState . url
297+ fetchAndLoadUrl ( initialState . url )
298+ }
299+ </ script >
300+ </ body >
301+ </ html >
0 commit comments