@@ -62,6 +62,10 @@ interface DemoRecord {
6262 files ?: DemoFile [ ] ;
6363}
6464
65+ interface WarningMessage {
66+ warning : string ;
67+ }
68+
6569const demos : Record < string , DemoRecord > = (
6670 demoContent as { demos : Record < string , DemoRecord > }
6771) . demos ;
@@ -135,10 +139,25 @@ function resolveHljsLanguage(lang: string): string | null {
135139/**
136140 * Parse a `lines` prop like `"10-20"` or `"5"` into `[start, end]` (1-indexed,
137141 * inclusive on both ends). Returns null on invalid input.
142+ *
143+ * Special forms:
144+ * - `"A-"` (trailing dash, no end) — treated as "start to end-of-file"; we
145+ * return `[start, Number.POSITIVE_INFINITY]` and the caller clamps to the
146+ * actual file length.
147+ * - empty string / whitespace — treated as "no range" (equivalent to absent
148+ * `lines`), returning null so the caller renders the full file.
138149 */
139150function parseLineRange ( input : string | undefined ) : [ number , number ] | null {
140151 if ( ! input ) return null ;
141152 const trimmed = input . trim ( ) ;
153+ if ( trimmed === "" ) return null ;
154+ // `A-` → start through end-of-file (caller clamps).
155+ const openEnded = trimmed . match ( / ^ ( \d + ) \s * [ - \u2013 ] \s * $ / ) ;
156+ if ( openEnded ) {
157+ const start = parseInt ( openEnded [ 1 ] , 10 ) ;
158+ if ( start > 0 ) return [ start , Number . POSITIVE_INFINITY ] ;
159+ return null ;
160+ }
142161 const dash = trimmed . match ( / ^ ( \d + ) \s * [ - \u2013 ] \s * ( \d + ) $ / ) ;
143162 if ( dash ) {
144163 const start = parseInt ( dash [ 1 ] , 10 ) ;
@@ -162,20 +181,23 @@ function regionFromFile(
162181 file : DemoFile ,
163182 lines ?: string ,
164183) : Region | WarningMessage {
165- const allLines = file . content . split ( "\n" ) ;
166- if ( ! lines ) {
184+ // Strip a single trailing newline so line counts and copied text are
185+ // consistent regardless of whether a `lines=` range is supplied.
186+ const normalized = file . content . replace ( / \n $ / , "" ) ;
187+ const allLines = normalized . split ( "\n" ) ;
188+ if ( ! lines || lines . trim ( ) === "" ) {
167189 return {
168190 file : file . filename ,
169191 startLine : 1 ,
170192 endLine : allLines . length ,
171- code : file . content . replace ( / \n $ / , "" ) ,
193+ code : normalized ,
172194 language : file . language ,
173195 } ;
174196 }
175197 const range = parseLineRange ( lines ) ;
176198 if ( ! range ) {
177199 return {
178- warning : `Invalid lines="${ lines } " — expected "A-B" or single line "A".` ,
200+ warning : `Invalid lines="${ lines } " — expected "A-B", "A-" (start to end), or single line "A".` ,
179201 } ;
180202 }
181203 const [ start , end ] = range ;
@@ -184,20 +206,24 @@ function regionFromFile(
184206 warning : `lines="${ lines } " is out of range (file has ${ allLines . length } lines).` ,
185207 } ;
186208 }
187- const slice = allLines . slice ( start - 1 , end ) . join ( "\n" ) ;
209+ // Clamp `end` once up front; also warn when the author's explicit range
210+ // drifted past the file so they notice the source changed under them.
211+ const effectiveEnd = Math . min ( end , allLines . length ) ;
212+ if ( Number . isFinite ( end ) && end > allLines . length ) {
213+ console . warn (
214+ `[snippet] lines="${ lines } " end (${ end } ) exceeds file length (${ allLines . length } ) for ${ file . filename } — clamping.` ,
215+ ) ;
216+ }
217+ const slice = allLines . slice ( start - 1 , effectiveEnd ) . join ( "\n" ) ;
188218 return {
189219 file : file . filename ,
190220 startLine : start ,
191- endLine : Math . min ( end , allLines . length ) ,
221+ endLine : effectiveEnd ,
192222 code : slice ,
193223 language : file . language ,
194224 } ;
195225}
196226
197- interface WarningMessage {
198- warning : string ;
199- }
200-
201227function isWarning ( v : Region | WarningMessage ) : v is WarningMessage {
202228 return ( v as WarningMessage ) . warning !== undefined ;
203229}
@@ -297,18 +323,39 @@ export function Snippet({
297323
298324 const hljsLang = resolveHljsLanguage ( reg . language ) ;
299325 let html : string ;
326+ let highlightFailed = false ;
300327 try {
301328 html = hljsLang
302329 ? hljs . highlight ( reg . code , { language : hljsLang , ignoreIllegals : true } )
303330 . value
304331 : hljs . highlightAuto ( reg . code ) . value ;
305- } catch {
332+ } catch ( err ) {
306333 // highlight.js should never throw with ignoreIllegals, but defensively
307- // fall back to unhighlighted text rather than crashing the render.
334+ // fall back to unhighlighted text rather than crashing the render. Log
335+ // enough context that authors can find the offending snippet.
336+ console . warn (
337+ `[snippet] highlight failed for ${ key } ${ reg . file } (language=${ reg . language } )` ,
338+ err ,
339+ ) ;
340+ html = escapeHtml ( reg . code ) ;
341+ highlightFailed = true ;
342+ }
343+ // Defense-in-depth: if hljs ever returns a non-string (unknown edge case),
344+ // fall back to escaped plain text so `dangerouslySetInnerHTML` can't
345+ // receive garbage.
346+ if ( typeof html !== "string" ) {
308347 html = escapeHtml ( reg . code ) ;
348+ highlightFailed = true ;
309349 }
310350
311351 const caption = title ?? reg . file ;
352+ // When highlighting failed we render escaped plain text; drop the `hljs`
353+ // class so the output doesn't get styled as though it were highlighted.
354+ const codeClassName = highlightFailed
355+ ? undefined
356+ : hljsLang
357+ ? `hljs language-${ hljsLang } `
358+ : "hljs" ;
312359
313360 return (
314361 < figure className = "my-5 rounded-lg border border-[var(--border)] overflow-hidden bg-[var(--bg-surface)]" >
@@ -327,7 +374,7 @@ export function Snippet({
327374 ) }
328375 < pre className = "text-[12.5px] leading-[1.55] overflow-x-auto p-4 m-0" >
329376 < code
330- className = { hljsLang ? `hljs language- ${ hljsLang } ` : "hljs" }
377+ className = { codeClassName }
331378 dangerouslySetInnerHTML = { { __html : html } }
332379 />
333380 </ pre >
0 commit comments