@abreen/tada 1.1.0 → 1.3.1
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 +69 -17
- package/bin/tada.ts +317 -25
- package/bin/validators.test.ts +4 -3
- package/bin/validators.ts +4 -2
- package/build/apply-base-path-plugin.ts +24 -13
- package/build/build-manifest.test.ts +349 -0
- package/build/build-manifest.ts +165 -0
- package/build/bundle.ts +19 -20
- package/build/code.test.ts +7 -3
- package/build/content-watch.ts +6 -6
- package/build/copy.ts +2 -2
- package/build/deflist-id-plugin.ts +1 -1
- package/build/external-links-plugin.ts +2 -2
- package/build/features.ts +1 -1
- package/build/generate-content-assets.ts +12 -7
- package/build/generate-favicon.ts +13 -6
- package/build/generate-fonts.ts +2 -2
- package/build/generate-katex-assets.ts +35 -0
- package/build/generate-manifest.ts +2 -2
- package/build/globals.test.ts +10 -11
- package/build/globals.ts +14 -7
- package/build/json-schema.test.ts +1 -1
- package/build/log.test.ts +1 -1
- package/build/log.ts +2 -2
- package/build/markdown-plugins.test.ts +116 -10
- package/build/pagefind.test.ts +2 -2
- package/build/pagefind.ts +5 -5
- package/build/pdf-text.test.ts +52 -0
- package/build/pipeline.ts +42 -15
- package/build/reachability.test.ts +1 -1
- package/build/serve.ts +20 -8
- package/build/site-variables.ts +13 -5
- package/build/templates.ts +16 -7
- package/build/toc-plugin.test.ts +2 -2
- package/build/toc-plugin.ts +1 -1
- package/build/types.ts +15 -6
- package/build/util.ts +6 -6
- package/build/utils/code.ts +5 -5
- package/build/utils/content-files.ts +3 -3
- package/build/utils/derive-theme.test.ts +1 -1
- package/build/utils/derive-theme.ts +1 -1
- package/build/utils/file-types.test.ts +1 -1
- package/build/utils/front-matter.test.ts +1 -4
- package/build/utils/front-matter.ts +2 -2
- package/build/utils/jdi-runner/LiterateRunner.java +13 -0
- package/build/utils/literate-java.ts +24 -12
- package/build/utils/markdown.ts +41 -9
- package/build/utils/paths.test.ts +2 -2
- package/build/utils/paths.ts +5 -1
- package/build/utils/render.ts +57 -36
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/validate-internal-links-plugin.test.ts +1 -1
- package/build/validate-internal-links-plugin.ts +1 -1
- package/build/watch-reachability-state.test.ts +2 -2
- package/build/watch-reachability-state.ts +1 -1
- package/build/watch.ts +91 -110
- package/init/content/labs/00/VowelCounter.java.md +129 -0
- package/init/content/labs/00/index.md +29 -0
- package/init/content/labs/index.md +9 -0
- package/{content → init/content}/lectures/01/Pair.java.md +1 -0
- package/{content → init/content}/lectures/index.md +1 -1
- package/{content → init/content}/markdown.md +26 -7
- package/package.json +19 -7
- package/{templates → schema}/nav.schema.json +1 -2
- package/{build → schema}/site.schema.json +3 -5
- package/src/_alerts.scss +4 -0
- package/src/_base.scss +90 -6
- package/src/_layout.scss +39 -2
- package/src/anchor/index.ts +0 -1
- package/src/index.ts +3 -3
- package/src/question/style.scss +8 -3
- package/src/search/index.test.ts +203 -0
- package/src/search/index.ts +3 -79
- package/src/search/pdf-utils.ts +79 -0
- package/src/search/style.scss +2 -20
- package/src/style.scss +20 -101
- package/src/timezone/index.ts +9 -25
- package/src/util.ts +1 -1
- package/templates/_top.html +2 -1
- package/types/dev.d.ts +1 -3
- package/types/site-variables.d.ts +6 -10
- package/types/untyped-modules.d.ts +7 -0
- package/build/utils/jdi-runner/LiterateRunner.class +0 -0
- package/config/authors.json +0 -1
- package/config/nav.json +0 -28
- /package/{content → init/content}/index.md +0 -0
- /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
- /package/{content → init/content}/lectures/01/demo.py +0 -0
- /package/{content → init/content}/lectures/01/index.md +0 -0
- /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
- /package/{content → init/content}/problem_sets/index.html +0 -0
- /package/{public → init/public}/avatars/alex.jpg +0 -0
- /package/{public → init/public}/test.txt +0 -0
- /package/{templates → schema}/authors.schema.json +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { makeLogger } from './log
|
|
4
|
-
import { isFeatureEnabled } from './features
|
|
5
|
-
import { initHighlighter } from './utils/shiki-highlighter
|
|
3
|
+
import { makeLogger } from './log';
|
|
4
|
+
import { isFeatureEnabled } from './features';
|
|
5
|
+
import { initHighlighter } from './utils/shiki-highlighter';
|
|
6
6
|
import {
|
|
7
7
|
getBuildContentFiles,
|
|
8
8
|
getContentDir,
|
|
@@ -11,16 +11,16 @@ import {
|
|
|
11
11
|
renderCopiedContentAsset,
|
|
12
12
|
renderLiterateJavaPageAsset,
|
|
13
13
|
renderPlainTextPageAsset,
|
|
14
|
-
} from './util
|
|
15
|
-
import { isLiterateJava } from './utils/file-types
|
|
16
|
-
import { checkJavac } from './utils/literate-java
|
|
14
|
+
} from './util';
|
|
15
|
+
import { isLiterateJava } from './utils/file-types';
|
|
16
|
+
import { checkJavac } from './utils/literate-java';
|
|
17
17
|
import type {
|
|
18
18
|
SiteVariables,
|
|
19
19
|
Asset,
|
|
20
20
|
ContentRenderOptions,
|
|
21
21
|
ContentRenderResult,
|
|
22
22
|
WatchState,
|
|
23
|
-
} from './types
|
|
23
|
+
} from './types';
|
|
24
24
|
|
|
25
25
|
const log = makeLogger(__filename);
|
|
26
26
|
|
|
@@ -102,6 +102,7 @@ export class ContentRenderer {
|
|
|
102
102
|
renderSourceAssets(
|
|
103
103
|
filePath: string,
|
|
104
104
|
contentDir: string,
|
|
105
|
+
distDir: string,
|
|
105
106
|
validInternalTargets: Set<string>,
|
|
106
107
|
assetFiles: string[],
|
|
107
108
|
): Asset[] {
|
|
@@ -113,6 +114,7 @@ export class ContentRenderer {
|
|
|
113
114
|
...renderLiterateJavaPageAsset({
|
|
114
115
|
filePath,
|
|
115
116
|
contentDir,
|
|
117
|
+
distDir,
|
|
116
118
|
siteVariables: this.siteVariables,
|
|
117
119
|
assetFiles,
|
|
118
120
|
skipExecution: !this.javacAvailable,
|
|
@@ -126,6 +128,7 @@ export class ContentRenderer {
|
|
|
126
128
|
...renderPlainTextPageAsset({
|
|
127
129
|
filePath,
|
|
128
130
|
contentDir,
|
|
131
|
+
distDir,
|
|
129
132
|
siteVariables: this.siteVariables,
|
|
130
133
|
validInternalTargets,
|
|
131
134
|
assetFiles,
|
|
@@ -141,6 +144,7 @@ export class ContentRenderer {
|
|
|
141
144
|
...renderCodePageAsset({
|
|
142
145
|
filePath,
|
|
143
146
|
contentDir,
|
|
147
|
+
distDir,
|
|
144
148
|
siteVariables: this.siteVariables,
|
|
145
149
|
assetFiles,
|
|
146
150
|
}),
|
|
@@ -274,6 +278,7 @@ export class ContentRenderer {
|
|
|
274
278
|
assets = this.renderSourceAssets(
|
|
275
279
|
filePath,
|
|
276
280
|
contentDir,
|
|
281
|
+
distDir,
|
|
277
282
|
validInternalTargets,
|
|
278
283
|
assetFiles,
|
|
279
284
|
);
|
|
@@ -3,10 +3,10 @@ import path from 'path';
|
|
|
3
3
|
import * as fontkit from 'fontkit';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import pngToIco from 'png-to-ico';
|
|
6
|
-
import { makeLogger } from './log
|
|
7
|
-
import { getPackageDir } from './utils/paths
|
|
8
|
-
import { deriveTheme } from './utils/derive-theme
|
|
9
|
-
import type { SiteVariables } from './types
|
|
6
|
+
import { makeLogger } from './log';
|
|
7
|
+
import { getPackageDir } from './utils/paths';
|
|
8
|
+
import { deriveTheme } from './utils/derive-theme';
|
|
9
|
+
import type { SiteVariables } from './types';
|
|
10
10
|
|
|
11
11
|
const log = makeLogger(__filename);
|
|
12
12
|
|
|
@@ -117,8 +117,15 @@ export async function generateFavicons(
|
|
|
117
117
|
const svgSize = 512;
|
|
118
118
|
const filenameBase = 'favicon';
|
|
119
119
|
|
|
120
|
-
if (!
|
|
121
|
-
throw new Error(
|
|
120
|
+
if (!symbol) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Favicon generation requires "symbol" in site config (or "faviconSymbol")',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (!color) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
'Favicon generation requires "themeColor" in site config (or "faviconColor")',
|
|
128
|
+
);
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
const { themeColorLight, textOnThemeLight } = deriveTheme(color);
|
package/build/generate-fonts.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { getPackageDir } from './utils/paths
|
|
4
|
-
import { makeLogger } from './log
|
|
3
|
+
import { getPackageDir } from './utils/paths';
|
|
4
|
+
import { makeLogger } from './log';
|
|
5
5
|
|
|
6
6
|
const log = makeLogger(__filename);
|
|
7
7
|
const FONTS_DIR = path.join(getPackageDir(), 'fonts');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as sass from 'sass';
|
|
4
|
+
import { makeLogger } from './log';
|
|
5
|
+
|
|
6
|
+
const log = makeLogger(__filename);
|
|
7
|
+
|
|
8
|
+
export function copyKatexAssets(distDir: string): void {
|
|
9
|
+
log.info`Copying KaTeX assets`;
|
|
10
|
+
|
|
11
|
+
const outDir = path.join(distDir, 'katex');
|
|
12
|
+
const outFontsDir = path.join(outDir, 'fonts');
|
|
13
|
+
fs.mkdirSync(outFontsDir, { recursive: true });
|
|
14
|
+
|
|
15
|
+
// Compile KaTeX SCSS with woff2-only font references
|
|
16
|
+
const katexScssDir = path.dirname(
|
|
17
|
+
require.resolve('katex/src/styles/katex.scss'),
|
|
18
|
+
);
|
|
19
|
+
const result = sass.compileString(
|
|
20
|
+
`@use 'katex' with ($use-woff2: true, $use-woff: false, $use-ttf: false, $font-folder: 'fonts');`,
|
|
21
|
+
{ loadPaths: [katexScssDir], style: 'compressed' },
|
|
22
|
+
);
|
|
23
|
+
fs.writeFileSync(path.join(outDir, 'katex.min.css'), result.css);
|
|
24
|
+
|
|
25
|
+
// Copy woff2 fonts
|
|
26
|
+
const katexDistDir = path.dirname(
|
|
27
|
+
require.resolve('katex/dist/katex.min.css'),
|
|
28
|
+
);
|
|
29
|
+
const fontsDir = path.join(katexDistDir, 'fonts');
|
|
30
|
+
for (const file of fs.readdirSync(fontsDir)) {
|
|
31
|
+
if (file.endsWith('.woff2')) {
|
|
32
|
+
fs.copyFileSync(path.join(fontsDir, file), path.join(outFontsDir, file));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { createApplyBasePath } from './util
|
|
4
|
-
import type { SiteVariables } from './types
|
|
3
|
+
import { createApplyBasePath } from './util';
|
|
4
|
+
import type { SiteVariables } from './types';
|
|
5
5
|
|
|
6
6
|
interface ManifestIcon {
|
|
7
7
|
src: string;
|
package/build/globals.test.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import createGlobals from './globals
|
|
3
|
-
import type { SiteVariables } from './types
|
|
2
|
+
import createGlobals from './globals';
|
|
3
|
+
import type { SiteVariables } from './types';
|
|
4
4
|
|
|
5
5
|
const baseSite: SiteVariables = {
|
|
6
6
|
base: 'http://localhost:8080',
|
|
7
7
|
basePath: '/',
|
|
8
|
+
title: 'Test',
|
|
9
|
+
titlePostfix: ' - Test',
|
|
10
|
+
themeColor: 'hsl(195 70% 40%)',
|
|
11
|
+
defaultTimeZone: 'America/New_York',
|
|
8
12
|
};
|
|
9
13
|
|
|
10
14
|
function makeGlobals(subPath = 'index', site: Partial<SiteVariables> = {}) {
|
|
@@ -26,16 +30,11 @@ describe('createGlobals', () => {
|
|
|
26
30
|
expect(g.cx).toBe(g.classNames);
|
|
27
31
|
});
|
|
28
32
|
|
|
29
|
-
test('
|
|
33
|
+
test('renderTimeZoneChooser includes hidden disabled select, inline script, and noscript', () => {
|
|
30
34
|
const g = makeGlobals('index', { defaultTimeZone: 'America/New_York' });
|
|
31
|
-
|
|
32
|
-
expect(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
test('timezoneChooser has no noscript when defaultTimeZone is not set', () => {
|
|
36
|
-
const g = makeGlobals('index');
|
|
37
|
-
expect(g.timezoneChooser).toContain('select');
|
|
38
|
-
expect(g.timezoneChooser).not.toContain('noscript');
|
|
35
|
+
const html = g.renderTimeZoneChooser();
|
|
36
|
+
expect(html).toContain('<select class="time-zone" hidden disabled>');
|
|
37
|
+
expect(html).toContain('<noscript>Times shown in ET.</noscript>');
|
|
39
38
|
});
|
|
40
39
|
});
|
|
41
40
|
|
package/build/globals.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
2
|
-
import type { SiteVariables } from './types
|
|
2
|
+
import type { SiteVariables } from './types';
|
|
3
3
|
|
|
4
4
|
interface Globals {
|
|
5
5
|
isHomePage: boolean;
|
|
@@ -7,7 +7,7 @@ interface Globals {
|
|
|
7
7
|
readableDate: (date: string | Date | null | undefined) => string;
|
|
8
8
|
classNames: (obj: Record<string, unknown>) => string;
|
|
9
9
|
cx: (obj: Record<string, unknown>) => string;
|
|
10
|
-
|
|
10
|
+
renderTimeZoneChooser: () => string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export default function createGlobals(
|
|
@@ -17,10 +17,17 @@ export default function createGlobals(
|
|
|
17
17
|
): Globals {
|
|
18
18
|
const defaultTz = timezones.find(
|
|
19
19
|
t => t.value === siteVariables.defaultTimeZone,
|
|
20
|
-
)
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
)!;
|
|
21
|
+
const options = timezones
|
|
22
|
+
.map(tz => {
|
|
23
|
+
const label =
|
|
24
|
+
tz.value === 'UTC' ? tz.label : `${tz.label} (${tz.abbreviation})`;
|
|
25
|
+
const selected = tz.value === defaultTz.value ? ' selected' : '';
|
|
26
|
+
return `<option value="${tz.value}"${selected}>${label}</option>`;
|
|
27
|
+
})
|
|
28
|
+
.join('');
|
|
29
|
+
const renderTimeZoneChooser = () =>
|
|
30
|
+
`<span class="time-zone-label" hidden>Times shown in </span><select class="time-zone" hidden disabled>${options}</select><noscript>Times shown in ${defaultTz.abbreviation}.</noscript>`;
|
|
24
31
|
|
|
25
32
|
return {
|
|
26
33
|
isHomePage: subPath === 'index',
|
|
@@ -28,7 +35,7 @@ export default function createGlobals(
|
|
|
28
35
|
readableDate,
|
|
29
36
|
classNames,
|
|
30
37
|
cx: classNames,
|
|
31
|
-
|
|
38
|
+
renderTimeZoneChooser,
|
|
32
39
|
};
|
|
33
40
|
}
|
|
34
41
|
|
package/build/log.test.ts
CHANGED
package/build/log.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { Gi, L, Ri, P, Yi, Li } from './colors
|
|
4
|
-
import type { Logger } from './types
|
|
3
|
+
import { Gi, L, Ri, P, Yi, Li } from './colors';
|
|
4
|
+
import type { Logger } from './types';
|
|
5
5
|
import FLAIR_STRINGS from './flair.json' with { type: 'json' };
|
|
6
6
|
|
|
7
7
|
const LEVELS = ['debug', 'info', 'warn', 'error'] as const;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
import MarkdownIt from 'markdown-it';
|
|
3
3
|
import deflist from 'markdown-it-deflist';
|
|
4
|
-
import applyBasePathPlugin from './apply-base-path-plugin
|
|
5
|
-
import deflistIdPlugin from './deflist-id-plugin
|
|
6
|
-
import externalLinksPlugin from './external-links-plugin
|
|
7
|
-
import headingSubtitlePlugin from './heading-subtitle-plugin
|
|
8
|
-
import { createMarkdown } from './utils/markdown
|
|
9
|
-
import { stripHtmlComments } from './utils/render
|
|
10
|
-
import type { SiteVariables } from './types
|
|
4
|
+
import applyBasePathPlugin from './apply-base-path-plugin';
|
|
5
|
+
import deflistIdPlugin from './deflist-id-plugin';
|
|
6
|
+
import externalLinksPlugin from './external-links-plugin';
|
|
7
|
+
import headingSubtitlePlugin from './heading-subtitle-plugin';
|
|
8
|
+
import { createMarkdown } from './utils/markdown';
|
|
9
|
+
import { stripHtmlComments, injectKatexStylesheet } from './utils/render';
|
|
10
|
+
import type { SiteVariables } from './types';
|
|
11
11
|
|
|
12
12
|
describe('apply-base-path-plugin', () => {
|
|
13
|
-
test('rewrites internal links, images, and raw html
|
|
13
|
+
test('rewrites internal links, images, and raw html sources', () => {
|
|
14
14
|
const md = new MarkdownIt({ html: true });
|
|
15
15
|
md.use(applyBasePathPlugin, {
|
|
16
16
|
basePath: '/course/',
|
|
@@ -25,12 +25,35 @@ describe('apply-base-path-plugin', () => {
|
|
|
25
25
|
'',
|
|
26
26
|
'',
|
|
27
27
|
'<img src="/images/raw.png" alt="Raw">',
|
|
28
|
+
'',
|
|
29
|
+
'<a href="/about/">About</a>',
|
|
28
30
|
].join('\n'),
|
|
29
31
|
);
|
|
30
32
|
|
|
31
33
|
expect(html).toContain('href="/course/src/example.html?view=1#top"');
|
|
32
34
|
expect(html).toContain('src="/course/images/pic.png"');
|
|
33
35
|
expect(html).toContain('src="/course/images/raw.png"');
|
|
36
|
+
expect(html).toContain('href="/course/about/"');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('does not rewrite external or relative raw html links', () => {
|
|
40
|
+
const md = new MarkdownIt({ html: true });
|
|
41
|
+
md.use(applyBasePathPlugin, {
|
|
42
|
+
basePath: '/course/',
|
|
43
|
+
codeLanguages: {},
|
|
44
|
+
features: { code: true },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const html = md.render(
|
|
48
|
+
[
|
|
49
|
+
'<a href="https://example.com/">External</a>',
|
|
50
|
+
'',
|
|
51
|
+
'<a href="relative.html">Relative</a>',
|
|
52
|
+
].join('\n'),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(html).toContain('href="https://example.com/"');
|
|
56
|
+
expect(html).toContain('href="relative.html"');
|
|
34
57
|
});
|
|
35
58
|
|
|
36
59
|
test('rewrites relative code file links without applying base path', () => {
|
|
@@ -122,7 +145,11 @@ describe('custom markdown containers', () => {
|
|
|
122
145
|
basePath: '/',
|
|
123
146
|
internalDomains: [],
|
|
124
147
|
codeLanguages: {},
|
|
125
|
-
features: {},
|
|
148
|
+
features: { search: true, code: true, favicon: false },
|
|
149
|
+
title: 'Test',
|
|
150
|
+
titlePostfix: ' - Test',
|
|
151
|
+
themeColor: 'steelblue',
|
|
152
|
+
defaultTimeZone: 'America/New_York',
|
|
126
153
|
} as SiteVariables,
|
|
127
154
|
{ validatorOptions: { enabled: false } },
|
|
128
155
|
);
|
|
@@ -218,7 +245,11 @@ describe('hidden_fence rule', () => {
|
|
|
218
245
|
basePath: '/',
|
|
219
246
|
internalDomains: [],
|
|
220
247
|
codeLanguages: {},
|
|
221
|
-
features: {},
|
|
248
|
+
features: { search: true, code: true, favicon: false },
|
|
249
|
+
title: 'Test',
|
|
250
|
+
titlePostfix: ' - Test',
|
|
251
|
+
themeColor: 'steelblue',
|
|
252
|
+
defaultTimeZone: 'America/New_York',
|
|
222
253
|
} as SiteVariables,
|
|
223
254
|
{ validatorOptions: { enabled: false } },
|
|
224
255
|
);
|
|
@@ -286,3 +317,78 @@ describe('stripHtmlComments', () => {
|
|
|
286
317
|
expect(stripHtmlComments(input)).toBe(input);
|
|
287
318
|
});
|
|
288
319
|
});
|
|
320
|
+
|
|
321
|
+
describe('katex plugin', () => {
|
|
322
|
+
function createProjectMarkdown() {
|
|
323
|
+
return createMarkdown(
|
|
324
|
+
{
|
|
325
|
+
base: '',
|
|
326
|
+
basePath: '/',
|
|
327
|
+
internalDomains: [],
|
|
328
|
+
codeLanguages: {},
|
|
329
|
+
features: { search: true, code: true, favicon: false },
|
|
330
|
+
title: 'Test',
|
|
331
|
+
titlePostfix: ' - Test',
|
|
332
|
+
themeColor: 'steelblue',
|
|
333
|
+
defaultTimeZone: 'America/New_York',
|
|
334
|
+
} as SiteVariables,
|
|
335
|
+
{ validatorOptions: { enabled: false } },
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
test('renders inline math with $ delimiters', () => {
|
|
340
|
+
const md = createProjectMarkdown();
|
|
341
|
+
const html = md.render('The equation $E = mc^2$ is famous.');
|
|
342
|
+
expect(html).toContain('class="katex"');
|
|
343
|
+
expect(html).toContain('mc');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('renders display math with $$ delimiters', () => {
|
|
347
|
+
const md = createProjectMarkdown();
|
|
348
|
+
const html = md.render('$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$');
|
|
349
|
+
expect(html).toContain('class="katex-display"');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('includes aria-label for inline math', () => {
|
|
353
|
+
const md = createProjectMarkdown();
|
|
354
|
+
const html = md.render('$E = mc^2$');
|
|
355
|
+
expect(html).toContain('aria-label="E, equals, m, c, squared"');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('includes aria-label for display math', () => {
|
|
359
|
+
const md = createProjectMarkdown();
|
|
360
|
+
const html = md.render('$$\\frac{1}{2}$$');
|
|
361
|
+
expect(html).toContain(
|
|
362
|
+
'aria-label="start fraction, 1, divided by, 2, end fraction"',
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('throws on invalid LaTeX syntax', () => {
|
|
367
|
+
const md = createProjectMarkdown();
|
|
368
|
+
expect(() => md.render('$\\invalidcommand{$')).toThrow();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('does not produce katex output when no math delimiters are used', () => {
|
|
372
|
+
const md = createProjectMarkdown();
|
|
373
|
+
const html = md.render('Just a normal paragraph with a $5 price tag.');
|
|
374
|
+
expect(html).not.toContain('class="katex"');
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('injectKatexStylesheet', () => {
|
|
379
|
+
test('injects deferred KaTeX stylesheet link into <head>', () => {
|
|
380
|
+
const html =
|
|
381
|
+
'<html><head><meta charset="UTF-8"></head><body></body></html>';
|
|
382
|
+
const result = injectKatexStylesheet(html, p => p);
|
|
383
|
+
|
|
384
|
+
expect(result).toContain('href="/katex/katex.min.css"');
|
|
385
|
+
expect(result).toContain('rel="stylesheet"');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('applies basePath to the stylesheet URL', () => {
|
|
389
|
+
const html = '<html><head></head><body></body></html>';
|
|
390
|
+
const result = injectKatexStylesheet(html, p => '/course' + p);
|
|
391
|
+
|
|
392
|
+
expect(result).toContain('href="/course/katex/katex.min.css"');
|
|
393
|
+
});
|
|
394
|
+
});
|
package/build/pagefind.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { describe, expect, test } from 'bun:test';
|
|
3
|
-
import { buildIndex, collectIndexTargets } from './pagefind
|
|
4
|
-
import type { SiteVariables } from './types
|
|
3
|
+
import { buildIndex, collectIndexTargets } from './pagefind';
|
|
4
|
+
import type { SiteVariables } from './types';
|
|
5
5
|
|
|
6
6
|
interface FakePagefindCalls {
|
|
7
7
|
htmlFiles?: { sourcePath: string; content: string }[];
|
package/build/pagefind.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { makeLogger } from './log
|
|
3
|
-
import { collectReachableSiteAssets } from './reachability
|
|
2
|
+
import { makeLogger } from './log';
|
|
3
|
+
import { collectReachableSiteAssets } from './reachability';
|
|
4
4
|
import {
|
|
5
5
|
getContentDir,
|
|
6
6
|
getFilesByExtensions,
|
|
7
7
|
normalizeOutputPath,
|
|
8
|
-
} from './util
|
|
9
|
-
import { assertMutoolAvailable, extractPdfPages } from './pdf-text
|
|
10
|
-
import type { SiteVariables } from './types
|
|
8
|
+
} from './util';
|
|
9
|
+
import { assertMutoolAvailable, extractPdfPages } from './pdf-text';
|
|
10
|
+
import type { SiteVariables } from './types';
|
|
11
11
|
|
|
12
12
|
const log = makeLogger(__filename);
|
|
13
13
|
const PAGEFIND_VERBOSE = process.env.TADA_LOG_LEVEL === 'debug';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { buildPdfPageRecords } from './pdf-text';
|
|
3
|
+
|
|
4
|
+
describe('buildPdfPageRecords', () => {
|
|
5
|
+
test('creates page records with 1-based page numbers', () => {
|
|
6
|
+
const result = buildPdfPageRecords(['Hello world', 'Second page']);
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
pages: [
|
|
9
|
+
{ pageNumber: 1, content: 'Hello world' },
|
|
10
|
+
{ pageNumber: 2, content: 'Second page' },
|
|
11
|
+
],
|
|
12
|
+
hasExtractedText: true,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('normalizes whitespace in extracted text', () => {
|
|
17
|
+
const result = buildPdfPageRecords([' lots of\n\tspaces ']);
|
|
18
|
+
expect(result).toEqual({
|
|
19
|
+
pages: [{ pageNumber: 1, content: 'lots of spaces' }],
|
|
20
|
+
hasExtractedText: true,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('skips blank pages', () => {
|
|
25
|
+
const result = buildPdfPageRecords(['Page one', '', ' ', 'Page four']);
|
|
26
|
+
expect(result).toEqual({
|
|
27
|
+
pages: [
|
|
28
|
+
{ pageNumber: 1, content: 'Page one' },
|
|
29
|
+
{ pageNumber: 4, content: 'Page four' },
|
|
30
|
+
],
|
|
31
|
+
hasExtractedText: true,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('returns hasExtractedText false when all pages are blank', () => {
|
|
36
|
+
const result = buildPdfPageRecords(['', ' ', '\n\t']);
|
|
37
|
+
expect(result).toEqual({ pages: [], hasExtractedText: false });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('returns hasExtractedText false for empty input', () => {
|
|
41
|
+
const result = buildPdfPageRecords([]);
|
|
42
|
+
expect(result).toEqual({ pages: [], hasExtractedText: false });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('handles a single page', () => {
|
|
46
|
+
const result = buildPdfPageRecords(['Only page']);
|
|
47
|
+
expect(result).toEqual({
|
|
48
|
+
pages: [{ pageNumber: 1, content: 'Only page' }],
|
|
49
|
+
hasExtractedText: true,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
package/build/pipeline.ts
CHANGED
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import { getDevSiteVariables, getProdSiteVariables } from './site-variables
|
|
4
|
-
import { compileTemplates } from './templates
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
3
|
+
import { getDevSiteVariables, getProdSiteVariables } from './site-variables';
|
|
4
|
+
import { compileTemplates } from './templates';
|
|
5
|
+
import {
|
|
6
|
+
getDistDir,
|
|
7
|
+
getProdDistDir,
|
|
8
|
+
getContentDir,
|
|
9
|
+
getPublicDir,
|
|
10
|
+
} from './utils/paths';
|
|
11
|
+
import { isFeatureEnabled } from './features';
|
|
12
|
+
import { bundle } from './bundle';
|
|
13
|
+
import { copyFonts } from './generate-fonts';
|
|
14
|
+
import { copyKatexAssets } from './generate-katex-assets';
|
|
15
|
+
import { generateFavicons } from './generate-favicon';
|
|
16
|
+
import { generateManifest } from './generate-manifest';
|
|
17
|
+
import { copyPublicFiles, copyContentAssets } from './copy';
|
|
18
|
+
import { getProcessedExtensions } from './utils/file-types';
|
|
19
|
+
import { ContentRenderer } from './generate-content-assets';
|
|
20
|
+
import { runPagefind } from './pagefind';
|
|
21
|
+
import { makeLogger, printFlair } from './log';
|
|
22
|
+
import { generateBuildManifest, getNextVersion } from './build-manifest';
|
|
23
|
+
import path from 'path';
|
|
16
24
|
|
|
17
25
|
const log = makeLogger(__filename);
|
|
18
26
|
|
|
19
27
|
async function runPipeline(mode: 'development' | 'production'): Promise<void> {
|
|
20
28
|
const isDev = mode === 'development';
|
|
21
29
|
const siteVariables = isDev ? getDevSiteVariables() : getProdSiteVariables();
|
|
22
|
-
|
|
30
|
+
let distDir: string;
|
|
31
|
+
let prodVersion: number;
|
|
32
|
+
|
|
33
|
+
if (isDev) {
|
|
34
|
+
distDir = getDistDir();
|
|
35
|
+
} else {
|
|
36
|
+
const prodBase = getProdDistDir();
|
|
37
|
+
prodVersion = getNextVersion(prodBase);
|
|
38
|
+
distDir = path.join(prodBase, `v${prodVersion}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
const contentDir = getContentDir();
|
|
24
42
|
const publicDir = getPublicDir();
|
|
25
43
|
|
|
@@ -33,8 +51,9 @@ async function runPipeline(mode: 'development' | 'production'): Promise<void> {
|
|
|
33
51
|
|
|
34
52
|
// Phase 2: Bundle + assets (parallel)
|
|
35
53
|
const parallelTasks: (Promise<unknown> | void)[] = [
|
|
36
|
-
bundle(siteVariables, { mode }),
|
|
54
|
+
bundle(siteVariables, { mode, distDir }),
|
|
37
55
|
copyFonts(distDir),
|
|
56
|
+
copyKatexAssets(distDir),
|
|
38
57
|
];
|
|
39
58
|
|
|
40
59
|
if (isFeatureEnabled(siteVariables, 'favicon')) {
|
|
@@ -72,6 +91,14 @@ async function runPipeline(mode: 'development' | 'production'): Promise<void> {
|
|
|
72
91
|
await runPagefind({ siteVariables, distPath: distDir, htmlAssetsByPath });
|
|
73
92
|
}
|
|
74
93
|
|
|
94
|
+
// Phase 5: Build manifest (production only)
|
|
95
|
+
if (!isDev) {
|
|
96
|
+
const prodBase = getProdDistDir();
|
|
97
|
+
const manifestPath = path.join(prodBase, `v${prodVersion!}.manifest.json`);
|
|
98
|
+
await generateBuildManifest(distDir, manifestPath);
|
|
99
|
+
log.info`Built dist-prod/v${prodVersion!}/`;
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
printFlair();
|
|
76
103
|
}
|
|
77
104
|
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import {
|
|
3
3
|
collectDirectSiteAssetLinks,
|
|
4
4
|
collectReachableSiteAssets,
|
|
5
|
-
} from './reachability
|
|
5
|
+
} from './reachability';
|
|
6
6
|
|
|
7
7
|
describe('reachability', () => {
|
|
8
8
|
test('collectDirectSiteAssetLinks resolves relative links and ignores excluded links', () => {
|