@abreen/tada 1.17.0 → 1.18.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 +41 -7
- package/bin/tada.ts +23 -9
- package/bin/validators.ts +34 -1
- package/build/branding.ts +41 -0
- package/build/build-manifest.ts +10 -5
- package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
- package/build/build-validation.ts +176 -0
- package/build/bundle.ts +75 -65
- package/build/columns-plugin.ts +27 -1
- package/build/config-loader.ts +3 -3
- package/build/custom-fonts.ts +8 -5
- package/build/deflist-id-plugin.ts +2 -2
- package/build/generate-favicon.ts +7 -6
- package/build/generate-fonts.ts +8 -15
- package/build/generate-katex-assets.ts +19 -17
- package/build/generate-web-app-manifest.ts +3 -11
- package/build/lodash-template.ts +14 -0
- package/build/output-publication.ts +238 -0
- package/build/pagefind.ts +49 -21
- package/build/pdf-text.ts +41 -1
- package/build/pipeline.ts +95 -101
- package/build/site-assets.ts +46 -0
- package/build/site-build.ts +267 -0
- package/build/source-model.ts +223 -229
- package/build/source-records.ts +69 -81
- package/build/template-globals.ts +3 -0
- package/build/templates.ts +12 -8
- package/build/toc-plugin.ts +21 -21
- package/build/types.d.ts +38 -38
- package/build/util.ts +0 -4
- package/build/utils/code.ts +51 -117
- package/build/utils/final-html.ts +171 -47
- package/build/utils/jdi-runner/LiterateRunner.java +9 -1
- package/build/utils/literate-java.ts +11 -7
- package/build/utils/markdown-partials.ts +4 -3
- package/build/utils/markdown.ts +6 -17
- package/build/utils/paths.ts +3 -0
- package/build/utils/plain-text.ts +18 -0
- package/build/utils/render.ts +197 -154
- package/build/utils/shiki-highlighter.ts +11 -0
- package/build/utils/source-template.ts +2 -2
- package/build/utils/trace-core.ts +8 -83
- package/build/utils/trace.ts +38 -95
- package/build/validate-config-links.ts +39 -6
- package/build/watch/build-incremental.ts +46 -103
- package/build/watch/compiler.ts +158 -37
- package/build/watch/engine.ts +207 -162
- package/build/watch/index.ts +9 -2
- package/build/watch/planner.ts +88 -144
- package/build/watch/runtime.ts +21 -34
- package/build/watch/types.d.ts +32 -64
- package/package.json +4 -5
- package/schema/site.schema.json +18 -2
- package/src/_content.scss +447 -0
- package/src/_fonts.scss +90 -0
- package/src/_layout.scss +50 -35
- package/src/appearance-picker/{style.scss → _index.scss} +1 -1
- package/src/code/{style.scss → _index.scss} +61 -0
- package/src/header/{_base.scss → _index.scss} +98 -6
- package/src/index.ts +1 -17
- package/src/navigate/{style.scss → _index.scss} +20 -0
- package/src/navigate/eligible.ts +0 -40
- package/src/navigate/index.ts +50 -5
- package/src/navigate/lifecycle.ts +19 -2
- package/src/navigate/runtime.ts +20 -4
- package/src/page-update/index.ts +33 -20
- package/src/print/{style.scss → _index.scss} +3 -1
- package/src/search/index.ts +30 -2
- package/src/slides/{style.scss → _index.scss} +5 -0
- package/src/style.scss +24 -536
- package/src/timezone/index.ts +29 -3
- package/src/timezone/time-format.ts +4 -0
- package/src/toc/index.ts +6 -5
- package/src/trace/index.ts +11 -1
- package/templates/_author.html +2 -2
- package/templates/_download.html +1 -1
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +3 -3
- package/templates/_nav.html +2 -2
- package/templates/_page-bottom.html +6 -0
- package/templates/_top.html +18 -14
- package/templates/code.html +22 -21
- package/templates/default.html +12 -11
- package/templates/literate.html +6 -5
- package/build/copy.ts +0 -82
- package/build/generate-content-assets.ts +0 -121
- package/build/utils/content-files.ts +0 -170
- package/build/watch/assets.ts +0 -103
- package/build/watch/build-full.ts +0 -97
- package/build/watch/build-helpers.ts +0 -66
- package/build/watch/build-result.ts +0 -19
- package/build/watch/fs-commit.ts +0 -153
- package/build/watch/mutations.ts +0 -73
- package/build/watch/snapshot.ts +0 -201
- package/build/watch/validation.ts +0 -50
- package/src/code.scss +0 -60
- package/src/critical.scss +0 -5
- package/src/header/style.scss +0 -91
- /package/src/{literate.scss → _literate.scss} +0 -0
- /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
- /package/src/anchor/{style.scss → _index.scss} +0 -0
- /package/src/page-update/{style.scss → _index.scss} +0 -0
- /package/src/question/{style.scss → _index.scss} +0 -0
- /package/src/search/{style.scss → _index.scss} +0 -0
- /package/src/timezone/{style.scss → _index.scss} +0 -0
- /package/src/toc/{style.scss → _index.scss} +0 -0
- /package/src/trace/{style.scss → _index.scss} +0 -0
package/build/utils/code.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parse as parseJava } from 'java-parser';
|
|
4
|
-
import {
|
|
4
|
+
import { hastToHtml } from 'shiki';
|
|
5
5
|
import { makeLogger } from '../log';
|
|
6
6
|
import { getExtensionToShikiLanguage } from '../site-variables';
|
|
7
|
-
import {
|
|
7
|
+
import { highlightCodeToHast } from './shiki-highlighter';
|
|
8
8
|
import externalLinksPlugin from '../external-links-plugin';
|
|
9
9
|
import { createApplyBasePath } from './paths';
|
|
10
10
|
import katexPlugin from './katex';
|
|
@@ -122,16 +122,7 @@ const JAVA_TYPE_DECLARATION_NODES = new Set([
|
|
|
122
122
|
'recordDeclaration',
|
|
123
123
|
]);
|
|
124
124
|
|
|
125
|
-
function extractJavaMethodMeta(
|
|
126
|
-
methodNode: CstNode,
|
|
127
|
-
requireBody = true,
|
|
128
|
-
): MethodMeta | null {
|
|
129
|
-
const methodBody = methodNode.children?.methodBody?.[0];
|
|
130
|
-
const hasBody = Boolean(methodBody?.children?.block?.length);
|
|
131
|
-
if (requireBody && !hasBody) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
125
|
+
function extractJavaMethodMeta(methodNode: CstNode): MethodMeta | null {
|
|
135
126
|
const methodHeader = methodNode.children?.methodHeader?.[0];
|
|
136
127
|
const methodDeclarator = methodHeader?.children?.methodDeclarator?.[0];
|
|
137
128
|
const identifier = methodDeclarator?.children?.Identifier?.[0];
|
|
@@ -293,16 +284,15 @@ export function extractJavaMethodToc(sourceCode: string): JavaTocEntry[] {
|
|
|
293
284
|
return;
|
|
294
285
|
}
|
|
295
286
|
|
|
296
|
-
if (
|
|
287
|
+
if (
|
|
288
|
+
(node.name === 'methodDeclaration' ||
|
|
289
|
+
node.name === 'interfaceMethodDeclaration') &&
|
|
290
|
+
typeDepth <= 1
|
|
291
|
+
) {
|
|
297
292
|
const method = extractJavaMethodMeta(node);
|
|
298
293
|
if (method) {
|
|
299
294
|
callables.push({ ...method, kind: 'method' });
|
|
300
295
|
}
|
|
301
|
-
} else if (node.name === 'interfaceMethodDeclaration' && typeDepth <= 1) {
|
|
302
|
-
const method = extractJavaMethodMeta(node, false);
|
|
303
|
-
if (method) {
|
|
304
|
-
callables.push({ ...method, kind: 'method' });
|
|
305
|
-
}
|
|
306
296
|
} else if (node.name === 'constructorDeclaration' && typeDepth === 1) {
|
|
307
297
|
const constructor = extractJavaConstructorMeta(node);
|
|
308
298
|
if (constructor) {
|
|
@@ -325,6 +315,13 @@ export function extractJavaMethodToc(sourceCode: string): JavaTocEntry[] {
|
|
|
325
315
|
for (const value of Object.values(children)) {
|
|
326
316
|
for (const child of value) {
|
|
327
317
|
if (child && child.name) {
|
|
318
|
+
// Anonymous classes have a classBody without a type declaration.
|
|
319
|
+
if (
|
|
320
|
+
node.name === 'unqualifiedClassInstanceCreationExpression' &&
|
|
321
|
+
child.name === 'classBody'
|
|
322
|
+
) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
328
325
|
visit(child, nextTypeDepth);
|
|
329
326
|
}
|
|
330
327
|
}
|
|
@@ -363,104 +360,42 @@ function escapeHtml(text: string): string {
|
|
|
363
360
|
.replace(/>/g, '>');
|
|
364
361
|
}
|
|
365
362
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
363
|
+
interface HastNode {
|
|
364
|
+
type: string;
|
|
365
|
+
value?: string;
|
|
366
|
+
children?: HastNode[];
|
|
370
367
|
}
|
|
371
368
|
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
): Node[] {
|
|
376
|
-
const containers: Node[] = [line];
|
|
377
|
-
|
|
378
|
-
for (const openElement of openElements) {
|
|
379
|
-
const clone = openElement.cloneNode(false);
|
|
380
|
-
containers[containers.length - 1].appendChild(clone);
|
|
381
|
-
containers.push(clone);
|
|
369
|
+
function hasText(node: HastNode): boolean {
|
|
370
|
+
if (node.type === 'text') {
|
|
371
|
+
return Boolean(node.value);
|
|
382
372
|
}
|
|
383
|
-
|
|
384
|
-
return containers;
|
|
373
|
+
return node.children?.some(hasText) ?? false;
|
|
385
374
|
}
|
|
386
375
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
let currentLineHasContent = false;
|
|
399
|
-
|
|
400
|
-
function finishCurrentLine(): void {
|
|
401
|
-
if (!currentLineHasContent) {
|
|
402
|
-
currentContainers[currentContainers.length - 1].appendChild(
|
|
403
|
-
document.createTextNode('\u00A0'),
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
lines.push(currentLine);
|
|
407
|
-
currentLine = createCodeLine(document);
|
|
408
|
-
currentContainers = cloneOpenElements(openElements, currentLine);
|
|
409
|
-
currentLineHasContent = false;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function visit(node: Node): void {
|
|
413
|
-
if (node.nodeType === 3) {
|
|
414
|
-
const parts = (node.textContent || '').split('\n');
|
|
415
|
-
for (let i = 0; i < parts.length; i++) {
|
|
416
|
-
if (parts[i].length > 0) {
|
|
417
|
-
currentContainers[currentContainers.length - 1].appendChild(
|
|
418
|
-
document.createTextNode(parts[i]),
|
|
419
|
-
);
|
|
420
|
-
currentLineHasContent = true;
|
|
421
|
-
}
|
|
422
|
-
if (i < parts.length - 1) {
|
|
423
|
-
finishCurrentLine();
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (node.nodeType !== 1) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const clone = node.cloneNode(false);
|
|
434
|
-
currentContainers[currentContainers.length - 1].appendChild(clone);
|
|
435
|
-
openElements.push(node);
|
|
436
|
-
currentContainers.push(clone);
|
|
437
|
-
|
|
438
|
-
for (const child of Array.from(node.childNodes)) {
|
|
439
|
-
visit(child);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
currentContainers.pop();
|
|
443
|
-
openElements.pop();
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
for (const child of Array.from(codeEl.childNodes)) {
|
|
447
|
-
visit(child);
|
|
376
|
+
/**
|
|
377
|
+
* Returns the HTML of each highlighted line. Shiki tokenizes line by line, so
|
|
378
|
+
* each `span.line` is balanced on its own, even inside multi-line tokens such
|
|
379
|
+
* as block comments. Empty lines get a non-breaking space so that their row
|
|
380
|
+
* keeps its height.
|
|
381
|
+
*/
|
|
382
|
+
function highlightLines(source: string, lang: string): string[] {
|
|
383
|
+
const pre = highlightCodeToHast(source, lang).children[0];
|
|
384
|
+
const code = pre?.type === 'element' ? pre.children[0] : undefined;
|
|
385
|
+
if (code?.type !== 'element') {
|
|
386
|
+
throw new Error('unexpected highlighter output');
|
|
448
387
|
}
|
|
449
388
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
389
|
+
const lines: string[] = [];
|
|
390
|
+
for (const line of code.children) {
|
|
391
|
+
if (line.type === 'element') {
|
|
392
|
+
const html = hastToHtml(line, {
|
|
393
|
+
characterReferences: { useNamedReferences: true },
|
|
394
|
+
});
|
|
395
|
+
lines.push(hasText(line) ? html : `${html} `);
|
|
455
396
|
}
|
|
456
|
-
lines.push(currentLine);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
while (lines.length < lineCount) {
|
|
460
|
-
lines.push(createCodeLine(document));
|
|
461
397
|
}
|
|
462
|
-
|
|
463
|
-
return lines.map(line => line.innerHTML);
|
|
398
|
+
return lines;
|
|
464
399
|
}
|
|
465
400
|
|
|
466
401
|
export function renderCodeSegment(
|
|
@@ -469,20 +404,19 @@ export function renderCodeSegment(
|
|
|
469
404
|
lang: string,
|
|
470
405
|
{ linkLineNumbers = true }: { linkLineNumbers?: boolean } = {},
|
|
471
406
|
): string {
|
|
472
|
-
|
|
473
|
-
|
|
407
|
+
// Lines from CRLF sources end with CR. Shiki treats CRLF as a line break
|
|
408
|
+
// but would keep the last line's CR as text.
|
|
409
|
+
const sourceLines = lines.map(line =>
|
|
410
|
+
line.endsWith('\r') ? line.slice(0, -1) : line,
|
|
411
|
+
);
|
|
412
|
+
let lineHtml: string[];
|
|
474
413
|
|
|
475
414
|
try {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
const inner = (fragment.querySelector('code') as HTMLElement).innerHTML;
|
|
479
|
-
lineHtml = splitHighlightedHtmlIntoLines(inner, lines.length);
|
|
415
|
+
const highlighted = highlightLines(sourceLines.join('\n'), lang);
|
|
416
|
+
lineHtml = sourceLines.map((_, i) => highlighted[i] ?? '');
|
|
480
417
|
} catch (err: unknown) {
|
|
481
418
|
log.error`Failed to highlight code block: ${(err as Error).message}`;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (!lineHtml) {
|
|
485
|
-
lineHtml = lines.map(line => escapeHtml(line));
|
|
419
|
+
lineHtml = sourceLines.map(line => escapeHtml(line));
|
|
486
420
|
}
|
|
487
421
|
|
|
488
422
|
const rows = lineHtml.map((line, i) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import {
|
|
2
|
+
import { decodeHTMLAttribute } from 'entities';
|
|
3
3
|
import { getExtensionToShikiLanguage } from '../site-variables';
|
|
4
4
|
import { createApplyBasePath, normalizeOutputPath } from './paths';
|
|
5
5
|
import { isInternalLink } from './link';
|
|
@@ -14,8 +14,10 @@ interface FinalizeHtmlPageOptions {
|
|
|
14
14
|
html: string;
|
|
15
15
|
siteVariables: SiteVariables;
|
|
16
16
|
sourceUrlPath: string;
|
|
17
|
-
validInternalTargets:
|
|
18
|
-
|
|
17
|
+
validInternalTargets: ReadonlySet<string>;
|
|
18
|
+
generatedPageTargets?: ReadonlySet<string>;
|
|
19
|
+
codePageSourceTargets?: ReadonlySet<string>;
|
|
20
|
+
literateJavaOutputPaths?: ReadonlySet<string>;
|
|
19
21
|
dependencyCollector?: RenderDependencyCollector;
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -24,6 +26,10 @@ interface FinalizedHtmlPage {
|
|
|
24
26
|
analysis: HtmlOutputAnalysis;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
type RewriterElement = HTMLRewriterTypes.Element;
|
|
30
|
+
|
|
31
|
+
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
32
|
+
|
|
27
33
|
function splitHref(href: string): { pathname: string; suffix: string } {
|
|
28
34
|
const match = href.match(/^([^?#]*)(.*)$/);
|
|
29
35
|
return { pathname: match ? match[1] : href, suffix: match ? match[2] : '' };
|
|
@@ -85,16 +91,16 @@ function rewriteAbsoluteSrcWithBasePath(
|
|
|
85
91
|
function resolveAnchorTarget({
|
|
86
92
|
href,
|
|
87
93
|
sourceUrlPath,
|
|
88
|
-
validInternalTargets,
|
|
89
94
|
codeExtensions,
|
|
95
|
+
codePageSourceTargets,
|
|
90
96
|
literateJavaOutputPaths,
|
|
91
97
|
skipCodeLinkRewrite,
|
|
92
98
|
}: {
|
|
93
99
|
href: string;
|
|
94
100
|
sourceUrlPath: string;
|
|
95
|
-
validInternalTargets: Set<string>;
|
|
96
101
|
codeExtensions: string[];
|
|
97
|
-
|
|
102
|
+
codePageSourceTargets?: ReadonlySet<string>;
|
|
103
|
+
literateJavaOutputPaths?: ReadonlySet<string>;
|
|
98
104
|
skipCodeLinkRewrite: boolean;
|
|
99
105
|
}): { finalHref: string; resolvedTarget: string | null } {
|
|
100
106
|
if (!isInternalLink(href)) {
|
|
@@ -116,19 +122,38 @@ function resolveAnchorTarget({
|
|
|
116
122
|
const htmlTarget = `${resolvedPath}.html`;
|
|
117
123
|
if (literateJavaOutputPaths?.has(resolvedPath)) {
|
|
118
124
|
resolvedTarget = resolvedPath;
|
|
119
|
-
} else if (
|
|
125
|
+
} else if (codePageSourceTargets?.has(resolvedPath)) {
|
|
120
126
|
finalPathname = `${pathname}.html`;
|
|
121
127
|
resolvedTarget = htmlTarget;
|
|
122
|
-
} else if (validInternalTargets.has(resolvedPath)) {
|
|
123
|
-
resolvedTarget = resolvedPath;
|
|
124
|
-
} else {
|
|
125
|
-
resolvedTarget = htmlTarget;
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
return { finalHref: `${finalPathname}${suffix}`, resolvedTarget };
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
// HTMLRewriter exposes attribute values exactly as authored, without decoding
|
|
135
|
+
// character references, so decode them the way an HTML parser would. It also
|
|
136
|
+
// reports empty values as null.
|
|
137
|
+
function getAttributeValue(
|
|
138
|
+
element: RewriterElement,
|
|
139
|
+
name: string,
|
|
140
|
+
): string | null {
|
|
141
|
+
const value = element.getAttribute(name);
|
|
142
|
+
if (value === null) {
|
|
143
|
+
return element.hasAttribute(name) ? '' : null;
|
|
144
|
+
}
|
|
145
|
+
return value.includes('&') ? decodeHTMLAttribute(value) : value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// HTMLRewriter writes new values verbatim apart from escaping double quotes.
|
|
149
|
+
function setAttributeValue(
|
|
150
|
+
element: RewriterElement,
|
|
151
|
+
name: string,
|
|
152
|
+
value: string,
|
|
153
|
+
): void {
|
|
154
|
+
element.setAttribute(name, value.replaceAll('&', '&'));
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
export function finalizeHtmlPage({
|
|
133
158
|
filePath,
|
|
134
159
|
html,
|
|
@@ -136,31 +161,43 @@ export function finalizeHtmlPage({
|
|
|
136
161
|
sourceUrlPath,
|
|
137
162
|
validInternalTargets,
|
|
138
163
|
literateJavaOutputPaths,
|
|
164
|
+
generatedPageTargets,
|
|
165
|
+
codePageSourceTargets,
|
|
139
166
|
dependencyCollector,
|
|
140
167
|
}: FinalizeHtmlPageOptions): FinalizedHtmlPage {
|
|
141
|
-
const dom = new JSDOM(html);
|
|
142
|
-
const document = dom.window.document;
|
|
143
168
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
144
169
|
const codeExtensions = Object.keys(
|
|
145
170
|
getExtensionToShikiLanguage(siteVariables),
|
|
146
171
|
);
|
|
147
172
|
const analysis: HtmlOutputAnalysis = { outgoingTargets: new Set() };
|
|
173
|
+
// Content link targets are recorded before classification targets, matching
|
|
174
|
+
// the order of the original separate passes.
|
|
175
|
+
const classificationTargets: string[] = [];
|
|
148
176
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
const origin = new URL(siteVariables.base).origin;
|
|
178
|
+
// Source paths contain no query or fragment. Escape filesystem delimiters
|
|
179
|
+
// while preserving the percent-encoded segments supplied by code pages.
|
|
180
|
+
const sourcePathname = sourceUrlPath.replace(/[?#]/g, encodeURIComponent);
|
|
181
|
+
const sourceUrl = new URL(applyBasePath(sourcePathname), origin);
|
|
182
|
+
const basePath = (siteVariables.basePath || '/').replace(/\/$/, '');
|
|
154
183
|
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
// Rewrites an href and returns its final value. Anchors inside `main.body`
|
|
185
|
+
// are page content: they are resolved, rewritten to code pages, and
|
|
186
|
+
// validated. Other hrefs only receive the base path.
|
|
187
|
+
function rewriteHref(
|
|
188
|
+
element: RewriterElement,
|
|
189
|
+
href: string,
|
|
190
|
+
isContent: boolean,
|
|
191
|
+
): string {
|
|
192
|
+
const isAnchor =
|
|
193
|
+
element.tagName === 'a' && element.namespaceURI === HTML_NAMESPACE;
|
|
157
194
|
|
|
158
|
-
if (
|
|
195
|
+
if (isAnchor && isContent) {
|
|
159
196
|
const { finalHref, resolvedTarget } = resolveAnchorTarget({
|
|
160
197
|
href,
|
|
161
198
|
sourceUrlPath,
|
|
162
|
-
validInternalTargets,
|
|
163
199
|
codeExtensions,
|
|
200
|
+
codePageSourceTargets,
|
|
164
201
|
literateJavaOutputPaths,
|
|
165
202
|
skipCodeLinkRewrite: element.hasAttribute('download'),
|
|
166
203
|
});
|
|
@@ -169,13 +206,11 @@ export function finalizeHtmlPage({
|
|
|
169
206
|
: finalHref;
|
|
170
207
|
|
|
171
208
|
if (rewrittenHref !== href) {
|
|
172
|
-
element
|
|
209
|
+
setAttributeValue(element, 'href', rewrittenHref);
|
|
173
210
|
}
|
|
174
211
|
|
|
175
212
|
if (resolvedTarget) {
|
|
176
|
-
|
|
177
|
-
analysis.outgoingTargets.add(resolvedTarget);
|
|
178
|
-
}
|
|
213
|
+
analysis.outgoingTargets.add(resolvedTarget);
|
|
179
214
|
|
|
180
215
|
const directoryIndexPath = getDirectoryIndexPath(resolvedTarget);
|
|
181
216
|
if (
|
|
@@ -196,7 +231,7 @@ export function finalizeHtmlPage({
|
|
|
196
231
|
dependencyCollector?.internalTargets?.add(resolvedTarget);
|
|
197
232
|
}
|
|
198
233
|
|
|
199
|
-
|
|
234
|
+
return rewrittenHref;
|
|
200
235
|
}
|
|
201
236
|
|
|
202
237
|
if (isAnchor && isInternalLink(href)) {
|
|
@@ -209,31 +244,120 @@ export function finalizeHtmlPage({
|
|
|
209
244
|
|
|
210
245
|
const rewrittenHref = rewriteAbsoluteHrefWithBasePath(href, applyBasePath);
|
|
211
246
|
if (rewrittenHref !== href) {
|
|
212
|
-
element
|
|
247
|
+
setAttributeValue(element, 'href', rewrittenHref);
|
|
213
248
|
}
|
|
249
|
+
return rewrittenHref;
|
|
214
250
|
}
|
|
215
251
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
252
|
+
// Recomputes the generated-page marker from an anchor's final href.
|
|
253
|
+
function classifyAnchor(anchor: RewriterElement, href: string | null): void {
|
|
254
|
+
if (anchor.hasAttribute('data-tada-page')) {
|
|
255
|
+
anchor.removeAttribute('data-tada-page');
|
|
220
256
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
257
|
+
if (href === null) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
let url: URL;
|
|
261
|
+
try {
|
|
262
|
+
url = new URL(href, sourceUrl);
|
|
263
|
+
} catch {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (url.origin !== origin) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (
|
|
270
|
+
basePath &&
|
|
271
|
+
url.pathname !== basePath &&
|
|
272
|
+
!url.pathname.startsWith(`${basePath}/`)
|
|
273
|
+
) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
let target = url.pathname.slice(basePath.length) || '/';
|
|
277
|
+
try {
|
|
278
|
+
target = decodeURIComponent(target);
|
|
279
|
+
} catch {
|
|
280
|
+
// Preserve malformed encodings for classification as authored.
|
|
281
|
+
}
|
|
282
|
+
target = normalizeOutputPath(target);
|
|
283
|
+
if (generatedPageTargets) {
|
|
284
|
+
classificationTargets.push(target);
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
generatedPageTargets?.has(target) &&
|
|
288
|
+
!anchor.hasAttribute('target') &&
|
|
289
|
+
!anchor.hasAttribute('download')
|
|
290
|
+
) {
|
|
291
|
+
anchor.setAttribute('data-tada-page', '');
|
|
225
292
|
}
|
|
226
293
|
}
|
|
227
294
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
295
|
+
// HTMLRewriter edits the original markup in place. It reads `<noscript>`
|
|
296
|
+
// contents as raw text, so a nested rewriter parses them as markup (as a
|
|
297
|
+
// parser with scripting disabled would), inheriting whether the element is
|
|
298
|
+
// inside `main.body`.
|
|
299
|
+
function rewrite(input: string, initialContentDepth: number): string {
|
|
300
|
+
let contentDepth = initialContentDepth;
|
|
301
|
+
let noscriptSource = '';
|
|
302
|
+
|
|
303
|
+
return new HTMLRewriter()
|
|
304
|
+
.on('main.body', {
|
|
305
|
+
element(element) {
|
|
306
|
+
contentDepth++;
|
|
307
|
+
element.onEndTag(() => {
|
|
308
|
+
contentDepth--;
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
.on('[href]', {
|
|
313
|
+
element(element) {
|
|
314
|
+
const href = getAttributeValue(element, 'href');
|
|
315
|
+
const finalHref = href
|
|
316
|
+
? rewriteHref(element, href, contentDepth > 0)
|
|
317
|
+
: href;
|
|
318
|
+
if (element.tagName === 'a') {
|
|
319
|
+
classifyAnchor(element, finalHref);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
.on('a:not([href])', {
|
|
324
|
+
element(element) {
|
|
325
|
+
classifyAnchor(element, null);
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
.on('[src]', {
|
|
329
|
+
element(element) {
|
|
330
|
+
const src = getAttributeValue(element, 'src');
|
|
331
|
+
if (!src) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const rewrittenSrc = rewriteAbsoluteSrcWithBasePath(
|
|
335
|
+
src,
|
|
336
|
+
applyBasePath,
|
|
337
|
+
);
|
|
338
|
+
if (rewrittenSrc !== src) {
|
|
339
|
+
setAttributeValue(element, 'src', rewrittenSrc);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
.on('noscript', {
|
|
344
|
+
text(chunk) {
|
|
345
|
+
noscriptSource += chunk.text;
|
|
346
|
+
if (!chunk.lastInTextNode) {
|
|
347
|
+
chunk.remove();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
chunk.replace(rewrite(noscriptSource, contentDepth), { html: true });
|
|
351
|
+
noscriptSource = '';
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
.transform(input);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const finalizedHtml = rewrite(html, 0);
|
|
358
|
+
for (const target of classificationTargets) {
|
|
359
|
+
dependencyCollector?.internalTargets?.add(target);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { html: finalizedHtml, analysis };
|
|
239
363
|
}
|
|
@@ -244,7 +244,15 @@ public class LiterateRunner {
|
|
|
244
244
|
case '\n' -> sb.append("\\n");
|
|
245
245
|
case '\r' -> sb.append("\\r");
|
|
246
246
|
case '\t' -> sb.append("\\t");
|
|
247
|
-
default ->
|
|
247
|
+
default -> {
|
|
248
|
+
if (c < 0x20) {
|
|
249
|
+
sb.append("\\u00");
|
|
250
|
+
sb.append(Character.forDigit(c >> 4, 16));
|
|
251
|
+
sb.append(Character.forDigit(c & 0xf, 16));
|
|
252
|
+
} else {
|
|
253
|
+
sb.append(c);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
248
256
|
}
|
|
249
257
|
}
|
|
250
258
|
sb.append('"');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import type Token from 'markdown-it/lib/token.mjs';
|
|
2
3
|
import os from 'os';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import { execFileSync } from 'child_process';
|
|
@@ -63,6 +64,15 @@ export function parseLiterateJava(
|
|
|
63
64
|
});
|
|
64
65
|
const tokens = md.parse(content, {});
|
|
65
66
|
|
|
67
|
+
return { pageVariables, content, ...extractLiterateJavaCode(tokens) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function extractLiterateJavaCode(
|
|
71
|
+
tokens: Token[],
|
|
72
|
+
): Pick<
|
|
73
|
+
LiterateJavaParseResult,
|
|
74
|
+
'javaSource' | 'codeBlocks' | 'visibleBlockIndices'
|
|
75
|
+
> {
|
|
66
76
|
const codeBlocks: LiterateCodeBlock[] = [];
|
|
67
77
|
let javaLine = 1;
|
|
68
78
|
|
|
@@ -98,13 +108,7 @@ export function parseLiterateJava(
|
|
|
98
108
|
const hiddenCount = codeBlocks.length - visibleBlockIndices.length;
|
|
99
109
|
log.debug`Parsed ${codeBlocks.length} code block(s) (${hiddenCount} hidden), ${javaSource.split('\n').length} Java line(s)`;
|
|
100
110
|
|
|
101
|
-
return {
|
|
102
|
-
pageVariables,
|
|
103
|
-
content,
|
|
104
|
-
javaSource,
|
|
105
|
-
codeBlocks,
|
|
106
|
-
visibleBlockIndices,
|
|
107
|
-
};
|
|
111
|
+
return { javaSource, codeBlocks, visibleBlockIndices };
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
export function hasMainMethod(javaSource: string): boolean {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import { compileTemplate } from '../lodash-template';
|
|
4
4
|
import type MarkdownIt from 'markdown-it';
|
|
5
5
|
import type StateCore from 'markdown-it/lib/rules_core/state_core.mjs';
|
|
6
6
|
import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs';
|
|
@@ -26,6 +26,7 @@ interface MarkdownPartialsEnv {
|
|
|
26
26
|
interface MarkdownPartialsOptions {
|
|
27
27
|
filePath: string;
|
|
28
28
|
templateParams: Record<string, unknown>;
|
|
29
|
+
preserveHtmlComments?: boolean;
|
|
29
30
|
dependencyCollector?: RenderDependencyCollector;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -77,12 +78,12 @@ function renderPartialContent(
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
const raw = fs.readFileSync(resolvedPath, 'utf-8');
|
|
80
|
-
const content = stripHtmlComments(raw);
|
|
81
|
+
const content = options.preserveHtmlComments ? raw : stripHtmlComments(raw);
|
|
81
82
|
|
|
82
83
|
try {
|
|
83
84
|
return {
|
|
84
85
|
resolvedPath,
|
|
85
|
-
content:
|
|
86
|
+
content: compileTemplate(content)(options.templateParams),
|
|
86
87
|
};
|
|
87
88
|
} catch (err) {
|
|
88
89
|
throw new Error(
|