@abreen/tada 1.1.0 → 1.2.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 +68 -16
- package/bin/tada.ts +289 -24
- 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 +9 -11
- package/build/globals.ts +6 -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 +118 -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 +3 -3
- package/build/site-variables.ts +13 -5
- package/build/templates.ts +7 -6
- 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/literate-java.ts +11 -10
- 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 +45 -32
- 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 +74 -98
- package/{content → init/content}/lectures/index.md +1 -1
- package/{content → init/content}/markdown.md +26 -7
- package/package.json +9 -7
- package/{templates → schema}/nav.schema.json +1 -2
- package/{build → schema}/site.schema.json +1 -2
- package/src/_alerts.scss +4 -0
- package/src/_base.scss +41 -2
- package/src/_layout.scss +37 -0
- package/src/anchor/index.ts +0 -1
- package/src/index.ts +3 -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 +0 -14
- package/src/style.scss +9 -57
- package/src/timezone/index.ts +5 -6
- package/src/util.ts +1 -1
- package/templates/_top.html +1 -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/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/Pair.java.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
|
|
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
|
-
|
|
90
|
-
@media (hover: hover) {
|
|
91
|
-
a:hover {
|
|
92
|
-
color: var(--fg2-color);
|
|
93
|
-
}
|
|
94
54
|
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
@@ -233,16 +191,6 @@ main details {
|
|
|
233
191
|
border: 2px solid var(--fg-color);
|
|
234
192
|
}
|
|
235
193
|
|
|
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
194
|
button,
|
|
247
195
|
a.button,
|
|
248
196
|
input[type='submit'] {
|
|
@@ -279,7 +227,7 @@ input[type='submit']:focus-visible,
|
|
|
279
227
|
input[type='text']:focus-visible,
|
|
280
228
|
input[type='search']:focus-visible {
|
|
281
229
|
border-color: var(--fg-color);
|
|
282
|
-
background: var(--bg2-color);
|
|
230
|
+
background-color: var(--bg2-color);
|
|
283
231
|
}
|
|
284
232
|
|
|
285
233
|
button.icon-button {
|
|
@@ -470,6 +418,10 @@ dl {
|
|
|
470
418
|
text-decoration-color: var(--fg2-color);
|
|
471
419
|
}
|
|
472
420
|
}
|
|
421
|
+
|
|
422
|
+
&:active {
|
|
423
|
+
text-decoration-color: var(--link-active-color);
|
|
424
|
+
}
|
|
473
425
|
}
|
|
474
426
|
}
|
|
475
427
|
|
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
|
|
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
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
<header data-pagefind-ignore>
|
|
55
55
|
<details>
|
|
56
56
|
<summary aria-label="Toggle site navigation">
|
|
57
|
-
|
|
57
|
+
<% if (site.symbol) { %><span class="logo" aria-hidden="true"><%= site.symbol %></span><% } %>
|
|
58
58
|
<span class="site-title" aria-hidden="true"><%= site.title %></span>
|
|
59
59
|
<div id="to-top-container" class="to-top-container"></div>
|
|
60
60
|
<% if (site.features.search) { %>
|
package/types/dev.d.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
interface
|
|
1
|
+
interface TimeZone {
|
|
2
2
|
value: string;
|
|
3
3
|
label: string;
|
|
4
4
|
abbreviation: string;
|
|
5
5
|
offsetMinutes?: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
defaultTimeZone: string;
|
|
14
|
-
timezones: TimezoneDef[];
|
|
15
|
-
};
|
|
16
|
-
}
|
|
8
|
+
declare const __SITE_BASE__: string;
|
|
9
|
+
declare const __SITE_BASE_PATH__: string;
|
|
10
|
+
declare const __SITE_TITLE_POSTFIX__: string;
|
|
11
|
+
declare const __SITE_DEFAULT_TIMEZONE__: string;
|
|
12
|
+
declare const __SITE_TIMEZONES__: TimeZone[];
|
|
@@ -22,6 +22,13 @@ declare module 'pagefind' {
|
|
|
22
22
|
export function close(): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
declare module 'katex/contrib/render-a11y-string' {
|
|
26
|
+
export default function renderA11yString(
|
|
27
|
+
text: string,
|
|
28
|
+
settings?: Record<string, unknown>,
|
|
29
|
+
): string;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
declare module 'jsdom' {
|
|
26
33
|
export class JSDOM {
|
|
27
34
|
constructor(html?: string | Buffer, options?: Record<string, unknown>);
|
package/config/authors.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "alex": { "name": "Alex Breen", "avatar": "/avatars/alex.jpg" } }
|
package/config/nav.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"title": "Navigation",
|
|
4
|
-
"links": [{ "text": "Home", "internal": "/index.html" }]
|
|
5
|
-
},
|
|
6
|
-
{
|
|
7
|
-
"title": "Topics",
|
|
8
|
-
"links": [
|
|
9
|
-
{ "text": "Lectures", "internal": "/lectures/index.html" },
|
|
10
|
-
{
|
|
11
|
-
"text": "Problem Sets",
|
|
12
|
-
"internal": "/problem_sets/index.html",
|
|
13
|
-
"disabled": true
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"title": "Links",
|
|
19
|
-
"links": [
|
|
20
|
-
{ "text": "Zoom", "external": "https://zoom.com" },
|
|
21
|
-
{
|
|
22
|
-
"text": "Canvas",
|
|
23
|
-
"external": "https://www.instructure.com/",
|
|
24
|
-
"disabled": true
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|
|
28
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|