@abreen/tada 1.15.8 → 1.15.9
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/build/bundle.ts +55 -26
- package/build/colors.ts +2 -2
- package/build/config-files.ts +11 -21
- package/build/config-loader.ts +1 -1
- package/build/external-links-plugin.ts +1 -0
- package/build/generate-favicon.ts +0 -2
- package/build/generate-web-app-manifest.ts +0 -2
- package/build/pdf-text.ts +2 -2
- package/build/reachability.ts +1 -1
- package/build/site-variables.ts +1 -3
- package/build/template-globals.ts +7 -0
- package/build/templates.ts +1 -1
- package/build/util.ts +1 -22
- package/build/utils/literate-java.ts +1 -1
- package/build/utils/markdown-partials.ts +1 -1
- package/build/utils/shiki-highlighter.ts +1 -4
- package/build/utils/trace-layout.ts +2 -2
- package/build/validate-config-links.ts +51 -1
- package/build/watch/config-paths.ts +1 -1
- package/build/watch/planner.ts +1 -1
- package/build/watch/snapshot.ts +4 -16
- package/package.json +5 -5
- package/schema/authors.schema.json +1 -1
- package/src/navigate/runtime.ts +57 -5
- package/src/page-update/index.ts +1 -1
- package/src/search/index.ts +0 -5
- package/src/search/pdf-utils.ts +1 -1
- package/src/slides/index.ts +1 -1
- package/src/timezone/index.ts +8 -44
- package/src/timezone/time-format.ts +43 -0
- package/src/toc/index.ts +7 -79
- package/src/toc/model.ts +78 -0
- package/templates/_author.html +2 -2
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +1 -1
- package/templates/_nav.html +2 -2
package/build/bundle.ts
CHANGED
|
@@ -19,6 +19,19 @@ import type { SiteVariables } from './types';
|
|
|
19
19
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
20
20
|
import pkg from '../package.json' with { type: 'json' };
|
|
21
21
|
|
|
22
|
+
interface BunBuildPlugin {
|
|
23
|
+
name: string;
|
|
24
|
+
setup(build: PluginBuilder): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CoverageHooks {
|
|
28
|
+
createBundlePlugin(): BunBuildPlugin;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CoverageGlobal {
|
|
32
|
+
__tadaCoverage?: CoverageHooks;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
export function getBundleNaming(): string {
|
|
23
36
|
const version = pkg.version.replace(/[^a-zA-Z0-9.-]/g, '-');
|
|
24
37
|
return `[name].bundle.tada-${version}.[ext]`;
|
|
@@ -92,9 +105,7 @@ function createDefine(
|
|
|
92
105
|
};
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
function createScssPlugin(
|
|
96
|
-
const themeDir = renderThemeScss(siteVariables);
|
|
97
|
-
|
|
108
|
+
function createScssPlugin(themeDir: string) {
|
|
98
109
|
return {
|
|
99
110
|
name: 'scss',
|
|
100
111
|
setup(build: PluginBuilder) {
|
|
@@ -108,6 +119,12 @@ function createScssPlugin(siteVariables: SiteVariables) {
|
|
|
108
119
|
};
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
function getCoverageBundlePlugin(): BunBuildPlugin | null {
|
|
123
|
+
return (
|
|
124
|
+
(globalThis as CoverageGlobal).__tadaCoverage?.createBundlePlugin() ?? null
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
export async function bundle(
|
|
112
129
|
siteVariables: SiteVariables,
|
|
113
130
|
{ mode = 'development', distDir }: { mode?: string; distDir?: string } = {},
|
|
@@ -121,30 +138,42 @@ export async function bundle(
|
|
|
121
138
|
path.resolve(packageDir, 'src/critical.scss'),
|
|
122
139
|
];
|
|
123
140
|
|
|
124
|
-
const
|
|
125
|
-
entrypoints,
|
|
126
|
-
outdir: resolvedDistDir,
|
|
127
|
-
naming: getBundleNaming(),
|
|
128
|
-
minify: mode === 'production',
|
|
129
|
-
sourcemap: isDev ? 'inline' : 'none',
|
|
130
|
-
define: createDefine(siteVariables, isDev),
|
|
131
|
-
external: ['*.woff2'],
|
|
132
|
-
plugins: [createScssPlugin(siteVariables)],
|
|
133
|
-
});
|
|
141
|
+
const themeDir = renderThemeScss(siteVariables);
|
|
134
142
|
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
try {
|
|
144
|
+
const plugins = [createScssPlugin(themeDir)];
|
|
145
|
+
const coverageBundlePlugin = getCoverageBundlePlugin();
|
|
146
|
+
if (coverageBundlePlugin) {
|
|
147
|
+
plugins.push(coverageBundlePlugin);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const result = await Bun.build({
|
|
151
|
+
entrypoints,
|
|
152
|
+
outdir: resolvedDistDir,
|
|
153
|
+
naming: getBundleNaming(),
|
|
154
|
+
minify: mode === 'production',
|
|
155
|
+
sourcemap: isDev ? 'inline' : 'none',
|
|
156
|
+
define: createDefine(siteVariables, isDev),
|
|
157
|
+
external: ['*.woff2'],
|
|
158
|
+
plugins,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!result.success) {
|
|
162
|
+
const messages = result.logs
|
|
163
|
+
.filter(log => log.level === 'error')
|
|
164
|
+
.map(log => log.message || String(log));
|
|
165
|
+
throw new Error(`Bundle failed:\n${messages.join('\n')}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Return the output filenames for asset tag injection
|
|
169
|
+
const assetFiles = result.outputs.map(output =>
|
|
170
|
+
toPosix(path.relative(resolvedDistDir, output.path)),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return assetFiles;
|
|
174
|
+
} finally {
|
|
175
|
+
fs.rmSync(themeDir, { recursive: true, force: true });
|
|
140
176
|
}
|
|
141
|
-
|
|
142
|
-
// Return the output filenames for asset tag injection
|
|
143
|
-
const assetFiles = result.outputs.map(output =>
|
|
144
|
-
toPosix(path.relative(resolvedDistDir, output.path)),
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
return assetFiles;
|
|
148
177
|
}
|
|
149
178
|
|
|
150
|
-
export { renderThemeScss
|
|
179
|
+
export { renderThemeScss };
|
package/build/colors.ts
CHANGED
|
@@ -12,11 +12,11 @@ export const R = style(['red']);
|
|
|
12
12
|
export const G = style(['green']);
|
|
13
13
|
export const B = style(['blue']);
|
|
14
14
|
export const Y = style(['yellow']);
|
|
15
|
-
export const L = style(['
|
|
15
|
+
export const L = style(['gray']);
|
|
16
16
|
export const P = style(['magenta']);
|
|
17
17
|
export const I = style(['italic', 'bold']);
|
|
18
18
|
|
|
19
19
|
export const Ri = style(['inverse', 'red']);
|
|
20
20
|
export const Gi = style(['inverse', 'green']);
|
|
21
21
|
export const Yi = style(['inverse', 'yellow']);
|
|
22
|
-
export const Li = style(['inverse', '
|
|
22
|
+
export const Li = style(['inverse', 'gray']);
|
package/build/config-files.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type SiteEnv = 'dev' | 'prod';
|
|
|
5
5
|
export type SiteConfigName = 'site.dev' | 'site.prod';
|
|
6
6
|
export type ProjectConfigName = 'nav' | 'authors';
|
|
7
7
|
export type ConfigName = SiteConfigName | ProjectConfigName;
|
|
8
|
-
|
|
8
|
+
type ProjectConfigExtension = '.yaml' | '.yml' | '.json';
|
|
9
9
|
|
|
10
10
|
export interface ResolvedProjectConfigFile<
|
|
11
11
|
Name extends ConfigName = ConfigName,
|
|
@@ -18,21 +18,21 @@ export interface ResolvedProjectConfigFile<
|
|
|
18
18
|
required: boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const CONFIG_FILE_EXTENSIONS = ['.yaml', '.yml', '.json'] as const;
|
|
22
|
+
const DEFAULT_CONFIG_FILE_EXTENSION = '.yaml' as const;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const SITE_CONFIG_BASE_NAMES = {
|
|
25
25
|
dev: 'site.dev',
|
|
26
26
|
prod: 'site.prod',
|
|
27
27
|
} as const satisfies Record<SiteEnv, SiteConfigName>;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const PROJECT_CONFIG_BASE_NAMES = {
|
|
30
30
|
nav: 'nav',
|
|
31
31
|
authors: 'authors',
|
|
32
32
|
} as const satisfies Record<ProjectConfigName, ProjectConfigName>;
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const REQUIRED_PROJECT_CONFIG_NAMES = ['nav'] as const;
|
|
35
|
+
const OPTIONAL_PROJECT_CONFIG_NAMES = ['authors'] as const;
|
|
36
36
|
export const PROJECT_CONFIG_NAMES = [
|
|
37
37
|
...REQUIRED_PROJECT_CONFIG_NAMES,
|
|
38
38
|
...OPTIONAL_PROJECT_CONFIG_NAMES,
|
|
@@ -66,7 +66,7 @@ export function getProjectConfigBaseName(
|
|
|
66
66
|
return PROJECT_CONFIG_BASE_NAMES[name];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
function getSupportedConfigFileNames(baseName: string): string[] {
|
|
70
70
|
return CONFIG_FILE_EXTENSIONS.map(ext => `${baseName}${ext}`);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -83,21 +83,17 @@ export function getSupportedConfigFileNamesText(baseName: string): string {
|
|
|
83
83
|
return joinWithOr(getSupportedConfigFileNames(baseName));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
function getDefaultConfigFileName(baseName: string): string {
|
|
87
87
|
return `${baseName}${DEFAULT_CONFIG_FILE_EXTENSION}`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
function getDefaultConfigFilePath(
|
|
91
91
|
projectDir: string,
|
|
92
92
|
baseName: string,
|
|
93
93
|
): string {
|
|
94
94
|
return path.join(projectDir, getDefaultConfigFileName(baseName));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
export function getDefaultSiteConfigFileName(env: SiteEnv): string {
|
|
98
|
-
return getDefaultConfigFileName(getSiteConfigBaseName(env));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
97
|
export function getDefaultSiteConfigPath(
|
|
102
98
|
projectDir: string,
|
|
103
99
|
env: SiteEnv,
|
|
@@ -105,12 +101,6 @@ export function getDefaultSiteConfigPath(
|
|
|
105
101
|
return getDefaultConfigFilePath(projectDir, getSiteConfigBaseName(env));
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
export function getDefaultProjectConfigFileName(
|
|
109
|
-
name: ProjectConfigName,
|
|
110
|
-
): string {
|
|
111
|
-
return getDefaultConfigFileName(getProjectConfigBaseName(name));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
104
|
export function getDefaultProjectConfigFilePath(
|
|
115
105
|
projectDir: string,
|
|
116
106
|
name: ProjectConfigName,
|
|
@@ -118,7 +108,7 @@ export function getDefaultProjectConfigFilePath(
|
|
|
118
108
|
return getDefaultConfigFilePath(projectDir, getProjectConfigBaseName(name));
|
|
119
109
|
}
|
|
120
110
|
|
|
121
|
-
|
|
111
|
+
function resolveConfigFile<Name extends ConfigName = ConfigName>(
|
|
122
112
|
projectDir: string,
|
|
123
113
|
{
|
|
124
114
|
name,
|
package/build/config-loader.ts
CHANGED
|
@@ -175,6 +175,7 @@ export default function externalLinks(
|
|
|
175
175
|
const newClassAttr = classAttr ? `${classAttr} external` : 'external';
|
|
176
176
|
token.attrSet('class', newClassAttr);
|
|
177
177
|
token.attrSet('target', '_blank');
|
|
178
|
+
token.attrSet('rel', 'noopener noreferrer');
|
|
178
179
|
log.debug`${href} -> "${newClassAttr}"`;
|
|
179
180
|
|
|
180
181
|
const closeIdx = findMatchingClose(children, i);
|
package/build/pdf-text.ts
CHANGED
|
@@ -8,12 +8,12 @@ interface MutoolOutput {
|
|
|
8
8
|
stderr: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface PdfPage {
|
|
12
12
|
pageNumber: number;
|
|
13
13
|
content: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface PdfExtractResult {
|
|
17
17
|
pages: PdfPage[];
|
|
18
18
|
hasExtractedText: boolean;
|
|
19
19
|
}
|
package/build/reachability.ts
CHANGED
|
@@ -37,7 +37,7 @@ function resolveTargetToAssetTarget(target: string): string | null {
|
|
|
37
37
|
return path.posix.extname(normalizedTarget) ? normalizedTarget : null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
function collectOutgoingHtmlAssetPaths(
|
|
41
41
|
analysis: HtmlOutputAnalysis,
|
|
42
42
|
knownHtmlAssetPaths: Set<string>,
|
|
43
43
|
): string[] {
|
package/build/site-variables.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bundledLanguages } from 'shiki';
|
|
2
2
|
import type { BundledLanguage } from 'shiki';
|
|
3
|
-
import {
|
|
3
|
+
import type { SiteEnv } from './config-files';
|
|
4
4
|
import { loadSiteConfig } from './config-loader';
|
|
5
5
|
import { compile as compileJsonSchema, doValidation } from './json-schema';
|
|
6
6
|
import { getProjectDir } from './utils/paths';
|
|
@@ -163,8 +163,6 @@ export function getProdSiteVariables(): SiteVariables {
|
|
|
163
163
|
return getSiteVariables('prod');
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
export { getSiteConfigBaseName };
|
|
167
|
-
|
|
168
166
|
function getValidatedSiteConfigRoot(
|
|
169
167
|
value: unknown,
|
|
170
168
|
fileName: string,
|
|
@@ -23,6 +23,7 @@ interface TemplateGlobals {
|
|
|
23
23
|
readableDate: (date: string | Date | null | undefined) => string;
|
|
24
24
|
classNames: (obj: Record<string, unknown>) => string;
|
|
25
25
|
cx: (obj: Record<string, unknown>) => string;
|
|
26
|
+
encodeAuthoredUrl: (value: string) => string;
|
|
26
27
|
renderTimeZoneChooser: () => string;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -51,10 +52,16 @@ export default function createTemplateGlobals(
|
|
|
51
52
|
readableDate,
|
|
52
53
|
classNames,
|
|
53
54
|
cx: classNames,
|
|
55
|
+
encodeAuthoredUrl,
|
|
54
56
|
renderTimeZoneChooser,
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
// Encode raw authored URL text, but preserve percent escapes authors already wrote.
|
|
61
|
+
export function encodeAuthoredUrl(value: string): string {
|
|
62
|
+
return encodeURI(value).replace(/%25([0-9a-fA-F]{2})/g, '%$1');
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
function isoDate(str: string | null | undefined): string | null {
|
|
59
66
|
if (str == null || str == '') {
|
|
60
67
|
return null;
|
package/build/templates.ts
CHANGED
package/build/util.ts
CHANGED
|
@@ -1,37 +1,16 @@
|
|
|
1
|
-
export {
|
|
2
|
-
addGeneratedRouteAliases,
|
|
3
|
-
getProcessedExts,
|
|
4
|
-
getSourceOutputPaths,
|
|
5
|
-
getSourceTargetPaths,
|
|
6
|
-
isBuildContentSource,
|
|
7
|
-
} from './source-model';
|
|
8
|
-
export {
|
|
9
|
-
getBuildContentFiles,
|
|
10
|
-
getContentFiles,
|
|
11
|
-
getContentOutputRelPaths,
|
|
12
|
-
getContentSourceOutputRelPaths,
|
|
13
|
-
getFilesByExtensions,
|
|
14
|
-
getValidInternalTargets,
|
|
15
|
-
shouldSkipContentFile,
|
|
16
|
-
} from './utils/content-files';
|
|
17
|
-
export { extensionIsMarkdown, isPartial } from './utils/file-types';
|
|
18
|
-
export { createMarkdown } from './utils/markdown';
|
|
1
|
+
export { getFilesByExtensions } from './utils/content-files';
|
|
19
2
|
export {
|
|
20
3
|
createApplyBasePath,
|
|
21
4
|
getContentDir,
|
|
22
5
|
getDistDir,
|
|
23
6
|
getPackageDir,
|
|
24
|
-
getProjectDir,
|
|
25
7
|
getPublicDir,
|
|
26
8
|
normalizeOutputPath,
|
|
27
9
|
toPosix,
|
|
28
10
|
} from './utils/paths';
|
|
29
11
|
export {
|
|
30
|
-
injectAssetTags,
|
|
31
|
-
preparePageTemplateHtml,
|
|
32
12
|
renderCodePageAsset,
|
|
33
13
|
renderCopiedContentAsset,
|
|
34
14
|
renderLiterateJavaPageAsset,
|
|
35
15
|
renderPlainTextPageAsset,
|
|
36
16
|
} from './utils/render';
|
|
37
|
-
export { parseFrontMatter } from './utils/front-matter';
|
|
@@ -32,7 +32,7 @@ export function runJavac(
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function getExecError(err: unknown): string {
|
|
36
36
|
const execErr = err as { stderr?: Buffer; message: string };
|
|
37
37
|
return execErr.stderr ? execErr.stderr.toString() : execErr.message;
|
|
38
38
|
}
|
|
@@ -23,7 +23,7 @@ interface MarkdownPartialsEnv {
|
|
|
23
23
|
[PARTIAL_CONTEXT_STACK_KEY]?: MarkdownPartialContext[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface MarkdownPartialsOptions {
|
|
27
27
|
filePath: string;
|
|
28
28
|
templateParams: Record<string, unknown>;
|
|
29
29
|
dependencyCollector?: RenderDependencyCollector;
|
|
@@ -64,10 +64,7 @@ export async function initHighlighter(
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
BundledLanguage,
|
|
69
|
-
BundledTheme
|
|
70
|
-
> {
|
|
67
|
+
function getHighlighter(): HighlighterGeneric<BundledLanguage, BundledTheme> {
|
|
71
68
|
if (!highlighter) {
|
|
72
69
|
throw new Error('Shiki highlighter not initialized');
|
|
73
70
|
}
|
|
@@ -33,7 +33,7 @@ const MIN_VALUE_WIDTH = 40;
|
|
|
33
33
|
const MIN_ARRAY_CELL_WIDTH = 48;
|
|
34
34
|
|
|
35
35
|
/** Information collected about a heap object across all trace steps. */
|
|
36
|
-
|
|
36
|
+
interface ObjectInfo {
|
|
37
37
|
id: string;
|
|
38
38
|
type: string;
|
|
39
39
|
firstStep: number;
|
|
@@ -61,7 +61,7 @@ export interface ObjectInfo {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** A tree node used to build the hierarchy for d3-flextree. */
|
|
64
|
-
|
|
64
|
+
interface TreeNode {
|
|
65
65
|
id: string;
|
|
66
66
|
width: number;
|
|
67
67
|
height: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import { encodeAuthoredUrl } from './template-globals';
|
|
2
3
|
import { normalizeOutputPath } from './utils/paths';
|
|
3
4
|
|
|
4
5
|
interface NavLink {
|
|
@@ -24,6 +25,51 @@ function requireRootRelativePath(
|
|
|
24
25
|
return `${errorPrefix} must start with "/": "${value}"`;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
const SPECIAL_URL_SCHEMES = new Set([
|
|
29
|
+
'file',
|
|
30
|
+
'ftp',
|
|
31
|
+
'http',
|
|
32
|
+
'https',
|
|
33
|
+
'ws',
|
|
34
|
+
'wss',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function isValidAbsoluteAuthorUrl(value: string): boolean {
|
|
38
|
+
if (value.trim() !== value) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const schemeMatch = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
|
|
43
|
+
if (!schemeMatch) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
48
|
+
if (
|
|
49
|
+
SPECIAL_URL_SCHEMES.has(scheme) &&
|
|
50
|
+
!value.startsWith(`${schemeMatch[1]}://`)
|
|
51
|
+
) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
new URL(encodeAuthoredUrl(value));
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getInternalHrefTarget(href: string): string {
|
|
64
|
+
const { pathname } = splitHref(href);
|
|
65
|
+
const normalized = normalizeOutputPath(pathname);
|
|
66
|
+
try {
|
|
67
|
+
return normalizeOutputPath(decodeURIComponent(normalized));
|
|
68
|
+
} catch {
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
27
73
|
export function validateNavLinks(
|
|
28
74
|
navData: unknown,
|
|
29
75
|
validTargets: Set<string>,
|
|
@@ -97,6 +143,10 @@ export function validateAuthorLinks(
|
|
|
97
143
|
}
|
|
98
144
|
|
|
99
145
|
if (author.url) {
|
|
146
|
+
if (isValidAbsoluteAuthorUrl(author.url)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
100
150
|
const urlRootRelativeError = requireRootRelativePath(
|
|
101
151
|
author.url,
|
|
102
152
|
`${fileName}: url for "${key}"`,
|
|
@@ -104,7 +154,7 @@ export function validateAuthorLinks(
|
|
|
104
154
|
if (urlRootRelativeError) {
|
|
105
155
|
errors.push(urlRootRelativeError);
|
|
106
156
|
} else {
|
|
107
|
-
const urlPath =
|
|
157
|
+
const urlPath = getInternalHrefTarget(author.url);
|
|
108
158
|
if (!validTargets.has(urlPath)) {
|
|
109
159
|
errors.push(`${fileName}: broken url for "${key}": "${author.url}"`);
|
|
110
160
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from '../config-files';
|
|
7
7
|
import { getProjectConfigDir } from '../templates';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
type WatchConfigPathKind = 'site' | 'nav' | 'authors';
|
|
10
10
|
|
|
11
11
|
function getConfigPathEntries(): Array<{
|
|
12
12
|
kind: WatchConfigPathKind;
|
package/build/watch/planner.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface TadaIncrementalWatchPlan {
|
|
|
14
14
|
publicToRemove: Set<string>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
type TadaWatchPlan = { kind: 'full' } | TadaIncrementalWatchPlan;
|
|
18
18
|
|
|
19
19
|
export function diffAuthorKeys(previous: unknown, next: unknown): Set<string> {
|
|
20
20
|
const previousMap =
|
package/build/watch/snapshot.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { normalizeOutputPath } from '../util';
|
|
4
3
|
import {
|
|
5
4
|
type TadaProjectScan,
|
|
6
5
|
assertNoOutputPathConflicts,
|
|
7
|
-
scanProject,
|
|
8
|
-
updateProjectScan,
|
|
9
6
|
} from '../source-model';
|
|
10
7
|
import type { Asset, HtmlOutputAnalysis, SiteVariables } from '../types';
|
|
11
8
|
import type { TadaBuildMeta } from './compiler-types';
|
|
@@ -60,7 +57,7 @@ function buildReverseMap(
|
|
|
60
57
|
return reverse;
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
|
|
60
|
+
function collectOutputOwners(snapshot: {
|
|
64
61
|
contentRecords: Map<string, TadaSourceRecord>;
|
|
65
62
|
publicRecords: Map<string, TadaSourceRecord>;
|
|
66
63
|
}): Map<string, TadaOutputOwner> {
|
|
@@ -164,7 +161,7 @@ export function collectSourceHtmlAnalysis(
|
|
|
164
161
|
return htmlAnalysisByOutputPath;
|
|
165
162
|
}
|
|
166
163
|
|
|
167
|
-
|
|
164
|
+
function collectHtmlAssetsByPath(
|
|
168
165
|
contentRecords: Map<string, TadaSourceRecord>,
|
|
169
166
|
): Map<string, string> {
|
|
170
167
|
const htmlAssetsByPath = new Map<string, string>();
|
|
@@ -178,7 +175,7 @@ export function collectHtmlAssetsByPath(
|
|
|
178
175
|
return htmlAssetsByPath;
|
|
179
176
|
}
|
|
180
177
|
|
|
181
|
-
|
|
178
|
+
function collectHtmlAnalysisByPath(
|
|
182
179
|
contentRecords: Map<string, TadaSourceRecord>,
|
|
183
180
|
): Map<string, HtmlOutputAnalysis> {
|
|
184
181
|
const htmlAnalysisByPath = new Map<string, HtmlOutputAnalysis>();
|
|
@@ -201,13 +198,4 @@ export function createBuildMeta(snapshot: TadaSnapshot): TadaBuildMeta {
|
|
|
201
198
|
};
|
|
202
199
|
}
|
|
203
200
|
|
|
204
|
-
export
|
|
205
|
-
return normalizeOutputPath(target);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export {
|
|
209
|
-
assertNoOutputPathConflicts,
|
|
210
|
-
scanProject,
|
|
211
|
-
updateProjectScan,
|
|
212
|
-
type TadaProjectScan,
|
|
213
|
-
};
|
|
201
|
+
export { assertNoOutputPathConflicts, type TadaProjectScan };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abreen/tada",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A static site generator",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"check-deps": "bun run scripts/check-deps.ts",
|
|
34
34
|
"init-example": "bun run bin/tada.ts init example --no-interactive",
|
|
35
35
|
"iew": "bun run scripts/init-example-watch.ts",
|
|
36
|
-
"lint": "eslint src/ build/ bin/",
|
|
36
|
+
"lint": "eslint src/ build/ bin/ templates/*.test.ts",
|
|
37
37
|
"lint:sass": "bun run scripts/lint-sass.ts",
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
39
|
"format": "prettier --write \"**/*.{ts,js,scss,json}\"",
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"chokidar": "^4.0.3",
|
|
62
62
|
"culori": "^4.0.2",
|
|
63
63
|
"d3-flextree": "^2.1.2",
|
|
64
|
-
"d3-hierarchy": "^3.1.2",
|
|
65
64
|
"fontkit": "^2.0.4",
|
|
66
65
|
"front-matter": "^4.0.2",
|
|
67
66
|
"java-parser": "^3.0.1",
|
|
@@ -101,7 +100,6 @@
|
|
|
101
100
|
"@types/bun": "^1.3.12",
|
|
102
101
|
"@types/culori": "^4.0.1",
|
|
103
102
|
"@types/d3-flextree": "^2.1.4",
|
|
104
|
-
"@types/d3-hierarchy": "^3.1.7",
|
|
105
103
|
"@types/fontkit": "^2.0.9",
|
|
106
104
|
"@types/istanbul-lib-coverage": "^2.0.6",
|
|
107
105
|
"@types/istanbul-lib-instrument": "^1.7.8",
|
|
@@ -109,12 +107,14 @@
|
|
|
109
107
|
"@types/istanbul-reports": "^3.0.4",
|
|
110
108
|
"@types/lodash": "^4.17.24",
|
|
111
109
|
"@types/markdown-it-container": "^4.0.0",
|
|
110
|
+
"@types/node": "^25.6.0",
|
|
112
111
|
"eslint": "^10.2.1",
|
|
113
112
|
"husky": "^9.1.7",
|
|
114
113
|
"istanbul-lib-coverage": "^3.2.2",
|
|
115
114
|
"istanbul-lib-instrument": "^6.0.3",
|
|
116
115
|
"istanbul-lib-report": "^3.0.1",
|
|
117
116
|
"istanbul-reports": "^3.2.0",
|
|
117
|
+
"knip": "^6.11.0",
|
|
118
118
|
"lint-staged": "^16.4.0",
|
|
119
119
|
"postcss-scss": "^4.0.9",
|
|
120
120
|
"prettier": "^3.8.3",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"stylelint": "^17.8.0",
|
|
123
123
|
"stylelint-config-recess-order": "^7.7.0",
|
|
124
124
|
"stylelint-config-standard-scss": "^17.0.0",
|
|
125
|
-
"typescript": "^
|
|
125
|
+
"typescript": "^6.0.3",
|
|
126
126
|
"typescript-eslint": "^8.58.2"
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"properties": {
|
|
7
7
|
"name": { "type": "string" },
|
|
8
8
|
"avatar": { "type": "string", "pattern": "^\\/[-a-zA-Z0-9_./#]*$" },
|
|
9
|
-
"url": { "type": "string", "
|
|
9
|
+
"url": { "type": "string", "minLength": 1 }
|
|
10
10
|
},
|
|
11
11
|
"additionalProperties": false
|
|
12
12
|
}
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -130,6 +130,14 @@ function getLocationKey(window: Window): string {
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function isAbortError(err: unknown): boolean {
|
|
134
|
+
return err instanceof Error && err.name === 'AbortError';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isActiveNavigation(controller: AbortController): boolean {
|
|
138
|
+
return currentAbortController === controller && !controller.signal.aborted;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
export function initNavigation(window: Window): void {
|
|
134
142
|
currentAbortController = null;
|
|
135
143
|
historyIndex = 0;
|
|
@@ -191,6 +199,8 @@ export async function navigateToUrl(
|
|
|
191
199
|
|
|
192
200
|
const controller = new AbortController();
|
|
193
201
|
currentAbortController = controller;
|
|
202
|
+
document.documentElement.classList.remove('nav-forward', 'nav-back');
|
|
203
|
+
cleanupViewTransitionNames(document);
|
|
194
204
|
|
|
195
205
|
const header = document.querySelector('header');
|
|
196
206
|
header?.classList.add('loading');
|
|
@@ -199,21 +209,46 @@ export async function navigateToUrl(
|
|
|
199
209
|
try {
|
|
200
210
|
response = await globals.fetch(url, { signal: controller.signal });
|
|
201
211
|
} catch (err: unknown) {
|
|
212
|
+
if (!isActiveNavigation(controller)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
202
215
|
header?.classList.remove('loading');
|
|
203
|
-
if (err
|
|
216
|
+
if (isAbortError(err)) {
|
|
204
217
|
return;
|
|
205
218
|
}
|
|
206
219
|
globals.setLocationHref(window, url);
|
|
207
220
|
return;
|
|
208
221
|
}
|
|
209
222
|
|
|
223
|
+
if (!isActiveNavigation(controller)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
if (!response.ok) {
|
|
211
228
|
header?.classList.remove('loading');
|
|
212
229
|
globals.setLocationHref(window, url);
|
|
213
230
|
return;
|
|
214
231
|
}
|
|
215
232
|
|
|
216
|
-
|
|
233
|
+
let html: string;
|
|
234
|
+
try {
|
|
235
|
+
html = await response.text();
|
|
236
|
+
} catch (err: unknown) {
|
|
237
|
+
if (!isActiveNavigation(controller)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
header?.classList.remove('loading');
|
|
241
|
+
if (isAbortError(err)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
globals.setLocationHref(window, url);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!isActiveNavigation(controller)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
217
252
|
header?.classList.remove('loading');
|
|
218
253
|
|
|
219
254
|
const DOMParserCtor = (window as unknown as { DOMParser: typeof DOMParser })
|
|
@@ -233,7 +268,11 @@ export async function navigateToUrl(
|
|
|
233
268
|
|
|
234
269
|
const parsed = new URL(url);
|
|
235
270
|
|
|
236
|
-
const doSwap = () => {
|
|
271
|
+
const doSwap = (): boolean => {
|
|
272
|
+
if (!isActiveNavigation(controller)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
237
276
|
teardownPerPageComponents();
|
|
238
277
|
swapContent(document, newDoc);
|
|
239
278
|
updateHead(document, newDoc);
|
|
@@ -257,6 +296,7 @@ export async function navigateToUrl(
|
|
|
257
296
|
|
|
258
297
|
scrollByIndex.set(historyIndex, window.scrollY);
|
|
259
298
|
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
299
|
+
return true;
|
|
260
300
|
};
|
|
261
301
|
|
|
262
302
|
const transitionDoc = document as Document & {
|
|
@@ -295,7 +335,9 @@ export async function navigateToUrl(
|
|
|
295
335
|
);
|
|
296
336
|
|
|
297
337
|
const transition = transitionDoc.startViewTransition(() => {
|
|
298
|
-
doSwap()
|
|
338
|
+
if (!doSwap()) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
299
341
|
|
|
300
342
|
const newTitleEl = document.querySelector('.title-and-info');
|
|
301
343
|
const newHeaderHeight =
|
|
@@ -323,13 +365,23 @@ export async function navigateToUrl(
|
|
|
323
365
|
});
|
|
324
366
|
|
|
325
367
|
await transition.finished;
|
|
368
|
+
if (!isActiveNavigation(controller)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
326
372
|
document.documentElement.classList.remove('nav-forward', 'nav-back');
|
|
327
373
|
cleanupViewTransitionNames(document);
|
|
328
374
|
} else {
|
|
329
|
-
doSwap()
|
|
375
|
+
if (!doSwap()) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
330
378
|
}
|
|
331
379
|
|
|
332
380
|
await mountPerPageComponents(window);
|
|
381
|
+
if (!isActiveNavigation(controller)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
333
385
|
currentAbortController = null;
|
|
334
386
|
dispatchNavigationEvent(window);
|
|
335
387
|
}
|
package/src/page-update/index.ts
CHANGED
package/src/search/index.ts
CHANGED
|
@@ -40,11 +40,6 @@ interface Pagefind {
|
|
|
40
40
|
|
|
41
41
|
let pagefind: Pagefind | null = null;
|
|
42
42
|
|
|
43
|
-
// Unit tests need to clear the module-scoped Pagefind instance between cases.
|
|
44
|
-
export function resetPagefindForTest() {
|
|
45
|
-
pagefind = null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
43
|
type SubResult = { title: string; url: string; excerpt: string };
|
|
49
44
|
|
|
50
45
|
type State = {
|
package/src/search/pdf-utils.ts
CHANGED
package/src/slides/index.ts
CHANGED
package/src/timezone/index.ts
CHANGED
|
@@ -1,50 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PERIOD_STYLE,
|
|
3
|
+
detectPeriodStyle,
|
|
4
|
+
normalizeHM,
|
|
5
|
+
to12Hour,
|
|
6
|
+
type PeriodStyle,
|
|
7
|
+
} from './time-format';
|
|
8
|
+
|
|
1
9
|
const STORAGE_KEY = 'timezoneSelection';
|
|
2
10
|
const TIMEZONE_SELECT_LABEL = 'Time zone';
|
|
3
11
|
|
|
4
|
-
type PeriodStyle = [am: string, pm: string];
|
|
5
|
-
|
|
6
|
-
const DEFAULT_PERIOD_STYLE: PeriodStyle = ['a.m.', 'p.m.'];
|
|
7
|
-
|
|
8
|
-
const PERIOD_PATTERNS: { pattern: RegExp; style: PeriodStyle }[] = [
|
|
9
|
-
{ pattern: /[AP]\.M\./, style: ['A.M.', 'P.M.'] },
|
|
10
|
-
{ pattern: /[AP]M/, style: ['AM', 'PM'] },
|
|
11
|
-
{ pattern: /[ap]\.m\./, style: ['a.m.', 'p.m.'] },
|
|
12
|
-
{ pattern: /[ap]m/, style: ['am', 'pm'] },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export function detectPeriodStyle(text: string): PeriodStyle | null {
|
|
16
|
-
for (const { pattern, style } of PERIOD_PATTERNS) {
|
|
17
|
-
if (pattern.test(text)) {
|
|
18
|
-
return style;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function pad(n: number | string) {
|
|
25
|
-
return String(n).padStart(2, '0');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function to12Hour(
|
|
29
|
-
h: number,
|
|
30
|
-
m: number,
|
|
31
|
-
style: PeriodStyle | null = DEFAULT_PERIOD_STYLE,
|
|
32
|
-
) {
|
|
33
|
-
const hour12 = ((h + 11) % 12) + 1;
|
|
34
|
-
if (!style) {
|
|
35
|
-
return `${hour12}:${pad(m)}`;
|
|
36
|
-
}
|
|
37
|
-
const period = h >= 12 ? style[1] : style[0];
|
|
38
|
-
return `${hour12}:${pad(m)} ${period}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function normalizeHM(totalMinutes: number) {
|
|
42
|
-
const mins = ((totalMinutes % 1440) + 1440) % 1440;
|
|
43
|
-
const h = Math.floor(mins / 60);
|
|
44
|
-
const m = mins % 60;
|
|
45
|
-
return [h, m] as const;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
12
|
function getDayShift(totalMinutes: number) {
|
|
49
13
|
// Negative partial days like -30 minutes should still count as the previous day.
|
|
50
14
|
return Math.floor(totalMinutes / 1440);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type PeriodStyle = [am: string, pm: string];
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PERIOD_STYLE: PeriodStyle = ['a.m.', 'p.m.'];
|
|
4
|
+
|
|
5
|
+
const PERIOD_PATTERNS: { pattern: RegExp; style: PeriodStyle }[] = [
|
|
6
|
+
{ pattern: /[AP]\.M\./, style: ['A.M.', 'P.M.'] },
|
|
7
|
+
{ pattern: /[AP]M/, style: ['AM', 'PM'] },
|
|
8
|
+
{ pattern: /[ap]\.m\./, style: ['a.m.', 'p.m.'] },
|
|
9
|
+
{ pattern: /[ap]m/, style: ['am', 'pm'] },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function detectPeriodStyle(text: string): PeriodStyle | null {
|
|
13
|
+
for (const { pattern, style } of PERIOD_PATTERNS) {
|
|
14
|
+
if (pattern.test(text)) {
|
|
15
|
+
return style;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pad(n: number | string) {
|
|
22
|
+
return String(n).padStart(2, '0');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function to12Hour(
|
|
26
|
+
h: number,
|
|
27
|
+
m: number,
|
|
28
|
+
style: PeriodStyle | null = DEFAULT_PERIOD_STYLE,
|
|
29
|
+
) {
|
|
30
|
+
const hour12 = ((h + 11) % 12) + 1;
|
|
31
|
+
if (!style) {
|
|
32
|
+
return `${hour12}:${pad(m)}`;
|
|
33
|
+
}
|
|
34
|
+
const period = h >= 12 ? style[1] : style[0];
|
|
35
|
+
return `${hour12}:${pad(m)} ${period}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeHM(totalMinutes: number) {
|
|
39
|
+
const mins = ((totalMinutes % 1440) + 1440) % 1440;
|
|
40
|
+
const h = Math.floor(mins / 60);
|
|
41
|
+
const m = mins % 60;
|
|
42
|
+
return [h, m] as const;
|
|
43
|
+
}
|
package/src/toc/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { debounce } from '../util';
|
|
2
|
+
import {
|
|
3
|
+
alertToTableItem,
|
|
4
|
+
getHighlightIndexes,
|
|
5
|
+
headingToTableItem,
|
|
6
|
+
switchCurrent,
|
|
7
|
+
type Dinkus,
|
|
8
|
+
} from './model';
|
|
2
9
|
|
|
3
10
|
const LATENCY_MS = 50;
|
|
4
11
|
|
|
5
|
-
export type HeadingLevel = '1' | '2' | '3' | '4' | '5' | '6';
|
|
6
|
-
export type AlertType = 'warning' | 'note';
|
|
7
|
-
|
|
8
|
-
export type Alert = { type: AlertType; title: string };
|
|
9
|
-
export type Heading = { level: HeadingLevel; innerHtml: string; id: string };
|
|
10
|
-
export type Dinkus = { type: 'dinkus' };
|
|
11
|
-
|
|
12
12
|
function getContainer(parent: HTMLElement): HTMLElement | null {
|
|
13
13
|
return parent.querySelector('nav.toc');
|
|
14
14
|
}
|
|
@@ -40,27 +40,6 @@ function scrollIfNeeded(element: HTMLElement, doc: Document) {
|
|
|
40
40
|
container.scrollTo({ top: desiredScrollTop });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function getHighlightIndexes(items: (Heading | Alert | Dinkus)[]) {
|
|
44
|
-
const indexes: (number | null)[] = [];
|
|
45
|
-
let currentHeadingIndex: number | null = null;
|
|
46
|
-
let tocIndex = 0;
|
|
47
|
-
|
|
48
|
-
items.forEach(item => {
|
|
49
|
-
if (!('level' in item) && item.type === 'dinkus') {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if ('level' in item) {
|
|
54
|
-
currentHeadingIndex = tocIndex;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
indexes.push(currentHeadingIndex ?? tocIndex);
|
|
58
|
-
tocIndex++;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return indexes;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
43
|
function getHeadingsAndAlerts(
|
|
65
44
|
parent: HTMLElement,
|
|
66
45
|
): (HTMLHeadingElement | HTMLDivElement)[] {
|
|
@@ -97,57 +76,6 @@ function getViewportActivationPoint(win: Window, doc: Document) {
|
|
|
97
76
|
return headerOffset + (win.innerHeight - headerOffset) / 3;
|
|
98
77
|
}
|
|
99
78
|
|
|
100
|
-
export function headingToTableItem(el: HTMLHeadingElement): Heading {
|
|
101
|
-
const level = el.tagName[1] as HeadingLevel;
|
|
102
|
-
|
|
103
|
-
const subtitle = el.querySelector('.heading-subtitle');
|
|
104
|
-
const subtitleText = subtitle?.textContent || '';
|
|
105
|
-
let mainText = el.textContent || '';
|
|
106
|
-
|
|
107
|
-
if (mainText.length > 0 && subtitleText.length > 0) {
|
|
108
|
-
mainText = mainText.replace(subtitleText, '').trim();
|
|
109
|
-
return {
|
|
110
|
-
level,
|
|
111
|
-
id: el.id,
|
|
112
|
-
innerHtml: `${mainText}: <span class="heading-subtitle">${subtitleText}</span>`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return { level, id: el.id, innerHtml: el.innerHTML };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function alertToTableItem(el: HTMLElement): Alert | null {
|
|
119
|
-
const classes = el.className
|
|
120
|
-
.split(' ')
|
|
121
|
-
.map(cl => cl.trim())
|
|
122
|
-
.filter(cl => cl != 'alert');
|
|
123
|
-
|
|
124
|
-
const firstClass = classes[0];
|
|
125
|
-
if (firstClass === 'warning' || firstClass === 'note') {
|
|
126
|
-
let title = el.querySelector('.title')?.innerHTML;
|
|
127
|
-
if (!title) {
|
|
128
|
-
if (firstClass === 'warning') {
|
|
129
|
-
title = 'Warning';
|
|
130
|
-
} else {
|
|
131
|
-
title = 'Note';
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return { type: firstClass, title };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function switchCurrent(
|
|
142
|
-
oldCurrent: HTMLElement | null,
|
|
143
|
-
newCurrent: HTMLElement,
|
|
144
|
-
) {
|
|
145
|
-
if (oldCurrent) {
|
|
146
|
-
oldCurrent.classList.remove('current');
|
|
147
|
-
}
|
|
148
|
-
newCurrent.classList.add('current');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
79
|
export default (window: Window) => {
|
|
152
80
|
const toc = window.document.querySelector('nav.toc') as HTMLElement;
|
|
153
81
|
if (toc == null) {
|
package/src/toc/model.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
type HeadingLevel = '1' | '2' | '3' | '4' | '5' | '6';
|
|
2
|
+
type AlertType = 'warning' | 'note';
|
|
3
|
+
|
|
4
|
+
export type Alert = { type: AlertType; title: string };
|
|
5
|
+
export type Heading = { level: HeadingLevel; innerHtml: string; id: string };
|
|
6
|
+
export type Dinkus = { type: 'dinkus' };
|
|
7
|
+
|
|
8
|
+
export function getHighlightIndexes(items: (Heading | Alert | Dinkus)[]) {
|
|
9
|
+
const indexes: (number | null)[] = [];
|
|
10
|
+
let currentHeadingIndex: number | null = null;
|
|
11
|
+
let tocIndex = 0;
|
|
12
|
+
|
|
13
|
+
items.forEach(item => {
|
|
14
|
+
if (!('level' in item) && item.type === 'dinkus') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if ('level' in item) {
|
|
19
|
+
currentHeadingIndex = tocIndex;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
indexes.push(currentHeadingIndex ?? tocIndex);
|
|
23
|
+
tocIndex++;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return indexes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function headingToTableItem(el: HTMLHeadingElement): Heading {
|
|
30
|
+
const level = el.tagName[1] as HeadingLevel;
|
|
31
|
+
|
|
32
|
+
const subtitle = el.querySelector('.heading-subtitle');
|
|
33
|
+
const subtitleText = subtitle?.textContent || '';
|
|
34
|
+
let mainText = el.textContent || '';
|
|
35
|
+
|
|
36
|
+
if (mainText.length > 0 && subtitleText.length > 0) {
|
|
37
|
+
mainText = mainText.replace(subtitleText, '').trim();
|
|
38
|
+
return {
|
|
39
|
+
level,
|
|
40
|
+
id: el.id,
|
|
41
|
+
innerHtml: `${mainText}: <span class="heading-subtitle">${subtitleText}</span>`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { level, id: el.id, innerHtml: el.innerHTML };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function alertToTableItem(el: HTMLElement): Alert | null {
|
|
48
|
+
const classes = el.className
|
|
49
|
+
.split(' ')
|
|
50
|
+
.map(cl => cl.trim())
|
|
51
|
+
.filter(cl => cl != 'alert');
|
|
52
|
+
|
|
53
|
+
const firstClass = classes[0];
|
|
54
|
+
if (firstClass === 'warning' || firstClass === 'note') {
|
|
55
|
+
let title = el.querySelector('.title')?.innerHTML;
|
|
56
|
+
if (!title) {
|
|
57
|
+
if (firstClass === 'warning') {
|
|
58
|
+
title = 'Warning';
|
|
59
|
+
} else {
|
|
60
|
+
title = 'Note';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { type: firstClass, title };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function switchCurrent(
|
|
71
|
+
oldCurrent: HTMLElement | null,
|
|
72
|
+
newCurrent: HTMLElement,
|
|
73
|
+
) {
|
|
74
|
+
if (oldCurrent) {
|
|
75
|
+
oldCurrent.classList.remove('current');
|
|
76
|
+
}
|
|
77
|
+
newCurrent.classList.add('current');
|
|
78
|
+
}
|
package/templates/_author.html
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<aside class="author">
|
|
2
2
|
Written by
|
|
3
3
|
<% if (page.author.url) { %>
|
|
4
|
-
<a href="
|
|
4
|
+
<a href="<%- encodeAuthoredUrl(page.author.url) %>">
|
|
5
5
|
<% } %>
|
|
6
6
|
<span class="text"><%= page.author.name %></span>
|
|
7
7
|
<% if (page.author.avatar) { %>
|
|
8
8
|
<img
|
|
9
9
|
width="32"
|
|
10
10
|
height="32"
|
|
11
|
-
src="
|
|
11
|
+
src="<%- encodeAuthoredUrl(page.author.avatar) %>"
|
|
12
12
|
alt="Picture of <%= page.author.name %>"
|
|
13
13
|
class="avatar"
|
|
14
14
|
/>
|
package/templates/_footer.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% if (site.features.footer) { %>
|
|
2
2
|
<footer data-pagefind-ignore>
|
|
3
|
-
<p>Built using <a href="https://github.com/abreen/Tada" target="_blank">Tada</a> <%= tadaVersion %></p>
|
|
3
|
+
<p>Built using <a href="https://github.com/abreen/Tada" target="_blank" rel="noopener noreferrer">Tada</a> <%= tadaVersion %></p>
|
|
4
4
|
</footer>
|
|
5
5
|
<% } %>
|
package/templates/_heading.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="title-and-info" data-pagefind-body>
|
|
2
2
|
<% if (page.parent) { %>
|
|
3
|
-
<a class="breadcrumb" href="
|
|
3
|
+
<a class="breadcrumb" href="<%- encodeAuthoredUrl(page.parent) %>" data-pagefind-ignore><%= page.parentLabel %></a>
|
|
4
4
|
<% } %>
|
|
5
5
|
<h1 data-pagefind-weight="10" data-pagefind-meta="title_html[data-title-html]" data-title-html="<%- page.titleHtml %>"><%= page.titleHtml %></h1>
|
|
6
6
|
|
package/templates/_nav.html
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<li>
|
|
8
8
|
<a
|
|
9
9
|
class="<%= cx({disabled: link.disabled, external: link.external}) %>"
|
|
10
|
-
<% if (!link.disabled) { %>href="
|
|
11
|
-
<% if (link.external) { %>target="_blank"<% } %>
|
|
10
|
+
<% if (!link.disabled) { %>href="<%- encodeAuthoredUrl(link.internal || link.external) %>"<% } %>
|
|
11
|
+
<% if (link.external) { %>target="_blank" rel="noopener noreferrer"<% } %>
|
|
12
12
|
>
|
|
13
13
|
<%= link.text %></a>
|
|
14
14
|
</li>
|