@abreen/tada 1.0.2 → 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 +96 -48
- package/bin/tada.ts +621 -0
- package/bin/validators.test.ts +205 -0
- package/bin/validators.ts +85 -0
- package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +37 -17
- package/build/build-manifest.test.ts +349 -0
- package/build/build-manifest.ts +165 -0
- package/build/bundle.ts +116 -0
- package/{webpack/code.test.js → build/code.test.ts} +10 -7
- package/build/colors.ts +25 -0
- package/build/content-watch.ts +107 -0
- package/build/copy.ts +118 -0
- package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
- package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
- package/build/features.ts +11 -0
- package/build/generate-content-assets.ts +320 -0
- package/build/generate-favicon.ts +172 -0
- package/build/generate-fonts.ts +31 -0
- package/build/generate-katex-assets.ts +35 -0
- package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
- package/build/globals.test.ts +99 -0
- package/build/globals.ts +88 -0
- package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
- package/build/json-schema.test.ts +57 -0
- package/build/json-schema.ts +33 -0
- package/build/log.test.ts +111 -0
- package/build/log.ts +167 -0
- package/build/markdown-plugins.test.ts +396 -0
- package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
- package/build/pagefind.ts +339 -0
- package/build/pdf-text.test.ts +52 -0
- package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
- package/build/pipeline.ts +120 -0
- package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
- package/{webpack/reachability.js → build/reachability.ts} +77 -34
- package/build/serve.ts +112 -0
- package/build/site-variables.ts +68 -0
- package/{webpack/templates.js → build/templates.ts} +38 -35
- package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
- package/build/toc-plugin.test.ts +105 -0
- package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
- package/build/types.ts +181 -0
- package/build/util.ts +26 -0
- package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
- package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
- package/build/utils/derive-theme.test.ts +111 -0
- package/build/utils/derive-theme.ts +85 -0
- package/build/utils/file-types.test.ts +61 -0
- package/build/utils/file-types.ts +13 -0
- package/build/utils/front-matter.test.ts +77 -0
- package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
- package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
- package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +68 -38
- package/{webpack/utils/markdown.js → build/utils/markdown.ts} +126 -49
- package/build/utils/paths.test.ts +91 -0
- package/{webpack/utils/paths.js → build/utils/paths.ts} +18 -22
- package/{webpack/utils/render.js → build/utils/render.ts} +208 -130
- package/build/utils/shiki-highlighter.ts +29 -0
- package/build/validate-internal-links-plugin.test.ts +106 -0
- package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
- package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
- package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
- package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
- package/build/watch.ts +549 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable.woff2 +0 -0
- package/{content → init/content}/index.md +9 -3
- package/{content → init/content}/lectures/index.md +1 -1
- package/{content → init/content}/markdown.md +28 -8
- package/init/content/problem_sets/index.html +14 -0
- package/package.json +33 -22
- package/{templates → schema}/nav.schema.json +1 -2
- package/{webpack → schema}/site.schema.json +4 -12
- package/src/_alerts.scss +96 -0
- package/src/_base.scss +145 -0
- package/src/{layout.scss → _layout.scss} +36 -1
- package/src/anchor/index.ts +0 -1
- package/src/anchor/style.scss +1 -9
- package/src/code/index.ts +3 -3
- package/src/code.scss +1 -1
- package/src/critical.scss +5 -0
- package/src/header/_base.scss +129 -0
- package/src/header/style.scss +3 -131
- package/src/index.ts +4 -5
- package/src/question/style.scss +1 -1
- package/src/search/index.test.ts +203 -0
- package/src/search/index.ts +38 -93
- package/src/search/pdf-utils.ts +79 -0
- package/src/search/style.scss +8 -28
- package/src/style.scss +15 -326
- package/src/timezone/index.ts +5 -6
- package/src/toc/style.scss +5 -39
- package/src/util.ts +9 -6
- package/templates/_theme.scss +38 -14
- package/templates/_top.html +1 -1
- package/tsconfig.json +10 -6
- package/types/dev.d.ts +1 -0
- package/types/file-system-access.d.ts +5 -0
- package/types/markdown-it-plugins.d.ts +11 -0
- package/types/site-variables.d.ts +6 -10
- package/types/untyped-modules.d.ts +47 -0
- package/bin/tada.js +0 -361
- package/config/authors.json +0 -1
- package/config/nav.json +0 -28
- package/content/problem_sets/index.md +0 -6
- package/types/dev.ts +0 -3
- package/webpack/build-state.js +0 -97
- package/webpack/colors.js +0 -15
- package/webpack/config.base.js +0 -151
- package/webpack/config.dev.js +0 -23
- package/webpack/config.prod.js +0 -32
- package/webpack/content-watch-plugin.js +0 -153
- package/webpack/features.js +0 -5
- package/webpack/generate-content-assets-plugin.js +0 -308
- package/webpack/generate-favicon-plugin.js +0 -198
- package/webpack/generate-fonts-plugin.js +0 -69
- package/webpack/globals.js +0 -74
- package/webpack/json-schema.js +0 -19
- package/webpack/log.js +0 -143
- package/webpack/markdown-plugins.test.js +0 -203
- package/webpack/pagefind-plugin.js +0 -379
- package/webpack/print-flair-plugin.js +0 -22
- package/webpack/serve.js +0 -104
- package/webpack/site-variables.js +0 -53
- package/webpack/util.js +0 -49
- package/webpack/utils/define-plugin.js +0 -20
- package/webpack/utils/file-types.js +0 -26
- package/webpack/utils/parse-hsl.js +0 -8
- package/webpack/utils/shiki-highlighter.js +0 -26
- package/webpack/watch.js +0 -166
- /package/{webpack → build}/flair.json +0 -0
- /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
- /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
- /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +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/{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,205 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { SiteConfigInput } from '../build/types';
|
|
3
|
+
import {
|
|
4
|
+
validateSymbol,
|
|
5
|
+
validateColor,
|
|
6
|
+
validateHue,
|
|
7
|
+
validateUrl,
|
|
8
|
+
validateBasePath,
|
|
9
|
+
createSiteConfig,
|
|
10
|
+
} from './validators';
|
|
11
|
+
|
|
12
|
+
describe('validateSymbol', () => {
|
|
13
|
+
test('accepts a single uppercase letter', () => {
|
|
14
|
+
expect(validateSymbol('A')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('accepts digits and uppercase letters up to 5 chars', () => {
|
|
18
|
+
expect(validateSymbol('CS 0')).toBeNull();
|
|
19
|
+
expect(validateSymbol('CS101')).toBeNull();
|
|
20
|
+
expect(validateSymbol('AB-CD')).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('rejects empty string', () => {
|
|
24
|
+
expect(validateSymbol('')).not.toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('rejects value longer than 5 chars', () => {
|
|
28
|
+
expect(validateSymbol('TOOLONG')).not.toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('rejects lowercase letters', () => {
|
|
32
|
+
expect(validateSymbol('abc')).not.toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('rejects special characters', () => {
|
|
36
|
+
expect(validateSymbol('A@B')).not.toBeNull();
|
|
37
|
+
expect(validateSymbol('A.B')).not.toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('validateColor', () => {
|
|
42
|
+
test('accepts CSS color names', () => {
|
|
43
|
+
expect(validateColor('red')).toBeNull();
|
|
44
|
+
expect(validateColor('tomato')).toBeNull();
|
|
45
|
+
expect(validateColor('blue')).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('accepts hex colors', () => {
|
|
49
|
+
expect(validateColor('#f00')).toBeNull();
|
|
50
|
+
expect(validateColor('#ff0000')).toBeNull();
|
|
51
|
+
expect(validateColor('#c04040')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('accepts HSL colors', () => {
|
|
55
|
+
expect(validateColor('hsl(195 70% 40%)')).toBeNull();
|
|
56
|
+
expect(validateColor('hsl(195, 70%, 40%)')).toBeNull();
|
|
57
|
+
expect(validateColor('hsl(195deg 70% 40%)')).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('accepts RGB colors', () => {
|
|
61
|
+
expect(validateColor('rgb(0 0 0)')).toBeNull();
|
|
62
|
+
expect(validateColor('rgb(255, 99, 71)')).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('rejects empty string', () => {
|
|
66
|
+
expect(validateColor('')).not.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('rejects invalid color strings', () => {
|
|
70
|
+
expect(validateColor('notacolor')).not.toBeNull();
|
|
71
|
+
expect(validateColor('hsl 195 70% 40%')).not.toBeNull();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('validateHue', () => {
|
|
76
|
+
test('accepts integer', () => {
|
|
77
|
+
expect(validateHue('0')).toBeNull();
|
|
78
|
+
expect(validateHue('180')).toBeNull();
|
|
79
|
+
expect(validateHue('360')).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('accepts integer with deg suffix', () => {
|
|
83
|
+
expect(validateHue('90deg')).toBeNull();
|
|
84
|
+
expect(validateHue('360deg')).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('rejects empty string', () => {
|
|
88
|
+
expect(validateHue('')).not.toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('rejects out of range', () => {
|
|
92
|
+
expect(validateHue('-1')).not.toBeNull();
|
|
93
|
+
expect(validateHue('361')).not.toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('rejects non-integer', () => {
|
|
97
|
+
expect(validateHue('1.5')).not.toBeNull();
|
|
98
|
+
expect(validateHue('abc')).not.toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('validateUrl', () => {
|
|
103
|
+
test('accepts https URL', () => {
|
|
104
|
+
expect(validateUrl('https://example.edu')).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('accepts http URL', () => {
|
|
108
|
+
expect(validateUrl('http://localhost')).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('accepts URL with port', () => {
|
|
112
|
+
expect(validateUrl('http://localhost:8080')).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('rejects empty string', () => {
|
|
116
|
+
expect(validateUrl('')).not.toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('rejects trailing slash', () => {
|
|
120
|
+
expect(validateUrl('https://example.edu/')).not.toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('rejects URL with path', () => {
|
|
124
|
+
expect(validateUrl('https://example.edu/foo')).not.toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('rejects non-http scheme', () => {
|
|
128
|
+
expect(validateUrl('ftp://example.edu')).not.toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('rejects bare domain', () => {
|
|
132
|
+
expect(validateUrl('example.edu')).not.toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('validateBasePath', () => {
|
|
137
|
+
test('accepts root path', () => {
|
|
138
|
+
expect(validateBasePath('/')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('accepts simple path segment', () => {
|
|
142
|
+
expect(validateBasePath('/foo')).toBeNull();
|
|
143
|
+
expect(validateBasePath('/foo-bar')).toBeNull();
|
|
144
|
+
expect(validateBasePath('/cs101')).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('rejects path without leading slash', () => {
|
|
148
|
+
expect(validateBasePath('foo')).not.toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('rejects trailing slash', () => {
|
|
152
|
+
expect(validateBasePath('/foo/')).not.toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('rejects spaces', () => {
|
|
156
|
+
expect(validateBasePath('/foo bar')).not.toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('rejects nested paths', () => {
|
|
160
|
+
expect(validateBasePath('/foo/bar')).not.toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('createSiteConfig', () => {
|
|
165
|
+
const base: SiteConfigInput = {
|
|
166
|
+
title: 'Test Site',
|
|
167
|
+
symbol: 'TS',
|
|
168
|
+
themeColor: 'hsl(200 50% 40%)',
|
|
169
|
+
tintHue: '20',
|
|
170
|
+
tintAmount: '100',
|
|
171
|
+
defaultTimeZone: 'America/New_York',
|
|
172
|
+
base: 'https://example.edu',
|
|
173
|
+
basePath: '/',
|
|
174
|
+
internalDomains: ['example.edu'],
|
|
175
|
+
features: { search: true, code: true, favicon: false },
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
test('returns an object with all expected keys', () => {
|
|
179
|
+
const config = createSiteConfig(base);
|
|
180
|
+
expect(config).toMatchObject({
|
|
181
|
+
title: 'Test Site',
|
|
182
|
+
symbol: 'TS',
|
|
183
|
+
themeColor: 'hsl(200 50% 40%)',
|
|
184
|
+
base: 'https://example.edu',
|
|
185
|
+
basePath: '/',
|
|
186
|
+
internalDomains: ['example.edu'],
|
|
187
|
+
defaultTimeZone: 'America/New_York',
|
|
188
|
+
features: { search: true, code: true, favicon: false },
|
|
189
|
+
codeLanguages: { java: 'java', py: 'python' },
|
|
190
|
+
vars: {},
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('coerces tintHue and tintAmount to numbers', () => {
|
|
195
|
+
const config = createSiteConfig(base);
|
|
196
|
+
expect(config.tintHue).toBe(20);
|
|
197
|
+
expect(config.tintAmount).toBe(100);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('coerces numeric string tintHue correctly', () => {
|
|
201
|
+
const config = createSiteConfig({ ...base, tintHue: '0', tintAmount: '0' });
|
|
202
|
+
expect(config.tintHue).toBe(0);
|
|
203
|
+
expect(config.tintAmount).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SiteConfigInput, SiteVariables } from '../build/types';
|
|
2
|
+
|
|
3
|
+
export function validateSymbol(value: string): string | null {
|
|
4
|
+
if (!value) {
|
|
5
|
+
return 'Symbol is required';
|
|
6
|
+
}
|
|
7
|
+
if (value.length > 5) {
|
|
8
|
+
return 'Symbol must be 5 characters or fewer';
|
|
9
|
+
}
|
|
10
|
+
if (!/^[A-Z0-9\- ]{1,5}$/.test(value)) {
|
|
11
|
+
return 'Symbol must contain only uppercase letters, digits, hyphens, and spaces';
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateColor(value: string): string | null {
|
|
17
|
+
if (!value) {
|
|
18
|
+
return 'Color is required';
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
if (!Bun.color(value)) {
|
|
22
|
+
throw new Error();
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
return 'Must be a valid CSS color, e.g. "tomato", "#c04040", or "hsl(195 70% 40%)"';
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateHue(value: string): string | null {
|
|
31
|
+
if (!value) {
|
|
32
|
+
return 'Hue is required';
|
|
33
|
+
}
|
|
34
|
+
const n = Number(String(value).replace(/deg$/, ''));
|
|
35
|
+
if (!Number.isInteger(n) || n < 0 || n > 360) {
|
|
36
|
+
return 'Must be an integer from 0 to 360, with or without "deg"';
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateUrl(value: string): string | null {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return 'URL is required';
|
|
44
|
+
}
|
|
45
|
+
if (!/^https?:\/\/[-.:a-zA-Z0-9]+$/.test(value)) {
|
|
46
|
+
return 'Must be a valid URL like https://example.edu (no trailing slash or path)';
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validateBasePath(value: string): string | null {
|
|
52
|
+
if (!/^\/[-a-zA-Z0-9]*$/.test(value)) {
|
|
53
|
+
return 'Must start with / and contain only letters, digits, and hyphens';
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createSiteConfig({
|
|
59
|
+
title,
|
|
60
|
+
symbol,
|
|
61
|
+
themeColor,
|
|
62
|
+
tintHue,
|
|
63
|
+
tintAmount,
|
|
64
|
+
defaultTimeZone,
|
|
65
|
+
base,
|
|
66
|
+
basePath,
|
|
67
|
+
internalDomains,
|
|
68
|
+
features,
|
|
69
|
+
}: SiteConfigInput): SiteVariables {
|
|
70
|
+
return {
|
|
71
|
+
title,
|
|
72
|
+
titlePostfix: ` - ${title}`,
|
|
73
|
+
symbol,
|
|
74
|
+
features,
|
|
75
|
+
base,
|
|
76
|
+
basePath,
|
|
77
|
+
internalDomains,
|
|
78
|
+
defaultTimeZone,
|
|
79
|
+
codeLanguages: { java: 'java', py: 'python' },
|
|
80
|
+
themeColor,
|
|
81
|
+
tintHue: Number(tintHue),
|
|
82
|
+
tintAmount: Number(tintAmount),
|
|
83
|
+
vars: {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
import type Token from 'markdown-it/lib/token.mjs';
|
|
3
|
+
import { createApplyBasePath } from './utils/paths';
|
|
4
|
+
import { isFeatureEnabled } from './features';
|
|
5
|
+
import { makeLogger } from './log';
|
|
6
|
+
import type { SiteVariables } from './types';
|
|
3
7
|
|
|
4
8
|
const log = makeLogger(__filename);
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
export default function applyBasePathPlugin(
|
|
11
|
+
md: MarkdownIt,
|
|
12
|
+
siteVariables: SiteVariables,
|
|
13
|
+
): void {
|
|
7
14
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
8
|
-
const rewriteCodeLinks = siteVariables
|
|
15
|
+
const rewriteCodeLinks = isFeatureEnabled(siteVariables, 'code');
|
|
9
16
|
|
|
10
|
-
function rewriteInternalHref(href) {
|
|
17
|
+
function rewriteInternalHref(href: string): string {
|
|
11
18
|
const match = href.match(/^([^?#]*)(.*)$/);
|
|
12
19
|
const pathname = match ? match[1] : href;
|
|
13
20
|
const suffix = match ? match[2] : '';
|
|
@@ -15,7 +22,7 @@ module.exports = function externalLinks(md, siteVariables, options = {}) {
|
|
|
15
22
|
|
|
16
23
|
if (rewriteCodeLinks) {
|
|
17
24
|
// Rewrite code file links to .html links
|
|
18
|
-
for (const ext of Object.keys(siteVariables.codeLanguages)) {
|
|
25
|
+
for (const ext of Object.keys(siteVariables.codeLanguages ?? {})) {
|
|
19
26
|
if (modifiedPath.endsWith(`.${ext}`)) {
|
|
20
27
|
modifiedPath = modifiedPath.replace(
|
|
21
28
|
new RegExp(`\\.${ext}$`),
|
|
@@ -29,9 +36,12 @@ module.exports = function externalLinks(md, siteVariables, options = {}) {
|
|
|
29
36
|
return modifiedPath + suffix;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
function checkAndApplyBasePath(token) {
|
|
39
|
+
function checkAndApplyBasePath(token: Token): void {
|
|
33
40
|
if (token.type === 'link_open') {
|
|
34
41
|
const href = token.attrGet('href');
|
|
42
|
+
if (!href) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
35
45
|
|
|
36
46
|
if (href.startsWith('/')) {
|
|
37
47
|
const modifiedHref = rewriteInternalHref(href);
|
|
@@ -58,15 +68,25 @@ module.exports = function externalLinks(md, siteVariables, options = {}) {
|
|
|
58
68
|
token.attrSet('src', afterApply);
|
|
59
69
|
}
|
|
60
70
|
} else if (token.type === 'html_block' || token.type === 'html_inline') {
|
|
61
|
-
token.content = token.content
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
token.content = token.content
|
|
72
|
+
.replace(
|
|
73
|
+
/(<a\b[^>]*\bhref=")(\/)([^"]*")/g,
|
|
74
|
+
(match, prefix, slash, rest) => {
|
|
75
|
+
const href = slash + rest.slice(0, -1);
|
|
76
|
+
const afterApply = applyBasePath(href);
|
|
77
|
+
log.debug`Applying base path to a tag href: ${href} -> ${afterApply}`;
|
|
78
|
+
return prefix + afterApply + '"';
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
.replace(
|
|
82
|
+
/(<img\b[^>]*\bsrc=")(\/)([^"]*")/g,
|
|
83
|
+
(match, prefix, slash, rest) => {
|
|
84
|
+
const src = slash + rest.slice(0, -1);
|
|
85
|
+
const afterApply = applyBasePath(src);
|
|
86
|
+
log.debug`Applying base path to img tag src: ${src} -> ${afterApply}`;
|
|
87
|
+
return prefix + afterApply + '"';
|
|
88
|
+
},
|
|
89
|
+
);
|
|
70
90
|
}
|
|
71
91
|
|
|
72
92
|
token.children?.map(checkAndApplyBasePath);
|
|
@@ -75,4 +95,4 @@ module.exports = function externalLinks(md, siteVariables, options = {}) {
|
|
|
75
95
|
md.core.ruler.push('apply_base_path', state => {
|
|
76
96
|
state.tokens.map(checkAndApplyBasePath);
|
|
77
97
|
});
|
|
78
|
-
}
|
|
98
|
+
}
|