@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.
- package/README.md +96 -48
- package/bin/tada.ts +621 -0
- package/bin/validators.test.ts +205 -0
- package/bin/validators.ts +85 -0
- package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +37 -17
- package/build/build-manifest.test.ts +349 -0
- package/build/build-manifest.ts +165 -0
- package/build/bundle.ts +116 -0
- package/{webpack/code.test.js → build/code.test.ts} +10 -7
- package/build/colors.ts +25 -0
- package/build/content-watch.ts +107 -0
- package/build/copy.ts +118 -0
- package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
- package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
- package/build/features.ts +11 -0
- package/build/generate-content-assets.ts +320 -0
- package/build/generate-favicon.ts +172 -0
- package/build/generate-fonts.ts +31 -0
- package/build/generate-katex-assets.ts +35 -0
- package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
- package/build/globals.test.ts +99 -0
- package/build/globals.ts +88 -0
- package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
- package/build/json-schema.test.ts +57 -0
- package/build/json-schema.ts +33 -0
- package/build/log.test.ts +111 -0
- package/build/log.ts +167 -0
- package/build/markdown-plugins.test.ts +396 -0
- package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
- package/build/pagefind.ts +339 -0
- package/build/pdf-text.test.ts +52 -0
- package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
- package/build/pipeline.ts +120 -0
- package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
- package/{webpack/reachability.js → build/reachability.ts} +77 -34
- package/build/serve.ts +112 -0
- package/build/site-variables.ts +68 -0
- package/{webpack/templates.js → build/templates.ts} +38 -35
- package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
- package/build/toc-plugin.test.ts +105 -0
- package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
- package/build/types.ts +181 -0
- package/build/util.ts +26 -0
- package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
- package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
- package/build/utils/derive-theme.test.ts +111 -0
- package/build/utils/derive-theme.ts +85 -0
- package/build/utils/file-types.test.ts +61 -0
- package/build/utils/file-types.ts +13 -0
- package/build/utils/front-matter.test.ts +77 -0
- package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
- package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
- package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +68 -38
- package/{webpack/utils/markdown.js → build/utils/markdown.ts} +126 -49
- package/build/utils/paths.test.ts +91 -0
- package/{webpack/utils/paths.js → build/utils/paths.ts} +18 -22
- package/{webpack/utils/render.js → build/utils/render.ts} +208 -130
- package/build/utils/shiki-highlighter.ts +29 -0
- package/build/validate-internal-links-plugin.test.ts +106 -0
- package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
- package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
- package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
- package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
- package/build/watch.ts +549 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable.woff2 +0 -0
- package/{content → init/content}/index.md +9 -3
- package/{content → init/content}/lectures/index.md +1 -1
- package/{content → init/content}/markdown.md +28 -8
- package/init/content/problem_sets/index.html +14 -0
- package/package.json +33 -22
- package/{templates → schema}/nav.schema.json +1 -2
- package/{webpack → schema}/site.schema.json +4 -12
- package/src/_alerts.scss +96 -0
- package/src/_base.scss +145 -0
- package/src/{layout.scss → _layout.scss} +36 -1
- package/src/anchor/index.ts +0 -1
- package/src/anchor/style.scss +1 -9
- package/src/code/index.ts +3 -3
- package/src/code.scss +1 -1
- package/src/critical.scss +5 -0
- package/src/header/_base.scss +129 -0
- package/src/header/style.scss +3 -131
- package/src/index.ts +4 -5
- package/src/question/style.scss +1 -1
- package/src/search/index.test.ts +203 -0
- package/src/search/index.ts +38 -93
- package/src/search/pdf-utils.ts +79 -0
- package/src/search/style.scss +8 -28
- package/src/style.scss +15 -326
- package/src/timezone/index.ts +5 -6
- package/src/toc/style.scss +5 -39
- package/src/util.ts +9 -6
- package/templates/_theme.scss +38 -14
- package/templates/_top.html +1 -1
- package/tsconfig.json +10 -6
- package/types/dev.d.ts +1 -0
- package/types/file-system-access.d.ts +5 -0
- package/types/markdown-it-plugins.d.ts +11 -0
- package/types/site-variables.d.ts +6 -10
- package/types/untyped-modules.d.ts +47 -0
- package/bin/tada.js +0 -361
- package/config/authors.json +0 -1
- package/config/nav.json +0 -28
- package/content/problem_sets/index.md +0 -6
- package/types/dev.ts +0 -3
- package/webpack/build-state.js +0 -97
- package/webpack/colors.js +0 -15
- package/webpack/config.base.js +0 -151
- package/webpack/config.dev.js +0 -23
- package/webpack/config.prod.js +0 -32
- package/webpack/content-watch-plugin.js +0 -153
- package/webpack/features.js +0 -5
- package/webpack/generate-content-assets-plugin.js +0 -308
- package/webpack/generate-favicon-plugin.js +0 -198
- package/webpack/generate-fonts-plugin.js +0 -69
- package/webpack/globals.js +0 -74
- package/webpack/json-schema.js +0 -19
- package/webpack/log.js +0 -143
- package/webpack/markdown-plugins.test.js +0 -203
- package/webpack/pagefind-plugin.js +0 -379
- package/webpack/print-flair-plugin.js +0 -22
- package/webpack/serve.js +0 -104
- package/webpack/site-variables.js +0 -53
- package/webpack/util.js +0 -49
- package/webpack/utils/define-plugin.js +0 -20
- package/webpack/utils/file-types.js +0 -26
- package/webpack/utils/parse-hsl.js +0 -8
- package/webpack/utils/shiki-highlighter.js +0 -26
- package/webpack/watch.js +0 -166
- /package/{webpack → build}/flair.json +0 -0
- /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
- /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
- /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
- /package/{content → init/content}/lectures/01/Pair.java.md +0 -0
- /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
- /package/{content → init/content}/lectures/01/demo.py +0 -0
- /package/{content → init/content}/lectures/01/index.md +0 -0
- /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
- /package/{public → init/public}/avatars/alex.jpg +0 -0
- /package/{public → init/public}/test.txt +0 -0
- /package/{templates → schema}/authors.schema.json +0 -0
package/build/watch.ts
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fork } from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
7
|
+
import type { SiteVariables, WatchState } from './types';
|
|
8
|
+
import { B } from './colors';
|
|
9
|
+
import { makeLogger, printFlair } from './log';
|
|
10
|
+
import { getDevSiteVariables } from './site-variables';
|
|
11
|
+
import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
|
|
12
|
+
import {
|
|
13
|
+
getContentDir,
|
|
14
|
+
getPublicDir,
|
|
15
|
+
getDistDir,
|
|
16
|
+
getPackageDir,
|
|
17
|
+
} from './utils/paths';
|
|
18
|
+
import { isFeatureEnabled } from './features';
|
|
19
|
+
import { bundle } from './bundle';
|
|
20
|
+
import { copyFonts } from './generate-fonts';
|
|
21
|
+
import { copyKatexAssets } from './generate-katex-assets';
|
|
22
|
+
import { generateFavicons } from './generate-favicon';
|
|
23
|
+
import { generateManifest } from './generate-manifest';
|
|
24
|
+
import {
|
|
25
|
+
copyPublicFiles,
|
|
26
|
+
copyContentAssets,
|
|
27
|
+
copyContentFile,
|
|
28
|
+
copyPublicFile,
|
|
29
|
+
} from './copy';
|
|
30
|
+
import { getProcessedExtensions } from './utils/file-types';
|
|
31
|
+
import { ContentRenderer } from './generate-content-assets';
|
|
32
|
+
import { WatchPagefindRunner } from './pagefind';
|
|
33
|
+
import { ContentChangeDetector } from './content-watch';
|
|
34
|
+
|
|
35
|
+
function getArg(name: string): number | null {
|
|
36
|
+
const idx = process.argv.indexOf(name);
|
|
37
|
+
if (idx !== -1 && process.argv[idx + 1]) {
|
|
38
|
+
const val = parseInt(process.argv[idx + 1], 10);
|
|
39
|
+
if (val > 0 && val < 65536) {
|
|
40
|
+
return val;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const httpPort = getArg('--port');
|
|
47
|
+
const WEBSOCKET_PORT = getArg('--ws-port') ?? 35729;
|
|
48
|
+
const DEBOUNCE_MS = 300;
|
|
49
|
+
const RELOAD_CLIENT_PATH = path.resolve(
|
|
50
|
+
getPackageDir(),
|
|
51
|
+
'build/watch-reload-client.ts',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Bundle the reload client separately to work around a Bun bundler bug where
|
|
55
|
+
// side-effect-only JS entrypoints are tree-shaken when bundled alongside SCSS
|
|
56
|
+
// entrypoints processed by a plugin.
|
|
57
|
+
async function bundleReloadClient(): Promise<string[]> {
|
|
58
|
+
const result = await Bun.build({
|
|
59
|
+
entrypoints: [RELOAD_CLIENT_PATH],
|
|
60
|
+
outdir: getDistDir(),
|
|
61
|
+
naming: '[name].bundle.[ext]',
|
|
62
|
+
sourcemap: 'inline',
|
|
63
|
+
define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
|
|
64
|
+
});
|
|
65
|
+
return result.outputs.map(output =>
|
|
66
|
+
path
|
|
67
|
+
.relative(getDistDir(), output.path)
|
|
68
|
+
.split(path.sep)
|
|
69
|
+
.join(path.posix.sep),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type ChangeCategory = 'content' | 'public' | 'config';
|
|
74
|
+
|
|
75
|
+
const log = makeLogger(__filename);
|
|
76
|
+
const wslog = makeLogger('WebSocket');
|
|
77
|
+
|
|
78
|
+
// --- WebSocket server (unchanged) ---
|
|
79
|
+
|
|
80
|
+
let webSocketsReady = false;
|
|
81
|
+
let watcherReady = false;
|
|
82
|
+
let webServerReady = false;
|
|
83
|
+
let webServerTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
84
|
+
let serveStarted = false;
|
|
85
|
+
|
|
86
|
+
let wss: WebSocketServer | null = null;
|
|
87
|
+
try {
|
|
88
|
+
wss = new WebSocketServer({ port: WEBSOCKET_PORT });
|
|
89
|
+
|
|
90
|
+
wss.on('connection', (conn: WebSocket) => {
|
|
91
|
+
wslog.debug`WebSocket client connected`;
|
|
92
|
+
if (watcherReady && conn.readyState === WebSocket.OPEN) {
|
|
93
|
+
if (initialBuildFailed) {
|
|
94
|
+
conn.send('error');
|
|
95
|
+
}
|
|
96
|
+
conn.send('ready');
|
|
97
|
+
}
|
|
98
|
+
conn.on('close', () => {
|
|
99
|
+
wslog.debug`WebSocket client disconnected`;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
wss.on('error', (err: Error) => {
|
|
104
|
+
wslog.error`WebSocket server error: ${err.message}`;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
wss.on('listening', () => {
|
|
108
|
+
wslog.debug`WebSocket server listening at ws://localhost:${WEBSOCKET_PORT}`;
|
|
109
|
+
webSocketsReady = true;
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
wslog.error`Failed to start WebSocket server on port ${WEBSOCKET_PORT}: ${(err as Error).message}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function broadcast(msg: string): void {
|
|
116
|
+
if (wss == null || !webSocketsReady) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
wslog.debug`Broadcasting "${msg}" to WebSocket clients`;
|
|
120
|
+
wss.clients.forEach((client: WebSocket) => {
|
|
121
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
122
|
+
client.send(msg);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Dev server ---
|
|
128
|
+
|
|
129
|
+
function serve(): void {
|
|
130
|
+
const serveArgs = httpPort != null ? ['--port', String(httpPort)] : [];
|
|
131
|
+
const child = fork(path.join(__dirname, 'serve.js'), serveArgs, {
|
|
132
|
+
stdio: 'inherit',
|
|
133
|
+
});
|
|
134
|
+
child.on('close', (code: number | null) => {
|
|
135
|
+
webServerReady = false;
|
|
136
|
+
log.error`Web server exited with code ${code}`;
|
|
137
|
+
process.exit(2);
|
|
138
|
+
});
|
|
139
|
+
child.on('error', (err: Error) => {
|
|
140
|
+
webServerReady = false;
|
|
141
|
+
log.error`Web server failed: ${err.message}`;
|
|
142
|
+
});
|
|
143
|
+
child.on('message', (msg: Record<string, unknown>) => {
|
|
144
|
+
if (msg.ready) {
|
|
145
|
+
webServerReady = true;
|
|
146
|
+
clearTimeout(webServerTimeout);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
webServerTimeout = setTimeout(() => {
|
|
151
|
+
if (webServerReady) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
log.error`Web server failed to report within 10 seconds, exiting`;
|
|
155
|
+
process.exit(3);
|
|
156
|
+
}, 10000);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Path helpers ---
|
|
160
|
+
|
|
161
|
+
function toContentMarkdownPath(filePath: string): string | null {
|
|
162
|
+
if (!filePath) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
166
|
+
if (!['.md', '.markdown'].includes(ext)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalizedContentDir = path.resolve(contentDir) + path.sep;
|
|
171
|
+
const normalizedFilePath = path.resolve(filePath);
|
|
172
|
+
if (!normalizedFilePath.startsWith(normalizedContentDir)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return path
|
|
177
|
+
.relative(contentDir, normalizedFilePath)
|
|
178
|
+
.split(path.sep)
|
|
179
|
+
.join(path.posix.sep);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function toPublicRelativePath(filePath: string): string | null {
|
|
183
|
+
if (!filePath) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const normalizedPublicDir = path.resolve(publicDir) + path.sep;
|
|
187
|
+
const normalizedFilePath = path.resolve(filePath);
|
|
188
|
+
if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return path
|
|
192
|
+
.relative(publicDir, normalizedFilePath)
|
|
193
|
+
.split(path.sep)
|
|
194
|
+
.join(path.posix.sep);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- Watch mode ---
|
|
198
|
+
|
|
199
|
+
const contentDir: string = getContentDir();
|
|
200
|
+
const publicDir: string = getPublicDir();
|
|
201
|
+
const distDir: string = getDistDir();
|
|
202
|
+
|
|
203
|
+
let siteVariables: SiteVariables;
|
|
204
|
+
let processedExtSet = new Set<string>();
|
|
205
|
+
let publicRelPaths: Set<string> = new Set();
|
|
206
|
+
let contentAssetRelPaths: Set<string> = new Set();
|
|
207
|
+
let contentRenderer: ContentRenderer;
|
|
208
|
+
let changeDetector: ContentChangeDetector;
|
|
209
|
+
let pagefindRunner: WatchPagefindRunner | undefined;
|
|
210
|
+
let assetFiles: string[] = [];
|
|
211
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
212
|
+
const pendingChanges = new Set<string>();
|
|
213
|
+
let rebuilding = false;
|
|
214
|
+
|
|
215
|
+
async function initialBuild(): Promise<void> {
|
|
216
|
+
siteVariables = getDevSiteVariables();
|
|
217
|
+
processedExtSet = new Set(
|
|
218
|
+
getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
|
|
219
|
+
);
|
|
220
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
221
|
+
|
|
222
|
+
compileTemplates(siteVariables);
|
|
223
|
+
contentRenderer = new ContentRenderer(siteVariables);
|
|
224
|
+
changeDetector = new ContentChangeDetector(siteVariables);
|
|
225
|
+
|
|
226
|
+
if (isFeatureEnabled(siteVariables, 'search')) {
|
|
227
|
+
pagefindRunner = new WatchPagefindRunner(siteVariables);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await contentRenderer.initHighlighter();
|
|
231
|
+
|
|
232
|
+
assetFiles = [
|
|
233
|
+
...(await bundle(siteVariables, { mode: 'development' })),
|
|
234
|
+
...(await bundleReloadClient()),
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
copyFonts(distDir);
|
|
238
|
+
copyKatexAssets(distDir);
|
|
239
|
+
|
|
240
|
+
if (isFeatureEnabled(siteVariables, 'favicon')) {
|
|
241
|
+
await generateFavicons(siteVariables, distDir);
|
|
242
|
+
generateManifest(siteVariables, distDir);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
publicRelPaths = copyPublicFiles(publicDir, distDir);
|
|
246
|
+
contentAssetRelPaths = copyContentAssets(
|
|
247
|
+
contentDir,
|
|
248
|
+
distDir,
|
|
249
|
+
[...processedExtSet],
|
|
250
|
+
publicRelPaths,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const result = contentRenderer.processContent({ distDir, assetFiles });
|
|
254
|
+
|
|
255
|
+
for (const err of result.errors) {
|
|
256
|
+
log.error`${err.message}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (result.errors.length === 0) {
|
|
260
|
+
printFlair();
|
|
261
|
+
|
|
262
|
+
if (pagefindRunner) {
|
|
263
|
+
pagefindRunner.update(distDir, result.htmlAssetsByPath);
|
|
264
|
+
setImmediate(() => pagefindRunner!.run());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!serveStarted) {
|
|
268
|
+
serveStarted = true;
|
|
269
|
+
serve();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function classifyChange(filePath: string): ChangeCategory | null {
|
|
275
|
+
const resolved = path.resolve(filePath);
|
|
276
|
+
const resolvedContentDir = path.resolve(contentDir) + path.sep;
|
|
277
|
+
const resolvedPublicDir = path.resolve(publicDir) + path.sep;
|
|
278
|
+
const jsonDataDir = getJsonDataDir();
|
|
279
|
+
const siteConfigPath = path.resolve('site.dev.json');
|
|
280
|
+
|
|
281
|
+
if (resolved.startsWith(resolvedContentDir)) {
|
|
282
|
+
return 'content';
|
|
283
|
+
}
|
|
284
|
+
if (resolved.startsWith(resolvedPublicDir)) {
|
|
285
|
+
return 'public';
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
resolved === siteConfigPath ||
|
|
289
|
+
JSON_DATA_FILES.some(f => resolved === path.resolve(jsonDataDir, f))
|
|
290
|
+
) {
|
|
291
|
+
return 'config';
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function rebuild(): Promise<void> {
|
|
297
|
+
if (rebuilding) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
rebuilding = true;
|
|
301
|
+
|
|
302
|
+
const changes = new Set(pendingChanges);
|
|
303
|
+
pendingChanges.clear();
|
|
304
|
+
|
|
305
|
+
broadcast('rebuilding');
|
|
306
|
+
|
|
307
|
+
// Classify changes
|
|
308
|
+
const categories = new Set<ChangeCategory>();
|
|
309
|
+
for (const filePath of changes) {
|
|
310
|
+
const category = classifyChange(filePath);
|
|
311
|
+
if (category) {
|
|
312
|
+
categories.add(category);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let succeeded = false;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
// Site config changed, full restart
|
|
320
|
+
if (categories.has('config')) {
|
|
321
|
+
log.event`Site config changed, restarting`;
|
|
322
|
+
siteVariables = getDevSiteVariables();
|
|
323
|
+
processedExtSet = new Set(
|
|
324
|
+
getProcessedExtensions(Object.keys(siteVariables.codeLanguages || {})),
|
|
325
|
+
);
|
|
326
|
+
contentRenderer = new ContentRenderer(siteVariables);
|
|
327
|
+
changeDetector = new ContentChangeDetector(siteVariables);
|
|
328
|
+
if (isFeatureEnabled(siteVariables, 'search')) {
|
|
329
|
+
pagefindRunner = new WatchPagefindRunner(siteVariables);
|
|
330
|
+
}
|
|
331
|
+
compileTemplates(siteVariables);
|
|
332
|
+
await contentRenderer.initHighlighter();
|
|
333
|
+
|
|
334
|
+
assetFiles = [
|
|
335
|
+
...(await bundle(siteVariables, { mode: 'development' })),
|
|
336
|
+
...(await bundleReloadClient()),
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
copyFonts(distDir);
|
|
340
|
+
copyKatexAssets(distDir);
|
|
341
|
+
|
|
342
|
+
if (isFeatureEnabled(siteVariables, 'favicon')) {
|
|
343
|
+
await generateFavicons(siteVariables, distDir);
|
|
344
|
+
generateManifest(siteVariables, distDir);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const result = contentRenderer.processContent({ distDir, assetFiles });
|
|
348
|
+
for (const err of result.errors) {
|
|
349
|
+
log.error`${err.message}`;
|
|
350
|
+
}
|
|
351
|
+
if (result.errors.length === 0) {
|
|
352
|
+
printFlair();
|
|
353
|
+
if (!serveStarted) {
|
|
354
|
+
serveStarted = true;
|
|
355
|
+
serve();
|
|
356
|
+
}
|
|
357
|
+
broadcast('reload');
|
|
358
|
+
if (pagefindRunner) {
|
|
359
|
+
pagefindRunner.update(distDir, result.htmlAssetsByPath);
|
|
360
|
+
setImmediate(() => pagefindRunner!.run());
|
|
361
|
+
}
|
|
362
|
+
succeeded = true;
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Public file changed, copy just that file
|
|
368
|
+
if (categories.has('public') && !categories.has('content')) {
|
|
369
|
+
for (const filePath of changes) {
|
|
370
|
+
if (classifyChange(filePath) === 'public') {
|
|
371
|
+
const absPath = path.resolve(filePath);
|
|
372
|
+
if (fs.existsSync(absPath)) {
|
|
373
|
+
copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
|
|
374
|
+
const rel = path
|
|
375
|
+
.relative(publicDir, absPath)
|
|
376
|
+
.split(path.sep)
|
|
377
|
+
.join(path.posix.sep);
|
|
378
|
+
publicRelPaths.add(rel);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
printFlair();
|
|
383
|
+
broadcast('reload');
|
|
384
|
+
succeeded = true;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Content changed, incremental rebuild
|
|
389
|
+
const detection = changeDetector.detectChanges(changes);
|
|
390
|
+
|
|
391
|
+
if (detection.needsRestart) {
|
|
392
|
+
log.event`Content structure changed, full rebuild`;
|
|
393
|
+
contentRenderer = new ContentRenderer(siteVariables);
|
|
394
|
+
await contentRenderer.initHighlighter();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Log changed content files
|
|
398
|
+
for (const filePath of changes) {
|
|
399
|
+
const markdownPath = toContentMarkdownPath(filePath);
|
|
400
|
+
if (markdownPath) {
|
|
401
|
+
log.event`${B`${markdownPath}`} changed, rebuilding`;
|
|
402
|
+
} else {
|
|
403
|
+
const pubPath = toPublicRelativePath(filePath);
|
|
404
|
+
if (pubPath) {
|
|
405
|
+
log.event`${B`public/${pubPath}`} changed`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Copy any changed public files too
|
|
411
|
+
if (categories.has('public')) {
|
|
412
|
+
for (const filePath of changes) {
|
|
413
|
+
if (classifyChange(filePath) === 'public') {
|
|
414
|
+
const absPath = path.resolve(filePath);
|
|
415
|
+
if (fs.existsSync(absPath)) {
|
|
416
|
+
copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
|
|
417
|
+
const rel = path
|
|
418
|
+
.relative(publicDir, absPath)
|
|
419
|
+
.split(path.sep)
|
|
420
|
+
.join(path.posix.sep);
|
|
421
|
+
publicRelPaths.add(rel);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Copy any changed non-processed content files (images, PDFs, etc.)
|
|
428
|
+
if (categories.has('content')) {
|
|
429
|
+
for (const filePath of changes) {
|
|
430
|
+
if (classifyChange(filePath) !== 'content') {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const absPath = path.resolve(filePath);
|
|
434
|
+
const ext = path.extname(absPath).slice(1).toLowerCase();
|
|
435
|
+
if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
|
|
436
|
+
copyContentFile(contentDir, distDir, absPath, publicRelPaths);
|
|
437
|
+
const rel = path
|
|
438
|
+
.relative(contentDir, absPath)
|
|
439
|
+
.split(path.sep)
|
|
440
|
+
.join(path.posix.sep);
|
|
441
|
+
contentAssetRelPaths.add(rel);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const watchState: WatchState | undefined = detection.needsRestart
|
|
447
|
+
? undefined
|
|
448
|
+
: {
|
|
449
|
+
changedContentFiles: detection.changedContentFiles,
|
|
450
|
+
templatesChanged: detection.templatesChanged,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const result = contentRenderer.processContent({
|
|
454
|
+
distDir,
|
|
455
|
+
assetFiles,
|
|
456
|
+
watchState,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
for (const err of result.errors) {
|
|
460
|
+
log.error`${err.message}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (result.errors.length === 0) {
|
|
464
|
+
printFlair();
|
|
465
|
+
if (!serveStarted) {
|
|
466
|
+
serveStarted = true;
|
|
467
|
+
serve();
|
|
468
|
+
}
|
|
469
|
+
broadcast('reload');
|
|
470
|
+
|
|
471
|
+
if (pagefindRunner) {
|
|
472
|
+
pagefindRunner.update(distDir, result.htmlAssetsByPath);
|
|
473
|
+
setImmediate(() => pagefindRunner!.run());
|
|
474
|
+
}
|
|
475
|
+
succeeded = true;
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
log.error`Build failed: ${(err as Error).message}`;
|
|
479
|
+
} finally {
|
|
480
|
+
if (!succeeded) {
|
|
481
|
+
broadcast('error');
|
|
482
|
+
}
|
|
483
|
+
rebuilding = false;
|
|
484
|
+
|
|
485
|
+
// If more changes accumulated during rebuild, schedule another
|
|
486
|
+
if (pendingChanges.size > 0) {
|
|
487
|
+
scheduleRebuild();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function scheduleRebuild(): void {
|
|
493
|
+
if (debounceTimer) {
|
|
494
|
+
clearTimeout(debounceTimer);
|
|
495
|
+
}
|
|
496
|
+
debounceTimer = setTimeout(rebuild, DEBOUNCE_MS);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function onFileChange(filePath: string): void {
|
|
500
|
+
pendingChanges.add(filePath);
|
|
501
|
+
scheduleRebuild();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// --- Start ---
|
|
505
|
+
|
|
506
|
+
let initialBuildFailed = false;
|
|
507
|
+
|
|
508
|
+
initialBuild()
|
|
509
|
+
.catch(err => {
|
|
510
|
+
log.error`Initial build failed: ${(err as Error).message}`;
|
|
511
|
+
initialBuildFailed = true;
|
|
512
|
+
})
|
|
513
|
+
.finally(() => {
|
|
514
|
+
// Watch content, public, data files, site config
|
|
515
|
+
const watchPaths: string[] = [contentDir, publicDir];
|
|
516
|
+
|
|
517
|
+
// Watch data files (nav.json, authors.json)
|
|
518
|
+
const jsonDataDir = getJsonDataDir();
|
|
519
|
+
for (const dataFile of JSON_DATA_FILES) {
|
|
520
|
+
const dataPath = path.join(jsonDataDir, dataFile);
|
|
521
|
+
if (fs.existsSync(dataPath)) {
|
|
522
|
+
watchPaths.push(dataPath);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Watch site config
|
|
527
|
+
const siteConfigPath = path.resolve('site.dev.json');
|
|
528
|
+
if (fs.existsSync(siteConfigPath)) {
|
|
529
|
+
watchPaths.push(siteConfigPath);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
533
|
+
ignoreInitial: true,
|
|
534
|
+
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
watcher.on('change', onFileChange);
|
|
538
|
+
watcher.on('add', onFileChange);
|
|
539
|
+
watcher.on('unlink', onFileChange);
|
|
540
|
+
|
|
541
|
+
watcher.on('ready', () => {
|
|
542
|
+
watcherReady = true;
|
|
543
|
+
if (initialBuildFailed) {
|
|
544
|
+
broadcast('error');
|
|
545
|
+
}
|
|
546
|
+
broadcast('ready');
|
|
547
|
+
log.info`Watching for changes...`;
|
|
548
|
+
});
|
|
549
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
title: Home
|
|
2
2
|
description: The home page for <%= site.title %>.
|
|
3
3
|
author: alex
|
|
4
|
+
published: 2026-03-20
|
|
5
|
+
toolName: Tada
|
|
4
6
|
|
|
5
7
|
## Welcome
|
|
6
8
|
|
|
7
|
-
This is an example site built with
|
|
9
|
+
This is an example site built with <%= page.toolName %>.
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
## Getting started
|
|
10
13
|
|
|
@@ -14,6 +17,9 @@ contains the site configuration, and `templates/` contains the HTML layouts.
|
|
|
14
17
|
See the [Markdown examples page](/markdown.html) for syntax examples.
|
|
15
18
|
|
|
16
19
|
!!! note
|
|
17
|
-
|
|
18
|
-
documentation on configuration, templates, and content authoring.
|
|
20
|
+
For more documentation, see the [GitHub page][github].
|
|
19
21
|
!!!
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
[github]: https://github.com/abreen/Tada
|
|
@@ -2,7 +2,7 @@ title: Lectures
|
|
|
2
2
|
author: alex
|
|
3
3
|
description: An example page for <%= site.title %>.
|
|
4
4
|
|
|
5
|
-
<%=
|
|
5
|
+
<%= renderTimeZoneChooser() %>
|
|
6
6
|
|
|
7
7
|
One-hour lectures are held weekly at <time datetime="17:40">5:40 PM</time>.
|
|
8
8
|
Each lecture is recorded and made available by password-protected link.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
title: Markdown Examples
|
|
2
2
|
description: Examples of Markdown syntax supported by Tada.
|
|
3
|
-
toc: true
|
|
4
3
|
author: alex
|
|
4
|
+
published: 2026-03-20
|
|
5
|
+
toc: true
|
|
5
6
|
|
|
6
7
|
Markdown and HTML files in the `content/` directory
|
|
7
8
|
must contain ["front matter"][front-matter] (YAML-formatted metadata).
|
|
@@ -127,6 +128,20 @@ See `config/site.dev.json` and `config/site.prod.json` to change `site.basePath`
|
|
|
127
128
|
and `site.internalDomains`.
|
|
128
129
|
|
|
129
130
|
|
|
131
|
+
## LaTeX
|
|
132
|
+
|
|
133
|
+
$\LaTeX$ support is implemented via [KaTeX](https://katex.org/). The standard
|
|
134
|
+
dollar sign-based syntax is supported. Wrap inline math in single `$`,
|
|
135
|
+
or use `$$` for a math block.
|
|
136
|
+
|
|
137
|
+
Here's an example: selection sort performs $O(n^2)$ comparisons. And to
|
|
138
|
+
demonstrate a block, here's the exact number of comparisons:
|
|
139
|
+
|
|
140
|
+
$$
|
|
141
|
+
(n-1) + (n-2) + \dots + 1 = \sum_{i=1}^{n-1}i
|
|
142
|
+
$$
|
|
143
|
+
|
|
144
|
+
|
|
130
145
|
## Additional syntax
|
|
131
146
|
|
|
132
147
|
!!! note Headings are links
|
|
@@ -227,7 +242,7 @@ Beware of this rule.
|
|
|
227
242
|
A time zone chooser for testing styles (see the "time zone chooser" section
|
|
228
243
|
below):
|
|
229
244
|
|
|
230
|
-
<%=
|
|
245
|
+
<%= renderTimeZoneChooser() %>
|
|
231
246
|
|
|
232
247
|
A `<time>` element for testing styles: <time datetime="15:00">3 pm</time>
|
|
233
248
|
|
|
@@ -325,14 +340,19 @@ These features aren't Markdown-specific, but are included here for reference.
|
|
|
325
340
|
|
|
326
341
|
Allows the user to see times in their local time zone.
|
|
327
342
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
343
|
+
Insert the time zone chooser using Lodash templates, calling the
|
|
344
|
+
`renderTimeZoneChooser()` function (made available on any page as a global):
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
<%= '\x3C%= renderTimeZoneChooser() %\x3E' %>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
To produce:
|
|
331
351
|
|
|
332
|
-
<%=
|
|
352
|
+
<%= renderTimeZoneChooser() %>
|
|
333
353
|
|
|
334
|
-
|
|
335
|
-
when the user makes a time zone selection.
|
|
354
|
+
Then, wrap your times in `<time datetime="...">` elements and they will be
|
|
355
|
+
updated when the user makes a time zone selection.
|
|
336
356
|
|
|
337
357
|
Use
|
|
338
358
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
title: Problem Sets
|
|
2
|
+
author: alex
|
|
3
|
+
description: An example page for <%= site.title %> which is not generated.
|
|
4
|
+
skip: true
|
|
5
|
+
|
|
6
|
+
<p>
|
|
7
|
+
For convenience or backwards compatibility, you can write standard HTML
|
|
8
|
+
files. The front matter section should still be specified for HTML files.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p>
|
|
12
|
+
This page is not generated during the build because <code>skip</code>
|
|
13
|
+
is set to <code>true</code>.
|
|
14
|
+
</p>
|