@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 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, "code": true, "favicon": 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
- "codeLanguages": { "java": "java", "py": "python" },
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
- | `codeLanguages` | Map file extension to Shiki language (e.g., `"java": "java"`) |
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, code: true, favicon: false, footer: 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, code: true, favicon: true, footer: 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
- codeLanguages: { java: 'java', py: 'python' },
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 { isFeatureEnabled } from './features';
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
- if (rewriteCodeLinks) {
83
- for (const ext of Object.keys(siteVariables.codeLanguages ?? {})) {
84
- if (modifiedPath.endsWith(`.${ext}`)) {
85
- if (literateJavaOutputPaths && sourceUrlPath) {
86
- const resolved = resolveAgainstSource(modifiedPath);
87
- if (literateJavaOutputPaths.has(resolved)) {
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
- // Only rewrite if the .html version exists. Public files with
92
- // code extensions are copied as-is and have no .html page.
93
- if (validTargets && sourceUrlPath) {
94
- const resolved = resolveAgainstSource(modifiedPath);
95
- if (!validTargets.has(`${resolved}.html`)) {
96
- break;
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.startsWith('/')) {
141
- const afterApply = applyBasePath(href);
142
- log.debug`Applying base path to raw HTML anchor: ${href} -> ${afterApply}`;
143
- anchor.setAttribute('href', afterApply);
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.startsWith('/')) {
154
- const afterApply = applyBasePath(href);
155
- log.debug`Applying base path to raw HTML anchor: ${href} -> ${afterApply}`;
156
- anchor.setAttribute('href', afterApply);
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
  }
@@ -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.codeLanguages || {}),
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 { isFeatureEnabled } from './features';
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
- const langs = [
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.codeLanguages || {};
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.codeLanguages || {};
144
+ const codeExtensions = getExtensionToShikiLanguage(this.siteVariables);
151
145
  if (ext.slice(1) in codeExtensions) {
152
- if (isFeatureEnabled(this.siteVariables, 'code')) {
153
- assets.push(
154
- ...renderCodePageAsset({
155
- filePath,
156
- contentDir,
157
- distDir,
158
- siteVariables: this.siteVariables,
159
- assetFiles,
160
- }),
161
- );
162
- } else if (!this.loggedCodeDisabled) {
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.codeLanguages || {}),
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.codeLanguages || {}),
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.codeLanguages || {}),
74
+ Object.keys(getExtensionToShikiLanguage(siteVariables)),
74
75
  );
75
76
  copyContentAssets(contentDir, distDir, processedExtensions, publicRelPaths);
76
77
 
@@ -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, code: true, favicon: true, footer: 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
- codeLanguages?: Record<string, string>;
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
  }
@@ -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 { getHighlighter } from './shiki-highlighter';
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(siteVariables.codeLanguages ?? {});
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 highlighter = getHighlighter();
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
- if (codeEnabled) {
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
- if (codeEnabled) {
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
- if (codeEnabled) {
224
- addGeneratedRouteAliases(targets, `/${relPath}.html`);
225
- }
216
+ addGeneratedRouteAliases(targets, `/${relPath}.html`);
226
217
  targets.add(normalizeOutputPath(`/${relPath}`));
227
218
  }
228
219
  }
@@ -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 { getHighlighter } from './shiki-highlighter';
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
- const highlighter = getHighlighter();
356
- const useLang = highlighter.getLoadedLanguages().includes(lang)
357
- ? lang
358
- : 'text';
359
- return highlighter.codeToHtml(code, {
360
- lang: useLang,
361
- themes: { light: 'github-light', dark: 'github-dark' },
362
- defaultColor: false,
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;
@@ -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 = siteVariables.codeLanguages![ext.slice(1).toLowerCase()];
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: isFeatureEnabled(siteVariables, 'code')
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: isFeatureEnabled(siteVariables, 'code')
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 { HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki';
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
- export async function initHighlighter(langs: string[]): Promise<void> {
10
- if (highlighter) {
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
- log.debug`Initializing syntax highlighter`;
14
- const { createHighlighter } = await import('shiki');
15
- highlighter = await createHighlighter({
16
- themes: ['github-light', 'github-dark'],
17
- langs,
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` only when
10
- * `features.code` is enabled.
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,
@@ -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
- const langs = [
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(
@@ -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(siteVariables.codeLanguages || {}).map(
91
- ext => ext.toLowerCase(),
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
- if (codeEnabled) {
132
- assets.push(
133
- ...renderCodePageAsset({
134
- filePath,
135
- contentDir: scan.contentDir,
136
- distDir: outputDir,
137
- siteVariables,
138
- assetFiles,
139
- dependencyCollector: deps.collector,
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,
@@ -8,7 +8,7 @@ import {
8
8
  shouldSkipContentFile,
9
9
  toPosix,
10
10
  } from '../util';
11
- import { isFeatureEnabled } from '../features';
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(Object.keys(siteVariables.codeLanguages || {})).map(
110
- ext => ext.toLowerCase(),
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
- if (codeEnabled) {
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
- if (codeEnabled) {
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 when `features.code` is `true`.)
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
- Syntax highlighting is controlled by the `codeLanguages` configuration object
39
- and is generated at build time by [Shiki](https://shiki.style).
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
@@ -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
- ```markdown
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
- ```markdown
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
- ```text
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
- ```markdown
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
- ```markdown
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
- ```markdown
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
- ```markdown
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
  }
@@ -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 `features.code` is `true`) and it won't appear in search results
4
- /// (even if `features.search` is `true`).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.10.2",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -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
- "codeLanguages": {
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
  }
@@ -8,7 +8,7 @@ import {
8
8
  } from '../validators';
9
9
  import { NAVIGATION_EVENT, refreshCurrentPage } from '../navigate/runtime';
10
10
 
11
- const POLL_INTERVAL_MS = 60_000;
11
+ const POLL_INTERVAL_MS = 600_000;
12
12
 
13
13
  export interface PageUpdateOptions {
14
14
  pollIntervalMs?: number;