@abreen/tada 1.11.3 → 1.13.4

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 (85) hide show
  1. package/README.md +65 -64
  2. package/bin/tada.ts +61 -66
  3. package/build/build-manifest.ts +14 -3
  4. package/build/bundle.ts +0 -1
  5. package/build/config-files.ts +183 -0
  6. package/build/config-loader.ts +159 -0
  7. package/build/copy.ts +0 -83
  8. package/build/generate-content-assets.ts +75 -160
  9. package/build/globals.ts +48 -89
  10. package/build/log.ts +27 -18
  11. package/build/pagefind.ts +33 -26
  12. package/build/pipeline.ts +98 -79
  13. package/build/reachability.ts +51 -248
  14. package/build/serve.ts +35 -6
  15. package/build/site-variables.ts +20 -11
  16. package/build/source-model.ts +400 -0
  17. package/build/{watch/records.ts → source-records.ts} +111 -49
  18. package/build/template-globals.ts +91 -0
  19. package/build/templates.ts +50 -40
  20. package/build/test-helpers.ts +15 -0
  21. package/build/toc-plugin.ts +5 -1
  22. package/build/types.d.ts +9 -1
  23. package/build/util.ts +8 -0
  24. package/build/utils/code.ts +6 -16
  25. package/build/utils/content-files.ts +57 -126
  26. package/build/utils/final-html.ts +239 -0
  27. package/build/utils/markdown.ts +237 -17
  28. package/build/utils/paths.ts +2 -1
  29. package/build/utils/render.ts +108 -99
  30. package/build/utils/trace-core.ts +3 -7
  31. package/build/validate-config-links.ts +91 -14
  32. package/build/watch/assets.ts +0 -2
  33. package/build/watch/build-full.ts +14 -15
  34. package/build/watch/build-incremental.ts +15 -8
  35. package/build/watch/compiler-types.d.ts +6 -1
  36. package/build/watch/compiler.ts +27 -15
  37. package/build/watch/index.ts +2 -7
  38. package/build/watch/planner.ts +32 -16
  39. package/build/watch/reload.ts +4 -0
  40. package/build/watch/runtime.ts +29 -45
  41. package/build/watch/snapshot.ts +50 -402
  42. package/build/watch/validation.ts +6 -3
  43. package/build/watch-reachability-state.ts +16 -23
  44. package/build/watch-reload-client.ts +11 -4
  45. package/init/content/index.md +1 -1
  46. package/init/content/labs/02/index.md +61 -0
  47. package/init/content/labs/02/selection_sort.py +23 -0
  48. package/init/content/labs/index.md +4 -2
  49. package/init/content/lectures/01/index.md +1 -1
  50. package/init/content/markdown.md +1 -1
  51. package/package.json +3 -4
  52. package/src/_base.scss +67 -0
  53. package/src/anchor/README.md +9 -0
  54. package/src/anchor/index.ts +115 -16
  55. package/src/anchor/style.scss +70 -10
  56. package/src/code/index.ts +46 -18
  57. package/src/code.scss +2 -1
  58. package/src/globals.ts +47 -0
  59. package/src/index.ts +1 -0
  60. package/src/navigate/index.ts +2 -5
  61. package/src/navigate/lifecycle.ts +3 -1
  62. package/src/navigate/runtime.ts +6 -5
  63. package/src/page-update/index.ts +20 -16
  64. package/src/print/style.scss +0 -4
  65. package/src/search/index.ts +53 -25
  66. package/src/slides/index.ts +481 -0
  67. package/src/slides/style.scss +104 -0
  68. package/src/style.scss +8 -3
  69. package/src/timezone/index.ts +16 -20
  70. package/src/top/index.ts +2 -4
  71. package/src/trace/index.ts +116 -14
  72. package/src/trace/style.scss +1 -1
  73. package/src/util.ts +6 -2
  74. package/templates/_author.html +2 -2
  75. package/templates/_heading.html +1 -1
  76. package/templates/_nav.html +2 -2
  77. package/templates/_theme.scss +4 -3
  78. package/templates/_top.html +14 -14
  79. package/templates/default.html +9 -0
  80. package/types/dev.d.ts +1 -1
  81. package/types/site-variables.d.ts +4 -5
  82. package/watch/fs-commit.ts +63 -10
  83. package/build/apply-base-path-plugin.ts +0 -186
  84. package/build/content-watch.ts +0 -104
  85. package/build/validate-internal-links-plugin.ts +0 -207
@@ -1,186 +0,0 @@
1
- import path from 'path';
2
- import type MarkdownIt from 'markdown-it';
3
- import type Token from 'markdown-it/lib/token.mjs';
4
- import { JSDOM } from 'jsdom';
5
- import { createApplyBasePath } from './utils/paths';
6
- import { getExtensionToShikiLanguage } from './site-variables';
7
- import { isInternalLink } from './utils/link';
8
- import { makeLogger } from './log';
9
- import type { SiteVariables } from './types';
10
-
11
- const log = makeLogger(import.meta.url);
12
-
13
- function serializeStartTag(element: Element): string {
14
- const attrs = Array.from(element.attributes).map(attr =>
15
- attr.value === '' ? attr.name : `${attr.name}="${attr.value}"`,
16
- );
17
- return `<${element.tagName.toLowerCase()}${attrs.length > 0 ? ` ${attrs.join(' ')}` : ''}>`;
18
- }
19
-
20
- function isStandaloneOpeningAnchorTag(html: string): boolean {
21
- if (!/^<a\b/i.test(html)) {
22
- return false;
23
- }
24
-
25
- let i = 2;
26
- let quote: '"' | "'" | null = null;
27
-
28
- while (i < html.length) {
29
- const char = html[i];
30
- if (quote) {
31
- if (char === quote) {
32
- quote = null;
33
- }
34
- i += 1;
35
- continue;
36
- }
37
-
38
- if (char === '"' || char === "'") {
39
- quote = char;
40
- i += 1;
41
- continue;
42
- }
43
-
44
- if (char === '>') {
45
- return html.slice(i + 1).trim() === '';
46
- }
47
-
48
- i += 1;
49
- }
50
-
51
- return false;
52
- }
53
-
54
- interface ApplyBasePathOptions {
55
- literateJavaOutputPaths?: Set<string>;
56
- sourceUrlPath?: string;
57
- validTargets?: Set<string>;
58
- }
59
-
60
- export default function applyBasePathPlugin(
61
- md: MarkdownIt,
62
- siteVariables: SiteVariables,
63
- pluginOptions: ApplyBasePathOptions = {},
64
- ): void {
65
- const applyBasePath = createApplyBasePath(siteVariables);
66
- const literateJavaOutputPaths = pluginOptions.literateJavaOutputPaths;
67
- const sourceUrlPath = pluginOptions.sourceUrlPath;
68
- const validTargets = pluginOptions.validTargets;
69
-
70
- function rewriteInternalHref(href: string): string {
71
- const resolveAgainstSource = (p: string): string =>
72
- p.startsWith('/')
73
- ? p
74
- : path.posix.join(path.posix.dirname(sourceUrlPath!), p);
75
-
76
- const match = href.match(/^([^?#]*)(.*)$/);
77
- const pathname = match ? match[1] : href;
78
- const suffix = match ? match[2] : '';
79
- let modifiedPath = pathname;
80
-
81
- for (const ext of Object.keys(getExtensionToShikiLanguage(siteVariables))) {
82
- if (modifiedPath.endsWith(`.${ext}`)) {
83
- if (literateJavaOutputPaths && sourceUrlPath) {
84
- const resolved = resolveAgainstSource(modifiedPath);
85
- if (literateJavaOutputPaths.has(resolved)) {
86
- break;
87
- }
88
- }
89
- // Only rewrite if the .html version exists. Public files with
90
- // code extensions are copied as-is and have no .html page.
91
- if (validTargets && sourceUrlPath) {
92
- const resolved = resolveAgainstSource(modifiedPath);
93
- if (!validTargets.has(`${resolved}.html`)) {
94
- break;
95
- }
96
- }
97
- modifiedPath += '.html';
98
- break;
99
- }
100
- }
101
-
102
- return modifiedPath + suffix;
103
- }
104
-
105
- function checkAndApplyBasePath(token: Token): void {
106
- if (token.type === 'link_open') {
107
- const href = token.attrGet('href');
108
- if (!href) {
109
- return;
110
- }
111
-
112
- if (href.startsWith('/')) {
113
- const modifiedHref = rewriteInternalHref(href);
114
- const afterApply = applyBasePath(modifiedHref);
115
- log.debug`Applying base path: ${href} -> ${afterApply}`;
116
- token.attrSet('href', afterApply);
117
- } else if (isInternalLink(href)) {
118
- // Relative link: only rewrite code extensions, don't apply base path
119
- const modifiedHref = rewriteInternalHref(href);
120
- if (modifiedHref !== href) {
121
- log.debug`Rewriting internal link: ${href} -> ${modifiedHref}`;
122
- token.attrSet('href', modifiedHref);
123
- }
124
- }
125
- } else if (token.type === 'image') {
126
- const src = token.attrGet('src');
127
- if (src && src.startsWith('/')) {
128
- const afterApply = applyBasePath(src);
129
- log.debug`Applying base path to image: ${src} -> ${afterApply}`;
130
- token.attrSet('src', afterApply);
131
- }
132
- } else if (token.type === 'html_block' || token.type === 'html_inline') {
133
- if (isStandaloneOpeningAnchorTag(token.content)) {
134
- const dom = new JSDOM(`<body>${token.content}__tada__</a></body>`);
135
- const anchor = dom.window.document.body.querySelector('a[href]');
136
- const href = anchor?.getAttribute('href');
137
- if (anchor && href && isInternalLink(href)) {
138
- const rewrittenHref = rewriteInternalHref(href);
139
- const finalHref = href.startsWith('/')
140
- ? applyBasePath(rewrittenHref)
141
- : rewrittenHref;
142
- log.debug`Rewriting raw HTML anchor: ${href} -> ${finalHref}`;
143
- anchor.setAttribute('href', finalHref);
144
- token.content = serializeStartTag(anchor);
145
- }
146
- } else {
147
- const dom = new JSDOM(`<body>${token.content}</body>`);
148
- const { document } = dom.window;
149
- let changed = false;
150
-
151
- for (const anchor of document.body.querySelectorAll('a[href]')) {
152
- const href = anchor.getAttribute('href');
153
- if (href && isInternalLink(href)) {
154
- const rewrittenHref = rewriteInternalHref(href);
155
- const finalHref = href.startsWith('/')
156
- ? applyBasePath(rewrittenHref)
157
- : rewrittenHref;
158
- log.debug`Rewriting raw HTML anchor: ${href} -> ${finalHref}`;
159
- anchor.setAttribute('href', finalHref);
160
- changed = true;
161
- }
162
- }
163
-
164
- for (const image of document.body.querySelectorAll('img[src]')) {
165
- const src = image.getAttribute('src');
166
- if (src && src.startsWith('/')) {
167
- const afterApply = applyBasePath(src);
168
- log.debug`Applying base path to raw HTML image: ${src} -> ${afterApply}`;
169
- image.setAttribute('src', afterApply);
170
- changed = true;
171
- }
172
- }
173
-
174
- if (changed) {
175
- token.content = document.body.innerHTML;
176
- }
177
- }
178
- }
179
-
180
- token.children?.forEach(checkAndApplyBasePath);
181
- }
182
-
183
- md.core.ruler.push('apply_base_path', state => {
184
- state.tokens.forEach(checkAndApplyBasePath);
185
- });
186
- }
@@ -1,104 +0,0 @@
1
- import path from 'path';
2
- import type { SiteVariables } from './types';
3
- import { getExtensionToShikiLanguage } from './site-variables';
4
- import { getContentDir, getBuildContentFiles, isPartial } from './util';
5
- import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
6
- import { B } from './colors';
7
- import { makeLogger } from './log';
8
-
9
- const log = makeLogger(import.meta.url);
10
-
11
- interface ChangeDetectionResult {
12
- templateError: Error | null;
13
- needsRestart: boolean;
14
- changedContentFiles?: Set<string>;
15
- jsonDataChanged?: boolean;
16
- partialsChanged?: boolean;
17
- }
18
-
19
- export class ContentChangeDetector {
20
- private siteVariables: SiteVariables;
21
- private lastSig: string | null;
22
-
23
- constructor(siteVariables: SiteVariables) {
24
- this.siteVariables = siteVariables;
25
- this.lastSig = null;
26
- }
27
-
28
- detectChanges(modifiedFiles: Iterable<string>): ChangeDetectionResult {
29
- const resolvedFiles = new Set(
30
- [...modifiedFiles].map(filePath => path.resolve(filePath)),
31
- );
32
-
33
- const jsonDataDir = getJsonDataDir();
34
-
35
- // Try to recompile templates (also re-reads JSON data files)
36
- let templateError: Error | null = null;
37
- try {
38
- compileTemplates(this.siteVariables);
39
- } catch (err) {
40
- templateError = err as Error;
41
- }
42
-
43
- if (templateError) {
44
- return { templateError, needsRestart: false };
45
- }
46
-
47
- const contentDir = getContentDir();
48
- const normalizedContentDir = path.resolve(contentDir) + path.sep;
49
- const buildContentFiles = getBuildContentFiles(
50
- contentDir,
51
- Object.keys(getExtensionToShikiLanguage(this.siteVariables)),
52
- );
53
-
54
- // Detect structural changes (files added or deleted)
55
- const sig = buildContentFiles.slice().sort().join('\0');
56
- let needsRestart = false;
57
- if (this.lastSig !== null && sig !== this.lastSig) {
58
- needsRestart = true;
59
- }
60
- this.lastSig = sig;
61
-
62
- const changedContentFiles = new Set(
63
- [...resolvedFiles].filter(filePath =>
64
- filePath.startsWith(normalizedContentDir),
65
- ),
66
- );
67
-
68
- // Check if any JSON data file changed (nav.json, authors.json)
69
- const jsonDataPaths = JSON_DATA_FILES.map(f =>
70
- path.resolve(jsonDataDir, f),
71
- );
72
- const changedJsonDataPaths = [...resolvedFiles].filter(filePath =>
73
- jsonDataPaths.includes(filePath),
74
- );
75
- const jsonDataChanged = changedJsonDataPaths.length > 0;
76
- const changedPartials = [...changedContentFiles].filter(f => isPartial(f));
77
- const partialsChanged = changedPartials.length > 0;
78
-
79
- for (const filePath of changedJsonDataPaths) {
80
- log.event`${B`${path.basename(filePath)}`} changed, rebuilding`;
81
- }
82
-
83
- if (partialsChanged) {
84
- for (const filePath of changedPartials) {
85
- log.event`Partial ${B`${path.basename(filePath)}`} changed, rebuilding`;
86
- }
87
- }
88
-
89
- const changedTraceFiles = [...changedContentFiles].filter(
90
- f => f.endsWith('.java') || f.endsWith('.py'),
91
- );
92
- for (const filePath of changedTraceFiles) {
93
- log.event`${B`${path.basename(filePath)}`} changed, re-running traces`;
94
- }
95
-
96
- return {
97
- templateError: null,
98
- needsRestart,
99
- changedContentFiles,
100
- jsonDataChanged,
101
- partialsChanged,
102
- };
103
- }
104
- }
@@ -1,207 +0,0 @@
1
- import type MarkdownIt from 'markdown-it';
2
- import type Token from 'markdown-it/lib/token.mjs';
3
- import { JSDOM } from 'jsdom';
4
- import path from 'path';
5
- import { isInternalLink } from './utils/link';
6
- import { makeLogger } from './log';
7
-
8
- const log = makeLogger(import.meta.url);
9
-
10
- interface ValidateInternalLinksOptions {
11
- enabled?: boolean;
12
- filePath?: string;
13
- sourceUrlPath?: string;
14
- validTargets?: Set<string>;
15
- codeExtensions?: string[];
16
- onValidLink?: (targetPath: string) => void;
17
- }
18
-
19
- function stripQueryAndHash(href: string): string {
20
- return href.split('#')[0].split('?')[0];
21
- }
22
-
23
- function normalizePathname(pathname: string): string {
24
- const normalized = path.posix.normalize(pathname);
25
- if (normalized === '.') {
26
- return '/';
27
- }
28
- return normalized.startsWith('/') ? normalized : `/${normalized}`;
29
- }
30
-
31
- function createCodeExtPattern(codeExtensions: string[]): RegExp | null {
32
- const escaped = codeExtensions.map(ext =>
33
- ext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
34
- );
35
- if (escaped.length === 0) {
36
- return null;
37
- }
38
- return new RegExp(`\\.(${escaped.join('|')})$`, 'i');
39
- }
40
-
41
- function rewriteCodeLink(
42
- pathname: string,
43
- codeExtPattern: RegExp | null,
44
- ): string {
45
- if (!codeExtPattern || !codeExtPattern.test(pathname)) {
46
- return pathname;
47
- }
48
-
49
- return `${pathname}.html`;
50
- }
51
-
52
- function resolveLinkPath(
53
- sourceUrlPath: string,
54
- rawHref: string,
55
- codeExtPattern: RegExp | null,
56
- ): string | null {
57
- const hrefPath = stripQueryAndHash(rawHref.trim());
58
- if (!hrefPath) {
59
- return null;
60
- }
61
-
62
- const sourceDir = path.posix.dirname(sourceUrlPath);
63
- const resolved = hrefPath.startsWith('/')
64
- ? normalizePathname(hrefPath)
65
- : normalizePathname(path.posix.join(sourceDir, hrefPath));
66
-
67
- // Decode percent-encoded characters so the resolved path can be matched
68
- // against `validTargets`, which contains raw filesystem-derived paths.
69
- // markdown-it's normalizeLink percent-encodes hrefs, so a link like
70
- // `[x](/my notes.md)` arrives here as `/my%20notes.md`.
71
- let decoded: string;
72
- try {
73
- decoded = normalizePathname(decodeURIComponent(resolved));
74
- } catch {
75
- decoded = resolved;
76
- }
77
-
78
- return rewriteCodeLink(decoded, codeExtPattern);
79
- }
80
-
81
- function getDirectoryIndexPath(pathname: string): string {
82
- return normalizePathname(path.posix.join(pathname, 'index.html'));
83
- }
84
-
85
- export default function validateInternalLinks(
86
- md: MarkdownIt,
87
- options: ValidateInternalLinksOptions = {},
88
- ): void {
89
- const {
90
- enabled = true,
91
- filePath,
92
- sourceUrlPath,
93
- validTargets,
94
- codeExtensions = [],
95
- onValidLink,
96
- } = options;
97
-
98
- if (!enabled) {
99
- return;
100
- }
101
-
102
- if (!filePath || !sourceUrlPath || !(validTargets instanceof Set)) {
103
- throw new Error(
104
- 'validate-internal-links-plugin requires filePath, sourceUrlPath, and validTargets',
105
- );
106
- }
107
-
108
- const codeExtPattern = createCodeExtPattern(codeExtensions);
109
- const seenErrors = new Set<string>();
110
-
111
- function reportBrokenLink(rawHref: string, resolvedPath: string): void {
112
- const key = `${rawHref}|${resolvedPath}`;
113
- if (seenErrors.has(key)) {
114
- return;
115
- }
116
- seenErrors.add(key);
117
-
118
- log.error`${filePath}: broken internal link: "${rawHref}" (resolved to "${resolvedPath}")`;
119
- }
120
-
121
- function reportDirectoryLink(
122
- rawHref: string,
123
- resolvedPath: string,
124
- indexPath: string,
125
- ): void {
126
- const key = `${rawHref}|${resolvedPath}|directory`;
127
- if (seenErrors.has(key)) {
128
- return;
129
- }
130
- seenErrors.add(key);
131
-
132
- log.error`${filePath}: directory link must reference index.html explicitly: "${rawHref}" (resolved to "${resolvedPath}", expected "${indexPath}")`;
133
- }
134
-
135
- function validateHref(rawHref: string): void {
136
- if (!rawHref) {
137
- return;
138
- }
139
-
140
- const href = rawHref.trim();
141
- if (!href || !isInternalLink(href)) {
142
- return;
143
- }
144
-
145
- const resolvedPath = resolveLinkPath(sourceUrlPath!, href, codeExtPattern);
146
- if (!resolvedPath) {
147
- return;
148
- }
149
-
150
- const directoryIndexPath = getDirectoryIndexPath(resolvedPath);
151
- if (
152
- directoryIndexPath !== resolvedPath &&
153
- validTargets!.has(directoryIndexPath)
154
- ) {
155
- reportDirectoryLink(rawHref, resolvedPath, directoryIndexPath);
156
- return;
157
- }
158
-
159
- if (!validTargets!.has(resolvedPath)) {
160
- // If code extension rewriting changed the path, check the original
161
- // path too. Public files with code extensions are copied as-is and
162
- // do not produce .html versions.
163
- if (codeExtPattern) {
164
- const unrewritten = resolveLinkPath(sourceUrlPath!, href, null);
165
- if (unrewritten && validTargets!.has(unrewritten)) {
166
- onValidLink?.(unrewritten);
167
- return;
168
- }
169
- }
170
- reportBrokenLink(rawHref, resolvedPath);
171
- return;
172
- }
173
-
174
- onValidLink?.(resolvedPath);
175
- }
176
-
177
- function validateToken(token: Token): void {
178
- if (token.type === 'link_open') {
179
- const href = token.attrGet('href');
180
- if (href) {
181
- validateHref(href);
182
- }
183
- } else if (token.type === 'html_block' || token.type === 'html_inline') {
184
- const dom = new JSDOM(`<body>${token.content}</body>`);
185
- for (const anchor of dom.window.document.body.querySelectorAll(
186
- 'a[href]',
187
- )) {
188
- const href = anchor.getAttribute('href');
189
- if (href) {
190
- validateHref(href);
191
- }
192
- }
193
- }
194
-
195
- token.children?.forEach(validateToken);
196
- }
197
-
198
- md.core.ruler.push('validate_internal_links', state => {
199
- state.tokens.forEach(validateToken);
200
-
201
- if (seenErrors.size > 0) {
202
- throw new Error(
203
- `${filePath}: found ${seenErrors.size} broken internal link(s)`,
204
- );
205
- }
206
- });
207
- }