@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
@@ -0,0 +1,91 @@
1
+ import timezones from '../src/timezone/timezones.json' with { type: 'json' };
2
+ import type { SiteVariables } from './types';
3
+
4
+ const MONTHS = [
5
+ '',
6
+ 'January',
7
+ 'February',
8
+ 'March',
9
+ 'April',
10
+ 'May',
11
+ 'June',
12
+ 'July',
13
+ 'August',
14
+ 'September',
15
+ 'October',
16
+ 'November',
17
+ 'December',
18
+ ];
19
+
20
+ interface TemplateGlobals {
21
+ isHomePage: boolean;
22
+ isoDate: (str: string | null | undefined) => string | null;
23
+ readableDate: (date: string | Date | null | undefined) => string;
24
+ classNames: (obj: Record<string, unknown>) => string;
25
+ cx: (obj: Record<string, unknown>) => string;
26
+ renderTimeZoneChooser: () => string;
27
+ }
28
+
29
+ export default function createTemplateGlobals(
30
+ pageVariables: Record<string, unknown>,
31
+ siteVariables: SiteVariables,
32
+ subPath: string,
33
+ ): TemplateGlobals {
34
+ const defaultTz = timezones.find(
35
+ t => t.value === siteVariables.defaultTimeZone,
36
+ )!;
37
+ const options = timezones
38
+ .map(tz => {
39
+ const label =
40
+ tz.value === 'UTC' ? tz.label : `${tz.label} (${tz.abbreviation})`;
41
+ const selected = tz.value === defaultTz.value ? ' selected' : '';
42
+ return `<option value="${tz.value}"${selected}>${label}</option>`;
43
+ })
44
+ .join('');
45
+ const renderTimeZoneChooser = () =>
46
+ `<span class="time-zone-label" hidden>Times shown in </span><select class="time-zone" hidden disabled aria-label="Time zone">${options}</select><noscript>Times shown in ${defaultTz.abbreviation}.</noscript>`;
47
+
48
+ return {
49
+ isHomePage: subPath === 'index',
50
+ isoDate,
51
+ readableDate,
52
+ classNames,
53
+ cx: classNames,
54
+ renderTimeZoneChooser,
55
+ };
56
+ }
57
+
58
+ function isoDate(str: string | null | undefined): string | null {
59
+ if (str == null || str == '') {
60
+ return null;
61
+ }
62
+ const date = new Date(str);
63
+ return date.toISOString().slice(0, 10);
64
+ }
65
+
66
+ function readableDate(date: string | Date | null | undefined): string {
67
+ if (date == null || date == '') {
68
+ return '';
69
+ }
70
+
71
+ if (!(date instanceof Date)) {
72
+ date = new Date(date);
73
+ }
74
+
75
+ const str = date.toISOString();
76
+ const year = str.slice(0, 4);
77
+ const month = Number(str.slice(5, 7));
78
+ const day = Number(str.slice(8, 10));
79
+
80
+ return `${MONTHS[month]} ${day}, ${year}`;
81
+ }
82
+
83
+ export function classNames(obj: Record<string, unknown>): string {
84
+ const names: string[] = [];
85
+ for (const key in obj) {
86
+ if (obj[key]) {
87
+ names.push(key);
88
+ }
89
+ }
90
+ return names.join(' ');
91
+ }
@@ -1,6 +1,15 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import _ from 'lodash';
4
+ import {
5
+ PROJECT_CONFIG_NAMES,
6
+ getProjectConfigBaseName,
7
+ type ProjectConfigName,
8
+ } from './config-files';
9
+ import {
10
+ loadProjectConfig,
11
+ type LoadedProjectConfigFile,
12
+ } from './config-loader';
4
13
  import { compile as compileJsonSchema, doValidation } from './json-schema';
5
14
  import { makeLogger } from './log';
6
15
  import { getPackageDir, getProjectDir } from './utils/paths';
@@ -12,34 +21,33 @@ const log: Logger = makeLogger(import.meta.url);
12
21
  // Store all templates in memory (don't read template files during build)
13
22
  const templates: Record<string, string> = {};
14
23
 
15
- // All parsed data (from .json files)
16
- const jsonData: Record<string, unknown> = {};
24
+ // All parsed project config files
25
+ const projectConfig: Partial<Record<ProjectConfigName, unknown>> = {};
26
+ const loadedProjectConfig: Partial<
27
+ Record<ProjectConfigName, LoadedProjectConfigFile>
28
+ > = {};
17
29
 
18
- // Compiled JSON Schema for the .json files
19
- const validators: Record<string, ValidateFunction> = {};
30
+ // Compiled JSON Schema for the project config files
31
+ const validators: Partial<Record<ProjectConfigName, ValidateFunction>> = {};
20
32
 
21
33
  // Keeps track of template call tree
22
34
  const renderStack: string[] = [];
23
35
  let errorStack: string[] | null = null;
24
36
 
25
- // JSON data files that live in the user's config directory
26
- const REQUIRED_JSON_DATA_FILES: string[] = ['nav.json'];
27
- const OPTIONAL_JSON_DATA_FILES: string[] = ['authors.json'];
28
- const JSON_DATA_FILES: string[] = [
29
- ...REQUIRED_JSON_DATA_FILES,
30
- ...OPTIONAL_JSON_DATA_FILES,
31
- ];
32
-
33
37
  function getHtmlTemplatesDir(): string {
34
38
  return path.resolve(getPackageDir(), 'templates');
35
39
  }
36
40
 
37
- function getJsonDataDir(): string {
41
+ function getProjectConfigDir(): string {
38
42
  return getProjectDir();
39
43
  }
40
44
 
41
- export function json(fileName: string): unknown {
42
- return jsonData[fileName];
45
+ export function config(name: ProjectConfigName): unknown {
46
+ return projectConfig[name];
47
+ }
48
+
49
+ export function getConfigFileName(name: ProjectConfigName): string | undefined {
50
+ return loadedProjectConfig[name]?.fileName;
43
51
  }
44
52
 
45
53
  export function render(
@@ -50,8 +58,8 @@ export function render(
50
58
  // Allow the template to call render(), it will use our params
51
59
  params.render = (otherFileName: string) => render(otherFileName, params);
52
60
 
53
- // Allow the template to read the JSON files we previously read into memory
54
- params.json = json;
61
+ // Allow templates to read the project config files we previously read into memory
62
+ params.config = config;
55
63
  }
56
64
 
57
65
  renderStack.push(fileName);
@@ -82,7 +90,11 @@ export function compileTemplates(
82
90
  }
83
91
 
84
92
  Object.keys(templates).forEach(k => delete templates[k]);
85
- Object.keys(jsonData).forEach(k => delete jsonData[k]);
93
+ for (const name of PROJECT_CONFIG_NAMES) {
94
+ delete projectConfig[name];
95
+ delete loadedProjectConfig[name];
96
+ delete validators[name];
97
+ }
86
98
 
87
99
  // Load HTML templates from the package
88
100
  const htmlDir = getHtmlTemplatesDir();
@@ -97,41 +109,39 @@ export function compileTemplates(
97
109
  }
98
110
  });
99
111
 
100
- // Load JSON data files from the project's config directory
101
- const jsonDir = getJsonDataDir();
102
- for (const fileName of JSON_DATA_FILES) {
103
- const filePath = path.join(jsonDir, fileName);
104
- if (!fs.existsSync(filePath)) {
105
- if (REQUIRED_JSON_DATA_FILES.includes(fileName)) {
106
- throw new Error(`Missing required config file: ${filePath}`);
107
- }
112
+ // Load project config files from the project's config directory
113
+ const projectConfigDir = getProjectConfigDir();
114
+ for (const name of PROJECT_CONFIG_NAMES) {
115
+ const loaded = loadProjectConfig(projectConfigDir, name, siteVariables);
116
+ if (!loaded) {
108
117
  continue;
109
118
  }
110
119
 
111
120
  // Schema validation
112
121
  const schemaDir = path.resolve(getPackageDir(), 'schema');
113
- const schemaFile = `${path.parse(fileName).name}.schema.json`;
122
+ const schemaFile = `${getProjectConfigBaseName(name)}.schema.json`;
114
123
  const schemaPath = path.join(schemaDir, schemaFile);
115
124
  if (!fs.existsSync(schemaPath)) {
116
- throw new Error(`Missing JSON Schema for ${fileName}: ${schemaPath}`);
125
+ throw new Error(
126
+ `Missing JSON Schema for ${loaded.fileName}: ${schemaPath}`,
127
+ );
117
128
  }
118
- compileAndSetValidator(schemaPath, fileName);
129
+ compileAndSetValidator(schemaPath, name);
119
130
 
120
- log.debug`Reading ${fileName}`;
121
- jsonData[fileName] = JSON.parse(
122
- _.template(fs.readFileSync(filePath, 'utf-8'))({
123
- vars: siteVariables.vars || {},
124
- site: siteVariables,
125
- }),
126
- );
131
+ log.debug`Reading ${loaded.fileName}`;
132
+ projectConfig[name] = loaded.value;
133
+ loadedProjectConfig[name] = loaded;
127
134
 
128
- doValidation(validators[fileName], jsonData[fileName], fileName);
135
+ doValidation(validators[name]!, projectConfig[name], loaded.fileName);
129
136
  }
130
137
  }
131
138
 
132
- function compileAndSetValidator(schemaPath: string, fileName: string): void {
139
+ function compileAndSetValidator(
140
+ schemaPath: string,
141
+ name: ProjectConfigName,
142
+ ): void {
133
143
  const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
134
- validators[fileName] = compileJsonSchema(schema);
144
+ validators[name] = compileJsonSchema(schema);
135
145
  }
136
146
 
137
- export { getHtmlTemplatesDir, getJsonDataDir, JSON_DATA_FILES };
147
+ export { getHtmlTemplatesDir, getProjectConfigDir };
@@ -0,0 +1,15 @@
1
+ type FsModule = typeof import('fs');
2
+ type FsModuleOverrides = object & { promises?: FsModule['promises'] };
3
+
4
+ type FsModuleMock<T extends FsModuleOverrides> = T & {
5
+ default: Omit<T, 'promises'> & Pick<FsModule, 'promises'>;
6
+ promises: FsModule['promises'];
7
+ };
8
+
9
+ export function createFsModuleMock<T extends FsModuleOverrides>(
10
+ overrides: T,
11
+ ): FsModuleMock<T> {
12
+ const promises = overrides.promises ?? ({} as FsModule['promises']);
13
+
14
+ return { ...overrides, default: { ...overrides, promises }, promises };
15
+ }
@@ -55,7 +55,11 @@ export function tocPlugin(md: MarkdownIt): void {
55
55
  }
56
56
 
57
57
  // Thematic breaks / dinkuses (only at top level)
58
- if (token.type === 'hr' && containerStack.length === 0) {
58
+ if (
59
+ token.type === 'hr' &&
60
+ containerStack.length === 0 &&
61
+ !state.env?.slides
62
+ ) {
59
63
  items.push({ kind: 'dinkus' });
60
64
  continue;
61
65
  }
package/build/types.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { BundledLanguage } from 'shiki';
2
+ import type { TadaProjectScan } from './source-model';
2
3
 
3
4
  export type PlainTextLanguage = 'text' | 'txt' | 'plain';
4
5
 
5
- /** Site configuration loaded from site.dev.json or site.prod.json */
6
+ /** Site configuration loaded from site.dev.yaml/yml/json or site.prod.yaml/yml/json */
6
7
  export interface SiteVariables {
7
8
  base: string;
8
9
  basePath: string;
@@ -33,15 +34,21 @@ export type FeatureConfig = {
33
34
  };
34
35
 
35
36
  /** A rendered content asset ready to write to dist/ */
37
+ export interface HtmlOutputAnalysis {
38
+ outgoingTargets: Set<string>;
39
+ }
40
+
36
41
  export interface Asset {
37
42
  assetPath: string;
38
43
  content: string | Buffer;
44
+ htmlAnalysis?: HtmlOutputAnalysis;
39
45
  }
40
46
 
41
47
  /** Options for the content rendering pipeline */
42
48
  export interface ContentRenderOptions {
43
49
  distDir: string;
44
50
  assetFiles: string[];
51
+ scan: TadaProjectScan;
45
52
  watchState?: WatchState;
46
53
  }
47
54
 
@@ -59,6 +66,7 @@ export interface ContentRenderResult {
59
66
  removedHtmlAssetPaths: Set<string>;
60
67
  removedOutputRelPaths: Set<string>;
61
68
  htmlAssetsByPath: Map<string, string>;
69
+ htmlAnalysisByPath: Map<string, HtmlOutputAnalysis>;
62
70
  buildContentFiles: string[];
63
71
  }
64
72
 
package/build/util.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export {
2
+ addGeneratedRouteAliases,
3
+ getProcessedExts,
4
+ getSourceOutputPaths,
5
+ getSourceTargetPaths,
6
+ isBuildContentSource,
7
+ } from './source-model';
1
8
  export {
2
9
  getBuildContentFiles,
3
10
  getContentFiles,
@@ -21,6 +28,7 @@ export {
21
28
  } from './utils/paths';
22
29
  export {
23
30
  injectAssetTags,
31
+ preparePageTemplateHtml,
24
32
  renderCodePageAsset,
25
33
  renderCopiedContentAsset,
26
34
  renderLiterateJavaPageAsset,
@@ -6,7 +6,6 @@ import { makeLogger } from '../log';
6
6
  import { getExtensionToShikiLanguage } from '../site-variables';
7
7
  import { highlightCode } from './shiki-highlighter';
8
8
  import externalLinksPlugin from '../external-links-plugin';
9
- import applyBasePathPlugin from '../apply-base-path-plugin';
10
9
  import { createApplyBasePath } from './paths';
11
10
  import { splitLines } from './literate-java';
12
11
  import type { JavaTocEntry, SiteVariables } from '../types';
@@ -40,13 +39,11 @@ const log = makeLogger(import.meta.url);
40
39
 
41
40
  const PROSE_LINE = /^\s*\/\/\/(\s|$)/;
42
41
 
43
- function createCodeMarkdown(
44
- siteVariables: SiteVariables,
45
- options: Record<string, unknown> = {},
46
- ): MarkdownIt {
47
- return new MarkdownIt({ html: true, typographer: true })
48
- .use(externalLinksPlugin, siteVariables)
49
- .use(applyBasePathPlugin, siteVariables, options);
42
+ function createCodeMarkdown(siteVariables: SiteVariables): MarkdownIt {
43
+ return new MarkdownIt({ html: true, typographer: true }).use(
44
+ externalLinksPlugin,
45
+ siteVariables,
46
+ );
50
47
  }
51
48
 
52
49
  // Matches Markdown links: [text](url)
@@ -504,15 +501,8 @@ export function renderCodeWithComments(
504
501
  lang: string,
505
502
  siteVariables: SiteVariables,
506
503
  pageDirPath?: string,
507
- sourceUrlPath?: string,
508
- validTargets?: Set<string>,
509
- literateJavaOutputPaths?: Set<string>,
510
504
  ): string {
511
- const md = createCodeMarkdown(siteVariables, {
512
- sourceUrlPath,
513
- validTargets,
514
- literateJavaOutputPaths,
515
- });
505
+ const md = createCodeMarkdown(siteVariables);
516
506
  const lines = splitLines(sourceCode);
517
507
 
518
508
  // Group lines into segments
@@ -1,13 +1,13 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { parseFrontMatterAndContent } from './front-matter';
4
3
  import {
5
- getProcessedExtensions,
6
- extensionIsMarkdown,
7
- isLiterateJava,
8
- isPartial,
9
- } from './file-types';
10
- import { getPublicDir, normalizeOutputPath, toPosix } from './paths';
4
+ getProcessedExts,
5
+ getSourceOutputPaths,
6
+ getSourceTargetPaths,
7
+ isBuildContentSource,
8
+ shouldSkipContentFile,
9
+ } from '../source-model';
10
+ import { getPublicDir, toPosix } from './paths';
11
11
 
12
12
  function walkFiles(dir: string): string[] {
13
13
  return fs.readdirSync(dir).flatMap(file => {
@@ -31,23 +31,14 @@ export function getContentFiles(
31
31
  });
32
32
  }
33
33
 
34
- export function shouldSkipContentFile(filePath: string): boolean {
35
- const ext = path.extname(filePath).toLowerCase();
36
- if (!(extensionIsMarkdown(ext) || ext === '.html')) {
37
- return false;
38
- }
39
-
40
- const raw = fs.readFileSync(filePath, 'utf-8');
41
- const { pageVariables } = parseFrontMatterAndContent(raw, ext);
42
- return pageVariables?.skip === true;
43
- }
44
-
45
34
  export function getBuildContentFiles(
46
35
  contentDir: string,
47
36
  codeExtensions: string[],
48
37
  ): string[] {
49
- return getContentFiles(contentDir, codeExtensions).filter(
50
- filePath => !isPartial(filePath) && !shouldSkipContentFile(filePath),
38
+ const processedExts = getProcessedExts(codeExtensions);
39
+
40
+ return getContentFiles(contentDir, codeExtensions).filter(filePath =>
41
+ isBuildContentSource(filePath, processedExts),
51
42
  );
52
43
  }
53
44
 
@@ -56,47 +47,17 @@ export function getContentSourceOutputRelPaths(
56
47
  filePath: string,
57
48
  codeExtensions: string[],
58
49
  ): Set<string> {
59
- const processedExtSet = new Set(getProcessedExtensions(codeExtensions));
60
- const relPath = toPosix(path.relative(contentDir, filePath));
61
- const ext = path.extname(filePath).slice(1).toLowerCase();
62
- const parsed = path.parse(relPath);
63
- const outputs = new Set<string>();
64
-
65
- if (!processedExtSet.has(ext)) {
66
- outputs.add(relPath);
67
- return outputs;
68
- }
69
-
50
+ const processedExts = getProcessedExts(codeExtensions);
70
51
  const buildContentFiles = new Set(
71
52
  getBuildContentFiles(contentDir, codeExtensions),
72
53
  );
73
- if (!buildContentFiles.has(filePath)) {
74
- return outputs;
75
- }
76
-
77
- if (isLiterateJava(filePath)) {
78
- outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
79
- outputs.add(toPosix(path.join(parsed.dir, parsed.name)));
80
- return outputs;
81
- }
82
-
83
- if (
84
- extensionIsMarkdown(parsed.ext.toLowerCase()) ||
85
- parsed.ext.toLowerCase() === '.html'
86
- ) {
87
- outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
88
- return outputs;
89
- }
90
-
91
- const codeExtensionSet = new Set(
92
- codeExtensions.map(value => value.toLowerCase()),
93
- );
94
- if (codeExtensionSet.has(ext)) {
95
- outputs.add(relPath);
96
- outputs.add(`${relPath}.html`);
97
- }
98
54
 
99
- return outputs;
55
+ return getSourceOutputPaths({
56
+ contentDir,
57
+ filePath,
58
+ processedExts,
59
+ buildContent: buildContentFiles.has(filePath),
60
+ });
100
61
  }
101
62
 
102
63
  export function getContentOutputRelPaths(
@@ -104,37 +65,23 @@ export function getContentOutputRelPaths(
104
65
  codeExtensions: string[],
105
66
  ): Set<string> {
106
67
  const buildContentFiles = getBuildContentFiles(contentDir, codeExtensions);
107
- const codeExtensionSet = new Set(
108
- codeExtensions.map(ext => ext.toLowerCase()),
109
- );
68
+ const processedExts = getProcessedExts(codeExtensions);
110
69
  const outputs = new Set<string>();
111
70
 
112
71
  for (const filePath of buildContentFiles) {
113
- const relPath = toPosix(path.relative(contentDir, filePath));
114
- const parsed = path.parse(relPath);
115
- const ext = parsed.ext.toLowerCase();
116
-
117
- if (isLiterateJava(filePath)) {
118
- outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
119
- outputs.add(toPosix(path.join(parsed.dir, parsed.name)));
120
- continue;
121
- }
122
-
123
- if (extensionIsMarkdown(ext) || ext === '.html') {
124
- outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
125
- continue;
126
- }
127
-
128
- if (codeExtensionSet.has(ext.slice(1))) {
129
- outputs.add(relPath);
130
- outputs.add(`${relPath}.html`);
72
+ for (const outputPath of getSourceOutputPaths({
73
+ contentDir,
74
+ filePath,
75
+ processedExts,
76
+ buildContent: true,
77
+ })) {
78
+ outputs.add(outputPath);
131
79
  }
132
80
  }
133
81
 
134
- const processedExtSet = new Set(getProcessedExtensions(codeExtensions));
135
82
  for (const filePath of walkFiles(contentDir)) {
136
83
  const ext = path.extname(filePath).slice(1).toLowerCase();
137
- if (processedExtSet.has(ext)) {
84
+ if (processedExts.has(ext)) {
138
85
  continue;
139
86
  }
140
87
  outputs.add(toPosix(path.relative(contentDir, filePath)));
@@ -143,24 +90,6 @@ export function getContentOutputRelPaths(
143
90
  return outputs;
144
91
  }
145
92
 
146
- function addGeneratedRouteAliases(
147
- pathSet: Set<string>,
148
- outputPath: string,
149
- ): void {
150
- const normalizedPath = normalizeOutputPath(outputPath);
151
- pathSet.add(normalizedPath);
152
-
153
- if (!normalizedPath.endsWith('/index.html')) {
154
- return;
155
- }
156
-
157
- const base = normalizedPath.slice(0, -'index.html'.length);
158
- pathSet.add(base);
159
- if (base.endsWith('/') && base.length > 1) {
160
- pathSet.add(base.slice(0, -1));
161
- }
162
- }
163
-
164
93
  function getPublicFiles(publicDir: string): string[] {
165
94
  if (!fs.existsSync(publicDir)) {
166
95
  return [];
@@ -191,49 +120,51 @@ export function getValidInternalTargets(
191
120
  codeExtensions: string[],
192
121
  ): Set<string> {
193
122
  const targets = new Set<string>();
194
- const codeExtensionSet = new Set(
195
- codeExtensions.map(ext => ext.toLowerCase()),
196
- );
123
+ const processedExts = getProcessedExts(codeExtensions);
197
124
 
198
125
  for (const filePath of contentFiles) {
199
- if (isPartial(filePath)) {
200
- continue;
201
- }
202
- const relPath = path.relative(contentDir, filePath);
203
- const parsed = path.parse(relPath);
204
- const ext = parsed.ext.toLowerCase();
205
- const subPath = toPosix(path.join(parsed.dir, parsed.name));
206
-
207
- if (isLiterateJava(filePath)) {
208
- // parsed.name is e.g. "VowelCounter.java"; the output page is
209
- // VowelCounter.java.html and the source file is VowelCounter.java
210
- const javaSubPath = toPosix(path.join(parsed.dir, parsed.name));
211
- addGeneratedRouteAliases(targets, `/${javaSubPath}.html`);
212
- targets.add(normalizeOutputPath(`/${javaSubPath}`));
213
- } else if (extensionIsMarkdown(ext) || ext === '.html') {
214
- addGeneratedRouteAliases(targets, `/${subPath}.html`);
215
- } else if (codeExtensionSet.has(ext.slice(1))) {
216
- addGeneratedRouteAliases(targets, `/${relPath}.html`);
217
- targets.add(normalizeOutputPath(`/${relPath}`));
126
+ for (const targetPath of getSourceTargetPaths({
127
+ kind: 'content',
128
+ rootDir: contentDir,
129
+ filePath,
130
+ processedExts,
131
+ buildContent: true,
132
+ })) {
133
+ targets.add(targetPath);
218
134
  }
219
135
  }
220
136
 
221
137
  // Include non-processed assets in content/ that are copied directly to dist/.
222
- const processedExtSet = new Set(getProcessedExtensions(codeExtensions));
223
138
  for (const filePath of walkFiles(contentDir)) {
224
139
  const ext = path.extname(filePath).slice(1).toLowerCase();
225
- if (processedExtSet.has(ext)) {
140
+ if (processedExts.has(ext)) {
226
141
  continue;
227
142
  }
228
- const relPath = path.relative(contentDir, filePath);
229
- targets.add(normalizeOutputPath(`/${relPath}`));
143
+ for (const targetPath of getSourceTargetPaths({
144
+ kind: 'content',
145
+ rootDir: contentDir,
146
+ filePath,
147
+ processedExts,
148
+ buildContent: false,
149
+ })) {
150
+ targets.add(targetPath);
151
+ }
230
152
  }
231
153
 
232
154
  const publicDir = getPublicDir();
233
155
  for (const filePath of getPublicFiles(publicDir)) {
234
- const relPath = path.relative(publicDir, filePath);
235
- targets.add(normalizeOutputPath(`/${relPath}`));
156
+ for (const targetPath of getSourceTargetPaths({
157
+ kind: 'public',
158
+ rootDir: publicDir,
159
+ filePath,
160
+ processedExts,
161
+ buildContent: true,
162
+ })) {
163
+ targets.add(targetPath);
164
+ }
236
165
  }
237
166
 
238
167
  return targets;
239
168
  }
169
+
170
+ export { shouldSkipContentFile };