@abreen/tada 1.4.1 → 1.5.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 +4 -2
- package/bin/tada.ts +81 -57
- package/build/apply-base-path-plugin.ts +22 -6
- package/build/code.test.ts +108 -1
- package/build/colors.test.ts +66 -0
- package/build/columns-plugin.ts +116 -0
- package/build/content-watch.ts +30 -21
- package/build/copy.ts +1 -1
- package/build/external-links-plugin.ts +1 -1
- package/build/generate-content-assets.ts +48 -6
- package/build/generate-favicon.ts +1 -1
- package/build/generate-fonts.ts +1 -1
- package/build/generate-katex-assets.ts +1 -1
- package/build/log.test.ts +7 -1
- package/build/log.ts +4 -1
- package/build/markdown-plugins.test.ts +65 -3
- package/build/pagefind.ts +1 -1
- package/build/pipeline.ts +4 -20
- package/build/serve.test.ts +59 -0
- package/build/serve.ts +29 -63
- package/build/templates.ts +2 -2
- package/build/toc-plugin.test.ts +40 -25
- package/build/toc-plugin.ts +9 -2
- package/build/{types.ts → types.d.ts} +88 -1
- package/build/util.ts +1 -1
- package/build/utils/code.ts +76 -3
- package/build/utils/content-files.test.ts +56 -0
- package/build/utils/content-files.ts +15 -11
- package/build/utils/file-types.test.ts +18 -0
- package/build/utils/file-types.ts +4 -0
- package/build/utils/include.test.ts +117 -0
- package/build/utils/include.ts +53 -0
- package/build/utils/jdi-runner/TraceRunner.java +416 -0
- package/build/utils/literate-java.ts +2 -2
- package/build/utils/markdown.ts +32 -14
- package/build/utils/paths.ts +1 -1
- package/build/utils/render.ts +102 -11
- package/build/utils/shiki-highlighter.ts +1 -1
- package/build/utils/trace-layout.test.ts +570 -0
- package/build/utils/trace-layout.ts +848 -0
- package/build/utils/trace-svg.test.ts +425 -0
- package/build/utils/trace-svg.ts +905 -0
- package/build/utils/trace.test.ts +169 -0
- package/build/utils/trace.ts +382 -0
- package/build/validate-internal-links-plugin.test.ts +30 -2
- package/build/validate-internal-links-plugin.ts +3 -3
- package/build/watch-reload-client.ts +4 -20
- package/build/watch.ts +436 -448
- package/init/content/index.md +10 -5
- package/init/content/labs/00/index.md +1 -1
- package/init/content/labs/01/SearchTreeDemo.java +31 -0
- package/init/content/labs/01/TraceDemo.java +22 -0
- package/init/content/labs/01/index.md +27 -0
- package/init/content/labs/index.md +2 -0
- package/init/content/lectures/01/Rectangle.java +2 -0
- package/init/content/lectures/01/index.md +2 -1
- package/init/content/lectures/01/rectangle.py +45 -0
- package/init/content/lectures/02/_pr1.md +5 -0
- package/init/content/lectures/02/index.md +11 -0
- package/init/content/lectures/02/subdir/_foobar.html +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +6 -0
- package/init/content/lectures/index.md +3 -0
- package/init/content/markdown.md +30 -0
- package/package.json +21 -4
- package/src/_base.scss +13 -2
- package/src/_layout.scss +6 -5
- package/src/anchor/index.test.ts +64 -0
- package/src/code/index.test.ts +71 -0
- package/src/code/style.scss +23 -0
- package/src/code.scss +5 -3
- package/src/header/_base.scss +48 -9
- package/src/header/index.test.ts +89 -0
- package/src/header/index.ts +1 -1
- package/src/header/style.scss +1 -5
- package/src/index.ts +18 -20
- package/src/literate/style.scss +15 -7
- package/src/navigate/eligible.test.ts +101 -0
- package/src/navigate/eligible.ts +62 -0
- package/src/navigate/index.test.ts +82 -0
- package/src/navigate/index.ts +365 -0
- package/src/navigate/lifecycle.test.ts +26 -0
- package/src/navigate/lifecycle.ts +65 -0
- package/src/navigate/style.scss +61 -0
- package/src/print/index.test.ts +71 -0
- package/src/question/index.test.ts +82 -0
- package/src/question/style.scss +12 -0
- package/src/search/index.ts +38 -14
- package/src/search/style.scss +23 -14
- package/src/style.scss +25 -12
- package/src/timezone/index.test.ts +301 -1
- package/src/timezone/index.ts +68 -56
- package/src/toc/index.test.ts +237 -0
- package/src/toc/index.ts +29 -69
- package/src/toc/style.scss +17 -0
- package/src/top/index.test.ts +46 -0
- package/src/top/index.ts +2 -2
- package/src/trace/index.test.ts +554 -0
- package/src/trace/index.ts +258 -0
- package/src/trace/style.scss +210 -0
- package/src/trace/types.d.ts +47 -0
- package/src/util.test.ts +135 -0
- package/templates/_top.html +23 -22
- package/types/jsdom.d.ts +9 -0
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ A static site generator. The successor to Presto.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Modern design (light & dark following system, floating header, styled lists)
|
|
8
|
+
- Fast client-side navigation
|
|
8
9
|
- Clickable/linkable landmarks (headings, deflists, alert boxes)
|
|
9
10
|
- Dynamic table of contents
|
|
10
11
|
* Floats on the side of the screen when window is large enough
|
|
@@ -29,6 +30,7 @@ A static site generator. The successor to Presto.
|
|
|
29
30
|
* `::: section ... :::` renders a special section with a fancy background
|
|
30
31
|
* `!!! note ... !!!` and `!!! warning ... !!!` render alert boxes
|
|
31
32
|
* `??? question ... ???` renders a Q & A section; answers hidden until click
|
|
33
|
+
* `+++ ... +++ ... +++` renders a two-column layout
|
|
32
34
|
* Special heading subtitles with `## Heading # A subtitle here`
|
|
33
35
|
- Automatically generated favicon
|
|
34
36
|
* Text, color, font and font weight taken from config file
|
|
@@ -233,7 +235,7 @@ Example site configuration JSON file:
|
|
|
233
235
|
| `codeLanguages` | Map file extension to Shiki language (e.g., `"java": "java"`) |
|
|
234
236
|
| `faviconColor` | *Optional*, background color for favicon (defaults to `themeColor`) |
|
|
235
237
|
| `faviconFontWeight` | *Optional*, font weight used for favicon text (default `700`) |
|
|
236
|
-
| `vars` | Arbitrary key/value variables exposed to templates/content (e.g., `<%= staffEmail %>`) |
|
|
238
|
+
| `vars` | Arbitrary key/value variables exposed to templates/content as `vars.*` (e.g., `<%= vars.staffEmail %>`) |
|
|
237
239
|
|
|
238
240
|
|
|
239
241
|
#### `nav.json`
|
|
@@ -326,7 +328,7 @@ templates][lodash].
|
|
|
326
328
|
- Site config values are available under `site` (e.g., `site.title`)
|
|
327
329
|
- Page variables (from front matter) are available under `page`
|
|
328
330
|
- Custom variables from the `"vars"` property of the config are available
|
|
329
|
-
|
|
331
|
+
under `vars` (e.g., `<%= vars.staffEmail %>`)
|
|
330
332
|
|
|
331
333
|
|
|
332
334
|
|
package/bin/tada.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import { parseArgs } from 'util';
|
|
6
6
|
import packageJson from '../package.json' with { type: 'json' };
|
|
7
7
|
import {
|
|
@@ -24,7 +24,7 @@ const { version } = packageJson;
|
|
|
24
24
|
|
|
25
25
|
const SYSTEM_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
26
26
|
|
|
27
|
-
const packageDir = path.resolve(
|
|
27
|
+
const packageDir = path.resolve(import.meta.dir, '..');
|
|
28
28
|
|
|
29
29
|
function requireSiteConfig(env: string): void {
|
|
30
30
|
const configPath = path.resolve(process.cwd(), `site.${env}.json`);
|
|
@@ -34,6 +34,17 @@ function requireSiteConfig(env: string): void {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function parsePortArg(name: string): number | undefined {
|
|
38
|
+
const idx = process.argv.indexOf(name);
|
|
39
|
+
if (idx !== -1 && process.argv[idx + 1]) {
|
|
40
|
+
const val = parseInt(process.argv[idx + 1], 10);
|
|
41
|
+
if (val > 0 && val < 65536) {
|
|
42
|
+
return val;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
const COMMANDS = {
|
|
38
49
|
init: null,
|
|
39
50
|
dev: 'Build the site for development',
|
|
@@ -151,21 +162,6 @@ function printUsage() {
|
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
function run(args: string[]): void {
|
|
155
|
-
try {
|
|
156
|
-
execFileSync('bun', args, { cwd: process.cwd(), stdio: 'inherit' });
|
|
157
|
-
} catch (err: unknown) {
|
|
158
|
-
const code = (err as { status?: number }).status;
|
|
159
|
-
if (code != null) {
|
|
160
|
-
console.error(`tada ${command} failed with exit code ${code}`);
|
|
161
|
-
process.exit(code);
|
|
162
|
-
} else {
|
|
163
|
-
console.error(`tada ${command} failed`);
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
165
|
function copyDirRecursive(src: string, dest: string): void {
|
|
170
166
|
fs.mkdirSync(dest, { recursive: true });
|
|
171
167
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -605,49 +601,77 @@ function diffCommand(args: string[]): void {
|
|
|
605
601
|
|
|
606
602
|
const command = process.argv[2];
|
|
607
603
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
case 'dev':
|
|
614
|
-
requireSiteConfig('dev');
|
|
615
|
-
run([path.join(packageDir, 'build/pipeline.ts'), 'dev']);
|
|
616
|
-
break;
|
|
617
|
-
|
|
618
|
-
case 'prod':
|
|
619
|
-
requireSiteConfig('prod');
|
|
620
|
-
run([path.join(packageDir, 'build/pipeline.ts'), 'prod']);
|
|
621
|
-
break;
|
|
622
|
-
|
|
623
|
-
case 'watch': {
|
|
624
|
-
requireSiteConfig('dev');
|
|
625
|
-
run([path.join(packageDir, 'build/watch.ts'), ...process.argv.slice(3)]);
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
case 'serve': {
|
|
630
|
-
run([path.join(packageDir, 'build/serve.ts'), ...process.argv.slice(3)]);
|
|
631
|
-
break;
|
|
632
|
-
}
|
|
604
|
+
try {
|
|
605
|
+
switch (command) {
|
|
606
|
+
case 'init':
|
|
607
|
+
await initCommand(process.argv.slice(3));
|
|
608
|
+
break;
|
|
633
609
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
610
|
+
case 'dev': {
|
|
611
|
+
requireSiteConfig('dev');
|
|
612
|
+
const { runPipeline } = await import(
|
|
613
|
+
path.join(packageDir, 'build/pipeline.ts')
|
|
614
|
+
);
|
|
615
|
+
await runPipeline('development');
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
637
618
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
619
|
+
case 'prod': {
|
|
620
|
+
requireSiteConfig('prod');
|
|
621
|
+
const { runPipeline } = await import(
|
|
622
|
+
path.join(packageDir, 'build/pipeline.ts')
|
|
623
|
+
);
|
|
624
|
+
await runPipeline('production');
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
641
627
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
628
|
+
case 'watch': {
|
|
629
|
+
requireSiteConfig('dev');
|
|
630
|
+
const { runWatch } = await import(
|
|
631
|
+
path.join(packageDir, 'build/watch.ts')
|
|
632
|
+
);
|
|
633
|
+
await runWatch({
|
|
634
|
+
httpPort: parsePortArg('--port'),
|
|
635
|
+
wsPort: parsePortArg('--ws-port'),
|
|
636
|
+
});
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
646
639
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
640
|
+
case 'serve': {
|
|
641
|
+
const { startServer } = await import(
|
|
642
|
+
path.join(packageDir, 'build/serve.ts')
|
|
643
|
+
);
|
|
644
|
+
const { getDistDir } = await import(
|
|
645
|
+
path.join(packageDir, 'build/util.ts')
|
|
646
|
+
);
|
|
647
|
+
startServer({
|
|
648
|
+
port: parsePortArg('--port') ?? 8080,
|
|
649
|
+
distDir: getDistDir(),
|
|
650
|
+
});
|
|
651
|
+
break;
|
|
651
652
|
}
|
|
652
|
-
|
|
653
|
+
|
|
654
|
+
case 'clean':
|
|
655
|
+
cleanCommand(process.argv.slice(3));
|
|
656
|
+
break;
|
|
657
|
+
|
|
658
|
+
case 'diff':
|
|
659
|
+
diffCommand(process.argv.slice(3));
|
|
660
|
+
break;
|
|
661
|
+
|
|
662
|
+
case '--version':
|
|
663
|
+
case '-v':
|
|
664
|
+
console.log(`tada v${version}`);
|
|
665
|
+
break;
|
|
666
|
+
|
|
667
|
+
default:
|
|
668
|
+
printUsage();
|
|
669
|
+
if (command && command !== '--help' && command !== '-h') {
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
} catch (err) {
|
|
675
|
+
console.error(`tada ${command} failed: ${(err as Error).message}`);
|
|
676
|
+
process.exit(1);
|
|
653
677
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import type MarkdownIt from 'markdown-it';
|
|
2
3
|
import type Token from 'markdown-it/lib/token.mjs';
|
|
3
4
|
import { createApplyBasePath } from './utils/paths';
|
|
@@ -5,14 +6,22 @@ import { isFeatureEnabled } from './features';
|
|
|
5
6
|
import { makeLogger } from './log';
|
|
6
7
|
import type { SiteVariables } from './types';
|
|
7
8
|
|
|
8
|
-
const log = makeLogger(
|
|
9
|
+
const log = makeLogger(import.meta.url);
|
|
10
|
+
|
|
11
|
+
interface ApplyBasePathOptions {
|
|
12
|
+
literateJavaOutputPaths?: Set<string>;
|
|
13
|
+
sourceUrlPath?: string;
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
export default function applyBasePathPlugin(
|
|
11
17
|
md: MarkdownIt,
|
|
12
18
|
siteVariables: SiteVariables,
|
|
19
|
+
pluginOptions: ApplyBasePathOptions = {},
|
|
13
20
|
): void {
|
|
14
21
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
15
22
|
const rewriteCodeLinks = isFeatureEnabled(siteVariables, 'code');
|
|
23
|
+
const literateJavaOutputPaths = pluginOptions.literateJavaOutputPaths;
|
|
24
|
+
const sourceUrlPath = pluginOptions.sourceUrlPath;
|
|
16
25
|
|
|
17
26
|
function rewriteInternalHref(href: string): string {
|
|
18
27
|
const match = href.match(/^([^?#]*)(.*)$/);
|
|
@@ -21,13 +30,20 @@ export default function applyBasePathPlugin(
|
|
|
21
30
|
let modifiedPath = pathname;
|
|
22
31
|
|
|
23
32
|
if (rewriteCodeLinks) {
|
|
24
|
-
// Rewrite code file links to .html links
|
|
25
33
|
for (const ext of Object.keys(siteVariables.codeLanguages ?? {})) {
|
|
26
34
|
if (modifiedPath.endsWith(`.${ext}`)) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
if (literateJavaOutputPaths && sourceUrlPath) {
|
|
36
|
+
const resolved = modifiedPath.startsWith('/')
|
|
37
|
+
? modifiedPath
|
|
38
|
+
: path.posix.join(
|
|
39
|
+
path.posix.dirname(sourceUrlPath),
|
|
40
|
+
modifiedPath,
|
|
41
|
+
);
|
|
42
|
+
if (literateJavaOutputPaths.has(resolved)) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
modifiedPath += '.html';
|
|
31
47
|
break;
|
|
32
48
|
}
|
|
33
49
|
}
|
package/build/code.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test, beforeAll } from 'bun:test';
|
|
2
2
|
import { initHighlighter } from './utils/shiki-highlighter';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
renderCodeWithComments,
|
|
5
|
+
extractJavaMethodToc,
|
|
6
|
+
rewriteProseLinks,
|
|
7
|
+
} from './utils/code';
|
|
4
8
|
import type { SiteVariables } from './types';
|
|
5
9
|
|
|
6
10
|
beforeAll(async () => {
|
|
@@ -162,4 +166,107 @@ describe('renderCodeWithComments', () => {
|
|
|
162
166
|
expect(html).not.toContain('id="L4" href="#L4"');
|
|
163
167
|
expect(html).toContain('<code class="shiki language-java">');
|
|
164
168
|
});
|
|
169
|
+
|
|
170
|
+
test('data-prose-source contains rewritten links when pageDirPath is provided', () => {
|
|
171
|
+
const source = '/// See [rect](./rect.py)\npublic class Foo {}\n';
|
|
172
|
+
const html = renderCodeWithComments(
|
|
173
|
+
source,
|
|
174
|
+
'java',
|
|
175
|
+
{
|
|
176
|
+
base: 'https://example.edu',
|
|
177
|
+
basePath: '/course',
|
|
178
|
+
codeLanguages: { java: 'java', py: 'python' },
|
|
179
|
+
internalDomains: [],
|
|
180
|
+
title: 'Test',
|
|
181
|
+
titlePostfix: ' - Test',
|
|
182
|
+
themeColor: 'steelblue',
|
|
183
|
+
defaultTimeZone: 'America/New_York',
|
|
184
|
+
} as SiteVariables,
|
|
185
|
+
'lectures/01',
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(html).toContain(
|
|
189
|
+
'https://example.edu/course/lectures/01/rect.py.html',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('rewriteProseLinks', () => {
|
|
195
|
+
const siteVariables = {
|
|
196
|
+
base: 'https://example.edu',
|
|
197
|
+
basePath: '/course',
|
|
198
|
+
codeLanguages: { java: 'java', py: 'python' },
|
|
199
|
+
internalDomains: [],
|
|
200
|
+
title: 'Test',
|
|
201
|
+
titlePostfix: ' - Test',
|
|
202
|
+
themeColor: 'steelblue',
|
|
203
|
+
defaultTimeZone: 'America/New_York',
|
|
204
|
+
} as SiteVariables;
|
|
205
|
+
|
|
206
|
+
test('rewrites relative link with base + basePath', () => {
|
|
207
|
+
const lines = ['/// See [rectangle.py](./rectangle.py)'];
|
|
208
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
209
|
+
expect(result[0]).toBe(
|
|
210
|
+
'/// See [rectangle.py](https://example.edu/course/lectures/01/rectangle.py.html)',
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('rewrites absolute link with base + basePath', () => {
|
|
215
|
+
const lines = ['/// See [about](/about.html)'];
|
|
216
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
217
|
+
expect(result[0]).toBe(
|
|
218
|
+
'/// See [about](https://example.edu/course/about.html)',
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('leaves external links unchanged', () => {
|
|
223
|
+
const lines = ['/// See [Google](https://google.com)'];
|
|
224
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
225
|
+
expect(result[0]).toBe('/// See [Google](https://google.com)');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('leaves anchor links unchanged', () => {
|
|
229
|
+
const lines = ['/// See [section](#overview)'];
|
|
230
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
231
|
+
expect(result[0]).toBe('/// See [section](#overview)');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('applies code extension rewriting', () => {
|
|
235
|
+
const lines = ['/// See [App](./App.java)'];
|
|
236
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
237
|
+
expect(result[0]).toBe(
|
|
238
|
+
'/// See [App](https://example.edu/course/lectures/01/App.java.html)',
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('handles multiple links on one line', () => {
|
|
243
|
+
const lines = ['/// See [a](./a.py) and [b](./b.java)'];
|
|
244
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
245
|
+
expect(result[0]).toBe(
|
|
246
|
+
'/// See [a](https://example.edu/course/lectures/01/a.py.html) and [b](https://example.edu/course/lectures/01/b.java.html)',
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('does not rewrite non-comment lines', () => {
|
|
251
|
+
const lines = ['String s = "[link](./file.py)";'];
|
|
252
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
253
|
+
expect(result[0]).toBe('String s = "[link](./file.py)";');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('preserves query string and fragment', () => {
|
|
257
|
+
const lines = ['/// See [code](./App.java?view=1#L5)'];
|
|
258
|
+
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
259
|
+
expect(result[0]).toBe(
|
|
260
|
+
'/// See [code](https://example.edu/course/lectures/01/App.java.html?view=1#L5)',
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('works with root basePath', () => {
|
|
265
|
+
const vars = { ...siteVariables, basePath: '/' } as SiteVariables;
|
|
266
|
+
const lines = ['/// See [rect](./rect.py)'];
|
|
267
|
+
const result = rewriteProseLinks(lines, vars, 'lectures/01');
|
|
268
|
+
expect(result[0]).toBe(
|
|
269
|
+
'/// See [rect](https://example.edu/lectures/01/rect.py.html)',
|
|
270
|
+
);
|
|
271
|
+
});
|
|
165
272
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { R, G, B, Y, L, P, I, Ri, Gi, Yi, Li } from './colors';
|
|
3
|
+
|
|
4
|
+
describe('color tag functions', () => {
|
|
5
|
+
test('R wraps text in red', () => {
|
|
6
|
+
const result = R`hello`;
|
|
7
|
+
expect(result).toContain('hello');
|
|
8
|
+
expect(typeof result).toBe('string');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('G wraps text in green', () => {
|
|
12
|
+
const result = G`hello`;
|
|
13
|
+
expect(result).toContain('hello');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('B wraps text in blue', () => {
|
|
17
|
+
const result = B`hello`;
|
|
18
|
+
expect(result).toContain('hello');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('Y wraps text in yellow', () => {
|
|
22
|
+
const result = Y`hello`;
|
|
23
|
+
expect(result).toContain('hello');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('L wraps text in dim', () => {
|
|
27
|
+
const result = L`hello`;
|
|
28
|
+
expect(result).toContain('hello');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('P wraps text in magenta', () => {
|
|
32
|
+
const result = P`hello`;
|
|
33
|
+
expect(result).toContain('hello');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('I wraps text in italic bold', () => {
|
|
37
|
+
const result = I`hello`;
|
|
38
|
+
expect(result).toContain('hello');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('Ri wraps text in inverse red', () => {
|
|
42
|
+
const result = Ri`hello`;
|
|
43
|
+
expect(result).toContain('hello');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Gi wraps text in inverse green', () => {
|
|
47
|
+
const result = Gi`hello`;
|
|
48
|
+
expect(result).toContain('hello');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('Yi wraps text in inverse yellow', () => {
|
|
52
|
+
const result = Yi`hello`;
|
|
53
|
+
expect(result).toContain('hello');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Li wraps text in inverse dim', () => {
|
|
57
|
+
const result = Li`hello`;
|
|
58
|
+
expect(result).toContain('hello');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('handles template interpolation', () => {
|
|
62
|
+
const name = 'world';
|
|
63
|
+
const result = R`hello ${name}`;
|
|
64
|
+
expect(result).toContain('hello world');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs';
|
|
3
|
+
|
|
4
|
+
const MARKER_CHAR = 0x2b; // '+'
|
|
5
|
+
const MIN_MARKERS = 3;
|
|
6
|
+
|
|
7
|
+
function isColumnsFence(state: StateBlock, line: number): boolean {
|
|
8
|
+
const start = state.bMarks[line] + state.tShift[line];
|
|
9
|
+
const max = state.eMarks[line];
|
|
10
|
+
|
|
11
|
+
if (state.sCount[line] - state.blkIndent >= 4) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (state.src.charCodeAt(start) !== MARKER_CHAR) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let pos = start + 1;
|
|
19
|
+
while (pos < max && state.src.charCodeAt(pos) === MARKER_CHAR) {
|
|
20
|
+
pos++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (pos - start < MIN_MARKERS) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Rest of line must be whitespace only
|
|
28
|
+
if (state.skipSpaces(pos) < max) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function columnsRule(
|
|
36
|
+
state: StateBlock,
|
|
37
|
+
startLine: number,
|
|
38
|
+
endLine: number,
|
|
39
|
+
silent: boolean,
|
|
40
|
+
): boolean {
|
|
41
|
+
if (!isColumnsFence(state, startLine)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Find the separator (second +++) and closing (third +++) fences
|
|
46
|
+
let separatorLine = -1;
|
|
47
|
+
let closeLine = -1;
|
|
48
|
+
|
|
49
|
+
for (let line = startLine + 1; line < endLine; line++) {
|
|
50
|
+
if (!isColumnsFence(state, line)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (separatorLine === -1) {
|
|
54
|
+
separatorLine = line;
|
|
55
|
+
} else {
|
|
56
|
+
closeLine = line;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (separatorLine === -1 || closeLine === -1) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (silent) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const oldParent = state.parentType;
|
|
69
|
+
const oldLineMax = state.lineMax;
|
|
70
|
+
|
|
71
|
+
// Outer wrapper: <div class="columns">
|
|
72
|
+
const tokOpen = state.push('columns_open', 'div', 1);
|
|
73
|
+
tokOpen.block = true;
|
|
74
|
+
tokOpen.map = [startLine, closeLine + 1];
|
|
75
|
+
tokOpen.markup = '+++';
|
|
76
|
+
tokOpen.attrJoin('class', 'columns');
|
|
77
|
+
|
|
78
|
+
// Column 1
|
|
79
|
+
const col1Open = state.push('column_open', 'div', 1);
|
|
80
|
+
col1Open.block = true;
|
|
81
|
+
col1Open.map = [startLine + 1, separatorLine];
|
|
82
|
+
|
|
83
|
+
state.lineMax = separatorLine;
|
|
84
|
+
state.md.block.tokenize(state, startLine + 1, separatorLine);
|
|
85
|
+
|
|
86
|
+
const col1Close = state.push('column_close', 'div', -1);
|
|
87
|
+
col1Close.block = true;
|
|
88
|
+
|
|
89
|
+
// Column 2
|
|
90
|
+
const col2Open = state.push('column_open', 'div', 1);
|
|
91
|
+
col2Open.block = true;
|
|
92
|
+
col2Open.map = [separatorLine + 1, closeLine];
|
|
93
|
+
|
|
94
|
+
state.lineMax = closeLine;
|
|
95
|
+
state.md.block.tokenize(state, separatorLine + 1, closeLine);
|
|
96
|
+
|
|
97
|
+
const col2Close = state.push('column_close', 'div', -1);
|
|
98
|
+
col2Close.block = true;
|
|
99
|
+
|
|
100
|
+
// Close wrapper
|
|
101
|
+
const tokClose = state.push('columns_close', 'div', -1);
|
|
102
|
+
tokClose.block = true;
|
|
103
|
+
tokClose.markup = '+++';
|
|
104
|
+
|
|
105
|
+
state.parentType = oldParent;
|
|
106
|
+
state.lineMax = oldLineMax;
|
|
107
|
+
state.line = closeLine + 1;
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default function columnsPlugin(md: MarkdownIt): void {
|
|
113
|
+
md.block.ruler.before('fence', 'columns', columnsRule, {
|
|
114
|
+
alt: ['paragraph', 'reference', 'blockquote', 'list'],
|
|
115
|
+
});
|
|
116
|
+
}
|
package/build/content-watch.ts
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import type { SiteVariables } from './types';
|
|
3
|
-
import { getContentDir, getBuildContentFiles } from './util';
|
|
4
|
-
import {
|
|
5
|
-
compileTemplates,
|
|
6
|
-
getHtmlTemplatesDir,
|
|
7
|
-
getJsonDataDir,
|
|
8
|
-
JSON_DATA_FILES,
|
|
9
|
-
} from './templates';
|
|
3
|
+
import { getContentDir, getBuildContentFiles, isPartial } from './util';
|
|
4
|
+
import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
|
|
10
5
|
import { getProjectDir } from './utils/paths';
|
|
11
6
|
import { B } from './colors';
|
|
12
7
|
import { makeLogger } from './log';
|
|
13
8
|
|
|
14
|
-
const log = makeLogger(
|
|
9
|
+
const log = makeLogger(import.meta.url);
|
|
15
10
|
|
|
16
11
|
interface ChangeDetectionResult {
|
|
17
12
|
templateError: Error | null;
|
|
18
13
|
needsRestart: boolean;
|
|
19
14
|
changedContentFiles?: Set<string>;
|
|
20
|
-
|
|
15
|
+
jsonDataChanged?: boolean;
|
|
16
|
+
partialsChanged?: boolean;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
export class ContentChangeDetector {
|
|
@@ -36,10 +32,9 @@ export class ContentChangeDetector {
|
|
|
36
32
|
[...(modifiedFiles || [])].map(filePath => path.resolve(filePath)),
|
|
37
33
|
);
|
|
38
34
|
|
|
39
|
-
const htmlTemplatesDir = getHtmlTemplatesDir();
|
|
40
35
|
const jsonDataDir = getJsonDataDir();
|
|
41
36
|
|
|
42
|
-
// Try to recompile templates
|
|
37
|
+
// Try to recompile templates (also re-reads JSON data files)
|
|
43
38
|
let templateError: Error | null = null;
|
|
44
39
|
try {
|
|
45
40
|
compileTemplates(this.siteVariables);
|
|
@@ -53,7 +48,6 @@ export class ContentChangeDetector {
|
|
|
53
48
|
|
|
54
49
|
const contentDir = getContentDir();
|
|
55
50
|
const normalizedContentDir = path.resolve(contentDir) + path.sep;
|
|
56
|
-
const normalizedHtmlDir = path.resolve(htmlTemplatesDir) + path.sep;
|
|
57
51
|
const buildContentFiles = getBuildContentFiles(
|
|
58
52
|
contentDir,
|
|
59
53
|
Object.keys(this.siteVariables.codeLanguages || {}),
|
|
@@ -73,22 +67,36 @@ export class ContentChangeDetector {
|
|
|
73
67
|
),
|
|
74
68
|
);
|
|
75
69
|
|
|
76
|
-
// Check if any
|
|
70
|
+
// Check if any JSON data file changed (nav.json, authors.json)
|
|
77
71
|
const jsonDataPaths = JSON_DATA_FILES.map(f =>
|
|
78
72
|
path.resolve(jsonDataDir, f),
|
|
79
73
|
);
|
|
80
|
-
const
|
|
81
|
-
filePath
|
|
82
|
-
filePath === path.resolve(htmlTemplatesDir) ||
|
|
83
|
-
filePath.startsWith(normalizedHtmlDir) ||
|
|
84
|
-
jsonDataPaths.includes(filePath),
|
|
74
|
+
const changedJsonDataPaths = [...resolvedFiles].filter(filePath =>
|
|
75
|
+
jsonDataPaths.includes(filePath),
|
|
85
76
|
);
|
|
86
|
-
const
|
|
77
|
+
const jsonDataChanged = changedJsonDataPaths.length > 0;
|
|
78
|
+
const partialsChanged = [...changedContentFiles].some(f => isPartial(f));
|
|
87
79
|
|
|
88
|
-
for (const filePath of
|
|
80
|
+
for (const filePath of changedJsonDataPaths) {
|
|
89
81
|
log.event`${B`${path.basename(filePath)}`} changed, rebuilding`;
|
|
90
82
|
}
|
|
91
83
|
|
|
84
|
+
if (partialsChanged) {
|
|
85
|
+
const changedPartials = [...changedContentFiles].filter(f =>
|
|
86
|
+
isPartial(f),
|
|
87
|
+
);
|
|
88
|
+
for (const filePath of changedPartials) {
|
|
89
|
+
log.event`Partial ${B`${path.basename(filePath)}`} changed, rebuilding`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const changedJavaFiles = [...changedContentFiles].filter(f =>
|
|
94
|
+
f.endsWith('.java'),
|
|
95
|
+
);
|
|
96
|
+
for (const filePath of changedJavaFiles) {
|
|
97
|
+
log.event`${B`${path.basename(filePath)}`} changed, re-running traces`;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
// Check if site config changed
|
|
93
101
|
const siteConfigChanged = resolvedFiles.has(
|
|
94
102
|
path.resolve(this.siteConfigPath),
|
|
@@ -101,7 +109,8 @@ export class ContentChangeDetector {
|
|
|
101
109
|
templateError: null,
|
|
102
110
|
needsRestart,
|
|
103
111
|
changedContentFiles,
|
|
104
|
-
|
|
112
|
+
jsonDataChanged,
|
|
113
|
+
partialsChanged,
|
|
105
114
|
};
|
|
106
115
|
}
|
|
107
116
|
}
|
package/build/copy.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type Token from 'markdown-it/lib/token.mjs';
|
|
|
3
3
|
import { makeLogger } from './log';
|
|
4
4
|
import type { SiteVariables } from './types';
|
|
5
5
|
|
|
6
|
-
const log = makeLogger(
|
|
6
|
+
const log = makeLogger(import.meta.url);
|
|
7
7
|
|
|
8
8
|
export default function externalLinks(
|
|
9
9
|
md: MarkdownIt,
|