1
- import React from "react" ;
1
+ import React , { useEffect , useState } from "react" ;
2
2
import CodeBlock from "@theme/CodeBlock" ;
3
3
4
4
interface SnippetProps extends React . ComponentProps < typeof CodeBlock > {
@@ -8,27 +8,73 @@ interface SnippetProps extends React.ComponentProps<typeof CodeBlock> {
8
8
lines ?: string ;
9
9
omitted_placeholder ?: string ;
10
10
strip_leading_spaces ?: boolean ;
11
+ /**
12
+ * Optional short hash of the content (first N characters of SHA-256),
13
+ * required only when `lines` is specified.
14
+ */
15
+ hash ?: string ;
11
16
}
12
17
13
18
/**
14
19
* A component for rendering a snippet of code, optionally filtering lines,
15
- * showing ellipses for omissions, and stripping all leading spaces.
20
+ * showing ellipses for omissions, stripping leading spaces, and validating hash .
16
21
*/
17
22
const Snippet : React . FC < SnippetProps > = ( {
18
23
children,
19
24
lines,
20
25
omitted_placeholder = "..." ,
21
26
strip_leading_spaces = false ,
27
+ hash,
22
28
...props
23
29
} ) => {
30
+ const [ error , setError ] = useState < string | null > ( null ) ;
31
+
24
32
if ( typeof children !== "string" ) {
25
- console . error (
33
+ throw new Error (
26
34
"Snippet expects children to be a string containing the file content."
27
35
) ;
28
- return null ;
29
36
}
30
37
31
- // Parse the `linesToInclude` metadata string into an array of line numbers.
38
+ /**
39
+ * Utility function to compute the SHA-256 hash of a string.
40
+ * @param content The input string
41
+ * @returns Promise resolving to a hex-encoded hash
42
+ */
43
+ const computeHash = async ( content : string ) : Promise < string > => {
44
+ const encoder = new TextEncoder ( ) ;
45
+ const data = encoder . encode ( content ) ;
46
+ const hashBuffer = await crypto . subtle . digest ( "SHA-256" , data ) ;
47
+ return Array . from ( new Uint8Array ( hashBuffer ) )
48
+ . map ( ( byte ) => byte . toString ( 16 ) . padStart ( 2 , "0" ) )
49
+ . join ( "" ) ;
50
+ } ;
51
+
52
+ useEffect ( ( ) => {
53
+ if ( lines ) {
54
+ computeHash ( children ) . then ( ( computedHash ) => {
55
+ const shortHash = computedHash . slice ( 0 , 7 ) ; // Use 7 characters for the short hash
56
+
57
+ if ( ! hash ) {
58
+ setError (
59
+ `The \`hash\` prop is required when \`lines\` is specified.\n` +
60
+ `Provide the following hash as the \`hash\` prop: ${ shortHash } `
61
+ ) ;
62
+ } else if ( shortHash !== hash ) {
63
+ setError (
64
+ `Snippet hash mismatch.\n` +
65
+ `Specified: ${ hash } , but content is: ${ shortHash } (full hash: ${ computedHash } ).\n` +
66
+ `Check if the line numbers are still relevant and update the hash.`
67
+ ) ;
68
+ }
69
+ } ) ;
70
+ }
71
+ } , [ children , lines , hash ] ) ;
72
+
73
+ if ( error ) {
74
+ throw new Error ( error ) ;
75
+ }
76
+
77
+ // Parse the `lines` metadata string into an array of line numbers.
32
78
const parseLineRanges = ( metaString ?: string ) : number [ ] => {
33
79
if ( ! metaString ) return [ ] ;
34
80
return metaString . split ( "," ) . flatMap ( ( range ) => {
@@ -46,16 +92,27 @@ const Snippet: React.FC<SnippetProps> = ({
46
92
if ( lines . length === 0 ) return content ; // If no specific lines are specified, return full content.
47
93
48
94
const includedContent : string [ ] = [ ] ;
95
+
96
+ // Filter lines and find the minimum indentation
97
+ const selectedLines = lines
98
+ . map ( ( line ) => allLines [ line - 1 ] || "" )
99
+ . filter ( ( line ) => line . trim ( ) . length > 0 ) ; // Ignore blank lines
100
+
101
+ const minIndent = selectedLines . reduce ( ( min , line ) => {
102
+ const indentMatch = line . match ( / ^ ( \s * ) \S / ) ;
103
+ const indentLength = indentMatch ? indentMatch [ 1 ] . length : 0 ;
104
+ return Math . min ( min , indentLength ) ;
105
+ } , Infinity ) ;
106
+
49
107
lines . forEach ( ( line , index ) => {
50
108
if ( index > 0 && lines [ index - 1 ] < line - 1 ) {
51
109
includedContent . push ( omitted_placeholder ) ; // Add placeholder for omitted lines
52
110
}
53
111
54
112
const rawLine = allLines [ line - 1 ] || "" ;
55
- const formattedLine = strip_leading_spaces
56
- ? rawLine . trimStart ( )
57
- : rawLine ;
58
- includedContent . push ( formattedLine ) ;
113
+ const trimmedLine =
114
+ rawLine . trim ( ) . length > 0 ? rawLine . slice ( minIndent ) : rawLine ;
115
+ includedContent . push ( trimmedLine ) ;
59
116
} ) ;
60
117
61
118
// Add placeholder if lines at the end are omitted
0 commit comments