@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
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
2
|
+
import type { SiteVariables } from './types.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
interface Globals {
|
|
5
|
+
isHomePage: boolean;
|
|
6
|
+
isoDate: (str: string | null | undefined) => string | null;
|
|
7
|
+
readableDate: (date: string | Date | null | undefined) => string;
|
|
8
|
+
classNames: (obj: Record<string, unknown>) => string;
|
|
9
|
+
cx: (obj: Record<string, unknown>) => string;
|
|
10
|
+
timezoneChooser: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function createGlobals(
|
|
14
|
+
pageVariables: Record<string, unknown>,
|
|
15
|
+
siteVariables: SiteVariables,
|
|
16
|
+
subPath: string,
|
|
17
|
+
): Globals {
|
|
18
|
+
const defaultTz = timezones.find(
|
|
19
|
+
t => t.value === siteVariables.defaultTimeZone,
|
|
20
|
+
);
|
|
6
21
|
const timezoneChooser = defaultTz
|
|
7
22
|
? `<select class="time-zone" hidden></select><noscript>Times shown in ${defaultTz.abbreviation}.</noscript>`
|
|
8
23
|
: '<select class="time-zone" hidden></select>';
|
|
@@ -15,9 +30,9 @@ module.exports = function createGlobals(site, page, subPath) {
|
|
|
15
30
|
cx: classNames,
|
|
16
31
|
timezoneChooser,
|
|
17
32
|
};
|
|
18
|
-
}
|
|
33
|
+
}
|
|
19
34
|
|
|
20
|
-
function isoDate(str) {
|
|
35
|
+
function isoDate(str: string | null | undefined): string | null {
|
|
21
36
|
if (str == null || str == '') {
|
|
22
37
|
return null;
|
|
23
38
|
}
|
|
@@ -25,7 +40,7 @@ function isoDate(str) {
|
|
|
25
40
|
return date.toISOString().slice(0, 10);
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
function readableDate(date) {
|
|
43
|
+
function readableDate(date: string | Date | null | undefined): string {
|
|
29
44
|
if (date == null || date == '') {
|
|
30
45
|
return '';
|
|
31
46
|
}
|
|
@@ -36,16 +51,16 @@ function readableDate(date) {
|
|
|
36
51
|
|
|
37
52
|
const str = date.toISOString();
|
|
38
53
|
const year = str.slice(0, 4);
|
|
39
|
-
let month = str.slice(5, 7);
|
|
54
|
+
let month: string = str.slice(5, 7);
|
|
40
55
|
if (month[0] === '0') {
|
|
41
56
|
month = month[1];
|
|
42
57
|
}
|
|
43
|
-
let day = str.slice(8, 10);
|
|
58
|
+
let day: string = str.slice(8, 10);
|
|
44
59
|
if (day[0] === '0') {
|
|
45
60
|
day = day[1];
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
const months = {
|
|
63
|
+
const months: Record<string, string> = {
|
|
49
64
|
1: 'January',
|
|
50
65
|
2: 'February',
|
|
51
66
|
3: 'March',
|
|
@@ -63,10 +78,10 @@ function readableDate(date) {
|
|
|
63
78
|
return `${months[month]} ${day}, ${year}`;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
function classNames(obj) {
|
|
67
|
-
const names = [];
|
|
81
|
+
function classNames(obj: Record<string, unknown>): string {
|
|
82
|
+
const names: string[] = [];
|
|
68
83
|
for (const key in obj) {
|
|
69
|
-
if (
|
|
84
|
+
if (obj[key]) {
|
|
70
85
|
names.push(key);
|
|
71
86
|
}
|
|
72
87
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
|
|
3
|
+
export default function specialHeadingsPlugin(md: MarkdownIt): void {
|
|
2
4
|
md.core.ruler.push('special_headings', state => {
|
|
3
5
|
const tokens = state.tokens;
|
|
4
6
|
for (let i = 0; i < tokens.length; i++) {
|
|
@@ -77,4 +79,4 @@ module.exports = function specialHeadingsPlugin(md) {
|
|
|
77
79
|
tokens[i].attrJoin('class', 'has-subtitle');
|
|
78
80
|
}
|
|
79
81
|
});
|
|
80
|
-
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { compile, doValidation } from './json-schema.js';
|
|
3
|
+
|
|
4
|
+
const schema = {
|
|
5
|
+
type: 'object',
|
|
6
|
+
properties: { name: { type: 'string' }, age: { type: 'number' } },
|
|
7
|
+
required: ['name'],
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('compile', () => {
|
|
12
|
+
test('returns a validate function', () => {
|
|
13
|
+
const validator = compile(schema);
|
|
14
|
+
expect(typeof validator).toBe('function');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('doValidation', () => {
|
|
19
|
+
test('does not throw for valid input', () => {
|
|
20
|
+
const validator = compile(schema);
|
|
21
|
+
expect(() =>
|
|
22
|
+
doValidation(validator, { name: 'Alice' }, 'test.json'),
|
|
23
|
+
).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('does not throw when optional fields are present', () => {
|
|
27
|
+
const validator = compile(schema);
|
|
28
|
+
expect(() =>
|
|
29
|
+
doValidation(validator, { name: 'Alice', age: 30 }, 'test.json'),
|
|
30
|
+
).not.toThrow();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('throws when required field is missing', () => {
|
|
34
|
+
const validator = compile(schema);
|
|
35
|
+
expect(() => doValidation(validator, {}, 'test.json')).toThrow(
|
|
36
|
+
'test.json failed validation',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('throws with additionalProperties message', () => {
|
|
41
|
+
const validator = compile(schema);
|
|
42
|
+
expect(() =>
|
|
43
|
+
doValidation(validator, { name: 'Alice', extra: true }, 'test.json'),
|
|
44
|
+
).toThrow('unknown property "extra"');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('throws with field path in error', () => {
|
|
48
|
+
const nestedSchema = {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: { name: { type: 'string' } },
|
|
51
|
+
};
|
|
52
|
+
const validator = compile(nestedSchema);
|
|
53
|
+
expect(() => doValidation(validator, { name: 123 }, 'data.json')).toThrow(
|
|
54
|
+
'data.json failed validation',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import JsonSchemaCompiler from 'ajv';
|
|
2
|
+
import type { ValidateFunction, ErrorObject } from 'ajv';
|
|
3
|
+
|
|
4
|
+
const compiler = new JsonSchemaCompiler();
|
|
5
|
+
|
|
6
|
+
export function compile(schema: object): ValidateFunction {
|
|
7
|
+
return compiler.compile(schema);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatValidationError(error: ErrorObject): string {
|
|
11
|
+
const path = error.instancePath || '/';
|
|
12
|
+
const parts: string[] = [path];
|
|
13
|
+
if (error.keyword === 'additionalProperties') {
|
|
14
|
+
parts.push(
|
|
15
|
+
`unknown property "${(error.params as { additionalProperty: string }).additionalProperty}"`,
|
|
16
|
+
);
|
|
17
|
+
} else {
|
|
18
|
+
parts.push(error.message || 'unknown error');
|
|
19
|
+
}
|
|
20
|
+
return parts.join(': ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function doValidation(
|
|
24
|
+
validator: ValidateFunction,
|
|
25
|
+
input: unknown,
|
|
26
|
+
fileName: string,
|
|
27
|
+
): void {
|
|
28
|
+
const valid = validator(input);
|
|
29
|
+
if (!valid) {
|
|
30
|
+
const details = validator.errors!.map(formatValidationError).join('\n');
|
|
31
|
+
throw new Error(`${fileName} failed validation:\n${details}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { makeLogger, getFlair } from './log.js';
|
|
3
|
+
|
|
4
|
+
function captureOutput(fn: () => void): { stdout: string; stderr: string } {
|
|
5
|
+
let stdout = '';
|
|
6
|
+
let stderr = '';
|
|
7
|
+
const origStdout = process.stdout.write;
|
|
8
|
+
const origStderr = process.stderr.write;
|
|
9
|
+
process.stdout.write = ((chunk: string) => {
|
|
10
|
+
stdout += chunk;
|
|
11
|
+
return true;
|
|
12
|
+
}) as typeof process.stdout.write;
|
|
13
|
+
process.stderr.write = ((chunk: string) => {
|
|
14
|
+
stderr += chunk;
|
|
15
|
+
return true;
|
|
16
|
+
}) as typeof process.stderr.write;
|
|
17
|
+
try {
|
|
18
|
+
fn();
|
|
19
|
+
} finally {
|
|
20
|
+
process.stdout.write = origStdout;
|
|
21
|
+
process.stderr.write = origStderr;
|
|
22
|
+
}
|
|
23
|
+
return { stdout, stderr };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('makeLogger', () => {
|
|
27
|
+
test('creates a logger with default info level', () => {
|
|
28
|
+
const log = makeLogger('test');
|
|
29
|
+
expect(log.minLogLevel).toBe('info');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('throws on invalid log level', () => {
|
|
33
|
+
expect(() => makeLogger('test', 'verbose')).toThrow('Invalid log level');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('info writes to stdout', () => {
|
|
37
|
+
const log = makeLogger('test', 'info');
|
|
38
|
+
const { stdout } = captureOutput(() => log.info`hello world`);
|
|
39
|
+
expect(stdout).toContain('hello world');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('debug is suppressed at info level', () => {
|
|
43
|
+
const log = makeLogger('test', 'info');
|
|
44
|
+
const { stderr } = captureOutput(() => log.debug`should not appear`);
|
|
45
|
+
expect(stderr).toBe('');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('debug appears at debug level', () => {
|
|
49
|
+
const log = makeLogger('test', 'debug');
|
|
50
|
+
const { stderr } = captureOutput(() => log.debug`visible`);
|
|
51
|
+
expect(stderr).toContain('visible');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('warn writes to stdout', () => {
|
|
55
|
+
const log = makeLogger('test', 'warn');
|
|
56
|
+
const { stdout } = captureOutput(() => log.warn`caution`);
|
|
57
|
+
expect(stdout).toContain('caution');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('error writes to stdout', () => {
|
|
61
|
+
const log = makeLogger('test', 'error');
|
|
62
|
+
const { stdout } = captureOutput(() => log.error`failure`);
|
|
63
|
+
expect(stdout).toContain('failure');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('event always writes', () => {
|
|
67
|
+
const log = makeLogger('test', 'error');
|
|
68
|
+
const { stdout } = captureOutput(() => log.event`done`);
|
|
69
|
+
expect(stdout).toContain('done');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('followup writes strings', () => {
|
|
73
|
+
const log = makeLogger('test');
|
|
74
|
+
const { stdout } = captureOutput(() => log.followup(['line1', 'line2']));
|
|
75
|
+
expect(stdout).toContain('line1');
|
|
76
|
+
expect(stdout).toContain('line2');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('setMinLogLevel changes filtering', () => {
|
|
80
|
+
const log = makeLogger('test', 'info');
|
|
81
|
+
log.setMinLogLevel('error');
|
|
82
|
+
const { stdout } = captureOutput(() => log.info`suppressed`);
|
|
83
|
+
expect(stdout).toBe('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('handles empty name', () => {
|
|
87
|
+
const log = makeLogger('');
|
|
88
|
+
expect(log.minLogLevel).toBe('info');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('handles __filename-style name', () => {
|
|
92
|
+
const log = makeLogger('/path/to/module.ts');
|
|
93
|
+
const { stdout } = captureOutput(() => log.info`test`);
|
|
94
|
+
expect(stdout).toContain('test');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('interpolates objects in template', () => {
|
|
98
|
+
const log = makeLogger('test');
|
|
99
|
+
const obj = { key: 'value' };
|
|
100
|
+
const { stdout } = captureOutput(() => log.info`data: ${obj}`);
|
|
101
|
+
expect(stdout).toContain('key');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('getFlair', () => {
|
|
106
|
+
test('returns a non-empty string with emoji', () => {
|
|
107
|
+
const flair = getFlair();
|
|
108
|
+
expect(flair.length).toBeGreaterThan(0);
|
|
109
|
+
expect(flair).toContain('🎉');
|
|
110
|
+
});
|
|
111
|
+
});
|
package/build/log.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Gi, L, Ri, P, Yi, Li } from './colors.js';
|
|
4
|
+
import type { Logger } from './types.js';
|
|
5
|
+
import FLAIR_STRINGS from './flair.json' with { type: 'json' };
|
|
6
|
+
|
|
7
|
+
const LEVELS = ['debug', 'info', 'warn', 'error'] as const;
|
|
8
|
+
|
|
9
|
+
type LogLevel = (typeof LEVELS)[number];
|
|
10
|
+
|
|
11
|
+
const ENV_LOG_LEVEL = process.env.TADA_LOG_LEVEL;
|
|
12
|
+
|
|
13
|
+
if (ENV_LOG_LEVEL && !LEVELS.includes(ENV_LOG_LEVEL as LogLevel)) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Invalid TADA_LOG_LEVEL "${ENV_LOG_LEVEL}", must be one of: ${LEVELS.join(', ')}`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shouldLog(loggerLevel: string, level: string): boolean {
|
|
20
|
+
return (
|
|
21
|
+
LEVELS.indexOf(level as LogLevel) >= LEVELS.indexOf(loggerLevel as LogLevel)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateLevel(level: string): void {
|
|
26
|
+
if (!LEVELS.includes(level as LogLevel)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Invalid log level "${level}", must be one of: ${LEVELS.join(', ')}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function print(
|
|
34
|
+
strings: string[],
|
|
35
|
+
stream: 'stdout' | 'stderr' = 'stdout',
|
|
36
|
+
end: string = '\n',
|
|
37
|
+
): void {
|
|
38
|
+
for (const s of strings) {
|
|
39
|
+
process[stream].write(s);
|
|
40
|
+
}
|
|
41
|
+
process[stream].write(end);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function makeLogger(name: string, logLevel: string = 'info'): Logger {
|
|
45
|
+
validateLevel(logLevel);
|
|
46
|
+
|
|
47
|
+
if (ENV_LOG_LEVEL) {
|
|
48
|
+
logLevel = ENV_LOG_LEVEL;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!name) {
|
|
52
|
+
name = '';
|
|
53
|
+
} else {
|
|
54
|
+
// Allow for passing __filename
|
|
55
|
+
name = path.basename(name, path.extname(name));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const logger: Logger = {
|
|
59
|
+
minLogLevel: logLevel,
|
|
60
|
+
/** Don't log if the level is < minLogLevel */
|
|
61
|
+
setMinLogLevel(minLogLevel: string) {
|
|
62
|
+
this.minLogLevel = minLogLevel;
|
|
63
|
+
},
|
|
64
|
+
getArgs(
|
|
65
|
+
level: string,
|
|
66
|
+
strings: TemplateStringsArray | string | string[],
|
|
67
|
+
args: unknown[],
|
|
68
|
+
labelFn: (strings: TemplateStringsArray, ...args: unknown[]) => string,
|
|
69
|
+
): string[] {
|
|
70
|
+
const params: string[] = [];
|
|
71
|
+
params.push(
|
|
72
|
+
labelFn` ${level} ` + ' ' + (level === 'debug' ? name + ' ' : ''),
|
|
73
|
+
);
|
|
74
|
+
params.push(format(strings, ...args));
|
|
75
|
+
return params;
|
|
76
|
+
},
|
|
77
|
+
debug(strings: TemplateStringsArray, ...args: unknown[]) {
|
|
78
|
+
if (shouldLog(this.minLogLevel, 'debug')) {
|
|
79
|
+
print(this.getArgs('debug', strings, args, L), 'stderr');
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
info(strings: TemplateStringsArray, ...args: unknown[]) {
|
|
83
|
+
if (shouldLog(this.minLogLevel, 'info')) {
|
|
84
|
+
print(this.getArgs('info', strings, args, Li));
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
warn(strings: TemplateStringsArray, ...args: unknown[]) {
|
|
88
|
+
if (shouldLog(this.minLogLevel, 'warn')) {
|
|
89
|
+
print(this.getArgs('warn', strings, args, Yi));
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
error(strings: TemplateStringsArray, ...args: unknown[]) {
|
|
93
|
+
if (shouldLog(this.minLogLevel, 'error')) {
|
|
94
|
+
print(this.getArgs('error', strings, args, Ri));
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
event(strings: TemplateStringsArray, ...args: unknown[]) {
|
|
98
|
+
print(this.getArgs('event', strings, args, Gi));
|
|
99
|
+
},
|
|
100
|
+
followup(strings: string[]) {
|
|
101
|
+
print(strings);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
logger.setMinLogLevel(logLevel);
|
|
106
|
+
return logger;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function format(
|
|
110
|
+
strings: TemplateStringsArray | string | string[],
|
|
111
|
+
...args: unknown[]
|
|
112
|
+
): string {
|
|
113
|
+
// Called as template tag: first arg is an array-like with .raw
|
|
114
|
+
if (strings && typeof strings === 'object' && 'raw' in strings) {
|
|
115
|
+
try {
|
|
116
|
+
return String.raw(strings as TemplateStringsArray, ...args.map(toString));
|
|
117
|
+
} catch {
|
|
118
|
+
// fallback to safe join
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (Array.isArray(strings)) {
|
|
122
|
+
args.unshift(...strings);
|
|
123
|
+
} else {
|
|
124
|
+
args.unshift(strings);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return args.map(toString).join(' ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toString(item: unknown): string {
|
|
134
|
+
if (item === undefined) {
|
|
135
|
+
return 'undefined';
|
|
136
|
+
}
|
|
137
|
+
if (item === null) {
|
|
138
|
+
return 'null';
|
|
139
|
+
}
|
|
140
|
+
if (typeof item === 'string') {
|
|
141
|
+
return item;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
if (typeof item === 'object') {
|
|
146
|
+
return inspect(item, {
|
|
147
|
+
compact: true,
|
|
148
|
+
depth: 2,
|
|
149
|
+
breakLength: 80,
|
|
150
|
+
maxStringLength: 250,
|
|
151
|
+
colors: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
throw new Error('not an object');
|
|
155
|
+
} catch {
|
|
156
|
+
return String(item);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getFlair(): string {
|
|
161
|
+
const i = Math.floor(Math.random() * FLAIR_STRINGS.length);
|
|
162
|
+
return P`${FLAIR_STRINGS[i]}!` + ' 🎉';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function printFlair(): void {
|
|
166
|
+
console.log(getFlair());
|
|
167
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import MarkdownIt from 'markdown-it';
|
|
3
|
+
import deflist from 'markdown-it-deflist';
|
|
4
|
+
import applyBasePathPlugin from './apply-base-path-plugin.js';
|
|
5
|
+
import deflistIdPlugin from './deflist-id-plugin.js';
|
|
6
|
+
import externalLinksPlugin from './external-links-plugin.js';
|
|
7
|
+
import headingSubtitlePlugin from './heading-subtitle-plugin.js';
|
|
8
|
+
import { createMarkdown } from './utils/markdown.js';
|
|
9
|
+
import { stripHtmlComments } from './utils/render.js';
|
|
10
|
+
import type { SiteVariables } from './types.js';
|
|
9
11
|
|
|
10
12
|
describe('apply-base-path-plugin', () => {
|
|
11
13
|
test('rewrites internal links, images, and raw html image sources', () => {
|
|
@@ -115,7 +117,13 @@ describe('deflist-id-plugin', () => {
|
|
|
115
117
|
describe('custom markdown containers', () => {
|
|
116
118
|
function createProjectMarkdown() {
|
|
117
119
|
return createMarkdown(
|
|
118
|
-
{
|
|
120
|
+
{
|
|
121
|
+
base: '',
|
|
122
|
+
basePath: '/',
|
|
123
|
+
internalDomains: [],
|
|
124
|
+
codeLanguages: {},
|
|
125
|
+
features: {},
|
|
126
|
+
} as SiteVariables,
|
|
119
127
|
{ validatorOptions: { enabled: false } },
|
|
120
128
|
);
|
|
121
129
|
}
|
|
@@ -201,3 +209,80 @@ describe('custom markdown containers', () => {
|
|
|
201
209
|
expect(html).toContain('<p>Careful</p>');
|
|
202
210
|
});
|
|
203
211
|
});
|
|
212
|
+
|
|
213
|
+
describe('hidden_fence rule', () => {
|
|
214
|
+
function createProjectMarkdown() {
|
|
215
|
+
return createMarkdown(
|
|
216
|
+
{
|
|
217
|
+
base: '',
|
|
218
|
+
basePath: '/',
|
|
219
|
+
internalDomains: [],
|
|
220
|
+
codeLanguages: {},
|
|
221
|
+
features: {},
|
|
222
|
+
} as SiteVariables,
|
|
223
|
+
{ validatorOptions: { enabled: false } },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test('removes triple-hyphen comments containing a code fence', () => {
|
|
228
|
+
const md = createProjectMarkdown();
|
|
229
|
+
|
|
230
|
+
const html = md.render(
|
|
231
|
+
['<!---', '```', 'import java.util.*;', '```', '-->'].join('\n'),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(html).not.toContain('<!---');
|
|
235
|
+
expect(html).not.toContain('import java.util');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('does not remove triple-hyphen comments without a code fence', () => {
|
|
239
|
+
const md = createProjectMarkdown();
|
|
240
|
+
|
|
241
|
+
const html = md.render(
|
|
242
|
+
['<!---', 'This is a plain comment.', '-->'].join('\n'),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// markdown-it passes through html_block tokens unchanged;
|
|
246
|
+
// the hidden_fence rule only converts comments that contain fences
|
|
247
|
+
expect(html).toContain('<!---');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('preserves double-hyphen HTML comments', () => {
|
|
251
|
+
const md = createProjectMarkdown();
|
|
252
|
+
|
|
253
|
+
const html = md.render(['<!-- standard HTML comment -->'].join('\n'));
|
|
254
|
+
|
|
255
|
+
expect(html).toContain('<!-- standard HTML comment -->');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('stripHtmlComments', () => {
|
|
260
|
+
test('removes a single triple-hyphen comment', () => {
|
|
261
|
+
const result = stripHtmlComments(
|
|
262
|
+
'<p>Before</p>\n<!--- hidden comment -->\n<p>After</p>',
|
|
263
|
+
);
|
|
264
|
+
expect(result).toBe('<p>Before</p>\n\n<p>After</p>');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('removes multiple triple-hyphen comments', () => {
|
|
268
|
+
const result = stripHtmlComments('<!--- first -->\ntext\n<!--- second -->');
|
|
269
|
+
expect(result).toBe('\ntext\n');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('removes triple-hyphen comments spanning multiple lines', () => {
|
|
273
|
+
const result = stripHtmlComments(
|
|
274
|
+
'<p>Keep</p>\n<!---\nLine 1\nLine 2\n-->\n<p>Also keep</p>',
|
|
275
|
+
);
|
|
276
|
+
expect(result).toBe('<p>Keep</p>\n\n<p>Also keep</p>');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('does not remove double-hyphen HTML comments', () => {
|
|
280
|
+
const input = '<p>Before</p>\n<!-- keep this -->\n<p>After</p>';
|
|
281
|
+
expect(stripHtmlComments(input)).toBe(input);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('returns input unchanged when there are no comments', () => {
|
|
285
|
+
const input = '<p>Hello world</p>';
|
|
286
|
+
expect(stripHtmlComments(input)).toBe(input);
|
|
287
|
+
});
|
|
288
|
+
});
|