@abreen/tada 1.8.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/bin/validators.ts +1 -1
  4. package/build/apply-base-path-plugin.ts +79 -17
  5. package/build/bundle.ts +9 -1
  6. package/build/content-watch.ts +3 -3
  7. package/build/copy.ts +30 -0
  8. package/build/generate-content-assets.ts +45 -59
  9. package/build/pagefind.ts +0 -7
  10. package/build/serve.ts +4 -0
  11. package/build/templates.ts +0 -2
  12. package/build/types.d.ts +19 -1
  13. package/build/util.ts +3 -0
  14. package/build/utils/content-files.ts +98 -0
  15. package/build/utils/include.ts +4 -0
  16. package/build/utils/python-runner/trace_runner.py +272 -0
  17. package/build/utils/render.ts +47 -15
  18. package/build/utils/source-template.ts +27 -0
  19. package/build/utils/trace-core.ts +203 -0
  20. package/build/utils/trace-java.ts +132 -0
  21. package/build/utils/trace-layout.ts +31 -3
  22. package/build/utils/trace-python.ts +26 -0
  23. package/build/utils/trace.ts +174 -294
  24. package/build/validate-internal-links-plugin.ts +16 -4
  25. package/build/watch/assets.ts +111 -0
  26. package/build/watch/build-full.ts +124 -0
  27. package/build/watch/build-incremental.ts +139 -0
  28. package/build/watch/compiler-types.d.ts +19 -0
  29. package/build/watch/compiler.ts +112 -0
  30. package/build/watch/index.ts +22 -0
  31. package/build/watch/mutations.ts +75 -0
  32. package/build/watch/planner.ts +230 -0
  33. package/build/watch/records.ts +190 -0
  34. package/build/watch/runtime.ts +143 -0
  35. package/build/watch/snapshot.ts +602 -0
  36. package/build/watch/validation.ts +36 -0
  37. package/build/watch-reload-client.ts +10 -5
  38. package/init/content/labs/01/index.md +6 -0
  39. package/init/content/labs/01/trace_demo.py +16 -0
  40. package/init/content/lectures/01/Rectangle.java +2 -0
  41. package/init/content/lectures/01/index.md +6 -0
  42. package/init/content/lectures/01/rectangle.py +5 -0
  43. package/package.json +42 -23
  44. package/python/command.ts +95 -0
  45. package/python/module.ts +18 -0
  46. package/src/_alerts.scss +11 -8
  47. package/src/_base.scss +25 -29
  48. package/src/_layout.scss +17 -14
  49. package/src/_mixins.scss +10 -9
  50. package/src/anchor/style.scss +4 -2
  51. package/src/code/index.ts +9 -2
  52. package/src/code/style.scss +14 -13
  53. package/src/code.scss +14 -13
  54. package/src/header/_base.scss +21 -24
  55. package/src/header/style.scss +16 -12
  56. package/src/index.ts +3 -0
  57. package/src/literate/style.scss +4 -4
  58. package/src/navigate/index.ts +43 -279
  59. package/src/navigate/lifecycle.ts +1 -1
  60. package/src/navigate/runtime.ts +341 -0
  61. package/src/page-update/README.md +4 -0
  62. package/src/page-update/index.ts +280 -0
  63. package/src/page-update/style.scss +53 -0
  64. package/src/print/style.scss +4 -4
  65. package/src/question/style.scss +10 -6
  66. package/src/search/index.ts +9 -12
  67. package/src/search/style.scss +44 -42
  68. package/src/style.scss +63 -58
  69. package/src/timezone/style.scss +5 -5
  70. package/src/toc/style.scss +17 -16
  71. package/src/trace/style.scss +27 -27
  72. package/src/validators.ts +45 -0
  73. package/templates/_footer.html +1 -1
  74. package/templates/_theme.scss +8 -19
  75. package/watch/engine.ts +296 -0
  76. package/watch/events.ts +10 -0
  77. package/watch/fs-commit.ts +102 -0
  78. package/watch/types.d.ts +103 -0
  79. package/build/watch.ts +0 -493
@@ -0,0 +1,602 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import {
4
+ getContentDir,
5
+ getDistDir,
6
+ getPublicDir,
7
+ normalizeOutputPath,
8
+ shouldSkipContentFile,
9
+ toPosix,
10
+ } from '../util';
11
+ import { isFeatureEnabled } from '../features';
12
+ import {
13
+ extensionIsMarkdown,
14
+ getProcessedExtensions,
15
+ isLiterateJava,
16
+ isPartial,
17
+ } from '../utils/file-types';
18
+ import type { ChangeBatch } from '../../watch/types';
19
+ import type { Asset, SiteVariables } from '../types';
20
+
21
+ export interface TadaSourceRecord {
22
+ sourcePath: string;
23
+ kind: 'content' | 'public';
24
+ outputs: Map<string, string | Buffer>;
25
+ partialDeps: Set<string>;
26
+ traceDeps: Set<string>;
27
+ internalTargets: Set<string>;
28
+ generatedOutputPaths: Set<string>;
29
+ authorKey?: string;
30
+ }
31
+
32
+ export interface TadaSnapshot {
33
+ siteVariables: SiteVariables;
34
+ assetFiles: string[];
35
+ navData: unknown;
36
+ authorsData: unknown;
37
+ processedExts: Set<string>;
38
+ contentRecords: Map<string, TadaSourceRecord>;
39
+ publicRecords: Map<string, TadaSourceRecord>;
40
+ outputOwners: Map<string, { kind: 'content' | 'public'; sourcePath: string }>;
41
+ reversePartialDeps: Map<string, Set<string>>;
42
+ reverseTraceDeps: Map<string, Set<string>>;
43
+ reverseInternalTargetDeps: Map<string, Set<string>>;
44
+ reverseAuthorDeps: Map<string, Set<string>>;
45
+ contentFiles: Set<string>;
46
+ buildContentFiles: Set<string>;
47
+ publicFiles: Set<string>;
48
+ literateJavaOutputPaths: Set<string>;
49
+ contentOwners: Map<string, string>;
50
+ publicOwners: Map<string, string>;
51
+ sourceOutputPaths: Map<string, Set<string>>;
52
+ sourceTargetPaths: Map<string, Set<string>>;
53
+ validTargets: Set<string>;
54
+ }
55
+
56
+ export interface TadaProjectScan {
57
+ contentDir: string;
58
+ publicDir: string;
59
+ distDir: string;
60
+ contentFiles: Set<string>;
61
+ buildContentFiles: Set<string>;
62
+ publicFiles: Set<string>;
63
+ validTargets: Set<string>;
64
+ literateJavaOutputPaths: Set<string>;
65
+ processedExts: Set<string>;
66
+ contentOwners: Map<string, string>;
67
+ publicOwners: Map<string, string>;
68
+ sourceOutputPaths: Map<string, Set<string>>;
69
+ sourceTargetPaths: Map<string, Set<string>>;
70
+ }
71
+
72
+ function walkFiles(dir: string): string[] {
73
+ if (!fs.existsSync(dir)) {
74
+ return [];
75
+ }
76
+
77
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
78
+ const fullPath = path.join(dir, entry.name);
79
+ if (entry.isDirectory()) {
80
+ return walkFiles(fullPath);
81
+ }
82
+ if (entry.isFile()) {
83
+ return [fullPath];
84
+ }
85
+ return [];
86
+ });
87
+ }
88
+
89
+ function buildReverseMap(
90
+ records: Iterable<TadaSourceRecord>,
91
+ selector: (record: TadaSourceRecord) => Iterable<string>,
92
+ ): Map<string, Set<string>> {
93
+ const reverse = new Map<string, Set<string>>();
94
+
95
+ for (const record of records) {
96
+ for (const key of selector(record)) {
97
+ if (!reverse.has(key)) {
98
+ reverse.set(key, new Set());
99
+ }
100
+ reverse.get(key)!.add(record.sourcePath);
101
+ }
102
+ }
103
+
104
+ return reverse;
105
+ }
106
+
107
+ function getProcessedExts(siteVariables: SiteVariables): Set<string> {
108
+ return new Set(
109
+ getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})).map(
110
+ ext => ext.toLowerCase(),
111
+ ),
112
+ );
113
+ }
114
+
115
+ function isBuildContentSource(
116
+ filePath: string,
117
+ processedExts: Set<string>,
118
+ ): boolean {
119
+ const ext = path.extname(filePath).slice(1).toLowerCase();
120
+ if (!processedExts.has(ext) || isPartial(filePath)) {
121
+ return false;
122
+ }
123
+ return !shouldSkipContentFile(filePath);
124
+ }
125
+
126
+ function addGeneratedRouteAliases(
127
+ targets: Set<string>,
128
+ outputPath: string,
129
+ ): void {
130
+ const normalizedPath = normalizeOutputPath(outputPath);
131
+ targets.add(normalizedPath);
132
+ if (!normalizedPath.endsWith('/index.html')) {
133
+ return;
134
+ }
135
+ const base = normalizedPath.slice(0, -'index.html'.length);
136
+ targets.add(base);
137
+ if (base.endsWith('/') && base.length > 1) {
138
+ targets.add(base.slice(0, -1));
139
+ }
140
+ }
141
+
142
+ function getContentOutputPathsForSource({
143
+ contentDir,
144
+ filePath,
145
+ processedExts,
146
+ buildContent,
147
+ codeEnabled,
148
+ }: {
149
+ contentDir: string;
150
+ filePath: string;
151
+ processedExts: Set<string>;
152
+ buildContent: boolean;
153
+ codeEnabled: boolean;
154
+ }): Set<string> {
155
+ const relPath = toPosix(path.relative(contentDir, filePath));
156
+ const parsed = path.parse(relPath);
157
+ const ext = path.extname(filePath).slice(1).toLowerCase();
158
+ const outputs = new Set<string>();
159
+
160
+ if (!processedExts.has(ext)) {
161
+ outputs.add(relPath);
162
+ return outputs;
163
+ }
164
+
165
+ if (!buildContent) {
166
+ return outputs;
167
+ }
168
+
169
+ if (isLiterateJava(filePath)) {
170
+ outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
171
+ outputs.add(toPosix(path.join(parsed.dir, parsed.name)));
172
+ return outputs;
173
+ }
174
+
175
+ if (
176
+ extensionIsMarkdown(parsed.ext.toLowerCase()) ||
177
+ parsed.ext.toLowerCase() === '.html'
178
+ ) {
179
+ outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
180
+ return outputs;
181
+ }
182
+
183
+ outputs.add(relPath);
184
+ if (codeEnabled) {
185
+ outputs.add(`${relPath}.html`);
186
+ }
187
+ return outputs;
188
+ }
189
+
190
+ function getTargetPathsForSource({
191
+ kind,
192
+ rootDir,
193
+ filePath,
194
+ processedExts,
195
+ buildContent,
196
+ codeEnabled,
197
+ }: {
198
+ kind: 'content' | 'public';
199
+ rootDir: string;
200
+ filePath: string;
201
+ processedExts: Set<string>;
202
+ buildContent: boolean;
203
+ codeEnabled: boolean;
204
+ }): Set<string> {
205
+ const targets = new Set<string>();
206
+ const relPath = toPosix(path.relative(rootDir, filePath));
207
+
208
+ if (kind === 'public') {
209
+ targets.add(normalizeOutputPath(`/${relPath}`));
210
+ return targets;
211
+ }
212
+
213
+ const ext = path.extname(filePath).slice(1).toLowerCase();
214
+ if (!processedExts.has(ext)) {
215
+ targets.add(normalizeOutputPath(`/${relPath}`));
216
+ return targets;
217
+ }
218
+
219
+ if (!buildContent || isPartial(filePath)) {
220
+ return targets;
221
+ }
222
+
223
+ const parsed = path.parse(relPath);
224
+ const subPath = toPosix(path.join(parsed.dir, parsed.name));
225
+
226
+ if (isLiterateJava(filePath)) {
227
+ const javaSubPath = toPosix(path.join(parsed.dir, parsed.name));
228
+ addGeneratedRouteAliases(targets, `/${javaSubPath}.html`);
229
+ targets.add(normalizeOutputPath(`/${javaSubPath}`));
230
+ return targets;
231
+ }
232
+
233
+ if (
234
+ extensionIsMarkdown(parsed.ext.toLowerCase()) ||
235
+ parsed.ext.toLowerCase() === '.html'
236
+ ) {
237
+ addGeneratedRouteAliases(targets, `/${subPath}.html`);
238
+ return targets;
239
+ }
240
+
241
+ if (codeEnabled) {
242
+ addGeneratedRouteAliases(targets, `/${relPath}.html`);
243
+ }
244
+ targets.add(normalizeOutputPath(`/${relPath}`));
245
+ return targets;
246
+ }
247
+
248
+ function cloneSetMap(
249
+ source: Map<string, Set<string>>,
250
+ ): Map<string, Set<string>> {
251
+ return new Map(
252
+ [...source.entries()].map(([key, values]) => [key, new Set(values)]),
253
+ );
254
+ }
255
+
256
+ export function scanProject(siteVariables: SiteVariables): TadaProjectScan {
257
+ const contentDir = getContentDir();
258
+ const publicDir = getPublicDir();
259
+ const distDir = getDistDir();
260
+ const processedExts = getProcessedExts(siteVariables);
261
+ const codeEnabled = isFeatureEnabled(siteVariables, 'code');
262
+ const contentFiles = new Set(walkFiles(contentDir).sort());
263
+ const publicFiles = new Set(walkFiles(publicDir).sort());
264
+ const buildContentFiles = new Set<string>();
265
+ const literateJavaOutputPaths = new Set<string>();
266
+ const contentOwners = new Map<string, string>();
267
+ const publicOwners = new Map<string, string>();
268
+ const sourceOutputPaths = new Map<string, Set<string>>();
269
+ const sourceTargetPaths = new Map<string, Set<string>>();
270
+ const validTargets = new Set<string>();
271
+
272
+ for (const filePath of contentFiles) {
273
+ const buildContent = isBuildContentSource(filePath, processedExts);
274
+ if (buildContent) {
275
+ buildContentFiles.add(filePath);
276
+ }
277
+ if (buildContent && isLiterateJava(filePath)) {
278
+ const parsed = path.parse(path.relative(contentDir, filePath));
279
+ literateJavaOutputPaths.add(
280
+ `/${toPosix(path.join(parsed.dir, parsed.name))}`,
281
+ );
282
+ }
283
+
284
+ const outputs = getContentOutputPathsForSource({
285
+ contentDir,
286
+ filePath,
287
+ processedExts,
288
+ buildContent,
289
+ codeEnabled,
290
+ });
291
+ sourceOutputPaths.set(filePath, outputs);
292
+ for (const outputPath of outputs) {
293
+ contentOwners.set(outputPath, filePath);
294
+ }
295
+
296
+ const targets = getTargetPathsForSource({
297
+ kind: 'content',
298
+ rootDir: contentDir,
299
+ filePath,
300
+ processedExts,
301
+ buildContent,
302
+ codeEnabled,
303
+ });
304
+ sourceTargetPaths.set(filePath, targets);
305
+ for (const target of targets) {
306
+ validTargets.add(target);
307
+ }
308
+ }
309
+
310
+ for (const filePath of publicFiles) {
311
+ const relPath = toPosix(path.relative(publicDir, filePath));
312
+ publicOwners.set(relPath, filePath);
313
+ sourceOutputPaths.set(filePath, new Set([relPath]));
314
+ const targets = getTargetPathsForSource({
315
+ kind: 'public',
316
+ rootDir: publicDir,
317
+ filePath,
318
+ processedExts,
319
+ buildContent: false,
320
+ codeEnabled,
321
+ });
322
+ sourceTargetPaths.set(filePath, targets);
323
+ for (const target of targets) {
324
+ validTargets.add(target);
325
+ }
326
+ }
327
+
328
+ return {
329
+ contentDir,
330
+ publicDir,
331
+ distDir,
332
+ contentFiles,
333
+ buildContentFiles,
334
+ publicFiles,
335
+ validTargets,
336
+ literateJavaOutputPaths,
337
+ processedExts,
338
+ contentOwners,
339
+ publicOwners,
340
+ sourceOutputPaths,
341
+ sourceTargetPaths,
342
+ };
343
+ }
344
+
345
+ export function updateProjectScan(
346
+ snapshot: TadaSnapshot,
347
+ batch: ChangeBatch,
348
+ ): TadaProjectScan {
349
+ const contentDir = getContentDir();
350
+ const publicDir = getPublicDir();
351
+ const distDir = getDistDir();
352
+ const codeEnabled = isFeatureEnabled(snapshot.siteVariables, 'code');
353
+ const contentFiles = new Set(snapshot.contentFiles);
354
+ const buildContentFiles = new Set(snapshot.buildContentFiles);
355
+ const publicFiles = new Set(snapshot.publicFiles);
356
+ const literateJavaOutputPaths = new Set(snapshot.literateJavaOutputPaths);
357
+ const contentOwners = new Map(snapshot.contentOwners);
358
+ const publicOwners = new Map(snapshot.publicOwners);
359
+ const sourceOutputPaths = cloneSetMap(snapshot.sourceOutputPaths);
360
+ const sourceTargetPaths = cloneSetMap(snapshot.sourceTargetPaths);
361
+
362
+ for (const change of batch.changes) {
363
+ const sourcePath = path.resolve(change.path);
364
+ const inContent = sourcePath.startsWith(`${contentDir}${path.sep}`);
365
+ const inPublic = sourcePath.startsWith(`${publicDir}${path.sep}`);
366
+ if (!inContent && !inPublic) {
367
+ continue;
368
+ }
369
+
370
+ for (const outputPath of sourceOutputPaths.get(sourcePath) || []) {
371
+ if (inContent) {
372
+ contentOwners.delete(outputPath);
373
+ } else {
374
+ publicOwners.delete(outputPath);
375
+ }
376
+ }
377
+ sourceOutputPaths.delete(sourcePath);
378
+ sourceTargetPaths.delete(sourcePath);
379
+ contentFiles.delete(sourcePath);
380
+ buildContentFiles.delete(sourcePath);
381
+ publicFiles.delete(sourcePath);
382
+
383
+ if (inContent && isLiterateJava(sourcePath)) {
384
+ const parsed = path.parse(path.relative(contentDir, sourcePath));
385
+ literateJavaOutputPaths.delete(
386
+ `/${toPosix(path.join(parsed.dir, parsed.name))}`,
387
+ );
388
+ }
389
+
390
+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
391
+ continue;
392
+ }
393
+
394
+ if (inContent) {
395
+ const buildContent = isBuildContentSource(
396
+ sourcePath,
397
+ snapshot.processedExts,
398
+ );
399
+ contentFiles.add(sourcePath);
400
+ if (buildContent) {
401
+ buildContentFiles.add(sourcePath);
402
+ }
403
+ if (buildContent && isLiterateJava(sourcePath)) {
404
+ const parsed = path.parse(path.relative(contentDir, sourcePath));
405
+ literateJavaOutputPaths.add(
406
+ `/${toPosix(path.join(parsed.dir, parsed.name))}`,
407
+ );
408
+ }
409
+
410
+ const outputs = getContentOutputPathsForSource({
411
+ contentDir,
412
+ filePath: sourcePath,
413
+ processedExts: snapshot.processedExts,
414
+ buildContent,
415
+ codeEnabled,
416
+ });
417
+ sourceOutputPaths.set(sourcePath, outputs);
418
+ for (const outputPath of outputs) {
419
+ contentOwners.set(outputPath, sourcePath);
420
+ }
421
+
422
+ sourceTargetPaths.set(
423
+ sourcePath,
424
+ getTargetPathsForSource({
425
+ kind: 'content',
426
+ rootDir: contentDir,
427
+ filePath: sourcePath,
428
+ processedExts: snapshot.processedExts,
429
+ buildContent,
430
+ codeEnabled,
431
+ }),
432
+ );
433
+ continue;
434
+ }
435
+
436
+ const relPath = toPosix(path.relative(publicDir, sourcePath));
437
+ publicFiles.add(sourcePath);
438
+ publicOwners.set(relPath, sourcePath);
439
+ sourceOutputPaths.set(sourcePath, new Set([relPath]));
440
+ sourceTargetPaths.set(
441
+ sourcePath,
442
+ getTargetPathsForSource({
443
+ kind: 'public',
444
+ rootDir: publicDir,
445
+ filePath: sourcePath,
446
+ processedExts: snapshot.processedExts,
447
+ buildContent: false,
448
+ codeEnabled,
449
+ }),
450
+ );
451
+ }
452
+
453
+ const validTargets = new Set<string>();
454
+ for (const targets of sourceTargetPaths.values()) {
455
+ for (const target of targets) {
456
+ validTargets.add(target);
457
+ }
458
+ }
459
+
460
+ return {
461
+ contentDir,
462
+ publicDir,
463
+ distDir,
464
+ contentFiles,
465
+ buildContentFiles,
466
+ publicFiles,
467
+ validTargets,
468
+ literateJavaOutputPaths,
469
+ processedExts: snapshot.processedExts,
470
+ contentOwners,
471
+ publicOwners,
472
+ sourceOutputPaths,
473
+ sourceTargetPaths,
474
+ };
475
+ }
476
+
477
+ export function collectOutputOwners(snapshot: {
478
+ contentRecords: Map<string, TadaSourceRecord>;
479
+ publicRecords: Map<string, TadaSourceRecord>;
480
+ }): Map<string, { kind: 'content' | 'public'; sourcePath: string }> {
481
+ const owners = new Map<
482
+ string,
483
+ { kind: 'content' | 'public'; sourcePath: string }
484
+ >();
485
+
486
+ for (const record of snapshot.contentRecords.values()) {
487
+ for (const outputPath of record.outputs.keys()) {
488
+ owners.set(outputPath, {
489
+ kind: 'content',
490
+ sourcePath: record.sourcePath,
491
+ });
492
+ }
493
+ }
494
+
495
+ for (const record of snapshot.publicRecords.values()) {
496
+ for (const outputPath of record.outputs.keys()) {
497
+ owners.set(outputPath, { kind: 'public', sourcePath: record.sourcePath });
498
+ }
499
+ }
500
+
501
+ return owners;
502
+ }
503
+
504
+ export function createSnapshot({
505
+ siteVariables,
506
+ assetFiles,
507
+ navData,
508
+ authorsData,
509
+ scan,
510
+ contentRecords,
511
+ publicRecords,
512
+ }: {
513
+ siteVariables: SiteVariables;
514
+ assetFiles: string[];
515
+ navData: unknown;
516
+ authorsData: unknown;
517
+ scan: TadaProjectScan;
518
+ contentRecords: Map<string, TadaSourceRecord>;
519
+ publicRecords: Map<string, TadaSourceRecord>;
520
+ }): TadaSnapshot {
521
+ const allRecords = [...contentRecords.values()];
522
+ const reverseAuthorDeps = new Map<string, Set<string>>();
523
+ for (const record of allRecords) {
524
+ if (!record.authorKey) {
525
+ continue;
526
+ }
527
+ if (!reverseAuthorDeps.has(record.authorKey)) {
528
+ reverseAuthorDeps.set(record.authorKey, new Set());
529
+ }
530
+ reverseAuthorDeps.get(record.authorKey)!.add(record.sourcePath);
531
+ }
532
+
533
+ return {
534
+ siteVariables,
535
+ assetFiles,
536
+ navData,
537
+ authorsData,
538
+ processedExts: scan.processedExts,
539
+ contentRecords,
540
+ publicRecords,
541
+ outputOwners: collectOutputOwners({ contentRecords, publicRecords }),
542
+ reversePartialDeps: buildReverseMap(
543
+ allRecords,
544
+ record => record.partialDeps,
545
+ ),
546
+ reverseTraceDeps: buildReverseMap(allRecords, record => record.traceDeps),
547
+ reverseInternalTargetDeps: buildReverseMap(
548
+ allRecords,
549
+ record => record.internalTargets,
550
+ ),
551
+ reverseAuthorDeps,
552
+ contentFiles: new Set(scan.contentFiles),
553
+ buildContentFiles: new Set(scan.buildContentFiles),
554
+ publicFiles: new Set(scan.publicFiles),
555
+ literateJavaOutputPaths: new Set(scan.literateJavaOutputPaths),
556
+ contentOwners: new Map(scan.contentOwners),
557
+ publicOwners: new Map(scan.publicOwners),
558
+ sourceOutputPaths: cloneSetMap(scan.sourceOutputPaths),
559
+ sourceTargetPaths: cloneSetMap(scan.sourceTargetPaths),
560
+ validTargets: new Set(scan.validTargets),
561
+ };
562
+ }
563
+
564
+ export function assertNoOutputPathConflicts(scan: TadaProjectScan): string[] {
565
+ const conflicts = [...scan.contentOwners.keys()].filter(relPath =>
566
+ scan.publicOwners.has(relPath),
567
+ );
568
+ return conflicts.sort();
569
+ }
570
+
571
+ export function collectSourceOutputs(
572
+ assets: Asset[],
573
+ generatedOutputPaths: Set<string>,
574
+ stageDir: string,
575
+ ): Map<string, string | Buffer> {
576
+ const outputs = new Map<string, string | Buffer>();
577
+ for (const asset of assets) {
578
+ outputs.set(asset.assetPath, asset.content);
579
+ }
580
+ for (const outputPath of generatedOutputPaths) {
581
+ outputs.set(outputPath, fs.readFileSync(path.join(stageDir, outputPath)));
582
+ }
583
+ return outputs;
584
+ }
585
+
586
+ export function collectHtmlAssetsByPath(
587
+ contentRecords: Map<string, TadaSourceRecord>,
588
+ ): Map<string, string> {
589
+ const htmlAssetsByPath = new Map<string, string>();
590
+ for (const record of contentRecords.values()) {
591
+ for (const [outputPath, content] of record.outputs) {
592
+ if (outputPath.endsWith('.html') && typeof content === 'string') {
593
+ htmlAssetsByPath.set(outputPath, content);
594
+ }
595
+ }
596
+ }
597
+ return htmlAssetsByPath;
598
+ }
599
+
600
+ export function normalizeInternalTarget(target: string): string {
601
+ return normalizeOutputPath(target);
602
+ }
@@ -0,0 +1,36 @@
1
+ import { B } from '../colors';
2
+ import { makeLogger } from '../log';
3
+ import { json } from '../templates';
4
+ import { validateConfigLinks } from '../validate-config-links';
5
+ import type { WatchDiagnostic } from '../../watch/types';
6
+ import { assertNoOutputPathConflicts, type TadaProjectScan } from './snapshot';
7
+
8
+ const log = makeLogger(import.meta.url);
9
+
10
+ export function diagnosticsFromMessages(messages: string[]): WatchDiagnostic[] {
11
+ return messages.map(message => ({ message }));
12
+ }
13
+
14
+ export function validateConfig(scan: TadaProjectScan): WatchDiagnostic[] {
15
+ const conflicts = assertNoOutputPathConflicts(scan);
16
+ if (conflicts.length === 0) {
17
+ return [];
18
+ }
19
+ for (const relPath of conflicts) {
20
+ log.error`content/${B`${relPath}`} conflicts with public/${B`${relPath}`}`;
21
+ }
22
+ const noun = conflicts.length === 1 ? 'file' : 'files';
23
+ return [
24
+ {
25
+ message: `${conflicts.length} ${noun} in content/ and public/ have the same path`,
26
+ },
27
+ ];
28
+ }
29
+
30
+ export function validateJsonLinks(
31
+ validTargets: Set<string>,
32
+ ): WatchDiagnostic[] {
33
+ return diagnosticsFromMessages(
34
+ validateConfigLinks(validTargets, json('nav.json'), json('authors.json')),
35
+ );
36
+ }
@@ -1,6 +1,14 @@
1
1
  declare const __WEBSOCKET_PORT__: number;
2
2
 
3
3
  (function () {
4
+ function setLoading(loading: boolean): void {
5
+ const header = document.querySelector('header');
6
+ if (header) {
7
+ header.classList.toggle('loading', loading);
8
+ }
9
+ document.body.classList.toggle('loading-cursor', loading);
10
+ }
11
+
4
12
  const style = document.createElement('style');
5
13
  style.textContent = `body.loading-cursor,
6
14
  body.loading-cursor * {
@@ -17,13 +25,10 @@ body.loading-cursor * {
17
25
  ws.onmessage = event => {
18
26
  if (event.data === 'rebuilding') {
19
27
  console.log('[watch-reload] Rebuilding...');
20
- const header = document.querySelector('header');
21
- if (header) {
22
- header.classList.add('loading');
23
- }
24
- document.body.classList.add('loading-cursor');
28
+ setLoading(true);
25
29
  } else if (event.data === 'reload') {
26
30
  console.log('[watch-reload] Reloading page...');
31
+ setLoading(false);
27
32
  window.history.scrollRestoration = 'auto';
28
33
  window.location.reload();
29
34
  }
@@ -14,6 +14,12 @@ A simple program with an array, recursion, and a string.
14
14
 
15
15
  <%= renderTrace('TraceDemo.java') %>
16
16
 
17
+ ### Python version
18
+
19
+ Here's a similar trace, but of a Python script:
20
+
21
+ <%= renderTrace('trace_demo.py') %>
22
+
17
23
  ## Binary search tree
18
24
 
19
25
  Building a binary search tree with parent references.
@@ -0,0 +1,16 @@
1
+ def sum(n: int):
2
+ if n <= 0:
3
+ return 0
4
+ return n + sum(n - 1)
5
+
6
+ nums = (pow(2, x) for x in range(7))
7
+ first = next(nums)
8
+ second = next(nums)
9
+
10
+ total = sum(4)
11
+ print("total", total)
12
+ print("nums", first, second)
13
+
14
+ s = "hello"
15
+ for c in s:
16
+ print(c)
@@ -4,6 +4,8 @@
4
4
  /// by scaling using a _scale factor_ (a floating-point number between 0 and 1).
5
5
  ///
6
6
  /// See [`rectangle.py`](./rectangle.py) for a Python version of this class.
7
+ ///
8
+ /// Here's `vars.foobar`: <%= vars.foobar %>
7
9
  public class Rectangle {
8
10
  private int width;
9
11
  private int height;