@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
@@ -1,8 +1,23 @@
1
- const timezones = require('../src/timezone/timezones.json');
1
+ import timezones from '../src/timezone/timezones.json' with { type: 'json' };
2
+ import type { SiteVariables } from './types.js';
2
3
 
3
- module.exports = function createGlobals(site, page, subPath) {
4
- const siteVars = page; // second arg receives siteVariables per render.js call convention
5
- const defaultTz = timezones.find(t => t.value === siteVars.defaultTimeZone);
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 (!!obj[key]) {
84
+ if (obj[key]) {
70
85
  names.push(key);
71
86
  }
72
87
  }
@@ -1,4 +1,6 @@
1
- module.exports = function specialHeadingsPlugin(md) {
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
- const { describe, expect, test } = require('bun:test');
2
- const MarkdownIt = require('markdown-it');
3
- const deflist = require('markdown-it-deflist');
4
- const applyBasePathPlugin = require('./apply-base-path-plugin');
5
- const deflistIdPlugin = require('./deflist-id-plugin');
6
- const externalLinksPlugin = require('./external-links-plugin');
7
- const headingSubtitlePlugin = require('./heading-subtitle-plugin');
8
- const { createMarkdown } = require('./utils/markdown');
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
- { basePath: '/', internalDomains: [], codeLanguages: {}, features: {} },
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
+ });