@abreen/tada 1.1.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 +69 -17
- package/bin/tada.ts +317 -25
- package/bin/validators.test.ts +4 -3
- package/bin/validators.ts +4 -2
- package/build/apply-base-path-plugin.ts +24 -13
- package/build/build-manifest.test.ts +349 -0
- package/build/build-manifest.ts +165 -0
- package/build/bundle.ts +19 -20
- package/build/code.test.ts +7 -3
- package/build/content-watch.ts +6 -6
- package/build/copy.ts +2 -2
- package/build/deflist-id-plugin.ts +1 -1
- package/build/external-links-plugin.ts +2 -2
- package/build/features.ts +1 -1
- package/build/generate-content-assets.ts +12 -7
- package/build/generate-favicon.ts +13 -6
- package/build/generate-fonts.ts +2 -2
- package/build/generate-katex-assets.ts +35 -0
- package/build/generate-manifest.ts +2 -2
- package/build/globals.test.ts +10 -11
- package/build/globals.ts +14 -7
- package/build/json-schema.test.ts +1 -1
- package/build/log.test.ts +1 -1
- package/build/log.ts +2 -2
- package/build/markdown-plugins.test.ts +116 -10
- package/build/pagefind.test.ts +2 -2
- package/build/pagefind.ts +5 -5
- package/build/pdf-text.test.ts +52 -0
- package/build/pipeline.ts +42 -15
- package/build/reachability.test.ts +1 -1
- package/build/serve.ts +20 -8
- package/build/site-variables.ts +13 -5
- package/build/templates.ts +16 -7
- package/build/toc-plugin.test.ts +2 -2
- package/build/toc-plugin.ts +1 -1
- package/build/types.ts +15 -6
- package/build/util.ts +6 -6
- package/build/utils/code.ts +5 -5
- package/build/utils/content-files.ts +3 -3
- package/build/utils/derive-theme.test.ts +1 -1
- package/build/utils/derive-theme.ts +1 -1
- package/build/utils/file-types.test.ts +1 -1
- package/build/utils/front-matter.test.ts +1 -4
- package/build/utils/front-matter.ts +2 -2
- package/build/utils/jdi-runner/LiterateRunner.java +13 -0
- package/build/utils/literate-java.ts +24 -12
- package/build/utils/markdown.ts +41 -9
- package/build/utils/paths.test.ts +2 -2
- package/build/utils/paths.ts +5 -1
- package/build/utils/render.ts +57 -36
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/validate-internal-links-plugin.test.ts +1 -1
- package/build/validate-internal-links-plugin.ts +1 -1
- package/build/watch-reachability-state.test.ts +2 -2
- package/build/watch-reachability-state.ts +1 -1
- package/build/watch.ts +91 -110
- package/init/content/labs/00/VowelCounter.java.md +129 -0
- package/init/content/labs/00/index.md +29 -0
- package/init/content/labs/index.md +9 -0
- package/{content → init/content}/lectures/01/Pair.java.md +1 -0
- package/{content → init/content}/lectures/index.md +1 -1
- package/{content → init/content}/markdown.md +26 -7
- package/package.json +19 -7
- package/{templates → schema}/nav.schema.json +1 -2
- package/{build → schema}/site.schema.json +3 -5
- package/src/_alerts.scss +4 -0
- package/src/_base.scss +90 -6
- package/src/_layout.scss +39 -2
- package/src/anchor/index.ts +0 -1
- package/src/index.ts +3 -3
- package/src/question/style.scss +8 -3
- package/src/search/index.test.ts +203 -0
- package/src/search/index.ts +3 -79
- package/src/search/pdf-utils.ts +79 -0
- package/src/search/style.scss +2 -20
- package/src/style.scss +20 -101
- package/src/timezone/index.ts +9 -25
- package/src/util.ts +1 -1
- package/templates/_top.html +2 -1
- package/types/dev.d.ts +1 -3
- package/types/site-variables.d.ts +6 -10
- package/types/untyped-modules.d.ts +7 -0
- package/build/utils/jdi-runner/LiterateRunner.class +0 -0
- package/config/authors.json +0 -1
- package/config/nav.json +0 -28
- /package/{content → init/content}/index.md +0 -0
- /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
- /package/{content → init/content}/lectures/01/demo.py +0 -0
- /package/{content → init/content}/lectures/01/index.md +0 -0
- /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
- /package/{content → init/content}/problem_sets/index.html +0 -0
- /package/{public → init/public}/avatars/alex.jpg +0 -0
- /package/{public → init/public}/test.txt +0 -0
- /package/{templates → schema}/authors.schema.json +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { getPdfPageNumber, getPdfBaseUrl, groupPdfResults } from './pdf-utils';
|
|
3
|
+
import type { Result } from './pdf-utils';
|
|
4
|
+
|
|
5
|
+
describe('getPdfPageNumber', () => {
|
|
6
|
+
test('returns page number from meta string', () => {
|
|
7
|
+
expect(getPdfPageNumber('/doc.pdf#page=3', '3')).toBe(3);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('prefers meta over URL hash', () => {
|
|
11
|
+
expect(getPdfPageNumber('/doc.pdf#page=5', '2')).toBe(2);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('falls back to URL hash when meta is undefined', () => {
|
|
15
|
+
expect(getPdfPageNumber('/doc.pdf#page=7', undefined)).toBe(7);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('falls back to URL hash when meta is empty', () => {
|
|
19
|
+
expect(getPdfPageNumber('/doc.pdf#page=4', '')).toBe(4);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('falls back to URL hash when meta is not a number', () => {
|
|
23
|
+
expect(getPdfPageNumber('/doc.pdf#page=4', 'abc')).toBe(4);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('returns null when neither meta nor hash has a page', () => {
|
|
27
|
+
expect(getPdfPageNumber('/doc.pdf', undefined)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('returns null for non-positive meta page number', () => {
|
|
31
|
+
expect(getPdfPageNumber('/doc.pdf', '0')).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('returns null for negative meta page number', () => {
|
|
35
|
+
expect(getPdfPageNumber('/doc.pdf', '-1')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('returns null for hash page=0', () => {
|
|
39
|
+
expect(getPdfPageNumber('/doc.pdf#page=0', undefined)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('parses page from hash with other params', () => {
|
|
43
|
+
expect(getPdfPageNumber('/doc.pdf#zoom=100&page=12', undefined)).toBe(12);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('handles case-insensitive page hash', () => {
|
|
47
|
+
expect(getPdfPageNumber('/doc.pdf#Page=9', undefined)).toBe(9);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('getPdfBaseUrl', () => {
|
|
52
|
+
test('returns base URL for a PDF with hash', () => {
|
|
53
|
+
expect(getPdfBaseUrl('/docs/guide.pdf#page=3')).toBe('/docs/guide.pdf');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('returns URL unchanged when no hash present', () => {
|
|
57
|
+
expect(getPdfBaseUrl('/docs/guide.pdf')).toBe('/docs/guide.pdf');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('returns null for non-PDF URL', () => {
|
|
61
|
+
expect(getPdfBaseUrl('/about/')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('returns null for non-PDF URL with hash', () => {
|
|
65
|
+
expect(getPdfBaseUrl('/about/#section')).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('handles .PDF extension case-insensitively', () => {
|
|
69
|
+
expect(getPdfBaseUrl('/docs/GUIDE.PDF#page=1')).toBe('/docs/GUIDE.PDF');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('groupPdfResults', () => {
|
|
74
|
+
function makeResult(overrides: Partial<Result>): Result {
|
|
75
|
+
return {
|
|
76
|
+
title: 'doc.pdf',
|
|
77
|
+
url: '/doc.pdf',
|
|
78
|
+
excerpt: '',
|
|
79
|
+
score: 1,
|
|
80
|
+
subResults: [],
|
|
81
|
+
pageNumber: null,
|
|
82
|
+
...overrides,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test('passes through non-PDF results unchanged', () => {
|
|
87
|
+
const results: Result[] = [
|
|
88
|
+
makeResult({ title: 'About', url: '/about/', score: 5 }),
|
|
89
|
+
makeResult({ title: 'Home', url: '/', score: 3 }),
|
|
90
|
+
];
|
|
91
|
+
const grouped = groupPdfResults(results);
|
|
92
|
+
expect(grouped).toEqual(results);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('groups multiple PDF page results into one result with sub-results', () => {
|
|
96
|
+
const results: Result[] = [
|
|
97
|
+
makeResult({
|
|
98
|
+
url: '/doc.pdf#page=3',
|
|
99
|
+
score: 10,
|
|
100
|
+
pageNumber: 3,
|
|
101
|
+
excerpt: 'page 3 text',
|
|
102
|
+
}),
|
|
103
|
+
makeResult({
|
|
104
|
+
url: '/doc.pdf#page=1',
|
|
105
|
+
score: 5,
|
|
106
|
+
pageNumber: 1,
|
|
107
|
+
excerpt: 'page 1 text',
|
|
108
|
+
}),
|
|
109
|
+
];
|
|
110
|
+
const grouped = groupPdfResults(results);
|
|
111
|
+
expect(grouped).toHaveLength(1);
|
|
112
|
+
expect(grouped[0].url).toBe('/doc.pdf');
|
|
113
|
+
expect(grouped[0].score).toBe(10);
|
|
114
|
+
expect(grouped[0].subResults).toEqual([
|
|
115
|
+
{ title: 'Page 1', url: '/doc.pdf#page=1', excerpt: 'page 1 text' },
|
|
116
|
+
{ title: 'Page 3', url: '/doc.pdf#page=3', excerpt: 'page 3 text' },
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('sub-results are sorted by page number', () => {
|
|
121
|
+
const results: Result[] = [
|
|
122
|
+
makeResult({ url: '/doc.pdf#page=5', score: 1, pageNumber: 5 }),
|
|
123
|
+
makeResult({ url: '/doc.pdf#page=2', score: 8, pageNumber: 2 }),
|
|
124
|
+
makeResult({ url: '/doc.pdf#page=9', score: 3, pageNumber: 9 }),
|
|
125
|
+
];
|
|
126
|
+
const grouped = groupPdfResults(results);
|
|
127
|
+
expect(grouped[0].subResults.map(s => s.title)).toEqual([
|
|
128
|
+
'Page 2',
|
|
129
|
+
'Page 5',
|
|
130
|
+
'Page 9',
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('primary result uses highest-scoring page', () => {
|
|
135
|
+
const results: Result[] = [
|
|
136
|
+
makeResult({
|
|
137
|
+
url: '/doc.pdf#page=1',
|
|
138
|
+
score: 2,
|
|
139
|
+
pageNumber: 1,
|
|
140
|
+
excerpt: 'low',
|
|
141
|
+
}),
|
|
142
|
+
makeResult({
|
|
143
|
+
url: '/doc.pdf#page=4',
|
|
144
|
+
score: 9,
|
|
145
|
+
pageNumber: 4,
|
|
146
|
+
excerpt: 'high',
|
|
147
|
+
}),
|
|
148
|
+
];
|
|
149
|
+
const grouped = groupPdfResults(results);
|
|
150
|
+
expect(grouped[0].excerpt).toBe('high');
|
|
151
|
+
expect(grouped[0].score).toBe(9);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('mixes non-PDF and grouped PDF results sorted by score', () => {
|
|
155
|
+
const results: Result[] = [
|
|
156
|
+
makeResult({
|
|
157
|
+
title: 'About',
|
|
158
|
+
url: '/about/',
|
|
159
|
+
score: 7,
|
|
160
|
+
pageNumber: null,
|
|
161
|
+
}),
|
|
162
|
+
makeResult({ url: '/doc.pdf#page=1', score: 10, pageNumber: 1 }),
|
|
163
|
+
makeResult({ url: '/doc.pdf#page=2', score: 3, pageNumber: 2 }),
|
|
164
|
+
];
|
|
165
|
+
const grouped = groupPdfResults(results);
|
|
166
|
+
expect(grouped).toHaveLength(2);
|
|
167
|
+
expect(grouped[0].url).toBe('/doc.pdf');
|
|
168
|
+
expect(grouped[0].score).toBe(10);
|
|
169
|
+
expect(grouped[1].url).toBe('/about/');
|
|
170
|
+
expect(grouped[1].score).toBe(7);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('does not group PDF result without a page number', () => {
|
|
174
|
+
const results: Result[] = [
|
|
175
|
+
makeResult({ url: '/doc.pdf', score: 5, pageNumber: null }),
|
|
176
|
+
];
|
|
177
|
+
const grouped = groupPdfResults(results);
|
|
178
|
+
expect(grouped).toHaveLength(1);
|
|
179
|
+
expect(grouped[0].url).toBe('/doc.pdf');
|
|
180
|
+
expect(grouped[0].subResults).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('groups results from different PDFs separately', () => {
|
|
184
|
+
const results: Result[] = [
|
|
185
|
+
makeResult({
|
|
186
|
+
title: 'a.pdf',
|
|
187
|
+
url: '/a.pdf#page=1',
|
|
188
|
+
score: 5,
|
|
189
|
+
pageNumber: 1,
|
|
190
|
+
}),
|
|
191
|
+
makeResult({
|
|
192
|
+
title: 'b.pdf',
|
|
193
|
+
url: '/b.pdf#page=1',
|
|
194
|
+
score: 8,
|
|
195
|
+
pageNumber: 1,
|
|
196
|
+
}),
|
|
197
|
+
];
|
|
198
|
+
const grouped = groupPdfResults(results);
|
|
199
|
+
expect(grouped).toHaveLength(2);
|
|
200
|
+
const urls = grouped.map(r => r.url).sort();
|
|
201
|
+
expect(urls).toEqual(['/a.pdf', '/b.pdf']);
|
|
202
|
+
});
|
|
203
|
+
});
|
package/src/search/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getElement, applyBasePath } from '../util';
|
|
2
|
+
import { getPdfPageNumber, groupPdfResults } from './pdf-utils';
|
|
3
|
+
import type { Result } from './pdf-utils';
|
|
2
4
|
|
|
3
5
|
const MAX_RESULTS = 24;
|
|
4
6
|
|
|
@@ -28,14 +30,6 @@ interface Pagefind {
|
|
|
28
30
|
let pagefind: Pagefind | null = null;
|
|
29
31
|
|
|
30
32
|
type SubResult = { title: string; url: string; excerpt: string };
|
|
31
|
-
type Result = {
|
|
32
|
-
title: string;
|
|
33
|
-
url: string;
|
|
34
|
-
excerpt: string;
|
|
35
|
-
score: number;
|
|
36
|
-
subResults: SubResult[];
|
|
37
|
-
pageNumber: number | null;
|
|
38
|
-
};
|
|
39
33
|
|
|
40
34
|
type State = {
|
|
41
35
|
value: string;
|
|
@@ -44,76 +38,6 @@ type State = {
|
|
|
44
38
|
totalResults: number;
|
|
45
39
|
};
|
|
46
40
|
|
|
47
|
-
function getPdfPageNumber(
|
|
48
|
-
url: string,
|
|
49
|
-
pageMeta: string | undefined,
|
|
50
|
-
): number | null {
|
|
51
|
-
const fromMeta = Number.parseInt(pageMeta ?? '', 10);
|
|
52
|
-
if (Number.isInteger(fromMeta) && fromMeta > 0) {
|
|
53
|
-
return fromMeta;
|
|
54
|
-
}
|
|
55
|
-
const match = url.match(/#(?:.*&)?page=(\d+)\b/i);
|
|
56
|
-
if (!match) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
const fromUrl = Number.parseInt(match[1], 10);
|
|
60
|
-
return Number.isInteger(fromUrl) && fromUrl >= 1 ? fromUrl : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getPdfBaseUrl(url: string): string | null {
|
|
64
|
-
const hashIndex = url.indexOf('#');
|
|
65
|
-
const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
66
|
-
return baseUrl.toLowerCase().endsWith('.pdf') ? baseUrl : null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function groupPdfResults(results: Result[]): Result[] {
|
|
70
|
-
const pdfGroups = new Map<string, Result[]>();
|
|
71
|
-
const other: Result[] = [];
|
|
72
|
-
|
|
73
|
-
for (const result of results) {
|
|
74
|
-
const baseUrl = getPdfBaseUrl(result.url);
|
|
75
|
-
if (!baseUrl || result.pageNumber == null) {
|
|
76
|
-
other.push(result);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (!pdfGroups.has(baseUrl)) {
|
|
80
|
-
pdfGroups.set(baseUrl, []);
|
|
81
|
-
}
|
|
82
|
-
pdfGroups.get(baseUrl)!.push(result);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const grouped: Result[] = [...other];
|
|
86
|
-
|
|
87
|
-
pdfGroups.forEach((pages, baseUrl) => {
|
|
88
|
-
const primary = pages
|
|
89
|
-
.slice()
|
|
90
|
-
.sort(
|
|
91
|
-
(a, b) =>
|
|
92
|
-
b.score - a.score || (a.pageNumber ?? 0) - (b.pageNumber ?? 0),
|
|
93
|
-
)[0];
|
|
94
|
-
const subResults = pages
|
|
95
|
-
.slice()
|
|
96
|
-
.sort((a, b) => (a.pageNumber ?? 0) - (b.pageNumber ?? 0))
|
|
97
|
-
.map(page => ({
|
|
98
|
-
title: `Page ${page.pageNumber ?? '?'}`,
|
|
99
|
-
url: page.url,
|
|
100
|
-
excerpt: page.excerpt,
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
grouped.push({
|
|
104
|
-
title: primary.title,
|
|
105
|
-
url: baseUrl,
|
|
106
|
-
excerpt: primary.excerpt,
|
|
107
|
-
score: primary.score,
|
|
108
|
-
subResults,
|
|
109
|
-
pageNumber: null,
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
grouped.sort((a, b) => b.score - a.score);
|
|
114
|
-
return grouped;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
41
|
async function doSearch(state: State) {
|
|
118
42
|
if (pagefind == null) {
|
|
119
43
|
return;
|
|
@@ -129,7 +53,7 @@ async function doSearch(state: State) {
|
|
|
129
53
|
const slice = search.results.slice(0, MAX_RESULTS);
|
|
130
54
|
const data = await Promise.all(slice.map(r => r.data()));
|
|
131
55
|
|
|
132
|
-
const titlePostfix =
|
|
56
|
+
const titlePostfix = __SITE_TITLE_POSTFIX__;
|
|
133
57
|
const results: Result[] = data.map((d: PagefindResult) => {
|
|
134
58
|
let title: string = d.meta?.title ?? d.url;
|
|
135
59
|
if (titlePostfix && title.endsWith(titlePostfix)) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type SubResult = { title: string; url: string; excerpt: string };
|
|
2
|
+
export type Result = {
|
|
3
|
+
title: string;
|
|
4
|
+
url: string;
|
|
5
|
+
excerpt: string;
|
|
6
|
+
score: number;
|
|
7
|
+
subResults: SubResult[];
|
|
8
|
+
pageNumber: number | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function getPdfPageNumber(
|
|
12
|
+
url: string,
|
|
13
|
+
pageMeta: string | undefined,
|
|
14
|
+
): number | null {
|
|
15
|
+
const fromMeta = Number.parseInt(pageMeta ?? '', 10);
|
|
16
|
+
if (Number.isInteger(fromMeta) && fromMeta > 0) {
|
|
17
|
+
return fromMeta;
|
|
18
|
+
}
|
|
19
|
+
const match = url.match(/#(?:.*&)?page=(\d+)\b/i);
|
|
20
|
+
if (!match) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const fromUrl = Number.parseInt(match[1], 10);
|
|
24
|
+
return Number.isInteger(fromUrl) && fromUrl >= 1 ? fromUrl : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getPdfBaseUrl(url: string): string | null {
|
|
28
|
+
const hashIndex = url.indexOf('#');
|
|
29
|
+
const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
30
|
+
return baseUrl.toLowerCase().endsWith('.pdf') ? baseUrl : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function groupPdfResults(results: Result[]): Result[] {
|
|
34
|
+
const pdfGroups = new Map<string, Result[]>();
|
|
35
|
+
const other: Result[] = [];
|
|
36
|
+
|
|
37
|
+
for (const result of results) {
|
|
38
|
+
const baseUrl = getPdfBaseUrl(result.url);
|
|
39
|
+
if (!baseUrl || result.pageNumber == null) {
|
|
40
|
+
other.push(result);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!pdfGroups.has(baseUrl)) {
|
|
44
|
+
pdfGroups.set(baseUrl, []);
|
|
45
|
+
}
|
|
46
|
+
pdfGroups.get(baseUrl)!.push(result);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const grouped: Result[] = [...other];
|
|
50
|
+
|
|
51
|
+
pdfGroups.forEach((pages, baseUrl) => {
|
|
52
|
+
const primary = pages
|
|
53
|
+
.slice()
|
|
54
|
+
.sort(
|
|
55
|
+
(a, b) =>
|
|
56
|
+
b.score - a.score || (a.pageNumber ?? 0) - (b.pageNumber ?? 0),
|
|
57
|
+
)[0];
|
|
58
|
+
const subResults = pages
|
|
59
|
+
.slice()
|
|
60
|
+
.sort((a, b) => (a.pageNumber ?? 0) - (b.pageNumber ?? 0))
|
|
61
|
+
.map(page => ({
|
|
62
|
+
title: `Page ${page.pageNumber ?? '?'}`,
|
|
63
|
+
url: page.url,
|
|
64
|
+
excerpt: page.excerpt,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
grouped.push({
|
|
68
|
+
title: primary.title,
|
|
69
|
+
url: baseUrl,
|
|
70
|
+
excerpt: primary.excerpt,
|
|
71
|
+
score: primary.score,
|
|
72
|
+
subResults,
|
|
73
|
+
pageNumber: null,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
grouped.sort((a, b) => b.score - a.score);
|
|
78
|
+
return grouped;
|
|
79
|
+
}
|
package/src/search/style.scss
CHANGED
|
@@ -8,21 +8,7 @@ header {
|
|
|
8
8
|
gap: 0.5rem;
|
|
9
9
|
|
|
10
10
|
input.search {
|
|
11
|
-
background-color: var(--bg-color);
|
|
12
|
-
background-image: var(--icon-search);
|
|
13
|
-
background-repeat: no-repeat;
|
|
14
|
-
background-position: left 6px center;
|
|
15
|
-
background-size: 16px 16px;
|
|
16
|
-
padding-left: 2rem;
|
|
17
|
-
|
|
18
|
-
border-radius: 18px;
|
|
19
11
|
flex: 1;
|
|
20
|
-
min-width: 0;
|
|
21
|
-
|
|
22
|
-
&:disabled {
|
|
23
|
-
opacity: 0.4;
|
|
24
|
-
cursor: not-allowed;
|
|
25
|
-
}
|
|
26
12
|
}
|
|
27
13
|
}
|
|
28
14
|
|
|
@@ -75,12 +61,8 @@ header {
|
|
|
75
61
|
color: var(--fg2-color);
|
|
76
62
|
|
|
77
63
|
kbd {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
border: 1px solid var(--fg2-color);
|
|
81
|
-
border-radius: 4px;
|
|
82
|
-
font-family: var(--font);
|
|
83
|
-
font-size: var(--font-size-small);
|
|
64
|
+
border-color: var(--fg2-color);
|
|
65
|
+
box-shadow: 0 2px 0 var(--fg2-color);
|
|
84
66
|
line-height: 1.4;
|
|
85
67
|
}
|
|
86
68
|
}
|
package/src/style.scss
CHANGED
|
@@ -47,60 +47,18 @@
|
|
|
47
47
|
outline-offset: 1px;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
.info {
|
|
51
|
-
@include user-select(none);
|
|
52
|
-
display: grid;
|
|
53
|
-
grid-template-columns: 1fr auto;
|
|
54
|
-
align-items: baseline;
|
|
55
|
-
color: var(--fg2-color);
|
|
56
|
-
font-size: var(--font-size-smaller);
|
|
57
|
-
|
|
58
|
-
@media (width < 450px) {
|
|
59
|
-
grid-template-columns: 1fr;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
aside.author {
|
|
63
|
-
a {
|
|
64
|
-
text-decoration: none;
|
|
65
|
-
|
|
66
|
-
.text {
|
|
67
|
-
color: var(--fg2-color);
|
|
68
|
-
text-decoration: underline;
|
|
69
|
-
text-decoration-color: var(--fg2-color);
|
|
70
|
-
text-decoration-thickness: 1px;
|
|
71
|
-
text-underline-offset: 3px;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.avatar {
|
|
76
|
-
width: 24px;
|
|
77
|
-
height: 24px;
|
|
78
|
-
vertical-align: middle;
|
|
79
|
-
margin-left: 4px;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
50
|
a.disabled {
|
|
85
51
|
color: var(--fg-color-translucent);
|
|
86
52
|
text-decoration-color: var(--fg-color-translucent);
|
|
87
53
|
cursor: not-allowed;
|
|
88
|
-
}
|
|
89
54
|
|
|
90
|
-
@media (hover: hover) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
a.disabled:hover {
|
|
96
|
-
color: var(--fg-color-translucent);
|
|
55
|
+
@media (hover: hover) {
|
|
56
|
+
&:hover {
|
|
57
|
+
color: var(--fg-color-translucent);
|
|
58
|
+
}
|
|
97
59
|
}
|
|
98
60
|
}
|
|
99
61
|
|
|
100
|
-
a:active {
|
|
101
|
-
color: var(--link-active-color);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
62
|
a.external::after {
|
|
105
63
|
content: '';
|
|
106
64
|
background: var(--icon-external-link);
|
|
@@ -175,34 +133,6 @@ a.footnote-ref,
|
|
|
175
133
|
@include user-select(none);
|
|
176
134
|
}
|
|
177
135
|
|
|
178
|
-
details .content > p:last-child,
|
|
179
|
-
details .content > div:last-child {
|
|
180
|
-
margin-bottom: 0;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
details summary {
|
|
184
|
-
cursor: pointer;
|
|
185
|
-
@include user-select(none);
|
|
186
|
-
padding: var(--details-padding);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
details .content {
|
|
190
|
-
padding: 0 var(--details-padding) var(--details-padding)
|
|
191
|
-
var(--details-padding);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
@media (hover: hover) {
|
|
195
|
-
details summary {
|
|
196
|
-
&:hover {
|
|
197
|
-
color: var(--fg2-color);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
&:active {
|
|
201
|
-
color: var(--link-active-color);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
136
|
hr {
|
|
207
137
|
border: none;
|
|
208
138
|
height: 32px;
|
|
@@ -223,26 +153,6 @@ hr::after {
|
|
|
223
153
|
mask-repeat: no-repeat;
|
|
224
154
|
}
|
|
225
155
|
|
|
226
|
-
main p,
|
|
227
|
-
main details {
|
|
228
|
-
margin-bottom: 1rem;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
main details {
|
|
232
|
-
border-radius: var(--border-radius);
|
|
233
|
-
border: 2px solid var(--fg-color);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
input[type='text'],
|
|
237
|
-
input[type='search'] {
|
|
238
|
-
background: var(--bg-color);
|
|
239
|
-
color: var(--fg-color);
|
|
240
|
-
border: var(--control-border-width) solid var(--fg2-color);
|
|
241
|
-
border-radius: var(--border-radius);
|
|
242
|
-
font-size: 12pt;
|
|
243
|
-
padding: 4px 4px;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
156
|
button,
|
|
247
157
|
a.button,
|
|
248
158
|
input[type='submit'] {
|
|
@@ -279,7 +189,7 @@ input[type='submit']:focus-visible,
|
|
|
279
189
|
input[type='text']:focus-visible,
|
|
280
190
|
input[type='search']:focus-visible {
|
|
281
191
|
border-color: var(--fg-color);
|
|
282
|
-
background: var(--bg2-color);
|
|
192
|
+
background-color: var(--bg2-color);
|
|
283
193
|
}
|
|
284
194
|
|
|
285
195
|
button.icon-button {
|
|
@@ -296,10 +206,6 @@ button.icon-button {
|
|
|
296
206
|
vertical-align: top;
|
|
297
207
|
}
|
|
298
208
|
|
|
299
|
-
li {
|
|
300
|
-
margin-bottom: 0.5rem;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
209
|
ol.styled-list,
|
|
304
210
|
ul.styled-list {
|
|
305
211
|
color: var(--theme-color-text);
|
|
@@ -470,6 +376,10 @@ dl {
|
|
|
470
376
|
text-decoration-color: var(--fg2-color);
|
|
471
377
|
}
|
|
472
378
|
}
|
|
379
|
+
|
|
380
|
+
&:active {
|
|
381
|
+
text-decoration-color: var(--link-active-color);
|
|
382
|
+
}
|
|
473
383
|
}
|
|
474
384
|
}
|
|
475
385
|
|
|
@@ -520,14 +430,23 @@ figure.bio {
|
|
|
520
430
|
}
|
|
521
431
|
|
|
522
432
|
figcaption {
|
|
523
|
-
font-size: var(--font-size-small);
|
|
524
|
-
color: var(--fg2-color);
|
|
525
433
|
text-align: center;
|
|
526
434
|
margin-top: var(--gap);
|
|
527
435
|
font-style: italic;
|
|
528
436
|
}
|
|
529
437
|
}
|
|
530
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
|
+
|
|
531
450
|
select {
|
|
532
451
|
font-family: var(--font);
|
|
533
452
|
color: var(--fg-color);
|
package/src/timezone/index.ts
CHANGED
|
@@ -84,14 +84,13 @@ function getOffsetMinutes(tz: string, date: Date): number {
|
|
|
84
84
|
return (utcTs - date.getTime()) / 60000;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
typeof window !== 'undefined' ? window.siteVariables.timezones : [];
|
|
87
|
+
// typeof guard prevents ReferenceError when tests import this module
|
|
88
|
+
// without going through the bundler (which substitutes __SITE_TIMEZONES__).
|
|
89
|
+
const TIMEZONES: TimeZone[] =
|
|
90
|
+
typeof __SITE_TIMEZONES__ !== 'undefined' ? __SITE_TIMEZONES__ : [];
|
|
92
91
|
|
|
93
92
|
function getDefaultTimezone() {
|
|
94
|
-
const value =
|
|
93
|
+
const value = __SITE_DEFAULT_TIMEZONE__;
|
|
95
94
|
return TIMEZONES.find(tz => tz.value === value)!;
|
|
96
95
|
}
|
|
97
96
|
|
|
@@ -101,25 +100,10 @@ function computeOffsets(baseDate: Date) {
|
|
|
101
100
|
});
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
function init(element:
|
|
105
|
-
|
|
106
|
-
element.
|
|
107
|
-
|
|
108
|
-
TIMEZONES.forEach(tz => {
|
|
109
|
-
const opt = document.createElement('option');
|
|
110
|
-
opt.value = tz.value;
|
|
111
|
-
if (tz.value === 'UTC') {
|
|
112
|
-
opt.textContent = tz.label;
|
|
113
|
-
} else {
|
|
114
|
-
opt.textContent = `${tz.label} (${tz.abbreviation})`;
|
|
115
|
-
}
|
|
116
|
-
if (tz.value === selectedTz) {
|
|
117
|
-
opt.selected = true;
|
|
118
|
-
}
|
|
119
|
-
element.appendChild(opt);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
element.removeAttribute('hidden');
|
|
103
|
+
function init(element: HTMLSelectElement, selectedTz: string) {
|
|
104
|
+
element.value = selectedTz;
|
|
105
|
+
element.hidden = false;
|
|
106
|
+
element.disabled = false;
|
|
123
107
|
}
|
|
124
108
|
|
|
125
109
|
function dominantPeriodStyle(
|
package/src/util.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function applyBasePath(subPath: string): string {
|
|
|
34
34
|
throw new Error('invalid internal path, must start with "/": ' + subPath);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
let path =
|
|
37
|
+
let path = __SITE_BASE_PATH__;
|
|
38
38
|
if (path.endsWith('/')) {
|
|
39
39
|
path = path.slice(0, -1);
|
|
40
40
|
}
|
package/templates/_top.html
CHANGED
|
@@ -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; }
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
<header data-pagefind-ignore>
|
|
55
56
|
<details>
|
|
56
57
|
<summary aria-label="Toggle site navigation">
|
|
57
|
-
|
|
58
|
+
<% if (site.symbol) { %><span class="logo" aria-hidden="true"><%= site.symbol %></span><% } %>
|
|
58
59
|
<span class="site-title" aria-hidden="true"><%= site.title %></span>
|
|
59
60
|
<div id="to-top-container" class="to-top-container"></div>
|
|
60
61
|
<% if (site.features.search) { %>
|
package/types/dev.d.ts
CHANGED