1
- import { component$ , useSignal , useStyles$ , useVisibleTask$ , type QRL } from '@builder.io/qwik' ;
1
+ import {
2
+ component$ ,
3
+ useSignal ,
4
+ useStyles$ ,
5
+ useTask$ ,
6
+ useVisibleTask$ ,
7
+ type QRL ,
8
+ type Signal ,
9
+ } from '@builder.io/qwik' ;
2
10
import { CopyCode } from '../copy-code/copy-code-block' ;
3
11
import styles from './code-block.css?inline' ;
4
12
import { shikiInstance , SHIKI_THEME , type ShikiLangs } from './shiki-config' ;
13
+ import { format } from 'prettier/standalone' ;
14
+ import parserHtml from 'prettier/plugins/html' ;
15
+ import parserTs from 'prettier/plugins/typescript' ;
16
+ import parserEstree from 'prettier/plugins/estree' ;
5
17
6
18
interface CodeBlockProps {
7
19
path ?: string ;
8
20
language ?: ShikiLangs ;
9
21
code : string ;
22
+ format ?: boolean ;
10
23
pathInView$ ?: QRL < ( name : string ) => void > ;
11
24
observerRootId ?: string ;
12
25
}
13
26
14
27
export const CodeBlock = component$ ( ( props : CodeBlockProps ) => {
15
28
const listSig = useSignal < Element > ( ) ;
29
+ const codeSig = useSignal < string | null > ( props . format ? null : props . code ) ;
30
+ const formatSig = useSignal ( ! ! props . format ) ;
31
+ const formatError = useSignal < string > ( ) ;
32
+
33
+ const language =
34
+ props . language ||
35
+ ( props . path
36
+ ? / \. ( [ c m ] ? [ j t ] s x ? | j s o n ) $ / . test ( props . path )
37
+ ? 'javascript'
38
+ : props . path . endsWith ( '.html' )
39
+ ? 'html'
40
+ : null
41
+ : null ) ;
42
+
16
43
useStyles$ ( styles ) ;
17
44
45
+ useTask$ ( async ( { track } ) => {
46
+ track ( ( ) => props . code ) ;
47
+ track ( formatSig ) ;
48
+
49
+ if ( formatSig . value ) {
50
+ try {
51
+ // simple formatting for html and js
52
+ if ( language === 'html' ) {
53
+ codeSig . value = await format ( props . code , {
54
+ parser : 'html' ,
55
+ plugins : [ parserHtml ] ,
56
+ htmlWhitespaceSensitivity : 'ignore' ,
57
+ } ) ;
58
+ } else if ( language === 'javascript' ) {
59
+ codeSig . value = await format ( props . code , {
60
+ parser : 'typescript' ,
61
+ plugins : [ parserTs , parserEstree ] ,
62
+ } ) ;
63
+ }
64
+ formatError . value = undefined ;
65
+ } catch ( e : any ) {
66
+ formatError . value = e . message ;
67
+ codeSig . value = props . code ;
68
+ }
69
+ } else {
70
+ codeSig . value = props . code ;
71
+ }
72
+ } ) ;
73
+
18
74
useVisibleTask$ ( async ( ) => {
19
75
const { pathInView$, path, observerRootId } = props ;
20
76
if ( pathInView$ && path && listSig . value !== undefined ) {
@@ -33,23 +89,37 @@ export const CodeBlock = component$((props: CodeBlockProps) => {
33
89
}
34
90
} ) ;
35
91
36
- let language = props . language ;
37
- if ( ! language && props . path && props . code ) {
38
- const ext = props . path . split ( '.' ) . pop ( ) ;
39
- language = ext === 'js' || ext === 'json' ? 'javascript' : ext === 'css' ? 'css' : 'html' ;
40
- }
41
-
42
- const highlighted = shikiInstance . codeToHtml ( props . code , {
43
- lang : language ! ,
44
- theme : SHIKI_THEME ,
45
- } ) ;
92
+ const highlighted =
93
+ codeSig . value != null &&
94
+ shikiInstance . codeToHtml ( codeSig . value , {
95
+ lang : language ! ,
96
+ theme : SHIKI_THEME ,
97
+ } ) ;
46
98
const className = `language-${ language } ` ;
47
99
return (
48
100
< div class = "relative" >
49
101
< pre class = { className } ref = { listSig } >
50
- < code class = { className } dangerouslySetInnerHTML = { highlighted } />
102
+ { highlighted && < code class = { className } dangerouslySetInnerHTML = { highlighted } /> }
51
103
</ pre >
104
+ { ( language === 'html' || language === 'javascript' ) && (
105
+ < PrettierToggle bind :value = { formatSig } />
106
+ ) }
52
107
< CopyCode code = { props . code } />
53
108
</ div >
54
109
) ;
55
110
} ) ;
111
+
112
+ const PrettierToggle = component$ ( ( props : { 'bind:value' : Signal < boolean > ; error ?: string } ) => {
113
+ return (
114
+ < label
115
+ class = "prettier-toggle"
116
+ title = { `Toggle Prettier ${ props . error ? `\n${ props . error } ` : '' } ` }
117
+ aria-label = "Toggle Prettier"
118
+ >
119
+ < input type = "checkbox" bind :checked = { props [ 'bind:value' ] } style = "display: none;" />
120
+ < span class = { [ props [ 'bind:value' ] . value ? 'checked' : '' , props . error ? 'error' : '' ] } >
121
+ P
122
+ </ span >
123
+ </ label >
124
+ ) ;
125
+ } ) ;
0 commit comments