1+ import React , { useMemo , useState , useEffect , useCallback } from 'react'
2+ import EventEmitter3 from 'eventemitter3'
3+ import ClipboardJS from 'clipboard'
4+ import { render } from 'react-dom'
5+ import DOMPurify from 'dompurify'
6+ import { html } from 'htm/react'
7+ import halfmoon from 'halfmoon'
8+ import slugify from 'slugify'
9+ import marked from 'marked'
10+
11+ // Syntax highlighting imports
12+ import { getHighlighterCore } from 'shikiji/core'
13+ import dracula from 'shikiji/themes/dracula.mjs'
14+ import { getWasmInlined } from 'shikiji/wasm'
15+ import yaml from 'shikiji/langs/yaml.mjs'
16+
17+ const supportedLangs = [ 'yaml' ] ;
18+ const bus = new EventEmitter3 ( ) ;
19+ window . bus = bus ;
20+
21+ const clipboard = new ClipboardJS ( '.copy-url' ) ;
22+ clipboard . on ( 'success' , e => {
23+ halfmoon . initStickyAlert ( {
24+ content : "Copied Link!" ,
25+ timeShown : 2000
26+ } )
27+ } ) ;
28+
29+ const highlighter = await getHighlighterCore ( {
30+ loadWasm : getWasmInlined ,
31+ themes : [ dracula ] ,
32+ langs : [ yaml ] ,
33+ } )
34+
35+ marked . use ( {
36+ renderer : {
37+ code : ( code , lang , escaped ) => {
38+ if ( lang === undefined || ! supportedLangs . includes ( lang ) ) {
39+ return `<pre><code>${ code } </code></pre>` ;
40+ } else {
41+ const tokenizedCode = highlighter . codeToHtml ( code ) ;
42+ return `<pre class="language-${ lang } "><code class="language-${ lang } ">${ tokenizedCode } </code></pre>` ;
43+ }
44+ }
45+ }
46+ } )
47+
48+ const { Kind, Group, Version, Schema } = JSON . parse ( document . getElementById ( 'pageData' ) . textContent ) ;
49+
50+ const properties = Schema . Properties ;
51+ if ( properties ?. apiVersion ) delete properties . apiVersion ;
52+ if ( properties ?. kind ) delete properties . kind ;
53+ if ( properties ?. metadata ?. Type == "object" ) delete properties . metadata ;
54+
55+ function getDescription ( schema ) {
56+ let desc = schema . Description || '' ;
57+ if ( desc . trim ( ) == '' ) {
58+ desc = '_No Description Provided._'
59+ }
60+ return DOMPurify . sanitize ( marked ( desc ) ) ;
61+ }
62+
63+ function CRD ( ) {
64+ const expandAll = useCallback ( ( ) => bus . emit ( 'expand-all' ) , [ ] ) ;
65+ const collapseAll = useCallback ( ( ) => bus . emit ( 'collapse-all' ) , [ ] ) ;
66+
67+ // this used to go under the codeblock, but our descriptions are a bit useless at the moment
68+ // <p class="font-size-18">${React.createElement('div', { dangerouslySetInnerHTML: { __html: getDescription(Schema) } }) }</p>
69+
70+ const gvkCode = `apiVersion: ${ Group } /${ Version } \nkind: ${ Kind } ` ;
71+ const gvkTokens = highlighter . codeToHtml ( gvkCode , { lang : 'yaml' , theme : 'dracula' } ) ;
72+
73+ return html `
74+ < div class ="parts d-md-flex justify-content-between mt-md-20 mb-md-20 ">
75+ < ${ PartLabel } type ="Kind" value=${ Kind } />
76+ < ${ PartLabel } type ="Group" value=${ Group } />
77+ < ${ PartLabel } type ="Version" value=${ Version } />
78+ </ div >
79+
80+ < hr class ="mb-md-20 " />
81+ ${ React . createElement ( "div" , { dangerouslySetInnerHTML : { __html : DOMPurify . sanitize ( gvkTokens ) } } ) }
82+
83+ < div class ="${ properties == null ? 'd-none' : 'd-flex' } flex-row-reverse mb-10 mt-10 ">
84+ < button class ="btn ml-10 " type ="button " onClick =${ expandAll } > + expand all</ button >
85+ < button class ="btn " type ="button " onClick =${ collapseAll } > - collapse all</ button >
86+ </ div >
87+ < div class ="collapse-group ">
88+ ${ properties != null
89+ ? Object . keys ( properties ) . map ( prop => SchemaPart ( { key : prop , property : properties [ prop ] } ) )
90+ : html `
91+ < p class ="font-size-18 ">
92+ This CRD has an empty or unspecified schema.
93+ </ p >
94+ `
95+ }
96+ </ div >
97+ ` ;
98+ }
99+
100+ function SchemaPart ( { key, property, parent, parentSlug } ) {
101+ const [ props , propKeys , required , type , schema ] = useMemo ( ( ) => {
102+ let schema = property ;
103+ let props = property . Properties || { } ;
104+
105+ let type = property . Type ;
106+ if ( type === 'array' ) {
107+ const itemsSchema = property . Items . Schema ;
108+ if ( itemsSchema . Type !== 'object' ) {
109+ type = `[]${ itemsSchema . Type } ` ;
110+ } else {
111+ schema = itemsSchema ;
112+ props = itemsSchema . Properties || { } ;
113+ type = `[]object` ;
114+ }
115+ }
116+ let propKeys = Object . keys ( props ) ;
117+
118+ let required = false ;
119+ if ( parent && parent . Required && parent . Required . includes ( key ) ) {
120+ required = true ;
121+ }
122+ return [ props , propKeys , required , type , schema ]
123+ } , [ parent , property ] ) ;
124+
125+ const slug = useMemo ( ( ) => slugify ( ( parentSlug ? `${ parentSlug } -` : '' ) + key ) , [ parentSlug , key ] ) ;
126+ const fullLink = useMemo ( ( ) => {
127+ const url = new URL ( location . href ) ;
128+ url . hash = `#${ slug } ` ;
129+ return url . toJSON ( ) ;
130+ } ) ;
131+ const isHyperlinked = useCallback ( ( ) => location . hash . substring ( 1 ) . startsWith ( slug ) , [ slug ] ) ;
132+
133+ const [ isOpen , setIsOpen ] = useState ( ( key == "spec" && ! parent ) || isHyperlinked ( ) ) ;
134+
135+ useEffect ( ( ) => {
136+ const handleHashChange = ( ) => {
137+ if ( ! isOpen && isHyperlinked ( ) ) {
138+ setIsOpen ( true ) ;
139+ }
140+ } ;
141+ window . addEventListener ( 'hashchange' , handleHashChange ) ;
142+ return ( ) => window . removeEventListener ( 'hashchange' , handleHashChange ) ;
143+ } , [ isOpen ] ) ;
144+
145+ useEffect ( ( ) => {
146+ const collapse = ( ) => setIsOpen ( false ) ;
147+ const expand = ( ) => setIsOpen ( true ) ;
148+ bus . on ( 'collapse-all' , collapse ) ;
149+ bus . on ( 'expand-all' , expand ) ;
150+ return ( ) => {
151+ bus . off ( 'collapse-all' , collapse ) ;
152+ bus . off ( 'expand-all' , expand ) ;
153+ } ;
154+ } , [ ] ) ;
155+
156+ return html `
157+ < details class ="collapse-panel " open ="${ isOpen } " onToggle =${ e => { setIsOpen ( e . target . open ) ; e . stopPropagation ( ) ; } } >
158+ < summary class ="collapse-header position-relative ">
159+ ${ key } < kbd class ="text-muted "> ${ type } </ kbd > ${ required ? html `< span class ="badge badge-primary "> required</ span > ` : '' }
160+ < button class ="btn btn-sm position-absolute right-0 top-0 m-5 copy-url z-10 " type ="button " data-clipboard-text ="${ fullLink } "> 🔗</ button >
161+ </ summary >
162+ < div id ="${ slug } " class ="collapse-content ">
163+ ${ React . createElement ( "div" , { className : 'property-description' , dangerouslySetInnerHTML : { __html : getDescription ( property ) } } ) }
164+ ${ propKeys . length > 0 ? html `< br /> ` : '' }
165+ < div class ="collapse-group ">
166+ ${ propKeys
167+ . map ( propKey => SchemaPart ( {
168+ parent : schema , parentKey : key , key : propKey , property : props [ propKey ] , parentSlug : slug
169+ } ) ) }
170+ </ div >
171+ </ div >
172+ </ details > ` ;
173+ }
174+
175+ function PartLabel ( { type, value } ) {
176+ return html `
177+ < div class ="mt-10 ">
178+ < span class ="font-weight-semibold font-size-24 "> ${ value } </ span >
179+ < br />
180+ < span class ="badge text-muted font-size-12 "> ${ type } </ span >
181+ </ div > ` ;
182+ }
183+
184+ render ( html `< ${ CRD } /> ` , document . querySelector ( '#renderTarget' ) ) ;
0 commit comments