@abreen/tada 1.0.2 → 1.2.0

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 (146) hide show
  1. package/README.md +96 -48
  2. package/bin/tada.ts +621 -0
  3. package/bin/validators.test.ts +205 -0
  4. package/bin/validators.ts +85 -0
  5. package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +37 -17
  6. package/build/build-manifest.test.ts +349 -0
  7. package/build/build-manifest.ts +165 -0
  8. package/build/bundle.ts +116 -0
  9. package/{webpack/code.test.js → build/code.test.ts} +10 -7
  10. package/build/colors.ts +25 -0
  11. package/build/content-watch.ts +107 -0
  12. package/build/copy.ts +118 -0
  13. package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
  14. package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
  15. package/build/features.ts +11 -0
  16. package/build/generate-content-assets.ts +320 -0
  17. package/build/generate-favicon.ts +172 -0
  18. package/build/generate-fonts.ts +31 -0
  19. package/build/generate-katex-assets.ts +35 -0
  20. package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
  21. package/build/globals.test.ts +99 -0
  22. package/build/globals.ts +88 -0
  23. package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
  24. package/build/json-schema.test.ts +57 -0
  25. package/build/json-schema.ts +33 -0
  26. package/build/log.test.ts +111 -0
  27. package/build/log.ts +167 -0
  28. package/build/markdown-plugins.test.ts +396 -0
  29. package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
  30. package/build/pagefind.ts +339 -0
  31. package/build/pdf-text.test.ts +52 -0
  32. package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
  33. package/build/pipeline.ts +120 -0
  34. package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
  35. package/{webpack/reachability.js → build/reachability.ts} +77 -34
  36. package/build/serve.ts +112 -0
  37. package/build/site-variables.ts +68 -0
  38. package/{webpack/templates.js → build/templates.ts} +38 -35
  39. package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
  40. package/build/toc-plugin.test.ts +105 -0
  41. package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
  42. package/build/types.ts +181 -0
  43. package/build/util.ts +26 -0
  44. package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
  45. package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
  46. package/build/utils/derive-theme.test.ts +111 -0
  47. package/build/utils/derive-theme.ts +85 -0
  48. package/build/utils/file-types.test.ts +61 -0
  49. package/build/utils/file-types.ts +13 -0
  50. package/build/utils/front-matter.test.ts +77 -0
  51. package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
  52. package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
  53. package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +68 -38
  54. package/{webpack/utils/markdown.js → build/utils/markdown.ts} +126 -49
  55. package/build/utils/paths.test.ts +91 -0
  56. package/{webpack/utils/paths.js → build/utils/paths.ts} +18 -22
  57. package/{webpack/utils/render.js → build/utils/render.ts} +208 -130
  58. package/build/utils/shiki-highlighter.ts +29 -0
  59. package/build/validate-internal-links-plugin.test.ts +106 -0
  60. package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
  61. package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
  62. package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
  63. package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
  64. package/build/watch.ts +549 -0
  65. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
  66. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
  67. package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
  68. package/fonts/inter/woff2/InterVariable.woff2 +0 -0
  69. package/{content → init/content}/index.md +9 -3
  70. package/{content → init/content}/lectures/index.md +1 -1
  71. package/{content → init/content}/markdown.md +28 -8
  72. package/init/content/problem_sets/index.html +14 -0
  73. package/package.json +33 -22
  74. package/{templates → schema}/nav.schema.json +1 -2
  75. package/{webpack → schema}/site.schema.json +4 -12
  76. package/src/_alerts.scss +96 -0
  77. package/src/_base.scss +145 -0
  78. package/src/{layout.scss → _layout.scss} +36 -1
  79. package/src/anchor/index.ts +0 -1
  80. package/src/anchor/style.scss +1 -9
  81. package/src/code/index.ts +3 -3
  82. package/src/code.scss +1 -1
  83. package/src/critical.scss +5 -0
  84. package/src/header/_base.scss +129 -0
  85. package/src/header/style.scss +3 -131
  86. package/src/index.ts +4 -5
  87. package/src/question/style.scss +1 -1
  88. package/src/search/index.test.ts +203 -0
  89. package/src/search/index.ts +38 -93
  90. package/src/search/pdf-utils.ts +79 -0
  91. package/src/search/style.scss +8 -28
  92. package/src/style.scss +15 -326
  93. package/src/timezone/index.ts +5 -6
  94. package/src/toc/style.scss +5 -39
  95. package/src/util.ts +9 -6
  96. package/templates/_theme.scss +38 -14
  97. package/templates/_top.html +1 -1
  98. package/tsconfig.json +10 -6
  99. package/types/dev.d.ts +1 -0
  100. package/types/file-system-access.d.ts +5 -0
  101. package/types/markdown-it-plugins.d.ts +11 -0
  102. package/types/site-variables.d.ts +6 -10
  103. package/types/untyped-modules.d.ts +47 -0
  104. package/bin/tada.js +0 -361
  105. package/config/authors.json +0 -1
  106. package/config/nav.json +0 -28
  107. package/content/problem_sets/index.md +0 -6
  108. package/types/dev.ts +0 -3
  109. package/webpack/build-state.js +0 -97
  110. package/webpack/colors.js +0 -15
  111. package/webpack/config.base.js +0 -151
  112. package/webpack/config.dev.js +0 -23
  113. package/webpack/config.prod.js +0 -32
  114. package/webpack/content-watch-plugin.js +0 -153
  115. package/webpack/features.js +0 -5
  116. package/webpack/generate-content-assets-plugin.js +0 -308
  117. package/webpack/generate-favicon-plugin.js +0 -198
  118. package/webpack/generate-fonts-plugin.js +0 -69
  119. package/webpack/globals.js +0 -74
  120. package/webpack/json-schema.js +0 -19
  121. package/webpack/log.js +0 -143
  122. package/webpack/markdown-plugins.test.js +0 -203
  123. package/webpack/pagefind-plugin.js +0 -379
  124. package/webpack/print-flair-plugin.js +0 -22
  125. package/webpack/serve.js +0 -104
  126. package/webpack/site-variables.js +0 -53
  127. package/webpack/util.js +0 -49
  128. package/webpack/utils/define-plugin.js +0 -20
  129. package/webpack/utils/file-types.js +0 -26
  130. package/webpack/utils/parse-hsl.js +0 -8
  131. package/webpack/utils/shiki-highlighter.js +0 -26
  132. package/webpack/watch.js +0 -166
  133. /package/{webpack → build}/flair.json +0 -0
  134. /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
  135. /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
  136. /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
  137. /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
  138. /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
  139. /package/{content → init/content}/lectures/01/Pair.java.md +0 -0
  140. /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
  141. /package/{content → init/content}/lectures/01/demo.py +0 -0
  142. /package/{content → init/content}/lectures/01/index.md +0 -0
  143. /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
  144. /package/{public → init/public}/avatars/alex.jpg +0 -0
  145. /package/{public → init/public}/test.txt +0 -0
  146. /package/{templates → schema}/authors.schema.json +0 -0
@@ -0,0 +1,107 @@
1
+ import path from 'path';
2
+ import type { SiteVariables } from './types';
3
+ import { getContentDir, getBuildContentFiles } from './util';
4
+ import {
5
+ compileTemplates,
6
+ getHtmlTemplatesDir,
7
+ getJsonDataDir,
8
+ JSON_DATA_FILES,
9
+ } from './templates';
10
+ import { getProjectDir } from './utils/paths';
11
+ import { B } from './colors';
12
+ import { makeLogger } from './log';
13
+
14
+ const log = makeLogger(__filename);
15
+
16
+ interface ChangeDetectionResult {
17
+ templateError: Error | null;
18
+ needsRestart: boolean;
19
+ changedContentFiles?: Set<string>;
20
+ templatesChanged?: boolean;
21
+ }
22
+
23
+ export class ContentChangeDetector {
24
+ private siteVariables: SiteVariables;
25
+ private siteConfigPath: string;
26
+ private lastSig: string | null;
27
+
28
+ constructor(siteVariables: SiteVariables) {
29
+ this.siteVariables = siteVariables;
30
+ this.siteConfigPath = path.resolve(getProjectDir(), 'site.dev.json');
31
+ this.lastSig = null;
32
+ }
33
+
34
+ detectChanges(modifiedFiles: Iterable<string>): ChangeDetectionResult {
35
+ const resolvedFiles = new Set(
36
+ [...(modifiedFiles || [])].map(filePath => path.resolve(filePath)),
37
+ );
38
+
39
+ const htmlTemplatesDir = getHtmlTemplatesDir();
40
+ const jsonDataDir = getJsonDataDir();
41
+
42
+ // Try to recompile templates
43
+ let templateError: Error | null = null;
44
+ try {
45
+ compileTemplates(this.siteVariables);
46
+ } catch (err) {
47
+ templateError = err as Error;
48
+ }
49
+
50
+ if (templateError) {
51
+ return { templateError, needsRestart: false };
52
+ }
53
+
54
+ const contentDir = getContentDir();
55
+ const normalizedContentDir = path.resolve(contentDir) + path.sep;
56
+ const normalizedHtmlDir = path.resolve(htmlTemplatesDir) + path.sep;
57
+ const buildContentFiles = getBuildContentFiles(
58
+ contentDir,
59
+ Object.keys(this.siteVariables.codeLanguages || {}),
60
+ );
61
+
62
+ // Detect structural changes (files added or deleted)
63
+ const sig = buildContentFiles.slice().sort().join('\0');
64
+ let needsRestart = false;
65
+ if (this.lastSig !== null && sig !== this.lastSig) {
66
+ needsRestart = true;
67
+ }
68
+ this.lastSig = sig;
69
+
70
+ const changedContentFiles = new Set(
71
+ [...resolvedFiles].filter(filePath =>
72
+ filePath.startsWith(normalizedContentDir),
73
+ ),
74
+ );
75
+
76
+ // Check if any HTML template or JSON data file changed
77
+ const jsonDataPaths = JSON_DATA_FILES.map(f =>
78
+ path.resolve(jsonDataDir, f),
79
+ );
80
+ const changedTemplatePaths = [...resolvedFiles].filter(
81
+ filePath =>
82
+ filePath === path.resolve(htmlTemplatesDir) ||
83
+ filePath.startsWith(normalizedHtmlDir) ||
84
+ jsonDataPaths.includes(filePath),
85
+ );
86
+ const templatesChanged = changedTemplatePaths.length > 0;
87
+
88
+ for (const filePath of changedTemplatePaths) {
89
+ log.event`${B`${path.basename(filePath)}`} changed, rebuilding`;
90
+ }
91
+
92
+ // Check if site config changed
93
+ const siteConfigChanged = resolvedFiles.has(
94
+ path.resolve(this.siteConfigPath),
95
+ );
96
+ if (siteConfigChanged) {
97
+ needsRestart = true;
98
+ }
99
+
100
+ return {
101
+ templateError: null,
102
+ needsRestart,
103
+ changedContentFiles,
104
+ templatesChanged,
105
+ };
106
+ }
107
+ }
package/build/copy.ts ADDED
@@ -0,0 +1,118 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { makeLogger } from './log';
4
+ import { B } from './colors';
5
+
6
+ const log = makeLogger(__filename);
7
+
8
+ interface CollectedFile {
9
+ abs: string;
10
+ rel: string;
11
+ }
12
+
13
+ function collectFiles(dir: string): CollectedFile[] {
14
+ let entries: fs.Dirent[];
15
+ try {
16
+ entries = fs.readdirSync(dir, { withFileTypes: true, recursive: true });
17
+ } catch {
18
+ return [];
19
+ }
20
+ return entries
21
+ .filter(entry => entry.isFile())
22
+ .map(entry => {
23
+ const abs = path.join(entry.parentPath, entry.name);
24
+ const rel = path.relative(dir, abs);
25
+ return { abs, rel: rel.split(path.sep).join(path.posix.sep) };
26
+ });
27
+ }
28
+
29
+ export function copyPublicFiles(
30
+ publicDir: string,
31
+ distDir: string,
32
+ ): Set<string> {
33
+ const files = collectFiles(publicDir);
34
+ const publicRelPaths = new Set<string>();
35
+ for (const { abs, rel } of files) {
36
+ const dest = path.join(distDir, rel);
37
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
38
+ fs.copyFileSync(abs, dest);
39
+ publicRelPaths.add(rel);
40
+ log.info`Copying public file ${B`${rel}`}`;
41
+ }
42
+ return publicRelPaths;
43
+ }
44
+
45
+ export function copyContentAssets(
46
+ contentDir: string,
47
+ distDir: string,
48
+ processedExtensions: string[],
49
+ publicRelPaths: Set<string>,
50
+ ): Set<string> {
51
+ const processedExtSet = new Set(processedExtensions);
52
+ const files = collectFiles(contentDir);
53
+ const contentAssetRelPaths = new Set<string>();
54
+ const conflicts: string[] = [];
55
+ for (const { abs, rel } of files) {
56
+ const ext = path.extname(abs).slice(1).toLowerCase();
57
+ if (processedExtSet.has(ext)) {
58
+ continue;
59
+ }
60
+ contentAssetRelPaths.add(rel);
61
+ if (publicRelPaths && publicRelPaths.has(rel)) {
62
+ conflicts.push(rel);
63
+ }
64
+ const dest = path.join(distDir, rel);
65
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
66
+ fs.copyFileSync(abs, dest);
67
+ }
68
+ if (conflicts.length > 0) {
69
+ for (const rel of conflicts) {
70
+ log.error`content/${B`${rel}`} conflicts with public/${B`${rel}`}`;
71
+ }
72
+ const noun = conflicts.length === 1 ? 'file' : 'files';
73
+ throw new Error(
74
+ `${conflicts.length} ${noun} in content/ and public/ have the same path`,
75
+ );
76
+ }
77
+ return contentAssetRelPaths;
78
+ }
79
+
80
+ export function copyPublicFile(
81
+ publicDir: string,
82
+ distDir: string,
83
+ filePath: string,
84
+ contentAssetRelPaths?: Set<string>,
85
+ ): void {
86
+ const rel = path
87
+ .relative(publicDir, filePath)
88
+ .split(path.sep)
89
+ .join(path.posix.sep);
90
+ if (contentAssetRelPaths && contentAssetRelPaths.has(rel)) {
91
+ log.error`public/${B`${rel}`} conflicts with content/${B`${rel}`}`;
92
+ throw new Error(`public/${rel} and content/${rel} have the same path`);
93
+ }
94
+ const dest = path.join(distDir, rel);
95
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
96
+ fs.copyFileSync(filePath, dest);
97
+ log.info`Copying public file ${B`${rel}`}`;
98
+ }
99
+
100
+ export function copyContentFile(
101
+ contentDir: string,
102
+ distDir: string,
103
+ filePath: string,
104
+ publicRelPaths?: Set<string>,
105
+ ): void {
106
+ const rel = path
107
+ .relative(contentDir, filePath)
108
+ .split(path.sep)
109
+ .join(path.posix.sep);
110
+ if (publicRelPaths && publicRelPaths.has(rel)) {
111
+ log.error`content/${B`${rel}`} conflicts with public/${B`${rel}`}`;
112
+ throw new Error(`content/${rel} and public/${rel} have the same path`);
113
+ }
114
+ const dest = path.join(distDir, rel);
115
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
116
+ fs.copyFileSync(filePath, dest);
117
+ log.info`Copying content file ${B`${rel}`}`;
118
+ }
@@ -1,11 +1,12 @@
1
- const textToId = require('./text-to-id');
1
+ import type MarkdownIt from 'markdown-it';
2
+ import textToId from './text-to-id';
2
3
 
3
- module.exports = function deflistIdPlugin(md) {
4
+ export default function deflistIdPlugin(md: MarkdownIt): void {
4
5
  md.core.ruler.push('deflist_id_injector', function (state) {
5
6
  const tokens = state.tokens;
6
- const used = new Map();
7
+ const used = new Map<string, number>();
7
8
 
8
- function slugify(str) {
9
+ function slugify(str: string): string {
9
10
  let slug = textToId(str);
10
11
 
11
12
  if (!slug) {
@@ -13,7 +14,7 @@ module.exports = function deflistIdPlugin(md) {
13
14
  }
14
15
 
15
16
  if (used.has(slug)) {
16
- const n = used.get(slug) + 1;
17
+ const n = (used.get(slug) as number) + 1;
17
18
  used.set(slug, n);
18
19
  slug = `${slug}-${n}`;
19
20
  } else {
@@ -59,4 +60,4 @@ module.exports = function deflistIdPlugin(md) {
59
60
  i++;
60
61
  }
61
62
  });
62
- };
63
+ }
@@ -1,15 +1,24 @@
1
- const { makeLogger } = require('./log');
1
+ import type MarkdownIt from 'markdown-it';
2
+ import type Token from 'markdown-it/lib/token.mjs';
3
+ import { makeLogger } from './log';
4
+ import type { SiteVariables } from './types';
2
5
 
3
6
  const log = makeLogger(__filename);
4
7
 
5
- module.exports = function externalLinks(md, siteVariables) {
6
- function addClass(token) {
8
+ export default function externalLinks(
9
+ md: MarkdownIt,
10
+ siteVariables: SiteVariables,
11
+ ): void {
12
+ function addClass(token: Token): void {
7
13
  if (token.type === 'link_open') {
8
14
  const href = token.attrGet('href');
15
+ if (!href) {
16
+ return;
17
+ }
9
18
 
10
19
  if (href.match(/^https?:\/\/.*$/)) {
11
20
  const url = new URL(href);
12
- if (!siteVariables.internalDomains.includes(url.host)) {
21
+ if (!siteVariables.internalDomains?.includes(url.host)) {
13
22
  const classAttr = token.attrGet('class');
14
23
  let newClassAttr;
15
24
 
@@ -34,4 +43,4 @@ module.exports = function externalLinks(md, siteVariables) {
34
43
  md.core.ruler.push('external_links', state => {
35
44
  state.tokens.map(addClass);
36
45
  });
37
- };
46
+ }
@@ -0,0 +1,11 @@
1
+ import type { SiteVariables } from './types';
2
+
3
+ export function isFeatureEnabled(
4
+ siteVariables: SiteVariables,
5
+ featureName: string,
6
+ ): boolean {
7
+ return (
8
+ siteVariables.features?.[featureName as keyof SiteVariables['features']] !==
9
+ false
10
+ );
11
+ }
@@ -0,0 +1,320 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { makeLogger } from './log';
4
+ import { isFeatureEnabled } from './features';
5
+ import { initHighlighter } from './utils/shiki-highlighter';
6
+ import {
7
+ getBuildContentFiles,
8
+ getContentDir,
9
+ getValidInternalTargets,
10
+ renderCodePageAsset,
11
+ renderCopiedContentAsset,
12
+ renderLiterateJavaPageAsset,
13
+ renderPlainTextPageAsset,
14
+ } from './util';
15
+ import { isLiterateJava } from './utils/file-types';
16
+ import { checkJavac } from './utils/literate-java';
17
+ import type {
18
+ SiteVariables,
19
+ Asset,
20
+ ContentRenderOptions,
21
+ ContentRenderResult,
22
+ WatchState,
23
+ } from './types';
24
+
25
+ const log = makeLogger(__filename);
26
+
27
+ export class ContentRenderer {
28
+ private siteVariables: SiteVariables;
29
+ private loggedCodeDisabled: boolean;
30
+ private sourceFileCache: Map<string, Asset[]>;
31
+ private lastBuildFiles: Set<string>;
32
+ private javacAvailable: boolean | undefined;
33
+
34
+ constructor(siteVariables: SiteVariables) {
35
+ this.siteVariables = siteVariables || {};
36
+ this.loggedCodeDisabled = false;
37
+ this.sourceFileCache = new Map();
38
+ this.lastBuildFiles = new Set();
39
+ }
40
+
41
+ async initHighlighter(): Promise<void> {
42
+ const langs = [
43
+ ...new Set([
44
+ 'plaintext',
45
+ 'text',
46
+ ...Object.values(this.siteVariables.codeLanguages || {}),
47
+ ]),
48
+ ];
49
+ await initHighlighter(langs);
50
+ }
51
+
52
+ getDirtySourceFiles(
53
+ buildContentFiles: string[],
54
+ { changedContentFiles, templatesChanged }: WatchState = {},
55
+ ): Set<string> {
56
+ if (
57
+ this.lastBuildFiles.size === 0 ||
58
+ !changedContentFiles ||
59
+ templatesChanged
60
+ ) {
61
+ return new Set(buildContentFiles);
62
+ }
63
+
64
+ const buildFileSet = new Set(buildContentFiles);
65
+ const dirtySourceFiles = new Set<string>();
66
+ for (const filePath of changedContentFiles) {
67
+ if (buildFileSet.has(filePath)) {
68
+ dirtySourceFiles.add(filePath);
69
+ }
70
+ }
71
+ return dirtySourceFiles;
72
+ }
73
+
74
+ isCopiedAssetSource(filePath: string): boolean {
75
+ const ext = path.extname(filePath).toLowerCase();
76
+ const codeExtensions = this.siteVariables.codeLanguages || {};
77
+ return Object.prototype.hasOwnProperty.call(codeExtensions, ext.slice(1));
78
+ }
79
+
80
+ getDirtyCopiedSourceFiles(
81
+ buildContentFiles: string[],
82
+ { changedContentFiles }: WatchState = {},
83
+ ): string[] {
84
+ if (this.lastBuildFiles.size === 0 || !changedContentFiles) {
85
+ return buildContentFiles.filter(filePath =>
86
+ this.isCopiedAssetSource(filePath),
87
+ );
88
+ }
89
+
90
+ const buildFileSet = new Set(buildContentFiles);
91
+ const dirtyCopiedSourceFiles: string[] = [];
92
+ for (const filePath of changedContentFiles) {
93
+ if (!buildFileSet.has(filePath) || !this.isCopiedAssetSource(filePath)) {
94
+ continue;
95
+ }
96
+ dirtyCopiedSourceFiles.push(filePath);
97
+ }
98
+
99
+ return dirtyCopiedSourceFiles;
100
+ }
101
+
102
+ renderSourceAssets(
103
+ filePath: string,
104
+ contentDir: string,
105
+ distDir: string,
106
+ validInternalTargets: Set<string>,
107
+ assetFiles: string[],
108
+ ): Asset[] {
109
+ const ext = path.extname(filePath).toLowerCase();
110
+ const assets: Asset[] = [];
111
+
112
+ if (isLiterateJava(filePath)) {
113
+ assets.push(
114
+ ...renderLiterateJavaPageAsset({
115
+ filePath,
116
+ contentDir,
117
+ distDir,
118
+ siteVariables: this.siteVariables,
119
+ assetFiles,
120
+ skipExecution: !this.javacAvailable,
121
+ }),
122
+ );
123
+ return assets;
124
+ }
125
+
126
+ if (ext === '.html' || ext === '.md' || ext === '.markdown') {
127
+ assets.push(
128
+ ...renderPlainTextPageAsset({
129
+ filePath,
130
+ contentDir,
131
+ distDir,
132
+ siteVariables: this.siteVariables,
133
+ validInternalTargets,
134
+ assetFiles,
135
+ }),
136
+ );
137
+ return assets;
138
+ }
139
+
140
+ const codeExtensions = this.siteVariables.codeLanguages || {};
141
+ if (ext.slice(1) in codeExtensions) {
142
+ if (isFeatureEnabled(this.siteVariables, 'code')) {
143
+ assets.push(
144
+ ...renderCodePageAsset({
145
+ filePath,
146
+ contentDir,
147
+ distDir,
148
+ siteVariables: this.siteVariables,
149
+ assetFiles,
150
+ }),
151
+ );
152
+ } else if (!this.loggedCodeDisabled) {
153
+ log.info`Not generating source code pages due to site.features.code = false`;
154
+ this.loggedCodeDisabled = true;
155
+ }
156
+ }
157
+
158
+ return assets;
159
+ }
160
+
161
+ writeUncachedAssets(
162
+ distDir: string,
163
+ copiedSourceFiles: string[],
164
+ contentDir: string,
165
+ ): void {
166
+ for (const filePath of copiedSourceFiles) {
167
+ for (const asset of renderCopiedContentAsset({ filePath, contentDir })) {
168
+ const outPath = path.join(distDir, asset.assetPath);
169
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
170
+ fs.writeFileSync(outPath, asset.content);
171
+ }
172
+ }
173
+ }
174
+
175
+ updateSourceCache(
176
+ filePath: string,
177
+ assets: Asset[],
178
+ changedHtmlAssetPaths: Set<string>,
179
+ removedHtmlAssetPaths: Set<string>,
180
+ ): void {
181
+ const previousAssets = this.sourceFileCache.get(filePath) || [];
182
+ const nextAssetPaths = new Set(assets.map(asset => asset.assetPath));
183
+
184
+ for (const asset of previousAssets) {
185
+ if (nextAssetPaths.has(asset.assetPath)) {
186
+ continue;
187
+ }
188
+ if (asset.assetPath.endsWith('.html')) {
189
+ removedHtmlAssetPaths.add(asset.assetPath);
190
+ }
191
+ }
192
+
193
+ for (const asset of assets) {
194
+ if (asset.assetPath.endsWith('.html')) {
195
+ changedHtmlAssetPaths.add(asset.assetPath);
196
+ }
197
+ }
198
+
199
+ this.sourceFileCache.set(filePath, assets);
200
+ }
201
+
202
+ writeCachedAssets(distDir: string, buildContentFiles: string[]): void {
203
+ for (const filePath of buildContentFiles) {
204
+ const assets = this.sourceFileCache.get(filePath) || [];
205
+ for (const asset of assets) {
206
+ const outPath = path.join(distDir, asset.assetPath);
207
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
208
+ fs.writeFileSync(outPath, asset.content);
209
+ }
210
+ }
211
+ }
212
+
213
+ pruneRemovedSources(
214
+ buildFileSet: Set<string>,
215
+ removedHtmlAssetPaths: Set<string>,
216
+ ): void {
217
+ for (const filePath of [...this.lastBuildFiles]) {
218
+ if (buildFileSet.has(filePath)) {
219
+ continue;
220
+ }
221
+
222
+ const previousAssets = this.sourceFileCache.get(filePath) || [];
223
+ for (const asset of previousAssets) {
224
+ if (asset.assetPath.endsWith('.html')) {
225
+ removedHtmlAssetPaths.add(asset.assetPath);
226
+ }
227
+ }
228
+ this.sourceFileCache.delete(filePath);
229
+ }
230
+ }
231
+
232
+ processContent({
233
+ distDir,
234
+ assetFiles,
235
+ watchState,
236
+ }: ContentRenderOptions): ContentRenderResult {
237
+ const contentDir: string = getContentDir();
238
+ const buildContentFiles: string[] = getBuildContentFiles(
239
+ contentDir,
240
+ Object.keys(this.siteVariables.codeLanguages || {}),
241
+ );
242
+ const buildFileSet = new Set<string>(buildContentFiles);
243
+ const dirtySourceFiles = this.getDirtySourceFiles(
244
+ buildContentFiles,
245
+ watchState,
246
+ );
247
+ const dirtyCopiedSourceFiles = this.getDirtyCopiedSourceFiles(
248
+ buildContentFiles,
249
+ watchState,
250
+ );
251
+ const changedHtmlAssetPaths = new Set<string>();
252
+ const removedHtmlAssetPaths = new Set<string>();
253
+ const validInternalTargets = getValidInternalTargets(
254
+ contentDir,
255
+ buildContentFiles,
256
+ Object.keys(this.siteVariables.codeLanguages || {}),
257
+ );
258
+ const errors: Error[] = [];
259
+
260
+ this.pruneRemovedSources(buildFileSet, removedHtmlAssetPaths);
261
+
262
+ if (dirtySourceFiles.size > 0) {
263
+ const noun = dirtySourceFiles.size === 1 ? 'file' : 'files';
264
+ log.info`Processing ${dirtySourceFiles.size} content ${noun}`;
265
+ }
266
+
267
+ const hasLiterateJava = [...dirtySourceFiles].some(isLiterateJava);
268
+ if (hasLiterateJava && this.javacAvailable === undefined) {
269
+ this.javacAvailable = checkJavac();
270
+ if (!this.javacAvailable) {
271
+ log.warn`javac was not found; literate Java pages will not include execution output`;
272
+ }
273
+ }
274
+
275
+ for (const filePath of dirtySourceFiles) {
276
+ let assets: Asset[];
277
+ try {
278
+ assets = this.renderSourceAssets(
279
+ filePath,
280
+ contentDir,
281
+ distDir,
282
+ validInternalTargets,
283
+ assetFiles,
284
+ );
285
+ } catch (err) {
286
+ errors.push(err as Error);
287
+ continue;
288
+ }
289
+ this.updateSourceCache(
290
+ filePath,
291
+ assets,
292
+ changedHtmlAssetPaths,
293
+ removedHtmlAssetPaths,
294
+ );
295
+ }
296
+
297
+ this.writeCachedAssets(distDir, buildContentFiles);
298
+ this.writeUncachedAssets(distDir, dirtyCopiedSourceFiles, contentDir);
299
+ this.lastBuildFiles = buildFileSet;
300
+
301
+ // Collect HTML asset content for Pagefind
302
+ const htmlAssetsByPath = new Map<string, string>();
303
+ for (const filePath of buildContentFiles) {
304
+ const assets = this.sourceFileCache.get(filePath) || [];
305
+ for (const asset of assets) {
306
+ if (asset.assetPath.endsWith('.html')) {
307
+ htmlAssetsByPath.set(asset.assetPath, asset.content as string);
308
+ }
309
+ }
310
+ }
311
+
312
+ return {
313
+ errors,
314
+ changedHtmlAssetPaths,
315
+ removedHtmlAssetPaths,
316
+ htmlAssetsByPath,
317
+ buildContentFiles,
318
+ };
319
+ }
320
+ }