@abreen/tada 1.0.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/LICENSE +21 -0
- package/README.md +290 -0
- package/bin/tada.js +361 -0
- package/config/authors.json +1 -0
- package/config/nav.json +28 -0
- package/content/index.md +19 -0
- package/content/lectures/01/Pair.java.md +296 -0
- package/content/lectures/01/Rectangle.java +80 -0
- package/content/lectures/01/demo.py +9 -0
- package/content/lectures/01/index.md +39 -0
- package/content/lectures/01/lecture1.pdf +0 -0
- package/content/lectures/index.md +25 -0
- package/content/markdown.md +379 -0
- package/content/problem_sets/index.md +6 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
- package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
- package/fonts/google-sans-code/LICENSE.txt +93 -0
- package/fonts/inter/InterVariable-Italic.ttf +0 -0
- package/fonts/inter/InterVariable.ttf +0 -0
- package/fonts/inter/LICENSE.txt +92 -0
- package/package.json +70 -0
- package/public/avatars/alex.jpg +0 -0
- package/public/test.txt +1 -0
- package/src/_mixins.scss +4 -0
- package/src/anchor/README.md +6 -0
- package/src/anchor/index.ts +34 -0
- package/src/anchor/style.scss +48 -0
- package/src/code/README.md +5 -0
- package/src/code/index.ts +113 -0
- package/src/code/style.scss +101 -0
- package/src/code.scss +54 -0
- package/src/header/README.md +8 -0
- package/src/header/index.ts +43 -0
- package/src/header/style.scss +228 -0
- package/src/index.ts +73 -0
- package/src/layout.scss +144 -0
- package/src/literate/style.scss +60 -0
- package/src/print/README.md +4 -0
- package/src/print/index.ts +32 -0
- package/src/print/style.scss +82 -0
- package/src/question/README.md +3 -0
- package/src/question/index.ts +25 -0
- package/src/question/style.scss +116 -0
- package/src/search/README.md +6 -0
- package/src/search/index.ts +574 -0
- package/src/search/style.scss +217 -0
- package/src/style.scss +815 -0
- package/src/timezone/index.test.ts +100 -0
- package/src/timezone/index.ts +298 -0
- package/src/timezone/style.scss +16 -0
- package/src/timezone/timezones.json +58 -0
- package/src/toc/README.md +3 -0
- package/src/toc/index.ts +322 -0
- package/src/toc/style.scss +203 -0
- package/src/top/README.md +4 -0
- package/src/top/index.ts +75 -0
- package/src/util.ts +122 -0
- package/templates/_author.html +27 -0
- package/templates/_bottom.html +3 -0
- package/templates/_download.html +1 -0
- package/templates/_heading.html +19 -0
- package/templates/_nav.html +18 -0
- package/templates/_theme.scss +97 -0
- package/templates/_top.html +87 -0
- package/templates/authors.schema.json +13 -0
- package/templates/code.html +31 -0
- package/templates/default.html +13 -0
- package/templates/literate.html +16 -0
- package/templates/nav.schema.json +27 -0
- package/tsconfig.json +15 -0
- package/types/dev.ts +3 -0
- package/types/sass.d.ts +1 -0
- package/types/site-variables.d.ts +16 -0
- package/webpack/apply-base-path-plugin.js +78 -0
- package/webpack/build-state.js +97 -0
- package/webpack/code.test.js +162 -0
- package/webpack/colors.js +15 -0
- package/webpack/config.base.js +147 -0
- package/webpack/config.dev.js +23 -0
- package/webpack/config.prod.js +32 -0
- package/webpack/content-watch-plugin.js +153 -0
- package/webpack/deflist-id-plugin.js +62 -0
- package/webpack/external-links-plugin.js +37 -0
- package/webpack/features.js +5 -0
- package/webpack/flair.json +1 -0
- package/webpack/generate-content-assets-plugin.js +308 -0
- package/webpack/generate-favicon-plugin.js +198 -0
- package/webpack/generate-fonts-plugin.js +69 -0
- package/webpack/generate-manifest-plugin.js +116 -0
- package/webpack/globals.js +74 -0
- package/webpack/heading-subtitle-plugin.js +80 -0
- package/webpack/json-schema.js +19 -0
- package/webpack/log.js +143 -0
- package/webpack/markdown-plugins.test.js +203 -0
- package/webpack/pagefind-plugin.js +379 -0
- package/webpack/pagefind-plugin.test.js +131 -0
- package/webpack/pdf-text.js +163 -0
- package/webpack/print-flair-plugin.js +22 -0
- package/webpack/reachability.js +273 -0
- package/webpack/reachability.test.js +80 -0
- package/webpack/serve.js +104 -0
- package/webpack/site-variables.js +53 -0
- package/webpack/site.schema.json +67 -0
- package/webpack/templates.js +128 -0
- package/webpack/text-to-id.js +8 -0
- package/webpack/toc-plugin.js +167 -0
- package/webpack/util.js +49 -0
- package/webpack/utils/code.js +439 -0
- package/webpack/utils/content-files.js +147 -0
- package/webpack/utils/define-plugin.js +20 -0
- package/webpack/utils/file-types.js +26 -0
- package/webpack/utils/front-matter.js +57 -0
- package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
- package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
- package/webpack/utils/literate-java.js +153 -0
- package/webpack/utils/markdown.js +244 -0
- package/webpack/utils/parse-hsl.js +8 -0
- package/webpack/utils/paths.js +58 -0
- package/webpack/utils/render.js +466 -0
- package/webpack/utils/shiki-highlighter.js +26 -0
- package/webpack/validate-internal-links-plugin.js +155 -0
- package/webpack/watch-reachability-state.js +273 -0
- package/webpack/watch-reachability-state.test.js +198 -0
- package/webpack/watch-reload-client.js +54 -0
- package/webpack/watch.js +166 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const { stripHtml } = require('string-strip-html');
|
|
5
|
+
const { makeLogger } = require('../log');
|
|
6
|
+
const { B } = require('../colors');
|
|
7
|
+
const createGlobals = require('../globals');
|
|
8
|
+
const { render, json } = require('../templates');
|
|
9
|
+
const {
|
|
10
|
+
extractJavaMethodToc,
|
|
11
|
+
renderCodeSegment,
|
|
12
|
+
renderCodeWithComments,
|
|
13
|
+
} = require('./code');
|
|
14
|
+
const { extensionIsMarkdown } = require('./file-types');
|
|
15
|
+
const { createApplyBasePath, normalizeOutputPath } = require('./paths');
|
|
16
|
+
const { parseFrontMatterAndContent } = require('./front-matter');
|
|
17
|
+
const { createMarkdown } = require('./markdown');
|
|
18
|
+
const { generateTocHtml, generateCodeTocHtml } = require('../toc-plugin');
|
|
19
|
+
const {
|
|
20
|
+
parseLiterateJava,
|
|
21
|
+
hasMainMethod,
|
|
22
|
+
deriveClassName,
|
|
23
|
+
compileJavaSource,
|
|
24
|
+
executeLiterateJava,
|
|
25
|
+
} = require('./literate-java');
|
|
26
|
+
|
|
27
|
+
const log = makeLogger(__filename);
|
|
28
|
+
|
|
29
|
+
const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
|
|
30
|
+
|
|
31
|
+
function resolveAuthor(pageVariables, filePath) {
|
|
32
|
+
if (!pageVariables.author) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const authors = json('authors.json');
|
|
36
|
+
const authorKey = pageVariables.author;
|
|
37
|
+
const authorEntry = authors[authorKey];
|
|
38
|
+
if (!authorEntry) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`${filePath}: unknown author "${authorKey}" (not found in authors.json)`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
pageVariables.author = authorEntry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateFrontMatter(pageVariables, filePath) {
|
|
47
|
+
let valid = true;
|
|
48
|
+
for (const field of REQUIRED_FRONT_MATTER_FIELDS) {
|
|
49
|
+
if (!pageVariables[field]) {
|
|
50
|
+
log.error`${filePath}: missing required front matter field: "${field}"`;
|
|
51
|
+
valid = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return valid;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createTemplateParameters({
|
|
58
|
+
pageVariables,
|
|
59
|
+
siteVariables,
|
|
60
|
+
content,
|
|
61
|
+
applyBasePath,
|
|
62
|
+
subPath,
|
|
63
|
+
}) {
|
|
64
|
+
return {
|
|
65
|
+
...(siteVariables.vars || {}),
|
|
66
|
+
...createGlobals(pageVariables, siteVariables, subPath),
|
|
67
|
+
site: siteVariables,
|
|
68
|
+
base: siteVariables.base,
|
|
69
|
+
basePath: siteVariables.basePath,
|
|
70
|
+
page: pageVariables,
|
|
71
|
+
content,
|
|
72
|
+
applyBasePath,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function injectWebpackAssets(html, compilation, applyBasePath) {
|
|
77
|
+
const assets = compilation.getAssets();
|
|
78
|
+
const jsAssets = assets
|
|
79
|
+
.filter(asset => asset.name.endsWith('.js'))
|
|
80
|
+
.map(asset => asset.name);
|
|
81
|
+
const cssAssets = assets
|
|
82
|
+
.filter(asset => asset.name.endsWith('.css'))
|
|
83
|
+
.map(asset => asset.name);
|
|
84
|
+
|
|
85
|
+
const scriptTags = jsAssets
|
|
86
|
+
.map(asset => `<script defer src="${applyBasePath('/' + asset)}"></script>`)
|
|
87
|
+
.join('');
|
|
88
|
+
const linkTags = cssAssets
|
|
89
|
+
.map(
|
|
90
|
+
asset => `<link href="${applyBasePath('/' + asset)}" rel="stylesheet">`,
|
|
91
|
+
)
|
|
92
|
+
.join('');
|
|
93
|
+
|
|
94
|
+
return html
|
|
95
|
+
.replace('<head>', `<head>${linkTags}`)
|
|
96
|
+
.replace('</head>', `${scriptTags}</head>`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toContentAssetPath(contentDir, filePath) {
|
|
100
|
+
return path
|
|
101
|
+
.relative(contentDir, filePath)
|
|
102
|
+
.split(path.sep)
|
|
103
|
+
.join(path.posix.sep);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderPlainTextPageAsset({
|
|
107
|
+
filePath,
|
|
108
|
+
contentDir,
|
|
109
|
+
siteVariables,
|
|
110
|
+
validInternalTargets,
|
|
111
|
+
compilation,
|
|
112
|
+
}) {
|
|
113
|
+
const { dir, name, ext } = path.parse(filePath);
|
|
114
|
+
const subPath = path.relative(contentDir, path.join(dir, name));
|
|
115
|
+
const applyBasePath = createApplyBasePath(siteVariables);
|
|
116
|
+
|
|
117
|
+
log.info`Rendering page ${B`${subPath + ext}`}`;
|
|
118
|
+
const { content, pageVariables, tocItems } = renderPlainTextContent(
|
|
119
|
+
filePath,
|
|
120
|
+
subPath,
|
|
121
|
+
siteVariables,
|
|
122
|
+
applyBasePath,
|
|
123
|
+
validInternalTargets,
|
|
124
|
+
{ validateInternalLinks: extensionIsMarkdown(ext.toLowerCase()) },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!validateFrontMatter(pageVariables, filePath)) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!pageVariables.template) {
|
|
132
|
+
pageVariables.template = 'default';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (pageVariables.toc && tocItems) {
|
|
136
|
+
pageVariables.tocHtml = generateTocHtml(tocItems);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const templateParameters = createTemplateParameters({
|
|
140
|
+
pageVariables,
|
|
141
|
+
siteVariables,
|
|
142
|
+
content,
|
|
143
|
+
applyBasePath,
|
|
144
|
+
subPath,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const html = injectWebpackAssets(
|
|
148
|
+
render(`${pageVariables.template}.html`, templateParameters),
|
|
149
|
+
compilation,
|
|
150
|
+
applyBasePath,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
assetPath: toContentAssetPath(contentDir, path.join(dir, `${name}.html`)),
|
|
156
|
+
content: html,
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderCodePageAsset({
|
|
162
|
+
filePath,
|
|
163
|
+
contentDir,
|
|
164
|
+
siteVariables,
|
|
165
|
+
compilation,
|
|
166
|
+
}) {
|
|
167
|
+
const { dir, name, ext } = path.parse(filePath);
|
|
168
|
+
const subPath = path.relative(contentDir, path.join(dir, name));
|
|
169
|
+
const applyBasePath = createApplyBasePath(siteVariables);
|
|
170
|
+
const lang = siteVariables.codeLanguages[ext.slice(1).toLowerCase()];
|
|
171
|
+
const sourceCode = fs.readFileSync(filePath, 'utf-8');
|
|
172
|
+
|
|
173
|
+
log.info`Rendering code page ${B`${subPath + ext}`}`;
|
|
174
|
+
const content = renderCodeWithComments(sourceCode, lang, siteVariables);
|
|
175
|
+
const codeFilePath = applyBasePath(
|
|
176
|
+
normalizeOutputPath(`/${toContentAssetPath(contentDir, filePath)}`),
|
|
177
|
+
);
|
|
178
|
+
const titleHtml = `<tt>${name + ext}</tt>`;
|
|
179
|
+
const tocItems = lang === 'java' ? extractJavaMethodToc(sourceCode) : [];
|
|
180
|
+
const tocHtml = generateCodeTocHtml(tocItems);
|
|
181
|
+
const pageVariables = {
|
|
182
|
+
template: 'code',
|
|
183
|
+
filePath,
|
|
184
|
+
title: `${name}${ext}`,
|
|
185
|
+
titleHtml,
|
|
186
|
+
codeFilePath,
|
|
187
|
+
downloadName: `${name}${ext}`,
|
|
188
|
+
tocItems,
|
|
189
|
+
tocHtml,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const templateParameters = createTemplateParameters({
|
|
193
|
+
pageVariables,
|
|
194
|
+
siteVariables,
|
|
195
|
+
content,
|
|
196
|
+
applyBasePath,
|
|
197
|
+
subPath,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const html = injectWebpackAssets(
|
|
201
|
+
render('code.html', templateParameters),
|
|
202
|
+
compilation,
|
|
203
|
+
applyBasePath,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return [
|
|
207
|
+
{
|
|
208
|
+
assetPath: toContentAssetPath(contentDir, path.join(dir, `${name}.html`)),
|
|
209
|
+
content: html,
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderCopiedContentAsset({ filePath, contentDir }) {
|
|
215
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
216
|
+
const label = ext === '.pdf' ? 'Copying' : 'Copying source file';
|
|
217
|
+
const relPath = toContentAssetPath(contentDir, filePath);
|
|
218
|
+
|
|
219
|
+
log.info`${label} ${B`${relPath}`}`;
|
|
220
|
+
return [{ assetPath: relPath, content: fs.readFileSync(filePath) }];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Parses the file, renders using template, returns HTML & params used to generate page */
|
|
224
|
+
function renderPlainTextContent(
|
|
225
|
+
filePath,
|
|
226
|
+
subPath,
|
|
227
|
+
siteVariables,
|
|
228
|
+
applyBasePath,
|
|
229
|
+
validInternalTargets,
|
|
230
|
+
{ validateInternalLinks = true } = {},
|
|
231
|
+
) {
|
|
232
|
+
const sourceUrlPath = `/${subPath}.html`;
|
|
233
|
+
const md = createMarkdown(siteVariables, {
|
|
234
|
+
validatorOptions: {
|
|
235
|
+
enabled: validateInternalLinks,
|
|
236
|
+
filePath,
|
|
237
|
+
sourceUrlPath,
|
|
238
|
+
validTargets: validInternalTargets,
|
|
239
|
+
codeExtensions:
|
|
240
|
+
siteVariables.features?.code === false
|
|
241
|
+
? []
|
|
242
|
+
: Object.keys(siteVariables.codeLanguages),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const ext = path.extname(filePath);
|
|
247
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
248
|
+
|
|
249
|
+
const { pageVariables, content } = parseFrontMatterAndContent(raw, ext);
|
|
250
|
+
|
|
251
|
+
// Handle substitutions inside front matter using siteVariables
|
|
252
|
+
const siteOnlyParams = createTemplateParameters({
|
|
253
|
+
pageVariables: {},
|
|
254
|
+
siteVariables,
|
|
255
|
+
content: null,
|
|
256
|
+
applyBasePath,
|
|
257
|
+
subPath,
|
|
258
|
+
});
|
|
259
|
+
const pageVariablesProcessed = Object.entries(pageVariables)
|
|
260
|
+
.map(([k, v]) => {
|
|
261
|
+
const newValue =
|
|
262
|
+
typeof v === 'string' ? _.template(v)(siteOnlyParams) : v;
|
|
263
|
+
return [k, newValue];
|
|
264
|
+
})
|
|
265
|
+
.reduce((acc, [k, v]) => {
|
|
266
|
+
acc[k] = v;
|
|
267
|
+
return acc;
|
|
268
|
+
}, {});
|
|
269
|
+
|
|
270
|
+
// Render title and description as inline Markdown
|
|
271
|
+
if (pageVariablesProcessed.title) {
|
|
272
|
+
const titleHtml = md.renderInline(pageVariablesProcessed.title);
|
|
273
|
+
pageVariablesProcessed.titleHtml = titleHtml;
|
|
274
|
+
pageVariablesProcessed.title = stripHtml(titleHtml).result;
|
|
275
|
+
}
|
|
276
|
+
if (pageVariablesProcessed.description) {
|
|
277
|
+
const descriptionHtml = md.renderInline(pageVariablesProcessed.description);
|
|
278
|
+
pageVariablesProcessed.descriptionHtml = descriptionHtml;
|
|
279
|
+
pageVariablesProcessed.description = stripHtml(descriptionHtml).result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
resolveAuthor(pageVariablesProcessed, filePath);
|
|
283
|
+
|
|
284
|
+
const strippedContent = stripHtmlComments(content);
|
|
285
|
+
|
|
286
|
+
const params = createTemplateParameters({
|
|
287
|
+
pageVariables: pageVariablesProcessed,
|
|
288
|
+
siteVariables,
|
|
289
|
+
content: strippedContent,
|
|
290
|
+
applyBasePath,
|
|
291
|
+
subPath,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
let html = null;
|
|
295
|
+
try {
|
|
296
|
+
html = _.template(strippedContent)(params);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`${filePath}: Lodash template error in page or template: ${err.message}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let tocItems = null;
|
|
304
|
+
if (extensionIsMarkdown(ext)) {
|
|
305
|
+
const env = {};
|
|
306
|
+
html = md.render(html, env);
|
|
307
|
+
tocItems = env.tocItems || null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { content: html, pageVariables: params.page, tocItems };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function stripHtmlComments(str) {
|
|
314
|
+
return str.replace(/<!---[\s\S]*?-->/g, '');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderLiterateJavaPageAsset({
|
|
318
|
+
filePath,
|
|
319
|
+
contentDir,
|
|
320
|
+
siteVariables,
|
|
321
|
+
compilation,
|
|
322
|
+
}) {
|
|
323
|
+
const { dir, name } = path.parse(filePath);
|
|
324
|
+
const className = deriveClassName(filePath);
|
|
325
|
+
const subPath = path.relative(contentDir, path.join(dir, className));
|
|
326
|
+
const applyBasePath = createApplyBasePath(siteVariables);
|
|
327
|
+
|
|
328
|
+
log.info`Rendering literate Java page ${B`${name}`}`;
|
|
329
|
+
|
|
330
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
331
|
+
const {
|
|
332
|
+
pageVariables,
|
|
333
|
+
content,
|
|
334
|
+
javaSource,
|
|
335
|
+
codeBlocks,
|
|
336
|
+
visibleBlockIndices,
|
|
337
|
+
} = parseLiterateJava(raw, siteVariables);
|
|
338
|
+
|
|
339
|
+
if (!validateFrontMatter(pageVariables, filePath)) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Compile the concatenated Java source
|
|
344
|
+
let tempDir;
|
|
345
|
+
let blockOutputMap = null;
|
|
346
|
+
try {
|
|
347
|
+
tempDir = compileJavaSource(javaSource, className);
|
|
348
|
+
|
|
349
|
+
// Execute if there is a main() method
|
|
350
|
+
if (hasMainMethod(javaSource)) {
|
|
351
|
+
const outputEntries = executeLiterateJava(className, tempDir, codeBlocks);
|
|
352
|
+
blockOutputMap = new Map(outputEntries);
|
|
353
|
+
}
|
|
354
|
+
} finally {
|
|
355
|
+
if (tempDir) {
|
|
356
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Render full markdown with a custom fence rule that replaces fences
|
|
361
|
+
// with Shiki-highlighted code blocks and optional JDI output columns
|
|
362
|
+
const md = createMarkdown(siteVariables, {
|
|
363
|
+
validatorOptions: { enabled: false },
|
|
364
|
+
});
|
|
365
|
+
let fenceIndex = 0;
|
|
366
|
+
|
|
367
|
+
md.renderer.rules.fence = (tokens, idx) => {
|
|
368
|
+
const token = tokens[idx];
|
|
369
|
+
const code = token.content;
|
|
370
|
+
const lines = code.endsWith('\n')
|
|
371
|
+
? code.slice(0, -1).split('\n')
|
|
372
|
+
: code.split('\n');
|
|
373
|
+
|
|
374
|
+
// Dedent: strip common leading whitespace for display
|
|
375
|
+
const minIndent = lines.reduce((min, line) => {
|
|
376
|
+
if (line.trim().length === 0) {
|
|
377
|
+
return min;
|
|
378
|
+
}
|
|
379
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
380
|
+
return Math.min(min, indent);
|
|
381
|
+
}, Infinity);
|
|
382
|
+
const dedented =
|
|
383
|
+
minIndent > 0 && minIndent < Infinity
|
|
384
|
+
? lines.map(l => l.slice(minIndent))
|
|
385
|
+
: lines;
|
|
386
|
+
|
|
387
|
+
const blockIdx = visibleBlockIndices[fenceIndex++];
|
|
388
|
+
const startLine = codeBlocks[blockIdx].javaStartLine;
|
|
389
|
+
|
|
390
|
+
const codeHtml = renderCodeSegment(dedented, startLine, 'java');
|
|
391
|
+
const output =
|
|
392
|
+
blockOutputMap && blockOutputMap.has(blockIdx)
|
|
393
|
+
? blockOutputMap.get(blockIdx)
|
|
394
|
+
: null;
|
|
395
|
+
|
|
396
|
+
if (output) {
|
|
397
|
+
const escapedOutput = output
|
|
398
|
+
.replace(/&/g, '&')
|
|
399
|
+
.replace(/</g, '<')
|
|
400
|
+
.replace(/>/g, '>');
|
|
401
|
+
return `<div class="literate-code-output">${codeHtml}<pre>${escapedOutput}</pre></div>`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return codeHtml;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const env = {};
|
|
408
|
+
const contentHtml = md.render(content, env);
|
|
409
|
+
|
|
410
|
+
// Build page variables
|
|
411
|
+
const javaFileName = `${className}.java`;
|
|
412
|
+
const codeFilePath = applyBasePath(
|
|
413
|
+
normalizeOutputPath(
|
|
414
|
+
`/${toContentAssetPath(contentDir, path.join(dir, javaFileName))}`,
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const titleHtml = md.renderInline(pageVariables.title);
|
|
419
|
+
pageVariables.titleHtml = titleHtml;
|
|
420
|
+
pageVariables.title = stripHtml(titleHtml).result;
|
|
421
|
+
pageVariables.template = 'literate';
|
|
422
|
+
pageVariables.codeFilePath = codeFilePath;
|
|
423
|
+
pageVariables.downloadName = javaFileName;
|
|
424
|
+
|
|
425
|
+
if (pageVariables.toc && env.tocItems) {
|
|
426
|
+
pageVariables.tocHtml = generateTocHtml(env.tocItems);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
resolveAuthor(pageVariables, filePath);
|
|
430
|
+
|
|
431
|
+
const templateParameters = createTemplateParameters({
|
|
432
|
+
pageVariables,
|
|
433
|
+
siteVariables,
|
|
434
|
+
content: contentHtml,
|
|
435
|
+
applyBasePath,
|
|
436
|
+
subPath,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const html = injectWebpackAssets(
|
|
440
|
+
render('literate.html', templateParameters),
|
|
441
|
+
compilation,
|
|
442
|
+
applyBasePath,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
return [
|
|
446
|
+
{
|
|
447
|
+
assetPath: toContentAssetPath(
|
|
448
|
+
contentDir,
|
|
449
|
+
path.join(dir, `${className}.html`),
|
|
450
|
+
),
|
|
451
|
+
content: html,
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
assetPath: toContentAssetPath(contentDir, path.join(dir, javaFileName)),
|
|
455
|
+
content: javaSource,
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
module.exports = {
|
|
461
|
+
injectWebpackAssets,
|
|
462
|
+
renderCodePageAsset,
|
|
463
|
+
renderCopiedContentAsset,
|
|
464
|
+
renderLiterateJavaPageAsset,
|
|
465
|
+
renderPlainTextPageAsset,
|
|
466
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { makeLogger } = require('../log');
|
|
2
|
+
|
|
3
|
+
const log = makeLogger(__filename);
|
|
4
|
+
|
|
5
|
+
let highlighter = null;
|
|
6
|
+
|
|
7
|
+
async function initHighlighter(langs) {
|
|
8
|
+
if (highlighter) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
log.info`Initializing syntax highlighter`;
|
|
12
|
+
const { createHighlighter } = await import('shiki');
|
|
13
|
+
highlighter = await createHighlighter({
|
|
14
|
+
themes: ['github-light', 'github-dark'],
|
|
15
|
+
langs,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getHighlighter() {
|
|
20
|
+
if (!highlighter) {
|
|
21
|
+
throw new Error('Shiki highlighter not initialized');
|
|
22
|
+
}
|
|
23
|
+
return highlighter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { initHighlighter, getHighlighter };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { makeLogger } = require('./log');
|
|
3
|
+
|
|
4
|
+
const log = makeLogger(__filename);
|
|
5
|
+
|
|
6
|
+
function stripQueryAndHash(href) {
|
|
7
|
+
return href.split('#')[0].split('?')[0];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isExternalOrAnchor(href) {
|
|
11
|
+
if (!href || href.startsWith('#') || href.startsWith('//')) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizePathname(pathname) {
|
|
19
|
+
const normalized = path.posix.normalize(pathname);
|
|
20
|
+
if (normalized === '.') {
|
|
21
|
+
return '/';
|
|
22
|
+
}
|
|
23
|
+
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createCodeExtPattern(codeExtensions) {
|
|
27
|
+
const escaped = codeExtensions.map(ext =>
|
|
28
|
+
ext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
|
29
|
+
);
|
|
30
|
+
if (escaped.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return new RegExp(`\\.(${escaped.join('|')})$`, 'i');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rewriteCodeLink(pathname, codeExtPattern) {
|
|
37
|
+
if (!codeExtPattern) {
|
|
38
|
+
return pathname;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return pathname.replace(codeExtPattern, '.html');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveLinkPath(sourceUrlPath, rawHref, codeExtPattern) {
|
|
45
|
+
const hrefPath = stripQueryAndHash(rawHref.trim());
|
|
46
|
+
if (!hrefPath) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sourceDir = path.posix.dirname(sourceUrlPath);
|
|
51
|
+
const resolved = hrefPath.startsWith('/')
|
|
52
|
+
? normalizePathname(hrefPath)
|
|
53
|
+
: normalizePathname(path.posix.join(sourceDir, hrefPath));
|
|
54
|
+
|
|
55
|
+
return rewriteCodeLink(resolved, codeExtPattern);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getDirectoryIndexPath(pathname) {
|
|
59
|
+
return normalizePathname(path.posix.join(pathname, 'index.html'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = function validateInternalLinks(md, options = {}) {
|
|
63
|
+
const {
|
|
64
|
+
enabled = true,
|
|
65
|
+
filePath,
|
|
66
|
+
sourceUrlPath,
|
|
67
|
+
validTargets,
|
|
68
|
+
codeExtensions = [],
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
if (!enabled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!filePath || !sourceUrlPath || !(validTargets instanceof Set)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'validate-internal-links-plugin requires filePath, sourceUrlPath, and validTargets',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const codeExtPattern = createCodeExtPattern(codeExtensions);
|
|
82
|
+
const seenErrors = new Set();
|
|
83
|
+
|
|
84
|
+
function reportBrokenLink(rawHref, resolvedPath) {
|
|
85
|
+
const key = `${rawHref}|${resolvedPath}`;
|
|
86
|
+
if (seenErrors.has(key)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
seenErrors.add(key);
|
|
90
|
+
|
|
91
|
+
log.error`${filePath}: broken internal link: "${rawHref}" (resolved to "${resolvedPath}")`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function reportDirectoryLink(rawHref, resolvedPath, indexPath) {
|
|
95
|
+
const key = `${rawHref}|${resolvedPath}|directory`;
|
|
96
|
+
if (seenErrors.has(key)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
seenErrors.add(key);
|
|
100
|
+
|
|
101
|
+
log.error`${filePath}: directory link must reference index.html explicitly: "${rawHref}" (resolved to "${resolvedPath}", expected "${indexPath}")`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateHref(rawHref) {
|
|
105
|
+
if (!rawHref) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const href = rawHref.trim();
|
|
110
|
+
if (!href || isExternalOrAnchor(href)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const resolvedPath = resolveLinkPath(sourceUrlPath, href, codeExtPattern);
|
|
115
|
+
if (!resolvedPath) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const directoryIndexPath = getDirectoryIndexPath(resolvedPath);
|
|
120
|
+
if (
|
|
121
|
+
directoryIndexPath !== resolvedPath &&
|
|
122
|
+
validTargets.has(directoryIndexPath)
|
|
123
|
+
) {
|
|
124
|
+
reportDirectoryLink(rawHref, resolvedPath, directoryIndexPath);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!validTargets.has(resolvedPath)) {
|
|
129
|
+
reportBrokenLink(rawHref, resolvedPath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function validateToken(token) {
|
|
134
|
+
if (token.type === 'link_open') {
|
|
135
|
+
validateHref(token.attrGet('href'));
|
|
136
|
+
} else if (token.type === 'html_block' || token.type === 'html_inline') {
|
|
137
|
+
token.content.replace(/<a\b[^>]*\bhref\s*=\s*"([^"]+)"/gi, (_, href) => {
|
|
138
|
+
validateHref(href);
|
|
139
|
+
return _;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
token.children?.forEach(validateToken);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
md.core.ruler.push('validate_internal_links', state => {
|
|
147
|
+
state.tokens.forEach(validateToken);
|
|
148
|
+
|
|
149
|
+
if (seenErrors.size > 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`${filePath}: found ${seenErrors.size} broken internal link(s)`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|