@abreen/tada 1.18.5 → 1.19.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 CHANGED
@@ -5,37 +5,62 @@
5
5
 
6
6
  A static site generator. The successor to Presto.
7
7
 
8
+ <figure>
9
+ <a href="https://abreen.io/Tada/markdown.html" target="_blank">
10
+ <img src=".github/screenshot.png" alt="Four screenshots of the Tada example site">
11
+ </a>
12
+ <figcaption>
13
+ A screenshot of the Tada example site in:
14
+ <ul>
15
+ <li>light mode</li>
16
+ <li>dark mode, high contrast</li>
17
+ <li>light mode, high contrast, & serif fonts</li>
18
+ <li>dark mode, standard contrast</li>
19
+ </ul>
20
+ </figcaption>
21
+ </figure>
22
+
23
+ [Visit the example site ↗](https://abreen.io/Tada/index.html)
24
+
25
+
8
26
  ## Features
9
27
 
10
- - Modern design (light & dark following system, floating header, styled lists)
11
- - Fast client-side navigation
12
- - Detects a new version of the current page and allows the user to reload
13
- - Clickable/linkable landmarks (headings, deflists, alert boxes)
14
- - Dynamic table of contents
15
- * Floats on the side of the screen when window is large enough
16
- * Renders headings, alert boxes, and `<hr>` elements
17
- * Highlights the heading currently being viewed
28
+ - Modern design
29
+ * Light & dark mode following system, customizable tint
30
+ * Standard & high contrast picker
31
+ * Sans-serif & serif font picker
32
+ * Plus, provide your own web fonts (with many tuning options)
33
+
34
+ - LaTeX math support
35
+ * Provided by [KaTeX][katex]
36
+ * Rendered completely at build time (zero load time on the client)
37
+
38
+ - Automatically generated favicon
39
+ * Text, color, font and font weight taken from config file
40
+ * Or, provide your own favicon file
41
+
42
+ - High quality output and ergonomic authoring
43
+ * Exhaustive internal link validation (broken links fail the build)
44
+ * External link handling (special visual treatment for external links,
45
+ no need to type `target="_blank"`)
46
+ * Internal links automatically prefixed with base path, if specified (no
47
+ need to keep writing the same URL prefix everywhere)
48
+
18
49
  - Built-in search powered by [Pagefind][pagefind]
19
50
  * Only pages in `content/` reachable from `/index.html` are indexed
51
+ * If `mutool` is installed, text inside PDFs in `content/` is indexed
52
+
20
53
  - Generated HTML pages for source code
21
54
  * Automatic code highlighting, clickable line numbers
22
55
  * Dynamic table of contents for each method/function
23
56
  * Converts new Markdown comment syntax ([added in Java 23][jep467]) to HTML
24
57
  * Indexed by Pagefind (classes, interfaces, methods, and fields)
25
- - Interactive execution traces via `renderTrace()`
26
- * Supports Java and Python source files
27
- * Renders step-by-step source highlighting, output, and memory diagrams
28
- - Slides mode: separate content by `---` and present sections as full-screen slides
29
- * Click icon near slide titles to start presenting at that slide
30
- * Right-click in presentation mode to annotate
31
- * Hold Shift while annotating to erase
32
- - PDF files are copied into `dist/`
33
- * Text of each PDF page is extracted using `mutool` (if present) and indexed
34
- - External link handling (special visual treatment for external links)
35
- - Internal link validation at build time (broken links fail the build)
36
- - Internal links automatically prefixed with base path, if specified
37
- - Time zone chooser (automatically adjusts `<datetime>` elements)
38
- - LaTeX math rendered at build time via [KaTeX][katex]
58
+
59
+ - Dynamic table of contents
60
+ * Floats on the side of the screen when window is large enough
61
+ * Renders headings, alert boxes, and `<hr>` elements
62
+ * Highlights the heading currently being viewed
63
+
39
64
  - Extended Markdown syntax
40
65
  * `<<< details ... <<<` renders a collapsible box
41
66
  * `::: section ... :::` renders a special section with a fancy background
@@ -45,9 +70,26 @@ A static site generator. The successor to Presto.
45
70
  * `+++ ... +++ ... +++` renders a two-column layout
46
71
  * Special heading subtitles with `## Heading # A subtitle here`
47
72
  * `{{{ _partial.md }}}` syntax for including partials
48
- - Custom header logo from an image in `public/`
49
- - Automatically generated favicon or custom ICO from `public/`
50
- * Text, color, font and font weight taken from config file
73
+
74
+ - Extremely fast client-side navigation
75
+ * A miniature SPA without the bloat of a framework
76
+ * Highly efficient, thoroughly covered by cross-platform browser tests
77
+ * Also detects a new version of the current page and supplies a reload button
78
+
79
+ ### Experimental features
80
+
81
+ - Interactive execution traces via `renderTrace()`
82
+ * Supports Java and Python source files
83
+ * Renders step-by-step source highlighting, output, and memory diagrams
84
+
85
+ - Slides mode: separate content by `---` and present sections as full-screen slides
86
+ * Click icon near slide titles to start presenting at that slide
87
+ * Right-click in presentation mode to annotate
88
+ * Hold Shift while annotating to erase
89
+
90
+
91
+
92
+ - Time zone chooser (automatically adjusts `<datetime>` elements)
51
93
 
52
94
  ## Prerequisites
53
95
 
@@ -107,9 +149,11 @@ Visit [http://localhost:8080/index.html](http://localhost:8080/index.html).
107
149
 
108
150
  Create a new Tada site in a new directory. Prompts for:
109
151
  - **Site title**: displayed in the header and `<title>` tag
110
- - **Symbol**: short text (1-5 uppercase characters) shown in the logo and favicon
111
- - **Theme color**: HSL color, e.g. `hsl(195 70% 40%)`
112
- - **Background tint hue**: hue (0-360) for background/foreground tinting (defaults to `33`)
152
+ - **Symbol**: short text (1-5 uppercase characters) shown in the logo and favicon,
153
+ or a single emoji shown in the logo (defaults to 🎉; emoji symbols turn off
154
+ generated favicons)
155
+ - **Theme color**: CSS color, e.g. `hsl(195 70% 40%)` (defaults to `darkturquoise`)
156
+ - **Background tint hue**: hue (0-360) for background/foreground tinting (defaults to `190`)
113
157
  - **Background tint amount**: percentage (0-100) of tint to apply (defaults to `100`)
114
158
  - **Default time zone**: for `<time>` elements (defaults to your system zone)
115
159
  - **Production base URL**: e.g. `https://example.edu`
@@ -268,16 +312,16 @@ vars:
268
312
  | `title` | Title for the whole site (also used to derive `titlePostfix`) |
269
313
  | `titlePostfix` | *Optional*, the string to append to each page's `title` |
270
314
  | `banner` | *Optional*, Markdown rendered in a bordered box above every page |
271
- | `symbol` | Text (1-5 chars) displayed in header when `logo` is absent; also used for generated favicons |
315
+ | `symbol` | Text (1-5 chars) or a single emoji displayed in header when `logo` is absent; also used for generated favicons |
272
316
  | `logo` | *Optional*, image path relative to `public/`, replacing the header symbol |
273
317
  | `favicon` | *Optional*, `.ico` path relative to `public/`, replacing generated favicons and the web app manifest |
274
318
  | `themeColor` | Theme color for the site (e.g., `"tomato"`, `"#c04040"`, `"hsl(195 70% 40%)"`) |
275
- | `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `33`) |
319
+ | `tintHue` | *Optional*, hue (0-360) for background and foreground tinting (default `190`) |
276
320
  | `tintAmount` | *Optional*, percentage (0-100) of tint to apply to the neutral palette (default `100`); link saturation stays fixed |
277
321
  | `defaultFont` | *Optional*, initial font pairing: `sans` or `serif` (default `sans`) |
278
322
  | `defaultContrast` | *Optional*, initial neutral-palette contrast: `standard` or `high` (default `standard`) |
279
323
  | `fontOverrides` | *Optional*, custom serif and serif-monospace WOFF2 faces from `public/`; each configured family requires `regular` and may include styled faces, OpenType `features`, and font-specific `tuning` |
280
- | `faviconSymbol` | *Optional*, the text to use instead of `symbol` in generated favicons |
324
+ | `faviconSymbol` | *Optional*, the text to use instead of `symbol` in generated favicons; required when `symbol` is an emoji and favicons are generated |
281
325
  | `features.search` | Enable search UI and Pagefind index generation |
282
326
  | `features.favicon` | Enable favicon links and generation (default `true`); `false` also omits custom favicon links |
283
327
  | `features.footer` | Show the Tada footer at the bottom of every page |
package/bin/tada.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  resolveSiteConfigFile,
28
28
  type SiteEnv,
29
29
  } from '../build/config-files';
30
+ import { isEmojiSymbol } from '../build/utils/symbol';
30
31
 
31
32
  const { version } = packageJson;
32
33
 
@@ -75,22 +76,22 @@ interface InitQuestion {
75
76
  const INIT_QUESTIONS: Record<string, InitQuestion> = {
76
77
  title: {
77
78
  prompt: 'Site title',
78
- defaultValue: 'Introduction to Computer Science',
79
+ defaultValue: 'Tada',
79
80
  validate: (v: string) => (v ? null : 'Title is required'),
80
81
  },
81
82
  symbol: {
82
- prompt: 'Logo symbol (1-5 uppercase chars)',
83
- defaultValue: 'CS 0',
83
+ prompt: 'Logo symbol (1-5 uppercase chars or one emoji)',
84
+ defaultValue: '🎉',
84
85
  validate: validateSymbol,
85
86
  },
86
87
  themeColor: {
87
88
  prompt: 'Theme color',
88
- defaultValue: 'hsl(195 70% 40%)',
89
+ defaultValue: 'darkturquoise',
89
90
  validate: validateColor,
90
91
  },
91
92
  tintHue: {
92
93
  prompt: 'Background tint hue (0-360)',
93
- defaultValue: '33',
94
+ defaultValue: '190',
94
95
  validate: validateHue,
95
96
  },
96
97
  tintAmount: {
@@ -338,6 +339,14 @@ async function initCommand(args: string[]): Promise<void> {
338
339
  // Create project directory
339
340
  fs.mkdirSync(projectDir);
340
341
 
342
+ // Generated favicons are text-only, so an emoji symbol starts without them
343
+ const features = {
344
+ search: true,
345
+ favicon: !isEmojiSymbol(symbol),
346
+ footer: true,
347
+ pickers: true,
348
+ };
349
+
341
350
  // Generate site configs
342
351
  const devConfig = createSiteConfig({
343
352
  title,
@@ -349,7 +358,7 @@ async function initCommand(args: string[]): Promise<void> {
349
358
  base: 'http://localhost:8080',
350
359
  basePath: '/',
351
360
  internalDomains: ['localhost'],
352
- features: { search: true, favicon: false, footer: true, pickers: true },
361
+ features,
353
362
  extensionToShikiLanguage: bare ? {} : { java: 'java', py: 'python' },
354
363
  shikiLanguages: bare ? [] : ['java', 'python', 'html'],
355
364
  });
@@ -364,7 +373,7 @@ async function initCommand(args: string[]): Promise<void> {
364
373
  base: prodBase,
365
374
  basePath: prodBasePath,
366
375
  internalDomains: [prodDomain],
367
- features: { search: true, favicon: true, footer: true, pickers: true },
376
+ features,
368
377
  extensionToShikiLanguage: bare ? {} : { java: 'java', py: 'python' },
369
378
  shikiLanguages: bare ? [] : ['java', 'python', 'html'],
370
379
  });
package/bin/validators.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
2
2
  import type { SiteConfigInput, SiteVariables } from '../build/types';
3
+ import { isValidSymbol } from '../build/utils/symbol';
3
4
 
4
5
  const SUPPORTED_TIME_ZONES: readonly string[] = timezones.map(tz => tz.value);
5
6
  const FALLBACK_TIME_ZONE = 'UTC';
@@ -8,13 +9,13 @@ export function validateSymbol(value: string): string | null {
8
9
  if (!value) {
9
10
  return 'Symbol is required';
10
11
  }
11
- if (value.length > 5) {
12
- return 'Symbol must be 5 characters or fewer';
12
+ if (isValidSymbol(value)) {
13
+ return null;
13
14
  }
14
- if (!/^[A-Z0-9\- ]{1,5}$/.test(value)) {
15
- return 'Symbol must contain only uppercase letters, digits, hyphens, and spaces';
15
+ if (value.length > 5) {
16
+ return 'Symbol must be 5 characters or fewer, or a single emoji';
16
17
  }
17
- return null;
18
+ return 'Symbol must contain only uppercase letters, digits, hyphens, and spaces, or be a single emoji';
18
19
  }
19
20
 
20
21
  export function validateColor(value: string): string | null {
package/build/bundle.ts CHANGED
@@ -51,7 +51,7 @@ function renderThemeScss(siteVariables: SiteVariables): string {
51
51
  const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
52
52
  const template = fs.readFileSync(templatePath, 'utf-8');
53
53
  const theme = deriveTheme(siteVariables.themeColor);
54
- const tintHue = siteVariables.tintHue ?? 33;
54
+ const tintHue = siteVariables.tintHue ?? 190;
55
55
  const tintAmount = siteVariables.tintAmount ?? 100;
56
56
 
57
57
  const linkHue = formatCssNumber(deriveLinkHue(tintHue));
@@ -29,6 +29,14 @@ export function parseConfigText(text: string, fileName: string): unknown {
29
29
  try {
30
30
  return Bun.YAML.parse(text);
31
31
  } catch (error) {
32
+ // YAML rejects JSON surrogate pair escapes such as emoji in `\ud83c\udf89` form
33
+ if (fileName.endsWith('.json')) {
34
+ try {
35
+ return JSON.parse(text);
36
+ } catch {
37
+ // Report the YAML error, as for any other config
38
+ }
39
+ }
32
40
  const message = error instanceof Error ? error.message : String(error);
33
41
  throw new Error(`${fileName}: ${message}`, { cause: error });
34
42
  }
@@ -4,6 +4,7 @@ import type { SiteEnv } from './config-files';
4
4
  import { loadSiteConfig } from './config-loader';
5
5
  import { compile as compileJsonSchema, doValidation } from './json-schema';
6
6
  import { getProjectDir } from './utils/paths';
7
+ import { isEmojiSymbol } from './utils/symbol';
7
8
  import type { PlainTextLanguage, SiteVariables } from './types';
8
9
  import siteSchema from '../schema/site.schema.json' with { type: 'json' };
9
10
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
@@ -131,10 +132,7 @@ function getSiteVariables(env: SiteEnv): SiteVariables {
131
132
  },
132
133
  } as SiteVariables;
133
134
 
134
- // Derive faviconSymbol from symbol if not explicitly set
135
- if (variables.symbol && !variables.faviconSymbol) {
136
- variables.faviconSymbol = variables.symbol;
137
- }
135
+ resolveFaviconSymbol(variables, fileName);
138
136
 
139
137
  // Derive faviconColor from themeColor if not explicitly set
140
138
  if (!variables.faviconColor) {
@@ -157,6 +155,28 @@ function getSiteVariables(env: SiteEnv): SiteVariables {
157
155
  return variables;
158
156
  }
159
157
 
158
+ /**
159
+ * Derive faviconSymbol from a text symbol. Generated favicons are drawn with a
160
+ * text font that has no emoji, so an emoji symbol needs a text faviconSymbol.
161
+ */
162
+ export function resolveFaviconSymbol(
163
+ variables: SiteVariables,
164
+ fileName: string,
165
+ ): void {
166
+ if (!variables.symbol || variables.faviconSymbol) {
167
+ return;
168
+ }
169
+ if (!isEmojiSymbol(variables.symbol)) {
170
+ variables.faviconSymbol = variables.symbol;
171
+ return;
172
+ }
173
+ if (variables.features.favicon !== false && !variables.favicon) {
174
+ throw new Error(
175
+ `${fileName}: faviconSymbol is required when symbol is an emoji and favicons are generated; set faviconSymbol to 1 to 5 uppercase characters, digits, hyphens, or spaces`,
176
+ );
177
+ }
178
+ }
179
+
160
180
  export function getDevSiteVariables(): SiteVariables {
161
181
  return getSiteVariables('dev');
162
182
  }
@@ -1,4 +1,5 @@
1
1
  import { encodePublicAssetPath } from './custom-fonts';
2
+ import { isEmojiSymbol } from './utils/symbol';
2
3
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
3
4
  import type { SiteVariables } from './types';
4
5
 
@@ -26,6 +27,7 @@ interface TemplateGlobals {
26
27
  cx: (obj: Record<string, unknown>) => string;
27
28
  encodeAuthoredUrl: (value: string) => string;
28
29
  encodePublicAssetPath: (value: string) => string;
30
+ isEmojiSymbol: (value: string | null | undefined) => boolean;
29
31
  renderTimeZoneChooser: () => string;
30
32
  }
31
33
 
@@ -56,6 +58,7 @@ export default function createTemplateGlobals(
56
58
  cx: classNames,
57
59
  encodeAuthoredUrl,
58
60
  encodePublicAssetPath,
61
+ isEmojiSymbol,
59
62
  renderTimeZoneChooser,
60
63
  };
61
64
  }
@@ -0,0 +1,21 @@
1
+ // One emoji: a flag, keycap, subdivision flag, or pictograph with optional
2
+ // skin tone and ZWJ joins. Written for the `u` flag so the site schema (Ajv)
3
+ // can share it; `\p{RGI_Emoji}` needs the `v` flag.
4
+ const EMOJI = String.raw`\p{RI}\p{RI}|[#*0-9]\uFE0F\u20E3|\u{1F3F4}[\u{E0061}-\u{E007A}]+\u{E007F}|(?!\p{RI})(?:\p{Emoji_Presentation}|\p{Extended_Pictographic}\uFE0F)\p{Emoji_Modifier}?(?:\u200D(?:\p{Emoji_Presentation}|\p{Extended_Pictographic}\uFE0F?)\p{Emoji_Modifier}?)*`;
5
+
6
+ /** Must match `faviconSymbol` in `schema/site.schema.json` */
7
+ export const TEXT_SYMBOL_PATTERN = '^[A-Z0-9\\- ]{1,5}$';
8
+
9
+ /** Must match `symbol` in `schema/site.schema.json` */
10
+ export const SYMBOL_PATTERN = `^(?:[A-Z0-9\\- ]{1,5}|${EMOJI})$`;
11
+
12
+ const SYMBOL_RE = new RegExp(SYMBOL_PATTERN, 'u');
13
+ const EMOJI_RE = new RegExp(`^(?:${EMOJI})$`, 'u');
14
+
15
+ export function isValidSymbol(value: string): boolean {
16
+ return SYMBOL_RE.test(value);
17
+ }
18
+
19
+ export function isEmojiSymbol(value: string | null | undefined): boolean {
20
+ return value != null && EMOJI_RE.test(value);
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.18.5",
3
+ "version": "1.19.0",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -31,14 +31,17 @@
31
31
  },
32
32
  "title": { "type": "string" },
33
33
  "banner": { "type": "string" },
34
- "symbol": { "type": "string", "pattern": "^[A-Z0-9\\- ]{1,5}$" },
34
+ "symbol": {
35
+ "type": "string",
36
+ "pattern": "^(?:[A-Z0-9\\- ]{1,5}|\\p{RI}\\p{RI}|[#*0-9]\\uFE0F\\u20E3|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]+\\u{E007F}|(?!\\p{RI})(?:\\p{Emoji_Presentation}|\\p{Extended_Pictographic}\\uFE0F)\\p{Emoji_Modifier}?(?:\\u200D(?:\\p{Emoji_Presentation}|\\p{Extended_Pictographic}\\uFE0F?)\\p{Emoji_Modifier}?)*)$"
37
+ },
35
38
  "titlePostfix": { "type": "string" },
36
39
  "themeColor": { "type": "string" },
37
40
  "tintHue": {
38
41
  "type": "integer",
39
42
  "minimum": 0,
40
43
  "maximum": 360,
41
- "default": 33
44
+ "default": 190
42
45
  },
43
46
  "tintAmount": {
44
47
  "type": "integer",
package/src/_content.scss CHANGED
@@ -184,7 +184,7 @@ button.icon-button:focus {
184
184
 
185
185
  ol.styled-list,
186
186
  ul.styled-list {
187
- font-weight: 700;
187
+ font-weight: 600;
188
188
  color: var(--theme-color-text);
189
189
  }
190
190
 
package/src/_icons.scss CHANGED
@@ -65,7 +65,7 @@
65
65
  @include define-icon(
66
66
  'heading-present',
67
67
  24,
68
- "<rect x='3.25' y='5.25' width='17.5' height='13.5' rx='2.25'/><path d='M10.25 9.25v5.5L14.75 12z' fill='black'/>"
68
+ "<rect x='4' y='5' width='16' height='11' rx='1'/><path d='M12 16v4M9 20h6'/>"
69
69
  );
70
70
  @include define-icon(
71
71
  'check',
package/src/_layout.scss CHANGED
@@ -220,6 +220,7 @@ body.literate main table {
220
220
  footer {
221
221
  @include user-select(none);
222
222
 
223
+ font-size: var(--font-size-smaller);
223
224
  color: var(--fg2-color);
224
225
  text-align: center;
225
226
 
@@ -184,6 +184,17 @@ header summary .logo {
184
184
  background: var(--theme-color);
185
185
  }
186
186
 
187
+ // A single emoji is the logo itself: no badge, sized like an image logo
188
+ header summary .logo-emoji {
189
+ padding: 0;
190
+ font-family:
191
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
192
+ font-size: 22px;
193
+ font-weight: normal;
194
+ line-height: 24px;
195
+ background: none;
196
+ }
197
+
187
198
  header summary .logo-image {
188
199
  flex: 0 0 auto;
189
200
  width: auto;
@@ -21,7 +21,7 @@
21
21
  .question-a-label {
22
22
  flex: 0 0 var(--question-label-width);
23
23
  margin: 0;
24
- font-weight: 700;
24
+ font-weight: 600;
25
25
  color: var(--theme-color-text);
26
26
 
27
27
  @include user-select(none);
@@ -23,7 +23,7 @@
23
23
  <link rel="apple-touch-icon" sizes="512x512" href="/favicon-512.png" />
24
24
  <link rel="apple-touch-icon" sizes="256x256" href="/favicon-256.png" />
25
25
  <link rel="manifest" href="/manifest.json" />
26
- <meta name="apple-mobile-web-app-title" content="<%- site.symbol %>">
26
+ <meta name="apple-mobile-web-app-title" content="<%- site.title %>">
27
27
  <meta name="mobile-web-app-capable" content="yes">
28
28
  <% } %>
29
29
  <% } %>
@@ -219,7 +219,7 @@
219
219
  <path class="menu-icon-line menu-icon-line-middle" d="M3 12h18" />
220
220
  <path class="menu-icon-line menu-icon-line-bottom" d="M3 18h18" />
221
221
  </svg>
222
- <% if (site.logo) { %><img class="logo-image" src="/<%- encodePublicAssetPath(site.logo) %>" alt="" aria-hidden="true" /><% } else if (site.symbol) { %><span class="logo" aria-hidden="true"><%- site.symbol %></span><% } %>
222
+ <% if (site.logo) { %><img class="logo-image" src="/<%- encodePublicAssetPath(site.logo) %>" alt="" aria-hidden="true" /><% } else if (site.symbol) { %><span class="<%- isEmojiSymbol(site.symbol) ? 'logo logo-emoji' : 'logo' %>" aria-hidden="true"><%- site.symbol %></span><% } %>
223
223
  <span class="header-title" aria-hidden="true">
224
224
  <span class="header-title-reel">
225
225
  <span class="site-title"><%- site.title %></span>