@abreen/tada 1.0.2 → 1.1.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 (120) hide show
  1. package/README.md +29 -33
  2. package/bin/tada.ts +356 -0
  3. package/bin/validators.test.ts +204 -0
  4. package/bin/validators.ts +83 -0
  5. package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +16 -7
  6. package/build/bundle.ts +117 -0
  7. package/{webpack/code.test.js → build/code.test.ts} +6 -7
  8. package/build/colors.ts +25 -0
  9. package/build/content-watch.ts +107 -0
  10. package/build/copy.ts +118 -0
  11. package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
  12. package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
  13. package/build/features.ts +11 -0
  14. package/build/generate-content-assets.ts +315 -0
  15. package/build/generate-favicon.ts +165 -0
  16. package/build/generate-fonts.ts +31 -0
  17. package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
  18. package/build/globals.test.ts +101 -0
  19. package/{webpack/globals.js → build/globals.ts} +28 -13
  20. package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
  21. package/build/json-schema.test.ts +57 -0
  22. package/build/json-schema.ts +33 -0
  23. package/build/log.test.ts +111 -0
  24. package/build/log.ts +167 -0
  25. package/{webpack/markdown-plugins.test.js → build/markdown-plugins.test.ts} +94 -9
  26. package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
  27. package/build/pagefind.ts +339 -0
  28. package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
  29. package/build/pipeline.ts +93 -0
  30. package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
  31. package/{webpack/reachability.js → build/reachability.ts} +77 -34
  32. package/build/serve.ts +112 -0
  33. package/{webpack/site-variables.js → build/site-variables.ts} +22 -15
  34. package/{webpack → build}/site.schema.json +3 -10
  35. package/{webpack/templates.js → build/templates.ts} +35 -33
  36. package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
  37. package/build/toc-plugin.test.ts +105 -0
  38. package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
  39. package/build/types.ts +172 -0
  40. package/build/util.ts +26 -0
  41. package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
  42. package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
  43. package/build/utils/derive-theme.test.ts +111 -0
  44. package/build/utils/derive-theme.ts +85 -0
  45. package/build/utils/file-types.test.ts +61 -0
  46. package/build/utils/file-types.ts +13 -0
  47. package/build/utils/front-matter.test.ts +80 -0
  48. package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
  49. package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
  50. package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +63 -34
  51. package/{webpack/utils/markdown.js → build/utils/markdown.ts} +94 -49
  52. package/build/utils/paths.test.ts +91 -0
  53. package/{webpack/utils/paths.js → build/utils/paths.ts} +14 -22
  54. package/{webpack/utils/render.js → build/utils/render.ts} +188 -123
  55. package/build/utils/shiki-highlighter.ts +29 -0
  56. package/build/validate-internal-links-plugin.test.ts +106 -0
  57. package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
  58. package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
  59. package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
  60. package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
  61. package/build/watch.ts +573 -0
  62. package/content/index.md +9 -3
  63. package/content/markdown.md +2 -1
  64. package/content/problem_sets/index.html +14 -0
  65. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
  66. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
  67. package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
  68. package/fonts/inter/woff2/InterVariable.woff2 +0 -0
  69. package/package.json +28 -19
  70. package/src/_alerts.scss +92 -0
  71. package/src/_base.scss +106 -0
  72. package/src/{layout.scss → _layout.scss} +0 -2
  73. package/src/anchor/style.scss +1 -9
  74. package/src/code/index.ts +3 -3
  75. package/src/code.scss +1 -1
  76. package/src/critical.scss +5 -0
  77. package/src/header/_base.scss +129 -0
  78. package/src/header/style.scss +3 -131
  79. package/src/index.ts +1 -2
  80. package/src/question/style.scss +1 -1
  81. package/src/search/index.ts +36 -15
  82. package/src/search/style.scss +9 -15
  83. package/src/style.scss +6 -269
  84. package/src/toc/style.scss +5 -39
  85. package/src/util.ts +8 -5
  86. package/templates/_theme.scss +38 -14
  87. package/tsconfig.json +10 -6
  88. package/types/file-system-access.d.ts +5 -0
  89. package/types/markdown-it-plugins.d.ts +11 -0
  90. package/types/untyped-modules.d.ts +40 -0
  91. package/bin/tada.js +0 -361
  92. package/content/problem_sets/index.md +0 -6
  93. package/webpack/build-state.js +0 -97
  94. package/webpack/colors.js +0 -15
  95. package/webpack/config.base.js +0 -151
  96. package/webpack/config.dev.js +0 -23
  97. package/webpack/config.prod.js +0 -32
  98. package/webpack/content-watch-plugin.js +0 -153
  99. package/webpack/features.js +0 -5
  100. package/webpack/generate-content-assets-plugin.js +0 -308
  101. package/webpack/generate-favicon-plugin.js +0 -198
  102. package/webpack/generate-fonts-plugin.js +0 -69
  103. package/webpack/json-schema.js +0 -19
  104. package/webpack/log.js +0 -143
  105. package/webpack/pagefind-plugin.js +0 -379
  106. package/webpack/print-flair-plugin.js +0 -22
  107. package/webpack/serve.js +0 -104
  108. package/webpack/util.js +0 -49
  109. package/webpack/utils/define-plugin.js +0 -20
  110. package/webpack/utils/file-types.js +0 -26
  111. package/webpack/utils/parse-hsl.js +0 -8
  112. package/webpack/utils/shiki-highlighter.js +0 -26
  113. package/webpack/watch.js +0 -166
  114. /package/{webpack → build}/flair.json +0 -0
  115. /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
  116. /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
  117. /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
  118. /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
  119. /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
  120. /package/types/{dev.ts → dev.d.ts} +0 -0
@@ -0,0 +1,83 @@
1
+ import type { SiteConfigInput, SiteVariables } from '../build/types.js';
2
+
3
+ export function validateSymbol(value: string): string | null {
4
+ if (!value) {
5
+ return 'Symbol is required';
6
+ }
7
+ if (value.length > 5) {
8
+ return 'Symbol must be 5 characters or fewer';
9
+ }
10
+ if (!/^[A-Z0-9\- ]{1,5}$/.test(value)) {
11
+ return 'Symbol must contain only uppercase letters, digits, hyphens, and spaces';
12
+ }
13
+ return null;
14
+ }
15
+
16
+ export function validateColor(value: string): string | null {
17
+ if (!value) {
18
+ return 'Color is required';
19
+ }
20
+ try {
21
+ if (!Bun.color(value)) {
22
+ throw new Error();
23
+ }
24
+ } catch {
25
+ return 'Must be a valid CSS color, e.g. "tomato", "#c04040", or "hsl(195 70% 40%)"';
26
+ }
27
+ return null;
28
+ }
29
+
30
+ export function validateHue(value: string): string | null {
31
+ if (!value) {
32
+ return 'Hue is required';
33
+ }
34
+ const n = Number(String(value).replace(/deg$/, ''));
35
+ if (!Number.isInteger(n) || n < 0 || n > 360) {
36
+ return 'Must be an integer from 0 to 360, with or without "deg"';
37
+ }
38
+ return null;
39
+ }
40
+
41
+ export function validateUrl(value: string): string | null {
42
+ if (!value) {
43
+ return 'URL is required';
44
+ }
45
+ if (!/^https?:\/\/[-.:a-zA-Z0-9]+$/.test(value)) {
46
+ return 'Must be a valid URL like https://example.edu (no trailing slash or path)';
47
+ }
48
+ return null;
49
+ }
50
+
51
+ export function validateBasePath(value: string): string | null {
52
+ if (!/^\/[-a-zA-Z0-9]*$/.test(value)) {
53
+ return 'Must start with / and contain only letters, digits, and hyphens';
54
+ }
55
+ return null;
56
+ }
57
+
58
+ export function createSiteConfig({
59
+ title,
60
+ symbol,
61
+ themeColor,
62
+ tintHue,
63
+ tintAmount,
64
+ defaultTimeZone,
65
+ base,
66
+ basePath,
67
+ internalDomains,
68
+ }: SiteConfigInput): SiteVariables {
69
+ return {
70
+ title,
71
+ symbol,
72
+ features: { search: true, code: true, favicon: true },
73
+ base,
74
+ basePath,
75
+ internalDomains,
76
+ defaultTimeZone,
77
+ codeLanguages: { java: 'java', py: 'python' },
78
+ themeColor,
79
+ tintHue: Number(tintHue),
80
+ tintAmount: Number(tintAmount),
81
+ vars: {},
82
+ };
83
+ }
@@ -1,13 +1,19 @@
1
- const { createApplyBasePath } = require('./utils/paths');
2
- const { makeLogger } = require('./log');
1
+ import type MarkdownIt from 'markdown-it';
2
+ import type Token from 'markdown-it/lib/token.mjs';
3
+ import { createApplyBasePath } from './utils/paths.js';
4
+ import { makeLogger } from './log.js';
5
+ import type { SiteVariables } from './types.js';
3
6
 
4
7
  const log = makeLogger(__filename);
5
8
 
6
- module.exports = function externalLinks(md, siteVariables, options = {}) {
9
+ export default function applyBasePathPlugin(
10
+ md: MarkdownIt,
11
+ siteVariables: SiteVariables,
12
+ ): void {
7
13
  const applyBasePath = createApplyBasePath(siteVariables);
8
14
  const rewriteCodeLinks = siteVariables.features?.code !== false;
9
15
 
10
- function rewriteInternalHref(href) {
16
+ function rewriteInternalHref(href: string): string {
11
17
  const match = href.match(/^([^?#]*)(.*)$/);
12
18
  const pathname = match ? match[1] : href;
13
19
  const suffix = match ? match[2] : '';
@@ -15,7 +21,7 @@ module.exports = function externalLinks(md, siteVariables, options = {}) {
15
21
 
16
22
  if (rewriteCodeLinks) {
17
23
  // Rewrite code file links to .html links
18
- for (const ext of Object.keys(siteVariables.codeLanguages)) {
24
+ for (const ext of Object.keys(siteVariables.codeLanguages ?? {})) {
19
25
  if (modifiedPath.endsWith(`.${ext}`)) {
20
26
  modifiedPath = modifiedPath.replace(
21
27
  new RegExp(`\\.${ext}$`),
@@ -29,9 +35,12 @@ module.exports = function externalLinks(md, siteVariables, options = {}) {
29
35
  return modifiedPath + suffix;
30
36
  }
31
37
 
32
- function checkAndApplyBasePath(token) {
38
+ function checkAndApplyBasePath(token: Token): void {
33
39
  if (token.type === 'link_open') {
34
40
  const href = token.attrGet('href');
41
+ if (!href) {
42
+ return;
43
+ }
35
44
 
36
45
  if (href.startsWith('/')) {
37
46
  const modifiedHref = rewriteInternalHref(href);
@@ -75,4 +84,4 @@ module.exports = function externalLinks(md, siteVariables, options = {}) {
75
84
  md.core.ruler.push('apply_base_path', state => {
76
85
  state.tokens.map(checkAndApplyBasePath);
77
86
  });
78
- };
87
+ }
@@ -0,0 +1,117 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import _ from 'lodash';
5
+ import * as sass from 'sass';
6
+ import { getPackageDir, getProjectDir, getDistDir } from './utils/paths.js';
7
+ import { deriveTheme } from './utils/derive-theme.js';
8
+ import type { PluginBuilder } from 'bun';
9
+ import type { SiteVariables } from './types.js';
10
+ import timezones from '../src/timezone/timezones.json' with { type: 'json' };
11
+
12
+ function renderThemeScss(siteVariables: SiteVariables): string {
13
+ const templatePath = path.join(getPackageDir(), 'templates/_theme.scss');
14
+ const template = fs.readFileSync(templatePath, 'utf-8');
15
+ const theme = deriveTheme(siteVariables.themeColor!);
16
+ const tintHue = siteVariables.tintHue ?? 20;
17
+ const tintAmount = siteVariables.tintAmount ?? 100;
18
+
19
+ const iconColor = `hsl(${tintHue}deg ${(8 * tintAmount) / 100}% 8%)`;
20
+ const iconColorHover = `hsl(${tintHue}deg ${(6 * tintAmount) / 100}% 60%)`;
21
+ const iconColorDark = `hsl(${tintHue}deg ${(20 * tintAmount) / 100}% 90%)`;
22
+ const iconColorHoverDark = `hsl(${tintHue}deg ${(6 * tintAmount) / 100}% 55%)`;
23
+ const iconColorTranslucentDark = `hsl(${tintHue}deg ${(85 * tintAmount) / 100}% 90%)`;
24
+
25
+ const rendered = _.template(template)({
26
+ ...theme,
27
+ tintHue,
28
+ tintAmount,
29
+ iconColor,
30
+ iconColorHover,
31
+ iconColorDark,
32
+ iconColorHoverDark,
33
+ iconColorTranslucentDark,
34
+ });
35
+
36
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
37
+ const configDir = path.join(tmpDir, 'config');
38
+ fs.mkdirSync(configDir);
39
+ fs.writeFileSync(path.join(configDir, '_theme.scss'), rendered);
40
+
41
+ return tmpDir;
42
+ }
43
+
44
+ function createDefine(
45
+ siteVariables: SiteVariables,
46
+ isDev = false,
47
+ ): Record<string, string> {
48
+ return {
49
+ 'window.siteVariables.base': JSON.stringify(siteVariables.base),
50
+ 'window.siteVariables.basePath': JSON.stringify(siteVariables.basePath),
51
+ 'window.siteVariables.titlePostfix': JSON.stringify(
52
+ siteVariables.titlePostfix,
53
+ ),
54
+ 'window.siteVariables.defaultTimeZone': JSON.stringify(
55
+ siteVariables.defaultTimeZone,
56
+ ),
57
+ 'window.siteVariables.timezones': JSON.stringify(timezones),
58
+ 'window.IS_DEV': JSON.stringify(isDev),
59
+ };
60
+ }
61
+
62
+ function createScssPlugin(siteVariables: SiteVariables) {
63
+ const themeDir = renderThemeScss(siteVariables);
64
+
65
+ return {
66
+ name: 'scss',
67
+ setup(build: PluginBuilder) {
68
+ build.onLoad({ filter: /\.scss$/ }, args => {
69
+ const result = sass.compile(args.path, {
70
+ loadPaths: [themeDir, getProjectDir()],
71
+ });
72
+ return { contents: result.css, loader: 'css' as const };
73
+ });
74
+ },
75
+ };
76
+ }
77
+
78
+ export async function bundle(
79
+ siteVariables: SiteVariables,
80
+ { mode = 'development' }: { mode?: string } = {},
81
+ ): Promise<string[]> {
82
+ const packageDir = getPackageDir();
83
+ const distDir = getDistDir();
84
+ const isDev = mode === 'development';
85
+
86
+ const entrypoints = [
87
+ path.resolve(packageDir, 'src/index.ts'),
88
+ path.resolve(packageDir, 'src/critical.scss'),
89
+ ];
90
+
91
+ const result = await Bun.build({
92
+ entrypoints,
93
+ outdir: distDir,
94
+ naming: '[name].bundle.[ext]',
95
+ minify: mode === 'production',
96
+ sourcemap: isDev ? 'inline' : 'none',
97
+ define: createDefine(siteVariables, isDev),
98
+ external: ['*.woff2'],
99
+ plugins: [createScssPlugin(siteVariables)],
100
+ });
101
+
102
+ if (!result.success) {
103
+ const messages = result.logs
104
+ .filter(log => log.level === 'error')
105
+ .map(log => log.message || String(log));
106
+ throw new Error(`Bundle failed:\n${messages.join('\n')}`);
107
+ }
108
+
109
+ // Return the output filenames for asset tag injection
110
+ const assetFiles = result.outputs.map(output =>
111
+ path.relative(distDir, output.path).split(path.sep).join(path.posix.sep),
112
+ );
113
+
114
+ return assetFiles;
115
+ }
116
+
117
+ export { renderThemeScss, createDefine };
@@ -1,9 +1,7 @@
1
- const { describe, expect, test, beforeAll } = require('bun:test');
2
- const { initHighlighter } = require('./utils/shiki-highlighter');
3
- const {
4
- renderCodeWithComments,
5
- extractJavaMethodToc,
6
- } = require('./utils/code');
1
+ import { describe, expect, test, beforeAll } from 'bun:test';
2
+ import { initHighlighter } from './utils/shiki-highlighter.js';
3
+ import { renderCodeWithComments, extractJavaMethodToc } from './utils/code.js';
4
+ import type { SiteVariables } from './types.js';
7
5
 
8
6
  beforeAll(async () => {
9
7
  await initHighlighter(['java', 'text', 'plaintext']);
@@ -148,9 +146,10 @@ public interface Greeter {
148
146
  describe('renderCodeWithComments', () => {
149
147
  test('renders build-time line rows for code segments', () => {
150
148
  const html = renderCodeWithComments('alpha\n\nbeta\n', 'java', {
149
+ base: '',
151
150
  basePath: '/',
152
151
  internalDomains: [],
153
- });
152
+ } as SiteVariables);
154
153
 
155
154
  expect(html).toContain('<span class="code-row">');
156
155
  expect(html).toContain('id="L1" href="#L1"');
@@ -0,0 +1,25 @@
1
+ import util from 'util';
2
+
3
+ export const R = (str: TemplateStringsArray, ...args: unknown[]): string =>
4
+ util.styleText(['red'], String.raw(str, ...args));
5
+ export const G = (str: TemplateStringsArray, ...args: unknown[]): string =>
6
+ util.styleText(['green'], String.raw(str, ...args));
7
+ export const B = (str: TemplateStringsArray, ...args: unknown[]): string =>
8
+ util.styleText(['blue'], String.raw(str, ...args));
9
+ export const Y = (str: TemplateStringsArray, ...args: unknown[]): string =>
10
+ util.styleText(['yellow'], String.raw(str, ...args));
11
+ export const L = (str: TemplateStringsArray, ...args: unknown[]): string =>
12
+ util.styleText(['blackBright'], String.raw(str, ...args));
13
+ export const P = (str: TemplateStringsArray, ...args: unknown[]): string =>
14
+ util.styleText(['magenta'], String.raw(str, ...args));
15
+ export const I = (str: TemplateStringsArray, ...args: unknown[]): string =>
16
+ util.styleText(['italic', 'bold'], String.raw(str, ...args));
17
+
18
+ export const Ri = (str: TemplateStringsArray, ...args: unknown[]): string =>
19
+ util.styleText(['inverse', 'red'], String.raw(str, ...args));
20
+ export const Gi = (str: TemplateStringsArray, ...args: unknown[]): string =>
21
+ util.styleText(['inverse', 'green'], String.raw(str, ...args));
22
+ export const Yi = (str: TemplateStringsArray, ...args: unknown[]): string =>
23
+ util.styleText(['inverse', 'yellow'], String.raw(str, ...args));
24
+ export const Li = (str: TemplateStringsArray, ...args: unknown[]): string =>
25
+ util.styleText(['inverse', 'blackBright'], String.raw(str, ...args));
@@ -0,0 +1,107 @@
1
+ import path from 'path';
2
+ import type { SiteVariables } from './types.js';
3
+ import { getContentDir, getBuildContentFiles } from './util.js';
4
+ import {
5
+ compileTemplates,
6
+ getHtmlTemplatesDir,
7
+ getJsonDataDir,
8
+ JSON_DATA_FILES,
9
+ } from './templates.js';
10
+ import { getProjectDir } from './utils/paths.js';
11
+ import { B } from './colors.js';
12
+ import { makeLogger } from './log.js';
13
+
14
+ const log = makeLogger(__filename);
15
+
16
+ interface ChangeDetectionResult {
17
+ templateError: Error | null;
18
+ needsRestart: boolean;
19
+ changedContentFiles?: Set<string>;
20
+ templatesChanged?: boolean;
21
+ }
22
+
23
+ export class ContentChangeDetector {
24
+ private siteVariables: SiteVariables;
25
+ private siteConfigPath: string;
26
+ private lastSig: string | null;
27
+
28
+ constructor(siteVariables: SiteVariables) {
29
+ this.siteVariables = siteVariables;
30
+ this.siteConfigPath = path.resolve(getProjectDir(), 'site.dev.json');
31
+ this.lastSig = null;
32
+ }
33
+
34
+ detectChanges(modifiedFiles: Iterable<string>): ChangeDetectionResult {
35
+ const resolvedFiles = new Set(
36
+ [...(modifiedFiles || [])].map(filePath => path.resolve(filePath)),
37
+ );
38
+
39
+ const htmlTemplatesDir = getHtmlTemplatesDir();
40
+ const jsonDataDir = getJsonDataDir();
41
+
42
+ // Try to recompile templates
43
+ let templateError: Error | null = null;
44
+ try {
45
+ compileTemplates(this.siteVariables);
46
+ } catch (err) {
47
+ templateError = err as Error;
48
+ }
49
+
50
+ if (templateError) {
51
+ return { templateError, needsRestart: false };
52
+ }
53
+
54
+ const contentDir = getContentDir();
55
+ const normalizedContentDir = path.resolve(contentDir) + path.sep;
56
+ const normalizedHtmlDir = path.resolve(htmlTemplatesDir) + path.sep;
57
+ const buildContentFiles = getBuildContentFiles(
58
+ contentDir,
59
+ Object.keys(this.siteVariables.codeLanguages || {}),
60
+ );
61
+
62
+ // Detect structural changes (files added or deleted)
63
+ const sig = buildContentFiles.slice().sort().join('\0');
64
+ let needsRestart = false;
65
+ if (this.lastSig !== null && sig !== this.lastSig) {
66
+ needsRestart = true;
67
+ }
68
+ this.lastSig = sig;
69
+
70
+ const changedContentFiles = new Set(
71
+ [...resolvedFiles].filter(filePath =>
72
+ filePath.startsWith(normalizedContentDir),
73
+ ),
74
+ );
75
+
76
+ // Check if any HTML template or JSON data file changed
77
+ const jsonDataPaths = JSON_DATA_FILES.map(f =>
78
+ path.resolve(jsonDataDir, f),
79
+ );
80
+ const changedTemplatePaths = [...resolvedFiles].filter(
81
+ filePath =>
82
+ filePath === path.resolve(htmlTemplatesDir) ||
83
+ filePath.startsWith(normalizedHtmlDir) ||
84
+ jsonDataPaths.includes(filePath),
85
+ );
86
+ const templatesChanged = changedTemplatePaths.length > 0;
87
+
88
+ for (const filePath of changedTemplatePaths) {
89
+ log.event`${B`${path.basename(filePath)}`} changed, rebuilding`;
90
+ }
91
+
92
+ // Check if site config changed
93
+ const siteConfigChanged = resolvedFiles.has(
94
+ path.resolve(this.siteConfigPath),
95
+ );
96
+ if (siteConfigChanged) {
97
+ needsRestart = true;
98
+ }
99
+
100
+ return {
101
+ templateError: null,
102
+ needsRestart,
103
+ changedContentFiles,
104
+ templatesChanged,
105
+ };
106
+ }
107
+ }
package/build/copy.ts ADDED
@@ -0,0 +1,118 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { makeLogger } from './log.js';
4
+ import { B } from './colors.js';
5
+
6
+ const log = makeLogger(__filename);
7
+
8
+ interface CollectedFile {
9
+ abs: string;
10
+ rel: string;
11
+ }
12
+
13
+ function collectFiles(dir: string): CollectedFile[] {
14
+ let entries: fs.Dirent[];
15
+ try {
16
+ entries = fs.readdirSync(dir, { withFileTypes: true, recursive: true });
17
+ } catch {
18
+ return [];
19
+ }
20
+ return entries
21
+ .filter(entry => entry.isFile())
22
+ .map(entry => {
23
+ const abs = path.join(entry.parentPath, entry.name);
24
+ const rel = path.relative(dir, abs);
25
+ return { abs, rel: rel.split(path.sep).join(path.posix.sep) };
26
+ });
27
+ }
28
+
29
+ export function copyPublicFiles(
30
+ publicDir: string,
31
+ distDir: string,
32
+ ): Set<string> {
33
+ const files = collectFiles(publicDir);
34
+ const publicRelPaths = new Set<string>();
35
+ for (const { abs, rel } of files) {
36
+ const dest = path.join(distDir, rel);
37
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
38
+ fs.copyFileSync(abs, dest);
39
+ publicRelPaths.add(rel);
40
+ log.info`Copying public file ${B`${rel}`}`;
41
+ }
42
+ return publicRelPaths;
43
+ }
44
+
45
+ export function copyContentAssets(
46
+ contentDir: string,
47
+ distDir: string,
48
+ processedExtensions: string[],
49
+ publicRelPaths: Set<string>,
50
+ ): Set<string> {
51
+ const processedExtSet = new Set(processedExtensions);
52
+ const files = collectFiles(contentDir);
53
+ const contentAssetRelPaths = new Set<string>();
54
+ const conflicts: string[] = [];
55
+ for (const { abs, rel } of files) {
56
+ const ext = path.extname(abs).slice(1).toLowerCase();
57
+ if (processedExtSet.has(ext)) {
58
+ continue;
59
+ }
60
+ contentAssetRelPaths.add(rel);
61
+ if (publicRelPaths && publicRelPaths.has(rel)) {
62
+ conflicts.push(rel);
63
+ }
64
+ const dest = path.join(distDir, rel);
65
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
66
+ fs.copyFileSync(abs, dest);
67
+ }
68
+ if (conflicts.length > 0) {
69
+ for (const rel of conflicts) {
70
+ log.error`content/${B`${rel}`} conflicts with public/${B`${rel}`}`;
71
+ }
72
+ const noun = conflicts.length === 1 ? 'file' : 'files';
73
+ throw new Error(
74
+ `${conflicts.length} ${noun} in content/ and public/ have the same path`,
75
+ );
76
+ }
77
+ return contentAssetRelPaths;
78
+ }
79
+
80
+ export function copyPublicFile(
81
+ publicDir: string,
82
+ distDir: string,
83
+ filePath: string,
84
+ contentAssetRelPaths?: Set<string>,
85
+ ): void {
86
+ const rel = path
87
+ .relative(publicDir, filePath)
88
+ .split(path.sep)
89
+ .join(path.posix.sep);
90
+ if (contentAssetRelPaths && contentAssetRelPaths.has(rel)) {
91
+ log.error`public/${B`${rel}`} conflicts with content/${B`${rel}`}`;
92
+ throw new Error(`public/${rel} and content/${rel} have the same path`);
93
+ }
94
+ const dest = path.join(distDir, rel);
95
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
96
+ fs.copyFileSync(filePath, dest);
97
+ log.info`Copying public file ${B`${rel}`}`;
98
+ }
99
+
100
+ export function copyContentFile(
101
+ contentDir: string,
102
+ distDir: string,
103
+ filePath: string,
104
+ publicRelPaths?: Set<string>,
105
+ ): void {
106
+ const rel = path
107
+ .relative(contentDir, filePath)
108
+ .split(path.sep)
109
+ .join(path.posix.sep);
110
+ if (publicRelPaths && publicRelPaths.has(rel)) {
111
+ log.error`content/${B`${rel}`} conflicts with public/${B`${rel}`}`;
112
+ throw new Error(`content/${rel} and public/${rel} have the same path`);
113
+ }
114
+ const dest = path.join(distDir, rel);
115
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
116
+ fs.copyFileSync(filePath, dest);
117
+ log.info`Copying content file ${B`${rel}`}`;
118
+ }
@@ -1,11 +1,12 @@
1
- const textToId = require('./text-to-id');
1
+ import type MarkdownIt from 'markdown-it';
2
+ import textToId from './text-to-id.js';
2
3
 
3
- module.exports = function deflistIdPlugin(md) {
4
+ export default function deflistIdPlugin(md: MarkdownIt): void {
4
5
  md.core.ruler.push('deflist_id_injector', function (state) {
5
6
  const tokens = state.tokens;
6
- const used = new Map();
7
+ const used = new Map<string, number>();
7
8
 
8
- function slugify(str) {
9
+ function slugify(str: string): string {
9
10
  let slug = textToId(str);
10
11
 
11
12
  if (!slug) {
@@ -13,7 +14,7 @@ module.exports = function deflistIdPlugin(md) {
13
14
  }
14
15
 
15
16
  if (used.has(slug)) {
16
- const n = used.get(slug) + 1;
17
+ const n = (used.get(slug) as number) + 1;
17
18
  used.set(slug, n);
18
19
  slug = `${slug}-${n}`;
19
20
  } else {
@@ -59,4 +60,4 @@ module.exports = function deflistIdPlugin(md) {
59
60
  i++;
60
61
  }
61
62
  });
62
- };
63
+ }
@@ -1,15 +1,24 @@
1
- const { makeLogger } = require('./log');
1
+ import type MarkdownIt from 'markdown-it';
2
+ import type Token from 'markdown-it/lib/token.mjs';
3
+ import { makeLogger } from './log.js';
4
+ import type { SiteVariables } from './types.js';
2
5
 
3
6
  const log = makeLogger(__filename);
4
7
 
5
- module.exports = function externalLinks(md, siteVariables) {
6
- function addClass(token) {
8
+ export default function externalLinks(
9
+ md: MarkdownIt,
10
+ siteVariables: SiteVariables,
11
+ ): void {
12
+ function addClass(token: Token): void {
7
13
  if (token.type === 'link_open') {
8
14
  const href = token.attrGet('href');
15
+ if (!href) {
16
+ return;
17
+ }
9
18
 
10
19
  if (href.match(/^https?:\/\/.*$/)) {
11
20
  const url = new URL(href);
12
- if (!siteVariables.internalDomains.includes(url.host)) {
21
+ if (!siteVariables.internalDomains?.includes(url.host)) {
13
22
  const classAttr = token.attrGet('class');
14
23
  let newClassAttr;
15
24
 
@@ -34,4 +43,4 @@ module.exports = function externalLinks(md, siteVariables) {
34
43
  md.core.ruler.push('external_links', state => {
35
44
  state.tokens.map(addClass);
36
45
  });
37
- };
46
+ }
@@ -0,0 +1,11 @@
1
+ import type { SiteVariables } from './types.js';
2
+
3
+ export function isFeatureEnabled(
4
+ siteVariables: SiteVariables,
5
+ featureName: string,
6
+ ): boolean {
7
+ return (
8
+ siteVariables.features?.[featureName as keyof SiteVariables['features']] !==
9
+ false
10
+ );
11
+ }