@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
|
@@ -1,39 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import { stripHtml } from 'string-strip-html';
|
|
5
|
+
import { isFeatureEnabled } from '../features';
|
|
6
|
+
import { makeLogger } from '../log';
|
|
7
|
+
import { B } from '../colors';
|
|
8
|
+
import createGlobals from '../globals';
|
|
9
|
+
import { render, json } from '../templates';
|
|
10
|
+
import {
|
|
10
11
|
extractJavaMethodToc,
|
|
11
12
|
renderCodeSegment,
|
|
12
13
|
renderCodeWithComments,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
} from './code';
|
|
15
|
+
import { extensionIsMarkdown } from './file-types';
|
|
16
|
+
import { createApplyBasePath, normalizeOutputPath } from './paths';
|
|
17
|
+
import { parseFrontMatterAndContent } from './front-matter';
|
|
18
|
+
import { createMarkdown } from './markdown';
|
|
19
|
+
import { generateTocHtml, generateCodeTocHtml } from '../toc-plugin';
|
|
20
|
+
import {
|
|
20
21
|
parseLiterateJava,
|
|
21
22
|
hasMainMethod,
|
|
22
23
|
deriveClassName,
|
|
23
24
|
compileJavaSource,
|
|
24
25
|
executeLiterateJava,
|
|
25
|
-
}
|
|
26
|
+
} from './literate-java';
|
|
27
|
+
import type {
|
|
28
|
+
Asset,
|
|
29
|
+
SiteVariables,
|
|
30
|
+
RenderPlainTextOptions,
|
|
31
|
+
RenderCodePageOptions,
|
|
32
|
+
RenderLiterateJavaOptions,
|
|
33
|
+
RenderCopiedContentOptions,
|
|
34
|
+
} from '../types';
|
|
26
35
|
|
|
27
36
|
const log = makeLogger(__filename);
|
|
28
37
|
|
|
29
38
|
const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
interface TemplateParametersInput {
|
|
41
|
+
pageVariables: Record<string, unknown>;
|
|
42
|
+
siteVariables: SiteVariables;
|
|
43
|
+
content: string | null;
|
|
44
|
+
applyBasePath: (subPath: string) => string;
|
|
45
|
+
subPath: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveAuthor(
|
|
49
|
+
pageVariables: Record<string, unknown>,
|
|
50
|
+
filePath: string,
|
|
51
|
+
): void {
|
|
32
52
|
if (!pageVariables.author) {
|
|
33
53
|
return;
|
|
34
54
|
}
|
|
35
|
-
const authors = json('authors.json')
|
|
36
|
-
|
|
55
|
+
const authors = json('authors.json') as
|
|
56
|
+
| Record<string, Record<string, unknown>>
|
|
57
|
+
| undefined;
|
|
58
|
+
if (!authors) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${filePath}: author "${pageVariables.author}" specified but no authors.json found`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const authorKey = pageVariables.author as string;
|
|
37
64
|
const authorEntry = authors[authorKey];
|
|
38
65
|
if (!authorEntry) {
|
|
39
66
|
throw new Error(
|
|
@@ -43,15 +70,20 @@ function resolveAuthor(pageVariables, filePath) {
|
|
|
43
70
|
pageVariables.author = authorEntry;
|
|
44
71
|
}
|
|
45
72
|
|
|
46
|
-
function validateFrontMatter(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
function validateFrontMatter(
|
|
74
|
+
pageVariables: Record<string, unknown>,
|
|
75
|
+
filePath: string,
|
|
76
|
+
): void {
|
|
77
|
+
const missing = REQUIRED_FRONT_MATTER_FIELDS.filter(
|
|
78
|
+
field => !pageVariables[field],
|
|
79
|
+
);
|
|
80
|
+
if (missing.length > 0) {
|
|
81
|
+
const noun = missing.length === 1 ? 'field' : 'fields';
|
|
82
|
+
const fields = missing.map(f => `"${f}"`).join(', ');
|
|
83
|
+
throw new Error(
|
|
84
|
+
`${filePath}: missing required front matter ${noun}: ${fields}`,
|
|
85
|
+
);
|
|
53
86
|
}
|
|
54
|
-
return valid;
|
|
55
87
|
}
|
|
56
88
|
|
|
57
89
|
function createTemplateParameters({
|
|
@@ -60,7 +92,7 @@ function createTemplateParameters({
|
|
|
60
92
|
content,
|
|
61
93
|
applyBasePath,
|
|
62
94
|
subPath,
|
|
63
|
-
}) {
|
|
95
|
+
}: TemplateParametersInput): Record<string, unknown> {
|
|
64
96
|
return {
|
|
65
97
|
...(siteVariables.vars || {}),
|
|
66
98
|
...createGlobals(pageVariables, siteVariables, subPath),
|
|
@@ -73,43 +105,65 @@ function createTemplateParameters({
|
|
|
73
105
|
};
|
|
74
106
|
}
|
|
75
107
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
export function injectAssetTags(
|
|
109
|
+
html: string,
|
|
110
|
+
assetFiles: string[],
|
|
111
|
+
applyBasePath: (subPath: string) => string,
|
|
112
|
+
distDir: string,
|
|
113
|
+
): string {
|
|
114
|
+
const jsAssets = assetFiles.filter(f => f.endsWith('.js'));
|
|
115
|
+
const cssAssets = assetFiles.filter(f => f.endsWith('.css'));
|
|
84
116
|
|
|
85
117
|
const scriptTags = jsAssets
|
|
86
118
|
.map(asset => `<script defer src="${applyBasePath('/' + asset)}"></script>`)
|
|
87
119
|
.join('');
|
|
88
|
-
const
|
|
120
|
+
const criticalAssets = cssAssets.filter(f => f.includes('critical.bundle.'));
|
|
121
|
+
const asyncAssets = cssAssets.filter(f => !f.includes('critical.bundle.'));
|
|
122
|
+
const criticalTags = criticalAssets
|
|
123
|
+
.map(asset => {
|
|
124
|
+
const css = fs.readFileSync(path.join(distDir, asset), 'utf-8');
|
|
125
|
+
return `<style>${css}</style>`;
|
|
126
|
+
})
|
|
127
|
+
.join('');
|
|
128
|
+
const asyncLinkTags = asyncAssets
|
|
89
129
|
.map(
|
|
90
|
-
asset =>
|
|
130
|
+
asset =>
|
|
131
|
+
`<link href="${applyBasePath('/' + asset)}" rel="stylesheet" media="print" onload="this.media='all'">` +
|
|
132
|
+
`<noscript><link href="${applyBasePath('/' + asset)}" rel="stylesheet"></noscript>`,
|
|
91
133
|
)
|
|
92
134
|
.join('');
|
|
93
135
|
|
|
94
136
|
return html
|
|
95
|
-
.replace('<head>', `<head>${
|
|
137
|
+
.replace('<head>', `<head>${criticalTags}${asyncLinkTags}`)
|
|
96
138
|
.replace('</head>', `${scriptTags}</head>`);
|
|
97
139
|
}
|
|
98
140
|
|
|
99
|
-
function
|
|
141
|
+
export function injectKatexStylesheet(
|
|
142
|
+
html: string,
|
|
143
|
+
applyBasePath: (subPath: string) => string,
|
|
144
|
+
): string {
|
|
145
|
+
const href = applyBasePath('/katex/katex.min.css');
|
|
146
|
+
const tag =
|
|
147
|
+
`<link href="${href}" rel="stylesheet" media="print" onload="this.media='all'">` +
|
|
148
|
+
`<noscript><link href="${href}" rel="stylesheet"></noscript>`;
|
|
149
|
+
return html.replace('<head>', `<head>${tag}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function toContentAssetPath(contentDir: string, filePath: string): string {
|
|
100
153
|
return path
|
|
101
154
|
.relative(contentDir, filePath)
|
|
102
155
|
.split(path.sep)
|
|
103
156
|
.join(path.posix.sep);
|
|
104
157
|
}
|
|
105
158
|
|
|
106
|
-
function renderPlainTextPageAsset({
|
|
159
|
+
export function renderPlainTextPageAsset({
|
|
107
160
|
filePath,
|
|
108
161
|
contentDir,
|
|
162
|
+
distDir,
|
|
109
163
|
siteVariables,
|
|
110
164
|
validInternalTargets,
|
|
111
|
-
|
|
112
|
-
}) {
|
|
165
|
+
assetFiles,
|
|
166
|
+
}: RenderPlainTextOptions): Asset[] {
|
|
113
167
|
const { dir, name, ext } = path.parse(filePath);
|
|
114
168
|
const subPath = path.relative(contentDir, path.join(dir, name));
|
|
115
169
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
@@ -124,16 +178,16 @@ function renderPlainTextPageAsset({
|
|
|
124
178
|
{ validateInternalLinks: extensionIsMarkdown(ext.toLowerCase()) },
|
|
125
179
|
);
|
|
126
180
|
|
|
127
|
-
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
181
|
+
validateFrontMatter(pageVariables, filePath);
|
|
130
182
|
|
|
131
183
|
if (!pageVariables.template) {
|
|
132
184
|
pageVariables.template = 'default';
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
if (pageVariables.toc && tocItems) {
|
|
136
|
-
pageVariables.tocHtml = generateTocHtml(
|
|
188
|
+
pageVariables.tocHtml = generateTocHtml(
|
|
189
|
+
tocItems as Parameters<typeof generateTocHtml>[0],
|
|
190
|
+
);
|
|
137
191
|
}
|
|
138
192
|
|
|
139
193
|
const templateParameters = createTemplateParameters({
|
|
@@ -144,11 +198,14 @@ function renderPlainTextPageAsset({
|
|
|
144
198
|
subPath,
|
|
145
199
|
});
|
|
146
200
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
);
|
|
201
|
+
const templateHtml = render(
|
|
202
|
+
`${pageVariables.template}.html`,
|
|
203
|
+
templateParameters,
|
|
204
|
+
) as string;
|
|
205
|
+
let html = injectAssetTags(templateHtml, assetFiles, applyBasePath, distDir);
|
|
206
|
+
if (templateHtml.includes('class="katex"')) {
|
|
207
|
+
html = injectKatexStylesheet(html, applyBasePath);
|
|
208
|
+
}
|
|
152
209
|
|
|
153
210
|
return [
|
|
154
211
|
{
|
|
@@ -158,16 +215,17 @@ function renderPlainTextPageAsset({
|
|
|
158
215
|
];
|
|
159
216
|
}
|
|
160
217
|
|
|
161
|
-
function renderCodePageAsset({
|
|
218
|
+
export function renderCodePageAsset({
|
|
162
219
|
filePath,
|
|
163
220
|
contentDir,
|
|
221
|
+
distDir,
|
|
164
222
|
siteVariables,
|
|
165
|
-
|
|
166
|
-
}) {
|
|
223
|
+
assetFiles,
|
|
224
|
+
}: RenderCodePageOptions): Asset[] {
|
|
167
225
|
const { dir, name, ext } = path.parse(filePath);
|
|
168
226
|
const subPath = path.relative(contentDir, path.join(dir, name));
|
|
169
227
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
170
|
-
const lang = siteVariables.codeLanguages[ext.slice(1).toLowerCase()];
|
|
228
|
+
const lang = siteVariables.codeLanguages![ext.slice(1).toLowerCase()];
|
|
171
229
|
const sourceCode = fs.readFileSync(filePath, 'utf-8');
|
|
172
230
|
|
|
173
231
|
log.info`Rendering code page ${B`${subPath + ext}`}`;
|
|
@@ -178,7 +236,7 @@ function renderCodePageAsset({
|
|
|
178
236
|
const titleHtml = `<tt>${name + ext}</tt>`;
|
|
179
237
|
const tocItems = lang === 'java' ? extractJavaMethodToc(sourceCode) : [];
|
|
180
238
|
const tocHtml = generateCodeTocHtml(tocItems);
|
|
181
|
-
const pageVariables = {
|
|
239
|
+
const pageVariables: Record<string, unknown> = {
|
|
182
240
|
template: 'code',
|
|
183
241
|
filePath,
|
|
184
242
|
title: `${name}${ext}`,
|
|
@@ -197,10 +255,11 @@ function renderCodePageAsset({
|
|
|
197
255
|
subPath,
|
|
198
256
|
});
|
|
199
257
|
|
|
200
|
-
const html =
|
|
201
|
-
render('code.html', templateParameters),
|
|
202
|
-
|
|
258
|
+
const html = injectAssetTags(
|
|
259
|
+
render('code.html', templateParameters) as string,
|
|
260
|
+
assetFiles,
|
|
203
261
|
applyBasePath,
|
|
262
|
+
distDir,
|
|
204
263
|
);
|
|
205
264
|
|
|
206
265
|
return [
|
|
@@ -211,9 +270,11 @@ function renderCodePageAsset({
|
|
|
211
270
|
];
|
|
212
271
|
}
|
|
213
272
|
|
|
214
|
-
function renderCopiedContentAsset({
|
|
215
|
-
|
|
216
|
-
|
|
273
|
+
export function renderCopiedContentAsset({
|
|
274
|
+
filePath,
|
|
275
|
+
contentDir,
|
|
276
|
+
}: RenderCopiedContentOptions): Asset[] {
|
|
277
|
+
const label = 'Copying source file';
|
|
217
278
|
const relPath = toContentAssetPath(contentDir, filePath);
|
|
218
279
|
|
|
219
280
|
log.info`${label} ${B`${relPath}`}`;
|
|
@@ -222,13 +283,17 @@ function renderCopiedContentAsset({ filePath, contentDir }) {
|
|
|
222
283
|
|
|
223
284
|
/** Parses the file, renders using template, returns HTML & params used to generate page */
|
|
224
285
|
function renderPlainTextContent(
|
|
225
|
-
filePath,
|
|
226
|
-
subPath,
|
|
227
|
-
siteVariables,
|
|
228
|
-
applyBasePath,
|
|
229
|
-
validInternalTargets
|
|
286
|
+
filePath: string,
|
|
287
|
+
subPath: string,
|
|
288
|
+
siteVariables: SiteVariables,
|
|
289
|
+
applyBasePath: (subPath: string) => string,
|
|
290
|
+
validInternalTargets: Set<string>,
|
|
230
291
|
{ validateInternalLinks = true } = {},
|
|
231
|
-
) {
|
|
292
|
+
): {
|
|
293
|
+
content: string | null;
|
|
294
|
+
pageVariables: Record<string, unknown>;
|
|
295
|
+
tocItems: unknown[] | null;
|
|
296
|
+
} {
|
|
232
297
|
const sourceUrlPath = `/${subPath}.html`;
|
|
233
298
|
const md = createMarkdown(siteVariables, {
|
|
234
299
|
validatorOptions: {
|
|
@@ -236,10 +301,9 @@ function renderPlainTextContent(
|
|
|
236
301
|
filePath,
|
|
237
302
|
sourceUrlPath,
|
|
238
303
|
validTargets: validInternalTargets,
|
|
239
|
-
codeExtensions:
|
|
240
|
-
siteVariables.
|
|
241
|
-
|
|
242
|
-
: Object.keys(siteVariables.codeLanguages),
|
|
304
|
+
codeExtensions: isFeatureEnabled(siteVariables, 'code')
|
|
305
|
+
? Object.keys(siteVariables.codeLanguages!)
|
|
306
|
+
: [],
|
|
243
307
|
},
|
|
244
308
|
});
|
|
245
309
|
|
|
@@ -256,25 +320,32 @@ function renderPlainTextContent(
|
|
|
256
320
|
applyBasePath,
|
|
257
321
|
subPath,
|
|
258
322
|
});
|
|
259
|
-
const pageVariablesProcessed = Object.entries(
|
|
323
|
+
const pageVariablesProcessed: Record<string, unknown> = Object.entries(
|
|
324
|
+
pageVariables,
|
|
325
|
+
)
|
|
260
326
|
.map(([k, v]) => {
|
|
261
327
|
const newValue =
|
|
262
328
|
typeof v === 'string' ? _.template(v)(siteOnlyParams) : v;
|
|
263
|
-
return [k, newValue];
|
|
329
|
+
return [k, newValue] as [string, unknown];
|
|
264
330
|
})
|
|
265
|
-
.reduce(
|
|
266
|
-
acc[k]
|
|
267
|
-
|
|
268
|
-
|
|
331
|
+
.reduce(
|
|
332
|
+
(acc, [k, v]) => {
|
|
333
|
+
acc[k] = v;
|
|
334
|
+
return acc;
|
|
335
|
+
},
|
|
336
|
+
{} as Record<string, unknown>,
|
|
337
|
+
);
|
|
269
338
|
|
|
270
339
|
// Render title and description as inline Markdown
|
|
271
340
|
if (pageVariablesProcessed.title) {
|
|
272
|
-
const titleHtml = md.renderInline(pageVariablesProcessed.title);
|
|
341
|
+
const titleHtml = md.renderInline(pageVariablesProcessed.title as string);
|
|
273
342
|
pageVariablesProcessed.titleHtml = titleHtml;
|
|
274
343
|
pageVariablesProcessed.title = stripHtml(titleHtml).result;
|
|
275
344
|
}
|
|
276
345
|
if (pageVariablesProcessed.description) {
|
|
277
|
-
const descriptionHtml = md.renderInline(
|
|
346
|
+
const descriptionHtml = md.renderInline(
|
|
347
|
+
pageVariablesProcessed.description as string,
|
|
348
|
+
);
|
|
278
349
|
pageVariablesProcessed.descriptionHtml = descriptionHtml;
|
|
279
350
|
pageVariablesProcessed.description = stripHtml(descriptionHtml).result;
|
|
280
351
|
}
|
|
@@ -291,35 +362,42 @@ function renderPlainTextContent(
|
|
|
291
362
|
subPath,
|
|
292
363
|
});
|
|
293
364
|
|
|
294
|
-
let html
|
|
365
|
+
let html: string;
|
|
295
366
|
try {
|
|
296
367
|
html = _.template(strippedContent)(params);
|
|
297
|
-
} catch (err) {
|
|
368
|
+
} catch (err: unknown) {
|
|
298
369
|
throw new Error(
|
|
299
|
-
`${filePath}: Lodash template error in page or template: ${err.message}`,
|
|
370
|
+
`${filePath}: Lodash template error in page or template: ${(err as Error).message}`,
|
|
371
|
+
{ cause: err },
|
|
300
372
|
);
|
|
301
373
|
}
|
|
302
374
|
|
|
303
|
-
let tocItems = null;
|
|
375
|
+
let tocItems: unknown[] | null = null;
|
|
304
376
|
if (extensionIsMarkdown(ext)) {
|
|
305
|
-
const env = {};
|
|
306
|
-
html = md.render(html
|
|
307
|
-
tocItems = env.tocItems || null;
|
|
377
|
+
const env: Record<string, unknown> = {};
|
|
378
|
+
html = md.render(html!, env);
|
|
379
|
+
tocItems = (env.tocItems as unknown[] | undefined) || null;
|
|
308
380
|
}
|
|
309
381
|
|
|
310
|
-
return {
|
|
382
|
+
return {
|
|
383
|
+
content: html,
|
|
384
|
+
pageVariables: params.page as Record<string, unknown>,
|
|
385
|
+
tocItems,
|
|
386
|
+
};
|
|
311
387
|
}
|
|
312
388
|
|
|
313
|
-
function stripHtmlComments(str) {
|
|
389
|
+
export function stripHtmlComments(str: string): string {
|
|
314
390
|
return str.replace(/<!---[\s\S]*?-->/g, '');
|
|
315
391
|
}
|
|
316
392
|
|
|
317
|
-
function renderLiterateJavaPageAsset({
|
|
393
|
+
export function renderLiterateJavaPageAsset({
|
|
318
394
|
filePath,
|
|
319
395
|
contentDir,
|
|
396
|
+
distDir,
|
|
320
397
|
siteVariables,
|
|
321
|
-
|
|
322
|
-
|
|
398
|
+
assetFiles,
|
|
399
|
+
skipExecution,
|
|
400
|
+
}: RenderLiterateJavaOptions): Asset[] {
|
|
323
401
|
const { dir, name } = path.parse(filePath);
|
|
324
402
|
const className = deriveClassName(filePath);
|
|
325
403
|
const subPath = path.relative(contentDir, path.join(dir, className));
|
|
@@ -336,24 +414,30 @@ function renderLiterateJavaPageAsset({
|
|
|
336
414
|
visibleBlockIndices,
|
|
337
415
|
} = parseLiterateJava(raw, siteVariables);
|
|
338
416
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
417
|
+
validateFrontMatter(pageVariables, filePath);
|
|
418
|
+
|
|
419
|
+
// Compile and execute the concatenated Java source
|
|
420
|
+
let tempDir: string | undefined;
|
|
421
|
+
let blockOutputMap: Map<number, string> | null = null;
|
|
422
|
+
if (!skipExecution) {
|
|
423
|
+
try {
|
|
424
|
+
tempDir = compileJavaSource(javaSource, className);
|
|
425
|
+
|
|
426
|
+
// Execute if there is a main() method
|
|
427
|
+
if (hasMainMethod(javaSource)) {
|
|
428
|
+
const outputEntries = executeLiterateJava(
|
|
429
|
+
className,
|
|
430
|
+
tempDir,
|
|
431
|
+
codeBlocks,
|
|
432
|
+
);
|
|
433
|
+
blockOutputMap = new Map(
|
|
434
|
+
outputEntries.map(e => [e.blockIndex, e.output]),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
} finally {
|
|
438
|
+
if (tempDir) {
|
|
439
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
440
|
+
}
|
|
357
441
|
}
|
|
358
442
|
}
|
|
359
443
|
|
|
@@ -376,7 +460,7 @@ function renderLiterateJavaPageAsset({
|
|
|
376
460
|
if (line.trim().length === 0) {
|
|
377
461
|
return min;
|
|
378
462
|
}
|
|
379
|
-
const indent = line.match(/^(\s*)/)[1].length;
|
|
463
|
+
const indent = line.match(/^(\s*)/)![1].length;
|
|
380
464
|
return Math.min(min, indent);
|
|
381
465
|
}, Infinity);
|
|
382
466
|
const dedented =
|
|
@@ -404,8 +488,8 @@ function renderLiterateJavaPageAsset({
|
|
|
404
488
|
return codeHtml;
|
|
405
489
|
};
|
|
406
490
|
|
|
407
|
-
const env = {};
|
|
408
|
-
const contentHtml = md.render(content, env);
|
|
491
|
+
const env: Record<string, unknown> = {};
|
|
492
|
+
const contentHtml = md.render(stripHtmlComments(content), env);
|
|
409
493
|
|
|
410
494
|
// Build page variables
|
|
411
495
|
const javaFileName = `${className}.java`;
|
|
@@ -415,7 +499,7 @@ function renderLiterateJavaPageAsset({
|
|
|
415
499
|
),
|
|
416
500
|
);
|
|
417
501
|
|
|
418
|
-
const titleHtml = md.renderInline(pageVariables.title);
|
|
502
|
+
const titleHtml = md.renderInline(pageVariables.title as string);
|
|
419
503
|
pageVariables.titleHtml = titleHtml;
|
|
420
504
|
pageVariables.title = stripHtml(titleHtml).result;
|
|
421
505
|
pageVariables.template = 'literate';
|
|
@@ -423,7 +507,9 @@ function renderLiterateJavaPageAsset({
|
|
|
423
507
|
pageVariables.downloadName = javaFileName;
|
|
424
508
|
|
|
425
509
|
if (pageVariables.toc && env.tocItems) {
|
|
426
|
-
pageVariables.tocHtml = generateTocHtml(
|
|
510
|
+
pageVariables.tocHtml = generateTocHtml(
|
|
511
|
+
env.tocItems as Parameters<typeof generateTocHtml>[0],
|
|
512
|
+
);
|
|
427
513
|
}
|
|
428
514
|
|
|
429
515
|
resolveAuthor(pageVariables, filePath);
|
|
@@ -436,11 +522,11 @@ function renderLiterateJavaPageAsset({
|
|
|
436
522
|
subPath,
|
|
437
523
|
});
|
|
438
524
|
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
applyBasePath
|
|
443
|
-
|
|
525
|
+
const templateHtml = render('literate.html', templateParameters) as string;
|
|
526
|
+
let html = injectAssetTags(templateHtml, assetFiles, applyBasePath, distDir);
|
|
527
|
+
if (templateHtml.includes('class="katex"')) {
|
|
528
|
+
html = injectKatexStylesheet(html, applyBasePath);
|
|
529
|
+
}
|
|
444
530
|
|
|
445
531
|
return [
|
|
446
532
|
{
|
|
@@ -456,11 +542,3 @@ function renderLiterateJavaPageAsset({
|
|
|
456
542
|
},
|
|
457
543
|
];
|
|
458
544
|
}
|
|
459
|
-
|
|
460
|
-
module.exports = {
|
|
461
|
-
injectWebpackAssets,
|
|
462
|
-
renderCodePageAsset,
|
|
463
|
-
renderCopiedContentAsset,
|
|
464
|
-
renderLiterateJavaPageAsset,
|
|
465
|
-
renderPlainTextPageAsset,
|
|
466
|
-
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki';
|
|
2
|
+
import { makeLogger } from '../log';
|
|
3
|
+
|
|
4
|
+
const log = makeLogger(__filename);
|
|
5
|
+
|
|
6
|
+
let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null =
|
|
7
|
+
null;
|
|
8
|
+
|
|
9
|
+
export async function initHighlighter(langs: string[]): Promise<void> {
|
|
10
|
+
if (highlighter) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
log.debug`Initializing syntax highlighter`;
|
|
14
|
+
const { createHighlighter } = await import('shiki');
|
|
15
|
+
highlighter = await createHighlighter({
|
|
16
|
+
themes: ['github-light', 'github-dark'],
|
|
17
|
+
langs,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getHighlighter(): HighlighterGeneric<
|
|
22
|
+
BundledLanguage,
|
|
23
|
+
BundledTheme
|
|
24
|
+
> {
|
|
25
|
+
if (!highlighter) {
|
|
26
|
+
throw new Error('Shiki highlighter not initialized');
|
|
27
|
+
}
|
|
28
|
+
return highlighter;
|
|
29
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import MarkdownIt from 'markdown-it';
|
|
3
|
+
import validateInternalLinks from './validate-internal-links-plugin';
|
|
4
|
+
|
|
5
|
+
function createMd(
|
|
6
|
+
validTargets: string[],
|
|
7
|
+
options: { sourceUrlPath?: string; codeExtensions?: string[] } = {},
|
|
8
|
+
) {
|
|
9
|
+
const md = new MarkdownIt({ html: true });
|
|
10
|
+
md.use(validateInternalLinks, {
|
|
11
|
+
enabled: true,
|
|
12
|
+
filePath: 'test.md',
|
|
13
|
+
sourceUrlPath: options.sourceUrlPath ?? '/test.html',
|
|
14
|
+
validTargets: new Set(validTargets),
|
|
15
|
+
codeExtensions: options.codeExtensions ?? [],
|
|
16
|
+
});
|
|
17
|
+
return md;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('validateInternalLinks', () => {
|
|
21
|
+
test('does nothing when disabled', () => {
|
|
22
|
+
const md = new MarkdownIt();
|
|
23
|
+
md.use(validateInternalLinks, { enabled: false });
|
|
24
|
+
expect(() => md.render('[link](/missing.html)')).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('throws when required options are missing', () => {
|
|
28
|
+
const md = new MarkdownIt();
|
|
29
|
+
expect(() => {
|
|
30
|
+
md.use(validateInternalLinks, { enabled: true });
|
|
31
|
+
md.render('test');
|
|
32
|
+
}).toThrow('requires filePath');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('allows valid internal links', () => {
|
|
36
|
+
const md = createMd(['/about.html']);
|
|
37
|
+
expect(() => md.render('[About](/about.html)')).not.toThrow();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('allows external links', () => {
|
|
41
|
+
const md = createMd([]);
|
|
42
|
+
expect(() => md.render('[Google](https://google.com)')).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('allows anchor links', () => {
|
|
46
|
+
const md = createMd([]);
|
|
47
|
+
expect(() => md.render('[Section](#top)')).not.toThrow();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('allows mailto links', () => {
|
|
51
|
+
const md = createMd([]);
|
|
52
|
+
expect(() => md.render('[Email](mailto:test@example.com)')).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('throws for broken internal link', () => {
|
|
56
|
+
const md = createMd(['/about.html']);
|
|
57
|
+
expect(() => md.render('[Missing](/missing.html)')).toThrow(
|
|
58
|
+
'broken internal link',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('strips query and hash before validating', () => {
|
|
63
|
+
const md = createMd(['/page.html']);
|
|
64
|
+
expect(() => md.render('[Page](/page.html?v=1#section)')).not.toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('resolves relative links from source path', () => {
|
|
68
|
+
const md = createMd(['/docs/guide.html'], {
|
|
69
|
+
sourceUrlPath: '/docs/index.html',
|
|
70
|
+
});
|
|
71
|
+
expect(() => md.render('[Guide](guide.html)')).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('throws for broken relative link', () => {
|
|
75
|
+
const md = createMd(['/docs/guide.html'], {
|
|
76
|
+
sourceUrlPath: '/docs/index.html',
|
|
77
|
+
});
|
|
78
|
+
expect(() => md.render('[Missing](missing.html)')).toThrow(
|
|
79
|
+
'broken internal link',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('rewrites code extensions to .html', () => {
|
|
84
|
+
const md = createMd(['/src/App.html'], { codeExtensions: ['java'] });
|
|
85
|
+
expect(() => md.render('[App](/src/App.java)')).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('detects directory links that should reference index.html', () => {
|
|
89
|
+
const md = createMd(['/docs/index.html']);
|
|
90
|
+
// The plugin logs the "directory link" message and throws a generic
|
|
91
|
+
// "broken internal link(s)" error, so match the thrown message.
|
|
92
|
+
expect(() => md.render('[Docs](/docs)')).toThrow('broken internal link');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('validates links in raw HTML blocks', () => {
|
|
96
|
+
const md = createMd(['/about.html']);
|
|
97
|
+
expect(() => md.render('<a href="/missing.html">link</a>')).toThrow(
|
|
98
|
+
'broken internal link',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('allows protocol-relative URLs', () => {
|
|
103
|
+
const md = createMd([]);
|
|
104
|
+
expect(() => md.render('[CDN](//cdn.example.com/lib.js)')).not.toThrow();
|
|
105
|
+
});
|
|
106
|
+
});
|