@abreen/tada 1.19.5 → 1.20.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
@@ -62,8 +62,6 @@ A static site generator. The successor to Presto.
62
62
  * Highlights the heading currently being viewed
63
63
 
64
64
  - Extended Markdown syntax
65
- * `<<< details ... <<<` renders a collapsible box
66
- * `::: section ... :::` renders a special section with a fancy background
67
65
  * `!!! note ... !!!` and `!!! warning ... !!!` render alert boxes
68
66
  * `??? question ... ???` renders a Q&A section; answer is hidden until click
69
67
  - To display clickable multiple choice options, use a [task list][task-list]
package/build/bundle.ts CHANGED
@@ -3,11 +3,7 @@ import path from 'path';
3
3
  import { compileTemplate } from './lodash-template';
4
4
  import * as sass from 'sass';
5
5
  import { getPackageDir, toPosix } from './utils/paths';
6
- import {
7
- deriveTheme,
8
- deriveLinkHue,
9
- deriveTraceLineActiveHue,
10
- } from './utils/derive-theme';
6
+ import { deriveTheme } from './utils/derive-theme';
11
7
  import type { PluginBuilder } from 'bun';
12
8
  import type { SiteVariables } from './types';
13
9
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
@@ -38,14 +34,6 @@ function getBundleNaming(): string {
38
34
  return `[name].bundle.tada-${version}.[ext]`;
39
35
  }
40
36
 
41
- function formatCssNumber(value: number, precision = 4): string {
42
- if (Number.isInteger(value)) {
43
- return String(value);
44
- }
45
-
46
- return value.toFixed(precision).replace(/\.?0+$/, '');
47
- }
48
-
49
37
  // Returns the rendered SCSS source of the theme module (`config/theme`)
50
38
  function renderThemeScss(siteVariables: SiteVariables): string {
51
39
  const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
@@ -54,14 +42,6 @@ function renderThemeScss(siteVariables: SiteVariables): string {
54
42
  const tintHue = siteVariables.tintHue ?? 190;
55
43
  const tintAmount = siteVariables.tintAmount ?? 100;
56
44
 
57
- const linkHue = formatCssNumber(deriveLinkHue(tintHue));
58
- const linkColor = `hsl(${linkHue}deg 66% 38%)`;
59
- const linkColorHover = `hsl(${linkHue}deg 56% 49%)`;
60
- const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
61
- const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
62
- const traceLineActiveHue = formatCssNumber(deriveTraceLineActiveHue(tintHue));
63
- const bgTraceLineActive = `hsl(${traceLineActiveHue}deg 100% 86%)`;
64
- const bgTraceLineActiveDark = `hsl(${traceLineActiveHue}deg 90% 18%)`;
65
45
  const customFontFaces = renderCustomFontFaceScss(siteVariables.fontOverrides);
66
46
  const customFontTuning = renderCustomFontTuningScss(
67
47
  siteVariables.fontOverrides,
@@ -71,12 +51,6 @@ function renderThemeScss(siteVariables: SiteVariables): string {
71
51
  ...theme,
72
52
  tintHue,
73
53
  tintAmount,
74
- linkColor,
75
- linkColorHover,
76
- linkColorDark,
77
- linkColorHoverDark,
78
- bgTraceLineActive,
79
- bgTraceLineActiveDark,
80
54
  serifFontStack: getSerifFontStack(siteVariables.fontOverrides),
81
55
  serifMonoFontStack: getSerifMonoFontStack(siteVariables.fontOverrides),
82
56
  serifFontFeatureSettings: renderFontFeatureSettings(
@@ -203,7 +203,7 @@ export function createPublicRecord(
203
203
  };
204
204
  }
205
205
 
206
- export function collectSourceOutputs(
206
+ function collectSourceOutputs(
207
207
  assets: Asset[],
208
208
  traceOutputs: ReadonlyMap<string, string>,
209
209
  ): Map<string, OutputContent> {
@@ -223,7 +223,7 @@ function cloneHtmlOutputAnalysis(
223
223
  return { outgoingTargets: new Set(analysis.outgoingTargets) };
224
224
  }
225
225
 
226
- export function collectSourceHtmlAnalysis(
226
+ function collectSourceHtmlAnalysis(
227
227
  assets: Asset[],
228
228
  ): Map<string, HtmlOutputAnalysis> {
229
229
  const htmlAnalysisByOutputPath = new Map<string, HtmlOutputAnalysis>();
@@ -23,6 +23,14 @@ interface AlertItem {
23
23
 
24
24
  type TocItem = HeadingItem | DinkusItem | AlertItem;
25
25
 
26
+ // Net <details> elements an HTML block opens. Markdown between a raw <details>
27
+ // and its </details> is parsed as separate tokens, so track the nesting here.
28
+ function detailsDepthChange(html: string): number {
29
+ const opened = html.match(/<details[\s>]/gi)?.length ?? 0;
30
+ const closed = html.match(/<\/details\s*>/gi)?.length ?? 0;
31
+ return opened - closed;
32
+ }
33
+
26
34
  export function tocPlugin(md: MarkdownIt): void {
27
35
  md.core.ruler.push('toc_collector', state => {
28
36
  if (!state.env) {
@@ -31,14 +39,27 @@ export function tocPlugin(md: MarkdownIt): void {
31
39
 
32
40
  const tokens = state.tokens;
33
41
  const items: TocItem[] = [];
34
- const containerStack: string[] = [];
42
+ let containerDepth = 0;
43
+ let detailsDepth = 0;
35
44
  const usedAlertIds = new Map<string, number>();
36
45
 
37
46
  for (let i = 0; i < tokens.length; i++) {
38
47
  const token = tokens[i];
39
48
 
40
- // Headings (included at any nesting level)
49
+ // Nothing inside a <details> is listed
50
+ if (token.type === 'html_block') {
51
+ detailsDepth = Math.max(
52
+ 0,
53
+ detailsDepth + detailsDepthChange(token.content),
54
+ );
55
+ continue;
56
+ }
57
+
58
+ // Headings (included at any container depth, but not inside a <details>)
41
59
  if (token.type === 'heading_open') {
60
+ if (detailsDepth > 0) {
61
+ continue;
62
+ }
42
63
  const inline = tokens[i + 1];
43
64
  if (!inline || inline.type !== 'inline') {
44
65
  continue;
@@ -56,22 +77,20 @@ export function tocPlugin(md: MarkdownIt): void {
56
77
  continue;
57
78
  }
58
79
 
59
- // Thematic breaks / dinkuses (only at top level)
80
+ // Thematic breaks / dinkuses (not inside an alert, question, or <details>)
60
81
  if (
61
82
  token.type === 'hr' &&
62
- containerStack.length === 0 &&
83
+ containerDepth === 0 &&
84
+ detailsDepth === 0 &&
63
85
  !state.env?.slides
64
86
  ) {
65
87
  items.push({ kind: 'dinkus' });
66
88
  continue;
67
89
  }
68
90
 
69
- // Alerts (only at top level or directly inside a section)
91
+ // Alerts (not inside another alert, question, or <details>)
70
92
  // Must be checked before generic container tracking below
71
93
  if (token.type === 'container_alert_open') {
72
- const depth = containerStack.length;
73
- const parentIsSection = depth === 1 && containerStack[0] === 'section';
74
-
75
94
  const match = token.info
76
95
  .trim()
77
96
  .match(/^(note|warning)(?:\s+"(.+)"|\s+(.+))?$/);
@@ -81,7 +100,7 @@ export function tocPlugin(md: MarkdownIt): void {
81
100
  // Assign IDs to every alert, including those excluded from the TOC.
82
101
  const id = deduplicateId(usedAlertIds, textToId(title || type));
83
102
  token.attrSet('id', id);
84
- if (depth === 0 || parentIsSection) {
103
+ if (containerDepth === 0 && detailsDepth === 0) {
85
104
  const displayTitle = title
86
105
  ? md.utils.escapeHtml(curlyQuote(title))
87
106
  : type === 'warning'
@@ -90,23 +109,19 @@ export function tocPlugin(md: MarkdownIt): void {
90
109
  items.push({ kind: 'alert', type, title: displayTitle, id });
91
110
  }
92
111
  }
93
- // Fall through to push 'alert' onto container stack
112
+ // Fall through to count the alert as a container
94
113
  }
95
114
 
96
115
  // Track container nesting
97
116
  if (token.type.startsWith('container_') && token.type.endsWith('_open')) {
98
- const containerType = token.type.slice(
99
- 'container_'.length,
100
- -'_open'.length,
101
- );
102
- containerStack.push(containerType);
117
+ containerDepth++;
103
118
  continue;
104
119
  }
105
120
  if (
106
121
  token.type.startsWith('container_') &&
107
122
  token.type.endsWith('_close')
108
123
  ) {
109
- containerStack.pop();
124
+ containerDepth--;
110
125
  continue;
111
126
  }
112
127
  }
package/build/types.d.ts CHANGED
@@ -217,10 +217,8 @@ export interface LiterateCodeBlock {
217
217
  hidden: boolean;
218
218
  }
219
219
 
220
- /** Result from parseLiterateJava */
220
+ /** Result from extractLiterateJavaCode */
221
221
  export interface LiterateJavaParseResult {
222
- pageVariables: Record<string, unknown>;
223
- content: string;
224
222
  javaSource: string;
225
223
  codeBlocks: LiterateCodeBlock[];
226
224
  visibleBlockIndices: number[];
@@ -1,5 +1,5 @@
1
- import { parse, oklch, rgb, toGamut, formatHex } from 'culori';
2
- import type { Oklch, Rgb } from 'culori';
1
+ import { parse, oklch, toGamut, formatHex, wcagContrast } from 'culori';
2
+ import type { Oklch } from 'culori';
3
3
  import type { DerivedTheme } from '../types';
4
4
 
5
5
  // OKLCH lightness range for the theme color used as backgrounds/outlines
@@ -14,36 +14,17 @@ const LIGHT_TEXT_L_MAX = 0.5;
14
14
  const DARK_TEXT_L_MIN = 0.7;
15
15
  const DARK_TEXT_L_MAX = 0.82;
16
16
 
17
- const LINK_ANCHOR_HUE = 212;
18
- const LINK_TINT_BLEND = 0.05;
19
- const TRACE_LINE_ACTIVE_ANCHOR_HUE = 52;
20
- const TRACE_LINE_ACTIVE_TINT_BLEND = 0.1;
21
-
22
17
  function clamp(v: number, min: number, max: number): number {
23
18
  return Math.min(Math.max(v, min), max);
24
19
  }
25
20
 
26
- function linearize(c: number): number {
27
- return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
28
- }
29
-
30
- function relativeLuminance({ r, g, b }: Rgb): number {
31
- return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
32
- }
33
-
34
- function contrastRatio(l1: number, l2: number): number {
35
- return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
36
- }
37
-
38
21
  // Pick black or white text for use on the given background color.
39
- // Prefer white unless black has substantially better contrast (1.5x),
22
+ // Prefer white unless black has substantially better contrast (1.9x),
40
23
  // which gives white text on saturated mid-tone colors like steelblue and tomato.
41
24
  function pickTextColor(bgHex: string): '#fff' | '#000' {
42
- const bgRgb = rgb(parse(bgHex)) as Rgb;
43
- const bgLum = relativeLuminance(bgRgb);
44
- const whiteContrast = contrastRatio(1, bgLum);
45
- const blackContrast = contrastRatio(bgLum, 0);
46
- return blackContrast > whiteContrast * 1.9 ? '#000' : '#fff';
25
+ return wcagContrast(bgHex, '#000') > wcagContrast(bgHex, '#fff') * 1.9
26
+ ? '#000'
27
+ : '#fff';
47
28
  }
48
29
 
49
30
  function toHex(oklchColor: Oklch): string {
@@ -56,48 +37,20 @@ export function deriveTheme(cssColor: string): DerivedTheme {
56
37
  throw new Error(`Invalid color: ${cssColor}`);
57
38
  }
58
39
 
59
- const base = oklch(parsed) as Oklch;
60
- const l = base.l;
61
- const c = base.c || 0;
62
- const h = base.h;
63
-
64
- const lightL = clamp(l, LIGHT_THEME_L_MIN, LIGHT_THEME_L_MAX);
65
- const darkL = clamp(l, DARK_THEME_L_MIN, DARK_THEME_L_MAX);
40
+ // Keep the hue and chroma of the theme color, clamping only its lightness
41
+ const base = oklch(parsed);
42
+ const withLightness = (min: number, max: number) =>
43
+ toHex({ ...base, l: clamp(base.l, min, max) });
66
44
 
67
- const textLightL = clamp(l, LIGHT_TEXT_L_MIN, LIGHT_TEXT_L_MAX);
68
- const textDarkL = clamp(l, DARK_TEXT_L_MIN, DARK_TEXT_L_MAX);
69
-
70
- const themeColorLight = toHex({ mode: 'oklch', l: lightL, c, h });
71
- const themeColorDark = toHex({ mode: 'oklch', l: darkL, c, h });
45
+ const themeColorLight = withLightness(LIGHT_THEME_L_MIN, LIGHT_THEME_L_MAX);
46
+ const themeColorDark = withLightness(DARK_THEME_L_MIN, DARK_THEME_L_MAX);
72
47
 
73
48
  return {
74
49
  themeColorLight,
75
50
  themeColorDark,
76
- themeColorTextLight: toHex({ mode: 'oklch', l: textLightL, c, h }),
77
- themeColorTextDark: toHex({ mode: 'oklch', l: textDarkL, c, h }),
51
+ themeColorTextLight: withLightness(LIGHT_TEXT_L_MIN, LIGHT_TEXT_L_MAX),
52
+ themeColorTextDark: withLightness(DARK_TEXT_L_MIN, DARK_TEXT_L_MAX),
78
53
  textOnThemeLight: pickTextColor(themeColorLight),
79
54
  textOnThemeDark: pickTextColor(themeColorDark),
80
55
  };
81
56
  }
82
-
83
- export function deriveLinkHue(tintHue: number): number {
84
- const diff = ((tintHue - LINK_ANCHOR_HUE + 540) % 360) - 180;
85
- return (LINK_ANCHOR_HUE + diff * LINK_TINT_BLEND + 360) % 360;
86
- }
87
-
88
- export function deriveTraceLineActiveHue(tintHue: number): number {
89
- const diff = ((tintHue - TRACE_LINE_ACTIVE_ANCHOR_HUE + 540) % 360) - 180;
90
- return (
91
- (TRACE_LINE_ACTIVE_ANCHOR_HUE + diff * TRACE_LINE_ACTIVE_TINT_BLEND + 360) %
92
- 360
93
- );
94
- }
95
-
96
- export function getTextOnColor(cssColor: string): '#fff' | '#000' {
97
- const parsed = parse(cssColor);
98
- if (!parsed) {
99
- throw new Error(`Invalid color: ${cssColor}`);
100
- }
101
- const gamutMapped = formatHex(toGamut('rgb', 'oklch')(parsed));
102
- return pickTextColor(gamutMapped);
103
- }
@@ -3,11 +3,8 @@ import type Token from 'markdown-it/lib/token.mjs';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
5
  import { execFileSync } from 'child_process';
6
- import { createMarkdown } from './markdown';
7
6
  import { makeLogger } from '../log';
8
- import { parseFrontMatterAndContent } from './front-matter';
9
7
  import type {
10
- SiteVariables,
11
8
  LiterateJavaParseResult,
12
9
  LiterateCodeBlock,
13
10
  LiterateRunnerEntry,
@@ -50,29 +47,9 @@ let runnerClassDir: string | null = null;
50
47
 
51
48
  const MAIN_PATTERN = /\bvoid\s+main\s*\(/m;
52
49
 
53
- export function parseLiterateJava(
54
- rawContent: string,
55
- siteVariables: SiteVariables,
56
- ): LiterateJavaParseResult {
57
- const { pageVariables, content } = parseFrontMatterAndContent(
58
- rawContent,
59
- '.md',
60
- );
61
-
62
- const md = createMarkdown(siteVariables, {
63
- validatorOptions: { enabled: false },
64
- });
65
- const tokens = md.parse(content, {});
66
-
67
- return { pageVariables, content, ...extractLiterateJavaCode(tokens) };
68
- }
69
-
70
50
  export function extractLiterateJavaCode(
71
51
  tokens: Token[],
72
- ): Pick<
73
- LiterateJavaParseResult,
74
- 'javaSource' | 'codeBlocks' | 'visibleBlockIndices'
75
- > {
52
+ ): LiterateJavaParseResult {
76
53
  const codeBlocks: LiterateCodeBlock[] = [];
77
54
  let javaLine = 1;
78
55
 
@@ -19,7 +19,6 @@ import katexPlugin from './katex';
19
19
  import markdownPartialsPlugin from './markdown-partials';
20
20
  import type { RenderDependencyCollector, SiteVariables } from '../types';
21
21
 
22
- const DETAILS_PATTERN = /^details\s+(.*)$/;
23
22
  const ALERT_PATTERN = /^(note|warning)(?:\s+"(.+)"|\s+(.+))?$/;
24
23
  const QUESTION_PATTERN = /^question\s+(.+)$/;
25
24
  const MULTIPLE_CHOICE_MARKER_LENGTH = 4;
@@ -65,40 +64,7 @@ export function createMarkdown(
65
64
  .use(superlinkPlugin, siteVariables)
66
65
  .use(tocPlugin)
67
66
  .use(columnsPlugin)
68
- .use(katexPlugin)
69
- .use(markdownItContainer, 'details', {
70
- marker: '<',
71
- validate: function (params: string) {
72
- return DETAILS_PATTERN.test(params.trim());
73
- },
74
-
75
- render: function (tokens: Token[], idx: number) {
76
- const m = tokens[idx].info.trim().match(DETAILS_PATTERN);
77
-
78
- if (tokens[idx].nesting === 1) {
79
- return (
80
- '<details><summary>' +
81
- markdown.renderInline(m![1]) +
82
- '</summary><div class="content">\n'
83
- );
84
- } else {
85
- return '</div></details>\n';
86
- }
87
- },
88
- })
89
- .use(markdownItContainer, 'section', {
90
- marker: ':',
91
- validate: function (params: string) {
92
- return !!params.trim().match(/^section$/);
93
- },
94
- render: function (tokens: Token[], idx: number) {
95
- if (tokens[idx].nesting === 1) {
96
- return '<section>\n';
97
- } else {
98
- return '</section>\n';
99
- }
100
- },
101
- });
67
+ .use(katexPlugin);
102
68
 
103
69
  if (filePath && options.templateParams) {
104
70
  markdown.use(markdownPartialsPlugin, {
@@ -159,11 +159,12 @@ Please note:
159
159
  To provide some confidence that our `Pair` class works, let's write some tests.
160
160
 
161
161
  <!---
162
- Code blocks inside of Tada's special containers, like alerts, collapisbles,
163
- sections, etc. are included in the final .java file.
162
+ Code blocks inside of Tada's special containers, like alerts, or inside raw HTML
163
+ elements, like details, are included in the final .java file.
164
164
  -->
165
165
 
166
- <<< details The `test()` helper method
166
+ <details>
167
+ <summary>The <code>test()</code> helper method</summary>
167
168
 
168
169
  ```
169
170
  private static void test(boolean condition) {
@@ -179,7 +180,7 @@ sections, etc. are included in the final .java file.
179
180
  }
180
181
  ```
181
182
 
182
- <<<
183
+ </details>
183
184
 
184
185
  <!---
185
186
  When Tada renders a literate code block, it trims leading whitespace common to
@@ -10,9 +10,12 @@ One-hour lectures are held weekly at <time datetime="17:40">5:40 PM</time>.
10
10
  Each lecture is recorded and made available by password-protected link.
11
11
  Return to this page to access the recordings.
12
12
 
13
- <<< details More details
13
+ <details>
14
+ <summary>More details</summary>
15
+
14
16
  Here is a sample collapsible with more information.
15
- <<<
17
+
18
+ </details>
16
19
 
17
20
  Following lecture, an optional office hour is held, during which you may ask
18
21
  questions and get hints for the problem sets.
@@ -202,24 +202,6 @@ headings, so you may [link directly to a definition](#binary-tree):
202
202
  ```
203
203
 
204
204
 
205
- ### Collapsible section
206
-
207
- Use
208
-
209
- ```
210
- <<< details Title of *collapsible*
211
- Here's the content that is visible when expanded.
212
- <<<
213
- ```
214
-
215
- to create a collapsible section (the `<details>` element):
216
-
217
- <<< details Title of *collapsible*
218
- Here's the content that is visible when expanded. (When the page is being
219
- printed, all collapsibles are automatically opened.)
220
- <<<
221
-
222
-
223
205
  ### Alerts
224
206
 
225
207
  Brightly colored boxes that call attention to specific warnings or information.
@@ -304,45 +286,6 @@ becomes a multiple choice question with clickable options:
304
286
  ???
305
287
 
306
288
 
307
- ### Generic section
308
-
309
- Break up a long page by introducing a `<section>` (a standard HTML 5 element)
310
- which must start with a heading. Generic sections are displayed with a slightly
311
- different background.
312
-
313
- Use
314
-
315
- ```
316
- ::: section
317
-
318
- ### Submitting your work
319
-
320
- Here are the steps to follow when you are ready to submit your work for
321
- this problem set.
322
-
323
- ...
324
-
325
- :::
326
- ```
327
-
328
- to create:
329
-
330
- ::: section
331
-
332
- ### Submitting your work
333
-
334
- Here are the steps to follow when you are ready to submit your work for
335
- this problem set.
336
-
337
- 1. Step one
338
- 2. Step two
339
- 3. Step three
340
- 4. Step four
341
-
342
- Email the files you changed to the course staff.
343
-
344
- :::
345
-
346
289
  ### Two-column layout
347
290
 
348
291
  Use
@@ -383,16 +326,33 @@ Use
383
326
  [KaTeX][The library behind math support](https://katex.org/)
384
327
  ```
385
328
 
386
- to create a link with a title and a description:
329
+ to render a large, button-styled link with a title and description:
387
330
 
388
331
  [Home][Back to the front page](index.html)
389
332
  [Markdown][You are here](markdown.html)
390
333
  [KaTeX][The library behind math support](https://katex.org/)
391
334
 
392
- Superlinks are rendered as block elements, so don't use them inline, in the
393
- middle of a paragraph, or in a heading. Put them on their own lines, as above.
394
- A superlink to another website, like the last one above, opens in a new tab and
395
- shows an external link icon in place of the chevron.
335
+ The syntax is an extension of the normal link syntax, but with a second set of
336
+ square brackets `[ ]` for the superlink description.
337
+
338
+ Because superlinks are block elements, don't use them in paragraphs or headings.
339
+ Instead, separate them from surrrounding content with blank lines.
340
+
341
+ Combine superlinks with [two-column layout](#two-column-layout) to achieve a row
342
+ or grid of links:
343
+
344
+ +++
345
+
346
+ [First][Back to the front page](index.html)
347
+ [Second][Back to the front page](index.html)
348
+ [Third][Back to the front page](index.html)
349
+
350
+ +++
351
+
352
+ [Fourth][You are here](https://example.com)
353
+ [Fifth][You are here](markdown.html)
354
+
355
+ +++
396
356
 
397
357
  ---
398
358
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.19.5",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
package/src/_alerts.scss CHANGED
@@ -29,10 +29,11 @@
29
29
  color: var(--fg-color);
30
30
  }
31
31
 
32
- tt,
33
- code {
34
- padding: inherit;
35
- background: inherit;
32
+ // Inline code keeps its padding. A border replaces the background chip,
33
+ // which would clash with the alert's own background.
34
+ :is(tt, code):not(pre *) {
35
+ background: none;
36
+ border: 1px solid var(--fg-color);
36
37
  }
37
38
 
38
39
  a:not(.superlink) {
package/src/_base.scss CHANGED
@@ -122,14 +122,6 @@ pre {
122
122
  }
123
123
  }
124
124
 
125
- details .content > *:first-child {
126
- margin-top: 0;
127
- }
128
-
129
- details .content > *:last-child {
130
- margin-bottom: 0;
131
- }
132
-
133
125
  details summary {
134
126
  padding: var(--details-padding);
135
127
  cursor: pointer;
@@ -146,11 +138,6 @@ main details:focus-within {
146
138
  outline-offset: 1px;
147
139
  }
148
140
 
149
- details .content {
150
- padding: 0 var(--details-padding) var(--details-padding)
151
- var(--details-padding);
152
- }
153
-
154
141
  @media (hover: hover) {
155
142
  details summary:hover {
156
143
  color: var(--fg2-color);
@@ -164,11 +151,33 @@ details summary:active {
164
151
  }
165
152
 
166
153
  main details {
154
+ padding: 0 var(--details-padding);
167
155
  margin-bottom: 1rem;
168
156
  border: 2px solid var(--fg-color);
169
157
  border-radius: var(--border-radius);
170
158
  }
171
159
 
160
+ main details[open] {
161
+ padding-bottom: var(--details-padding);
162
+ }
163
+
164
+ /*
165
+ * Plain <details> in content has no .content wrapper, and its body can be bare
166
+ * text, so the details element supplies the inset. The summary spans the full
167
+ * width so its whole row stays clickable.
168
+ */
169
+ main details > summary {
170
+ margin: 0 calc(-1 * var(--details-padding));
171
+ }
172
+
173
+ main details > summary + * {
174
+ margin-top: 0;
175
+ }
176
+
177
+ main details > :last-child {
178
+ margin-bottom: 0;
179
+ }
180
+
172
181
  input[type='text'],
173
182
  input[type='search'] {
174
183
  padding: 4px;
@@ -168,9 +168,11 @@ body.code .code-prose {
168
168
  color: inherit;
169
169
  }
170
170
 
171
- code {
172
- padding: inherit;
173
- background: inherit;
171
+ // The prose block is on the same background as the inline code chip, so a
172
+ // border delineates inline code instead
173
+ :is(tt, code):not(pre *) {
174
+ background: none;
175
+ border: 1px solid var(--fg2-color);
174
176
  }
175
177
  }
176
178
  }
@@ -171,6 +171,15 @@ header .content {
171
171
  position: relative;
172
172
  box-sizing: border-box;
173
173
  width: 100cqw;
174
+ padding: 0 var(--details-padding) var(--details-padding);
175
+ }
176
+
177
+ header .content > :first-child {
178
+ margin-top: 0;
179
+ }
180
+
181
+ header .content > :last-child {
182
+ margin-bottom: 0;
174
183
  }
175
184
 
176
185
  header summary .logo {
@@ -1,7 +1,7 @@
1
1
  import { getElement } from '../util';
2
2
  import { globals } from '../globals';
3
3
 
4
- export type HeaderTitleState = 'site' | 'page';
4
+ type HeaderTitleState = 'site' | 'page';
5
5
 
6
6
  export interface HeadingGeometry {
7
7
  /** Bottom edge of the page heading block (breadcrumbs, h1, and info) */
@@ -103,11 +103,6 @@ export function closeHeaderDetails(document: Document): void {
103
103
  }
104
104
  }
105
105
 
106
- export interface NavigationEventDetail {
107
- path: string;
108
- validators: ResponseValidators;
109
- }
110
-
111
106
  function dispatchNavigationEvent(
112
107
  window: Window,
113
108
  validators: ResponseValidators,
@@ -46,18 +46,16 @@
46
46
  }
47
47
 
48
48
  .code-body pre,
49
- .code-prose-content code,
50
49
  .file-header tt,
51
50
  .file-header code {
52
51
  border: none;
53
52
  }
54
53
 
55
- // .alert strips the default padding from inline code (since it also
56
- // strips the background). In print we add a border, so restore the
57
- // padding so the border doesn't hug the text.
58
- .alert .content tt,
59
- .alert .content code {
60
- padding: var(--mono-padding);
54
+ // Alerts and code prose draw a themed border around inline code on screen.
55
+ // Print uses the same light border as other inline code.
56
+ .alert .content :is(tt, code):not(pre *),
57
+ body.code .code-prose .code-prose-content :is(tt, code):not(pre *) {
58
+ border-color: #ccc;
61
59
  }
62
60
 
63
61
  .footnotes {
package/src/toc/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { debounce } from '../util';
2
2
  import {
3
3
  alertToTableItem,
4
+ getHeadingsAndAlerts,
4
5
  getHighlightIndexes,
5
6
  headingToTableItem,
6
7
  switchCurrent,
7
- type Dinkus,
8
8
  } from './model';
9
9
 
10
10
  const LATENCY_MS = 50;
@@ -40,26 +40,6 @@ function scrollIfNeeded(element: HTMLElement, doc: Document) {
40
40
  container.scrollTo({ top: desiredScrollTop });
41
41
  }
42
42
 
43
- function getHeadingsAndAlerts(
44
- parent: HTMLElement,
45
- ): (HTMLHeadingElement | HTMLDivElement)[] {
46
- return Array.from(
47
- parent.querySelectorAll(
48
- '.body h1, .body h2, .body h3, .body h4, .body h5, .body h6, :is(.body, .page-content) > div.alert, .body section > div.alert',
49
- ),
50
- );
51
- }
52
-
53
- function getTocElements(
54
- parent: HTMLElement,
55
- ): (HTMLHeadingElement | HTMLDivElement | HTMLHRElement)[] {
56
- return Array.from(
57
- parent.querySelectorAll(
58
- '.body h1, .body h2, .body h3, .body h4, .body h5, .body h6, :is(.body, .page-content) > div.alert, .body section > div.alert, :is(.body, .page-content) > hr',
59
- ),
60
- );
61
- }
62
-
63
43
  /* Calculate how much to offset scroll calculations based on floating header */
64
44
  function getHeaderOffset(doc: Document) {
65
45
  const element = doc.querySelector('header details summary');
@@ -156,17 +136,12 @@ export default (window: Window) => {
156
136
  }
157
137
 
158
138
  const headingsAndAlerts = getHeadingsAndAlerts(window.document.body);
159
- const items = getTocElements(window.document.body)
160
- .map(el => {
161
- const tag = el.tagName.toLowerCase();
162
- if (tag === 'hr') {
163
- return { type: 'dinkus' } as Dinkus;
164
- } else if (tag === 'div') {
165
- return alertToTableItem(el as HTMLElement);
166
- } else {
167
- return headingToTableItem(el as HTMLHeadingElement);
168
- }
169
- })
139
+ const items = headingsAndAlerts
140
+ .map(el =>
141
+ el.tagName === 'DIV'
142
+ ? alertToTableItem(el)
143
+ : headingToTableItem(el as HTMLHeadingElement),
144
+ )
170
145
  .filter(obj => obj != null);
171
146
 
172
147
  const highlightIndexes = getHighlightIndexes(items);
package/src/toc/model.ts CHANGED
@@ -3,18 +3,36 @@ type AlertType = 'warning' | 'note';
3
3
 
4
4
  export type Alert = { type: AlertType; title: string };
5
5
  export type Heading = { level: HeadingLevel; innerHtml: string; id: string };
6
- export type Dinkus = { type: 'dinkus' };
7
6
 
8
- export function getHighlightIndexes(items: (Heading | Alert | Dinkus)[]) {
7
+ const HEADINGS = '.body h1, .body h2, .body h3, .body h4, .body h5, .body h6';
8
+
9
+ function isNestedBlock(el: Element): boolean {
10
+ return el.parentElement?.closest('.alert, .question') != null;
11
+ }
12
+
13
+ // Like the build-time TOC, nothing inside a <details> is listed, and alerts are
14
+ // only listed outside of another alert or question. Headings are listed at any
15
+ // other depth.
16
+ export function getHeadingsAndAlerts(
17
+ parent: ParentNode,
18
+ ): (HTMLHeadingElement | HTMLDivElement)[] {
19
+ return Array.from(
20
+ parent.querySelectorAll<HTMLHeadingElement | HTMLDivElement>(
21
+ `${HEADINGS}, .body div.alert`,
22
+ ),
23
+ ).filter(
24
+ el =>
25
+ el.closest('details') == null &&
26
+ (!el.matches('div.alert') || !isNestedBlock(el)),
27
+ );
28
+ }
29
+
30
+ export function getHighlightIndexes(items: (Heading | Alert)[]) {
9
31
  const indexes: (number | null)[] = [];
10
32
  let currentHeadingIndex: number | null = null;
11
33
  let tocIndex = 0;
12
34
 
13
35
  items.forEach(item => {
14
- if (!('level' in item) && item.type === 'dinkus') {
15
- return;
16
- }
17
-
18
36
  if ('level' in item) {
19
37
  currentHeadingIndex = tocIndex;
20
38
  }
@@ -8,6 +8,11 @@
8
8
  /* TADA_CUSTOM_FONT_TUNING */
9
9
  }
10
10
 
11
+ // Lean $anchor up to $max toward --tint-hue, most when the tint is 90deg away
12
+ @function lean-toward-tint($anchor, $max) {
13
+ @return calc(#{$anchor} + #{$max} * sin(var(--tint-hue) - #{$anchor}));
14
+ }
15
+
11
16
  :root {
12
17
  --theme-color: <%= themeColorLight %>;
13
18
  --theme-color-text: <%= themeColorTextLight %>;
@@ -28,8 +33,11 @@
28
33
 
29
34
  // Secondary foreground color (lighter)
30
35
  --fg2-color: hsl(var(--tint-hue) calc(8% * var(--tint-amount)) 45%);
31
- --link-color: <%= linkColor %>;
32
- --link-color-hover: <%= linkColorHover %>;
36
+
37
+ // Link color; the @supports block below leans the hue toward the tint
38
+ --link-hue: 216deg;
39
+ --link-color: hsl(var(--link-hue) 66% 38%);
40
+ --link-color-hover: hsl(var(--link-hue) 56% 49%);
33
41
 
34
42
  // Main background color (the color of the page background)
35
43
  --bg-color: hsl(var(--tint-hue) calc(65% * var(--tint-amount)) 97%);
@@ -44,7 +52,8 @@
44
52
  --bg2-color-translucent: hsl(
45
53
  var(--tint-hue) calc(55% * var(--tint-amount)) 92% / 66%
46
54
  );
47
- --bg-trace-line-active: <%= bgTraceLineActive %>;
55
+ --trace-line-active-hue: 52deg;
56
+ --bg-trace-line-active: hsl(var(--trace-line-active-hue) 100% 86%);
48
57
  --box-shadow: 0 2px 9px
49
58
  hsl(var(--tint-hue) calc(85% * var(--tint-amount)) 10% / 25%);
50
59
  --backdrop-filter: blur(10px);
@@ -82,6 +91,14 @@
82
91
  color-scheme: light dark;
83
92
  }
84
93
 
94
+ // Browsers without CSS sin() keep the fixed hues above
95
+ @supports (scale: sin(90deg)) {
96
+ :root {
97
+ --link-hue: #{lean-toward-tint(216deg, 9deg)};
98
+ --trace-line-active-hue: #{lean-toward-tint(52deg, 18deg)};
99
+ }
100
+ }
101
+
85
102
  :root[data-font-preference='serif'] {
86
103
  --font: var(--serif-font);
87
104
  --mono-font: var(--serif-mono-font);
@@ -110,8 +127,8 @@
110
127
  var(--tint-hue) calc(85% * var(--tint-amount)) 90% / 33%
111
128
  );
112
129
  --fg2-color: hsl(var(--tint-hue) calc(10% * var(--tint-amount)) 60%);
113
- --link-color: <%= linkColorDark %>;
114
- --link-color-hover: <%= linkColorHoverDark %>;
130
+ --link-color: hsl(var(--link-hue) 50% 72%);
131
+ --link-color-hover: hsl(var(--link-hue) 40% 80%);
115
132
  --bg-color: hsl(var(--tint-hue) calc(35% * var(--tint-amount)) 5%);
116
133
  --bg-color-translucent: hsl(
117
134
  var(--tint-hue) calc(35% * var(--tint-amount)) 10% / 66%
@@ -120,7 +137,7 @@
120
137
  --bg2-color-translucent: hsl(
121
138
  var(--tint-hue) calc(40% * var(--tint-amount)) 12% / 66%
122
139
  );
123
- --bg-trace-line-active: <%= bgTraceLineActiveDark %>;
140
+ --bg-trace-line-active: hsl(var(--trace-line-active-hue) 90% 18%);
124
141
  --box-shadow: 0 3px 8px
125
142
  hsl(var(--tint-hue) calc(33% * var(--tint-amount)) 8% / 70%);
126
143
  --link-active-color: #fff;