@abreen/tada 1.19.4 → 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 +0 -2
- package/build/bundle.ts +1 -27
- package/build/source-records.ts +2 -2
- package/build/superlink-plugin.ts +50 -0
- package/build/toc-plugin.ts +31 -16
- package/build/types.d.ts +1 -3
- package/build/utils/derive-theme.ts +14 -61
- package/build/utils/literate-java.ts +1 -24
- package/build/utils/markdown.ts +1 -35
- package/init/content/lectures/01/Pair.java.md +5 -4
- package/init/content/lectures/index.md +5 -2
- package/init/content/markdown.md +22 -62
- package/package.json +5 -2
- package/src/_alerts.scss +5 -4
- package/src/_base.scss +22 -13
- package/src/code/_index.scss +5 -3
- package/src/header/_index.scss +9 -0
- package/src/header/index.ts +1 -1
- package/src/navigate/runtime.ts +7 -109
- package/src/print/_index.scss +5 -7
- package/src/toc/index.ts +7 -32
- package/src/toc/model.ts +24 -6
- package/templates/_theme.scss +23 -6
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(
|
package/build/source-records.ts
CHANGED
|
@@ -203,7 +203,7 @@ export function createPublicRecord(
|
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
|
|
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
|
-
|
|
226
|
+
function collectSourceHtmlAnalysis(
|
|
227
227
|
assets: Asset[],
|
|
228
228
|
): Map<string, HtmlOutputAnalysis> {
|
|
229
229
|
const htmlAnalysisByOutputPath = new Map<string, HtmlOutputAnalysis>();
|
|
@@ -8,6 +8,8 @@ const CARET = 0x5e;
|
|
|
8
8
|
const OPEN_PAREN = 0x28;
|
|
9
9
|
const CLOSE_PAREN = 0x29;
|
|
10
10
|
const NEWLINE = 0x0a;
|
|
11
|
+
const LESS_THAN = 0x3c;
|
|
12
|
+
const RAW_ANCHOR_OPEN = /^<a[\s>]/i;
|
|
11
13
|
|
|
12
14
|
// Renders `[Title][Description](/destination.html)` as a two-line superlink.
|
|
13
15
|
// It must run before the built-in `link` rule, which would otherwise
|
|
@@ -27,6 +29,46 @@ export default function superlinkPlugin(
|
|
|
27
29
|
return pos;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
// Whether a `<…>` construct (an autolink or inline HTML) renders an anchor.
|
|
33
|
+
// It is parsed alone with a throwaway `env`, and nothing in it can reach the
|
|
34
|
+
// superlink rule again.
|
|
35
|
+
function isAnchorMarkup(state: StateInline, markup: string): boolean {
|
|
36
|
+
const tokens: StateInline['tokens'] = [];
|
|
37
|
+
state.md.inline.parse(markup, state.md, {}, tokens);
|
|
38
|
+
return tokens.some(
|
|
39
|
+
token =>
|
|
40
|
+
token.type === 'link_open' ||
|
|
41
|
+
(token.type === 'html_inline' && RAW_ANCHOR_OPEN.test(token.content)),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// `parseLinkLabel` rejects bracketed links but not autolinks (`<https://…>`)
|
|
46
|
+
// or raw `<a>` tags. An anchor inside the superlink anchor is invalid HTML:
|
|
47
|
+
// the parser closes the outer one early, stranding the description.
|
|
48
|
+
//
|
|
49
|
+
// This walks the label the way `parseLinkLabel` does, with the silent
|
|
50
|
+
// `skipToken`, so the state's position cache keeps the work linear and rules
|
|
51
|
+
// that register things in `env`, such as inline footnotes, do not run.
|
|
52
|
+
function labelHasAnchor(
|
|
53
|
+
state: StateInline,
|
|
54
|
+
start: number,
|
|
55
|
+
end: number,
|
|
56
|
+
): boolean {
|
|
57
|
+
const oldPos = state.pos;
|
|
58
|
+
let found = false;
|
|
59
|
+
state.pos = start;
|
|
60
|
+
while (state.pos < end && !found) {
|
|
61
|
+
const pos = state.pos;
|
|
62
|
+
state.md.inline.skipToken(state);
|
|
63
|
+
found =
|
|
64
|
+
state.src.charCodeAt(pos) === LESS_THAN &&
|
|
65
|
+
state.pos - pos > 1 &&
|
|
66
|
+
isAnchorMarkup(state, state.src.slice(pos, state.pos));
|
|
67
|
+
}
|
|
68
|
+
state.pos = oldPos;
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
|
|
30
72
|
function pushPart(
|
|
31
73
|
state: StateInline,
|
|
32
74
|
name: 'title' | 'description',
|
|
@@ -115,6 +157,14 @@ export default function superlinkPlugin(
|
|
|
115
157
|
return false;
|
|
116
158
|
}
|
|
117
159
|
|
|
160
|
+
// Last, so labels are only scanned when everything else already matches
|
|
161
|
+
if (
|
|
162
|
+
labelHasAnchor(state, titleStart, titleEnd) ||
|
|
163
|
+
labelHasAnchor(state, descriptionStart, descriptionEnd)
|
|
164
|
+
) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
118
168
|
if (!silent) {
|
|
119
169
|
const external = isExternalHref(href, siteVariables);
|
|
120
170
|
const open = state.push('superlink_open', 'a', 1);
|
package/build/toc-plugin.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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 (
|
|
80
|
+
// Thematic breaks / dinkuses (not inside an alert, question, or <details>)
|
|
60
81
|
if (
|
|
61
82
|
token.type === 'hr' &&
|
|
62
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
2
|
-
import type { Oklch
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
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
|
|
68
|
-
const
|
|
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:
|
|
77
|
-
themeColorTextDark:
|
|
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
|
-
):
|
|
73
|
-
LiterateJavaParseResult,
|
|
74
|
-
'javaSource' | 'codeBlocks' | 'visibleBlockIndices'
|
|
75
|
-
> {
|
|
52
|
+
): LiterateJavaParseResult {
|
|
76
53
|
const codeBlocks: LiterateCodeBlock[] = [];
|
|
77
54
|
let javaLine = 1;
|
|
78
55
|
|
package/build/utils/markdown.ts
CHANGED
|
@@ -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,
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/init/content/markdown.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A static site generator",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"fontkit": "^2.0.4",
|
|
69
69
|
"front-matter": "^4.0.2",
|
|
70
70
|
"java-parser": "^3.0.1",
|
|
71
|
-
"katex": "^0.
|
|
71
|
+
"katex": "^0.18.2",
|
|
72
72
|
"lodash": "^4.18.1",
|
|
73
73
|
"markdown-it": "^14.1.1",
|
|
74
74
|
"markdown-it-anchor": "^9.2.0",
|
|
@@ -82,6 +82,9 @@
|
|
|
82
82
|
"sharp": "^0.35.3",
|
|
83
83
|
"shiki": "^4.0.2"
|
|
84
84
|
},
|
|
85
|
+
"overrides": {
|
|
86
|
+
"katex": "^0.18.2"
|
|
87
|
+
},
|
|
85
88
|
"lint-staged": {
|
|
86
89
|
"*.{ts,js}": "prettier --write",
|
|
87
90
|
"src/**/*.scss": [
|
package/src/_alerts.scss
CHANGED
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
color: var(--fg-color);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
background:
|
|
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;
|
package/src/code/_index.scss
CHANGED
|
@@ -168,9 +168,11 @@ body.code .code-prose {
|
|
|
168
168
|
color: inherit;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
code
|
|
172
|
-
|
|
173
|
-
|
|
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
|
}
|
package/src/header/_index.scss
CHANGED
|
@@ -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 {
|
package/src/header/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getElement } from '../util';
|
|
2
2
|
import { globals } from '../globals';
|
|
3
3
|
|
|
4
|
-
|
|
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) */
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -11,25 +11,14 @@ import { swapHeaderTitle } from '../header';
|
|
|
11
11
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
12
12
|
|
|
13
13
|
const LOADING_CURSOR_DELAY = 400;
|
|
14
|
-
const HOLD_SCROLL_FRAMES = 6;
|
|
15
|
-
const HOLD_SCROLL_RELEASE_EVENTS = [
|
|
16
|
-
'wheel',
|
|
17
|
-
'touchstart',
|
|
18
|
-
'keydown',
|
|
19
|
-
'pointerdown',
|
|
20
|
-
] as const;
|
|
21
14
|
|
|
22
15
|
let currentAbortController: AbortController | null = null;
|
|
23
16
|
let historyIndex = 0;
|
|
24
17
|
let currentPath = '';
|
|
25
18
|
let applyingFragment = false;
|
|
26
|
-
let fragmentTargetRequest = 0;
|
|
27
19
|
|
|
28
20
|
const scrollByIndex = new Map<number, number>();
|
|
29
21
|
const scrollByLocation = new Map<string, number>();
|
|
30
|
-
// Stylesheets the navigator added that have not loaded or failed yet. Those in
|
|
31
|
-
// the initial document have settled before the deferred client script runs.
|
|
32
|
-
const pendingStylesheets = new Map<Element, Promise<void>>();
|
|
33
22
|
|
|
34
23
|
type Direction = 'forward' | 'back';
|
|
35
24
|
|
|
@@ -78,19 +67,7 @@ function updateHead(document: Document, newDoc: Document): void {
|
|
|
78
67
|
|
|
79
68
|
for (const link of newDoc.querySelectorAll('link[rel="stylesheet"]')) {
|
|
80
69
|
if (!existingHrefs.has(link.getAttribute('href'))) {
|
|
81
|
-
|
|
82
|
-
pendingStylesheets.set(
|
|
83
|
-
clone,
|
|
84
|
-
new Promise(resolve => {
|
|
85
|
-
const settle = () => {
|
|
86
|
-
pendingStylesheets.delete(clone);
|
|
87
|
-
resolve();
|
|
88
|
-
};
|
|
89
|
-
clone.addEventListener('load', settle, { once: true });
|
|
90
|
-
clone.addEventListener('error', settle, { once: true });
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
document.head.appendChild(clone);
|
|
70
|
+
document.head.appendChild(link.cloneNode(true));
|
|
94
71
|
}
|
|
95
72
|
}
|
|
96
73
|
}
|
|
@@ -126,11 +103,6 @@ export function closeHeaderDetails(document: Document): void {
|
|
|
126
103
|
}
|
|
127
104
|
}
|
|
128
105
|
|
|
129
|
-
export interface NavigationEventDetail {
|
|
130
|
-
path: string;
|
|
131
|
-
validators: ResponseValidators;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
106
|
function dispatchNavigationEvent(
|
|
135
107
|
window: Window,
|
|
136
108
|
validators: ResponseValidators,
|
|
@@ -266,53 +238,16 @@ export function isApplyingFragment(): boolean {
|
|
|
266
238
|
// content loses it in every browser. A fragment navigation sets it in every
|
|
267
239
|
// browser, but it can reset the entry's history state and scroll position, so
|
|
268
240
|
// restore both.
|
|
269
|
-
|
|
270
|
-
// WebKit defers fragment scrolling until pending stylesheets load, which would
|
|
271
|
-
// later move the page away from the restored scroll position. Wait for them
|
|
272
|
-
// first, and skip the update if another request or navigation replaced it.
|
|
273
|
-
//
|
|
274
|
-
// That deferred scroll can also set :target before the wait ends, which would
|
|
275
|
-
// leave nothing to replace. WebKit then scrolls to the target again on the next
|
|
276
|
-
// frame, undoing a scroll back to the saved position. A fragment navigation of
|
|
277
|
-
// our own does not have that problem, so after waiting, repeat it even when
|
|
278
|
-
// :target matches, restoring the saved position the caller passed in. When the
|
|
279
|
-
// last stylesheet finishes loading just after the content swap, WebKit can
|
|
280
|
-
// still scroll to the target in the next rendering update, after our restore,
|
|
281
|
-
// so hold that position for a few frames.
|
|
282
|
-
export function applyFragmentTarget(
|
|
283
|
-
window: Window,
|
|
284
|
-
restoredTop?: number,
|
|
285
|
-
): void {
|
|
286
|
-
const request = ++fragmentTargetRequest;
|
|
287
|
-
const { href } = window.location;
|
|
288
|
-
let waited = false;
|
|
289
|
-
const apply = (): void => {
|
|
290
|
-
if (request !== fragmentTargetRequest || window.location.href !== href) {
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
const pending = Array.from(pendingStylesheets.values());
|
|
294
|
-
if (pending.length > 0) {
|
|
295
|
-
waited = true;
|
|
296
|
-
void Promise.all(pending).then(apply);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
replaceFragmentTarget(window, waited ? restoredTop : undefined);
|
|
300
|
-
};
|
|
301
|
-
apply();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function replaceFragmentTarget(window: Window, forcedTop?: number): void {
|
|
241
|
+
export function applyFragmentTarget(window: Window): void {
|
|
305
242
|
const { document, history, location } = window;
|
|
306
243
|
if (
|
|
307
|
-
forcedTop === undefined &&
|
|
308
244
|
document.querySelector(':target') === getHashTarget(document, location.hash)
|
|
309
245
|
) {
|
|
310
246
|
return;
|
|
311
247
|
}
|
|
312
248
|
const { href } = location;
|
|
313
249
|
const state = history.state;
|
|
314
|
-
const { scrollX } = window;
|
|
315
|
-
const scrollY = forcedTop ?? window.scrollY;
|
|
250
|
+
const { scrollX, scrollY } = window;
|
|
316
251
|
applyingFragment = true;
|
|
317
252
|
try {
|
|
318
253
|
globals.replaceLocation(
|
|
@@ -324,41 +259,6 @@ function replaceFragmentTarget(window: Window, forcedTop?: number): void {
|
|
|
324
259
|
}
|
|
325
260
|
history.replaceState(state, '', href);
|
|
326
261
|
window.scrollTo({ left: scrollX, top: scrollY });
|
|
327
|
-
if (forcedTop !== undefined) {
|
|
328
|
-
holdScroll(window, scrollX, scrollY);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Scroll back to the given position on each of the next few frames if the
|
|
333
|
-
// browser moved the page, until the visitor scrolls or the URL changes
|
|
334
|
-
function holdScroll(window: Window, left: number, top: number): void {
|
|
335
|
-
const { href } = window.location;
|
|
336
|
-
let held = true;
|
|
337
|
-
const release = (): void => {
|
|
338
|
-
held = false;
|
|
339
|
-
for (const type of HOLD_SCROLL_RELEASE_EVENTS) {
|
|
340
|
-
window.removeEventListener(type, release);
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
for (const type of HOLD_SCROLL_RELEASE_EVENTS) {
|
|
344
|
-
window.addEventListener(type, release, { passive: true });
|
|
345
|
-
}
|
|
346
|
-
let frames = 0;
|
|
347
|
-
const tick = (): void => {
|
|
348
|
-
if (!held || window.location.href !== href) {
|
|
349
|
-
release();
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (Math.abs(window.scrollY - top) > 1) {
|
|
353
|
-
window.scrollTo({ left, top });
|
|
354
|
-
}
|
|
355
|
-
if (++frames < HOLD_SCROLL_FRAMES) {
|
|
356
|
-
window.requestAnimationFrame(tick);
|
|
357
|
-
} else {
|
|
358
|
-
release();
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
window.requestAnimationFrame(tick);
|
|
362
262
|
}
|
|
363
263
|
|
|
364
264
|
export async function navigateToUrl(
|
|
@@ -472,12 +372,10 @@ export async function navigateToUrl(
|
|
|
472
372
|
teardownPerPageComponents();
|
|
473
373
|
swapContent(document, newDoc);
|
|
474
374
|
if (!pushHistory) {
|
|
475
|
-
// Restore the entry's :target before updateHead adds stylesheets
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
typeof scrollTarget === 'number' ? scrollTarget : undefined,
|
|
480
|
-
);
|
|
375
|
+
// Restore the entry's :target before updateHead adds stylesheets. WebKit
|
|
376
|
+
// defers fragment scrolling until they load, which would move the page
|
|
377
|
+
// away from the entry's restored scroll position.
|
|
378
|
+
applyFragmentTarget(window);
|
|
481
379
|
}
|
|
482
380
|
updateHead(document, newDoc);
|
|
483
381
|
mountAppearancePickerForPage(window);
|
package/src/print/_index.scss
CHANGED
|
@@ -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
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
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 =
|
|
160
|
-
.map(el =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
}
|
package/templates/_theme.scss
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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
|
-
--
|
|
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:
|
|
114
|
-
--link-color-hover:
|
|
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:
|
|
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;
|