microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
minestarks/circuit-magic

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/npm/qsharp/generate_katas_content.js

834lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4/// <reference lib="es2022"/>
5// @ts-check
6
7/**
8 * Katas Taxonomy
9 *
10 * A Kata is a top-level container of educational items which are used to explain a particular topic.
11 *
12 * This file builds the content for all the Katas. The katas ordering is conveyed by JSON file where each
13 * string in the array represents a folder that contains all the data to build the kata.
14 *
15 * Each Kata is organized in a directory where an index.md file provides a description on how the kata must be composed.
16 */
17
18import {
19 existsSync,
20 mkdirSync,
21 readFileSync,
22 writeFileSync,
23 readdirSync,
24 statSync,
25} from "node:fs";
26import { basename, dirname, join, relative, sep } from "node:path";
27import { fileURLToPath } from "node:url";
28
29import mdit from "markdown-it";
30import { plugin } from "./markdown_latex_plugin.js";
31const md = mdit("commonmark");
32md.use(plugin);
33
34// Set up the Markdown renderer with KaTeX support for validation
35import mk from "@vscode/markdown-it-katex";
36const mdValidator = mdit("commonmark");
37const katexOpts = {
38 enableMathBlockInHtml: true,
39 enableMathInlineInHtml: true,
40 throwOnError: true,
41};
42// @ts-expect-error: This isn't typed correctly for some reason
43mdValidator.use(mk.default, katexOpts);
44
45const validate = true; // Consider making this a command-line option
46let emitHtml = true;
47
48const forceRegeneration =
49 process.argv.includes("--force") || process.argv.includes("-f");
50
51const scriptDirPath = dirname(fileURLToPath(import.meta.url));
52const katasContentPath = join(
53 scriptDirPath,
54 "..",
55 "..",
56 "..",
57 "katas",
58 "content",
59);
60const katasGeneratedContentPath = join(scriptDirPath, "src");
61const contentFileNames = {
62 katasIndex: "index.json",
63 kataMarkdown: "index.md",
64};
65
66function tryGetTitleFromMarkdown(markdown, errorPrefix) {
67 const result = /^# (.*)/.exec(markdown);
68 if (result?.length !== 2)
69 throw new Error(`${errorPrefix}\nCould not get title from markdown`);
70 return result[1];
71}
72
73function tryGetTitleFromSegment(segment, errorPrefix) {
74 // The segment that represents the title can only be a markdown segment.
75 if (segment.type !== "markdown") {
76 throw new Error(
77 `${errorPrefix}\n` +
78 `segment is expected to be the title but found a segment of type '${segment.type}' instead`,
79 );
80 }
81
82 // Check that the segment has just one line.
83 const linesCount = segment.markdown.split(/\r?\n/).length;
84 if (linesCount !== 1) {
85 throw new Error(
86 `${errorPrefix}\n` +
87 `A title segment must be 1 line, but ${linesCount} lines are present\n` +
88 `Hint: is the markdown missing a @[section] macro?`,
89 );
90 }
91 const title = tryGetTitleFromMarkdown(segment.markdown, errorPrefix);
92
93 return title;
94}
95
96function tryParseJSON(json, errorPrefix) {
97 let parsed;
98 try {
99 parsed = JSON.parse(json);
100 } catch (e) {
101 throw new Error(`${errorPrefix}\n${e}`);
102 }
103 return parsed;
104}
105
106function tryReadFile(filePath, errorPrefix) {
107 let content;
108 try {
109 content = readFileSync(filePath, "utf8");
110 } catch (e) {
111 throw new Error(`${errorPrefix}\n${e}`);
112 }
113 return content;
114}
115
116function identifyMissingProperties(properties, required) {
117 return required.filter((property) => !Object.hasOwn(properties, property));
118}
119
120function getSourceId(sourcePath, basePath) {
121 return relative(basePath, sourcePath).replaceAll(sep, "__");
122}
123
124function aggregateSources(paths, globalCodeSources) {
125 const codeSources = [];
126 for (const path of paths) {
127 const id = getSourceId(path, globalCodeSources.basePath);
128 if (!(id in globalCodeSources.sources)) {
129 const code = tryReadFile(path, "Could not read code dependency");
130 globalCodeSources.sources[id] = code;
131 }
132 codeSources.push(id);
133 }
134 return codeSources;
135}
136
137function resolveSvgSegment(properties, baseFolderPath) {
138 const requiredProperties = ["path"];
139 const missingProperties = identifyMissingProperties(
140 properties,
141 requiredProperties,
142 );
143 if (missingProperties.length > 0) {
144 throw new Error(
145 `SVG macro is missing the following properties: ${missingProperties}`,
146 );
147 }
148
149 const svgPath = join(baseFolderPath, properties.path);
150 const svg = tryReadFile(
151 svgPath,
152 `Could not read the contents of the SVG file at ${svgPath}`,
153 );
154
155 // An SVG file is basically an HTML file. If it includes blank lines, this will
156 // cause issues when including in Markdown, as blank lines indicate the end of
157 // HTML content. Check for blank lines within the document.
158 if (/\n\s*\r?\n/.test(svg)) {
159 throw new Error(
160 `SVG file ${svgPath} includes blank lines, which will break the Markdown`,
161 );
162 }
163
164 properties["svg"] = svg;
165}
166
167function resolveEmbeddedContent(segments, baseFolderPath) {
168 for (const segment of segments) {
169 if (segment.type === "svg") {
170 resolveSvgSegment(segment.properties, baseFolderPath);
171 }
172 }
173}
174
175function appendToMarkdownSegment(markdownSegment, segmentToAppend) {
176 if (segmentToAppend.type === "markdown") {
177 markdownSegment.markdown += "\n" + segmentToAppend.markdown;
178 } else if (segmentToAppend.type === "svg") {
179 markdownSegment.markdown += "\n" + segmentToAppend.properties.svg;
180 } else {
181 throw new Error(
182 `Cannot append segment of type "${segmentToAppend.type}" into markdown segment`,
183 );
184 }
185}
186
187function coalesceIntoSingleMarkdownSegment(startingSegment, segmentsStack) {
188 const markdownSegment = { type: "markdown", markdown: "" };
189 appendToMarkdownSegment(markdownSegment, startingSegment);
190 const isCoalesceSupportedForSegment = (segment) =>
191 segment.type === "markdown" || segment.type === "svg";
192 while (
193 segmentsStack.length > 0 &&
194 isCoalesceSupportedForSegment(segmentsStack.at(-1))
195 ) {
196 const currentSegment = segmentsStack.pop();
197 appendToMarkdownSegment(markdownSegment, currentSegment);
198 }
199
200 return markdownSegment;
201}
202
203function coalesceSegments(segments) {
204 const coalescedSegments = [];
205 const segmentsStack = segments.reverse();
206 while (segmentsStack.length > 0) {
207 let currentSegment = segmentsStack.pop();
208 let coalescedSegment = null;
209 if (currentSegment.type === "markdown" || currentSegment.type === "svg") {
210 coalescedSegment = coalesceIntoSingleMarkdownSegment(
211 currentSegment,
212 segmentsStack,
213 );
214 } else {
215 coalescedSegment = currentSegment;
216 }
217
218 coalescedSegments.push(coalescedSegment);
219 }
220
221 return coalescedSegments;
222}
223
224function preProcessSegments(segments, baseFolderPath) {
225 resolveEmbeddedContent(segments, baseFolderPath);
226 const coalescedSegments = coalesceSegments(segments);
227 return coalescedSegments;
228}
229
230function parseMarkdown(markdown) {
231 const segments = [];
232 const macroRegex = /@\[(?<type>\w+)\]\((?<json>\{.*?\})\)((\r?\n)|$)/gs;
233 let latestProcessedIndex = 0;
234 while (latestProcessedIndex < markdown.length) {
235 const match = macroRegex.exec(markdown);
236 if (match !== null) {
237 // If there is something between the last processed index and the start of the match that is not just whitespace,
238 // it represents a text segment.
239 const delta = match.index - latestProcessedIndex;
240 if (delta > 0) {
241 const textSegment = tryCreateMarkdownSegment(
242 markdown.substring(latestProcessedIndex, match.index),
243 );
244 if (textSegment !== null) {
245 segments.push(textSegment);
246 }
247 }
248
249 // Create a segment that corresponds to the found macro.
250 const macroSegment = createMacroSegment(match);
251 segments.push(macroSegment);
252 latestProcessedIndex = macroRegex.lastIndex;
253 } else {
254 // No more matches were found, create a text segment with the remaining content.
255 const textSegment = tryCreateMarkdownSegment(
256 markdown.substring(latestProcessedIndex, markdown.length),
257 );
258 if (textSegment !== null) {
259 segments.push(textSegment);
260 }
261 latestProcessedIndex = markdown.length;
262 }
263 }
264
265 return segments;
266}
267
268function createExample(baseFolderPath, properties) {
269 // Validate that the data contains the required properties.
270 const requiredProperties = ["id", "codePath"];
271 const missingProperties = identifyMissingProperties(
272 properties,
273 requiredProperties,
274 );
275 if (missingProperties.length > 0) {
276 throw new Error(
277 `Example macro is missing the following properties: ${missingProperties}`,
278 );
279 }
280
281 // Generate the object using the macro properties.
282 const codePath = join(baseFolderPath, properties.codePath);
283 const code = tryReadFile(
284 codePath,
285 `Could not read the contents of the example code file at ${codePath}`,
286 );
287 return {
288 type: "example",
289 id: properties.id,
290 code,
291 };
292}
293
294function createTextContent(markdown) {
295 if (validate) {
296 try {
297 mdValidator.render(markdown);
298 } catch (e) {
299 console.log("LaTeX validation error: ", e);
300 }
301 }
302
303 return {
304 type: "text-content",
305 content: emitHtml ? md.render(markdown) : markdown,
306 };
307}
308
309function createSolution(baseFolderPath, properties) {
310 // Validate that the data contains the required properties.
311 const requiredProperties = ["id", "codePath"];
312 const missingProperties = identifyMissingProperties(
313 properties,
314 requiredProperties,
315 );
316 if (missingProperties.length > 0) {
317 throw new Error(
318 `Solution macro is missing the following properties: ${missingProperties}`,
319 );
320 }
321
322 // Generate the object using the macro properties.
323 const codePath = join(baseFolderPath, properties.codePath);
324 const code = tryReadFile(
325 codePath,
326 `Could not read the contents of the solution code file at ${codePath}`,
327 );
328 return {
329 type: "solution",
330 id: properties.id,
331 code,
332 };
333}
334
335function createExplainedSolution(markdownFilePath) {
336 const markdown = tryReadFile(
337 markdownFilePath,
338 `Could not read solution markdown file at ${markdownFilePath}`,
339 );
340
341 const solutionFolderPath = dirname(markdownFilePath);
342 const rawSegments = parseMarkdown(markdown);
343 const segments = preProcessSegments(rawSegments, solutionFolderPath);
344 const solutionItems = [];
345 for (const segment of segments) {
346 let solutionItem = null;
347 if (segment.type === "example") {
348 solutionItem = createExample(solutionFolderPath, segment.properties);
349 } else if (segment.type === "solution") {
350 solutionItem = createSolution(solutionFolderPath, segment.properties);
351 } else if (segment.type === "markdown") {
352 solutionItem = createTextContent(segment.markdown);
353 }
354
355 if (solutionItem !== null) {
356 solutionItems.push(solutionItem);
357 }
358 }
359
360 return {
361 type: "explained-solution",
362 items: solutionItems,
363 };
364}
365
366function createAnswer(markdownFilePath) {
367 const markdown = tryReadFile(
368 markdownFilePath,
369 `Could not read answer markdown file at ${markdownFilePath}`,
370 );
371
372 const answerFolderPath = dirname(markdownFilePath);
373 const rawSegments = parseMarkdown(markdown);
374 const segments = preProcessSegments(rawSegments, answerFolderPath);
375 const items = [];
376 for (const segment of segments) {
377 let answerItem = null;
378 if (segment.type === "example") {
379 answerItem = createExample(answerFolderPath, segment.properties);
380 } else if (segment.type === "markdown") {
381 answerItem = createTextContent(segment.markdown);
382 }
383
384 if (answerItem !== null) {
385 items.push(answerItem);
386 }
387 }
388
389 return { type: "answer", items };
390}
391
392function createQuestion(kataPath, properties) {
393 // Validate that the data contains the required properties.
394 const requiredProperties = ["descriptionPath", "answerPath"];
395 const missingProperties = identifyMissingProperties(
396 properties,
397 requiredProperties,
398 );
399 if (missingProperties.length > 0) {
400 throw new Error(
401 `Question macro is missing the following properties\n` +
402 `${missingProperties}\n` +
403 `Macro properties:\n` +
404 `${JSON.stringify(properties, undefined, 2)}`,
405 );
406 }
407
408 // Generate the object using the macro properties.
409 const descriptionMarkdown = tryReadFile(
410 join(kataPath, properties.descriptionPath),
411 `Could not read descripton for question ${properties.id}`,
412 );
413 const description = createTextContent(descriptionMarkdown);
414 const answer = createAnswer(join(kataPath, properties.answerPath));
415
416 return {
417 type: "question",
418 description,
419 answer,
420 };
421}
422
423function createExerciseSection(kataPath, properties, globalCodeSources) {
424 // Validate that the data contains the required properties.
425 const requiredProperties = ["id", "title", "path"];
426 const missingProperties = identifyMissingProperties(
427 properties,
428 requiredProperties,
429 );
430 if (missingProperties.length > 0) {
431 throw new Error(
432 `Exercise macro is missing the following properties\n` +
433 `${missingProperties}\n` +
434 `Macro properties:\n` +
435 `${JSON.stringify(properties, undefined, 2)}`,
436 );
437 }
438
439 const exercisePath = join(kataPath, properties.path);
440 // Generate the object using the macro properties.
441 // Get the description from the index.md file in the exercise folder.
442 const descriptionMarkdown = tryReadFile(
443 join(exercisePath, "index.md"),
444 `Could not read index.md file for exercise ${properties.id}`,
445 );
446 const description = createTextContent(descriptionMarkdown);
447
448 // Aggregate the exercise sources. The verification source file is Verification.qs.
449 let resolvedVerificationFile = join(exercisePath, "Verification.qs");
450
451 // Implicit dependencies to simplify dependency handling:
452 // ../KatasLibrary.qs must be included
453 // ./Common.qs must be included if present in current kata
454 const implicitDependencies = ["../KatasLibrary.qs"];
455 if (existsSync(join(kataPath, "./Common.qs"))) {
456 implicitDependencies.push("./Common.qs");
457 }
458
459 const resolvedDependencies = implicitDependencies.map((path) =>
460 join(kataPath, path),
461 );
462 const resolvedSources = [resolvedVerificationFile].concat(
463 resolvedDependencies,
464 );
465 const sourceIds = aggregateSources(resolvedSources, globalCodeSources);
466
467 // Get the placeholder code from the Placeholder.qs file in the exercise folder.
468 const placeholderCode = tryReadFile(
469 join(exercisePath, "Placeholder.qs"),
470 `Could not read Placeholder.qs file for exercise '${properties.id}'`,
471 );
472
473 // Get the solution from the solution.md file in the exercise folder.
474 const explainedSolution = createExplainedSolution(
475 join(exercisePath, "solution.md"),
476 );
477
478 return {
479 type: "exercise",
480 id: properties.id,
481 title: properties.title,
482 description,
483 sourceIds,
484 placeholderCode,
485 explainedSolution,
486 };
487}
488
489function createLessonSection(kataPath, properties, segmentsStack) {
490 // Validate that the data contains the required properties.
491 const requiredProperties = ["id", "title"];
492 const missingProperties = identifyMissingProperties(
493 properties,
494 requiredProperties,
495 );
496 if (missingProperties.length > 0) {
497 throw new Error(
498 `Section macro is missing the following properties\n` +
499 `${missingProperties}\n` +
500 `Macro properties:\n` +
501 `${JSON.stringify(properties, undefined, 2)}`,
502 );
503 }
504
505 // Continue processing segments until another section-delimiting segment appears.
506 const lessonItems = [];
507 const isSectionDelimiterSegment = (segment) =>
508 segment.type === "exercise" || segment.type === "section";
509 while (
510 segmentsStack.length > 0 &&
511 !isSectionDelimiterSegment(segmentsStack.at(-1))
512 ) {
513 const currentSegment = segmentsStack.pop();
514 let lessonItem = null;
515 if (currentSegment.type === "example") {
516 lessonItem = createExample(kataPath, currentSegment.properties);
517 } else if (currentSegment.type === "markdown") {
518 lessonItem = createTextContent(currentSegment.markdown);
519 } else if (currentSegment.type === "question") {
520 lessonItem = createQuestion(kataPath, currentSegment.properties);
521 }
522
523 // Check that a valid lesson item was created.
524 if (lessonItem === null) {
525 throw new Error(
526 `Lesson item could not be generated for segment of type '${currentSegment.type}'\n` +
527 `segment:\n` +
528 `${JSON.stringify(currentSegment, undefined, 2)}`,
529 );
530 }
531
532 lessonItems.push(lessonItem);
533 }
534
535 return {
536 type: "lesson",
537 id: properties.id,
538 title: properties.title,
539 items: lessonItems,
540 };
541}
542
543function createMacroSegment(match) {
544 const type = match.groups.type;
545 const propertiesJson = match.groups.json;
546 const properties = tryParseJSON(
547 propertiesJson,
548 `Invalid JSON for macro of type ${type}.\n` + `JSON: ${propertiesJson}`,
549 );
550 return {
551 type,
552 properties,
553 };
554}
555
556function tryCreateMarkdownSegment(text) {
557 const trimmedText = text.trim();
558 if (trimmedText.length > 0) {
559 return { type: "markdown", markdown: trimmedText };
560 }
561
562 return null;
563}
564
565function createKata(
566 kataPath,
567 id,
568 title,
569 segments,
570 globalCodeSources,
571 published,
572) {
573 // Validate that the kata has at least one segment.
574 if (segments.length === 0) {
575 throw new Error(`Kata '${id}' does not have any segments`);
576 }
577
578 // Create sections from the segments in the stack.
579 // Use the array of segments as a stack to keep track of the segments that have not been processed.
580 const segmentsStack = segments.reverse();
581 const sections = [];
582 while (segmentsStack.length > 0) {
583 const currentSegment = segmentsStack.pop();
584 let section = null;
585 if (currentSegment.type === "exercise") {
586 section = createExerciseSection(
587 kataPath,
588 currentSegment.properties,
589 globalCodeSources,
590 );
591 } else if (currentSegment.type === "section") {
592 section = createLessonSection(
593 kataPath,
594 currentSegment.properties,
595 segmentsStack,
596 );
597 }
598
599 // Check if a valid section was created.
600 if (section === null) {
601 throw new Error(
602 `Unexpexted segment of type '${currentSegment.type}'\n` +
603 `segment:\n` +
604 `${JSON.stringify(currentSegment, undefined, 2)}\n` +
605 `Hint: is the markdown missing a @[section] macro?`,
606 );
607 }
608
609 sections.push(section);
610 }
611
612 return {
613 id,
614 title,
615 sections,
616 published,
617 };
618}
619
620function generateKataContent(path, globalCodeSources, published) {
621 console.log(`- Creating content for kata at: ${path}`);
622 const markdownPath = join(path, contentFileNames.kataMarkdown);
623 const markdown = tryReadFile(
624 markdownPath,
625 "Could not read the contents of the kata markdown file",
626 );
627
628 const kataId = basename(path);
629 const rawSegments = parseMarkdown(markdown);
630
631 // The first segment in the kata must be the title.
632 const firstSegment = rawSegments.at(0);
633 const title = tryGetTitleFromSegment(
634 firstSegment,
635 `Could not get title for kata '${kataId}'`,
636 );
637
638 // Do not use the first segment since it was already processed to get the kata's title.
639 const segments = preProcessSegments(rawSegments.slice(1), path);
640 const kata = createKata(
641 path,
642 kataId,
643 title,
644 segments,
645 globalCodeSources,
646 published,
647 );
648 console.log(
649 `-- '${kata.id}' kata ${kata.published ? "" : "(unpublished)"} was successfully created`,
650 );
651 return kata;
652}
653
654function validateIdsUniqueness(katas) {
655 console.log("Validating IDs uniqueness across all katas");
656 const allIds = new Set();
657 const assertUniqueness = (id) => {
658 const idAlreadyExists = allIds.has(id);
659 if (idAlreadyExists) {
660 throw new Error(`"${id}" is not unique`);
661 }
662 allIds.add(id);
663 };
664
665 for (const kata of katas) {
666 // Check kata IDs are unique.
667 assertUniqueness(kata.id);
668 for (const section of kata.sections) {
669 // Check section IDs are unique.
670 assertUniqueness(section.id);
671 if (section.type === "exercise") {
672 // Check IDs for examples and solutions within exercises are unique.
673 section.explainedSolution.items.forEach((item) => {
674 if (item.type === "example" || item.type === "solution") {
675 assertUniqueness(item.id);
676 }
677 });
678 } else if (section.type === "lesson") {
679 // Check IDs for examples within lessons are unique.
680 section.items.forEach((item) => {
681 if (item.type === "example") {
682 assertUniqueness(item.id);
683 }
684 });
685 }
686 }
687 }
688}
689
690function generateKatasContent(katasPath, outputPath) {
691 console.log("Generating katas content");
692 const indexPath = join(katasPath, contentFileNames.katasIndex);
693 const indexJson = tryReadFile(
694 indexPath,
695 "Could not read the contents of the katas index file",
696 );
697 const publishedKatasDirs = tryParseJSON(
698 indexJson,
699 `Invalid katas index at ${indexPath}`,
700 );
701 const unpublishedKatasDirs = readdirSync(katasPath, { withFileTypes: true })
702 .filter((dirent) => dirent.isDirectory())
703 .map((dirent) => dirent.name)
704 .filter((dir) => !publishedKatasDirs.includes(dir));
705
706 // Unpublished katas are listed after published in alphabetical order
707 const allKatasDirs = publishedKatasDirs.concat(unpublishedKatasDirs);
708
709 // Initialize an object where all the global code sources will be aggregated.
710 const globalCodeSourcesContainer = {
711 basePath: katasPath,
712 sources: {},
713 };
714
715 // Generate an object for each kata and update the global code sources with the code they reference.
716 var katas = [];
717 for (const kataDir of allKatasDirs) {
718 const kataPath = join(katasPath, kataDir);
719 const published = publishedKatasDirs.includes(kataDir);
720 const kata = generateKataContent(
721 kataPath,
722 globalCodeSourcesContainer,
723 published,
724 );
725 katas.push(kata);
726 }
727
728 // Create the objects that will be written to a file.
729 const globalCodeSources = [];
730 for (let id in globalCodeSourcesContainer.sources) {
731 globalCodeSources.push({
732 id: id,
733 code: globalCodeSourcesContainer.sources[id],
734 });
735 }
736
737 // Validate the uniqueness of IDs.
738 validateIdsUniqueness(katas);
739
740 // Save the JS object to a file.
741 const katasContent = {
742 katas: katas,
743 globalCodeSources: globalCodeSources,
744 };
745
746 if (!existsSync(outputPath)) {
747 mkdirSync(outputPath);
748 }
749
750 const contentJsPath = join(
751 outputPath,
752 emitHtml ? "katas-content.generated.ts" : "katas-content.generated.md.ts",
753 );
754 writeFileSync(
755 contentJsPath,
756 `export default ${JSON.stringify(katasContent, undefined, 2)}`,
757 "utf-8",
758 );
759}
760
761function needsRegeneration(katasPath, outputPath) {
762 if (forceRegeneration) {
763 return true;
764 }
765
766 const outputFiles = [
767 join(outputPath, "katas-content.generated.ts"),
768 join(outputPath, "katas-content.generated.md.ts"),
769 ];
770
771 // Check if any output file is missing
772 for (const outputFile of outputFiles) {
773 if (!existsSync(outputFile)) {
774 console.log(`Output file ${outputFile} missing`);
775 return true;
776 }
777 }
778
779 // Get the oldest output file timestamp
780 let oldestOutputTime = Infinity;
781 for (const outputFile of outputFiles) {
782 try {
783 const stat = statSync(outputFile);
784 oldestOutputTime = Math.min(oldestOutputTime, stat.mtime.getTime());
785 } catch {
786 console.log(`Could not stat output file ${outputFile}`);
787 return true; // If we can't stat the file, regenerate
788 }
789 }
790
791 // Check if any input file is newer than the oldest output
792 function checkDirectory(dirPath) {
793 try {
794 const entries = readdirSync(dirPath, { withFileTypes: true });
795 for (const entry of entries) {
796 const fullPath = join(dirPath, entry.name);
797 if (entry.isDirectory()) {
798 if (checkDirectory(fullPath)) return true;
799 } else {
800 try {
801 const stat = statSync(fullPath);
802 if (stat.mtime.getTime() > oldestOutputTime) {
803 console.log(
804 `Input file newer than output: ${relative(process.cwd(), fullPath)}`,
805 );
806 return true;
807 }
808 } catch {
809 // If we can't stat an input file, be safe and regenerate
810 console.log(`Could not stat input file ${fullPath}`);
811 return true;
812 }
813 }
814 }
815 } catch {
816 // If we can't read the directory, be safe and regenerate
817 console.log(`Could not read directory ${dirPath}`);
818 return true;
819 }
820 return false;
821 }
822
823 return checkDirectory(katasPath);
824}
825
826if (needsRegeneration(katasContentPath, katasGeneratedContentPath)) {
827 // Generate HTML and Markdown versions of the katas bundle
828 emitHtml = true;
829 generateKatasContent(katasContentPath, katasGeneratedContentPath);
830 emitHtml = false;
831 generateKatasContent(katasContentPath, katasGeneratedContentPath);
832} else {
833 console.log("Content is up to date, skipping generation");
834}
835