@abreen/tada 1.17.0 → 1.18.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.
Files changed (107) hide show
  1. package/README.md +41 -7
  2. package/bin/tada.ts +23 -9
  3. package/bin/validators.ts +34 -1
  4. package/build/branding.ts +41 -0
  5. package/build/build-manifest.ts +10 -5
  6. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  7. package/build/build-validation.ts +176 -0
  8. package/build/bundle.ts +75 -65
  9. package/build/columns-plugin.ts +27 -1
  10. package/build/config-loader.ts +3 -3
  11. package/build/custom-fonts.ts +8 -5
  12. package/build/deflist-id-plugin.ts +2 -2
  13. package/build/generate-favicon.ts +7 -6
  14. package/build/generate-fonts.ts +8 -15
  15. package/build/generate-katex-assets.ts +19 -17
  16. package/build/generate-web-app-manifest.ts +3 -11
  17. package/build/lodash-template.ts +14 -0
  18. package/build/output-publication.ts +238 -0
  19. package/build/pagefind.ts +49 -21
  20. package/build/pdf-text.ts +41 -1
  21. package/build/pipeline.ts +95 -101
  22. package/build/site-assets.ts +46 -0
  23. package/build/site-build.ts +267 -0
  24. package/build/source-model.ts +223 -229
  25. package/build/source-records.ts +69 -81
  26. package/build/template-globals.ts +3 -0
  27. package/build/templates.ts +12 -8
  28. package/build/toc-plugin.ts +21 -21
  29. package/build/types.d.ts +38 -38
  30. package/build/util.ts +0 -4
  31. package/build/utils/code.ts +51 -117
  32. package/build/utils/final-html.ts +171 -47
  33. package/build/utils/jdi-runner/LiterateRunner.java +9 -1
  34. package/build/utils/literate-java.ts +11 -7
  35. package/build/utils/markdown-partials.ts +4 -3
  36. package/build/utils/markdown.ts +6 -17
  37. package/build/utils/paths.ts +3 -0
  38. package/build/utils/plain-text.ts +18 -0
  39. package/build/utils/render.ts +197 -154
  40. package/build/utils/shiki-highlighter.ts +11 -0
  41. package/build/utils/source-template.ts +2 -2
  42. package/build/utils/trace-core.ts +8 -83
  43. package/build/utils/trace.ts +38 -95
  44. package/build/validate-config-links.ts +39 -6
  45. package/build/watch/build-incremental.ts +46 -103
  46. package/build/watch/compiler.ts +158 -37
  47. package/build/watch/engine.ts +207 -162
  48. package/build/watch/index.ts +9 -2
  49. package/build/watch/planner.ts +88 -144
  50. package/build/watch/runtime.ts +21 -34
  51. package/build/watch/types.d.ts +32 -64
  52. package/package.json +4 -5
  53. package/schema/site.schema.json +18 -2
  54. package/src/_content.scss +447 -0
  55. package/src/_fonts.scss +90 -0
  56. package/src/_layout.scss +50 -35
  57. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  58. package/src/code/{style.scss → _index.scss} +61 -0
  59. package/src/header/{_base.scss → _index.scss} +98 -6
  60. package/src/index.ts +1 -17
  61. package/src/navigate/{style.scss → _index.scss} +20 -0
  62. package/src/navigate/eligible.ts +0 -40
  63. package/src/navigate/index.ts +50 -5
  64. package/src/navigate/lifecycle.ts +19 -2
  65. package/src/navigate/runtime.ts +20 -4
  66. package/src/page-update/index.ts +33 -20
  67. package/src/print/{style.scss → _index.scss} +3 -1
  68. package/src/search/index.ts +30 -2
  69. package/src/slides/{style.scss → _index.scss} +5 -0
  70. package/src/style.scss +24 -536
  71. package/src/timezone/index.ts +29 -3
  72. package/src/timezone/time-format.ts +4 -0
  73. package/src/toc/index.ts +6 -5
  74. package/src/trace/index.ts +11 -1
  75. package/templates/_author.html +2 -2
  76. package/templates/_download.html +1 -1
  77. package/templates/_footer.html +1 -1
  78. package/templates/_heading.html +3 -3
  79. package/templates/_nav.html +2 -2
  80. package/templates/_page-bottom.html +6 -0
  81. package/templates/_top.html +18 -14
  82. package/templates/code.html +22 -21
  83. package/templates/default.html +12 -11
  84. package/templates/literate.html +6 -5
  85. package/build/copy.ts +0 -82
  86. package/build/generate-content-assets.ts +0 -121
  87. package/build/utils/content-files.ts +0 -170
  88. package/build/watch/assets.ts +0 -103
  89. package/build/watch/build-full.ts +0 -97
  90. package/build/watch/build-helpers.ts +0 -66
  91. package/build/watch/build-result.ts +0 -19
  92. package/build/watch/fs-commit.ts +0 -153
  93. package/build/watch/mutations.ts +0 -73
  94. package/build/watch/snapshot.ts +0 -201
  95. package/build/watch/validation.ts +0 -50
  96. package/src/code.scss +0 -60
  97. package/src/critical.scss +0 -5
  98. package/src/header/style.scss +0 -91
  99. /package/src/{literate.scss → _literate.scss} +0 -0
  100. /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
  101. /package/src/anchor/{style.scss → _index.scss} +0 -0
  102. /package/src/page-update/{style.scss → _index.scss} +0 -0
  103. /package/src/question/{style.scss → _index.scss} +0 -0
  104. /package/src/search/{style.scss → _index.scss} +0 -0
  105. /package/src/timezone/{style.scss → _index.scss} +0 -0
  106. /package/src/toc/{style.scss → _index.scss} +0 -0
  107. /package/src/trace/{style.scss → _index.scss} +0 -0
@@ -1,182 +1,126 @@
1
1
  import path from 'path';
2
- import { loadProjectConfig } from '../config-loader';
3
- import { getProjectConfigDir } from '../templates';
4
- import type { ChangeBatch } from './types';
5
- import type { TadaProjectScan, TadaSnapshot } from './snapshot';
6
- import { classifyWatchConfigPath } from './config-paths';
2
+ import type { SourceEntry, TadaProjectScan } from '../source-model';
3
+ import type { TadaSnapshot } from '../site-build';
7
4
 
8
5
  export interface TadaIncrementalWatchPlan {
9
6
  kind: 'incremental';
10
7
  scan: TadaProjectScan;
11
- contentToRender: Set<string>;
12
- publicToRender: Set<string>;
13
- contentToRemove: Set<string>;
14
- publicToRemove: Set<string>;
8
+ renderSources: Set<string>;
9
+ removeSources: Set<string>;
15
10
  }
16
11
 
17
12
  type TadaWatchPlan = { kind: 'full' } | TadaIncrementalWatchPlan;
18
13
 
19
- export function diffAuthorKeys(previous: unknown, next: unknown): Set<string> {
20
- const previousMap =
21
- previous && typeof previous === 'object'
22
- ? (previous as Record<string, unknown>)
23
- : {};
24
- const nextMap =
25
- next && typeof next === 'object' ? (next as Record<string, unknown>) : {};
26
- const keys = new Set([...Object.keys(previousMap), ...Object.keys(nextMap)]);
27
- const changed = new Set<string>();
28
-
29
- for (const key of keys) {
30
- if (JSON.stringify(previousMap[key]) !== JSON.stringify(nextMap[key])) {
31
- changed.add(key);
32
- }
33
- }
34
-
35
- return changed;
14
+ function sameItems(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
15
+ return a.size === b.size && [...a].every(item => b.has(item));
36
16
  }
37
17
 
38
- function addDependents(
39
- destination: Set<string>,
40
- reverseMap: Map<string, Set<string>>,
41
- key: string,
42
- ): void {
43
- const dependents = reverseMap.get(key);
44
- if (!dependents) {
45
- return;
46
- }
47
- for (const dependent of dependents) {
48
- destination.add(dependent);
49
- }
18
+ function sameSourceEntry(a: SourceEntry, b: SourceEntry | undefined): boolean {
19
+ return (
20
+ b !== undefined &&
21
+ a.kind === b.kind &&
22
+ a.renderKind === b.renderKind &&
23
+ sameItems(a.outputs, b.outputs) &&
24
+ sameItems(a.targets, b.targets)
25
+ );
50
26
  }
51
27
 
52
- function sourceExists(filePath: string, scan: TadaProjectScan): boolean {
53
- return scan.contentFiles.has(filePath) || scan.publicFiles.has(filePath);
28
+ export function diffAuthorKeys(previous: unknown, next: unknown): Set<string> {
29
+ const asMap = (value: unknown): Record<string, unknown> =>
30
+ value && typeof value === 'object'
31
+ ? (value as Record<string, unknown>)
32
+ : {};
33
+ const before = asMap(previous);
34
+ const after = asMap(next);
35
+ return new Set(
36
+ [...Object.keys(before), ...Object.keys(after)].filter(
37
+ key => JSON.stringify(before[key]) !== JSON.stringify(after[key]),
38
+ ),
39
+ );
54
40
  }
55
41
 
56
42
  export function createTadaWatchPlan({
57
43
  snapshot,
58
- batch,
44
+ paths,
59
45
  scan,
46
+ full = false,
47
+ changedAuthors = new Set<string>(),
60
48
  }: {
61
49
  snapshot: TadaSnapshot;
62
- batch: ChangeBatch;
50
+ paths: ReadonlySet<string>;
63
51
  scan: TadaProjectScan;
52
+ full?: boolean;
53
+ changedAuthors?: ReadonlySet<string>;
64
54
  }): TadaWatchPlan {
65
- const contentToRender = new Set<string>();
66
- const publicToRender = new Set<string>();
67
- const contentToRemove = new Set<string>();
68
- const publicToRemove = new Set<string>();
69
- const projectConfigDir = getProjectConfigDir();
70
-
71
- for (const change of batch.changes) {
72
- const configKind = classifyWatchConfigPath(change.path);
73
- if (configKind === 'site' || configKind === 'nav') {
74
- return { kind: 'full' };
75
- }
55
+ if (full) {
56
+ return { kind: 'full' };
76
57
  }
77
-
78
- for (const change of batch.changes) {
79
- const resolvedPath = path.resolve(change.path);
80
- if (classifyWatchConfigPath(resolvedPath) === 'authors') {
81
- const nextAuthorsData = loadProjectConfig(
82
- projectConfigDir,
83
- 'authors',
84
- snapshot.siteVariables,
85
- )?.value;
86
- if (snapshot.authorsData === undefined || nextAuthorsData === undefined) {
87
- return { kind: 'full' };
88
- }
89
- const changedAuthorKeys = diffAuthorKeys(
90
- snapshot.authorsData,
91
- nextAuthorsData,
92
- );
93
- for (const key of changedAuthorKeys) {
94
- addDependents(contentToRender, snapshot.reverseAuthorDeps, key);
95
- }
96
- continue;
97
- }
98
-
99
- if (scan.contentFiles.has(resolvedPath)) {
100
- contentToRender.add(resolvedPath);
101
- } else if (snapshot.contentRecords.has(resolvedPath)) {
102
- contentToRemove.add(resolvedPath);
58
+ const renderSources = new Set<string>();
59
+ const removeSources = new Set(
60
+ [...snapshot.records.keys()].filter(source => !scan.sources.has(source)),
61
+ );
62
+ const include = (
63
+ index: ReadonlyMap<string, ReadonlySet<string>>,
64
+ key: string,
65
+ ) => {
66
+ for (const source of index.get(key) ?? []) {
67
+ renderSources.add(source);
103
68
  }
104
-
105
- if (scan.publicFiles.has(resolvedPath)) {
106
- publicToRender.add(resolvedPath);
107
- } else if (snapshot.publicRecords.has(resolvedPath)) {
108
- publicToRemove.add(resolvedPath);
69
+ };
70
+ const changedSources = new Set([...paths, ...removeSources]);
71
+ for (const source of snapshot.scan.sources.keys()) {
72
+ if (!scan.sources.has(source)) {
73
+ changedSources.add(source);
109
74
  }
110
-
111
- addDependents(contentToRender, snapshot.reversePartialDeps, resolvedPath);
112
- addDependents(contentToRender, snapshot.reverseTraceDeps, resolvedPath);
113
75
  }
114
-
115
- const changedTargets = new Set<string>();
116
- for (const target of snapshot.scan.validTargets) {
117
- if (!scan.validTargets.has(target)) {
118
- changedTargets.add(target);
76
+ // A directory event may arrive without events for the files inside it.
77
+ const changedDirPrefixes = [...paths].map(changed => changed + path.sep);
78
+ for (const [source, entry] of scan.sources) {
79
+ const inChangedDirectory = changedDirPrefixes.some(prefix =>
80
+ source.startsWith(prefix),
81
+ );
82
+ if (
83
+ inChangedDirectory ||
84
+ !sameSourceEntry(entry, snapshot.scan.sources.get(source))
85
+ ) {
86
+ changedSources.add(source);
119
87
  }
120
88
  }
121
- for (const target of scan.validTargets) {
122
- if (!snapshot.scan.validTargets.has(target)) {
123
- changedTargets.add(target);
89
+ for (const source of changedSources) {
90
+ if (scan.sources.has(source)) {
91
+ renderSources.add(source);
124
92
  }
93
+ include(snapshot.fileDependents, source);
125
94
  }
126
- for (const target of changedTargets) {
127
- addDependents(contentToRender, snapshot.reverseInternalTargetDeps, target);
95
+ for (const key of changedAuthors) {
96
+ include(snapshot.authorDependents, key);
128
97
  }
129
-
130
- for (const [outputPath, owner] of snapshot.outputOwners) {
131
- const nextContentOwner = scan.contentOwners.get(outputPath);
132
- const nextPublicOwner = scan.publicOwners.get(outputPath);
133
- const nextKind = nextPublicOwner
134
- ? 'public'
135
- : nextContentOwner
136
- ? 'content'
137
- : null;
138
- const nextSourcePath = nextPublicOwner || nextContentOwner;
139
-
140
- if (!nextKind || !nextSourcePath) {
141
- continue;
142
- }
143
- if (owner.kind !== nextKind || owner.sourcePath !== nextSourcePath) {
144
- if (nextKind === 'content') {
145
- contentToRender.add(nextSourcePath);
146
- } else {
147
- publicToRender.add(nextSourcePath);
148
- }
98
+ for (const target of new Set([
99
+ ...snapshot.scan.validTargets,
100
+ ...scan.validTargets,
101
+ ...snapshot.scan.generatedPageTargets,
102
+ ...scan.generatedPageTargets,
103
+ ])) {
104
+ if (
105
+ snapshot.scan.validTargets.has(target) !==
106
+ scan.validTargets.has(target) ||
107
+ snapshot.scan.generatedPageTargets.has(target) !==
108
+ scan.generatedPageTargets.has(target)
109
+ ) {
110
+ include(snapshot.targetDependents, target);
149
111
  }
150
112
  }
151
-
152
- for (const [outputPath, nextSourcePath] of scan.contentOwners) {
153
- if (!snapshot.outputOwners.has(outputPath)) {
154
- contentToRender.add(nextSourcePath);
155
- }
156
- }
157
- for (const [outputPath, nextSourcePath] of scan.publicOwners) {
158
- if (!snapshot.outputOwners.has(outputPath)) {
159
- publicToRender.add(nextSourcePath);
160
- }
161
- }
162
-
163
- for (const sourcePath of [...contentToRender]) {
164
- if (!sourceExists(sourcePath, scan)) {
165
- contentToRender.delete(sourcePath);
113
+ for (const [output, producers] of scan.outputProducers) {
114
+ for (const source of producers) {
115
+ if (snapshot.outputs.get(output)?.sourcePath !== source) {
116
+ renderSources.add(source);
117
+ }
166
118
  }
167
119
  }
168
- for (const sourcePath of [...publicToRender]) {
169
- if (!sourceExists(sourcePath, scan)) {
170
- publicToRender.delete(sourcePath);
120
+ for (const source of renderSources) {
121
+ if (!scan.sources.has(source)) {
122
+ renderSources.delete(source);
171
123
  }
172
124
  }
173
-
174
- return {
175
- kind: 'incremental',
176
- scan,
177
- contentToRender,
178
- publicToRender,
179
- contentToRemove,
180
- publicToRemove,
181
- };
125
+ return { kind: 'incremental', scan, renderSources, removeSources };
182
126
  }
@@ -3,7 +3,10 @@ import { B } from '../colors';
3
3
  import { makeLogger, printFlair } from '../log';
4
4
  import { startServer } from '../serve';
5
5
  import { WatchPagefindRunner } from '../pagefind';
6
+ import { isFeatureEnabled } from '../features';
7
+ import { printDiagnostics } from '../build-validation';
6
8
  import type { WatchLifecycleEvent } from './types';
9
+ import type { TadaBuildMeta } from '../build-types';
7
10
  import {
8
11
  WATCH_RELOAD_MESSAGE_REBUILDING,
9
12
  WATCH_RELOAD_MESSAGE_RELOAD,
@@ -14,28 +17,16 @@ import {
14
17
  const log = makeLogger(import.meta.url);
15
18
  const wslog = makeLogger('WebSocket');
16
19
 
17
- function describeChange(kind: 'add' | 'change' | 'unlink'): string {
18
- if (kind === 'add') {
19
- return 'added, rebuilding';
20
- }
21
- if (kind === 'unlink') {
22
- return 'removed, rebuilding';
23
- }
24
- return 'changed, rebuilding';
25
- }
26
-
27
20
  export class TadaWatchRuntime {
28
21
  private httpPort: number | undefined;
29
22
  private distDir: string;
30
23
  private server: Bun.Server<undefined> | null;
31
24
  private pagefindRunner: WatchPagefindRunner | undefined;
32
- private pagefindSiteVariablesKey: string | undefined;
33
25
 
34
26
  constructor({ httpPort, distDir }: { httpPort?: number; distDir: string }) {
35
27
  this.httpPort = httpPort;
36
28
  this.distDir = distDir;
37
29
  this.server = null;
38
- this.pagefindSiteVariablesKey = undefined;
39
30
  }
40
31
 
41
32
  private broadcast(message: string): void {
@@ -64,48 +55,44 @@ export class TadaWatchRuntime {
64
55
  wslog.debug`WebSocket server listening at ws://localhost:${this.server.port}${WATCH_RELOAD_PATH}`;
65
56
  }
66
57
 
67
- async onEvent(event: WatchLifecycleEvent): Promise<void> {
58
+ close(): void {
59
+ this.server?.stop(true);
60
+ this.server = null;
61
+ }
62
+
63
+ async onEvent(event: WatchLifecycleEvent<TadaBuildMeta>): Promise<void> {
68
64
  switch (event.kind) {
69
65
  case 'build-started':
70
- if (!event.batch) {
66
+ if (!event.paths) {
71
67
  return;
72
68
  }
73
- for (const change of event.batch.changes) {
74
- log.event`${B`${path.basename(change.path)}`} ${describeChange(change.kind)}`;
69
+ for (const filePath of event.paths) {
70
+ log.event`${B`${path.basename(filePath)}`} changed, rebuilding`;
75
71
  }
76
72
  this.broadcast(WATCH_RELOAD_MESSAGE_REBUILDING);
77
73
  return;
78
74
  case 'build-succeeded': {
79
75
  printFlair();
80
76
  this.ensureServerStarted();
81
- if (event.batch) {
77
+ if (event.paths) {
82
78
  this.broadcast(WATCH_RELOAD_MESSAGE_RELOAD);
83
79
  }
84
- if (event.meta.siteVariables.features.search !== false) {
85
- const siteVariablesKey = JSON.stringify(event.meta.siteVariables);
86
- if (
87
- !this.pagefindRunner ||
88
- this.pagefindSiteVariablesKey !== siteVariablesKey
89
- ) {
90
- this.pagefindRunner = new WatchPagefindRunner();
91
- this.pagefindSiteVariablesKey = siteVariablesKey;
92
- }
93
- this.pagefindRunner.update(
80
+ if (isFeatureEnabled(event.meta.siteVariables, 'search')) {
81
+ // One indexer for the whole session runs one index at a time, so
82
+ // two runs never write pagefind/ concurrently.
83
+ const runner = (this.pagefindRunner ??= new WatchPagefindRunner());
84
+ runner.update(
94
85
  this.distDir,
95
86
  event.meta.htmlAssetsByPath,
96
87
  event.meta.htmlAnalysisByPath,
88
+ event.meta.pdfSourceByOutputPath,
97
89
  );
98
- setImmediate(() => this.pagefindRunner!.run());
99
- } else {
100
- this.pagefindRunner = undefined;
101
- this.pagefindSiteVariablesKey = undefined;
90
+ setImmediate(() => runner.run());
102
91
  }
103
92
  return;
104
93
  }
105
94
  case 'build-failed':
106
- for (const diagnostic of event.diagnostics) {
107
- log.error`${diagnostic.message}`;
108
- }
95
+ printDiagnostics(event.diagnostics);
109
96
  return;
110
97
  case 'watching':
111
98
  log.info`Watching for changes...`;
@@ -1,17 +1,6 @@
1
1
  import type { ChokidarOptions } from 'chokidar';
2
- import type { TadaBuildMeta } from './compiler-types';
3
- import type { TadaSnapshot } from './snapshot';
4
-
5
- export type WatchEventKind = 'add' | 'change' | 'unlink';
6
-
7
- export interface FileChange {
8
- path: string;
9
- kind: WatchEventKind;
10
- }
11
-
12
- export interface ChangeBatch {
13
- changes: FileChange[];
14
- }
2
+ import type { BuildDiagnostic } from '../build-types';
3
+ import type { TadaSnapshot } from '../site-build';
15
4
 
16
5
  export interface WatchTarget {
17
6
  path: string;
@@ -19,66 +8,45 @@ export interface WatchTarget {
19
8
  filter?: (filePath: string) => boolean;
20
9
  }
21
10
 
22
- export interface WatchDiagnostic {
23
- message: string;
24
- }
11
+ export type WatchBuildResult<Meta> =
12
+ | { ok: true; meta: Meta }
13
+ | { ok: false; diagnostics: BuildDiagnostic[] };
25
14
 
26
- export interface WriteFileMutation {
27
- kind: 'write';
28
- path: string;
29
- content: string | Buffer;
30
- }
15
+ export type WatchLifecycleEvent<Meta> =
16
+ | { kind: 'watching' }
17
+ | { kind: 'build-started'; paths?: ReadonlySet<string> }
18
+ | { kind: 'build-succeeded'; paths?: ReadonlySet<string>; meta: Meta }
19
+ | {
20
+ kind: 'build-failed';
21
+ paths?: ReadonlySet<string>;
22
+ diagnostics: BuildDiagnostic[];
23
+ };
31
24
 
32
- export interface DeleteFileMutation {
33
- kind: 'delete';
34
- path: string;
25
+ export interface WatchClock {
26
+ setTimeout: typeof setTimeout;
27
+ clearTimeout: typeof clearTimeout;
35
28
  }
36
29
 
37
- export type FileMutation = WriteFileMutation | DeleteFileMutation;
38
-
39
- export interface ReplaceRootCommitPlan {
40
- kind: 'replace-root';
41
- stagedPath: string;
42
- targetPath: string;
30
+ export interface WatchEngineOptions<Meta> {
31
+ targets: WatchTarget[];
32
+ build: (paths?: ReadonlySet<string>) => Promise<WatchBuildResult<Meta>>;
33
+ debounceMs?: number;
34
+ onEvent?: (event: WatchLifecycleEvent<Meta>) => void | Promise<void>;
43
35
  }
44
36
 
45
- export interface ApplyMutationsCommitPlan {
46
- kind: 'apply-mutations';
47
- rootDir: string;
48
- mutations: FileMutation[];
37
+ export interface WatchHandle {
38
+ done: Promise<void>;
39
+ close(): Promise<void>;
49
40
  }
50
41
 
51
- export type CommitPlan = ReplaceRootCommitPlan | ApplyMutationsCommitPlan;
52
-
53
- export type CompilerBuildResult =
42
+ /** The result of compiling one watch build, before it is written */
43
+ export type CompileOutcome =
54
44
  | {
55
45
  ok: true;
56
46
  snapshot: TadaSnapshot;
57
- commit: CommitPlan;
58
- meta: TadaBuildMeta;
47
+ /** Full builds are written against the files on disk */
48
+ full: boolean;
49
+ /** Sources whose outputs are rewritten even if they look unchanged */
50
+ forceSourcePaths?: ReadonlySet<string>;
59
51
  }
60
- | { ok: false; diagnostics: WatchDiagnostic[] };
61
-
62
- export interface WatchCompiler {
63
- getWatchTargets(): WatchTarget[];
64
- build(
65
- snapshot: TadaSnapshot | undefined,
66
- batch?: ChangeBatch,
67
- ): Promise<CompilerBuildResult>;
68
- }
69
-
70
- export type WatchLifecycleEvent =
71
- | { kind: 'watching' }
72
- | { kind: 'build-started'; batch?: ChangeBatch }
73
- | { kind: 'build-succeeded'; batch?: ChangeBatch; meta: TadaBuildMeta }
74
- | {
75
- kind: 'build-failed';
76
- batch?: ChangeBatch;
77
- diagnostics: WatchDiagnostic[];
78
- };
79
-
80
- export interface WatchEngineOptions {
81
- compiler: WatchCompiler;
82
- debounceMs?: number;
83
- onEvent?: (event: WatchLifecycleEvent) => void | Promise<void>;
84
- }
52
+ | { ok: false; diagnostics: BuildDiagnostic[] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -65,10 +65,10 @@
65
65
  "chokidar": "^4.0.3",
66
66
  "culori": "^4.0.2",
67
67
  "d3-flextree": "^2.1.2",
68
+ "entities": "^4.5.0",
68
69
  "fontkit": "^2.0.4",
69
70
  "front-matter": "^4.0.2",
70
71
  "java-parser": "^3.0.1",
71
- "jsdom": "^26.1.0",
72
72
  "katex": "^0.16.45",
73
73
  "lodash": "^4.18.1",
74
74
  "markdown-it": "^14.1.1",
@@ -81,8 +81,7 @@
81
81
  "quote-quote": "^1.2.0",
82
82
  "sass": "^1.99.0",
83
83
  "sharp": "^0.35.3",
84
- "shiki": "^4.0.2",
85
- "string-strip-html": "^8.5.0"
84
+ "shiki": "^4.0.2"
86
85
  },
87
86
  "lint-staged": {
88
87
  "*.{ts,js}": "prettier --write",
@@ -100,7 +99,6 @@
100
99
  "devDependencies": {
101
100
  "@eslint/js": "^10.0.1",
102
101
  "@playwright/test": "^1.59.1",
103
- "@saintedlama/lcov-parse": "^1.0.1",
104
102
  "@types/bun": "^1.3.12",
105
103
  "@types/culori": "^4.0.1",
106
104
  "@types/d3-flextree": "^2.1.4",
@@ -118,6 +116,7 @@
118
116
  "istanbul-lib-instrument": "^6.0.3",
119
117
  "istanbul-lib-report": "^3.0.1",
120
118
  "istanbul-reports": "^3.2.0",
119
+ "jsdom": "^26.1.0",
121
120
  "knip": "^6.11.0",
122
121
  "lint-staged": "^16.4.0",
123
122
  "postcss-scss": "^4.0.9",
@@ -46,6 +46,8 @@
46
46
  "maximum": 100,
47
47
  "default": 100
48
48
  },
49
+ "logo": { "type": "string" },
50
+ "favicon": { "type": "string" },
49
51
  "faviconColor": { "type": "string" },
50
52
  "faviconSymbol": { "type": "string", "pattern": "^[A-Z0-9\\- ]{1,5}$" },
51
53
  "faviconFontWeight": {
@@ -83,7 +85,14 @@
83
85
  "$defs": {
84
86
  "serifFontOverride": {
85
87
  "type": "object",
86
- "required": ["regular"],
88
+ "anyOf": [
89
+ { "required": ["regular"] },
90
+ {
91
+ "required": ["tuning"],
92
+ "properties": { "tuning": {} },
93
+ "additionalProperties": false
94
+ }
95
+ ],
87
96
  "properties": {
88
97
  "regular": { "$ref": "#/$defs/publicWoff2Path" },
89
98
  "italic": { "$ref": "#/$defs/publicWoff2Path" },
@@ -110,7 +119,14 @@
110
119
  },
111
120
  "serifMonoFontOverride": {
112
121
  "type": "object",
113
- "required": ["regular"],
122
+ "anyOf": [
123
+ { "required": ["regular"] },
124
+ {
125
+ "required": ["tuning"],
126
+ "properties": { "tuning": {} },
127
+ "additionalProperties": false
128
+ }
129
+ ],
114
130
  "properties": {
115
131
  "regular": { "$ref": "#/$defs/publicWoff2Path" },
116
132
  "italic": { "$ref": "#/$defs/publicWoff2Path" },