@@ -42,19 +42,74 @@ function isTryDotNetConfigured() {
4242 return typeof origin === 'string' && origin . trim ( ) . length > 0 ;
4343}
4444
45+ // ── Runnable-listings data (loaded once from chapter-listings.json) ──────────
46+
47+ /** @type {Promise<Set<string>>|null } */
48+ let _runnableListingsPromise = null ;
49+
50+ /**
51+ * Loads chapter-listings.json (once) and builds a Set of normalised
52+ * "chapter.listing" keys, e.g. "1.3", "12.50".
53+ * Only includes listings where both can_compile and can_run are true.
54+ * @returns {Promise<Set<string>> }
55+ */
56+ function loadRunnableListings ( ) {
57+ if ( _runnableListingsPromise ) return _runnableListingsPromise ;
58+
59+ _runnableListingsPromise = fetch ( '/js/chapter-listings.json' )
60+ . then ( res => {
61+ if ( ! res . ok ) {
62+ const msg = res . status === 404
63+ ? 'chapter-listings.json not found (404). The NuGet content package may not be restored — Run buttons will not be shown.'
64+ : `Failed to load chapter-listings.json: ${ res . status } ` ;
65+ throw new Error ( msg ) ;
66+ }
67+ return res . json ( ) ;
68+ } )
69+ . then ( data => {
70+ const set = new Set ( ) ;
71+ const chapters = data . chapters || { } ;
72+ for ( const [ , files ] of Object . entries ( chapters ) ) {
73+ for ( const fileObj of files ) {
74+ // fileObj is now { filename: "01.03.cs", can_compile: true, can_run: true }
75+ if ( ! fileObj . can_compile || ! fileObj . can_run ) continue ; // Skip listings that can't compile or can't run
76+
77+ const filename = fileObj . filename ;
78+ // filename looks like "01.03.cs" → chapter 1, listing 3
79+ const m = filename . match ( / ^ ( \d + ) \. ( \d + ) \. / ) ;
80+ if ( m ) {
81+ set . add ( `${ parseInt ( m [ 1 ] , 10 ) } .${ parseInt ( m [ 2 ] , 10 ) } ` ) ;
82+ }
83+ }
84+ }
85+ return set ;
86+ } )
87+ . catch ( err => {
88+ console . warn ( 'Could not load runnable listings:' , err ) ;
89+ _runnableListingsPromise = null ; // Allow retry on next call (e.g. transient network error)
90+ return new Set ( ) ; // graceful degradation — no Run buttons
91+ } ) ;
92+
93+ return _runnableListingsPromise ;
94+ }
95+
96+ /**
97+ * Strips #region / #endregion directive lines (INCLUDE, EXCLUDE, etc.)
98+ * from source code while keeping the code between them intact.
99+ * @param {string } code - Raw source code
100+ * @returns {string } Code with region directive lines removed
101+ */
102+ function stripRegionDirectives ( code ) {
103+ return code . replace ( / ^ \s * # (?: r e g i o n | e n d r e g i o n ) \s + (?: I N C L U D E | E X C L U D E ) .* $ / gm, '' ) . trim ( ) ;
104+ }
105+
45106/**
46107 * Creates scaffolding for user code to run in the TryDotNet environment.
47108 * @param {string } userCode - The user's C# code to wrap
48109 * @returns {string } Scaffolded code with proper using statements and Main method
49110 */
50111function createScaffolding ( userCode ) {
51- return `using System;
52- using System.Collections.Generic;
53- using System.Linq;
54- using System.Text;
55- using System.Globalization;
56- using System.Text.RegularExpressions;
57-
112+ return `
58113namespace Program
59114{
60115 class Program
@@ -222,7 +277,7 @@ export function useTryDotNet() {
222277 const files = [ { name : fileName , content : fileContent } ] ;
223278 const project = { package : 'console' , files : files } ;
224279 const document = isComplete
225- ? { fileName : fileName }
280+ ? fileName
226281 : { fileName : fileName , region : 'controller' } ;
227282
228283 const configuration = {
@@ -357,37 +412,22 @@ export function useTryDotNet() {
357412
358413 /**
359414 * Checks if code is a complete C# program that doesn't need scaffolding.
360- * Complete programs must have a namespace declaration with class and Main,
361- * or be a class named Program with Main.
415+ * A program is "complete" when it contains a namespace declaration, OR
416+ * when it defines any class with a static Main method.
417+ * Top-level statement files (no class, no namespace) return false and
418+ * will be wrapped by createScaffolding().
362419 * @param {string } code - Source code to check
363420 * @returns {boolean } True if code is complete, false if it needs scaffolding
364421 */
365422 function isCompleteProgram ( code ) {
366423 // Check for explicit namespace declaration (most reliable indicator)
367424 const hasNamespace = / n a m e s p a c e \s + \w + / i. test ( code ) ;
368425
369- // Check if it's a class specifically named "Program" with Main method
370- const isProgramClass = / c l a s s \s + P r o g r a m \s * [ \r \n { ] / . test ( code ) &&
426+ // Check if any class has a static Main method
427+ const hasClassWithMain = / c l a s s \s + \w + / . test ( code ) &&
371428 / s t a t i c \s + ( v o i d | a s y n c \s + T a s k ) \s + M a i n \s * \( / . test ( code ) ;
372429
373- // Only consider it complete if it has namespace or is the Program class
374- return hasNamespace || isProgramClass ;
375- }
376-
377- /**
378- * Extracts executable code snippet from source code.
379- * If code contains #region INCLUDE, extracts only that portion.
380- * Otherwise returns the full code.
381- * @param {string } code - Source code to process
382- * @returns {string } Extracted code snippet
383- */
384- function extractCodeSnippet ( code ) {
385- // Extract code from #region INCLUDE if present
386- const regionMatch = code . match ( / # r e g i o n \s + I N C L U D E \s * \n ( [ \s \S ] * ?) \n \s * # e n d r e g i o n \s + I N C L U D E / ) ;
387- if ( regionMatch ) {
388- return regionMatch [ 1 ] . trim ( ) ;
389- }
390- return code ;
430+ return hasNamespace || hasClassWithMain ;
391431 }
392432
393433 /**
@@ -403,8 +443,16 @@ export function useTryDotNet() {
403443 }
404444 const data = await response . json ( ) ;
405445 const code = data . content || '' ;
406- // Extract the snippet portion if it has INCLUDE regions
407- return extractCodeSnippet ( code ) ;
446+
447+ // Complete programs (namespace or class+Main) are sent as-is, but
448+ // with common usings prepended when the file has none — TryDotNet's
449+ // 'console' package does not provide SDK implicit global usings.
450+ // Top-level statement files get region directives stripped so the
451+ // scaffolding wrapper doesn't contain raw #region lines.
452+ if ( isCompleteProgram ( code ) ) {
453+ return code ;
454+ }
455+ return stripRegionDirectives ( code ) ;
408456 }
409457
410458 /**
@@ -502,12 +550,19 @@ export function useTryDotNet() {
502550 /**
503551 * Injects Run buttons into code block sections.
504552 * Skipped entirely when TryDotNet origin is not configured.
553+ * Only adds buttons for listings present in chapter-listings.json.
505554 */
506- function injectRunButtons ( ) {
555+ async function injectRunButtons ( ) {
507556 if ( ! isTryDotNetConfigured ( ) ) {
508557 return ; // Don't show Run buttons when the service is not configured
509558 }
510559
560+ // Pre-load the runnable listings set so we can check membership below
561+ const runnableSet = await loadRunnableListings ( ) ;
562+ if ( runnableSet . size === 0 ) {
563+ return ; // JSON failed to load or is empty — no buttons
564+ }
565+
511566 const codeBlocks = document . querySelectorAll ( '.code-block-section' ) ;
512567
513568 codeBlocks . forEach ( ( block ) => {
@@ -553,8 +608,8 @@ export function useTryDotNet() {
553608 } ) ;
554609 }
555610
556- // Only add button for listing 1.1
557- if ( chapter === '1' && listing === '1' ) {
611+ // Only add button for listings present in the curated JSON
612+ if ( chapter && listing && runnableSet . has ( ` ${ parseInt ( chapter , 10 ) } . ${ parseInt ( listing , 10 ) } ` ) ) {
558613 // Wrap existing content in a span to keep it together
559614 const contentWrapper = document . createElement ( 'span' ) ;
560615 while ( heading . firstChild ) {
0 commit comments