@abreen/tada 1.5.7 → 1.6.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
@@ -1,5 +1,8 @@
1
1
  # Tada :tada:
2
2
 
3
+ [![CI/CD](https://github.com/abreen/Tada/actions/workflows/ci.yml/badge.svg)](https://github.com/abreen/Tada/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/%40abreen%2Ftada)](https://www.npmjs.com/package/@abreen/tada)
5
+
3
6
  A static site generator. The successor to Presto.
4
7
 
5
8
  ## Features
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { toPosix } from './utils/paths';
3
4
 
4
5
  const EXCLUDED_DIRS = new Set(['pagefind']);
5
6
  const EXCLUDED_FILES = new Set(['tada.manifest.json']);
@@ -68,7 +69,7 @@ export async function walkAndHash(
68
69
  }
69
70
 
70
71
  const abs = path.join(entry.parentPath, entry.name);
71
- const rel = path.relative(dir, abs).split(path.sep).join(path.posix.sep);
72
+ const rel = toPosix(path.relative(dir, abs));
72
73
 
73
74
  const topLevel = rel.split('/')[0];
74
75
  if (EXCLUDED_DIRS.has(topLevel)) {
package/build/bundle.ts CHANGED
@@ -3,7 +3,12 @@ import os from 'os';
3
3
  import path from 'path';
4
4
  import _ from 'lodash';
5
5
  import * as sass from 'sass';
6
- import { getPackageDir, getProjectDir, getDistDir } from './utils/paths';
6
+ import {
7
+ getPackageDir,
8
+ getProjectDir,
9
+ getDistDir,
10
+ toPosix,
11
+ } from './utils/paths';
7
12
  import { deriveTheme } from './utils/derive-theme';
8
13
  import type { PluginBuilder } from 'bun';
9
14
  import type { SiteVariables } from './types';
@@ -104,10 +109,7 @@ export async function bundle(
104
109
 
105
110
  // Return the output filenames for asset tag injection
106
111
  const assetFiles = result.outputs.map(output =>
107
- path
108
- .relative(resolvedDistDir, output.path)
109
- .split(path.sep)
110
- .join(path.posix.sep),
112
+ toPosix(path.relative(resolvedDistDir, output.path)),
111
113
  );
112
114
 
113
115
  return assetFiles;
package/build/copy.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { toPosix } from './utils/paths';
3
4
  import { makeLogger } from './log';
4
5
  import { B } from './colors';
5
6
 
@@ -22,7 +23,7 @@ function collectFiles(dir: string): CollectedFile[] {
22
23
  .map(entry => {
23
24
  const abs = path.join(entry.parentPath, entry.name);
24
25
  const rel = path.relative(dir, abs);
25
- return { abs, rel: rel.split(path.sep).join(path.posix.sep) };
26
+ return { abs, rel: toPosix(rel) };
26
27
  });
27
28
  }
28
29
 
@@ -83,10 +84,7 @@ export function copyPublicFile(
83
84
  filePath: string,
84
85
  contentAssetRelPaths?: Set<string>,
85
86
  ): void {
86
- const rel = path
87
- .relative(publicDir, filePath)
88
- .split(path.sep)
89
- .join(path.posix.sep);
87
+ const rel = toPosix(path.relative(publicDir, filePath));
90
88
  if (contentAssetRelPaths && contentAssetRelPaths.has(rel)) {
91
89
  log.error`public/${B`${rel}`} conflicts with content/${B`${rel}`}`;
92
90
  throw new Error(`public/${rel} and content/${rel} have the same path`);
@@ -103,10 +101,7 @@ export function copyContentFile(
103
101
  filePath: string,
104
102
  publicRelPaths?: Set<string>,
105
103
  ): void {
106
- const rel = path
107
- .relative(contentDir, filePath)
108
- .split(path.sep)
109
- .join(path.posix.sep);
104
+ const rel = toPosix(path.relative(contentDir, filePath));
110
105
  if (publicRelPaths && publicRelPaths.has(rel)) {
111
106
  log.error`content/${B`${rel}`} conflicts with public/${B`${rel}`}`;
112
107
  throw new Error(`content/${rel} and public/${rel} have the same path`);
@@ -16,6 +16,7 @@ import {
16
16
  } from './util';
17
17
  import { isLiterateJava } from './utils/file-types';
18
18
  import { checkJavac } from './utils/literate-java';
19
+ import { toPosix } from './utils/paths';
19
20
  import type {
20
21
  SiteVariables,
21
22
  Asset,
@@ -291,10 +292,7 @@ export class ContentRenderer {
291
292
  for (const filePath of buildContentFiles) {
292
293
  if (isLiterateJava(filePath)) {
293
294
  const parsed = path.parse(path.relative(contentDir, filePath));
294
- const javaPath = path
295
- .join(parsed.dir, parsed.name)
296
- .split(path.sep)
297
- .join(path.posix.sep);
295
+ const javaPath = toPosix(path.join(parsed.dir, parsed.name));
298
296
  literateJavaOutputPaths.add(`/${javaPath}`);
299
297
  }
300
298
  }
@@ -7,7 +7,7 @@ import {
7
7
  isLiterateJava,
8
8
  isPartial,
9
9
  } from './file-types';
10
- import { getPublicDir, normalizeOutputPath } from './paths';
10
+ import { getPublicDir, normalizeOutputPath, toPosix } from './paths';
11
11
 
12
12
  function walkFiles(dir: string): string[] {
13
13
  return fs.readdirSync(dir).flatMap(file => {
@@ -111,18 +111,12 @@ export function getValidInternalTargets(
111
111
  const relPath = path.relative(contentDir, filePath);
112
112
  const parsed = path.parse(relPath);
113
113
  const ext = parsed.ext.toLowerCase();
114
- const subPath = path
115
- .join(parsed.dir, parsed.name)
116
- .split(path.sep)
117
- .join(path.posix.sep);
114
+ const subPath = toPosix(path.join(parsed.dir, parsed.name));
118
115
 
119
116
  if (isLiterateJava(filePath)) {
120
117
  // parsed.name is e.g. "VowelCounter.java"; the output page is
121
118
  // VowelCounter.java.html and the source file is VowelCounter.java
122
- const javaSubPath = path
123
- .join(parsed.dir, parsed.name)
124
- .split(path.sep)
125
- .join(path.posix.sep);
119
+ const javaSubPath = toPosix(path.join(parsed.dir, parsed.name));
126
120
  addGeneratedRouteAliases(targets, `/${javaSubPath}.html`);
127
121
  targets.add(normalizeOutputPath(`/${javaSubPath}`));
128
122
  } else if (extensionIsMarkdown(ext) || ext === '.html') {
@@ -45,8 +45,23 @@ export function createApplyBasePath(
45
45
  };
46
46
  }
47
47
 
48
+ export function toPosix(p: string): string {
49
+ return p.split(path.sep).join(path.posix.sep);
50
+ }
51
+
52
+ /**
53
+ * Convert an OS-native relative filesystem path to a URL path: convert
54
+ * separators to '/', then percent-encode each segment so characters that
55
+ * are unsafe in URLs (space, ?, #, non-ASCII, etc.) are encoded. Use this
56
+ * anywhere a filesystem-derived path is emitted as an href or looked up
57
+ * against a URL-encoded href.
58
+ */
59
+ export function toUrlPath(p: string): string {
60
+ return toPosix(p).split('/').map(encodeURIComponent).join('/');
61
+ }
62
+
48
63
  export function normalizeOutputPath(outputPath: string): string {
49
- const normalized = path.posix.normalize(outputPath);
64
+ const normalized = path.posix.normalize(toPosix(outputPath));
50
65
  if (normalized === '.' || normalized === '') {
51
66
  return '/';
52
67
  }
@@ -17,7 +17,12 @@ import {
17
17
  import { extensionIsMarkdown } from './file-types';
18
18
  import { createTraceHelpers } from './trace';
19
19
  import { createIncludeFunction } from './include';
20
- import { createApplyBasePath, normalizeOutputPath } from './paths';
20
+ import {
21
+ createApplyBasePath,
22
+ normalizeOutputPath,
23
+ toPosix,
24
+ toUrlPath,
25
+ } from './paths';
21
26
  import { parseFrontMatterAndContent } from './front-matter';
22
27
  import { createMarkdown } from './markdown';
23
28
  import { generateTocHtml, generateCodeTocHtml } from '../toc-plugin';
@@ -164,10 +169,7 @@ export function injectKatexStylesheet(
164
169
  }
165
170
 
166
171
  function toContentAssetPath(contentDir: string, filePath: string): string {
167
- return path
168
- .relative(contentDir, filePath)
169
- .split(path.sep)
170
- .join(path.posix.sep);
172
+ return toPosix(path.relative(contentDir, filePath));
171
173
  }
172
174
 
173
175
  export function renderPlainTextPageAsset({
@@ -182,7 +184,7 @@ export function renderPlainTextPageAsset({
182
184
  javacAvailable,
183
185
  }: RenderPlainTextOptions): Asset[] {
184
186
  const { dir, name, ext } = path.parse(filePath);
185
- const subPath = path.relative(contentDir, path.join(dir, name));
187
+ const subPath = toPosix(path.relative(contentDir, path.join(dir, name)));
186
188
  const applyBasePath = createApplyBasePath(siteVariables);
187
189
 
188
190
  log.info`Rendering page ${B`${subPath + ext}`}`;
@@ -248,11 +250,11 @@ export function renderCodePageAsset({
248
250
  assetFiles,
249
251
  }: RenderCodePageOptions): Asset[] {
250
252
  const { dir, name, ext } = path.parse(filePath);
251
- const subPath = path.relative(contentDir, path.join(dir, name));
253
+ const subPath = toPosix(path.relative(contentDir, path.join(dir, name)));
252
254
  const applyBasePath = createApplyBasePath(siteVariables);
253
255
  const lang = siteVariables.codeLanguages![ext.slice(1).toLowerCase()];
254
256
  const sourceCode = fs.readFileSync(filePath, 'utf-8');
255
- const pageDirPath = path.relative(contentDir, dir);
257
+ const pageDirPath = toPosix(path.relative(contentDir, dir));
256
258
 
257
259
  log.info`Rendering code page ${B`${subPath + ext}`}`;
258
260
  const content = renderCodeWithComments(
@@ -262,7 +264,7 @@ export function renderCodePageAsset({
262
264
  pageDirPath,
263
265
  );
264
266
  const codeFilePath = applyBasePath(
265
- normalizeOutputPath(`/${toContentAssetPath(contentDir, filePath)}`),
267
+ normalizeOutputPath(`/${toUrlPath(path.relative(contentDir, filePath))}`),
266
268
  );
267
269
  const titleHtml = `<tt>${name + ext}</tt>`;
268
270
  const tocItems = lang === 'java' ? extractJavaMethodToc(sourceCode) : [];
@@ -316,7 +318,9 @@ export function renderCopiedContentAsset({
316
318
 
317
319
  if (filePath.endsWith('.java') && isFeatureEnabled(siteVariables, 'code')) {
318
320
  const sourceCode = fs.readFileSync(filePath, 'utf-8');
319
- const pageDirPath = path.relative(contentDir, path.dirname(filePath));
321
+ const pageDirPath = toPosix(
322
+ path.relative(contentDir, path.dirname(filePath)),
323
+ );
320
324
  const rewrittenLines = rewriteProseLinks(
321
325
  sourceCode.split('\n'),
322
326
  siteVariables,
@@ -501,7 +505,7 @@ export function renderLiterateJavaPageAsset({
501
505
  }: RenderLiterateJavaOptions): Asset[] {
502
506
  const { dir, name } = path.parse(filePath);
503
507
  const className = deriveClassName(filePath);
504
- const subPath = path.relative(contentDir, path.join(dir, className));
508
+ const subPath = toPosix(path.relative(contentDir, path.join(dir, className)));
505
509
  const applyBasePath = createApplyBasePath(siteVariables);
506
510
 
507
511
  log.info`Rendering literate Java page ${B`${name}`}`;
@@ -619,7 +623,7 @@ export function renderLiterateJavaPageAsset({
619
623
  const javaFileName = `${className}.java`;
620
624
  const codeFilePath = applyBasePath(
621
625
  normalizeOutputPath(
622
- `/${toContentAssetPath(contentDir, path.join(dir, javaFileName))}`,
626
+ `/${toUrlPath(path.relative(contentDir, path.join(dir, javaFileName)))}`,
623
627
  ),
624
628
  );
625
629
 
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { makeLogger } from '../log';
6
6
  import { B } from '../colors';
7
- import { normalizeOutputPath } from './paths';
7
+ import { normalizeOutputPath, toPosix } from './paths';
8
8
  import { hasMainMethod } from './literate-java';
9
9
  import { renderCodeSegment } from './code';
10
10
  import { computeLayout } from './trace-layout';
@@ -324,10 +324,7 @@ export function createTraceHelpers(context: TraceContext): {
324
324
 
325
325
  const highlightedSource = highlightSource(javaFile, source);
326
326
 
327
- const relDir = path
328
- .relative(contentDir, pageDir)
329
- .split(path.sep)
330
- .join(path.posix.sep);
327
+ const relDir = toPosix(path.relative(contentDir, pageDir));
331
328
  const traceOutputDir = path.join(distDir, relDir, '_traces', className);
332
329
 
333
330
  const manifest = chunkTraceOutput(
@@ -69,7 +69,18 @@ function resolveLinkPath(
69
69
  ? normalizePathname(hrefPath)
70
70
  : normalizePathname(path.posix.join(sourceDir, hrefPath));
71
71
 
72
- return rewriteCodeLink(resolved, codeExtPattern);
72
+ // Decode percent-encoded characters so the resolved path can be matched
73
+ // against `validTargets`, which contains raw filesystem-derived paths.
74
+ // markdown-it's normalizeLink percent-encodes hrefs, so a link like
75
+ // `[x](/my notes.md)` arrives here as `/my%20notes.md`.
76
+ let decoded: string;
77
+ try {
78
+ decoded = normalizePathname(decodeURIComponent(resolved));
79
+ } catch {
80
+ decoded = resolved;
81
+ }
82
+
83
+ return rewriteCodeLink(decoded, codeExtPattern);
73
84
  }
74
85
 
75
86
  function getDirectoryIndexPath(pathname: string): string {
@@ -26,6 +26,7 @@ declare const __WEBSOCKET_PORT__: number;
26
26
  document.body.classList.add('loading-cursor');
27
27
  } else if (event.data === 'reload') {
28
28
  console.log('[watch-reload] Reloading page...');
29
+ window.history.scrollRestoration = 'auto';
29
30
  window.location.reload();
30
31
  }
31
32
  };
package/build/watch.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  getPublicDir,
13
13
  getDistDir,
14
14
  getPackageDir,
15
+ toPosix,
15
16
  } from './utils/paths';
16
17
  import { isFeatureEnabled } from './features';
17
18
  import { bundle } from './bundle';
@@ -55,10 +56,7 @@ export async function runWatch(options: {
55
56
  define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
56
57
  });
57
58
  return result.outputs.map(output =>
58
- path
59
- .relative(getDistDir(), output.path)
60
- .split(path.sep)
61
- .join(path.posix.sep),
59
+ toPosix(path.relative(getDistDir(), output.path)),
62
60
  );
63
61
  }
64
62
 
@@ -154,10 +152,7 @@ export async function runWatch(options: {
154
152
  return null;
155
153
  }
156
154
 
157
- return path
158
- .relative(contentDir, normalizedFilePath)
159
- .split(path.sep)
160
- .join(path.posix.sep);
155
+ return toPosix(path.relative(contentDir, normalizedFilePath));
161
156
  }
162
157
 
163
158
  function toPublicRelativePath(filePath: string): string | null {
@@ -169,10 +164,7 @@ export async function runWatch(options: {
169
164
  if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
170
165
  return null;
171
166
  }
172
- return path
173
- .relative(publicDir, normalizedFilePath)
174
- .split(path.sep)
175
- .join(path.posix.sep);
167
+ return toPosix(path.relative(publicDir, normalizedFilePath));
176
168
  }
177
169
 
178
170
  // Watch mode
@@ -354,10 +346,7 @@ export async function runWatch(options: {
354
346
  const absPath = path.resolve(filePath);
355
347
  if (fs.existsSync(absPath)) {
356
348
  copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
357
- const rel = path
358
- .relative(publicDir, absPath)
359
- .split(path.sep)
360
- .join(path.posix.sep);
349
+ const rel = toPosix(path.relative(publicDir, absPath));
361
350
  publicRelPaths.add(rel);
362
351
  }
363
352
  }
@@ -397,10 +386,7 @@ export async function runWatch(options: {
397
386
  const absPath = path.resolve(filePath);
398
387
  if (fs.existsSync(absPath)) {
399
388
  copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
400
- const rel = path
401
- .relative(publicDir, absPath)
402
- .split(path.sep)
403
- .join(path.posix.sep);
389
+ const rel = toPosix(path.relative(publicDir, absPath));
404
390
  publicRelPaths.add(rel);
405
391
  }
406
392
  }
@@ -417,10 +403,7 @@ export async function runWatch(options: {
417
403
  const ext = path.extname(absPath).slice(1).toLowerCase();
418
404
  if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
419
405
  copyContentFile(contentDir, distDir, absPath, publicRelPaths);
420
- const rel = path
421
- .relative(contentDir, absPath)
422
- .split(path.sep)
423
- .join(path.posix.sep);
406
+ const rel = toPosix(path.relative(contentDir, absPath));
424
407
  contentAssetRelPaths.add(rel);
425
408
  }
426
409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.5.7",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -29,10 +29,10 @@
29
29
  "prepack": "bun run scripts/prepack.ts",
30
30
  "lint": "eslint src/ build/ bin/",
31
31
  "typecheck": "tsc --noEmit",
32
- "test:functional": "python3 -m pip install -q -r functional_tests/requirements.txt && python3 -m pytest functional_tests/ -v -n auto --durations=10",
33
- "test:all": "bun test && bun run test:playwright && bun run test:functional",
34
- "test:coverage": "rm -rf coverage/unit coverage/functional && bun test --coverage && TADA_COVERAGE=1 python3 -m pip install -q -r functional_tests/requirements.txt && TADA_COVERAGE=1 python3 -m pytest functional_tests/ -v -n auto --durations=10 && bun run scripts/coverage-report.ts",
35
- "test:playwright": "bunx playwright test",
32
+ "test:functional": "bun run scripts/test.ts functional",
33
+ "test:all": "bun run scripts/test.ts all",
34
+ "test:coverage": "bun run scripts/test.ts coverage",
35
+ "test:playwright": "bun run scripts/test.ts playwright",
36
36
  "init-example": "bun run bin/tada.ts init example --no-interactive",
37
37
  "iew": "if [ -d example ]; then cd example; else bun run init-example && cd example; fi && ../bin/tada.ts watch"
38
38
  },
@@ -41,7 +41,8 @@
41
41
  },
42
42
  "os": [
43
43
  "darwin",
44
- "linux"
44
+ "linux",
45
+ "win32"
45
46
  ],
46
47
  "dependencies": {
47
48
  "@vscode/markdown-it-katex": "^1.1.2",
@@ -77,7 +78,7 @@
77
78
  },
78
79
  "devDependencies": {
79
80
  "@eslint/js": "^10.0.1",
80
- "@playwright/test": "^1.59.1",
81
+ "@playwright/test": "1.56.1",
81
82
  "@saintedlama/lcov-parse": "^1.0.1",
82
83
  "@types/bun": "^1.3.10",
83
84
  "@types/culori": "^4.0.1",
package/src/style.scss CHANGED
@@ -396,8 +396,8 @@ dl {
396
396
  }
397
397
  }
398
398
 
399
- dd {
400
- margin-bottom: var(--gap);
399
+ dd > *:first-child {
400
+ margin-top: 0;
401
401
  }
402
402
  }
403
403