1+ const keyboardSymbols = {
2+ CTRL : '⌃' ,
3+ CONTROL : '⌃' ,
4+ SHIFT : '⇧' ,
5+ ALT : '⎇' ,
6+ META : '⌘' ,
7+ ENTER : '⏎' ,
8+ ESC : '⎋' ,
9+ TAB : '⇥' ,
10+ BACKSPACE : '⌫' ,
11+ DELETE : '⌦' ,
12+ ARROWUP : '↑' ,
13+ ARROWDOWN : '↓' ,
14+ ARROWLEFT : '←' ,
15+ ARROWRIGHT : '→' ,
16+ PAGEUP : '⇞' ,
17+ PAGEDOWN : '⇟' ,
18+ HOME : '↖' ,
19+ END : '↘' ,
20+ SPACE : '␣'
21+ }
22+
23+ document . querySelectorAll ( 'kbd' ) . forEach ( kbd => {
24+ let gotSymbol = false
25+ const originalText = kbd . innerHTML
26+ const text = originalText . split ( " " )
27+ // console.log(text)
28+ const newText = text . map ( text => {
29+ text = text . trim ( )
30+ if ( keyboardSymbols [ text . toUpperCase ( ) ] ) {
31+ text = keyboardSymbols [ text . toUpperCase ( ) ]
32+ gotSymbol = true
33+ }
34+ return text
35+ } )
36+ if ( ! gotSymbol ) return kbd . innerHTML = originalText
37+ kbd . innerHTML = newText . join ( " " )
38+ kbd . setAttribute ( 'aria-label' , `Keyboard shortcut: ${ originalText } ` )
39+ } )
40+
41+ const getCodeType = pre => {
42+ if ( ! pre . firstChild && ! pre . firstChild . classList ) return null
43+ return Array . from ( pre . firstChild . classList ) . find ( cls => cls . startsWith ( 'language-' ) )
44+ }
45+
46+ const createToolsContainer = ( language , codeContent ) => {
47+ const toolsContainer = document . createElement ( 'div' )
48+ const pre = codeContent ?. parentElement
49+ const parentOfPre = pre ?. parentElement
50+ let expandButton = null
51+ let ariaLabel = pre ?. getAttribute ( 'aria-label' ) || 'Code block'
52+
53+ const codeTypeDiv = createTypeDiv ( language )
54+ const copyButton = createCopyButton ( codeContent )
55+ if ( parentOfPre . nodeName . toLowerCase ( ) !== "blockquote" ) {
56+ expandButton = createExpandButton ( toolsContainer )
57+ }
58+
59+ toolsContainer . className = 'tools-container'
60+ ariaLabel += `, ${ copyButton . getAttribute ( 'aria-label' ) } `
61+
62+ toolsContainer . appendChild ( codeTypeDiv )
63+ toolsContainer . appendChild ( copyButton )
64+ if ( expandButton ) {
65+ toolsContainer . appendChild ( expandButton )
66+ ariaLabel += `, ${ expandButton . getAttribute ( 'aria-label' ) } `
67+ }
68+
69+ pre . setAttribute ( 'aria-label' , ariaLabel )
70+ return toolsContainer
71+ }
72+
73+ const createExpandablePre = ( pre ) => {
74+ pre . className = 'expandable'
75+ pre . setAttribute ( 'tabindex' , '0' )
76+ pre . setAttribute ( 'role' , 'region' )
77+ pre . addEventListener ( 'keydown' , ( event ) => {
78+ const expandOption = pre . querySelector ( '#expand-option' )
79+ if ( event . key === 'Enter' ) {
80+ if ( ! expandOption ) return
81+ if ( event . target . id === 'copy-option' ) return
82+ event . currentTarget . classList . toggle ( 'expanded' )
83+ pre . querySelector ( '#expand-option' ) . firstChild . innerText =
84+ pre . classList . contains ( 'expanded' ) ? 'Collapse' : 'Expand'
85+ }
86+
87+ if ( event . key === 'c' && event . ctrlKey ) {
88+ const copyButton = pre . querySelector ( '#copy-option' )
89+ if ( copyButton ) copyButton . click ( )
90+ }
91+ } )
92+ return pre
93+ }
94+
95+ const createExpandButton = ( container ) => {
96+ const expandButton = createInteractiveButton ( {
97+ text : 'Expand' ,
98+ id : 'expand-option' ,
99+ label : 'Press Enter to expand code block or collapse it' ,
100+ defaultState : false ,
101+ } )
102+ const shortcut = createKeyboardShortcut ( {
103+ keyCode : 'Enter' ,
104+ } )
105+
106+ expandButton . addEventListener ( 'click' , ( ) => {
107+ if ( ! container . parentElement . classList . contains ( 'expanded' ) ) {
108+ container . parentElement . classList . add ( 'expanded' )
109+ expandButton . firstChild . innerText = 'Collapse'
110+ expandButton . setAttribute ( 'aria-pressed' , 'true' )
111+ } else {
112+ container . parentElement . classList . remove ( 'expanded' )
113+ expandButton . firstChild . innerText = 'Expand'
114+ expandButton . setAttribute ( 'aria-pressed' , 'false' )
115+ }
116+ } )
117+
118+ expandButton . addEventListener ( 'keydown' , ( event ) => {
119+ if ( event . key === 'Enter' ) {
120+ event . currentTarget . click ( )
121+ }
122+ } )
123+
124+ expandButton . appendChild ( shortcut )
125+
126+ return expandButton
127+ }
128+
129+ const createTypeDiv = language => {
130+ const typeDiv = document . createElement ( 'div' )
131+ typeDiv . className = 'code-type'
132+ typeDiv . innerText = language || 'Code'
133+ return typeDiv
134+ }
135+
136+ const createCopyButton = codeContent => {
137+ const shortcut = createKeyboardShortcut ( {
138+ keyCode : 'c' ,
139+ keyModifier : 'Ctrl'
140+ } )
141+ const copyButton = createInteractiveButton ( {
142+ text : 'Copy' ,
143+ id : 'copy-option' ,
144+ label : 'Press Control + C to copy code to clipboard' ,
145+ defaultState : false ,
146+ } )
147+
148+ copyButton . addEventListener ( 'click' , ( ) => {
149+ navigator . clipboard . writeText ( codeContent . textContent )
150+ . then ( ( ) => {
151+ copyButton . firstChild . innerText = 'Copied!'
152+ setTimeout ( ( ) => {
153+ copyButton . firstChild . innerText = 'Copy'
154+ } , 2000 )
155+ } )
156+ . catch ( err => {
157+ console . error ( 'Failed to copy text: ' , err )
158+ copyButton . firstChild . innerText = 'Error'
159+ } )
160+ } )
161+ copyButton . addEventListener ( 'keydown' , ( event ) => {
162+ if ( event . key === 'Enter' ) {
163+ copyButton . click ( )
164+ }
165+ } )
166+
167+ copyButton . appendChild ( shortcut )
168+
169+ return copyButton
170+ }
171+
172+ const createKeyboardShortcut = ( { keyCode, keyModifier } ) => {
173+ const shortcut = document . createElement ( 'kbd' )
174+ const keyCodeElement = document . createElement ( 'span' )
175+ let keyModifierElement
176+
177+ switch ( keyModifier ) {
178+ case 'Ctrl' :
179+ keyModifierElement = createKeyboardModifier ( {
180+ keyboardSymbol : getKeyboardCode ( "Ctrl" ) ,
181+ keyboardModifier : 'Control'
182+ } )
183+ break ;
184+ case 'Shift' :
185+ keyModifierElement = createKeyboardModifier ( {
186+ keyboardSymbol : getKeyboardCode ( "Shift" ) ,
187+ keyboardModifier : 'Shift'
188+ } )
189+ break ;
190+ }
191+
192+ keyCodeElement . className = 'key-code'
193+ keyCodeElement . innerText = getKeyboardCode ( keyCode )
194+
195+ shortcut . className = 'keyboard-shortcut'
196+ if ( keyModifier ) shortcut . appendChild ( keyModifierElement )
197+ shortcut . appendChild ( keyCodeElement )
198+ return shortcut
199+ }
200+
201+ const getKeyboardCode = ( text ) => {
202+ return keyboardSymbols [ text . toUpperCase ( ) ] || text
203+ }
204+
205+ const createKeyboardModifier = ( { keyboardSymbol, keyboardModifier } ) => {
206+ const keyModifierElement = document . createElement ( 'span' )
207+ const keyModifierText = document . createElement ( 'span' )
208+ const keyModifierSymbol = document . createElement ( 'span' )
209+
210+ keyModifierElement . className = 'key-modifier'
211+
212+ keyModifierText . innerText = keyboardModifier ?? 'Control'
213+ keyModifierText . className = 'sr-only'
214+
215+ keyModifierSymbol . innerText = keyboardSymbol ?? '⌃'
216+ keyModifierSymbol . ariaHidden = true
217+
218+ keyModifierElement . appendChild ( keyModifierText )
219+ keyModifierElement . appendChild ( keyModifierSymbol )
220+
221+ return keyModifierElement
222+ }
223+
224+ const createInteractiveButton = ( { text, id, label, defaultState } = { } ) => {
225+ const button = document . createElement ( 'button' )
226+ const buttonText = document . createElement ( 'span' )
227+ buttonText . innerText = text || 'Button'
228+ button . id = id || 'interactive-button'
229+ button . setAttribute ( 'aria-label' , label || 'Interactive button' )
230+ button . setAttribute ( 'aria-pressed' , defaultState ? 'true' : 'false' )
231+ button . appendChild ( buttonText )
232+ return button
233+ }
234+
235+ const enhancePreElement = pre => {
236+ const codeType = getCodeType ( pre )
237+ if ( codeType ) pre . setAttribute ( 'data-language' , codeType . replace ( 'language-' , '' ) )
238+
239+ const codeContent = pre . querySelector ( 'code' )
240+ const language = pre . getAttribute ( 'data-language' )
241+ const toolsContainer = createToolsContainer ( language , codeContent )
242+
243+ createExpandablePre ( pre )
244+ pre . insertBefore ( toolsContainer , pre . firstChild )
245+ }
246+
1247const checkTableOfContents = ( ) => {
2248 const toc = document . getElementById ( "main" ) . querySelector ( ".table-of-contents" )
3249 const skipToMain = document . querySelector ( 'a[href="#main"]' )
@@ -19,6 +265,7 @@ const createSkipLink = (id, text = "Skip pass the Table of Contents") => {
19265 return skipLink
20266}
21267
268+ document . querySelectorAll ( 'pre' ) . forEach ( enhancePreElement )
22269checkTableOfContents ( )
23270Array . from ( document . links ) . filter ( link => link . hostname != window . location . hostname )
24271 . forEach ( link => link . target = '_blank' )
0 commit comments