@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.
- package/README.md +29 -33
- package/bin/tada.ts +356 -0
- package/bin/validators.test.ts +204 -0
- package/bin/validators.ts +83 -0
- package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +16 -7
- package/build/bundle.ts +117 -0
- package/{webpack/code.test.js → build/code.test.ts} +6 -7
- package/build/colors.ts +25 -0
- package/build/content-watch.ts +107 -0
- package/build/copy.ts +118 -0
- package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
- package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
- package/build/features.ts +11 -0
- package/build/generate-content-assets.ts +315 -0
- package/build/generate-favicon.ts +165 -0
- package/build/generate-fonts.ts +31 -0
- package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
- package/build/globals.test.ts +101 -0
- package/{webpack/globals.js → build/globals.ts} +28 -13
- package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
- package/build/json-schema.test.ts +57 -0
- package/build/json-schema.ts +33 -0
- package/build/log.test.ts +111 -0
- package/build/log.ts +167 -0
- package/{webpack/markdown-plugins.test.js → build/markdown-plugins.test.ts} +94 -9
- package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
- package/build/pagefind.ts +339 -0
- package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
- package/build/pipeline.ts +93 -0
- package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
- package/{webpack/reachability.js → build/reachability.ts} +77 -34
- package/build/serve.ts +112 -0
- package/{webpack/site-variables.js → build/site-variables.ts} +22 -15
- package/{webpack → build}/site.schema.json +3 -10
- package/{webpack/templates.js → build/templates.ts} +35 -33
- package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
- package/build/toc-plugin.test.ts +105 -0
- package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
- package/build/types.ts +172 -0
- package/build/util.ts +26 -0
- package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
- package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
- package/build/utils/derive-theme.test.ts +111 -0
- package/build/utils/derive-theme.ts +85 -0
- package/build/utils/file-types.test.ts +61 -0
- package/build/utils/file-types.ts +13 -0
- package/build/utils/front-matter.test.ts +80 -0
- package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
- package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
- package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +63 -34
- package/{webpack/utils/markdown.js → build/utils/markdown.ts} +94 -49
- package/build/utils/paths.test.ts +91 -0
- package/{webpack/utils/paths.js → build/utils/paths.ts} +14 -22
- package/{webpack/utils/render.js → build/utils/render.ts} +188 -123
- package/build/utils/shiki-highlighter.ts +29 -0
- package/build/validate-internal-links-plugin.test.ts +106 -0
- package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
- package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
- package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
- package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
- package/build/watch.ts +573 -0
- package/content/index.md +9 -3
- package/content/markdown.md +2 -1
- package/content/problem_sets/index.html +14 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
- package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
- package/fonts/inter/woff2/InterVariable.woff2 +0 -0
- package/package.json +28 -19
- package/src/_alerts.scss +92 -0
- package/src/_base.scss +106 -0
- package/src/{layout.scss → _layout.scss} +0 -2
- package/src/anchor/style.scss +1 -9
- package/src/code/index.ts +3 -3
- package/src/code.scss +1 -1
- package/src/critical.scss +5 -0
- package/src/header/_base.scss +129 -0
- package/src/header/style.scss +3 -131
- package/src/index.ts +1 -2
- package/src/question/style.scss +1 -1
- package/src/search/index.ts +36 -15
- package/src/search/style.scss +9 -15
- package/src/style.scss +6 -269
- package/src/toc/style.scss +5 -39
- package/src/util.ts +8 -5
- package/templates/_theme.scss +38 -14
- package/tsconfig.json +10 -6
- package/types/file-system-access.d.ts +5 -0
- package/types/markdown-it-plugins.d.ts +11 -0
- package/types/untyped-modules.d.ts +40 -0
- package/bin/tada.js +0 -361
- package/content/problem_sets/index.md +0 -6
- package/webpack/build-state.js +0 -97
- package/webpack/colors.js +0 -15
- package/webpack/config.base.js +0 -151
- package/webpack/config.dev.js +0 -23
- package/webpack/config.prod.js +0 -32
- package/webpack/content-watch-plugin.js +0 -153
- package/webpack/features.js +0 -5
- package/webpack/generate-content-assets-plugin.js +0 -308
- package/webpack/generate-favicon-plugin.js +0 -198
- package/webpack/generate-fonts-plugin.js +0 -69
- package/webpack/json-schema.js +0 -19
- package/webpack/log.js +0 -143
- package/webpack/pagefind-plugin.js +0 -379
- package/webpack/print-flair-plugin.js +0 -22
- package/webpack/serve.js +0 -104
- package/webpack/util.js +0 -49
- package/webpack/utils/define-plugin.js +0 -20
- package/webpack/utils/file-types.js +0 -26
- package/webpack/utils/parse-hsl.js +0 -8
- package/webpack/utils/shiki-highlighter.js +0 -26
- package/webpack/watch.js +0 -166
- /package/{webpack → build}/flair.json +0 -0
- /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
- /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
- /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
- /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
- /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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/build/bundle.ts
ADDED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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"');
|
package/build/colors.ts
ADDED
|
@@ -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
|
-
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
import textToId from './text-to-id.js';
|
|
2
3
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
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
|
+
}
|