@abreen/tada 1.0.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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/bin/tada.js +361 -0
  4. package/config/authors.json +1 -0
  5. package/config/nav.json +28 -0
  6. package/content/index.md +19 -0
  7. package/content/lectures/01/Pair.java.md +296 -0
  8. package/content/lectures/01/Rectangle.java +80 -0
  9. package/content/lectures/01/demo.py +9 -0
  10. package/content/lectures/01/index.md +39 -0
  11. package/content/lectures/01/lecture1.pdf +0 -0
  12. package/content/lectures/index.md +25 -0
  13. package/content/markdown.md +379 -0
  14. package/content/problem_sets/index.md +6 -0
  15. package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
  16. package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
  17. package/fonts/google-sans-code/LICENSE.txt +93 -0
  18. package/fonts/inter/InterVariable-Italic.ttf +0 -0
  19. package/fonts/inter/InterVariable.ttf +0 -0
  20. package/fonts/inter/LICENSE.txt +92 -0
  21. package/package.json +70 -0
  22. package/public/avatars/alex.jpg +0 -0
  23. package/public/test.txt +1 -0
  24. package/src/_mixins.scss +4 -0
  25. package/src/anchor/README.md +6 -0
  26. package/src/anchor/index.ts +34 -0
  27. package/src/anchor/style.scss +48 -0
  28. package/src/code/README.md +5 -0
  29. package/src/code/index.ts +113 -0
  30. package/src/code/style.scss +101 -0
  31. package/src/code.scss +54 -0
  32. package/src/header/README.md +8 -0
  33. package/src/header/index.ts +43 -0
  34. package/src/header/style.scss +228 -0
  35. package/src/index.ts +73 -0
  36. package/src/layout.scss +144 -0
  37. package/src/literate/style.scss +60 -0
  38. package/src/print/README.md +4 -0
  39. package/src/print/index.ts +32 -0
  40. package/src/print/style.scss +82 -0
  41. package/src/question/README.md +3 -0
  42. package/src/question/index.ts +25 -0
  43. package/src/question/style.scss +116 -0
  44. package/src/search/README.md +6 -0
  45. package/src/search/index.ts +574 -0
  46. package/src/search/style.scss +217 -0
  47. package/src/style.scss +815 -0
  48. package/src/timezone/index.test.ts +100 -0
  49. package/src/timezone/index.ts +298 -0
  50. package/src/timezone/style.scss +16 -0
  51. package/src/timezone/timezones.json +58 -0
  52. package/src/toc/README.md +3 -0
  53. package/src/toc/index.ts +322 -0
  54. package/src/toc/style.scss +203 -0
  55. package/src/top/README.md +4 -0
  56. package/src/top/index.ts +75 -0
  57. package/src/util.ts +122 -0
  58. package/templates/_author.html +27 -0
  59. package/templates/_bottom.html +3 -0
  60. package/templates/_download.html +1 -0
  61. package/templates/_heading.html +19 -0
  62. package/templates/_nav.html +18 -0
  63. package/templates/_theme.scss +97 -0
  64. package/templates/_top.html +87 -0
  65. package/templates/authors.schema.json +13 -0
  66. package/templates/code.html +31 -0
  67. package/templates/default.html +13 -0
  68. package/templates/literate.html +16 -0
  69. package/templates/nav.schema.json +27 -0
  70. package/tsconfig.json +15 -0
  71. package/types/dev.ts +3 -0
  72. package/types/sass.d.ts +1 -0
  73. package/types/site-variables.d.ts +16 -0
  74. package/webpack/apply-base-path-plugin.js +78 -0
  75. package/webpack/build-state.js +97 -0
  76. package/webpack/code.test.js +162 -0
  77. package/webpack/colors.js +15 -0
  78. package/webpack/config.base.js +147 -0
  79. package/webpack/config.dev.js +23 -0
  80. package/webpack/config.prod.js +32 -0
  81. package/webpack/content-watch-plugin.js +153 -0
  82. package/webpack/deflist-id-plugin.js +62 -0
  83. package/webpack/external-links-plugin.js +37 -0
  84. package/webpack/features.js +5 -0
  85. package/webpack/flair.json +1 -0
  86. package/webpack/generate-content-assets-plugin.js +308 -0
  87. package/webpack/generate-favicon-plugin.js +198 -0
  88. package/webpack/generate-fonts-plugin.js +69 -0
  89. package/webpack/generate-manifest-plugin.js +116 -0
  90. package/webpack/globals.js +74 -0
  91. package/webpack/heading-subtitle-plugin.js +80 -0
  92. package/webpack/json-schema.js +19 -0
  93. package/webpack/log.js +143 -0
  94. package/webpack/markdown-plugins.test.js +203 -0
  95. package/webpack/pagefind-plugin.js +379 -0
  96. package/webpack/pagefind-plugin.test.js +131 -0
  97. package/webpack/pdf-text.js +163 -0
  98. package/webpack/print-flair-plugin.js +22 -0
  99. package/webpack/reachability.js +273 -0
  100. package/webpack/reachability.test.js +80 -0
  101. package/webpack/serve.js +104 -0
  102. package/webpack/site-variables.js +53 -0
  103. package/webpack/site.schema.json +67 -0
  104. package/webpack/templates.js +128 -0
  105. package/webpack/text-to-id.js +8 -0
  106. package/webpack/toc-plugin.js +167 -0
  107. package/webpack/util.js +49 -0
  108. package/webpack/utils/code.js +439 -0
  109. package/webpack/utils/content-files.js +147 -0
  110. package/webpack/utils/define-plugin.js +20 -0
  111. package/webpack/utils/file-types.js +26 -0
  112. package/webpack/utils/front-matter.js +57 -0
  113. package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
  114. package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
  115. package/webpack/utils/literate-java.js +153 -0
  116. package/webpack/utils/markdown.js +244 -0
  117. package/webpack/utils/parse-hsl.js +8 -0
  118. package/webpack/utils/paths.js +58 -0
  119. package/webpack/utils/render.js +466 -0
  120. package/webpack/utils/shiki-highlighter.js +26 -0
  121. package/webpack/validate-internal-links-plugin.js +155 -0
  122. package/webpack/watch-reachability-state.js +273 -0
  123. package/webpack/watch-reachability-state.test.js +198 -0
  124. package/webpack/watch-reload-client.js +54 -0
  125. package/webpack/watch.js +166 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alex Breen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # Tada :tada:
2
+
3
+ A static site generator. The successor to Presto.
4
+
5
+ ## Features
6
+
7
+ - Modern design (light & dark following system, floating header, styled lists)
8
+ - Clickable/linkable landmarks (headings, deflists, alert boxes)
9
+ - Dynamic table of contents
10
+ * Floats on the side of the screen when window is large enough
11
+ * Renders headings, alert boxes, and `<hr>` elements
12
+ * Highlights the heading currently being viewed
13
+ - Built-in search powered by [Pagefind][pagefind]
14
+ * Only pages reachable by links on the site appear in search results
15
+ - Generated HTML pages for source code
16
+ * Automatic code highlighting, clickable line numbers
17
+ * Dynamic table of contents for each method/function
18
+ * Converts new Markdown comment syntax ([added in Java 23][jep467]) to HTML
19
+ * Indexed by Pagefind
20
+ - PDF files are copied into `dist/`
21
+ - Text of each PDF page is extracted using [mutool][mutool] and indexed
22
+ - External link handling (special visual treatment for external links)
23
+ - Internal link validation at build time (broken links fail the build)
24
+ - Internal links automatically prefixed with base path, if specified
25
+ - Time zone chooser (automatically adjusts `<datetime>` elements)
26
+ - Extended Markdown syntax
27
+ * `<<< details ... <<<` renders a collapsible box
28
+ * `::: section ... :::` renders a special section with a fancy background
29
+ * `!!! note ... !!!` and `!!! warning ... !!!` render alert boxes
30
+ * `??? review ... ???` renders a Q & A section; answers hidden until click
31
+ * Special heading subtitles with `## Heading # A subtitle here`
32
+ - Automatically generated favicon
33
+ * Text, color, font and font weight taken from config file
34
+
35
+
36
+ ## Installation
37
+
38
+ Install [Bun](https://bun.sh/), then install Tada globally:
39
+
40
+ ```
41
+ bun add -g @abreen/tada
42
+ ```
43
+
44
+
45
+ ## Quick start
46
+
47
+ Create a new site:
48
+
49
+ ```
50
+ tada init mysite
51
+ ```
52
+
53
+ This will ask you a few questions (site title, logo symbol, theme color, etc.)
54
+ and create a new directory with everything you need.
55
+
56
+ Then build and preview your site:
57
+
58
+ ```
59
+ cd mysite
60
+ tada dev
61
+ tada serve
62
+ ```
63
+
64
+ Visit [http://localhost:8080/index.html](http://localhost:8080/index.html).
65
+
66
+
67
+ ## CLI commands
68
+
69
+ ### `tada init <dirname>`
70
+
71
+ Create a new Tada site in a new directory. Prompts for:
72
+ - **Site title** --- displayed in the header and `<title>` tag
73
+ - **Symbol** --- short text (1-5 uppercase characters) shown in the logo and favicon
74
+ - **Theme color** --- HSL color, e.g. `hsl(195 70% 40%)`
75
+ - **Background tint hue** --- hue (0-360) for background/foreground tinting (defaults to `20`)
76
+ - **Background tint amount** --- percentage (0-100) of tint to apply (defaults to `100`)
77
+ - **Default time zone** --- for `<datetime>` elements (defaults to your system zone)
78
+ - **Production base URL** --- e.g. `https://example.edu`
79
+ - **Production base path** --- e.g. `/cs101` (defaults to `/`)
80
+
81
+ Pass `--default` to use the default values for all options without being
82
+ prompted.
83
+
84
+
85
+ ### `tada dev`
86
+
87
+ Build the site for local development (using `config/site.dev.json`)
88
+ into the `dist/` directory.
89
+
90
+
91
+ ### `tada serve`
92
+
93
+ Start a development web server at `http://localhost:8080` which serves the
94
+ files in the `dist/` directory.
95
+
96
+
97
+ ### `tada watch`
98
+
99
+ Start a development web server, watch for changes and rebuild automatically.
100
+
101
+
102
+ ### `tada clean`
103
+
104
+ Remove the `dist/` directory.
105
+
106
+
107
+ ### `tada prod`
108
+
109
+ Build the site for production (uses `config/site.prod.json`).
110
+
111
+
112
+ ## Prerequisites
113
+
114
+ - [Bun](https://bun.sh/)
115
+ - [MuPDF](https://mupdf.com/) (optional, for PDF text extraction in search)
116
+ - On macOS: `brew install mupdf-tools`
117
+ - On Fedora: `dnf install mupdf`
118
+
119
+ > You may skip MuPDF if you don't need search results to include links to PDF
120
+ > pages. You can also turn off `features.search` in the config to disable
121
+ > search entirely.
122
+
123
+
124
+ ## Configuration
125
+
126
+ Build-time site config lives in:
127
+
128
+ - `config/site.dev.json` (used by `tada dev` / `tada watch`)
129
+ - `config/site.prod.json` (used by `tada prod`)
130
+ - `config/nav.json` (navigation structure)
131
+ - `config/authors.json` (author data)
132
+
133
+ Example site configuration JSON file:
134
+
135
+ ```json
136
+ {
137
+ "title": "Intro to Computer Science",
138
+ "titlePostfix": " - CS 0",
139
+ "symbol": "CS 0",
140
+ "themeColor": "hsl(351 70% 40%)",
141
+ "tintAmount": 0,
142
+ "features": { "search": true, "code": true, "favicon": true },
143
+ "base": "https://example.edu",
144
+ "basePath": "/cs0",
145
+ "internalDomains": ["example.edu"],
146
+ "defaultTimeZone": "America/New_York",
147
+ "codeLanguages": { "java": "java", "py": "python" },
148
+ "vars": {
149
+ "staffEmail": "staff@example.edu"
150
+ }
151
+ }
152
+ ```
153
+
154
+ | Field | Description |
155
+ |-------|-------------|
156
+ | `title` | Title for the whole site (also used to derive `titlePostfix`) |
157
+ | `titlePostfix` | *Optional*, the string to append to each page's `title` |
158
+ | `symbol` | Text (1-5 chars) displayed in header (also used as the favicon symbol) |
159
+ | `themeColor` | HSL theme color for the site, e.g. `"hsl(195 70% 40%)"` |
160
+ | `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `20`) |
161
+ | `tintAmount` | *Optional*, percentage (0-100) of tint to apply (default `100`) |
162
+ | `faviconSymbol` | *Optional*, the text to use instead of `symbol` in the favicon |
163
+ | `features.search` | Enable search UI and Pagefind index generation |
164
+ | `features.code` | Enable generated source-code HTML pages for configured code extensions |
165
+ | `features.favicon` | Enable automatically generated favicons |
166
+ | `base` | Full base URL of the deployed site, used for metadata and URL generation |
167
+ | `basePath` | URL prefix for deployment under a subpath (e.g., `"/cs101"`), use `"/"` at root |
168
+ | `internalDomains` | Domain names treated as internal by link processing (not marked external) |
169
+ | `codeLanguages` | Map file extension to Shiki language (e.g., `"java": "java"`) |
170
+ | `faviconColor` | *Optional*, HSL color override for favicon (defaults to `themeColor`) |
171
+ | `faviconFontWeight` | *Optional*, font weight used for favicon text (default `700`) |
172
+ | `vars` | Arbitrary key/value variables exposed to templates/content (e.g., `<%= staffEmail %>`) |
173
+
174
+
175
+ #### `nav.json`
176
+
177
+ Defines the site navigation structure. The file contains an array of section
178
+ objects. Each section contains an array of link objects (internal or external,
179
+ and whether the link is disabled). You should specify at least two sections,
180
+ but three or more sections are supported.
181
+
182
+ ```json
183
+ [
184
+ {
185
+ "title": "Navigation",
186
+ "links": [{ "text": "Home", "internal": "/index.html" }]
187
+ },
188
+ {
189
+ "title": "Topics",
190
+ "links": [
191
+ { "text": "Lectures", "internal": "/lectures/index.html" },
192
+ {
193
+ "text": "Problem Sets",
194
+ "internal": "/problem_sets/index.html",
195
+ "disabled": true
196
+ }
197
+ ]
198
+ },
199
+ {
200
+ "title": "Links",
201
+ "links": [
202
+ { "text": "Zoom", "external": "https://zoom.com" }
203
+ ]
204
+ }
205
+ ]
206
+ ```
207
+
208
+
209
+ #### `authors.json`
210
+
211
+ Maps author handles (used in front matter `author` fields) to display names
212
+ and avatars. Each key is a handle (e.g., `jsmith`) and each value is an object
213
+ with `name`, `avatar`, and optionally `url`.
214
+
215
+ ```json
216
+ {
217
+ "jsmith": { "name": "Jane Smith", "avatar": "/avatars/jsmith.jpg" },
218
+ "ajones": {
219
+ "name": "Alex Jones",
220
+ "avatar": "/avatars/ajones.jpg",
221
+ "url": "/staff/ajones.html"
222
+ }
223
+ }
224
+ ```
225
+
226
+
227
+ ### Log level
228
+
229
+ Set the `TADA_LOG_LEVEL` environment variable to control build log verbosity.
230
+ Valid levels (from most to least verbose): `debug`, `info`, `warn`, `error`.
231
+ The default level is `info`. To see *all* logs generated by Tada, set the env
232
+ var to `debug`:
233
+
234
+ ```
235
+ TADA_LOG_LEVEL=debug tada dev
236
+ ```
237
+
238
+
239
+ ## Content
240
+
241
+ Site content lives in the `content/` directory. Markdown is converted to HTML.
242
+ HTML files should contain front matter and are also built, but not processed
243
+ like Markdown files are.
244
+
245
+ PDF, `.txt`, ZIP, images, and other kinds of files are copied into `dist/`
246
+ in the same locations. All files in `public/` are copied directly into `dist/`
247
+ with zero processing.
248
+
249
+
250
+ ### Front matter fields
251
+
252
+ Each file in `content/` should start with "front matter" (a YAML-formatted
253
+ list of variables parsed using the [`front-matter`][front-matter] library).
254
+
255
+ | Field | Description |
256
+ |-------|-------------|
257
+ | `title` (required) | Page title (`<title>` tag and page heading) |
258
+ | `skip` | Set to `true` to skip building this page completely |
259
+ | `author` | Author handle (e.g. `jsmith`) resolved to a full object via `config/authors.json` |
260
+ | `description` | Meta description for the page |
261
+ | `toc` | Set to `true` to show a table of contents |
262
+ | `parent` & `parentLabel` | URL and label for a breadcrumb link displayed above the title |
263
+ | `published` | Year, month, and day of publishing (e.g, `2025-09-09`) |
264
+
265
+
266
+ ### Variable substitution
267
+
268
+ Plain text content (e.g., HTML and Markdown) are processed using [Lodash
269
+ templates][lodash].
270
+
271
+ - Site config values are available under `site` (e.g., `site.title`, `site.symbol`)
272
+ - Page variables (from front matter) are available under `page` (e.g., `page.author`)
273
+ - Custom variables from the `"vars"` property of the config are available
274
+ without any prefix (e.g., `<%= staffEmail %>`)
275
+
276
+
277
+ ## Templates
278
+
279
+ HTML page layouts are internal to the Tada package. You don't need to modify
280
+ them; they are designed to work with the client-side components and styles
281
+ bundled in the package.
282
+
283
+
284
+
285
+ [inter]: https://fonts.google.com/specimen/Inter
286
+ [lodash]: https://lodash.info/doc/template
287
+ [front-matter]: https://www.npmjs.com/package/front-matter
288
+ [pagefind]: https://pagefind.app/
289
+ [mutool]: https://mupdf.readthedocs.io/en/latest/tools/mutool.html
290
+ [jep467]: https://openjdk.org/jeps/467
package/bin/tada.js ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env bun
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+ const { execSync } = require('child_process');
6
+
7
+ const SYSTEM_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
8
+
9
+ const packageDir = path.resolve(__dirname, '..');
10
+
11
+ const COMMANDS = {
12
+ init: 'Create a new Tada site',
13
+ dev: 'Build the site for development',
14
+ prod: 'Build the site for production',
15
+ watch: 'Watch for changes and rebuild',
16
+ serve: 'Start a local development server',
17
+ clean: 'Remove the dist/ directory',
18
+ };
19
+
20
+ const INIT_QUESTIONS = {
21
+ title: {
22
+ prompt: 'Site title',
23
+ defaultValue: 'Introduction to Computer Science',
24
+ validate: v => (v ? null : 'Title is required'),
25
+ },
26
+ symbol: {
27
+ prompt: 'Logo symbol (1-5 uppercase chars)',
28
+ defaultValue: 'CS 0',
29
+ validate: validateSymbol,
30
+ },
31
+ themeColor: {
32
+ prompt: 'Theme color',
33
+ defaultValue: 'hsl(195 70% 40%)',
34
+ validate: validateHslColor,
35
+ },
36
+ tintHue: {
37
+ prompt: 'Background tint hue (0-360)',
38
+ defaultValue: '20',
39
+ validate: v => {
40
+ const n = Number(v);
41
+ if (!Number.isInteger(n) || n < 0 || n > 360) {
42
+ return 'Must be an integer from 0 to 360';
43
+ }
44
+ return null;
45
+ },
46
+ },
47
+ tintAmount: {
48
+ prompt: 'Background tint amount (0-100)',
49
+ defaultValue: '100',
50
+ validate: v => {
51
+ const n = Number(v);
52
+ if (!Number.isInteger(n) || n < 0 || n > 100) {
53
+ return 'Must be an integer from 0 to 100';
54
+ }
55
+ return null;
56
+ },
57
+ },
58
+ defaultTimeZone: {
59
+ prompt: 'Default time zone',
60
+ defaultValue: SYSTEM_TIME_ZONE,
61
+ validate: v => (v ? null : 'Time zone is required'),
62
+ },
63
+ prodBase: {
64
+ prompt: 'Production base URL',
65
+ defaultValue: 'https://example.edu',
66
+ validate: validateUrl,
67
+ },
68
+ prodBasePath: {
69
+ prompt: 'Production base path',
70
+ defaultValue: '/',
71
+ validate: validateBasePath,
72
+ },
73
+ };
74
+
75
+ function printUsage() {
76
+ console.log('Usage: tada <command>\n');
77
+ console.log('Commands:');
78
+ console.log(' init <dirname> Initialize a new site');
79
+ for (const [cmd, desc] of Object.entries(COMMANDS)) {
80
+ if (cmd === 'init') {
81
+ console.log(' [--default] Use defaults for all config options');
82
+ continue;
83
+ }
84
+ console.log(` ${cmd.padEnd(18)} ${desc}`);
85
+ }
86
+ }
87
+
88
+ const webpackCli = path.join(packageDir, 'node_modules/webpack-cli/bin/cli.js');
89
+
90
+ function run(cmd) {
91
+ execSync(cmd, { cwd: process.cwd(), stdio: 'inherit' });
92
+ }
93
+
94
+ function copyDirRecursive(src, dest) {
95
+ fs.mkdirSync(dest, { recursive: true });
96
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
97
+ const srcPath = path.join(src, entry.name);
98
+ const destPath = path.join(dest, entry.name);
99
+ if (entry.isDirectory()) {
100
+ copyDirRecursive(srcPath, destPath);
101
+ } else {
102
+ fs.copyFileSync(srcPath, destPath);
103
+ }
104
+ }
105
+ }
106
+
107
+ // --- init command ---
108
+
109
+ function ask(rl, question, { defaultValue, validate } = {}) {
110
+ const suffix = defaultValue != null ? ` (default: ${defaultValue})` : '';
111
+ return new Promise(resolve => {
112
+ function prompt() {
113
+ rl.question(`${question}${suffix}? `, answer => {
114
+ const value = answer.trim() || defaultValue || '';
115
+ if (validate) {
116
+ const error = validate(value);
117
+ if (error) {
118
+ console.error(`Error: ${error}`);
119
+ prompt();
120
+ return;
121
+ }
122
+ }
123
+ resolve(value);
124
+ });
125
+ }
126
+ prompt();
127
+ });
128
+ }
129
+
130
+ function validateSymbol(value) {
131
+ if (!value) {
132
+ return 'Symbol is required';
133
+ }
134
+ if (value.length > 5) {
135
+ return 'Symbol must be 5 characters or fewer';
136
+ }
137
+ if (!/^[A-Z0-9\- ]{1,5}$/.test(value)) {
138
+ return 'Symbol must contain only uppercase letters, digits, hyphens, and spaces';
139
+ }
140
+ return null;
141
+ }
142
+
143
+ function validateHslColor(value) {
144
+ if (!value) {
145
+ return 'Color is required';
146
+ }
147
+ if (!/^hsl\(\d+(deg)? \d+% \d+%\)$/.test(value)) {
148
+ return 'Color must be in HSL format, e.g. hsl(195 70% 40%)';
149
+ }
150
+ return null;
151
+ }
152
+
153
+ function validateUrl(value) {
154
+ if (!value) {
155
+ return 'URL is required';
156
+ }
157
+ if (!/^https?:\/\/[-.:a-zA-Z0-9]+$/.test(value)) {
158
+ return 'Must be a valid URL like https://example.edu (no trailing slash or path)';
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function validateBasePath(value) {
164
+ if (!/^\/[-a-zA-Z0-9]*$/.test(value)) {
165
+ return 'Must start with / and contain only letters, digits, and hyphens';
166
+ }
167
+ return null;
168
+ }
169
+
170
+ function createSiteConfig({
171
+ title,
172
+ symbol,
173
+ themeColor,
174
+ tintHue,
175
+ tintAmount,
176
+ defaultTimeZone,
177
+ base,
178
+ basePath,
179
+ internalDomains,
180
+ }) {
181
+ return {
182
+ title,
183
+ symbol,
184
+ features: { search: true, code: true, favicon: true },
185
+ base,
186
+ basePath,
187
+ internalDomains,
188
+ defaultTimeZone,
189
+ codeLanguages: { java: 'java', py: 'python' },
190
+ themeColor,
191
+ tintHue: Number(tintHue),
192
+ tintAmount: Number(tintAmount),
193
+ vars: {},
194
+ };
195
+ }
196
+
197
+ async function initCommand(args) {
198
+ const dirname = args[0];
199
+ const useDefaults = args[1] === '--default';
200
+
201
+ if (!dirname) {
202
+ console.error('Error: Provide a name for the new directory');
203
+ console.log('Usage: tada init <dirname> [--default]');
204
+ process.exit(1);
205
+ }
206
+
207
+ const projectDir = path.resolve(process.cwd(), dirname);
208
+ if (fs.existsSync(projectDir)) {
209
+ console.error(`Error: "${dirname}" already exists`);
210
+ process.exit(1);
211
+ }
212
+
213
+ const rl = readline.createInterface({
214
+ input: process.stdin,
215
+ output: process.stdout,
216
+ });
217
+
218
+ let message = `Creating a new Tada site in ${projectDir}`;
219
+ if (useDefaults) {
220
+ console.log(message + ' using default config');
221
+ } else {
222
+ console.log(message);
223
+ }
224
+
225
+ const config = {};
226
+
227
+ for (const [key, { prompt, defaultValue, validate }] of Object.entries(
228
+ INIT_QUESTIONS,
229
+ )) {
230
+ if (useDefaults) {
231
+ config[key] = defaultValue;
232
+ } else {
233
+ config[key] = await ask(rl, prompt, { defaultValue, validate });
234
+ }
235
+ }
236
+
237
+ const {
238
+ title,
239
+ symbol,
240
+ themeColor,
241
+ tintHue,
242
+ tintAmount,
243
+ defaultTimeZone,
244
+ prodBase,
245
+ prodBasePath,
246
+ } = config;
247
+
248
+ rl.close();
249
+
250
+ // Derive internal domain from production base URL
251
+ const prodDomain = new URL(prodBase).hostname;
252
+
253
+ // Create project directory
254
+ fs.mkdirSync(path.join(projectDir, 'config'), { recursive: true });
255
+
256
+ // Generate site configs
257
+ const devConfig = createSiteConfig({
258
+ title,
259
+ symbol,
260
+ themeColor,
261
+ tintHue,
262
+ tintAmount,
263
+ defaultTimeZone,
264
+ base: 'http://localhost:8080',
265
+ basePath: '/',
266
+ internalDomains: ['localhost'],
267
+ });
268
+
269
+ const prodConfig = createSiteConfig({
270
+ title,
271
+ symbol,
272
+ themeColor,
273
+ tintHue,
274
+ tintAmount,
275
+ defaultTimeZone,
276
+ base: prodBase,
277
+ basePath: prodBasePath,
278
+ internalDomains: [prodDomain],
279
+ });
280
+
281
+ fs.writeFileSync(
282
+ path.join(projectDir, 'config/site.dev.json'),
283
+ JSON.stringify(devConfig, null, 2) + '\n',
284
+ );
285
+
286
+ fs.writeFileSync(
287
+ path.join(projectDir, 'config/site.prod.json'),
288
+ JSON.stringify(prodConfig, null, 2) + '\n',
289
+ );
290
+
291
+ // Copy nav and authors data files to the project's config directory
292
+ fs.copyFileSync(
293
+ path.join(packageDir, 'config/nav.json'),
294
+ path.join(projectDir, 'config/nav.json'),
295
+ );
296
+ fs.copyFileSync(
297
+ path.join(packageDir, 'config/authors.json'),
298
+ path.join(projectDir, 'config/authors.json'),
299
+ );
300
+
301
+ // Copy content/ and public/ from the package
302
+ copyDirRecursive(
303
+ path.join(packageDir, 'content'),
304
+ path.join(projectDir, 'content'),
305
+ );
306
+ copyDirRecursive(
307
+ path.join(packageDir, 'public'),
308
+ path.join(projectDir, 'public'),
309
+ );
310
+
311
+ console.log(`\nGenerated a new site in ${projectDir}`);
312
+ console.log(`\nNext steps:`);
313
+ console.log(` cd ${dirname}`);
314
+ console.log(` tada dev`);
315
+ console.log(` tada serve`);
316
+ }
317
+
318
+ // --- main ---
319
+
320
+ const command = process.argv[2];
321
+
322
+ switch (command) {
323
+ case 'init':
324
+ initCommand(process.argv.slice(3));
325
+ break;
326
+
327
+ case 'dev':
328
+ run(
329
+ `bun ${webpackCli} --config ${path.join(packageDir, 'webpack/config.dev.js')}`,
330
+ );
331
+ break;
332
+
333
+ case 'prod':
334
+ run(
335
+ `bun ${webpackCli} --config ${path.join(packageDir, 'webpack/config.prod.js')}`,
336
+ );
337
+ break;
338
+
339
+ case 'watch':
340
+ run(`bun ${path.join(packageDir, 'webpack/watch.js')}`);
341
+ break;
342
+
343
+ case 'serve':
344
+ run(`bun ${path.join(packageDir, 'webpack/serve.js')}`);
345
+ break;
346
+
347
+ case 'clean':
348
+ fs.rmSync(path.resolve(process.cwd(), 'dist'), {
349
+ recursive: true,
350
+ force: true,
351
+ });
352
+ console.log('Cleaned dist/');
353
+ break;
354
+
355
+ default:
356
+ printUsage();
357
+ if (command && command !== '--help' && command !== '-h') {
358
+ process.exit(1);
359
+ }
360
+ break;
361
+ }
@@ -0,0 +1 @@
1
+ { "alex": { "name": "Alex Breen", "avatar": "/avatars/alex.jpg" } }
@@ -0,0 +1,28 @@
1
+ [
2
+ {
3
+ "title": "Navigation",
4
+ "links": [{ "text": "Home", "internal": "/index.html" }]
5
+ },
6
+ {
7
+ "title": "Topics",
8
+ "links": [
9
+ { "text": "Lectures", "internal": "/lectures/index.html" },
10
+ {
11
+ "text": "Problem Sets",
12
+ "internal": "/problem_sets/index.html",
13
+ "disabled": true
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "title": "Links",
19
+ "links": [
20
+ { "text": "Zoom", "external": "https://zoom.com" },
21
+ {
22
+ "text": "Canvas",
23
+ "external": "https://www.instructure.com/",
24
+ "disabled": true
25
+ }
26
+ ]
27
+ }
28
+ ]