@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.
Files changed (103) hide show
  1. package/README.md +4 -2
  2. package/bin/tada.ts +81 -57
  3. package/build/apply-base-path-plugin.ts +22 -6
  4. package/build/code.test.ts +108 -1
  5. package/build/colors.test.ts +66 -0
  6. package/build/columns-plugin.ts +116 -0
  7. package/build/content-watch.ts +30 -21
  8. package/build/copy.ts +1 -1
  9. package/build/external-links-plugin.ts +1 -1
  10. package/build/generate-content-assets.ts +48 -6
  11. package/build/generate-favicon.ts +1 -1
  12. package/build/generate-fonts.ts +1 -1
  13. package/build/generate-katex-assets.ts +1 -1
  14. package/build/log.test.ts +7 -1
  15. package/build/log.ts +4 -1
  16. package/build/markdown-plugins.test.ts +65 -3
  17. package/build/pagefind.ts +1 -1
  18. package/build/pipeline.ts +4 -20
  19. package/build/serve.test.ts +59 -0
  20. package/build/serve.ts +29 -63
  21. package/build/templates.ts +2 -2
  22. package/build/toc-plugin.test.ts +40 -25
  23. package/build/toc-plugin.ts +9 -2
  24. package/build/{types.ts → types.d.ts} +88 -1
  25. package/build/util.ts +1 -1
  26. package/build/utils/code.ts +76 -3
  27. package/build/utils/content-files.test.ts +56 -0
  28. package/build/utils/content-files.ts +15 -11
  29. package/build/utils/file-types.test.ts +18 -0
  30. package/build/utils/file-types.ts +4 -0
  31. package/build/utils/include.test.ts +117 -0
  32. package/build/utils/include.ts +53 -0
  33. package/build/utils/jdi-runner/TraceRunner.java +416 -0
  34. package/build/utils/literate-java.ts +2 -2
  35. package/build/utils/markdown.ts +32 -14
  36. package/build/utils/paths.ts +1 -1
  37. package/build/utils/render.ts +102 -11
  38. package/build/utils/shiki-highlighter.ts +1 -1
  39. package/build/utils/trace-layout.test.ts +570 -0
  40. package/build/utils/trace-layout.ts +848 -0
  41. package/build/utils/trace-svg.test.ts +425 -0
  42. package/build/utils/trace-svg.ts +905 -0
  43. package/build/utils/trace.test.ts +169 -0
  44. package/build/utils/trace.ts +382 -0
  45. package/build/validate-internal-links-plugin.test.ts +30 -2
  46. package/build/validate-internal-links-plugin.ts +3 -3
  47. package/build/watch-reload-client.ts +4 -20
  48. package/build/watch.ts +436 -448
  49. package/init/content/index.md +10 -5
  50. package/init/content/labs/00/index.md +1 -1
  51. package/init/content/labs/01/SearchTreeDemo.java +31 -0
  52. package/init/content/labs/01/TraceDemo.java +22 -0
  53. package/init/content/labs/01/index.md +27 -0
  54. package/init/content/labs/index.md +2 -0
  55. package/init/content/lectures/01/Rectangle.java +2 -0
  56. package/init/content/lectures/01/index.md +2 -1
  57. package/init/content/lectures/01/rectangle.py +45 -0
  58. package/init/content/lectures/02/_pr1.md +5 -0
  59. package/init/content/lectures/02/index.md +11 -0
  60. package/init/content/lectures/02/subdir/_foobar.html +3 -0
  61. package/init/content/lectures/02/subdir/_pr2.md +6 -0
  62. package/init/content/lectures/index.md +3 -0
  63. package/init/content/markdown.md +30 -0
  64. package/package.json +21 -4
  65. package/src/_base.scss +13 -2
  66. package/src/_layout.scss +6 -5
  67. package/src/anchor/index.test.ts +64 -0
  68. package/src/code/index.test.ts +71 -0
  69. package/src/code/style.scss +23 -0
  70. package/src/code.scss +5 -3
  71. package/src/header/_base.scss +48 -9
  72. package/src/header/index.test.ts +89 -0
  73. package/src/header/index.ts +1 -1
  74. package/src/header/style.scss +1 -5
  75. package/src/index.ts +18 -20
  76. package/src/literate/style.scss +15 -7
  77. package/src/navigate/eligible.test.ts +101 -0
  78. package/src/navigate/eligible.ts +62 -0
  79. package/src/navigate/index.test.ts +82 -0
  80. package/src/navigate/index.ts +365 -0
  81. package/src/navigate/lifecycle.test.ts +26 -0
  82. package/src/navigate/lifecycle.ts +65 -0
  83. package/src/navigate/style.scss +61 -0
  84. package/src/print/index.test.ts +71 -0
  85. package/src/question/index.test.ts +82 -0
  86. package/src/question/style.scss +12 -0
  87. package/src/search/index.ts +38 -14
  88. package/src/search/style.scss +23 -14
  89. package/src/style.scss +25 -12
  90. package/src/timezone/index.test.ts +301 -1
  91. package/src/timezone/index.ts +68 -56
  92. package/src/toc/index.test.ts +237 -0
  93. package/src/toc/index.ts +29 -69
  94. package/src/toc/style.scss +17 -0
  95. package/src/top/index.test.ts +46 -0
  96. package/src/top/index.ts +2 -2
  97. package/src/trace/index.test.ts +554 -0
  98. package/src/trace/index.ts +258 -0
  99. package/src/trace/style.scss +210 -0
  100. package/src/trace/types.d.ts +47 -0
  101. package/src/util.test.ts +135 -0
  102. package/templates/_top.html +23 -22
  103. 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
- without any prefix (e.g., `<%= staffEmail %>`)
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
- import { execFileSync } from 'child_process';
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(__dirname, '..');
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
- switch (command) {
609
- case 'init':
610
- initCommand(process.argv.slice(3));
611
- break;
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
- case 'clean':
635
- cleanCommand(process.argv.slice(3));
636
- break;
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
- case 'diff':
639
- diffCommand(process.argv.slice(3));
640
- break;
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
- case '--version':
643
- case '-v':
644
- console.log(`tada v${version}`);
645
- break;
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
- default:
648
- printUsage();
649
- if (command && command !== '--help' && command !== '-h') {
650
- process.exit(1);
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
- break;
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(__filename);
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
- modifiedPath = modifiedPath.replace(
28
- new RegExp(`\\.${ext}$`),
29
- '.html',
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
  }
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, test, beforeAll } from 'bun:test';
2
2
  import { initHighlighter } from './utils/shiki-highlighter';
3
- import { renderCodeWithComments, extractJavaMethodToc } from './utils/code';
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
+ }
@@ -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(__filename);
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
- templatesChanged?: boolean;
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 HTML template or JSON data file changed
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 changedTemplatePaths = [...resolvedFiles].filter(
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 templatesChanged = changedTemplatePaths.length > 0;
77
+ const jsonDataChanged = changedJsonDataPaths.length > 0;
78
+ const partialsChanged = [...changedContentFiles].some(f => isPartial(f));
87
79
 
88
- for (const filePath of changedTemplatePaths) {
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
- templatesChanged,
112
+ jsonDataChanged,
113
+ partialsChanged,
105
114
  };
106
115
  }
107
116
  }
package/build/copy.ts CHANGED
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import { makeLogger } from './log';
4
4
  import { B } from './colors';
5
5
 
6
- const log = makeLogger(__filename);
6
+ const log = makeLogger(import.meta.url);
7
7
 
8
8
  interface CollectedFile {
9
9
  abs: string;
@@ -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(__filename);
6
+ const log = makeLogger(import.meta.url);
7
7
 
8
8
  export default function externalLinks(
9
9
  md: MarkdownIt,