1- import React , { useState } from 'react' ;
1+ import React , { useEffect , useState } from 'react' ;
22import { NavLink , useLocation } from 'react-router-dom' ;
3+ import hljs from 'highlight.js/lib/core' ;
4+ import bash from 'highlight.js/lib/languages/bash' ;
5+ import dockerfile from 'highlight.js/lib/languages/dockerfile' ;
6+ import ini from 'highlight.js/lib/languages/ini' ;
7+ import javascript from 'highlight.js/lib/languages/javascript' ;
8+ import json from 'highlight.js/lib/languages/json' ;
9+ import powershell from 'highlight.js/lib/languages/powershell' ;
10+ import python from 'highlight.js/lib/languages/python' ;
11+ import typescript from 'highlight.js/lib/languages/typescript' ;
12+ import xml from 'highlight.js/lib/languages/xml' ;
13+ import yaml from 'highlight.js/lib/languages/yaml' ;
14+
15+ hljs . registerLanguage ( 'bash' , bash ) ;
16+ hljs . registerLanguage ( 'dockerfile' , dockerfile ) ;
17+ hljs . registerLanguage ( 'ini' , ini ) ;
18+ hljs . registerLanguage ( 'javascript' , javascript ) ;
19+ hljs . registerLanguage ( 'json' , json ) ;
20+ hljs . registerLanguage ( 'powershell' , powershell ) ;
21+ hljs . registerLanguage ( 'python' , python ) ;
22+ hljs . registerLanguage ( 'typescript' , typescript ) ;
23+ hljs . registerLanguage ( 'xml' , xml ) ;
24+ hljs . registerLanguage ( 'yaml' , yaml ) ;
325
426interface SidebarSection {
527 title : string ;
@@ -9,7 +31,7 @@ interface SidebarSection {
931
1032const sections : SidebarSection [ ] = [
1133 {
12- title : 'Getting Started ' ,
34+ title : 'Nihil Wrapper ' ,
1335 icon : (
1436 < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
1537 < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.63 8.41m5.96 5.96a14.926 14.926 0 01-5.84 2.58m0 0a14.926 14.926 0 01-5.84-2.58" />
@@ -19,33 +41,27 @@ const sections: SidebarSection[] = [
1941 { label : 'Linux' , to : '/docs/installation/linux' } ,
2042 { label : 'macOS' , to : '/docs/installation/macos' } ,
2143 { label : 'Windows' , to : '/docs/installation/windows' } ,
22- ] ,
23- } ,
24- {
25- title : 'Usage' ,
26- icon : (
27- < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
28- < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
29- </ svg >
30- ) ,
31- items : [
3244 { label : 'CLI Commands' , to : '/docs/usage' } ,
3345 { label : 'Configuration' , to : '/docs/configuration' } ,
46+ { label : 'Services' , to : '/docs/service' } ,
47+ { label : 'Shell Completion' , to : '/docs/completion' } ,
48+ { label : 'Command History' , to : '/docs/history' } ,
3449 ] ,
3550 } ,
3651 {
37- title : 'Images' ,
52+ title : 'Nihil Images' ,
3853 icon : (
3954 < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
40- < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M21 7.5l- 2.25-1.313M21 7.5v2.25m0-2.25l-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2. 25 1.313M3 7.5v2.25m9 3l2 .25-1.313M12 12.75l- 2.25-1.313M12 12.75V15m0 6.75l2.25-1.313M12 21.75V19.5m0 2.25l -2.25-1.313m0-16.875L12 2.25l2. 25 1.313M21 14.25v2.25l- 2.25 1.313m-13.5 0L3 16.5v-2.25 " />
55+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2. 25 0 0021 18V6a2 .25 2.25 0 00-2.25 -2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z " />
4156 </ svg >
4257 ) ,
4358 items : [
4459 { label : 'Available Images' , to : '/docs/images' } ,
60+ { label : 'Architecture' , to : '/docs/architecture' } ,
4561 ] ,
4662 } ,
4763 {
48- title : 'nihil-history ' ,
64+ title : 'Nihil History ' ,
4965 icon : (
5066 < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
5167 < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
@@ -56,39 +72,16 @@ const sections: SidebarSection[] = [
5672 ] ,
5773 } ,
5874 {
59- title : 'Features' ,
60- icon : (
61- < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
62- < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
63- </ svg >
64- ) ,
65- items : [
66- { label : 'Shell Completion' , to : '/docs/completion' } ,
67- { label : 'Command History' , to : '/docs/history' } ,
68- ] ,
69- } ,
70- {
71- title : 'Project' ,
75+ title : 'About' ,
7276 icon : (
7377 < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
7478 < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
7579 </ svg >
7680 ) ,
7781 items : [
78- { label : 'Architecture' , to : '/docs/architecture' } ,
79- { label : 'Contributing' , to : '/docs/contributing' } ,
80- ] ,
81- } ,
82- {
83- title : 'More' ,
84- icon : (
85- < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
86- < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 1.5 } d = "M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
87- </ svg >
88- ) ,
89- items : [
82+ { label : 'About Nihil' , to : '/docs/about' } ,
9083 { label : 'FAQ' , to : '/docs/faq' } ,
91- { label : 'About ' , to : '/docs/about ' } ,
84+ { label : 'Contributing ' , to : '/docs/contributing ' } ,
9285 ] ,
9386 } ,
9487] ;
@@ -154,6 +147,118 @@ export const DocsLayout: React.FC<{ children: React.ReactNode }> = ({ children }
154147 const isInSection = ( section : SidebarSection ) =>
155148 section . items . some ( ( item ) => location . pathname === item . to ) ;
156149
150+ useEffect ( ( ) => {
151+ const candidates = [ 'bash' , 'powershell' , 'json' , 'yaml' , 'python' , 'xml' , 'javascript' , 'typescript' , 'ini' , 'dockerfile' ] ;
152+ const commandLineRe = / ^ \s * ( \$ | # ) ? \s * [ a - z A - Z 0 - 9 _ . - ] + (?: \s + [ - ] [ - \w ] + | \s + [ A - Z a - z 0 - 9 _ . / ~ : @ ' " = + - ] + ) * \s * $ / ;
153+ const terminalPrefixes = [ 'nihil ' , 'nhi ' , 'nxh ' , 'docker ' , 'git ' , 'pip ' , 'pipx ' , 'python ' , 'npm ' , 'pnpm ' , 'yarn ' , 'curl ' , 'wget ' , 'sudo ' ] ;
154+ const looksLikeTerminal = ( text : string ) => {
155+ const lines = text
156+ . split ( '\n' )
157+ . map ( ( line ) => line . trim ( ) )
158+ . filter ( Boolean ) ;
159+ if ( ! lines . length ) return false ;
160+ const matches = lines . filter ( ( line ) => {
161+ const lower = line . toLowerCase ( ) ;
162+ if ( lower . startsWith ( '#' ) ) return true ;
163+ if ( terminalPrefixes . some ( ( prefix ) => lower . startsWith ( prefix ) ) ) return true ;
164+ return commandLineRe . test ( line ) ;
165+ } ) . length ;
166+ return matches / lines . length >= 0.6 ;
167+ } ;
168+ const escapeHtml = ( value : string ) =>
169+ value
170+ . replaceAll ( '&' , '&' )
171+ . replaceAll ( '<' , '<' )
172+ . replaceAll ( '>' , '>' ) ;
173+ const renderTerminalHtml = ( text : string ) => {
174+ const lines = text . split ( '\n' ) ;
175+ return lines
176+ . map ( ( rawLine ) => {
177+ const line = rawLine . trim ( ) ;
178+ if ( ! line ) return '<span class="cmd-line"></span>' ;
179+ if ( line . startsWith ( '#' ) ) {
180+ return `<span class="cmd-comment">${ escapeHtml ( rawLine ) } </span>` ;
181+ }
182+ const tokens = line . split ( / \s + / ) ;
183+ let prompt = '' ;
184+ let command = tokens [ 0 ] ?? '' ;
185+ let rest = tokens . slice ( 1 ) ;
186+ if ( ( command === '$' || command === '#' ) && tokens . length > 1 ) {
187+ prompt = command ;
188+ command = tokens [ 1 ] ;
189+ rest = tokens . slice ( 2 ) ;
190+ }
191+ const promptPart = prompt
192+ ? `<span class="cmd-prompt">${ escapeHtml ( prompt ) } </span> `
193+ : '' ;
194+ const commandPart = `<span class="cmd-bin">${ escapeHtml ( command ) } </span>` ;
195+ const argsPart = rest . length
196+ ? ` <span class="cmd-args">${ escapeHtml ( rest . join ( ' ' ) ) } </span>`
197+ : '' ;
198+ return `<span class="cmd-line">${ promptPart } ${ commandPart } ${ argsPart } </span>` ;
199+ } )
200+ . join ( '' ) ;
201+ } ;
202+
203+ const blocks = document . querySelectorAll ( '.docs-content pre' ) ;
204+ blocks . forEach ( ( pre ) => {
205+ if ( pre . dataset . enhanced === '1' ) return ;
206+ pre . dataset . enhanced = '1' ;
207+
208+ let code = pre . querySelector ( 'code' ) ;
209+ if ( ! code ) {
210+ code = document . createElement ( 'code' ) ;
211+ code . textContent = pre . textContent ?? '' ;
212+ pre . textContent = '' ;
213+ pre . appendChild ( code ) ;
214+ }
215+
216+ const source = code . textContent ?? '' ;
217+ const isTerminal = looksLikeTerminal ( source ) ;
218+ const highlighted = isTerminal
219+ ? { value : renderTerminalHtml ( source ) }
220+ : hljs . highlightAuto ( source , candidates ) ;
221+ const language = isTerminal ? 'shell' : ( highlighted . language ?? 'text' ) ;
222+ code . innerHTML = highlighted . value ;
223+ code . classList . add ( 'hljs' , `language-${ language } ` ) ;
224+
225+ const wrapper = document . createElement ( 'div' ) ;
226+ wrapper . className = 'code-shell' ;
227+ pre . parentNode ?. insertBefore ( wrapper , pre ) ;
228+ wrapper . appendChild ( pre ) ;
229+
230+ const header = document . createElement ( 'div' ) ;
231+ header . className = 'code-shell-head' ;
232+
233+ const label = document . createElement ( 'span' ) ;
234+ label . className = 'code-shell-lang' ;
235+ label . textContent = language ;
236+
237+ const button = document . createElement ( 'button' ) ;
238+ button . type = 'button' ;
239+ button . className = 'code-shell-copy' ;
240+ button . textContent = 'Copy' ;
241+ button . onclick = async ( ) => {
242+ try {
243+ await navigator . clipboard . writeText ( source ) ;
244+ button . textContent = 'Copied' ;
245+ window . setTimeout ( ( ) => {
246+ button . textContent = 'Copy' ;
247+ } , 1200 ) ;
248+ } catch {
249+ button . textContent = 'Error' ;
250+ window . setTimeout ( ( ) => {
251+ button . textContent = 'Copy' ;
252+ } , 1200 ) ;
253+ }
254+ } ;
255+
256+ header . append ( label , button ) ;
257+ wrapper . prepend ( header ) ;
258+ pre . classList . add ( 'code-shell-pre' ) ;
259+ } ) ;
260+ } , [ location . pathname ] ) ;
261+
157262 return (
158263 < div className = "flex gap-12 items-start" >
159264 < aside className = "w-60 shrink-0 sticky top-24 hidden md:block" >
@@ -170,7 +275,7 @@ export const DocsLayout: React.FC<{ children: React.ReactNode }> = ({ children }
170275 ) ) }
171276 </ div >
172277 </ aside >
173- < div className = "flex-1 min-w-0" > { children } </ div >
278+ < div className = "flex-1 min-w-0 docs-content " > { children } </ div >
174279 </ div >
175280 ) ;
176281} ;
0 commit comments