Skip to content

Commit 1fe3978

Browse files
Iteration 2: Getting all simple code listings working (#898)
## Description Describe your changes here. Fixes #Issue_Number (if available) ### Ensure that your pull request has followed all the steps below: - [ ] Code compilation - [ ] Created tests which fail without the change (if possible) - [ ] All tests passing - [ ] Extended the README / documentation, if necessary --------- Co-authored-by: Kevin B <Keboo@users.noreply.github.com>
1 parent ae3b748 commit 1fe3978

1 file changed

Lines changed: 90 additions & 35 deletions

File tree

EssentialCSharp.Web/wwwroot/js/trydotnet-module.js

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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*#(?:region|endregion)\s+(?:INCLUDE|EXCLUDE).*$/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
*/
50111
function 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 `
58113
namespace 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 = /namespace\s+\w+/i.test(code);
368425

369-
// Check if it's a class specifically named "Program" with Main method
370-
const isProgramClass = /class\s+Program\s*[\r\n{]/.test(code) &&
426+
// Check if any class has a static Main method
427+
const hasClassWithMain = /class\s+\w+/.test(code) &&
371428
/static\s+(void|async\s+Task)\s+Main\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(/#region\s+INCLUDE\s*\n([\s\S]*?)\n\s*#endregion\s+INCLUDE/);
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

Comments
 (0)