@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.
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/bin/tada.js +361 -0
- package/config/authors.json +1 -0
- package/config/nav.json +28 -0
- package/content/index.md +19 -0
- package/content/lectures/01/Pair.java.md +296 -0
- package/content/lectures/01/Rectangle.java +80 -0
- package/content/lectures/01/demo.py +9 -0
- package/content/lectures/01/index.md +39 -0
- package/content/lectures/01/lecture1.pdf +0 -0
- package/content/lectures/index.md +25 -0
- package/content/markdown.md +379 -0
- package/content/problem_sets/index.md +6 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
- package/fonts/google-sans-code/LICENSE.txt +93 -0
- package/fonts/inter/InterVariable-Italic.ttf +0 -0
- package/fonts/inter/InterVariable.ttf +0 -0
- package/fonts/inter/LICENSE.txt +92 -0
- package/package.json +70 -0
- package/public/avatars/alex.jpg +0 -0
- package/public/test.txt +1 -0
- package/src/_mixins.scss +4 -0
- package/src/anchor/README.md +6 -0
- package/src/anchor/index.ts +34 -0
- package/src/anchor/style.scss +48 -0
- package/src/code/README.md +5 -0
- package/src/code/index.ts +113 -0
- package/src/code/style.scss +101 -0
- package/src/code.scss +54 -0
- package/src/header/README.md +8 -0
- package/src/header/index.ts +43 -0
- package/src/header/style.scss +228 -0
- package/src/index.ts +73 -0
- package/src/layout.scss +144 -0
- package/src/literate/style.scss +60 -0
- package/src/print/README.md +4 -0
- package/src/print/index.ts +32 -0
- package/src/print/style.scss +82 -0
- package/src/question/README.md +3 -0
- package/src/question/index.ts +25 -0
- package/src/question/style.scss +116 -0
- package/src/search/README.md +6 -0
- package/src/search/index.ts +574 -0
- package/src/search/style.scss +217 -0
- package/src/style.scss +815 -0
- package/src/timezone/index.test.ts +100 -0
- package/src/timezone/index.ts +298 -0
- package/src/timezone/style.scss +16 -0
- package/src/timezone/timezones.json +58 -0
- package/src/toc/README.md +3 -0
- package/src/toc/index.ts +322 -0
- package/src/toc/style.scss +203 -0
- package/src/top/README.md +4 -0
- package/src/top/index.ts +75 -0
- package/src/util.ts +122 -0
- package/templates/_author.html +27 -0
- package/templates/_bottom.html +3 -0
- package/templates/_download.html +1 -0
- package/templates/_heading.html +19 -0
- package/templates/_nav.html +18 -0
- package/templates/_theme.scss +97 -0
- package/templates/_top.html +87 -0
- package/templates/authors.schema.json +13 -0
- package/templates/code.html +31 -0
- package/templates/default.html +13 -0
- package/templates/literate.html +16 -0
- package/templates/nav.schema.json +27 -0
- package/tsconfig.json +15 -0
- package/types/dev.ts +3 -0
- package/types/sass.d.ts +1 -0
- package/types/site-variables.d.ts +16 -0
- package/webpack/apply-base-path-plugin.js +78 -0
- package/webpack/build-state.js +97 -0
- package/webpack/code.test.js +162 -0
- package/webpack/colors.js +15 -0
- package/webpack/config.base.js +147 -0
- package/webpack/config.dev.js +23 -0
- package/webpack/config.prod.js +32 -0
- package/webpack/content-watch-plugin.js +153 -0
- package/webpack/deflist-id-plugin.js +62 -0
- package/webpack/external-links-plugin.js +37 -0
- package/webpack/features.js +5 -0
- package/webpack/flair.json +1 -0
- package/webpack/generate-content-assets-plugin.js +308 -0
- package/webpack/generate-favicon-plugin.js +198 -0
- package/webpack/generate-fonts-plugin.js +69 -0
- package/webpack/generate-manifest-plugin.js +116 -0
- package/webpack/globals.js +74 -0
- package/webpack/heading-subtitle-plugin.js +80 -0
- package/webpack/json-schema.js +19 -0
- package/webpack/log.js +143 -0
- package/webpack/markdown-plugins.test.js +203 -0
- package/webpack/pagefind-plugin.js +379 -0
- package/webpack/pagefind-plugin.test.js +131 -0
- package/webpack/pdf-text.js +163 -0
- package/webpack/print-flair-plugin.js +22 -0
- package/webpack/reachability.js +273 -0
- package/webpack/reachability.test.js +80 -0
- package/webpack/serve.js +104 -0
- package/webpack/site-variables.js +53 -0
- package/webpack/site.schema.json +67 -0
- package/webpack/templates.js +128 -0
- package/webpack/text-to-id.js +8 -0
- package/webpack/toc-plugin.js +167 -0
- package/webpack/util.js +49 -0
- package/webpack/utils/code.js +439 -0
- package/webpack/utils/content-files.js +147 -0
- package/webpack/utils/define-plugin.js +20 -0
- package/webpack/utils/file-types.js +26 -0
- package/webpack/utils/front-matter.js +57 -0
- package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
- package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
- package/webpack/utils/literate-java.js +153 -0
- package/webpack/utils/markdown.js +244 -0
- package/webpack/utils/parse-hsl.js +8 -0
- package/webpack/utils/paths.js +58 -0
- package/webpack/utils/render.js +466 -0
- package/webpack/utils/shiki-highlighter.js +26 -0
- package/webpack/validate-internal-links-plugin.js +155 -0
- package/webpack/watch-reachability-state.js +273 -0
- package/webpack/watch-reachability-state.test.js +198 -0
- package/webpack/watch-reload-client.js +54 -0
- 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" } }
|
package/config/nav.json
ADDED
|
@@ -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
|
+
]
|