@abreen/tada 1.10.2 → 1.11.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 +11 -4
- package/bin/tada.ts +6 -2
- package/bin/validators.ts +4 -1
- package/build/apply-base-path-plugin.ts +30 -27
- package/build/content-watch.ts +2 -1
- package/build/generate-content-assets.ts +20 -30
- package/build/pipeline.ts +2 -1
- package/build/site-variables.ts +103 -2
- package/build/types.d.ts +13 -2
- package/build/utils/code.ts +14 -9
- package/build/utils/content-files.ts +3 -12
- package/build/utils/markdown.ts +22 -10
- package/build/utils/render.ts +15 -12
- package/build/utils/shiki-highlighter.ts +63 -9
- package/build/utils/source-template.ts +2 -2
- package/build/watch/assets.ts +2 -8
- package/build/watch/records.ts +16 -16
- package/build/watch/snapshot.ts +6 -22
- package/init/content/lectures/01/index.md +5 -3
- package/init/content/markdown.md +8 -7
- package/init/public/Test.java +2 -2
- package/package.json +1 -1
- package/schema/site.schema.json +4 -9
- package/src/page-update/index.ts +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,12 @@ Install Tada globally:
|
|
|
67
67
|
bun install --global @abreen/tada
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
To update Tada to the latest version:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
bun update --global @abreen/tada
|
|
74
|
+
```
|
|
75
|
+
|
|
70
76
|
## Quick start
|
|
71
77
|
|
|
72
78
|
Create a new site:
|
|
@@ -204,12 +210,13 @@ Example site configuration JSON file:
|
|
|
204
210
|
"symbol": "CS 0",
|
|
205
211
|
"themeColor": "hsl(351 70% 40%)",
|
|
206
212
|
"tintAmount": 0,
|
|
207
|
-
"features": { "search": true, "
|
|
213
|
+
"features": { "search": true, "favicon": true },
|
|
208
214
|
"base": "https://example.edu",
|
|
209
215
|
"basePath": "/cs0",
|
|
210
216
|
"internalDomains": ["example.edu"],
|
|
211
217
|
"defaultTimeZone": "America/New_York",
|
|
212
|
-
"
|
|
218
|
+
"extensionToShikiLanguage": { "java": "java", "py": "python" },
|
|
219
|
+
"shikiLanguages": ["java", "python"],
|
|
213
220
|
"vars": {
|
|
214
221
|
"staffEmail": "staff@example.edu"
|
|
215
222
|
}
|
|
@@ -226,13 +233,13 @@ Example site configuration JSON file:
|
|
|
226
233
|
| `tintAmount` | *Optional*, percentage (0-100) of tint to apply (default `100`) |
|
|
227
234
|
| `faviconSymbol` | *Optional*, the text to use instead of `symbol` in the favicon |
|
|
228
235
|
| `features.search` | Enable search UI and Pagefind index generation |
|
|
229
|
-
| `features.code` | Enable generated source-code HTML pages for configured code extensions |
|
|
230
236
|
| `features.favicon` | Enable automatically generated favicons |
|
|
231
237
|
| `features.footer` | Show the Tada footer at the bottom of every page |
|
|
232
238
|
| `base` | Full base URL of the deployed site, used for metadata and URL generation |
|
|
233
239
|
| `basePath` | URL prefix for deployment under a subpath (e.g., `"/cs101"`), use `"/"` at root |
|
|
234
240
|
| `internalDomains` | Domain names treated as internal by link processing (not marked external) |
|
|
235
|
-
| `
|
|
241
|
+
| `extensionToShikiLanguage` | *Optional*, map from source-file extension to the Shiki language used for generated code pages and copied-source processing |
|
|
242
|
+
| `shikiLanguages` | *Optional*, list of Shiki languages permitted in code blocks |
|
|
236
243
|
| `faviconColor` | *Optional*, background color for favicon (defaults to `themeColor`) |
|
|
237
244
|
| `faviconFontWeight` | *Optional*, font weight used for favicon text (default `700`) |
|
|
238
245
|
| `vars` | Arbitrary key/value variables exposed to templates/content as `vars.*` (e.g., `<%= vars.staffEmail %>`) |
|
package/bin/tada.ts
CHANGED
|
@@ -332,7 +332,9 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
332
332
|
base: 'http://localhost:8080',
|
|
333
333
|
basePath: '/',
|
|
334
334
|
internalDomains: ['localhost'],
|
|
335
|
-
features: { search: true,
|
|
335
|
+
features: { search: true, favicon: false, footer: true },
|
|
336
|
+
extensionToShikiLanguage: bare ? {} : { java: 'java', py: 'python' },
|
|
337
|
+
shikiLanguages: bare ? [] : ['java', 'python', 'html'],
|
|
336
338
|
});
|
|
337
339
|
|
|
338
340
|
const prodConfig = createSiteConfig({
|
|
@@ -345,7 +347,9 @@ async function initCommand(args: string[]): Promise<void> {
|
|
|
345
347
|
base: prodBase,
|
|
346
348
|
basePath: prodBasePath,
|
|
347
349
|
internalDomains: [prodDomain],
|
|
348
|
-
features: { search: true,
|
|
350
|
+
features: { search: true, favicon: true, footer: true },
|
|
351
|
+
extensionToShikiLanguage: bare ? {} : { java: 'java', py: 'python' },
|
|
352
|
+
shikiLanguages: bare ? [] : ['java', 'python', 'html'],
|
|
349
353
|
});
|
|
350
354
|
|
|
351
355
|
fs.writeFileSync(
|
package/bin/validators.ts
CHANGED
|
@@ -66,6 +66,8 @@ export function createSiteConfig({
|
|
|
66
66
|
basePath,
|
|
67
67
|
internalDomains,
|
|
68
68
|
features,
|
|
69
|
+
extensionToShikiLanguage,
|
|
70
|
+
shikiLanguages,
|
|
69
71
|
}: SiteConfigInput): SiteVariables {
|
|
70
72
|
return {
|
|
71
73
|
title,
|
|
@@ -76,7 +78,8 @@ export function createSiteConfig({
|
|
|
76
78
|
basePath,
|
|
77
79
|
internalDomains,
|
|
78
80
|
defaultTimeZone,
|
|
79
|
-
|
|
81
|
+
extensionToShikiLanguage,
|
|
82
|
+
shikiLanguages,
|
|
80
83
|
themeColor,
|
|
81
84
|
tintHue: Number(tintHue),
|
|
82
85
|
tintAmount: Number(tintAmount),
|
|
@@ -3,7 +3,7 @@ import type MarkdownIt from 'markdown-it';
|
|
|
3
3
|
import type Token from 'markdown-it/lib/token.mjs';
|
|
4
4
|
import { JSDOM } from 'jsdom';
|
|
5
5
|
import { createApplyBasePath } from './utils/paths';
|
|
6
|
-
import {
|
|
6
|
+
import { getExtensionToShikiLanguage } from './site-variables';
|
|
7
7
|
import { isInternalLink } from './utils/link';
|
|
8
8
|
import { makeLogger } from './log';
|
|
9
9
|
import type { SiteVariables } from './types';
|
|
@@ -63,7 +63,6 @@ export default function applyBasePathPlugin(
|
|
|
63
63
|
pluginOptions: ApplyBasePathOptions = {},
|
|
64
64
|
): void {
|
|
65
65
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
66
|
-
const rewriteCodeLinks = isFeatureEnabled(siteVariables, 'code');
|
|
67
66
|
const literateJavaOutputPaths = pluginOptions.literateJavaOutputPaths;
|
|
68
67
|
const sourceUrlPath = pluginOptions.sourceUrlPath;
|
|
69
68
|
const validTargets = pluginOptions.validTargets;
|
|
@@ -79,26 +78,24 @@ export default function applyBasePathPlugin(
|
|
|
79
78
|
const suffix = match ? match[2] : '';
|
|
80
79
|
let modifiedPath = pathname;
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
81
|
+
for (const ext of Object.keys(getExtensionToShikiLanguage(siteVariables))) {
|
|
82
|
+
if (modifiedPath.endsWith(`.${ext}`)) {
|
|
83
|
+
if (literateJavaOutputPaths && sourceUrlPath) {
|
|
84
|
+
const resolved = resolveAgainstSource(modifiedPath);
|
|
85
|
+
if (literateJavaOutputPaths.has(resolved)) {
|
|
86
|
+
break;
|
|
90
87
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
}
|
|
89
|
+
// Only rewrite if the .html version exists. Public files with
|
|
90
|
+
// code extensions are copied as-is and have no .html page.
|
|
91
|
+
if (validTargets && sourceUrlPath) {
|
|
92
|
+
const resolved = resolveAgainstSource(modifiedPath);
|
|
93
|
+
if (!validTargets.has(`${resolved}.html`)) {
|
|
94
|
+
break;
|
|
98
95
|
}
|
|
99
|
-
modifiedPath += '.html';
|
|
100
|
-
break;
|
|
101
96
|
}
|
|
97
|
+
modifiedPath += '.html';
|
|
98
|
+
break;
|
|
102
99
|
}
|
|
103
100
|
}
|
|
104
101
|
|
|
@@ -137,10 +134,13 @@ export default function applyBasePathPlugin(
|
|
|
137
134
|
const dom = new JSDOM(`<body>${token.content}__tada__</a></body>`);
|
|
138
135
|
const anchor = dom.window.document.body.querySelector('a[href]');
|
|
139
136
|
const href = anchor?.getAttribute('href');
|
|
140
|
-
if (anchor && href && href
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
if (anchor && href && isInternalLink(href)) {
|
|
138
|
+
const rewrittenHref = rewriteInternalHref(href);
|
|
139
|
+
const finalHref = href.startsWith('/')
|
|
140
|
+
? applyBasePath(rewrittenHref)
|
|
141
|
+
: rewrittenHref;
|
|
142
|
+
log.debug`Rewriting raw HTML anchor: ${href} -> ${finalHref}`;
|
|
143
|
+
anchor.setAttribute('href', finalHref);
|
|
144
144
|
token.content = serializeStartTag(anchor);
|
|
145
145
|
}
|
|
146
146
|
} else {
|
|
@@ -150,10 +150,13 @@ export default function applyBasePathPlugin(
|
|
|
150
150
|
|
|
151
151
|
for (const anchor of document.body.querySelectorAll('a[href]')) {
|
|
152
152
|
const href = anchor.getAttribute('href');
|
|
153
|
-
if (href && href
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
if (href && isInternalLink(href)) {
|
|
154
|
+
const rewrittenHref = rewriteInternalHref(href);
|
|
155
|
+
const finalHref = href.startsWith('/')
|
|
156
|
+
? applyBasePath(rewrittenHref)
|
|
157
|
+
: rewrittenHref;
|
|
158
|
+
log.debug`Rewriting raw HTML anchor: ${href} -> ${finalHref}`;
|
|
159
|
+
anchor.setAttribute('href', finalHref);
|
|
157
160
|
changed = true;
|
|
158
161
|
}
|
|
159
162
|
}
|
package/build/content-watch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import type { SiteVariables } from './types';
|
|
3
|
+
import { getExtensionToShikiLanguage } from './site-variables';
|
|
3
4
|
import { getContentDir, getBuildContentFiles, isPartial } from './util';
|
|
4
5
|
import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
|
|
5
6
|
import { B } from './colors';
|
|
@@ -47,7 +48,7 @@ export class ContentChangeDetector {
|
|
|
47
48
|
const normalizedContentDir = path.resolve(contentDir) + path.sep;
|
|
48
49
|
const buildContentFiles = getBuildContentFiles(
|
|
49
50
|
contentDir,
|
|
50
|
-
Object.keys(this.siteVariables
|
|
51
|
+
Object.keys(getExtensionToShikiLanguage(this.siteVariables)),
|
|
51
52
|
);
|
|
52
53
|
|
|
53
54
|
// Detect structural changes (files added or deleted)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { makeLogger } from './log';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getExtensionToShikiLanguage,
|
|
6
|
+
getRuntimeBundledShikiLanguages,
|
|
7
|
+
} from './site-variables';
|
|
5
8
|
import { validateConfigLinks } from './validate-config-links';
|
|
6
9
|
import { json } from './templates';
|
|
7
10
|
import { initHighlighter } from './utils/shiki-highlighter';
|
|
@@ -30,7 +33,6 @@ const log = makeLogger(import.meta.url);
|
|
|
30
33
|
|
|
31
34
|
export class ContentRenderer {
|
|
32
35
|
private siteVariables: SiteVariables;
|
|
33
|
-
private loggedCodeDisabled: boolean;
|
|
34
36
|
private sourceFileCache: Map<string, Asset[]>;
|
|
35
37
|
private lastBuildFiles: Set<string>;
|
|
36
38
|
private javacAvailable: boolean | undefined;
|
|
@@ -47,7 +49,6 @@ export class ContentRenderer {
|
|
|
47
49
|
|
|
48
50
|
constructor(siteVariables: SiteVariables) {
|
|
49
51
|
this.siteVariables = siteVariables;
|
|
50
|
-
this.loggedCodeDisabled = false;
|
|
51
52
|
this.sourceFileCache = new Map();
|
|
52
53
|
this.lastBuildFiles = new Set();
|
|
53
54
|
}
|
|
@@ -57,14 +58,7 @@ export class ContentRenderer {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
async initHighlighter(): Promise<void> {
|
|
60
|
-
|
|
61
|
-
...new Set([
|
|
62
|
-
'plaintext',
|
|
63
|
-
'text',
|
|
64
|
-
...Object.values(this.siteVariables.codeLanguages || {}),
|
|
65
|
-
]),
|
|
66
|
-
];
|
|
67
|
-
await initHighlighter(langs);
|
|
61
|
+
await initHighlighter(getRuntimeBundledShikiLanguages(this.siteVariables));
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
getDirtySourceFiles(
|
|
@@ -99,7 +93,7 @@ export class ContentRenderer {
|
|
|
99
93
|
|
|
100
94
|
isCopiedAssetSource(filePath: string): boolean {
|
|
101
95
|
const ext = path.extname(filePath).toLowerCase();
|
|
102
|
-
const codeExtensions = this.siteVariables
|
|
96
|
+
const codeExtensions = getExtensionToShikiLanguage(this.siteVariables);
|
|
103
97
|
return Object.prototype.hasOwnProperty.call(codeExtensions, ext.slice(1));
|
|
104
98
|
}
|
|
105
99
|
|
|
@@ -147,22 +141,19 @@ export class ContentRenderer {
|
|
|
147
141
|
return assets;
|
|
148
142
|
}
|
|
149
143
|
|
|
150
|
-
const codeExtensions = this.siteVariables
|
|
144
|
+
const codeExtensions = getExtensionToShikiLanguage(this.siteVariables);
|
|
151
145
|
if (ext.slice(1) in codeExtensions) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
log.info`Not generating source code pages due to site.features.code = false`;
|
|
164
|
-
this.loggedCodeDisabled = true;
|
|
165
|
-
}
|
|
146
|
+
assets.push(
|
|
147
|
+
...renderCodePageAsset({
|
|
148
|
+
filePath,
|
|
149
|
+
contentDir,
|
|
150
|
+
distDir,
|
|
151
|
+
siteVariables: this.siteVariables,
|
|
152
|
+
validInternalTargets,
|
|
153
|
+
assetFiles,
|
|
154
|
+
literateJavaOutputPaths,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
166
157
|
}
|
|
167
158
|
|
|
168
159
|
return assets;
|
|
@@ -251,7 +242,7 @@ export class ContentRenderer {
|
|
|
251
242
|
const contentDir: string = getContentDir();
|
|
252
243
|
const buildContentFiles: string[] = getBuildContentFiles(
|
|
253
244
|
contentDir,
|
|
254
|
-
Object.keys(this.siteVariables
|
|
245
|
+
Object.keys(getExtensionToShikiLanguage(this.siteVariables)),
|
|
255
246
|
);
|
|
256
247
|
const buildFileSet = new Set<string>(buildContentFiles);
|
|
257
248
|
const dirtySourceFiles = this.getDirtySourceFiles(
|
|
@@ -265,8 +256,7 @@ export class ContentRenderer {
|
|
|
265
256
|
const validInternalTargets = getValidInternalTargets(
|
|
266
257
|
contentDir,
|
|
267
258
|
buildContentFiles,
|
|
268
|
-
Object.keys(this.siteVariables
|
|
269
|
-
isFeatureEnabled(this.siteVariables, 'code'),
|
|
259
|
+
Object.keys(getExtensionToShikiLanguage(this.siteVariables)),
|
|
270
260
|
);
|
|
271
261
|
const literateJavaOutputPaths = new Set<string>();
|
|
272
262
|
for (const filePath of buildContentFiles) {
|
package/build/pipeline.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { getDevSiteVariables, getProdSiteVariables } from './site-variables';
|
|
3
|
+
import { getExtensionToShikiLanguage } from './site-variables';
|
|
3
4
|
import { compileTemplates } from './templates';
|
|
4
5
|
import {
|
|
5
6
|
getDistDir,
|
|
@@ -70,7 +71,7 @@ export async function runPipeline(
|
|
|
70
71
|
|
|
71
72
|
const publicRelPaths = copyPublicFiles(publicDir, distDir);
|
|
72
73
|
const processedExtensions = getProcessedExtensions(
|
|
73
|
-
Object.keys(siteVariables
|
|
74
|
+
Object.keys(getExtensionToShikiLanguage(siteVariables)),
|
|
74
75
|
);
|
|
75
76
|
copyContentAssets(contentDir, distDir, processedExtensions, publicRelPaths);
|
|
76
77
|
|
package/build/site-variables.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { bundledLanguages } from 'shiki';
|
|
4
|
+
import type { BundledLanguage } from 'shiki';
|
|
3
5
|
import { compile as compileJsonSchema, doValidation } from './json-schema';
|
|
4
6
|
import { getProjectDir } from './utils/paths';
|
|
5
|
-
import type { SiteVariables } from './types';
|
|
7
|
+
import type { PlainTextLanguage, SiteVariables } from './types';
|
|
6
8
|
import siteSchema from '../schema/site.schema.json' with { type: 'json' };
|
|
7
9
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
8
10
|
|
|
@@ -10,7 +12,7 @@ const configDir = getProjectDir();
|
|
|
10
12
|
|
|
11
13
|
const DEFAULT: Partial<SiteVariables> = {
|
|
12
14
|
basePath: '/',
|
|
13
|
-
features: { search: true,
|
|
15
|
+
features: { search: true, favicon: true, footer: true },
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
const isValid = compileJsonSchema(siteSchema);
|
|
@@ -21,12 +23,111 @@ function getJson(filePath: string): Record<string, unknown> {
|
|
|
21
23
|
);
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
const PLAIN_TEXT_LANGUAGES = ['plain', 'text', 'txt'] as const;
|
|
27
|
+
|
|
28
|
+
export function isPlainTextLanguage(lang: string): lang is PlainTextLanguage {
|
|
29
|
+
return PLAIN_TEXT_LANGUAGES.some(value => value === lang);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isBundledLanguage(lang: string): lang is BundledLanguage {
|
|
33
|
+
return Object.hasOwn(bundledLanguages, lang);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isSupportedShikiLanguage(
|
|
37
|
+
lang: string,
|
|
38
|
+
): lang is BundledLanguage | PlainTextLanguage {
|
|
39
|
+
return isBundledLanguage(lang) || isPlainTextLanguage(lang);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateExtensionToShikiLanguage(
|
|
43
|
+
value: unknown,
|
|
44
|
+
fileName: string,
|
|
45
|
+
): Record<string, BundledLanguage | PlainTextLanguage> | undefined {
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const validated: Record<string, BundledLanguage | PlainTextLanguage> = {};
|
|
54
|
+
for (const [ext, lang] of Object.entries(value)) {
|
|
55
|
+
if (typeof lang !== 'string') {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!isSupportedShikiLanguage(lang)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${fileName}: extensionToShikiLanguage.${ext} "${lang}" is not a supported Shiki language`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
validated[ext] = lang;
|
|
64
|
+
}
|
|
65
|
+
return validated;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateShikiLanguages(
|
|
69
|
+
value: unknown,
|
|
70
|
+
fileName: string,
|
|
71
|
+
): BundledLanguage[] | undefined {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(value)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const validated: BundledLanguage[] = [];
|
|
80
|
+
for (let i = 0; i < value.length; i++) {
|
|
81
|
+
const lang = value[i];
|
|
82
|
+
if (typeof lang !== 'string') {
|
|
83
|
+
throw new Error(`${fileName}: shikiLanguages[${i}] must be a string`);
|
|
84
|
+
}
|
|
85
|
+
if (isPlainTextLanguage(lang)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`${fileName}: shikiLanguages[${i}] "${lang}" must be a bundled Shiki language`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!isBundledLanguage(lang)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`${fileName}: shikiLanguages[${i}] "${lang}" is not a supported Shiki language`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
validated.push(lang);
|
|
96
|
+
}
|
|
97
|
+
return validated;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getExtensionToShikiLanguage(
|
|
101
|
+
siteVariables: SiteVariables,
|
|
102
|
+
): Record<string, BundledLanguage | PlainTextLanguage> {
|
|
103
|
+
return siteVariables.extensionToShikiLanguage ?? {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getRuntimeBundledShikiLanguages(
|
|
107
|
+
siteVariables: SiteVariables,
|
|
108
|
+
): BundledLanguage[] {
|
|
109
|
+
const configured = Object.values(getExtensionToShikiLanguage(siteVariables))
|
|
110
|
+
.filter(isBundledLanguage)
|
|
111
|
+
.concat(siteVariables.shikiLanguages ?? []);
|
|
112
|
+
return [...new Set(configured)];
|
|
113
|
+
}
|
|
114
|
+
|
|
24
115
|
function getSiteVariables(env: string): SiteVariables {
|
|
25
116
|
const fileName = `site.${env}.json`;
|
|
26
117
|
const fromFile = getJson(fileName);
|
|
118
|
+
const extensionToShikiLanguage = validateExtensionToShikiLanguage(
|
|
119
|
+
fromFile.extensionToShikiLanguage,
|
|
120
|
+
fileName,
|
|
121
|
+
);
|
|
122
|
+
const shikiLanguages = validateShikiLanguages(
|
|
123
|
+
fromFile.shikiLanguages,
|
|
124
|
+
fileName,
|
|
125
|
+
);
|
|
27
126
|
const variables = {
|
|
28
127
|
...DEFAULT,
|
|
29
128
|
...fromFile,
|
|
129
|
+
...(extensionToShikiLanguage ? { extensionToShikiLanguage } : {}),
|
|
130
|
+
...(shikiLanguages ? { shikiLanguages } : {}),
|
|
30
131
|
features: {
|
|
31
132
|
...DEFAULT.features,
|
|
32
133
|
...((fromFile.features as Record<string, unknown>) || {}),
|
package/build/types.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { BundledLanguage } from 'shiki';
|
|
2
|
+
|
|
3
|
+
export type PlainTextLanguage = 'text' | 'txt' | 'plain';
|
|
4
|
+
|
|
1
5
|
/** Site configuration loaded from site.dev.json or site.prod.json */
|
|
2
6
|
export interface SiteVariables {
|
|
3
7
|
base: string;
|
|
@@ -12,7 +16,11 @@ export interface SiteVariables {
|
|
|
12
16
|
internalDomains?: string[];
|
|
13
17
|
defaultTimeZone: string;
|
|
14
18
|
features: FeatureConfig;
|
|
15
|
-
|
|
19
|
+
extensionToShikiLanguage?: Record<
|
|
20
|
+
string,
|
|
21
|
+
BundledLanguage | PlainTextLanguage
|
|
22
|
+
>;
|
|
23
|
+
shikiLanguages?: BundledLanguage[];
|
|
16
24
|
tintHue?: number;
|
|
17
25
|
tintAmount?: number;
|
|
18
26
|
vars?: Record<string, unknown>;
|
|
@@ -20,7 +28,6 @@ export interface SiteVariables {
|
|
|
20
28
|
|
|
21
29
|
export type FeatureConfig = {
|
|
22
30
|
search: boolean;
|
|
23
|
-
code: boolean;
|
|
24
31
|
favicon: boolean;
|
|
25
32
|
footer: boolean;
|
|
26
33
|
};
|
|
@@ -102,6 +109,8 @@ export interface RenderCodePageOptions {
|
|
|
102
109
|
distDir: string;
|
|
103
110
|
siteVariables: SiteVariables;
|
|
104
111
|
assetFiles: string[];
|
|
112
|
+
validInternalTargets: Set<string>;
|
|
113
|
+
literateJavaOutputPaths?: Set<string>;
|
|
105
114
|
dependencyCollector?: RenderDependencyCollector;
|
|
106
115
|
}
|
|
107
116
|
|
|
@@ -278,4 +287,6 @@ export interface SiteConfigInput {
|
|
|
278
287
|
basePath: string;
|
|
279
288
|
internalDomains: string[];
|
|
280
289
|
features: FeatureConfig;
|
|
290
|
+
extensionToShikiLanguage: Record<string, BundledLanguage | PlainTextLanguage>;
|
|
291
|
+
shikiLanguages: BundledLanguage[];
|
|
281
292
|
}
|
package/build/utils/code.ts
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import { parse as parseJava } from 'java-parser';
|
|
4
4
|
import { JSDOM } from 'jsdom';
|
|
5
5
|
import { makeLogger } from '../log';
|
|
6
|
-
import {
|
|
6
|
+
import { getExtensionToShikiLanguage } from '../site-variables';
|
|
7
|
+
import { highlightCode } from './shiki-highlighter';
|
|
7
8
|
import externalLinksPlugin from '../external-links-plugin';
|
|
8
9
|
import applyBasePathPlugin from '../apply-base-path-plugin';
|
|
9
10
|
import { createApplyBasePath } from './paths';
|
|
@@ -61,7 +62,9 @@ export function rewriteProseLinks(
|
|
|
61
62
|
pageDirPath: string,
|
|
62
63
|
): string[] {
|
|
63
64
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
64
|
-
const codeExtensions = Object.keys(
|
|
65
|
+
const codeExtensions = Object.keys(
|
|
66
|
+
getExtensionToShikiLanguage(siteVariables),
|
|
67
|
+
);
|
|
65
68
|
|
|
66
69
|
function rewriteCodeExt(p: string): string {
|
|
67
70
|
for (const ext of codeExtensions) {
|
|
@@ -473,12 +476,7 @@ export function renderCodeSegment(
|
|
|
473
476
|
let lineHtml: string[] | undefined;
|
|
474
477
|
|
|
475
478
|
try {
|
|
476
|
-
const
|
|
477
|
-
const html = highlighter.codeToHtml(source, {
|
|
478
|
-
lang,
|
|
479
|
-
themes: { light: 'github-light', dark: 'github-dark' },
|
|
480
|
-
defaultColor: false,
|
|
481
|
-
});
|
|
479
|
+
const html = highlightCode(source, lang);
|
|
482
480
|
const fragment = JSDOM.fragment(html);
|
|
483
481
|
const inner = (fragment.querySelector('code') as HTMLElement).innerHTML;
|
|
484
482
|
lineHtml = splitHighlightedHtmlIntoLines(inner, lines.length);
|
|
@@ -506,8 +504,15 @@ export function renderCodeWithComments(
|
|
|
506
504
|
lang: string,
|
|
507
505
|
siteVariables: SiteVariables,
|
|
508
506
|
pageDirPath?: string,
|
|
507
|
+
sourceUrlPath?: string,
|
|
508
|
+
validTargets?: Set<string>,
|
|
509
|
+
literateJavaOutputPaths?: Set<string>,
|
|
509
510
|
): string {
|
|
510
|
-
const md = createCodeMarkdown(siteVariables
|
|
511
|
+
const md = createCodeMarkdown(siteVariables, {
|
|
512
|
+
sourceUrlPath,
|
|
513
|
+
validTargets,
|
|
514
|
+
literateJavaOutputPaths,
|
|
515
|
+
});
|
|
511
516
|
const lines = splitLines(sourceCode);
|
|
512
517
|
|
|
513
518
|
// Group lines into segments
|
|
@@ -55,7 +55,6 @@ export function getContentSourceOutputRelPaths(
|
|
|
55
55
|
contentDir: string,
|
|
56
56
|
filePath: string,
|
|
57
57
|
codeExtensions: string[],
|
|
58
|
-
codeEnabled: boolean,
|
|
59
58
|
): Set<string> {
|
|
60
59
|
const processedExtSet = new Set(getProcessedExtensions(codeExtensions));
|
|
61
60
|
const relPath = toPosix(path.relative(contentDir, filePath));
|
|
@@ -94,9 +93,7 @@ export function getContentSourceOutputRelPaths(
|
|
|
94
93
|
);
|
|
95
94
|
if (codeExtensionSet.has(ext)) {
|
|
96
95
|
outputs.add(relPath);
|
|
97
|
-
|
|
98
|
-
outputs.add(`${relPath}.html`);
|
|
99
|
-
}
|
|
96
|
+
outputs.add(`${relPath}.html`);
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
return outputs;
|
|
@@ -105,7 +102,6 @@ export function getContentSourceOutputRelPaths(
|
|
|
105
102
|
export function getContentOutputRelPaths(
|
|
106
103
|
contentDir: string,
|
|
107
104
|
codeExtensions: string[],
|
|
108
|
-
codeEnabled: boolean,
|
|
109
105
|
): Set<string> {
|
|
110
106
|
const buildContentFiles = getBuildContentFiles(contentDir, codeExtensions);
|
|
111
107
|
const codeExtensionSet = new Set(
|
|
@@ -131,9 +127,7 @@ export function getContentOutputRelPaths(
|
|
|
131
127
|
|
|
132
128
|
if (codeExtensionSet.has(ext.slice(1))) {
|
|
133
129
|
outputs.add(relPath);
|
|
134
|
-
|
|
135
|
-
outputs.add(`${relPath}.html`);
|
|
136
|
-
}
|
|
130
|
+
outputs.add(`${relPath}.html`);
|
|
137
131
|
}
|
|
138
132
|
}
|
|
139
133
|
|
|
@@ -195,7 +189,6 @@ export function getValidInternalTargets(
|
|
|
195
189
|
contentDir: string,
|
|
196
190
|
contentFiles: string[],
|
|
197
191
|
codeExtensions: string[],
|
|
198
|
-
codeEnabled: boolean,
|
|
199
192
|
): Set<string> {
|
|
200
193
|
const targets = new Set<string>();
|
|
201
194
|
const codeExtensionSet = new Set(
|
|
@@ -220,9 +213,7 @@ export function getValidInternalTargets(
|
|
|
220
213
|
} else if (extensionIsMarkdown(ext) || ext === '.html') {
|
|
221
214
|
addGeneratedRouteAliases(targets, `/${subPath}.html`);
|
|
222
215
|
} else if (codeExtensionSet.has(ext.slice(1))) {
|
|
223
|
-
|
|
224
|
-
addGeneratedRouteAliases(targets, `/${relPath}.html`);
|
|
225
|
-
}
|
|
216
|
+
addGeneratedRouteAliases(targets, `/${relPath}.html`);
|
|
226
217
|
targets.add(normalizeOutputPath(`/${relPath}`));
|
|
227
218
|
}
|
|
228
219
|
}
|
package/build/utils/markdown.ts
CHANGED
|
@@ -8,7 +8,8 @@ import markdownItFootnote from 'markdown-it-footnote';
|
|
|
8
8
|
import markdownItDeflist from 'markdown-it-deflist';
|
|
9
9
|
import markdownItContainer from 'markdown-it-container';
|
|
10
10
|
import textToId, { deduplicateId } from '../text-to-id';
|
|
11
|
-
import {
|
|
11
|
+
import { highlightCode } from './shiki-highlighter';
|
|
12
|
+
import { isBundledLanguage, isPlainTextLanguage } from '../site-variables';
|
|
12
13
|
import headingSubtitlePlugin from '../heading-subtitle-plugin';
|
|
13
14
|
import deflistIdPlugin from '../deflist-id-plugin';
|
|
14
15
|
import externalLinksPlugin from '../external-links-plugin';
|
|
@@ -27,6 +28,7 @@ const QUESTION_PATTERN = /^question\s+(.+)$/;
|
|
|
27
28
|
|
|
28
29
|
interface CreateMarkdownOptions {
|
|
29
30
|
validatorOptions?: Record<string, unknown>;
|
|
31
|
+
filePath?: string;
|
|
30
32
|
literateJavaOutputPaths?: Set<string>;
|
|
31
33
|
sourceUrlPath?: string;
|
|
32
34
|
validTargets?: Set<string>;
|
|
@@ -58,6 +60,7 @@ export function createMarkdown(
|
|
|
58
60
|
): MarkdownIt {
|
|
59
61
|
const {
|
|
60
62
|
validatorOptions = {},
|
|
63
|
+
filePath,
|
|
61
64
|
literateJavaOutputPaths,
|
|
62
65
|
sourceUrlPath,
|
|
63
66
|
validTargets,
|
|
@@ -352,15 +355,24 @@ export function createMarkdown(
|
|
|
352
355
|
const token = tokens[idx];
|
|
353
356
|
const lang = token.info.trim().split(/\s+/)[0] || 'text';
|
|
354
357
|
const code = token.content;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
lang
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
358
|
+
let useLang: string;
|
|
359
|
+
|
|
360
|
+
if (isPlainTextLanguage(lang)) {
|
|
361
|
+
useLang = 'text';
|
|
362
|
+
} else if (isBundledLanguage(lang)) {
|
|
363
|
+
if (!(siteVariables.shikiLanguages ?? []).includes(lang)) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`${filePath ?? 'Markdown'}: Markdown fence language "${lang}" is not listed in site.shikiLanguages`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
useLang = lang;
|
|
369
|
+
} else {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`${filePath ?? 'Markdown'}: Markdown fence language "${lang}" is not a supported Shiki language`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return highlightCode(code, useLang);
|
|
364
376
|
};
|
|
365
377
|
|
|
366
378
|
return markdown;
|
package/build/utils/render.ts
CHANGED
|
@@ -3,10 +3,10 @@ import path from 'path';
|
|
|
3
3
|
import _ from 'lodash';
|
|
4
4
|
import type MarkdownIt from 'markdown-it';
|
|
5
5
|
import { stripHtml } from 'string-strip-html';
|
|
6
|
-
import { isFeatureEnabled } from '../features';
|
|
7
6
|
import { makeLogger } from '../log';
|
|
8
7
|
import { B } from '../colors';
|
|
9
8
|
import createGlobals from '../globals';
|
|
9
|
+
import { getExtensionToShikiLanguage } from '../site-variables';
|
|
10
10
|
import { render, json } from '../templates';
|
|
11
11
|
import { validateParentLink } from '../validate-config-links';
|
|
12
12
|
import {
|
|
@@ -284,14 +284,20 @@ export function renderCodePageAsset({
|
|
|
284
284
|
distDir,
|
|
285
285
|
siteVariables,
|
|
286
286
|
assetFiles,
|
|
287
|
+
validInternalTargets,
|
|
288
|
+
literateJavaOutputPaths,
|
|
287
289
|
}: RenderCodePageOptions): Asset[] {
|
|
288
290
|
const { dir, name, ext } = path.parse(filePath);
|
|
289
291
|
const subPath = toPosix(path.relative(contentDir, path.join(dir, name)));
|
|
290
292
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
291
|
-
const lang =
|
|
293
|
+
const lang =
|
|
294
|
+
getExtensionToShikiLanguage(siteVariables)[ext.slice(1).toLowerCase()];
|
|
292
295
|
const rawSource = fs.readFileSync(filePath, 'utf-8');
|
|
293
296
|
const sourceCode = applySourceTemplate(rawSource, siteVariables, filePath);
|
|
294
297
|
const pageDirPath = toPosix(path.relative(contentDir, dir));
|
|
298
|
+
const sourceUrlPath = normalizeOutputPath(
|
|
299
|
+
`/${toUrlPath(path.relative(contentDir, filePath))}.html`,
|
|
300
|
+
);
|
|
295
301
|
|
|
296
302
|
log.info`Rendering code page ${B`${subPath + ext}`}`;
|
|
297
303
|
const content = renderCodeWithComments(
|
|
@@ -299,6 +305,9 @@ export function renderCodePageAsset({
|
|
|
299
305
|
lang,
|
|
300
306
|
siteVariables,
|
|
301
307
|
pageDirPath,
|
|
308
|
+
sourceUrlPath,
|
|
309
|
+
validInternalTargets,
|
|
310
|
+
literateJavaOutputPaths,
|
|
302
311
|
);
|
|
303
312
|
const codeFilePath = applyBasePath(
|
|
304
313
|
normalizeOutputPath(`/${toUrlPath(path.relative(contentDir, filePath))}`),
|
|
@@ -354,10 +363,6 @@ export function renderCopiedContentAsset({
|
|
|
354
363
|
|
|
355
364
|
log.info`${label} ${B`${relPath}`}`;
|
|
356
365
|
|
|
357
|
-
if (!isFeatureEnabled(siteVariables, 'code')) {
|
|
358
|
-
return [{ assetPath: relPath, content: fs.readFileSync(filePath) }];
|
|
359
|
-
}
|
|
360
|
-
|
|
361
366
|
const rawSource = fs.readFileSync(filePath, 'utf-8');
|
|
362
367
|
const templated = applySourceTemplate(rawSource, siteVariables, filePath);
|
|
363
368
|
|
|
@@ -419,14 +424,13 @@ function renderPlainTextContent(
|
|
|
419
424
|
} {
|
|
420
425
|
const sourceUrlPath = `/${subPath}.html`;
|
|
421
426
|
const md = createMarkdown(siteVariables, {
|
|
427
|
+
filePath,
|
|
422
428
|
validatorOptions: {
|
|
423
429
|
enabled: validateInternalLinks,
|
|
424
430
|
filePath,
|
|
425
431
|
sourceUrlPath,
|
|
426
432
|
validTargets: validInternalTargets,
|
|
427
|
-
codeExtensions:
|
|
428
|
-
? Object.keys(siteVariables.codeLanguages!)
|
|
429
|
-
: [],
|
|
433
|
+
codeExtensions: Object.keys(getExtensionToShikiLanguage(siteVariables)),
|
|
430
434
|
onValidLink: (targetPath: string) => {
|
|
431
435
|
dependencyCollector?.internalTargets?.add(targetPath);
|
|
432
436
|
},
|
|
@@ -597,13 +601,12 @@ export function renderLiterateJavaPageAsset({
|
|
|
597
601
|
// with Shiki-highlighted code blocks and optional JDI output columns
|
|
598
602
|
const sourceUrlPath = `/${subPath}.java.html`;
|
|
599
603
|
const md = createMarkdown(siteVariables, {
|
|
604
|
+
filePath,
|
|
600
605
|
validatorOptions: {
|
|
601
606
|
filePath,
|
|
602
607
|
sourceUrlPath,
|
|
603
608
|
validTargets: validInternalTargets,
|
|
604
|
-
codeExtensions:
|
|
605
|
-
? Object.keys(siteVariables.codeLanguages!)
|
|
606
|
-
: [],
|
|
609
|
+
codeExtensions: Object.keys(getExtensionToShikiLanguage(siteVariables)),
|
|
607
610
|
onValidLink: (targetPath: string) => {
|
|
608
611
|
dependencyCollector?.internalTargets?.add(targetPath);
|
|
609
612
|
},
|
|
@@ -1,21 +1,67 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
HighlighterGeneric,
|
|
3
|
+
BundledLanguage,
|
|
4
|
+
BundledTheme,
|
|
5
|
+
ThemeRegistrationAny,
|
|
6
|
+
} from 'shiki';
|
|
2
7
|
import { makeLogger } from '../log';
|
|
8
|
+
import type { PlainTextLanguage } from '../types';
|
|
3
9
|
|
|
4
10
|
const log = makeLogger(import.meta.url);
|
|
11
|
+
const COMMENT_COLOR = 'var(--fg2-color)';
|
|
12
|
+
const SHIKI_THEMES = {
|
|
13
|
+
light: 'tada-github-light',
|
|
14
|
+
dark: 'tada-github-dark',
|
|
15
|
+
} as const;
|
|
16
|
+
const COMMENT_SCOPES = [
|
|
17
|
+
'comment',
|
|
18
|
+
'punctuation.definition.comment',
|
|
19
|
+
'string.comment',
|
|
20
|
+
];
|
|
5
21
|
|
|
6
22
|
let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null =
|
|
7
23
|
null;
|
|
8
24
|
|
|
9
|
-
|
|
10
|
-
|
|
25
|
+
async function createThemeWithCommentOverride(
|
|
26
|
+
themeName: 'github-light' | 'github-dark',
|
|
27
|
+
customName: 'tada-github-light' | 'tada-github-dark',
|
|
28
|
+
): Promise<ThemeRegistrationAny> {
|
|
29
|
+
const { bundledThemes } = await import('shiki');
|
|
30
|
+
const theme = (await bundledThemes[themeName]()).default;
|
|
31
|
+
return {
|
|
32
|
+
...theme,
|
|
33
|
+
name: customName,
|
|
34
|
+
tokenColors: [
|
|
35
|
+
...(theme.tokenColors ?? []),
|
|
36
|
+
{ scope: COMMENT_SCOPES, settings: { foreground: COMMENT_COLOR } },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function initHighlighter(
|
|
42
|
+
langs: Array<BundledLanguage | PlainTextLanguage>,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const langsWithFallback = [
|
|
45
|
+
...new Set<BundledLanguage | PlainTextLanguage>(['text', ...langs]),
|
|
46
|
+
];
|
|
47
|
+
if (!highlighter) {
|
|
48
|
+
log.debug`Initializing syntax highlighter`;
|
|
49
|
+
const { createHighlighter } = await import('shiki');
|
|
50
|
+
const themes = await Promise.all([
|
|
51
|
+
createThemeWithCommentOverride('github-light', 'tada-github-light'),
|
|
52
|
+
createThemeWithCommentOverride('github-dark', 'tada-github-dark'),
|
|
53
|
+
]);
|
|
54
|
+
highlighter = await createHighlighter({ themes, langs: langsWithFallback });
|
|
11
55
|
return;
|
|
12
56
|
}
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
57
|
+
|
|
58
|
+
const missingLangs = langsWithFallback.filter(
|
|
59
|
+
lang => !highlighter!.getLoadedLanguages().includes(lang),
|
|
60
|
+
);
|
|
61
|
+
if (missingLangs.length > 0) {
|
|
62
|
+
log.debug`Loading additional syntax languages: ${missingLangs.join(', ')}`;
|
|
63
|
+
await highlighter.loadLanguage(...missingLangs);
|
|
64
|
+
}
|
|
19
65
|
}
|
|
20
66
|
|
|
21
67
|
export function getHighlighter(): HighlighterGeneric<
|
|
@@ -27,3 +73,11 @@ export function getHighlighter(): HighlighterGeneric<
|
|
|
27
73
|
}
|
|
28
74
|
return highlighter;
|
|
29
75
|
}
|
|
76
|
+
|
|
77
|
+
export function highlightCode(code: string, lang: string): string {
|
|
78
|
+
return getHighlighter().codeToHtml(code, {
|
|
79
|
+
lang,
|
|
80
|
+
themes: SHIKI_THEMES,
|
|
81
|
+
defaultColor: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -6,8 +6,8 @@ import type { SiteVariables } from '../types';
|
|
|
6
6
|
* authors can interpolate site variables (like `<%= vars.fullCourseName %>`)
|
|
7
7
|
* in their source code. The template context mirrors the one used for JSON
|
|
8
8
|
* data files in `build/templates.ts`: `vars` and `site`. Invoked from
|
|
9
|
-
* `renderCodePageAsset` and `renderCopiedContentAsset`
|
|
10
|
-
*
|
|
9
|
+
* `renderCodePageAsset` and `renderCopiedContentAsset` for source files whose
|
|
10
|
+
* extension is mapped in `site.extensionToShikiLanguage`.
|
|
11
11
|
*/
|
|
12
12
|
export function applySourceTemplate(
|
|
13
13
|
source: string,
|
package/build/watch/assets.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { copyFonts } from '../generate-fonts';
|
|
|
5
5
|
import { copyKatexAssets } from '../generate-katex-assets';
|
|
6
6
|
import { generateFavicons } from '../generate-favicon';
|
|
7
7
|
import { generateWebAppManifest } from '../generate-web-app-manifest';
|
|
8
|
+
import { getRuntimeBundledShikiLanguages } from '../site-variables';
|
|
8
9
|
import { getPackageDir, toPosix } from '../util';
|
|
9
10
|
import { initHighlighter } from '../utils/shiki-highlighter';
|
|
10
11
|
import type { SiteVariables } from '../types';
|
|
@@ -27,14 +28,7 @@ export function removeDirIfExists(dir: string): void {
|
|
|
27
28
|
export async function ensureHighlighter(
|
|
28
29
|
siteVariables: SiteVariables,
|
|
29
30
|
): Promise<void> {
|
|
30
|
-
|
|
31
|
-
...new Set([
|
|
32
|
-
'plaintext',
|
|
33
|
-
'text',
|
|
34
|
-
...Object.values(siteVariables.codeLanguages || {}),
|
|
35
|
-
]),
|
|
36
|
-
];
|
|
37
|
-
await initHighlighter(langs);
|
|
31
|
+
await initHighlighter(getRuntimeBundledShikiLanguages(siteVariables));
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
export async function bundleWatchAssets(
|
package/build/watch/records.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { getExtensionToShikiLanguage } from '../site-variables';
|
|
3
4
|
import {
|
|
4
5
|
renderCodePageAsset,
|
|
5
6
|
renderCopiedContentAsset,
|
|
@@ -87,12 +88,11 @@ export function renderContentRecord({
|
|
|
87
88
|
persistOutputs?: boolean;
|
|
88
89
|
}): TadaSourceRecord {
|
|
89
90
|
const deps = createDependencyCollector();
|
|
90
|
-
const codeExtensions = Object.keys(
|
|
91
|
-
|
|
92
|
-
);
|
|
91
|
+
const codeExtensions = Object.keys(
|
|
92
|
+
getExtensionToShikiLanguage(siteVariables),
|
|
93
|
+
).map(ext => ext.toLowerCase());
|
|
93
94
|
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
94
95
|
const assets: Asset[] = [];
|
|
95
|
-
const codeEnabled = siteVariables.features.code !== false;
|
|
96
96
|
|
|
97
97
|
if (isLiterateJava(filePath)) {
|
|
98
98
|
assets.push(
|
|
@@ -128,18 +128,18 @@ export function renderContentRecord({
|
|
|
128
128
|
}),
|
|
129
129
|
);
|
|
130
130
|
} else if (codeExtensions.includes(ext)) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
|
|
131
|
+
assets.push(
|
|
132
|
+
...renderCodePageAsset({
|
|
133
|
+
filePath,
|
|
134
|
+
contentDir: scan.contentDir,
|
|
135
|
+
distDir: outputDir,
|
|
136
|
+
siteVariables,
|
|
137
|
+
validInternalTargets: scan.validTargets,
|
|
138
|
+
assetFiles,
|
|
139
|
+
literateJavaOutputPaths: scan.literateJavaOutputPaths,
|
|
140
|
+
dependencyCollector: deps.collector,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
143
|
assets.push(
|
|
144
144
|
...renderCopiedContentAsset({
|
|
145
145
|
filePath,
|
package/build/watch/snapshot.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
shouldSkipContentFile,
|
|
9
9
|
toPosix,
|
|
10
10
|
} from '../util';
|
|
11
|
-
import {
|
|
11
|
+
import { getExtensionToShikiLanguage } from '../site-variables';
|
|
12
12
|
import {
|
|
13
13
|
extensionIsMarkdown,
|
|
14
14
|
getProcessedExtensions,
|
|
@@ -106,9 +106,9 @@ function buildReverseMap(
|
|
|
106
106
|
|
|
107
107
|
function getProcessedExts(siteVariables: SiteVariables): Set<string> {
|
|
108
108
|
return new Set(
|
|
109
|
-
getProcessedExtensions(
|
|
110
|
-
|
|
111
|
-
),
|
|
109
|
+
getProcessedExtensions(
|
|
110
|
+
Object.keys(getExtensionToShikiLanguage(siteVariables)),
|
|
111
|
+
).map(ext => ext.toLowerCase()),
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -144,13 +144,11 @@ function getContentOutputPathsForSource({
|
|
|
144
144
|
filePath,
|
|
145
145
|
processedExts,
|
|
146
146
|
buildContent,
|
|
147
|
-
codeEnabled,
|
|
148
147
|
}: {
|
|
149
148
|
contentDir: string;
|
|
150
149
|
filePath: string;
|
|
151
150
|
processedExts: Set<string>;
|
|
152
151
|
buildContent: boolean;
|
|
153
|
-
codeEnabled: boolean;
|
|
154
152
|
}): Set<string> {
|
|
155
153
|
const relPath = toPosix(path.relative(contentDir, filePath));
|
|
156
154
|
const parsed = path.parse(relPath);
|
|
@@ -181,9 +179,7 @@ function getContentOutputPathsForSource({
|
|
|
181
179
|
}
|
|
182
180
|
|
|
183
181
|
outputs.add(relPath);
|
|
184
|
-
|
|
185
|
-
outputs.add(`${relPath}.html`);
|
|
186
|
-
}
|
|
182
|
+
outputs.add(`${relPath}.html`);
|
|
187
183
|
return outputs;
|
|
188
184
|
}
|
|
189
185
|
|
|
@@ -193,14 +189,12 @@ function getTargetPathsForSource({
|
|
|
193
189
|
filePath,
|
|
194
190
|
processedExts,
|
|
195
191
|
buildContent,
|
|
196
|
-
codeEnabled,
|
|
197
192
|
}: {
|
|
198
193
|
kind: 'content' | 'public';
|
|
199
194
|
rootDir: string;
|
|
200
195
|
filePath: string;
|
|
201
196
|
processedExts: Set<string>;
|
|
202
197
|
buildContent: boolean;
|
|
203
|
-
codeEnabled: boolean;
|
|
204
198
|
}): Set<string> {
|
|
205
199
|
const targets = new Set<string>();
|
|
206
200
|
const relPath = toPosix(path.relative(rootDir, filePath));
|
|
@@ -238,9 +232,7 @@ function getTargetPathsForSource({
|
|
|
238
232
|
return targets;
|
|
239
233
|
}
|
|
240
234
|
|
|
241
|
-
|
|
242
|
-
addGeneratedRouteAliases(targets, `/${relPath}.html`);
|
|
243
|
-
}
|
|
235
|
+
addGeneratedRouteAliases(targets, `/${relPath}.html`);
|
|
244
236
|
targets.add(normalizeOutputPath(`/${relPath}`));
|
|
245
237
|
return targets;
|
|
246
238
|
}
|
|
@@ -258,7 +250,6 @@ export function scanProject(siteVariables: SiteVariables): TadaProjectScan {
|
|
|
258
250
|
const publicDir = getPublicDir();
|
|
259
251
|
const distDir = getDistDir();
|
|
260
252
|
const processedExts = getProcessedExts(siteVariables);
|
|
261
|
-
const codeEnabled = isFeatureEnabled(siteVariables, 'code');
|
|
262
253
|
const contentFiles = new Set(walkFiles(contentDir).sort());
|
|
263
254
|
const publicFiles = new Set(walkFiles(publicDir).sort());
|
|
264
255
|
const buildContentFiles = new Set<string>();
|
|
@@ -286,7 +277,6 @@ export function scanProject(siteVariables: SiteVariables): TadaProjectScan {
|
|
|
286
277
|
filePath,
|
|
287
278
|
processedExts,
|
|
288
279
|
buildContent,
|
|
289
|
-
codeEnabled,
|
|
290
280
|
});
|
|
291
281
|
sourceOutputPaths.set(filePath, outputs);
|
|
292
282
|
for (const outputPath of outputs) {
|
|
@@ -299,7 +289,6 @@ export function scanProject(siteVariables: SiteVariables): TadaProjectScan {
|
|
|
299
289
|
filePath,
|
|
300
290
|
processedExts,
|
|
301
291
|
buildContent,
|
|
302
|
-
codeEnabled,
|
|
303
292
|
});
|
|
304
293
|
sourceTargetPaths.set(filePath, targets);
|
|
305
294
|
for (const target of targets) {
|
|
@@ -317,7 +306,6 @@ export function scanProject(siteVariables: SiteVariables): TadaProjectScan {
|
|
|
317
306
|
filePath,
|
|
318
307
|
processedExts,
|
|
319
308
|
buildContent: false,
|
|
320
|
-
codeEnabled,
|
|
321
309
|
});
|
|
322
310
|
sourceTargetPaths.set(filePath, targets);
|
|
323
311
|
for (const target of targets) {
|
|
@@ -349,7 +337,6 @@ export function updateProjectScan(
|
|
|
349
337
|
const contentDir = getContentDir();
|
|
350
338
|
const publicDir = getPublicDir();
|
|
351
339
|
const distDir = getDistDir();
|
|
352
|
-
const codeEnabled = isFeatureEnabled(snapshot.siteVariables, 'code');
|
|
353
340
|
const contentFiles = new Set(snapshot.contentFiles);
|
|
354
341
|
const buildContentFiles = new Set(snapshot.buildContentFiles);
|
|
355
342
|
const publicFiles = new Set(snapshot.publicFiles);
|
|
@@ -412,7 +399,6 @@ export function updateProjectScan(
|
|
|
412
399
|
filePath: sourcePath,
|
|
413
400
|
processedExts: snapshot.processedExts,
|
|
414
401
|
buildContent,
|
|
415
|
-
codeEnabled,
|
|
416
402
|
});
|
|
417
403
|
sourceOutputPaths.set(sourcePath, outputs);
|
|
418
404
|
for (const outputPath of outputs) {
|
|
@@ -427,7 +413,6 @@ export function updateProjectScan(
|
|
|
427
413
|
filePath: sourcePath,
|
|
428
414
|
processedExts: snapshot.processedExts,
|
|
429
415
|
buildContent,
|
|
430
|
-
codeEnabled,
|
|
431
416
|
}),
|
|
432
417
|
);
|
|
433
418
|
continue;
|
|
@@ -445,7 +430,6 @@ export function updateProjectScan(
|
|
|
445
430
|
filePath: sourcePath,
|
|
446
431
|
processedExts: snapshot.processedExts,
|
|
447
432
|
buildContent: false,
|
|
448
|
-
codeEnabled,
|
|
449
433
|
}),
|
|
450
434
|
);
|
|
451
435
|
}
|
|
@@ -13,7 +13,8 @@ The value of `vars.foobar` is <%= vars.foobar %>.
|
|
|
13
13
|
You can add any variables you want to the `site.dev.json` and `site.prod.json`
|
|
14
14
|
files under the `"vars"` property and access them using Lodash template syntax
|
|
15
15
|
from Markdown files, HTML files, and source code. (Lodash template subtitution
|
|
16
|
-
only works in source code files
|
|
16
|
+
only works in source code files whose extension is listed in
|
|
17
|
+
`extensionToShikiLanguage`.)
|
|
17
18
|
|
|
18
19
|
## Slides
|
|
19
20
|
|
|
@@ -35,8 +36,9 @@ you should see a result for page 2 of `lecture1.pdf`.
|
|
|
35
36
|
- [A direct link](./Pair.java) to the source code
|
|
36
37
|
|
|
37
38
|
!!! note
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
Code pages and code blocks are highlighted by [Shiki](https://shiki.style).
|
|
40
|
+
A source code file's extension must be present in the config file under
|
|
41
|
+
`extensionToShikiLanguage` to be converted into a code page.
|
|
40
42
|
!!!
|
|
41
43
|
|
|
42
44
|
## Review questions
|
package/init/content/markdown.md
CHANGED
|
@@ -92,7 +92,7 @@ By default, MarkdownIt supports [the table syntax from GitHub "flavored" Markdow
|
|
|
92
92
|
|
|
93
93
|
Here's a basic table:
|
|
94
94
|
|
|
95
|
-
```
|
|
95
|
+
```
|
|
96
96
|
| Syntax | Description |
|
|
97
97
|
| ------ | ------------ |
|
|
98
98
|
| Cell | Another cell |
|
|
@@ -102,7 +102,7 @@ Here's a basic table:
|
|
|
102
102
|
You can omit the starting and ending vertical pipes, and the pipes don't have
|
|
103
103
|
to perfectly match on every line:
|
|
104
104
|
|
|
105
|
-
```
|
|
105
|
+
```
|
|
106
106
|
Syntax | Description
|
|
107
107
|
--- | ---
|
|
108
108
|
Cell | Another cell
|
|
@@ -197,7 +197,7 @@ Binary tree
|
|
|
197
197
|
Each term (bolded) of the definition list gets its own `id` attribute, like
|
|
198
198
|
headings, so you may [link directly to a definition](#binary-tree):
|
|
199
199
|
|
|
200
|
-
```
|
|
200
|
+
```
|
|
201
201
|
/example.html#binary-tree
|
|
202
202
|
```
|
|
203
203
|
|
|
@@ -256,7 +256,7 @@ Testing <dfn>definition</dfn> style
|
|
|
256
256
|
|
|
257
257
|
You can specify a custom title:
|
|
258
258
|
|
|
259
|
-
```
|
|
259
|
+
```
|
|
260
260
|
!!! warning Double-check your answers
|
|
261
261
|
No partial credit is offered for these questions, double-check your answers!
|
|
262
262
|
!!!
|
|
@@ -275,7 +275,7 @@ table of contents alongside headings.
|
|
|
275
275
|
|
|
276
276
|
Use
|
|
277
277
|
|
|
278
|
-
```
|
|
278
|
+
```
|
|
279
279
|
??? question What is a base case? Give an example.
|
|
280
280
|
|
|
281
281
|
The <dfn>base case</dfn> is the simplest version of the problem that can be
|
|
@@ -302,7 +302,7 @@ different background.
|
|
|
302
302
|
|
|
303
303
|
Use
|
|
304
304
|
|
|
305
|
-
```
|
|
305
|
+
```
|
|
306
306
|
::: section
|
|
307
307
|
|
|
308
308
|
### Submitting your work
|
|
@@ -337,7 +337,7 @@ Email the files you changed to the course staff.
|
|
|
337
337
|
|
|
338
338
|
Use
|
|
339
339
|
|
|
340
|
-
```
|
|
340
|
+
```
|
|
341
341
|
+++
|
|
342
342
|
Content in the first column (on the left)...
|
|
343
343
|
+++
|
|
@@ -356,6 +356,7 @@ including lists:
|
|
|
356
356
|
+++
|
|
357
357
|
Here's a paragraph in the second column. We'll use a code block on this side:
|
|
358
358
|
```java
|
|
359
|
+
// Return true if the number is odd
|
|
359
360
|
static boolean isOdd(int n) {
|
|
360
361
|
return n % 2 == 1;
|
|
361
362
|
}
|
package/init/public/Test.java
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// A source code file which is simply copied into the built site.
|
|
2
2
|
/// Tada will not process this file: it won't create an HTML file for it
|
|
3
|
-
/// (even if
|
|
4
|
-
|
|
3
|
+
/// (even if its extension is listed in `extensionToShikiLanguage`) and
|
|
4
|
+
// it won't appear in search results (even if `features.search` is `true`).
|
|
5
5
|
public class Test {
|
|
6
6
|
public static void main(String[] args) {
|
|
7
7
|
System.out.println("hello");
|
package/package.json
CHANGED
package/schema/site.schema.json
CHANGED
|
@@ -4,15 +4,9 @@
|
|
|
4
4
|
"properties": {
|
|
5
5
|
"features": {
|
|
6
6
|
"type": "object",
|
|
7
|
-
"default": {
|
|
8
|
-
"search": true,
|
|
9
|
-
"code": true,
|
|
10
|
-
"favicon": true,
|
|
11
|
-
"footer": true
|
|
12
|
-
},
|
|
7
|
+
"default": { "search": true, "favicon": true, "footer": true },
|
|
13
8
|
"properties": {
|
|
14
9
|
"search": { "type": "boolean", "default": true },
|
|
15
|
-
"code": { "type": "boolean", "default": true },
|
|
16
10
|
"favicon": { "type": "boolean", "default": true },
|
|
17
11
|
"footer": { "type": "boolean", "default": true }
|
|
18
12
|
},
|
|
@@ -55,10 +49,11 @@
|
|
|
55
49
|
},
|
|
56
50
|
"defaultTimeZone": { "type": "string" },
|
|
57
51
|
"vars": { "type": "object" },
|
|
58
|
-
"
|
|
52
|
+
"extensionToShikiLanguage": {
|
|
59
53
|
"type": "object",
|
|
60
54
|
"additionalProperties": { "type": "string" }
|
|
61
|
-
}
|
|
55
|
+
},
|
|
56
|
+
"shikiLanguages": { "type": "array", "items": { "type": "string" } }
|
|
62
57
|
},
|
|
63
58
|
"additionalProperties": false
|
|
64
59
|
}
|
package/src/page-update/index.ts
CHANGED