@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,20 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import MarkdownIt from 'markdown-it';
|
|
2
|
+
import { parse as parseJava } from 'java-parser';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
import { makeLogger } from '../log.js';
|
|
5
|
+
import { getHighlighter } from './shiki-highlighter.js';
|
|
6
|
+
import externalLinksPlugin from '../external-links-plugin.js';
|
|
7
|
+
import applyBasePathPlugin from '../apply-base-path-plugin.js';
|
|
8
|
+
import type { JavaTocEntry, SiteVariables } from '../types.js';
|
|
9
|
+
|
|
10
|
+
interface CstNode {
|
|
11
|
+
name?: string;
|
|
12
|
+
image?: string;
|
|
13
|
+
startLine?: number;
|
|
14
|
+
startOffset?: number;
|
|
15
|
+
children?: Record<string, CstNode[]>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface MethodMeta {
|
|
19
|
+
baseName: string;
|
|
20
|
+
line: number;
|
|
21
|
+
params: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FieldMeta {
|
|
25
|
+
name: string;
|
|
26
|
+
line: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CodeSegment {
|
|
30
|
+
type: string | null;
|
|
31
|
+
lines: string[];
|
|
32
|
+
startLine: number;
|
|
33
|
+
}
|
|
6
34
|
|
|
7
35
|
const log = makeLogger(__filename);
|
|
8
36
|
|
|
9
37
|
const PROSE_LINE = /^\s*\/\/\/(\s|$)/;
|
|
10
38
|
|
|
11
|
-
function createCodeMarkdown(
|
|
39
|
+
function createCodeMarkdown(
|
|
40
|
+
siteVariables: SiteVariables,
|
|
41
|
+
options: Record<string, unknown> = {},
|
|
42
|
+
): MarkdownIt {
|
|
12
43
|
return new MarkdownIt({ html: true, typographer: true })
|
|
13
|
-
.use(
|
|
14
|
-
.use(
|
|
44
|
+
.use(externalLinksPlugin, siteVariables)
|
|
45
|
+
.use(applyBasePathPlugin, siteVariables, options);
|
|
15
46
|
}
|
|
16
47
|
|
|
17
|
-
const KIND_LABELS = {
|
|
48
|
+
const KIND_LABELS: Record<string, string> = {
|
|
18
49
|
constructor: 'Constructor',
|
|
19
50
|
field: 'Field',
|
|
20
51
|
method: 'Method',
|
|
@@ -27,7 +58,10 @@ const JAVA_TYPE_DECLARATION_NODES = new Set([
|
|
|
27
58
|
'recordDeclaration',
|
|
28
59
|
]);
|
|
29
60
|
|
|
30
|
-
function extractJavaMethodMeta(
|
|
61
|
+
function extractJavaMethodMeta(
|
|
62
|
+
methodNode: CstNode,
|
|
63
|
+
requireBody = true,
|
|
64
|
+
): MethodMeta | null {
|
|
31
65
|
const methodBody = methodNode.children?.methodBody?.[0];
|
|
32
66
|
const hasBody = Boolean(methodBody?.children?.block?.length);
|
|
33
67
|
if (requireBody && !hasBody) {
|
|
@@ -50,7 +84,9 @@ function extractJavaMethodMeta(methodNode, requireBody = true) {
|
|
|
50
84
|
};
|
|
51
85
|
}
|
|
52
86
|
|
|
53
|
-
function extractJavaConstructorMeta(
|
|
87
|
+
function extractJavaConstructorMeta(
|
|
88
|
+
constructorNode: CstNode,
|
|
89
|
+
): MethodMeta | null {
|
|
54
90
|
const constructorDeclarator =
|
|
55
91
|
constructorNode.children?.constructorDeclarator?.[0];
|
|
56
92
|
const identifier =
|
|
@@ -69,7 +105,9 @@ function extractJavaConstructorMeta(constructorNode) {
|
|
|
69
105
|
};
|
|
70
106
|
}
|
|
71
107
|
|
|
72
|
-
function extractParameterNames(
|
|
108
|
+
function extractParameterNames(
|
|
109
|
+
formalParameterListNode: CstNode | undefined,
|
|
110
|
+
): string[] {
|
|
73
111
|
if (!formalParameterListNode) {
|
|
74
112
|
return [];
|
|
75
113
|
}
|
|
@@ -77,7 +115,7 @@ function extractParameterNames(formalParameterListNode) {
|
|
|
77
115
|
const formalParameters =
|
|
78
116
|
formalParameterListNode.children?.formalParameter || [];
|
|
79
117
|
return formalParameters
|
|
80
|
-
.map(parameterNode => {
|
|
118
|
+
.map((parameterNode: CstNode) => {
|
|
81
119
|
const regularParameter =
|
|
82
120
|
parameterNode.children?.variableParaRegularParameter?.[0];
|
|
83
121
|
if (regularParameter) {
|
|
@@ -92,16 +130,19 @@ function extractParameterNames(formalParameterListNode) {
|
|
|
92
130
|
parameterNode.children?.variableArityParameter?.[0];
|
|
93
131
|
return varArgParameter?.children?.Identifier?.[0]?.image || null;
|
|
94
132
|
})
|
|
95
|
-
.filter(Boolean);
|
|
133
|
+
.filter(Boolean) as string[];
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
function formatCallableName(
|
|
136
|
+
function formatCallableName(
|
|
137
|
+
baseName: string,
|
|
138
|
+
parameterNames: string[],
|
|
139
|
+
): string {
|
|
99
140
|
return `${baseName}(${parameterNames.join(', ')})`;
|
|
100
141
|
}
|
|
101
142
|
|
|
102
|
-
function collectTokensInOrder(node) {
|
|
103
|
-
const tokens = [];
|
|
104
|
-
function collect(n) {
|
|
143
|
+
function collectTokensInOrder(node: CstNode): CstNode[] {
|
|
144
|
+
const tokens: CstNode[] = [];
|
|
145
|
+
function collect(n: CstNode | undefined): void {
|
|
105
146
|
if (!n) {
|
|
106
147
|
return;
|
|
107
148
|
}
|
|
@@ -119,17 +160,17 @@ function collectTokensInOrder(node) {
|
|
|
119
160
|
}
|
|
120
161
|
}
|
|
121
162
|
collect(node);
|
|
122
|
-
tokens.sort((a, b) => a.startOffset - b.startOffset);
|
|
163
|
+
tokens.sort((a, b) => (a.startOffset ?? 0) - (b.startOffset ?? 0));
|
|
123
164
|
return tokens;
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
function buildTypeString(unannTypeNode) {
|
|
167
|
+
function buildTypeString(unannTypeNode: CstNode): string {
|
|
127
168
|
return collectTokensInOrder(unannTypeNode)
|
|
128
169
|
.map(t => (t.image === ',' ? ', ' : t.image))
|
|
129
170
|
.join('');
|
|
130
171
|
}
|
|
131
172
|
|
|
132
|
-
function extractJavaFieldMetas(fieldNode) {
|
|
173
|
+
function extractJavaFieldMetas(fieldNode: CstNode): FieldMeta[] {
|
|
133
174
|
const unannType = fieldNode.children?.unannType?.[0];
|
|
134
175
|
if (!unannType) {
|
|
135
176
|
return [];
|
|
@@ -142,7 +183,7 @@ function extractJavaFieldMetas(fieldNode) {
|
|
|
142
183
|
return [];
|
|
143
184
|
}
|
|
144
185
|
|
|
145
|
-
const results = [];
|
|
186
|
+
const results: FieldMeta[] = [];
|
|
146
187
|
for (const declarator of variableDeclaratorList.children
|
|
147
188
|
?.variableDeclarator || []) {
|
|
148
189
|
const declaratorId = declarator.children?.variableDeclaratorId?.[0];
|
|
@@ -151,7 +192,7 @@ function extractJavaFieldMetas(fieldNode) {
|
|
|
151
192
|
continue;
|
|
152
193
|
}
|
|
153
194
|
|
|
154
|
-
const dimsNode = declaratorId
|
|
195
|
+
const dimsNode = declaratorId?.children?.dims?.[0];
|
|
155
196
|
const dimsStr = dimsNode
|
|
156
197
|
? collectTokensInOrder(dimsNode)
|
|
157
198
|
.map(t => t.image)
|
|
@@ -166,18 +207,24 @@ function extractJavaFieldMetas(fieldNode) {
|
|
|
166
207
|
return results;
|
|
167
208
|
}
|
|
168
209
|
|
|
169
|
-
function extractJavaMethodToc(sourceCode) {
|
|
170
|
-
let cst;
|
|
210
|
+
export function extractJavaMethodToc(sourceCode: string): JavaTocEntry[] {
|
|
211
|
+
let cst: CstNode;
|
|
171
212
|
try {
|
|
172
|
-
cst = parseJava(sourceCode);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
log.error`Failed to parse Java source for TOC: ${err.message}`;
|
|
213
|
+
cst = parseJava(sourceCode) as CstNode;
|
|
214
|
+
} catch (err: unknown) {
|
|
215
|
+
log.error`Failed to parse Java source for TOC: ${(err as Error).message}`;
|
|
175
216
|
return [];
|
|
176
217
|
}
|
|
177
218
|
|
|
178
|
-
const callables
|
|
219
|
+
const callables: Array<{
|
|
220
|
+
kind: 'method' | 'constructor' | 'field';
|
|
221
|
+
baseName?: string;
|
|
222
|
+
name?: string;
|
|
223
|
+
line: number;
|
|
224
|
+
params?: string[];
|
|
225
|
+
}> = [];
|
|
179
226
|
|
|
180
|
-
function visit(node, typeDepth) {
|
|
227
|
+
function visit(node: CstNode, typeDepth: number): void {
|
|
181
228
|
if (!node || !node.name) {
|
|
182
229
|
return;
|
|
183
230
|
}
|
|
@@ -225,36 +272,44 @@ function extractJavaMethodToc(sourceCode) {
|
|
|
225
272
|
return callables.map(callable => {
|
|
226
273
|
const label = KIND_LABELS[callable.kind] ?? 'Member';
|
|
227
274
|
if (callable.name !== undefined) {
|
|
228
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
kind: callable.kind,
|
|
277
|
+
label,
|
|
278
|
+
name: callable.name,
|
|
279
|
+
line: callable.line,
|
|
280
|
+
};
|
|
229
281
|
}
|
|
230
282
|
return {
|
|
231
283
|
kind: callable.kind,
|
|
232
284
|
label,
|
|
233
|
-
name: formatCallableName(callable.baseName
|
|
285
|
+
name: formatCallableName(callable.baseName!, callable.params!),
|
|
234
286
|
line: callable.line,
|
|
235
287
|
};
|
|
236
288
|
});
|
|
237
289
|
}
|
|
238
290
|
|
|
239
|
-
function escapeAttr(str) {
|
|
291
|
+
function escapeAttr(str: string): string {
|
|
240
292
|
return str.replace(/&/g, '&').replace(/"/g, '"');
|
|
241
293
|
}
|
|
242
294
|
|
|
243
|
-
function escapeHtml(text) {
|
|
295
|
+
function escapeHtml(text: string): string {
|
|
244
296
|
return text
|
|
245
297
|
.replace(/&/g, '&')
|
|
246
298
|
.replace(/</g, '<')
|
|
247
299
|
.replace(/>/g, '>');
|
|
248
300
|
}
|
|
249
301
|
|
|
250
|
-
function createCodeLine(document) {
|
|
302
|
+
function createCodeLine(document: Document): HTMLSpanElement {
|
|
251
303
|
const line = document.createElement('span');
|
|
252
304
|
line.className = 'code-line';
|
|
253
305
|
return line;
|
|
254
306
|
}
|
|
255
307
|
|
|
256
|
-
function cloneOpenElements(
|
|
257
|
-
|
|
308
|
+
function cloneOpenElements(
|
|
309
|
+
openElements: Node[],
|
|
310
|
+
line: HTMLSpanElement,
|
|
311
|
+
): Node[] {
|
|
312
|
+
const containers: Node[] = [line];
|
|
258
313
|
|
|
259
314
|
for (const openElement of openElements) {
|
|
260
315
|
const clone = openElement.cloneNode(false);
|
|
@@ -265,17 +320,20 @@ function cloneOpenElements(openElements, line) {
|
|
|
265
320
|
return containers;
|
|
266
321
|
}
|
|
267
322
|
|
|
268
|
-
function splitHighlightedHtmlIntoLines(
|
|
323
|
+
function splitHighlightedHtmlIntoLines(
|
|
324
|
+
highlightedHtml: string,
|
|
325
|
+
lineCount: number,
|
|
326
|
+
): string[] {
|
|
269
327
|
const fragment = JSDOM.fragment(`<code>${highlightedHtml}</code>`);
|
|
270
|
-
const codeEl = fragment.firstChild;
|
|
328
|
+
const codeEl = fragment.firstChild as HTMLElement;
|
|
271
329
|
const document = codeEl.ownerDocument;
|
|
272
|
-
const lines = [];
|
|
273
|
-
const openElements = [];
|
|
330
|
+
const lines: HTMLSpanElement[] = [];
|
|
331
|
+
const openElements: Node[] = [];
|
|
274
332
|
let currentLine = createCodeLine(document);
|
|
275
|
-
let currentContainers = [currentLine];
|
|
333
|
+
let currentContainers: Node[] = [currentLine];
|
|
276
334
|
let currentLineHasContent = false;
|
|
277
335
|
|
|
278
|
-
function finishCurrentLine() {
|
|
336
|
+
function finishCurrentLine(): void {
|
|
279
337
|
if (!currentLineHasContent) {
|
|
280
338
|
currentContainers[currentContainers.length - 1].appendChild(
|
|
281
339
|
document.createTextNode('\u00A0'),
|
|
@@ -287,7 +345,7 @@ function splitHighlightedHtmlIntoLines(highlightedHtml, lineCount) {
|
|
|
287
345
|
currentLineHasContent = false;
|
|
288
346
|
}
|
|
289
347
|
|
|
290
|
-
function visit(node) {
|
|
348
|
+
function visit(node: Node): void {
|
|
291
349
|
if (node.nodeType === 3) {
|
|
292
350
|
const parts = (node.textContent || '').split('\n');
|
|
293
351
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -341,9 +399,13 @@ function splitHighlightedHtmlIntoLines(highlightedHtml, lineCount) {
|
|
|
341
399
|
return lines.map(line => line.innerHTML);
|
|
342
400
|
}
|
|
343
401
|
|
|
344
|
-
function renderCodeSegment(
|
|
402
|
+
export function renderCodeSegment(
|
|
403
|
+
lines: string[],
|
|
404
|
+
startLine: number,
|
|
405
|
+
lang: string,
|
|
406
|
+
): string {
|
|
345
407
|
const source = lines.join('\n');
|
|
346
|
-
let lineHtml;
|
|
408
|
+
let lineHtml: string[] | undefined;
|
|
347
409
|
|
|
348
410
|
try {
|
|
349
411
|
const highlighter = getHighlighter();
|
|
@@ -353,10 +415,10 @@ function renderCodeSegment(lines, startLine, lang) {
|
|
|
353
415
|
defaultColor: false,
|
|
354
416
|
});
|
|
355
417
|
const fragment = JSDOM.fragment(html);
|
|
356
|
-
const inner = fragment.querySelector('code').innerHTML;
|
|
418
|
+
const inner = (fragment.querySelector('code') as HTMLElement).innerHTML;
|
|
357
419
|
lineHtml = splitHighlightedHtmlIntoLines(inner, lines.length);
|
|
358
|
-
} catch (err) {
|
|
359
|
-
log.error`Failed to highlight code block: ${err.message}`;
|
|
420
|
+
} catch (err: unknown) {
|
|
421
|
+
log.error`Failed to highlight code block: ${(err as Error).message}`;
|
|
360
422
|
}
|
|
361
423
|
|
|
362
424
|
if (!lineHtml) {
|
|
@@ -371,7 +433,11 @@ function renderCodeSegment(lines, startLine, lang) {
|
|
|
371
433
|
return `<pre>${rows.join('')}</pre>`;
|
|
372
434
|
}
|
|
373
435
|
|
|
374
|
-
function renderCodeWithComments(
|
|
436
|
+
export function renderCodeWithComments(
|
|
437
|
+
sourceCode: string,
|
|
438
|
+
lang: string,
|
|
439
|
+
siteVariables: SiteVariables,
|
|
440
|
+
): string {
|
|
375
441
|
const md = createCodeMarkdown(siteVariables);
|
|
376
442
|
const lines = sourceCode.split('\n');
|
|
377
443
|
if (lines[lines.length - 1] === '') {
|
|
@@ -379,9 +445,9 @@ function renderCodeWithComments(sourceCode, lang, siteVariables) {
|
|
|
379
445
|
}
|
|
380
446
|
|
|
381
447
|
// Group lines into segments
|
|
382
|
-
const segments = [];
|
|
383
|
-
let currentType = null;
|
|
384
|
-
let currentLines = [];
|
|
448
|
+
const segments: CodeSegment[] = [];
|
|
449
|
+
let currentType: string | null = null;
|
|
450
|
+
let currentLines: string[] = [];
|
|
385
451
|
let currentStart = 1;
|
|
386
452
|
|
|
387
453
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -430,10 +496,3 @@ function renderCodeWithComments(sourceCode, lang, siteVariables) {
|
|
|
430
496
|
})
|
|
431
497
|
.join('');
|
|
432
498
|
}
|
|
433
|
-
|
|
434
|
-
module.exports = {
|
|
435
|
-
createCodeMarkdown,
|
|
436
|
-
extractJavaMethodToc,
|
|
437
|
-
renderCodeSegment,
|
|
438
|
-
renderCodeWithComments,
|
|
439
|
-
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parseFrontMatterAndContent } from './front-matter.js';
|
|
4
|
+
import {
|
|
5
|
+
getProcessedExtensions,
|
|
6
6
|
extensionIsMarkdown,
|
|
7
7
|
isLiterateJava,
|
|
8
|
-
}
|
|
9
|
-
|
|
8
|
+
} from './file-types.js';
|
|
9
|
+
import { getPublicDir, normalizeOutputPath } from './paths.js';
|
|
10
10
|
|
|
11
|
-
function walkFiles(dir) {
|
|
11
|
+
function walkFiles(dir: string): string[] {
|
|
12
12
|
return fs.readdirSync(dir).flatMap(file => {
|
|
13
13
|
const fullPath = path.join(dir, file);
|
|
14
14
|
if (fs.statSync(fullPath).isDirectory()) {
|
|
@@ -18,8 +18,11 @@ function walkFiles(dir) {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function getContentFiles(
|
|
22
|
-
|
|
21
|
+
export function getContentFiles(
|
|
22
|
+
contentDir: string,
|
|
23
|
+
codeExtensions: string[],
|
|
24
|
+
): string[] {
|
|
25
|
+
const extensions = ['md', 'html', ...codeExtensions];
|
|
23
26
|
const pattern = new RegExp(`\\.(${extensions.join('|')})$`);
|
|
24
27
|
|
|
25
28
|
return walkFiles(contentDir).filter(filePath => {
|
|
@@ -27,7 +30,7 @@ function getContentFiles(contentDir, codeExtensions) {
|
|
|
27
30
|
});
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
function shouldSkipContentFile(filePath) {
|
|
33
|
+
export function shouldSkipContentFile(filePath: string): boolean {
|
|
31
34
|
const ext = path.extname(filePath).toLowerCase();
|
|
32
35
|
if (!(extensionIsMarkdown(ext) || ext === '.html')) {
|
|
33
36
|
return false;
|
|
@@ -38,13 +41,19 @@ function shouldSkipContentFile(filePath) {
|
|
|
38
41
|
return pageVariables?.skip === true;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
function getBuildContentFiles(
|
|
44
|
+
export function getBuildContentFiles(
|
|
45
|
+
contentDir: string,
|
|
46
|
+
codeExtensions: string[],
|
|
47
|
+
): string[] {
|
|
42
48
|
return getContentFiles(contentDir, codeExtensions).filter(
|
|
43
49
|
filePath => !shouldSkipContentFile(filePath),
|
|
44
50
|
);
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
function addGeneratedRouteAliases(
|
|
53
|
+
function addGeneratedRouteAliases(
|
|
54
|
+
pathSet: Set<string>,
|
|
55
|
+
outputPath: string,
|
|
56
|
+
): void {
|
|
48
57
|
const normalizedPath = normalizeOutputPath(outputPath);
|
|
49
58
|
pathSet.add(normalizedPath);
|
|
50
59
|
|
|
@@ -59,7 +68,7 @@ function addGeneratedRouteAliases(pathSet, outputPath) {
|
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
function getPublicFiles(publicDir) {
|
|
71
|
+
function getPublicFiles(publicDir: string): string[] {
|
|
63
72
|
if (!fs.existsSync(publicDir)) {
|
|
64
73
|
return [];
|
|
65
74
|
}
|
|
@@ -67,7 +76,10 @@ function getPublicFiles(publicDir) {
|
|
|
67
76
|
return walkFiles(publicDir);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
function getFilesByExtensions(
|
|
79
|
+
export function getFilesByExtensions(
|
|
80
|
+
rootDir: string,
|
|
81
|
+
extensions: string[],
|
|
82
|
+
): string[] {
|
|
71
83
|
if (!fs.existsSync(rootDir)) {
|
|
72
84
|
return [];
|
|
73
85
|
}
|
|
@@ -80,12 +92,15 @@ function getFilesByExtensions(rootDir, extensions) {
|
|
|
80
92
|
});
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
function getValidInternalTargets(
|
|
84
|
-
|
|
95
|
+
export function getValidInternalTargets(
|
|
96
|
+
contentDir: string,
|
|
97
|
+
contentFiles: string[],
|
|
98
|
+
codeExtensions: string[],
|
|
99
|
+
): Set<string> {
|
|
100
|
+
const targets = new Set<string>();
|
|
85
101
|
const codeExtensionSet = new Set(
|
|
86
102
|
codeExtensions.map(ext => ext.toLowerCase()),
|
|
87
103
|
);
|
|
88
|
-
const contentAssetExtensionSet = new Set(PUBLIC_ASSET_EXTENSIONS);
|
|
89
104
|
|
|
90
105
|
for (const filePath of contentFiles) {
|
|
91
106
|
const relPath = path.relative(contentDir, filePath);
|
|
@@ -110,21 +125,19 @@ function getValidInternalTargets(contentDir, contentFiles, codeExtensions) {
|
|
|
110
125
|
);
|
|
111
126
|
} else if (extensionIsMarkdown(ext) || ext === '.html') {
|
|
112
127
|
addGeneratedRouteAliases(targets, `/${subPath}.html`);
|
|
113
|
-
} else if (ext === '.pdf') {
|
|
114
|
-
targets.add(normalizeOutputPath(`/${relPath}`));
|
|
115
128
|
} else if (codeExtensionSet.has(ext.slice(1))) {
|
|
116
129
|
addGeneratedRouteAliases(targets, `/${subPath}.html`);
|
|
117
130
|
targets.add(normalizeOutputPath(`/${relPath}`));
|
|
118
|
-
} else if (contentAssetExtensionSet.has(ext.slice(1))) {
|
|
119
|
-
targets.add(normalizeOutputPath(`/${relPath}`));
|
|
120
131
|
}
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
// Include non-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
// Include non-processed assets in content/ that are copied directly to dist/.
|
|
135
|
+
const processedExtSet = new Set(getProcessedExtensions(codeExtensions));
|
|
136
|
+
for (const filePath of walkFiles(contentDir)) {
|
|
137
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
138
|
+
if (processedExtSet.has(ext)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
128
141
|
const relPath = path.relative(contentDir, filePath);
|
|
129
142
|
targets.add(normalizeOutputPath(`/${relPath}`));
|
|
130
143
|
}
|
|
@@ -137,11 +150,3 @@ function getValidInternalTargets(contentDir, contentFiles, codeExtensions) {
|
|
|
137
150
|
|
|
138
151
|
return targets;
|
|
139
152
|
}
|
|
140
|
-
|
|
141
|
-
module.exports = {
|
|
142
|
-
extensionIsMarkdown,
|
|
143
|
-
getBuildContentFiles,
|
|
144
|
-
getContentFiles,
|
|
145
|
-
getValidInternalTargets,
|
|
146
|
-
shouldSkipContentFile,
|
|
147
|
-
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { parse, oklch } from 'culori';
|
|
3
|
+
import { deriveTheme, getTextOnColor } from './derive-theme.js';
|
|
4
|
+
|
|
5
|
+
function getL(hex: string): number {
|
|
6
|
+
return oklch(parse(hex))!.l;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('deriveTheme', () => {
|
|
10
|
+
test('returns valid hex strings for all outputs', () => {
|
|
11
|
+
const result = deriveTheme('steelblue');
|
|
12
|
+
for (const value of Object.values(result)) {
|
|
13
|
+
expect(value).toMatch(/^#[0-9a-f]{3,6}$/);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('light theme color lightness is in range', () => {
|
|
18
|
+
for (const color of [
|
|
19
|
+
'cornsilk',
|
|
20
|
+
'navy',
|
|
21
|
+
'steelblue',
|
|
22
|
+
'tomato',
|
|
23
|
+
'#000',
|
|
24
|
+
'#fff',
|
|
25
|
+
]) {
|
|
26
|
+
const { themeColorLight } = deriveTheme(color);
|
|
27
|
+
const l = getL(themeColorLight);
|
|
28
|
+
expect(l).toBeGreaterThanOrEqual(0.34);
|
|
29
|
+
expect(l).toBeLessThanOrEqual(0.63);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('dark theme color lightness is in range', () => {
|
|
34
|
+
for (const color of [
|
|
35
|
+
'cornsilk',
|
|
36
|
+
'navy',
|
|
37
|
+
'steelblue',
|
|
38
|
+
'tomato',
|
|
39
|
+
'#000',
|
|
40
|
+
'#fff',
|
|
41
|
+
]) {
|
|
42
|
+
const { themeColorDark } = deriveTheme(color);
|
|
43
|
+
const l = getL(themeColorDark);
|
|
44
|
+
expect(l).toBeGreaterThanOrEqual(0.54);
|
|
45
|
+
expect(l).toBeLessThanOrEqual(0.81);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('light text color lightness is in range', () => {
|
|
50
|
+
for (const color of ['cornsilk', 'navy', 'steelblue', 'tomato', 'lime']) {
|
|
51
|
+
const { themeColorTextLight } = deriveTheme(color);
|
|
52
|
+
const l = getL(themeColorTextLight);
|
|
53
|
+
expect(l).toBeGreaterThanOrEqual(0.34);
|
|
54
|
+
expect(l).toBeLessThanOrEqual(0.51);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('dark text color lightness is in range', () => {
|
|
59
|
+
for (const color of ['cornsilk', 'navy', 'steelblue', 'tomato', 'lime']) {
|
|
60
|
+
const { themeColorTextDark } = deriveTheme(color);
|
|
61
|
+
const l = getL(themeColorTextDark);
|
|
62
|
+
expect(l).toBeGreaterThanOrEqual(0.69);
|
|
63
|
+
expect(l).toBeLessThanOrEqual(0.83);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('white text on dark colors', () => {
|
|
68
|
+
expect(deriveTheme('navy').textOnThemeLight).toBe('#fff');
|
|
69
|
+
expect(deriveTheme('#000').textOnThemeLight).toBe('#fff');
|
|
70
|
+
expect(deriveTheme('hsl(351 70% 40%)').textOnThemeLight).toBe('#fff');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('black text on bright colors', () => {
|
|
74
|
+
expect(deriveTheme('lime').textOnThemeLight).toBe('#000');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('white text on saturated and mid-tone colors', () => {
|
|
78
|
+
expect(deriveTheme('steelblue').textOnThemeLight).toBe('#fff');
|
|
79
|
+
expect(deriveTheme('teal').textOnThemeLight).toBe('#fff');
|
|
80
|
+
expect(deriveTheme('tomato').textOnThemeLight).toBe('#fff');
|
|
81
|
+
expect(deriveTheme('cornsilk').textOnThemeLight).toBe('#fff');
|
|
82
|
+
expect(deriveTheme('hsl(195 70% 40%)').textOnThemeLight).toBe('#fff');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('steelblue produces values close to original', () => {
|
|
86
|
+
const result = deriveTheme('steelblue');
|
|
87
|
+
// steelblue OKLCH L is ~0.588 which is within light theme range,
|
|
88
|
+
// so it should be unchanged (or very close)
|
|
89
|
+
expect(result.themeColorLight).toBe('#4682b4');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('throws on invalid color', () => {
|
|
93
|
+
expect(() => deriveTheme('notacolor')).toThrow('Invalid color');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('getTextOnColor', () => {
|
|
98
|
+
test('returns white for dark colors', () => {
|
|
99
|
+
expect(getTextOnColor('navy')).toBe('#fff');
|
|
100
|
+
expect(getTextOnColor('#000')).toBe('#fff');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('returns black for bright colors', () => {
|
|
104
|
+
expect(getTextOnColor('cornsilk')).toBe('#000');
|
|
105
|
+
expect(getTextOnColor('#fff')).toBe('#000');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('throws on invalid color', () => {
|
|
109
|
+
expect(() => getTextOnColor('notacolor')).toThrow('Invalid color');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { parse, oklch, rgb, toGamut, formatHex } from 'culori';
|
|
2
|
+
import type { Oklch, Rgb } from 'culori';
|
|
3
|
+
import type { DerivedTheme } from '../types.js';
|
|
4
|
+
|
|
5
|
+
// OKLCH lightness range for the theme color used as backgrounds/outlines
|
|
6
|
+
const LIGHT_THEME_L_MIN = 0.35;
|
|
7
|
+
const LIGHT_THEME_L_MAX = 0.62;
|
|
8
|
+
const DARK_THEME_L_MIN = 0.55;
|
|
9
|
+
const DARK_THEME_L_MAX = 0.8;
|
|
10
|
+
|
|
11
|
+
// OKLCH lightness range for theme-derived text on page backgrounds
|
|
12
|
+
const LIGHT_TEXT_L_MIN = 0.35;
|
|
13
|
+
const LIGHT_TEXT_L_MAX = 0.5;
|
|
14
|
+
const DARK_TEXT_L_MIN = 0.7;
|
|
15
|
+
const DARK_TEXT_L_MAX = 0.82;
|
|
16
|
+
|
|
17
|
+
function clamp(v: number, min: number, max: number): number {
|
|
18
|
+
return Math.min(Math.max(v, min), max);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function linearize(c: number): number {
|
|
22
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function relativeLuminance({ r, g, b }: Rgb): number {
|
|
26
|
+
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function contrastRatio(l1: number, l2: number): number {
|
|
30
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pick black or white text for use on the given background color.
|
|
34
|
+
// Prefer white unless black has substantially better contrast (1.5x),
|
|
35
|
+
// which gives white text on saturated mid-tone colors like steelblue and tomato.
|
|
36
|
+
function pickTextColor(bgHex: string): '#fff' | '#000' {
|
|
37
|
+
const bgRgb = rgb(parse(bgHex)) as Rgb;
|
|
38
|
+
const bgLum = relativeLuminance(bgRgb);
|
|
39
|
+
const whiteContrast = contrastRatio(1, bgLum);
|
|
40
|
+
const blackContrast = contrastRatio(bgLum, 0);
|
|
41
|
+
return blackContrast > whiteContrast * 1.9 ? '#000' : '#fff';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toHex(oklchColor: Oklch): string {
|
|
45
|
+
return formatHex(toGamut('rgb', 'oklch')(oklchColor));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function deriveTheme(cssColor: string): DerivedTheme {
|
|
49
|
+
const parsed = parse(cssColor);
|
|
50
|
+
if (!parsed) {
|
|
51
|
+
throw new Error(`Invalid color: ${cssColor}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const base = oklch(parsed) as Oklch;
|
|
55
|
+
const l = base.l;
|
|
56
|
+
const c = base.c || 0;
|
|
57
|
+
const h = base.h;
|
|
58
|
+
|
|
59
|
+
const lightL = clamp(l, LIGHT_THEME_L_MIN, LIGHT_THEME_L_MAX);
|
|
60
|
+
const darkL = clamp(l, DARK_THEME_L_MIN, DARK_THEME_L_MAX);
|
|
61
|
+
|
|
62
|
+
const textLightL = clamp(l, LIGHT_TEXT_L_MIN, LIGHT_TEXT_L_MAX);
|
|
63
|
+
const textDarkL = clamp(l, DARK_TEXT_L_MIN, DARK_TEXT_L_MAX);
|
|
64
|
+
|
|
65
|
+
const themeColorLight = toHex({ mode: 'oklch', l: lightL, c, h });
|
|
66
|
+
const themeColorDark = toHex({ mode: 'oklch', l: darkL, c, h });
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
themeColorLight,
|
|
70
|
+
themeColorDark,
|
|
71
|
+
themeColorTextLight: toHex({ mode: 'oklch', l: textLightL, c, h }),
|
|
72
|
+
themeColorTextDark: toHex({ mode: 'oklch', l: textDarkL, c, h }),
|
|
73
|
+
textOnThemeLight: pickTextColor(themeColorLight),
|
|
74
|
+
textOnThemeDark: pickTextColor(themeColorDark),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getTextOnColor(cssColor: string): '#fff' | '#000' {
|
|
79
|
+
const parsed = parse(cssColor);
|
|
80
|
+
if (!parsed) {
|
|
81
|
+
throw new Error(`Invalid color: ${cssColor}`);
|
|
82
|
+
}
|
|
83
|
+
const gamutMapped = formatHex(toGamut('rgb', 'oklch')(parsed));
|
|
84
|
+
return pickTextColor(gamutMapped);
|
|
85
|
+
}
|