@abreen/tada 1.17.1 → 1.18.1
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 +38 -7
- package/assets/material-symbols/chevron_right_24px.svg +1 -0
- package/bin/tada.ts +16 -7
- package/bin/validators.ts +33 -0
- package/build/branding.ts +41 -0
- package/build/build-manifest.ts +9 -4
- 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/config-loader.ts +3 -3
- package/build/custom-fonts.ts +8 -5
- 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/material-symbols.ts +7 -4
- 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 -244
- package/build/source-records.ts +69 -81
- package/build/template-globals.ts +3 -0
- package/build/templates.ts +5 -2
- package/build/toc-plugin.ts +21 -21
- package/build/types.d.ts +44 -38
- package/build/util.ts +0 -4
- package/build/utils/code.ts +38 -101
- package/build/utils/final-html.ts +171 -47
- 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 +227 -165
- 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 +60 -20
- package/build/watch/build-incremental.ts +46 -103
- package/build/watch/compiler.ts +158 -37
- package/build/watch/engine.ts +207 -176
- 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/init/content/labs/00/VowelCounter.java.md +5 -2
- package/init/content/labs/00/index.md +3 -2
- package/init/content/labs/01/index.md +3 -2
- package/init/content/labs/02/index.md +3 -2
- package/init/content/lectures/01/index.md +3 -2
- package/init/content/lectures/02/index.md +3 -2
- package/package.json +4 -4
- package/schema/site.schema.json +18 -2
- package/src/_content.scss +463 -0
- package/src/_fonts.scss +90 -0
- package/src/_layout.scss +50 -35
- package/src/{material-symbols.scss → _material-symbols.scss} +7 -0
- 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} +29 -3
- package/src/navigate/breadcrumbs.ts +129 -0
- package/src/navigate/eligible.ts +0 -40
- package/src/navigate/index.ts +38 -4
- package/src/navigate/lifecycle.ts +19 -2
- package/src/navigate/runtime.ts +57 -22
- package/src/page-update/index.ts +33 -20
- package/src/print/{style.scss → _index.scss} +3 -1
- package/src/search/index.ts +7 -0
- package/src/slides/{style.scss → _index.scss} +5 -0
- package/src/style.scss +24 -536
- package/src/toc/index.ts +2 -2
- package/templates/_author.html +2 -2
- package/templates/_download.html +1 -1
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +17 -4
- 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/assets/material-symbols/south_east_20px.svg +0 -1
- 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 -218
- package/build/watch/mutations.ts +0 -73
- package/build/watch/snapshot.ts +0 -201
- package/build/watch/validation.ts +0 -57
- 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/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';
|
|
@@ -360,104 +360,42 @@ function escapeHtml(text: string): string {
|
|
|
360
360
|
.replace(/>/g, '>');
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
363
|
+
interface HastNode {
|
|
364
|
+
type: string;
|
|
365
|
+
value?: string;
|
|
366
|
+
children?: HastNode[];
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
function
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
): Node[] {
|
|
373
|
-
const containers: Node[] = [line];
|
|
374
|
-
|
|
375
|
-
for (const openElement of openElements) {
|
|
376
|
-
const clone = openElement.cloneNode(false);
|
|
377
|
-
containers[containers.length - 1].appendChild(clone);
|
|
378
|
-
containers.push(clone);
|
|
369
|
+
function hasText(node: HastNode): boolean {
|
|
370
|
+
if (node.type === 'text') {
|
|
371
|
+
return Boolean(node.value);
|
|
379
372
|
}
|
|
380
|
-
|
|
381
|
-
return containers;
|
|
373
|
+
return node.children?.some(hasText) ?? false;
|
|
382
374
|
}
|
|
383
375
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
let currentLineHasContent = false;
|
|
396
|
-
|
|
397
|
-
function finishCurrentLine(): void {
|
|
398
|
-
if (!currentLineHasContent) {
|
|
399
|
-
currentContainers[currentContainers.length - 1].appendChild(
|
|
400
|
-
document.createTextNode('\u00A0'),
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
lines.push(currentLine);
|
|
404
|
-
currentLine = createCodeLine(document);
|
|
405
|
-
currentContainers = cloneOpenElements(openElements, currentLine);
|
|
406
|
-
currentLineHasContent = false;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function visit(node: Node): void {
|
|
410
|
-
if (node.nodeType === 3) {
|
|
411
|
-
const parts = (node.textContent || '').split('\n');
|
|
412
|
-
for (let i = 0; i < parts.length; i++) {
|
|
413
|
-
if (parts[i].length > 0) {
|
|
414
|
-
currentContainers[currentContainers.length - 1].appendChild(
|
|
415
|
-
document.createTextNode(parts[i]),
|
|
416
|
-
);
|
|
417
|
-
currentLineHasContent = true;
|
|
418
|
-
}
|
|
419
|
-
if (i < parts.length - 1) {
|
|
420
|
-
finishCurrentLine();
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (node.nodeType !== 1) {
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const clone = node.cloneNode(false);
|
|
431
|
-
currentContainers[currentContainers.length - 1].appendChild(clone);
|
|
432
|
-
openElements.push(node);
|
|
433
|
-
currentContainers.push(clone);
|
|
434
|
-
|
|
435
|
-
for (const child of Array.from(node.childNodes)) {
|
|
436
|
-
visit(child);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
currentContainers.pop();
|
|
440
|
-
openElements.pop();
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
for (const child of Array.from(codeEl.childNodes)) {
|
|
444
|
-
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');
|
|
445
387
|
}
|
|
446
388
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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} `);
|
|
452
396
|
}
|
|
453
|
-
lines.push(currentLine);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
while (lines.length < lineCount) {
|
|
457
|
-
lines.push(createCodeLine(document));
|
|
458
397
|
}
|
|
459
|
-
|
|
460
|
-
return lines.map(line => line.innerHTML);
|
|
398
|
+
return lines;
|
|
461
399
|
}
|
|
462
400
|
|
|
463
401
|
export function renderCodeSegment(
|
|
@@ -466,20 +404,19 @@ export function renderCodeSegment(
|
|
|
466
404
|
lang: string,
|
|
467
405
|
{ linkLineNumbers = true }: { linkLineNumbers?: boolean } = {},
|
|
468
406
|
): string {
|
|
469
|
-
|
|
470
|
-
|
|
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[];
|
|
471
413
|
|
|
472
414
|
try {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
const inner = (fragment.querySelector('code') as HTMLElement).innerHTML;
|
|
476
|
-
lineHtml = splitHighlightedHtmlIntoLines(inner, lines.length);
|
|
415
|
+
const highlighted = highlightLines(sourceLines.join('\n'), lang);
|
|
416
|
+
lineHtml = sourceLines.map((_, i) => highlighted[i] ?? '');
|
|
477
417
|
} catch (err: unknown) {
|
|
478
418
|
log.error`Failed to highlight code block: ${(err as Error).message}`;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (!lineHtml) {
|
|
482
|
-
lineHtml = lines.map(line => escapeHtml(line));
|
|
419
|
+
lineHtml = sourceLines.map(line => escapeHtml(line));
|
|
483
420
|
}
|
|
484
421
|
|
|
485
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
|
}
|
|
@@ -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(
|
package/build/utils/markdown.ts
CHANGED
|
@@ -7,7 +7,6 @@ import markdownItAnchor from 'markdown-it-anchor';
|
|
|
7
7
|
import markdownItFootnote from 'markdown-it-footnote';
|
|
8
8
|
import markdownItDeflist from 'markdown-it-deflist';
|
|
9
9
|
import markdownItContainer from 'markdown-it-container';
|
|
10
|
-
import textToId, { deduplicateId } from '../text-to-id';
|
|
11
10
|
import { highlightCode } from './shiki-highlighter';
|
|
12
11
|
import { isBundledLanguage, isPlainTextLanguage } from '../site-variables';
|
|
13
12
|
import headingSubtitlePlugin from '../heading-subtitle-plugin';
|
|
@@ -28,10 +27,11 @@ interface CreateMarkdownOptions {
|
|
|
28
27
|
filePath?: string;
|
|
29
28
|
slides?: boolean;
|
|
30
29
|
validatorOptions?: Record<string, unknown>;
|
|
31
|
-
literateJavaOutputPaths?:
|
|
30
|
+
literateJavaOutputPaths?: ReadonlySet<string>;
|
|
32
31
|
sourceUrlPath?: string;
|
|
33
|
-
validTargets?:
|
|
32
|
+
validTargets?: ReadonlySet<string>;
|
|
34
33
|
templateParams?: Record<string, unknown>;
|
|
34
|
+
preserveHtmlComments?: boolean;
|
|
35
35
|
dependencyCollector?: RenderDependencyCollector;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -102,6 +102,7 @@ export function createMarkdown(
|
|
|
102
102
|
markdown.use(markdownPartialsPlugin, {
|
|
103
103
|
filePath,
|
|
104
104
|
templateParams: options.templateParams,
|
|
105
|
+
preserveHtmlComments: options.preserveHtmlComments,
|
|
105
106
|
dependencyCollector: options.dependencyCollector,
|
|
106
107
|
});
|
|
107
108
|
}
|
|
@@ -458,18 +459,12 @@ export function createMarkdown(
|
|
|
458
459
|
state.tokens = transformedTokens;
|
|
459
460
|
});
|
|
460
461
|
|
|
461
|
-
const usedIds = new Map<string, number>();
|
|
462
462
|
markdown.use(markdownItContainer, 'alert', {
|
|
463
463
|
marker: '!',
|
|
464
464
|
validate: function (params: string) {
|
|
465
465
|
return ALERT_PATTERN.test(params.trim());
|
|
466
466
|
},
|
|
467
|
-
render: function (
|
|
468
|
-
tokens: Token[],
|
|
469
|
-
idx: number,
|
|
470
|
-
_options: unknown,
|
|
471
|
-
env: Record<string, unknown>,
|
|
472
|
-
) {
|
|
467
|
+
render: function (tokens: Token[], idx: number) {
|
|
473
468
|
const matches = tokens[idx].info.trim().match(ALERT_PATTERN);
|
|
474
469
|
|
|
475
470
|
if (tokens[idx].nesting === 1) {
|
|
@@ -483,13 +478,7 @@ export function createMarkdown(
|
|
|
483
478
|
const displayTitle = title
|
|
484
479
|
? markdown.utils.escapeHtml(curlyQuote(title))
|
|
485
480
|
: capitalize(type || '');
|
|
486
|
-
const
|
|
487
|
-
const titleId = deduplicateId(usedIds, baseId);
|
|
488
|
-
|
|
489
|
-
if (!env.alertIds) {
|
|
490
|
-
env.alertIds = [];
|
|
491
|
-
}
|
|
492
|
-
(env.alertIds as string[]).push(titleId);
|
|
481
|
+
const titleId = tokens[idx].attrGet('id')!;
|
|
493
482
|
|
|
494
483
|
let html = `<div class="${classNames.join(' ')}">`;
|
|
495
484
|
html += `<p class="title" id="${titleId}">${displayTitle}</p>\n`;
|
package/build/utils/paths.ts
CHANGED
|
@@ -2,6 +2,9 @@ import path from 'path';
|
|
|
2
2
|
import { globals } from '../globals';
|
|
3
3
|
import type { SiteVariables } from '../types';
|
|
4
4
|
|
|
5
|
+
/** The search index directory inside the output directory */
|
|
6
|
+
export const SEARCH_INDEX_DIR = 'pagefind';
|
|
7
|
+
|
|
5
8
|
export function getPackageDir(): string {
|
|
6
9
|
return path.resolve(import.meta.dir, '..', '..');
|
|
7
10
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { decodeHTML } from 'entities';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts rendered inline HTML (such as a front matter title rendered as
|
|
5
|
+
* Markdown) into the plain text a reader sees: tags and comments are removed
|
|
6
|
+
* by an HTML parser, and character references are decoded exactly once.
|
|
7
|
+
*/
|
|
8
|
+
export function htmlToPlainText(html: string): string {
|
|
9
|
+
let text = '';
|
|
10
|
+
new HTMLRewriter()
|
|
11
|
+
.onDocument({
|
|
12
|
+
text(chunk) {
|
|
13
|
+
text += chunk.text;
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
.transform(html);
|
|
17
|
+
return decodeHTML(text);
|
|
18
|
+
}
|