@abreen/tada 1.5.8 → 1.6.2
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 +3 -0
- package/build/build-manifest.ts +2 -1
- package/build/bundle.ts +18 -6
- package/build/copy.ts +4 -9
- package/build/external-links-plugin.ts +181 -22
- package/build/generate-content-assets.ts +2 -4
- package/build/utils/content-files.ts +3 -9
- package/build/utils/derive-theme.ts +8 -0
- package/build/utils/paths.ts +16 -1
- package/build/utils/render.ts +31 -12
- package/build/utils/trace.ts +2 -5
- package/build/validate-internal-links-plugin.ts +12 -1
- package/build/watch-reload-client.ts +1 -0
- package/build/watch.ts +7 -24
- package/package.json +8 -7
- package/src/_alerts.scss +11 -6
- package/src/_base.scss +7 -10
- package/src/_layout.scss +23 -1
- package/src/_mixins.scss +41 -0
- package/src/anchor/style.scss +17 -24
- package/src/header/_base.scss +8 -0
- package/src/header/style.scss +1 -18
- package/src/index.ts +12 -0
- package/src/navigate/index.ts +47 -29
- package/src/print/style.scss +1 -1
- package/src/style.scss +18 -47
- package/src/toc/index.ts +1 -1
- package/src/toc/style.scss +4 -33
- package/src/top/index.ts +6 -2
- package/templates/_theme.scss +11 -24
- package/templates/_top.html +2 -0
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Tada :tada:
|
|
2
2
|
|
|
3
|
+
[](https://github.com/abreen/Tada/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@abreen/tada)
|
|
5
|
+
|
|
3
6
|
A static site generator. The successor to Presto.
|
|
4
7
|
|
|
5
8
|
## Features
|
package/build/build-manifest.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { toPosix } from './utils/paths';
|
|
3
4
|
|
|
4
5
|
const EXCLUDED_DIRS = new Set(['pagefind']);
|
|
5
6
|
const EXCLUDED_FILES = new Set(['tada.manifest.json']);
|
|
@@ -68,7 +69,7 @@ export async function walkAndHash(
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
const abs = path.join(entry.parentPath, entry.name);
|
|
71
|
-
const rel = path.relative(dir, abs)
|
|
72
|
+
const rel = toPosix(path.relative(dir, abs));
|
|
72
73
|
|
|
73
74
|
const topLevel = rel.split('/')[0];
|
|
74
75
|
if (EXCLUDED_DIRS.has(topLevel)) {
|
package/build/bundle.ts
CHANGED
|
@@ -3,8 +3,13 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import _ from 'lodash';
|
|
5
5
|
import * as sass from 'sass';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
getPackageDir,
|
|
8
|
+
getProjectDir,
|
|
9
|
+
getDistDir,
|
|
10
|
+
toPosix,
|
|
11
|
+
} from './utils/paths';
|
|
12
|
+
import { deriveTheme, deriveLinkHue } from './utils/derive-theme';
|
|
8
13
|
import type { PluginBuilder } from 'bun';
|
|
9
14
|
import type { SiteVariables } from './types';
|
|
10
15
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
@@ -22,6 +27,12 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
22
27
|
const iconColorHoverDark = `hsl(${tintHue}deg ${(6 * tintAmount) / 100}% 60%)`;
|
|
23
28
|
const iconColorTranslucentDark = `hsl(${tintHue}deg ${(85 * tintAmount) / 100}% 90%)`;
|
|
24
29
|
|
|
30
|
+
const linkHue = deriveLinkHue(tintHue);
|
|
31
|
+
const linkColor = `hsl(${linkHue}deg 44.4% 49.4%)`;
|
|
32
|
+
const linkColorHover = `hsl(${linkHue}deg 34% 60%)`;
|
|
33
|
+
const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
|
|
34
|
+
const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
|
|
35
|
+
|
|
25
36
|
const rendered = _.template(template)({
|
|
26
37
|
...theme,
|
|
27
38
|
tintHue,
|
|
@@ -31,6 +42,10 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
31
42
|
iconColorDark,
|
|
32
43
|
iconColorHoverDark,
|
|
33
44
|
iconColorTranslucentDark,
|
|
45
|
+
linkColor,
|
|
46
|
+
linkColorHover,
|
|
47
|
+
linkColorDark,
|
|
48
|
+
linkColorHoverDark,
|
|
34
49
|
});
|
|
35
50
|
|
|
36
51
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
|
|
@@ -104,10 +119,7 @@ export async function bundle(
|
|
|
104
119
|
|
|
105
120
|
// Return the output filenames for asset tag injection
|
|
106
121
|
const assetFiles = result.outputs.map(output =>
|
|
107
|
-
path
|
|
108
|
-
.relative(resolvedDistDir, output.path)
|
|
109
|
-
.split(path.sep)
|
|
110
|
-
.join(path.posix.sep),
|
|
122
|
+
toPosix(path.relative(resolvedDistDir, output.path)),
|
|
111
123
|
);
|
|
112
124
|
|
|
113
125
|
return assetFiles;
|
package/build/copy.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { toPosix } from './utils/paths';
|
|
3
4
|
import { makeLogger } from './log';
|
|
4
5
|
import { B } from './colors';
|
|
5
6
|
|
|
@@ -22,7 +23,7 @@ function collectFiles(dir: string): CollectedFile[] {
|
|
|
22
23
|
.map(entry => {
|
|
23
24
|
const abs = path.join(entry.parentPath, entry.name);
|
|
24
25
|
const rel = path.relative(dir, abs);
|
|
25
|
-
return { abs, rel: rel
|
|
26
|
+
return { abs, rel: toPosix(rel) };
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -83,10 +84,7 @@ export function copyPublicFile(
|
|
|
83
84
|
filePath: string,
|
|
84
85
|
contentAssetRelPaths?: Set<string>,
|
|
85
86
|
): void {
|
|
86
|
-
const rel = path
|
|
87
|
-
.relative(publicDir, filePath)
|
|
88
|
-
.split(path.sep)
|
|
89
|
-
.join(path.posix.sep);
|
|
87
|
+
const rel = toPosix(path.relative(publicDir, filePath));
|
|
90
88
|
if (contentAssetRelPaths && contentAssetRelPaths.has(rel)) {
|
|
91
89
|
log.error`public/${B`${rel}`} conflicts with content/${B`${rel}`}`;
|
|
92
90
|
throw new Error(`public/${rel} and content/${rel} have the same path`);
|
|
@@ -103,10 +101,7 @@ export function copyContentFile(
|
|
|
103
101
|
filePath: string,
|
|
104
102
|
publicRelPaths?: Set<string>,
|
|
105
103
|
): void {
|
|
106
|
-
const rel = path
|
|
107
|
-
.relative(contentDir, filePath)
|
|
108
|
-
.split(path.sep)
|
|
109
|
-
.join(path.posix.sep);
|
|
104
|
+
const rel = toPosix(path.relative(contentDir, filePath));
|
|
110
105
|
if (publicRelPaths && publicRelPaths.has(rel)) {
|
|
111
106
|
log.error`content/${B`${rel}`} conflicts with public/${B`${rel}`}`;
|
|
112
107
|
throw new Error(`content/${rel} and public/${rel} have the same path`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type MarkdownIt from 'markdown-it';
|
|
2
|
-
import
|
|
2
|
+
import Token from 'markdown-it/lib/token.mjs';
|
|
3
3
|
import { makeLogger } from './log';
|
|
4
4
|
import type { SiteVariables } from './types';
|
|
5
5
|
|
|
@@ -9,38 +9,197 @@ export default function externalLinks(
|
|
|
9
9
|
md: MarkdownIt,
|
|
10
10
|
siteVariables: SiteVariables,
|
|
11
11
|
): void {
|
|
12
|
-
function
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
function isExternal(href: string): boolean {
|
|
13
|
+
if (!href.match(/^https?:\/\/.*$/)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const url = new URL(href);
|
|
17
|
+
return !siteVariables.internalDomains?.includes(url.host);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findMatchingClose(children: Token[], openIdx: number): number {
|
|
21
|
+
let depth = 1;
|
|
22
|
+
for (let i = openIdx + 1; i < children.length; i++) {
|
|
23
|
+
if (children[i].type === 'link_open') {
|
|
24
|
+
depth++;
|
|
25
|
+
} else if (children[i].type === 'link_close') {
|
|
26
|
+
depth--;
|
|
27
|
+
if (depth === 0) {
|
|
28
|
+
return i;
|
|
29
|
+
}
|
|
17
30
|
}
|
|
31
|
+
}
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeSpanOpen(): Token {
|
|
36
|
+
const t = new Token('html_inline', '', 0);
|
|
37
|
+
t.content = '<span class="external-link-tail">';
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
function makeSpanClose(): Token {
|
|
42
|
+
const t = new Token('html_inline', '', 0);
|
|
43
|
+
t.content = '</span>';
|
|
44
|
+
return t;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Wrap the trailing word of an external link in
|
|
48
|
+
// <span class="external-link-tail"> so the icon (added via that span's
|
|
49
|
+
// ::after) stays glued to the last word and doesn't wrap on its own.
|
|
50
|
+
// The span is inserted inside any inline-formatting elements (em,
|
|
51
|
+
// strong, code, ...) that are still open at the split point, so the
|
|
52
|
+
// resulting markup stays nested.
|
|
53
|
+
function wrapTail(linkChildren: Token[]): Token[] | null {
|
|
54
|
+
if (linkChildren.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
24
57
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
58
|
+
// Find the rightmost split char (space or hyphen) in a text token,
|
|
59
|
+
// skipping any candidate that would produce an empty tail (e.g. a
|
|
60
|
+
// trailing hyphen with no content after it).
|
|
61
|
+
let splitTokenIdx = -1;
|
|
62
|
+
let splitCharIdx = -1;
|
|
63
|
+
for (let i = linkChildren.length - 1; i >= 0; i--) {
|
|
64
|
+
const tok = linkChildren[i];
|
|
65
|
+
if (tok.type !== 'text') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const text = tok.content;
|
|
69
|
+
const hasLaterTokens = i < linkChildren.length - 1;
|
|
70
|
+
let foundIdx = -1;
|
|
71
|
+
for (let j = text.length - 1; j >= 0; j--) {
|
|
72
|
+
const c = text[j];
|
|
73
|
+
if (c !== ' ' && c !== '-') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (j < text.length - 1 || hasLaterTokens) {
|
|
77
|
+
foundIdx = j;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (foundIdx !== -1) {
|
|
82
|
+
splitTokenIdx = i;
|
|
83
|
+
splitCharIdx = foundIdx;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
30
87
|
|
|
31
|
-
|
|
32
|
-
|
|
88
|
+
if (splitTokenIdx === -1) {
|
|
89
|
+
// No space anywhere in the link's text. Wrap the whole content.
|
|
90
|
+
return [makeSpanOpen(), ...linkChildren, makeSpanClose()];
|
|
91
|
+
}
|
|
33
92
|
|
|
34
|
-
|
|
35
|
-
|
|
93
|
+
// Find where to put the span close: walk forward from the split
|
|
94
|
+
// point tracking nesting depth (locally opened elements after the
|
|
95
|
+
// split). The span must close before any -1 token that would close
|
|
96
|
+
// an element that was already open at the split point.
|
|
97
|
+
let spanCloseIdx = linkChildren.length;
|
|
98
|
+
let depth = 0;
|
|
99
|
+
for (let i = splitTokenIdx + 1; i < linkChildren.length; i++) {
|
|
100
|
+
const tok = linkChildren[i];
|
|
101
|
+
if (tok.nesting === 1) {
|
|
102
|
+
depth++;
|
|
103
|
+
} else if (tok.nesting === -1) {
|
|
104
|
+
if (depth > 0) {
|
|
105
|
+
depth--;
|
|
106
|
+
} else {
|
|
107
|
+
spanCloseIdx = i;
|
|
108
|
+
break;
|
|
36
109
|
}
|
|
37
110
|
}
|
|
38
111
|
}
|
|
39
112
|
|
|
40
|
-
|
|
113
|
+
const splitToken = linkChildren[splitTokenIdx];
|
|
114
|
+
const before = splitToken.content.slice(0, splitCharIdx + 1);
|
|
115
|
+
const after = splitToken.content.slice(splitCharIdx + 1);
|
|
116
|
+
|
|
117
|
+
const result: Token[] = [];
|
|
118
|
+
|
|
119
|
+
// Tokens before the split, unchanged.
|
|
120
|
+
for (let i = 0; i < splitTokenIdx; i++) {
|
|
121
|
+
result.push(linkChildren[i]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The "before" part of the split text token.
|
|
125
|
+
if (before.length > 0) {
|
|
126
|
+
const beforeToken = new Token('text', '', 0);
|
|
127
|
+
beforeToken.content = before;
|
|
128
|
+
result.push(beforeToken);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
result.push(makeSpanOpen());
|
|
132
|
+
|
|
133
|
+
// The "after" part of the split text token.
|
|
134
|
+
if (after.length > 0) {
|
|
135
|
+
const afterToken = new Token('text', '', 0);
|
|
136
|
+
afterToken.content = after;
|
|
137
|
+
result.push(afterToken);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Tokens between the split and the span close.
|
|
141
|
+
for (let i = splitTokenIdx + 1; i < spanCloseIdx; i++) {
|
|
142
|
+
result.push(linkChildren[i]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
result.push(makeSpanClose());
|
|
146
|
+
|
|
147
|
+
// Tokens from the span close position to the end.
|
|
148
|
+
for (let i = spanCloseIdx; i < linkChildren.length; i++) {
|
|
149
|
+
result.push(linkChildren[i]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function processChildren(children: Token[]): Token[] {
|
|
156
|
+
const result: Token[] = [];
|
|
157
|
+
let i = 0;
|
|
158
|
+
while (i < children.length) {
|
|
159
|
+
const token = children[i];
|
|
160
|
+
|
|
161
|
+
if (token.type !== 'link_open') {
|
|
162
|
+
result.push(token);
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const href = token.attrGet('href');
|
|
168
|
+
if (!href || !isExternal(href)) {
|
|
169
|
+
result.push(token);
|
|
170
|
+
i++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const classAttr = token.attrGet('class');
|
|
175
|
+
const newClassAttr = classAttr ? `${classAttr} external` : 'external';
|
|
176
|
+
token.attrSet('class', newClassAttr);
|
|
177
|
+
token.attrSet('target', '_blank');
|
|
178
|
+
log.debug`${href} -> "${newClassAttr}"`;
|
|
179
|
+
|
|
180
|
+
const closeIdx = findMatchingClose(children, i);
|
|
181
|
+
if (closeIdx === -1) {
|
|
182
|
+
result.push(token);
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const linkContent = children.slice(i + 1, closeIdx);
|
|
188
|
+
const wrapped = wrapTail(linkContent);
|
|
189
|
+
|
|
190
|
+
result.push(token);
|
|
191
|
+
result.push(...(wrapped ?? linkContent));
|
|
192
|
+
result.push(children[closeIdx]);
|
|
193
|
+
i = closeIdx + 1;
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
41
196
|
}
|
|
42
197
|
|
|
43
198
|
md.core.ruler.push('external_links', state => {
|
|
44
|
-
state.tokens.
|
|
199
|
+
state.tokens.forEach(token => {
|
|
200
|
+
if (token.children) {
|
|
201
|
+
token.children = processChildren(token.children);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
45
204
|
});
|
|
46
205
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from './util';
|
|
17
17
|
import { isLiterateJava } from './utils/file-types';
|
|
18
18
|
import { checkJavac } from './utils/literate-java';
|
|
19
|
+
import { toPosix } from './utils/paths';
|
|
19
20
|
import type {
|
|
20
21
|
SiteVariables,
|
|
21
22
|
Asset,
|
|
@@ -291,10 +292,7 @@ export class ContentRenderer {
|
|
|
291
292
|
for (const filePath of buildContentFiles) {
|
|
292
293
|
if (isLiterateJava(filePath)) {
|
|
293
294
|
const parsed = path.parse(path.relative(contentDir, filePath));
|
|
294
|
-
const javaPath = path
|
|
295
|
-
.join(parsed.dir, parsed.name)
|
|
296
|
-
.split(path.sep)
|
|
297
|
-
.join(path.posix.sep);
|
|
295
|
+
const javaPath = toPosix(path.join(parsed.dir, parsed.name));
|
|
298
296
|
literateJavaOutputPaths.add(`/${javaPath}`);
|
|
299
297
|
}
|
|
300
298
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
isLiterateJava,
|
|
8
8
|
isPartial,
|
|
9
9
|
} from './file-types';
|
|
10
|
-
import { getPublicDir, normalizeOutputPath } from './paths';
|
|
10
|
+
import { getPublicDir, normalizeOutputPath, toPosix } from './paths';
|
|
11
11
|
|
|
12
12
|
function walkFiles(dir: string): string[] {
|
|
13
13
|
return fs.readdirSync(dir).flatMap(file => {
|
|
@@ -111,18 +111,12 @@ export function getValidInternalTargets(
|
|
|
111
111
|
const relPath = path.relative(contentDir, filePath);
|
|
112
112
|
const parsed = path.parse(relPath);
|
|
113
113
|
const ext = parsed.ext.toLowerCase();
|
|
114
|
-
const subPath = path
|
|
115
|
-
.join(parsed.dir, parsed.name)
|
|
116
|
-
.split(path.sep)
|
|
117
|
-
.join(path.posix.sep);
|
|
114
|
+
const subPath = toPosix(path.join(parsed.dir, parsed.name));
|
|
118
115
|
|
|
119
116
|
if (isLiterateJava(filePath)) {
|
|
120
117
|
// parsed.name is e.g. "VowelCounter.java"; the output page is
|
|
121
118
|
// VowelCounter.java.html and the source file is VowelCounter.java
|
|
122
|
-
const javaSubPath = path
|
|
123
|
-
.join(parsed.dir, parsed.name)
|
|
124
|
-
.split(path.sep)
|
|
125
|
-
.join(path.posix.sep);
|
|
119
|
+
const javaSubPath = toPosix(path.join(parsed.dir, parsed.name));
|
|
126
120
|
addGeneratedRouteAliases(targets, `/${javaSubPath}.html`);
|
|
127
121
|
targets.add(normalizeOutputPath(`/${javaSubPath}`));
|
|
128
122
|
} else if (extensionIsMarkdown(ext) || ext === '.html') {
|
|
@@ -14,6 +14,9 @@ const LIGHT_TEXT_L_MAX = 0.5;
|
|
|
14
14
|
const DARK_TEXT_L_MIN = 0.7;
|
|
15
15
|
const DARK_TEXT_L_MAX = 0.82;
|
|
16
16
|
|
|
17
|
+
const LINK_ANCHOR_HUE = 212;
|
|
18
|
+
const LINK_TINT_BLEND = 0.05;
|
|
19
|
+
|
|
17
20
|
function clamp(v: number, min: number, max: number): number {
|
|
18
21
|
return Math.min(Math.max(v, min), max);
|
|
19
22
|
}
|
|
@@ -75,6 +78,11 @@ export function deriveTheme(cssColor: string): DerivedTheme {
|
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
export function deriveLinkHue(tintHue: number): number {
|
|
82
|
+
const diff = ((tintHue - LINK_ANCHOR_HUE + 540) % 360) - 180;
|
|
83
|
+
return (LINK_ANCHOR_HUE + diff * LINK_TINT_BLEND + 360) % 360;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
export function getTextOnColor(cssColor: string): '#fff' | '#000' {
|
|
79
87
|
const parsed = parse(cssColor);
|
|
80
88
|
if (!parsed) {
|
package/build/utils/paths.ts
CHANGED
|
@@ -45,8 +45,23 @@ export function createApplyBasePath(
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function toPosix(p: string): string {
|
|
49
|
+
return p.split(path.sep).join(path.posix.sep);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert an OS-native relative filesystem path to a URL path: convert
|
|
54
|
+
* separators to '/', then percent-encode each segment so characters that
|
|
55
|
+
* are unsafe in URLs (space, ?, #, non-ASCII, etc.) are encoded. Use this
|
|
56
|
+
* anywhere a filesystem-derived path is emitted as an href or looked up
|
|
57
|
+
* against a URL-encoded href.
|
|
58
|
+
*/
|
|
59
|
+
export function toUrlPath(p: string): string {
|
|
60
|
+
return toPosix(p).split('/').map(encodeURIComponent).join('/');
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
export function normalizeOutputPath(outputPath: string): string {
|
|
49
|
-
const normalized = path.posix.normalize(outputPath);
|
|
64
|
+
const normalized = path.posix.normalize(toPosix(outputPath));
|
|
50
65
|
if (normalized === '.' || normalized === '') {
|
|
51
66
|
return '/';
|
|
52
67
|
}
|
package/build/utils/render.ts
CHANGED
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
import { extensionIsMarkdown } from './file-types';
|
|
18
18
|
import { createTraceHelpers } from './trace';
|
|
19
19
|
import { createIncludeFunction } from './include';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createApplyBasePath,
|
|
22
|
+
normalizeOutputPath,
|
|
23
|
+
toPosix,
|
|
24
|
+
toUrlPath,
|
|
25
|
+
} from './paths';
|
|
21
26
|
import { parseFrontMatterAndContent } from './front-matter';
|
|
22
27
|
import { createMarkdown } from './markdown';
|
|
23
28
|
import { generateTocHtml, generateCodeTocHtml } from '../toc-plugin';
|
|
@@ -41,12 +46,17 @@ const log = makeLogger(import.meta.url);
|
|
|
41
46
|
|
|
42
47
|
const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
|
|
43
48
|
|
|
49
|
+
function isWatchMode(assetFiles: string[]): boolean {
|
|
50
|
+
return assetFiles.some(f => f.includes('watch-reload-client'));
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
interface TemplateParametersInput {
|
|
45
54
|
pageVariables: Record<string, unknown>;
|
|
46
55
|
siteVariables: SiteVariables;
|
|
47
56
|
content: string | null;
|
|
48
57
|
applyBasePath: (subPath: string) => string;
|
|
49
58
|
subPath: string;
|
|
59
|
+
isWatchMode: boolean;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
function resolveAuthor(
|
|
@@ -96,6 +106,7 @@ function createTemplateParameters({
|
|
|
96
106
|
content,
|
|
97
107
|
applyBasePath,
|
|
98
108
|
subPath,
|
|
109
|
+
isWatchMode,
|
|
99
110
|
}: TemplateParametersInput): Record<string, unknown> {
|
|
100
111
|
return {
|
|
101
112
|
vars: siteVariables.vars || {},
|
|
@@ -106,6 +117,7 @@ function createTemplateParameters({
|
|
|
106
117
|
page: pageVariables,
|
|
107
118
|
content,
|
|
108
119
|
applyBasePath,
|
|
120
|
+
isWatchMode,
|
|
109
121
|
};
|
|
110
122
|
}
|
|
111
123
|
|
|
@@ -164,10 +176,7 @@ export function injectKatexStylesheet(
|
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
function toContentAssetPath(contentDir: string, filePath: string): string {
|
|
167
|
-
return path
|
|
168
|
-
.relative(contentDir, filePath)
|
|
169
|
-
.split(path.sep)
|
|
170
|
-
.join(path.posix.sep);
|
|
179
|
+
return toPosix(path.relative(contentDir, filePath));
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
export function renderPlainTextPageAsset({
|
|
@@ -182,16 +191,18 @@ export function renderPlainTextPageAsset({
|
|
|
182
191
|
javacAvailable,
|
|
183
192
|
}: RenderPlainTextOptions): Asset[] {
|
|
184
193
|
const { dir, name, ext } = path.parse(filePath);
|
|
185
|
-
const subPath = path.relative(contentDir, path.join(dir, name));
|
|
194
|
+
const subPath = toPosix(path.relative(contentDir, path.join(dir, name)));
|
|
186
195
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
187
196
|
|
|
188
197
|
log.info`Rendering page ${B`${subPath + ext}`}`;
|
|
198
|
+
const watchMode = isWatchMode(assetFiles);
|
|
189
199
|
const { content, pageVariables, tocItems, alertIds } = renderPlainTextContent(
|
|
190
200
|
filePath,
|
|
191
201
|
subPath,
|
|
192
202
|
siteVariables,
|
|
193
203
|
applyBasePath,
|
|
194
204
|
validInternalTargets,
|
|
205
|
+
watchMode,
|
|
195
206
|
{
|
|
196
207
|
validateInternalLinks: extensionIsMarkdown(ext.toLowerCase()),
|
|
197
208
|
traceCache,
|
|
@@ -221,6 +232,7 @@ export function renderPlainTextPageAsset({
|
|
|
221
232
|
content,
|
|
222
233
|
applyBasePath,
|
|
223
234
|
subPath,
|
|
235
|
+
isWatchMode: watchMode,
|
|
224
236
|
});
|
|
225
237
|
|
|
226
238
|
const templateHtml = render(
|
|
@@ -248,11 +260,11 @@ export function renderCodePageAsset({
|
|
|
248
260
|
assetFiles,
|
|
249
261
|
}: RenderCodePageOptions): Asset[] {
|
|
250
262
|
const { dir, name, ext } = path.parse(filePath);
|
|
251
|
-
const subPath = path.relative(contentDir, path.join(dir, name));
|
|
263
|
+
const subPath = toPosix(path.relative(contentDir, path.join(dir, name)));
|
|
252
264
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
253
265
|
const lang = siteVariables.codeLanguages![ext.slice(1).toLowerCase()];
|
|
254
266
|
const sourceCode = fs.readFileSync(filePath, 'utf-8');
|
|
255
|
-
const pageDirPath = path.relative(contentDir, dir);
|
|
267
|
+
const pageDirPath = toPosix(path.relative(contentDir, dir));
|
|
256
268
|
|
|
257
269
|
log.info`Rendering code page ${B`${subPath + ext}`}`;
|
|
258
270
|
const content = renderCodeWithComments(
|
|
@@ -262,7 +274,7 @@ export function renderCodePageAsset({
|
|
|
262
274
|
pageDirPath,
|
|
263
275
|
);
|
|
264
276
|
const codeFilePath = applyBasePath(
|
|
265
|
-
normalizeOutputPath(`/${
|
|
277
|
+
normalizeOutputPath(`/${toUrlPath(path.relative(contentDir, filePath))}`),
|
|
266
278
|
);
|
|
267
279
|
const titleHtml = `<tt>${name + ext}</tt>`;
|
|
268
280
|
const tocItems = lang === 'java' ? extractJavaMethodToc(sourceCode) : [];
|
|
@@ -284,6 +296,7 @@ export function renderCodePageAsset({
|
|
|
284
296
|
content,
|
|
285
297
|
applyBasePath,
|
|
286
298
|
subPath,
|
|
299
|
+
isWatchMode: isWatchMode(assetFiles),
|
|
287
300
|
});
|
|
288
301
|
|
|
289
302
|
const html = injectAssetTags(
|
|
@@ -316,7 +329,9 @@ export function renderCopiedContentAsset({
|
|
|
316
329
|
|
|
317
330
|
if (filePath.endsWith('.java') && isFeatureEnabled(siteVariables, 'code')) {
|
|
318
331
|
const sourceCode = fs.readFileSync(filePath, 'utf-8');
|
|
319
|
-
const pageDirPath =
|
|
332
|
+
const pageDirPath = toPosix(
|
|
333
|
+
path.relative(contentDir, path.dirname(filePath)),
|
|
334
|
+
);
|
|
320
335
|
const rewrittenLines = rewriteProseLinks(
|
|
321
336
|
sourceCode.split('\n'),
|
|
322
337
|
siteVariables,
|
|
@@ -335,6 +350,7 @@ function renderPlainTextContent(
|
|
|
335
350
|
siteVariables: SiteVariables,
|
|
336
351
|
applyBasePath: (subPath: string) => string,
|
|
337
352
|
validInternalTargets: Set<string>,
|
|
353
|
+
isWatchMode: boolean,
|
|
338
354
|
{
|
|
339
355
|
validateInternalLinks = true,
|
|
340
356
|
traceCache,
|
|
@@ -392,6 +408,7 @@ function renderPlainTextContent(
|
|
|
392
408
|
content: null,
|
|
393
409
|
applyBasePath,
|
|
394
410
|
subPath,
|
|
411
|
+
isWatchMode,
|
|
395
412
|
});
|
|
396
413
|
const pageVariablesProcessed: Record<string, unknown> = Object.entries(
|
|
397
414
|
pageVariables,
|
|
@@ -442,6 +459,7 @@ function renderPlainTextContent(
|
|
|
442
459
|
content: strippedContent,
|
|
443
460
|
applyBasePath,
|
|
444
461
|
subPath,
|
|
462
|
+
isWatchMode,
|
|
445
463
|
});
|
|
446
464
|
|
|
447
465
|
if (traceCache && contentDir && distDir) {
|
|
@@ -501,7 +519,7 @@ export function renderLiterateJavaPageAsset({
|
|
|
501
519
|
}: RenderLiterateJavaOptions): Asset[] {
|
|
502
520
|
const { dir, name } = path.parse(filePath);
|
|
503
521
|
const className = deriveClassName(filePath);
|
|
504
|
-
const subPath = path.relative(contentDir, path.join(dir, className));
|
|
522
|
+
const subPath = toPosix(path.relative(contentDir, path.join(dir, className)));
|
|
505
523
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
506
524
|
|
|
507
525
|
log.info`Rendering literate Java page ${B`${name}`}`;
|
|
@@ -619,7 +637,7 @@ export function renderLiterateJavaPageAsset({
|
|
|
619
637
|
const javaFileName = `${className}.java`;
|
|
620
638
|
const codeFilePath = applyBasePath(
|
|
621
639
|
normalizeOutputPath(
|
|
622
|
-
`/${
|
|
640
|
+
`/${toUrlPath(path.relative(contentDir, path.join(dir, javaFileName)))}`,
|
|
623
641
|
),
|
|
624
642
|
);
|
|
625
643
|
|
|
@@ -645,6 +663,7 @@ export function renderLiterateJavaPageAsset({
|
|
|
645
663
|
content: contentHtml,
|
|
646
664
|
applyBasePath,
|
|
647
665
|
subPath,
|
|
666
|
+
isWatchMode: isWatchMode(assetFiles),
|
|
648
667
|
});
|
|
649
668
|
|
|
650
669
|
const templateHtml = render('literate.html', templateParameters) as string;
|
package/build/utils/trace.ts
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { makeLogger } from '../log';
|
|
6
6
|
import { B } from '../colors';
|
|
7
|
-
import { normalizeOutputPath } from './paths';
|
|
7
|
+
import { normalizeOutputPath, toPosix } from './paths';
|
|
8
8
|
import { hasMainMethod } from './literate-java';
|
|
9
9
|
import { renderCodeSegment } from './code';
|
|
10
10
|
import { computeLayout } from './trace-layout';
|
|
@@ -324,10 +324,7 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
324
324
|
|
|
325
325
|
const highlightedSource = highlightSource(javaFile, source);
|
|
326
326
|
|
|
327
|
-
const relDir = path
|
|
328
|
-
.relative(contentDir, pageDir)
|
|
329
|
-
.split(path.sep)
|
|
330
|
-
.join(path.posix.sep);
|
|
327
|
+
const relDir = toPosix(path.relative(contentDir, pageDir));
|
|
331
328
|
const traceOutputDir = path.join(distDir, relDir, '_traces', className);
|
|
332
329
|
|
|
333
330
|
const manifest = chunkTraceOutput(
|
|
@@ -69,7 +69,18 @@ function resolveLinkPath(
|
|
|
69
69
|
? normalizePathname(hrefPath)
|
|
70
70
|
: normalizePathname(path.posix.join(sourceDir, hrefPath));
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
// Decode percent-encoded characters so the resolved path can be matched
|
|
73
|
+
// against `validTargets`, which contains raw filesystem-derived paths.
|
|
74
|
+
// markdown-it's normalizeLink percent-encodes hrefs, so a link like
|
|
75
|
+
// `[x](/my notes.md)` arrives here as `/my%20notes.md`.
|
|
76
|
+
let decoded: string;
|
|
77
|
+
try {
|
|
78
|
+
decoded = normalizePathname(decodeURIComponent(resolved));
|
|
79
|
+
} catch {
|
|
80
|
+
decoded = resolved;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return rewriteCodeLink(decoded, codeExtPattern);
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
function getDirectoryIndexPath(pathname: string): string {
|
|
@@ -26,6 +26,7 @@ declare const __WEBSOCKET_PORT__: number;
|
|
|
26
26
|
document.body.classList.add('loading-cursor');
|
|
27
27
|
} else if (event.data === 'reload') {
|
|
28
28
|
console.log('[watch-reload] Reloading page...');
|
|
29
|
+
window.history.scrollRestoration = 'auto';
|
|
29
30
|
window.location.reload();
|
|
30
31
|
}
|
|
31
32
|
};
|