@abreen/tada 1.2.0 → 1.3.1

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
@@ -28,7 +28,7 @@ A static site generator. The successor to Presto.
28
28
  * `<<< details ... <<<` renders a collapsible box
29
29
  * `::: section ... :::` renders a special section with a fancy background
30
30
  * `!!! note ... !!!` and `!!! warning ... !!!` render alert boxes
31
- * `??? review ... ???` renders a Q & A section; answers hidden until click
31
+ * `??? question ... ???` renders a Q & A section; answers hidden until click
32
32
  * Special heading subtitles with `## Heading # A subtitle here`
33
33
  - Automatically generated favicon
34
34
  * Text, color, font and font weight taken from config file
package/bin/tada.ts CHANGED
@@ -116,6 +116,21 @@ function printUsage() {
116
116
  console.log(' --tint-amount, --default-time-zone, --prod-base,');
117
117
  console.log(' --prod-base-path]');
118
118
  continue;
119
+ } else if (cmd === 'watch') {
120
+ console.log(` ${cmd.padEnd(18)} ${desc}`);
121
+ console.log(
122
+ ' [--port <n>] HTTP server port (default: 8080)',
123
+ );
124
+ console.log(
125
+ ' [--ws-port <n>] WebSocket port for live reload (default: 35729)',
126
+ );
127
+ continue;
128
+ } else if (cmd === 'serve') {
129
+ console.log(` ${cmd.padEnd(18)} ${desc}`);
130
+ console.log(
131
+ ' [--port <n>] HTTP server port (default: 8080)',
132
+ );
133
+ continue;
119
134
  } else if (cmd === 'clean') {
120
135
  console.log(' clean Remove the dist/ directory');
121
136
  console.log(
@@ -137,7 +152,18 @@ function printUsage() {
137
152
  }
138
153
 
139
154
  function run(cmd: string): void {
140
- execSync(cmd, { cwd: process.cwd(), stdio: 'inherit' });
155
+ try {
156
+ execSync(cmd, { cwd: process.cwd(), stdio: 'inherit' });
157
+ } catch (err: unknown) {
158
+ const code = (err as { status?: number }).status;
159
+ if (code != null) {
160
+ console.error(`tada ${command} failed with exit code ${code}`);
161
+ process.exit(code);
162
+ } else {
163
+ console.error(`tada ${command} failed`);
164
+ process.exit(1);
165
+ }
166
+ }
141
167
  }
142
168
 
143
169
  function copyDirRecursive(src: string, dest: string): void {
@@ -370,6 +396,7 @@ async function initCommand(args: string[]): Promise<void> {
370
396
  title: 'Topics',
371
397
  links: [
372
398
  { text: 'Lectures', internal: '/lectures/index.html' },
399
+ { text: 'Labs', internal: '/labs/index.html' },
373
400
  {
374
401
  text: 'Problem Sets',
375
402
  internal: '/problem_sets/index.html',
@@ -30,10 +30,11 @@ describe('createGlobals', () => {
30
30
  expect(g.cx).toBe(g.classNames);
31
31
  });
32
32
 
33
- test('renderTimeZoneChooser includes noscript when defaultTimeZone is set', () => {
33
+ test('renderTimeZoneChooser includes hidden disabled select, inline script, and noscript', () => {
34
34
  const g = makeGlobals('index', { defaultTimeZone: 'America/New_York' });
35
- expect(g.renderTimeZoneChooser()).toContain('noscript');
36
- expect(g.renderTimeZoneChooser()).toContain('select');
35
+ const html = g.renderTimeZoneChooser();
36
+ expect(html).toContain('<select class="time-zone" hidden disabled>');
37
+ expect(html).toContain('<noscript>Times shown in ET.</noscript>');
37
38
  });
38
39
  });
39
40
 
package/build/globals.ts CHANGED
@@ -18,8 +18,16 @@ export default function createGlobals(
18
18
  const defaultTz = timezones.find(
19
19
  t => t.value === siteVariables.defaultTimeZone,
20
20
  )!;
21
+ const options = timezones
22
+ .map(tz => {
23
+ const label =
24
+ tz.value === 'UTC' ? tz.label : `${tz.label} (${tz.abbreviation})`;
25
+ const selected = tz.value === defaultTz.value ? ' selected' : '';
26
+ return `<option value="${tz.value}"${selected}>${label}</option>`;
27
+ })
28
+ .join('');
21
29
  const renderTimeZoneChooser = () =>
22
- `<select class="time-zone" hidden></select><noscript>Times shown in ${defaultTz.abbreviation}.</noscript>`;
30
+ `<span class="time-zone-label" hidden>Times shown in </span><select class="time-zone" hidden disabled>${options}</select><noscript>Times shown in ${defaultTz.abbreviation}.</noscript>`;
23
31
 
24
32
  return {
25
33
  isHomePage: subPath === 'index',
@@ -382,9 +382,7 @@ describe('injectKatexStylesheet', () => {
382
382
  const result = injectKatexStylesheet(html, p => p);
383
383
 
384
384
  expect(result).toContain('href="/katex/katex.min.css"');
385
- expect(result).toContain('media="print"');
386
- expect(result).toContain('onload="this.media=\'all\'"');
387
- expect(result).toContain('<noscript>');
385
+ expect(result).toContain('rel="stylesheet"');
388
386
  });
389
387
 
390
388
  test('applies basePath to the stylesheet URL', () => {
package/build/serve.ts CHANGED
@@ -20,7 +20,9 @@ try {
20
20
  process.exit(1);
21
21
  }
22
22
 
23
- function resolvePathname(pathname: string): string | null {
23
+ function resolvePathname(
24
+ pathname: string,
25
+ ): { filePath: string; mtime: Date } | null {
24
26
  let decodedPath: string;
25
27
  try {
26
28
  decodedPath = decodeURIComponent(pathname);
@@ -49,17 +51,27 @@ function resolvePathname(pathname: string): string | null {
49
51
  return null;
50
52
  }
51
53
 
52
- return resolvedPath;
54
+ return { filePath: resolvedPath, mtime: stat.mtime };
53
55
  }
54
56
 
55
57
  function createResponse(req: Request): Response {
56
58
  const url = new URL(req.url);
57
- const filePath = resolvePathname(url.pathname);
58
- if (!filePath) {
59
+ const result = resolvePathname(url.pathname);
60
+ if (!result) {
59
61
  return new Response('Not Found', { status: 404 });
60
62
  }
61
63
 
62
- return new Response(Bun.file(filePath));
64
+ const headers = {
65
+ 'Cache-Control': 'no-cache',
66
+ 'Last-Modified': result.mtime.toUTCString(),
67
+ };
68
+
69
+ const ims = req.headers.get('If-Modified-Since');
70
+ if (ims && new Date(ims) >= new Date(headers['Last-Modified'])) {
71
+ return new Response(null, { status: 304, headers });
72
+ }
73
+
74
+ return new Response(Bun.file(result.filePath), { headers });
63
75
  }
64
76
 
65
77
  function getPortArg(): number {
@@ -10,7 +10,7 @@ const configDir = getProjectDir();
10
10
 
11
11
  const DEFAULT: Partial<SiteVariables> = {
12
12
  basePath: '/',
13
- features: { search: true, code: true, favicon: false },
13
+ features: { search: true, code: true, favicon: true },
14
14
  };
15
15
 
16
16
  const isValid = compileJsonSchema(siteSchema);
@@ -23,7 +23,12 @@ const renderStack: string[] = [];
23
23
  let errorStack: string[] | null = null;
24
24
 
25
25
  // JSON data files that live in the user's config directory
26
- const JSON_DATA_FILES: string[] = ['nav.json', 'authors.json'];
26
+ const REQUIRED_JSON_DATA_FILES: string[] = ['nav.json'];
27
+ const OPTIONAL_JSON_DATA_FILES: string[] = ['authors.json'];
28
+ const JSON_DATA_FILES: string[] = [
29
+ ...REQUIRED_JSON_DATA_FILES,
30
+ ...OPTIONAL_JSON_DATA_FILES,
31
+ ];
27
32
 
28
33
  function getHtmlTemplatesDir(): string {
29
34
  return path.resolve(getPackageDir(), 'templates');
@@ -97,6 +102,9 @@ export function compileTemplates(
97
102
  for (const fileName of JSON_DATA_FILES) {
98
103
  const filePath = path.join(jsonDir, fileName);
99
104
  if (!fs.existsSync(filePath)) {
105
+ if (REQUIRED_JSON_DATA_FILES.includes(fileName)) {
106
+ throw new Error(`Missing required config file: ${filePath}`);
107
+ }
100
108
  continue;
101
109
  }
102
110
 
@@ -95,6 +95,18 @@ public class LiterateRunner {
95
95
  stderrThread.setDaemon(true);
96
96
  stderrThread.start();
97
97
 
98
+ // Forward stdin to target VM in a background thread
99
+ OutputStream targetStdin = process.getOutputStream();
100
+ Thread stdinThread = new Thread(() -> {
101
+ try {
102
+ System.in.transferTo(targetStdin);
103
+ targetStdin.close();
104
+ } catch (IOException ignored) {
105
+ }
106
+ });
107
+ stdinThread.setDaemon(true);
108
+ stdinThread.start();
109
+
98
110
  int activeBlock = -1;
99
111
 
100
112
  try {
@@ -199,6 +211,7 @@ public class LiterateRunner {
199
211
  } catch (VMDisconnectedException ignored) {
200
212
  }
201
213
  stderrThread.join(1000);
214
+ stdinThread.join(1000);
202
215
  }
203
216
 
204
217
  // Output JSON to our own stdout
@@ -39,6 +39,12 @@ export function parseLiterateJava(
39
39
  continue;
40
40
  }
41
41
 
42
+ // Skip fences with a non-Java language tag (e.g., ```text)
43
+ const lang = token.info.trim();
44
+ if (token.type === 'fence' && lang !== '' && lang !== 'java') {
45
+ continue;
46
+ }
47
+
42
48
  const code = token.content;
43
49
  const codeLines = code.endsWith('\n')
44
50
  ? code.slice(0, -1).split('\n')
@@ -138,6 +144,7 @@ export function executeLiterateJava(
138
144
  className: string,
139
145
  classPath: string,
140
146
  codeBlocks: LiterateCodeBlock[],
147
+ stdin?: string,
141
148
  ): LiterateRunnerEntry[] {
142
149
  const runnerDir = path.join(__dirname, 'jdi-runner');
143
150
  ensureRunnerCompiled(runnerDir);
@@ -153,9 +160,13 @@ export function executeLiterateJava(
153
160
  const result = execFileSync(
154
161
  'java',
155
162
  ['-cp', runnerDir, 'LiterateRunner', className, classPath, rangesJson],
156
- { timeout: 30000, encoding: 'utf-8' },
163
+ { timeout: 30000, encoding: 'utf-8', input: stdin },
157
164
  );
158
- const entries: LiterateRunnerEntry[] = JSON.parse(result);
165
+ const raw: [number, string][] = JSON.parse(result);
166
+ const entries: LiterateRunnerEntry[] = raw.map(([blockIndex, output]) => ({
167
+ blockIndex,
168
+ output,
169
+ }));
159
170
  log.debug`LiterateRunner returned ${entries.length} output entries`;
160
171
  return entries;
161
172
  } catch (err: unknown) {
@@ -127,9 +127,7 @@ export function injectAssetTags(
127
127
  .join('');
128
128
  const asyncLinkTags = asyncAssets
129
129
  .map(
130
- asset =>
131
- `<link href="${applyBasePath('/' + asset)}" rel="stylesheet" media="print" onload="this.media='all'">` +
132
- `<noscript><link href="${applyBasePath('/' + asset)}" rel="stylesheet"></noscript>`,
130
+ asset => `<link href="${applyBasePath('/' + asset)}" rel="stylesheet">`,
133
131
  )
134
132
  .join('');
135
133
 
@@ -143,9 +141,7 @@ export function injectKatexStylesheet(
143
141
  applyBasePath: (subPath: string) => string,
144
142
  ): string {
145
143
  const href = applyBasePath('/katex/katex.min.css');
146
- const tag =
147
- `<link href="${href}" rel="stylesheet" media="print" onload="this.media='all'">` +
148
- `<noscript><link href="${href}" rel="stylesheet"></noscript>`;
144
+ const tag = `<link href="${href}" rel="stylesheet">`;
149
145
  return html.replace('<head>', `<head>${tag}`);
150
146
  }
151
147
 
@@ -416,6 +412,9 @@ export function renderLiterateJavaPageAsset({
416
412
 
417
413
  validateFrontMatter(pageVariables, filePath);
418
414
 
415
+ const stdin =
416
+ typeof pageVariables.stdin === 'string' ? pageVariables.stdin : undefined;
417
+
419
418
  // Compile and execute the concatenated Java source
420
419
  let tempDir: string | undefined;
421
420
  let blockOutputMap: Map<number, string> | null = null;
@@ -429,6 +428,7 @@ export function renderLiterateJavaPageAsset({
429
428
  className,
430
429
  tempDir,
431
430
  codeBlocks,
431
+ stdin,
432
432
  );
433
433
  blockOutputMap = new Map(
434
434
  outputEntries.map(e => [e.blockIndex, e.output]),
@@ -447,9 +447,17 @@ export function renderLiterateJavaPageAsset({
447
447
  validatorOptions: { enabled: false },
448
448
  });
449
449
  let fenceIndex = 0;
450
+ const defaultFence = md.renderer.rules.fence!;
450
451
 
451
- md.renderer.rules.fence = (tokens, idx) => {
452
+ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
452
453
  const token = tokens[idx];
454
+ const lang = token.info.trim();
455
+
456
+ // Non-Java fences render with the default renderer (no line numbers)
457
+ if (lang !== '' && lang !== 'java') {
458
+ return defaultFence(tokens, idx, options, env, self);
459
+ }
460
+
453
461
  const code = token.content;
454
462
  const lines = code.endsWith('\n')
455
463
  ? code.slice(0, -1).split('\n')
package/build/watch.ts CHANGED
@@ -75,7 +75,7 @@ type ChangeCategory = 'content' | 'public' | 'config';
75
75
  const log = makeLogger(__filename);
76
76
  const wslog = makeLogger('WebSocket');
77
77
 
78
- // --- WebSocket server (unchanged) ---
78
+ // --- WebSocket server ---
79
79
 
80
80
  let webSocketsReady = false;
81
81
  let watcherReady = false;
@@ -511,23 +511,28 @@ initialBuild()
511
511
  initialBuildFailed = true;
512
512
  })
513
513
  .finally(() => {
514
- // Watch content, public, data files, site config
515
514
  const watchPaths: string[] = [contentDir, publicDir];
516
515
 
517
- // Watch data files (nav.json, authors.json)
516
+ // Watch config files (nav.json, authors.json, site config) via the
517
+ // project root directory (depth 0). Chokidar v4 doesn't emit unlink
518
+ // for individually watched files, so we must watch the directory.
518
519
  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);
520
+ const configFilePaths = new Set([
521
+ ...JSON_DATA_FILES.map(f => path.resolve(jsonDataDir, f)),
522
+ path.resolve('site.dev.json'),
523
+ ]);
524
+ const configWatcher = chokidar.watch(jsonDataDir, {
525
+ ignoreInitial: true,
526
+ depth: 0,
527
+ });
528
+ const onConfigFileChange = (filePath: string) => {
529
+ if (configFilePaths.has(path.resolve(filePath))) {
530
+ onFileChange(filePath);
523
531
  }
524
- }
525
-
526
- // Watch site config
527
- const siteConfigPath = path.resolve('site.dev.json');
528
- if (fs.existsSync(siteConfigPath)) {
529
- watchPaths.push(siteConfigPath);
530
- }
532
+ };
533
+ configWatcher.on('add', onConfigFileChange);
534
+ configWatcher.on('change', onConfigFileChange);
535
+ configWatcher.on('unlink', onConfigFileChange);
531
536
 
532
537
  const watcher = chokidar.watch(watchPaths, {
533
538
  ignoreInitial: true,
@@ -0,0 +1,129 @@
1
+ parent: /labs/00/index.html
2
+ parentLabel: Lab 0
3
+ title: Counting vowels
4
+ author: alex
5
+ description: An example of a literate Java program that takes predefined input.
6
+ stdin: "hello, world!\n"
7
+
8
+ A Java program can process input in different ways: it could open a file and
9
+ read it; it could open a network connection and wait for data from another
10
+ computer on the Internet, or it can read from the standard in.
11
+
12
+ The <dfn>standard in</dfn> (or `stdin`) is one of three streams available to a
13
+ running program. The other two streams are the standard out and standard error.
14
+
15
+ <figure>
16
+ <svg viewBox="0 50 410 90" xmlns="http://www.w3.org/2000/svg" font-size="14" fill="none" style="margin:1rem auto;display:block;max-width:410px">
17
+ <defs>
18
+ <marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
19
+ <path d="M0,0 L8,4 L0,8 Z" stroke="currentColor" fill="currentColor"></path>
20
+ </marker>
21
+ </defs>
22
+ <!-- Java program box -->
23
+ <rect x="155" y="60" width="100" height="60" stroke-width="2" stroke="currentColor"></rect>
24
+ <text x="205" y="95" text-anchor="middle" fill="currentColor">Program</text>
25
+ <!-- stdin arrow (incoming from left) -->
26
+ <path d="M20 90H143" marker-end="url(#arrow)" stroke="currentColor"></path>
27
+ <text x="80" y="82" text-anchor="middle" fill="currentColor" font-family="var(--mono-font)">stdin</text>
28
+ <!-- stdout arrow (outgoing, upper right) -->
29
+ <path d="M267 75H390" marker-end="url(#arrow)" stroke="currentColor"></path>
30
+ <text x="328" y="67" text-anchor="middle" fill="currentColor" font-family="var(--mono-font)">stdout</text>
31
+ <!-- stderr arrow (outgoing, lower right) -->
32
+ <path d="M267 105H390" marker-end="url(#arrow)" stroke="currentColor"></path>
33
+ <text x="328" y="125" text-anchor="middle" fill="currentColor" font-family="var(--mono-font)">stderr</text>
34
+ </svg>
35
+ <figcaption>
36
+ A running Java program (depicted as a box) with the standard in stream as an
37
+ incoming arrow and the standard out & error streams as outgoing arrows.
38
+ </figcaption>
39
+ </figure>
40
+
41
+ When a Java program calls `System.out.print()` (or `IO.print()`), the program
42
+ sends data to the standard out. When a program uses `System.err.print()`, it
43
+ sends data to the standard error, an output stream similar to `System.out`
44
+ but reserved for error messages.
45
+
46
+ `System.in` is an `InputStream` object that allows a program to read characters
47
+ from the standard input, one character at a time. Its `read()` method returns an
48
+ integer value.
49
+
50
+ - If the integer is positive, it represents a Unicode character coming through
51
+ the stream.
52
+ - If the integer is -1, there is no more input in the stream.
53
+
54
+ The -1 is known as a <dfn>sentinel</dfn>, or a reserved value known ahead of
55
+ time to represent a specific condition. Because all Unicode characters are
56
+ positive numbers, -1 cannot be converted to a character, and is instead used
57
+ to represent the specific condition of "*no more input in the stream*."
58
+
59
+ Let's implement a program that uses `System.in.read()` to get all the characters
60
+ from the standard in, counting how many characters are vowels. When there are
61
+ no more characters in the stream, the program prints the total number of vowels.
62
+
63
+ <!---
64
+ Hiding the method signature for main() keeps us focused on the program's logic,
65
+ and avoids discussion of IOException.
66
+ ```
67
+ void main() throws Exception {
68
+ ```
69
+ -->
70
+
71
+ We'll start the program with a `numVowels` counter initialized to zero,
72
+ the additive identity, and a second integer variable `ch` used to store the
73
+ return value of the `read()` method:
74
+
75
+ ```
76
+ int numVowels = 0;
77
+ int ch;
78
+ ```
79
+
80
+ Using an indefinite loop (since we don't know in advance how many characters
81
+ we'll get), we call the `read()` method and check if it returned the sentinel:
82
+
83
+ ```
84
+ while (true) {
85
+ ch = System.in.read();
86
+ if (ch == -1) {
87
+ break;
88
+ }
89
+ ```
90
+
91
+ If `read()` returns the sentinel, we stop the loop with `break`. Otherwise,
92
+ we test whether `ch` is a vowel:
93
+
94
+ ```
95
+ if ("aeiouAEIOU".indexOf(ch) >= 0) {
96
+ numVowels++;
97
+ }
98
+ ```
99
+
100
+ <!---
101
+ Hide the closing brace of the `while` loop to avoid indenting the conditional.
102
+ ```
103
+ }
104
+ ```
105
+ -->
106
+
107
+ Putting lower case and upper case vowels into a `String` allows us to use the
108
+ `indexOf()` method to check if `ch` is one of those characters. If `ch` is
109
+ nowhere in the string, `indexOf()` returns -1.
110
+
111
+ When reading input from a terminal, the loop ends when the user presses
112
+ <kbd>Ctrl</kbd>+<kbd>D</kbd> on the keyboard. If we type the input
113
+
114
+ ```text
115
+ hello, world!
116
+ ```
117
+
118
+ Then the last line of the program, which prints the total count, will output 3:
119
+
120
+ ```
121
+ IO.println("vowels: " + numVowels);
122
+ ```
123
+
124
+ <!---
125
+ Hiding the closing brace for main().
126
+ ```
127
+ }
128
+ ```
129
+ -->
@@ -0,0 +1,29 @@
1
+ parent: /labs/index.html
2
+ parentLabel: Labs
3
+ title: Lab 0
4
+ author: alex
5
+ description: An example lab page for <%= site.title %>.
6
+
7
+ In this lab, we'll explore the [`VowelCounter` program](./VowelCounter.html).
8
+
9
+ ## Review questions
10
+
11
+ ??? question What is an input stream?
12
+
13
+ An input stream is an abstraction that represents arbitrary data going into a
14
+ program. The data in a stream doesn't have a predefined size or length.
15
+ When you write a program that uses an input stream, the program may wait for
16
+ more data.
17
+
18
+ ???
19
+
20
+ ??? question What is a sentinel? Provide an example.
21
+
22
+ A sentinel is a special value a program uses to change its behavior.
23
+ For example, `System.in.read()` returns -1 when the standard in stream doesn't
24
+ have any more characters.
25
+
26
+ Another example: the character `'q'` might be reserved to indicate a command
27
+ like "quit." When a user presses <kbd>Q</kbd>, the program exits.
28
+
29
+ ???
@@ -0,0 +1,9 @@
1
+ title: Labs
2
+ author: alex
3
+ description: An example page for <%= site.title %>.
4
+
5
+ In each one-hour lab, we'll dive deeper and write code together, exploring how
6
+ to apply the concepts we learned in lecture to solve real-world problems and
7
+ write full-length programs.
8
+
9
+ 0. [`VowelCounter` and user input](/labs/00/index.html)
@@ -1,5 +1,6 @@
1
1
  title: A generic `Pair` class
2
2
  author: alex
3
+ description: An example of a literate Java program.
3
4
 
4
5
  <!---
5
6
  You may hide code from the generated HTML page but still include it in the
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/abreen/Tada.git"
10
+ },
11
+ "author": "Alex Breen <npm@abreen.email>",
7
12
  "bin": {
8
13
  "tada": "bin/tada.ts"
9
14
  },
@@ -20,6 +25,7 @@
20
25
  ],
21
26
  "scripts": {
22
27
  "prepare": "husky && bun run scripts/check-deps.ts",
28
+ "prepack": "bun run scripts/prepack.ts",
23
29
  "init-example": "bun run bin/tada.ts init example --no-interactive",
24
30
  "lint": "eslint src/ build/ bin/",
25
31
  "typecheck": "tsc --noEmit",
@@ -29,6 +35,10 @@
29
35
  "engines": {
30
36
  "bun": ">=1.0.0"
31
37
  },
38
+ "os": [
39
+ "darwin",
40
+ "linux"
41
+ ],
32
42
  "dependencies": {
33
43
  "@tsconfig/node23": "^23.0.2",
34
44
  "@vscode/markdown-it-katex": "^1.1.2",
@@ -4,13 +4,12 @@
4
4
  "properties": {
5
5
  "features": {
6
6
  "type": "object",
7
- "default": { "search": true, "code": true },
7
+ "default": { "search": true, "code": true, "favicon": true },
8
8
  "properties": {
9
9
  "search": { "type": "boolean", "default": true },
10
10
  "code": { "type": "boolean", "default": true },
11
- "favicon": { "type": "boolean", "default": false }
11
+ "favicon": { "type": "boolean", "default": true }
12
12
  },
13
- "required": ["search", "code", "favicon"],
14
13
  "additionalProperties": false
15
14
  },
16
15
  "base": { "type": "string", "pattern": "^https?:\\/\\/[-.:a-zA-Z0-9]+$" },
package/src/_base.scss CHANGED
@@ -1,3 +1,5 @@
1
+ @use 'mixins' as *;
2
+
1
3
  :root {
2
4
  --box-border-width: 5px;
3
5
 
@@ -13,10 +15,6 @@
13
15
  --main-width: 900px;
14
16
  }
15
17
 
16
- p {
17
- margin: 0 0 1rem 0;
18
- }
19
-
20
18
  h1,
21
19
  h2,
22
20
  h3,
@@ -118,6 +116,43 @@ pre {
118
116
  }
119
117
  }
120
118
 
119
+ details .content > *:first-child {
120
+ margin-top: 0;
121
+ }
122
+
123
+ details .content > *:last-child {
124
+ margin-bottom: 0;
125
+ }
126
+
127
+ details summary {
128
+ cursor: pointer;
129
+ @include user-select(none);
130
+ padding: var(--details-padding);
131
+ }
132
+
133
+ details .content {
134
+ padding: 0 var(--details-padding) var(--details-padding)
135
+ var(--details-padding);
136
+ }
137
+
138
+ @media (hover: hover) {
139
+ details summary {
140
+ &:hover {
141
+ color: var(--fg2-color);
142
+ }
143
+
144
+ &:active {
145
+ color: var(--link-active-color);
146
+ }
147
+ }
148
+ }
149
+
150
+ main details {
151
+ margin-bottom: 1rem;
152
+ border-radius: var(--border-radius);
153
+ border: 2px solid var(--fg-color);
154
+ }
155
+
121
156
  input[type='text'],
122
157
  input[type='search'] {
123
158
  background: var(--bg-color);
@@ -143,3 +178,13 @@ input[type='search'] {
143
178
  cursor: not-allowed;
144
179
  }
145
180
  }
181
+
182
+ figure {
183
+ margin: 1rem 0;
184
+
185
+ figcaption {
186
+ margin: 0 3rem;
187
+ font-size: var(--font-size-smaller);
188
+ color: var(--fg2-color);
189
+ }
190
+ }
package/src/_layout.scss CHANGED
@@ -52,7 +52,7 @@ body.literate main table {
52
52
  width: 100%;
53
53
  }
54
54
 
55
- @media (max-width: 899px) {
55
+ @media (width < 900px) {
56
56
  .toc-is-active .main-content {
57
57
  display: flex;
58
58
  flex-direction: column;
@@ -81,7 +81,7 @@ body.literate main table {
81
81
  }
82
82
  }
83
83
 
84
- @media (min-width: 900px) {
84
+ @media (width >= 900px) {
85
85
  body.default main table,
86
86
  body.literate main table {
87
87
  width: auto;
@@ -35,8 +35,7 @@
35
35
  @include user-select(none);
36
36
  }
37
37
 
38
- .question-a-body > ol:first-child,
39
- .question-a-body > ul:first-child {
38
+ .question-a-body > *:first-child {
40
39
  margin-top: 0;
41
40
  }
42
41
 
@@ -49,19 +48,25 @@
49
48
  @media (prefers-reduced-motion: no-preference) {
50
49
  transition:
51
50
  color 0.25s,
52
- background-color 0.25s;
51
+ background-color 0.25s,
52
+ border-color 0.25s,
53
+ box-shadow 0.25s;
53
54
  }
54
55
  }
55
56
 
56
57
  .question-a-body:not([data-revealed]) > * {
57
58
  color: transparent;
58
59
  background-color: var(--bg2-color);
60
+ border-color: transparent;
61
+ box-shadow: none;
59
62
  pointer-events: none;
60
63
  }
61
64
 
62
65
  .question-a-body:not([data-revealed]) > * * {
63
66
  color: transparent;
64
67
  background-color: var(--bg2-color);
68
+ border-color: transparent;
69
+ box-shadow: none;
65
70
  }
66
71
 
67
72
  .question-a-body::after {
@@ -61,12 +61,8 @@ header {
61
61
  color: var(--fg2-color);
62
62
 
63
63
  kbd {
64
- display: inline-block;
65
- padding: 1px 5px;
66
- border: 1px solid var(--fg2-color);
67
- border-radius: 4px;
68
- font-family: var(--font);
69
- font-size: var(--font-size-small);
64
+ border-color: var(--fg2-color);
65
+ box-shadow: 0 2px 0 var(--fg2-color);
70
66
  line-height: 1.4;
71
67
  }
72
68
  }
package/src/style.scss CHANGED
@@ -133,34 +133,6 @@ a.footnote-ref,
133
133
  @include user-select(none);
134
134
  }
135
135
 
136
- details .content > p:last-child,
137
- details .content > div:last-child {
138
- margin-bottom: 0;
139
- }
140
-
141
- details summary {
142
- cursor: pointer;
143
- @include user-select(none);
144
- padding: var(--details-padding);
145
- }
146
-
147
- details .content {
148
- padding: 0 var(--details-padding) var(--details-padding)
149
- var(--details-padding);
150
- }
151
-
152
- @media (hover: hover) {
153
- details summary {
154
- &:hover {
155
- color: var(--fg2-color);
156
- }
157
-
158
- &:active {
159
- color: var(--link-active-color);
160
- }
161
- }
162
- }
163
-
164
136
  hr {
165
137
  border: none;
166
138
  height: 32px;
@@ -181,16 +153,6 @@ hr::after {
181
153
  mask-repeat: no-repeat;
182
154
  }
183
155
 
184
- main p,
185
- main details {
186
- margin-bottom: 1rem;
187
- }
188
-
189
- main details {
190
- border-radius: var(--border-radius);
191
- border: 2px solid var(--fg-color);
192
- }
193
-
194
156
  button,
195
157
  a.button,
196
158
  input[type='submit'] {
@@ -244,10 +206,6 @@ button.icon-button {
244
206
  vertical-align: top;
245
207
  }
246
208
 
247
- li {
248
- margin-bottom: 0.5rem;
249
- }
250
-
251
209
  ol.styled-list,
252
210
  ul.styled-list {
253
211
  color: var(--theme-color-text);
@@ -472,14 +430,23 @@ figure.bio {
472
430
  }
473
431
 
474
432
  figcaption {
475
- font-size: var(--font-size-small);
476
- color: var(--fg2-color);
477
433
  text-align: center;
478
434
  margin-top: var(--gap);
479
435
  font-style: italic;
480
436
  }
481
437
  }
482
438
 
439
+ kbd {
440
+ display: inline-block;
441
+ padding: 1px 5px;
442
+ border: 1px solid var(--fg-color);
443
+ border-radius: 4px;
444
+ box-shadow: 0 2px 0 var(--fg-color);
445
+ font-family: var(--font);
446
+ font-size: var(--font-size-small);
447
+ font-weight: 500;
448
+ }
449
+
483
450
  select {
484
451
  font-family: var(--font);
485
452
  color: var(--fg-color);
@@ -100,25 +100,10 @@ function computeOffsets(baseDate: Date) {
100
100
  });
101
101
  }
102
102
 
103
- function init(element: HTMLDataListElement, selectedTz: string) {
104
- const beforeText = window.document.createTextNode('Times shown in ');
105
- element.parentElement?.insertBefore(beforeText, element);
106
-
107
- TIMEZONES.forEach(tz => {
108
- const opt = document.createElement('option');
109
- opt.value = tz.value;
110
- if (tz.value === 'UTC') {
111
- opt.textContent = tz.label;
112
- } else {
113
- opt.textContent = `${tz.label} (${tz.abbreviation})`;
114
- }
115
- if (tz.value === selectedTz) {
116
- opt.selected = true;
117
- }
118
- element.appendChild(opt);
119
- });
120
-
121
- element.removeAttribute('hidden');
103
+ function init(element: HTMLSelectElement, selectedTz: string) {
104
+ element.value = selectedTz;
105
+ element.hidden = false;
106
+ element.disabled = false;
122
107
  }
123
108
 
124
109
  function dominantPeriodStyle(
@@ -42,6 +42,7 @@
42
42
  }]
43
43
  }
44
44
  </script>
45
+ <script>document.head.appendChild(document.createElement('style')).textContent='.time-zone-label[hidden],select.time-zone[hidden]{display:revert}'</script>
45
46
  <noscript><style>
46
47
  .question-a-body:not([data-revealed]) > * { color: unset; background-color: unset; }
47
48
  .question-a-body:not([data-revealed]) > * * { color: unset; background-color: unset; }