@ts-for-gir/typedoc-theme 4.0.0-beta.41 → 4.0.0-beta.42
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/package.json +4 -2
- package/src/partials/layout.ts +12 -2
- package/src/partials/module-reflection.ts +15 -7
- package/src/partials/sidebar.ts +11 -3
- package/src/search-splitter.ts +163 -0
- package/src/theme.ts +5 -0
- package/src/utils.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ts-for-gir/typedoc-theme",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.42",
|
|
4
4
|
"description": "Custom TypeDoc theme inspired by gi-docgen for ts-for-gir",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -35,11 +35,13 @@
|
|
|
35
35
|
".": "./src/index.ts"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@ts-for-gir/tsconfig": "^4.0.0-beta.
|
|
38
|
+
"@ts-for-gir/tsconfig": "^4.0.0-beta.42",
|
|
39
|
+
"@types/lunr": "^2.3.7",
|
|
39
40
|
"@types/node": "^24.12.0",
|
|
40
41
|
"typescript": "^5.9.3"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
44
|
+
"lunr": "^2.3.9",
|
|
43
45
|
"typedoc": "^0.28.17"
|
|
44
46
|
}
|
|
45
47
|
}
|
package/src/partials/layout.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { PageEvent, Reflection, RenderTemplate } from "typedoc";
|
|
2
2
|
import { JSX } from "typedoc";
|
|
3
3
|
import type { GiDocgenThemeRenderContext } from "../context.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { sanitizeModuleName } from "../search-splitter.ts";
|
|
5
|
+
import { findOwningModule, getDisplayName, getHierarchyRoots } from "../utils.ts";
|
|
6
|
+
|
|
7
|
+
/** Determine which search chunk file to load for this page. */
|
|
8
|
+
function getSearchScript(props: PageEvent<Reflection>): string {
|
|
9
|
+
const owningModule = findOwningModule(props.model);
|
|
10
|
+
if (owningModule) {
|
|
11
|
+
return `assets/search-${sanitizeModuleName(owningModule.name)}.js`;
|
|
12
|
+
}
|
|
13
|
+
return "assets/search-modules.js";
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
export const giDocgenLayout = (
|
|
7
17
|
context: GiDocgenThemeRenderContext,
|
|
@@ -64,7 +74,7 @@ export const giDocgenLayout = (
|
|
|
64
74
|
}),
|
|
65
75
|
JSX.createElement("script", {
|
|
66
76
|
async: true,
|
|
67
|
-
src: context.relativeURL(
|
|
77
|
+
src: context.relativeURL(getSearchScript(props), true),
|
|
68
78
|
id: "tsd-search-script",
|
|
69
79
|
}),
|
|
70
80
|
JSX.createElement("script", {
|
|
@@ -161,11 +161,19 @@ export function giDocgenModuleReflection(
|
|
|
161
161
|
if (mod.isDeclaration() && isCompanionNamespace(mod as DeclarationReflection)) {
|
|
162
162
|
const parent = mod.parent;
|
|
163
163
|
const siblings = parent && "children" in parent ? (parent as DeclarationReflection).children : undefined;
|
|
164
|
-
const
|
|
165
|
-
(child) =>
|
|
164
|
+
const companionOwner = siblings?.find(
|
|
165
|
+
(child) =>
|
|
166
|
+
child !== mod &&
|
|
167
|
+
child.kindOf(ReflectionKind.Class | ReflectionKind.Interface | ReflectionKind.Enum) &&
|
|
168
|
+
child.name === mod.name,
|
|
166
169
|
);
|
|
167
|
-
if (
|
|
168
|
-
const
|
|
170
|
+
if (companionOwner) {
|
|
171
|
+
const ownerUrl = context.urlTo(companionOwner);
|
|
172
|
+
const kindName = companionOwner.kindOf(ReflectionKind.Enum)
|
|
173
|
+
? "enum"
|
|
174
|
+
: companionOwner.kindOf(ReflectionKind.Interface)
|
|
175
|
+
? "interface"
|
|
176
|
+
: "class";
|
|
169
177
|
return JSX.createElement(
|
|
170
178
|
JSX.Fragment,
|
|
171
179
|
null,
|
|
@@ -173,13 +181,13 @@ export function giDocgenModuleReflection(
|
|
|
173
181
|
"p",
|
|
174
182
|
null,
|
|
175
183
|
"This namespace is a companion to the ",
|
|
176
|
-
JSX.createElement("a", { href:
|
|
177
|
-
|
|
184
|
+
JSX.createElement("a", { href: ownerUrl }, companionOwner.name),
|
|
185
|
+
` ${kindName}. See the ${kindName} page for full documentation.`,
|
|
178
186
|
),
|
|
179
187
|
JSX.createElement(
|
|
180
188
|
"script",
|
|
181
189
|
null,
|
|
182
|
-
JSX.createElement(JSX.Raw, { html: `window.location.replace("${
|
|
190
|
+
JSX.createElement(JSX.Raw, { html: `window.location.replace("${ownerUrl}");` }),
|
|
183
191
|
),
|
|
184
192
|
);
|
|
185
193
|
}
|
package/src/partials/sidebar.ts
CHANGED
|
@@ -11,9 +11,17 @@ import {
|
|
|
11
11
|
isCompanionNamespace,
|
|
12
12
|
} from "../utils.ts";
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
declare const __TS_FOR_GIR_VERSION__: string;
|
|
15
|
+
|
|
16
|
+
function getTsForGirVersion(): string {
|
|
17
|
+
if (typeof __TS_FOR_GIR_VERSION__ !== "undefined") {
|
|
18
|
+
return __TS_FOR_GIR_VERSION__;
|
|
19
|
+
}
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
return JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf8")).version as string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TSFOR_GIR_VERSION = getTsForGirVersion();
|
|
17
25
|
|
|
18
26
|
/** Render the module info section (namespace name, versions, dependencies). */
|
|
19
27
|
function giDocgenModuleInfo(
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-processes TypeDoc's monolithic search.js into per-module chunks.
|
|
3
|
+
*
|
|
4
|
+
* TypeDoc generates a single search.js containing all 460k+ entries (16 MB compressed).
|
|
5
|
+
* This module splits it into one file per GIR module so each page only loads the
|
|
6
|
+
* search data it needs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
12
|
+
import lunr from "lunr";
|
|
13
|
+
|
|
14
|
+
// TypeDoc ReflectionKind.Module = 2
|
|
15
|
+
const KIND_MODULE = 2;
|
|
16
|
+
|
|
17
|
+
interface SearchDocument {
|
|
18
|
+
kind: number;
|
|
19
|
+
name: string;
|
|
20
|
+
url: string;
|
|
21
|
+
classes?: string;
|
|
22
|
+
parent?: string;
|
|
23
|
+
icon?: string | number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SearchData {
|
|
27
|
+
rows: SearchDocument[];
|
|
28
|
+
index: object; // serialized lunr index — we don't need to load it
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize a module name for use as a filename.
|
|
33
|
+
* e.g. "Gtk-4.0" → "gtk-4.0", "gjs/cairo" → "gjs_cairo"
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeModuleName(name: string): string {
|
|
36
|
+
return name.toLowerCase().replace(/[/\\]/g, "_");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract the top-level module name from a search row.
|
|
41
|
+
* - Module entries (kind=2): module = row.name
|
|
42
|
+
* - Everything else: match the parent prefix against known module names
|
|
43
|
+
*
|
|
44
|
+
* We can't simply split on "." because module names themselves contain dots
|
|
45
|
+
* (e.g. "AppStream-1.0" has parent "AppStream-1.0.AppStream.Component").
|
|
46
|
+
*/
|
|
47
|
+
function getModuleName(row: SearchDocument, moduleNames: Set<string>): string | undefined {
|
|
48
|
+
if (row.kind === KIND_MODULE) return row.name;
|
|
49
|
+
if (!row.parent) return undefined;
|
|
50
|
+
|
|
51
|
+
// Try to match the parent prefix against known module names
|
|
52
|
+
for (const mod of moduleNames) {
|
|
53
|
+
if (row.parent === mod || row.parent.startsWith(`${mod}.`)) {
|
|
54
|
+
return mod;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build a compressed search chunk (same format as TypeDoc's search.js)
|
|
63
|
+
* from a subset of rows.
|
|
64
|
+
*/
|
|
65
|
+
function buildChunk(rows: SearchDocument[]): string {
|
|
66
|
+
const builder = new lunr.Builder();
|
|
67
|
+
builder.pipeline.add(lunr.trimmer);
|
|
68
|
+
builder.ref("id");
|
|
69
|
+
builder.field("name", { boost: 10 });
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < rows.length; i++) {
|
|
72
|
+
builder.add({ id: i, name: rows[i].name });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const index = builder.build();
|
|
76
|
+
const data = { rows, index };
|
|
77
|
+
const compressed = deflateSync(Buffer.from(JSON.stringify(data)));
|
|
78
|
+
return compressed.toString("base64");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read and decompress TypeDoc's search.js, returning the parsed search data.
|
|
83
|
+
*/
|
|
84
|
+
function readSearchData(assetsDir: string): SearchData {
|
|
85
|
+
const raw = readFileSync(join(assetsDir, "search.js"), "utf-8");
|
|
86
|
+
|
|
87
|
+
// Extract base64 payload from: window.searchData = "...";
|
|
88
|
+
const match = raw.match(/window\.searchData\s*=\s*"([^"]+)"/);
|
|
89
|
+
if (!match?.[1]) {
|
|
90
|
+
throw new Error("Could not extract searchData from search.js");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const decoded = Buffer.from(match[1], "base64");
|
|
94
|
+
const decompressed = inflateSync(decoded);
|
|
95
|
+
return JSON.parse(decompressed.toString("utf-8")) as SearchData;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Split the monolithic search.js into per-module chunks.
|
|
100
|
+
*
|
|
101
|
+
* Produces:
|
|
102
|
+
* - `search-modules.js` — module-level entries only (for the homepage)
|
|
103
|
+
* - `search-{module}.js` — per-module entries (for module pages)
|
|
104
|
+
* - Replaces `search.js` with a null stub
|
|
105
|
+
*/
|
|
106
|
+
export async function splitSearchIndex(outputDir: string): Promise<void> {
|
|
107
|
+
const assetsDir = join(outputDir, "assets");
|
|
108
|
+
|
|
109
|
+
let searchData: SearchData;
|
|
110
|
+
try {
|
|
111
|
+
searchData = readSearchData(assetsDir);
|
|
112
|
+
} catch {
|
|
113
|
+
// No search.js or unparseable — nothing to split
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { rows } = searchData;
|
|
118
|
+
|
|
119
|
+
// Collect known module names first (kind=2 entries)
|
|
120
|
+
const moduleNames = new Set<string>();
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
if (row.kind === KIND_MODULE) {
|
|
123
|
+
moduleNames.add(row.name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Group rows by module
|
|
128
|
+
const moduleMap = new Map<string, SearchDocument[]>();
|
|
129
|
+
const moduleEntries: SearchDocument[] = [];
|
|
130
|
+
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
if (row.kind === KIND_MODULE) {
|
|
133
|
+
moduleEntries.push(row);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const mod = getModuleName(row, moduleNames);
|
|
137
|
+
if (!mod) continue;
|
|
138
|
+
|
|
139
|
+
let bucket = moduleMap.get(mod);
|
|
140
|
+
if (!bucket) {
|
|
141
|
+
bucket = [];
|
|
142
|
+
moduleMap.set(mod, bucket);
|
|
143
|
+
}
|
|
144
|
+
bucket.push(row);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Write per-module chunks
|
|
148
|
+
for (const [moduleName, moduleRows] of moduleMap) {
|
|
149
|
+
const chunk = buildChunk(moduleRows);
|
|
150
|
+
const filename = `search-${sanitizeModuleName(moduleName)}.js`;
|
|
151
|
+
writeFileSync(join(assetsDir, filename), `window.searchData = "${chunk}";`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Write module index for homepage
|
|
155
|
+
const modulesChunk = buildChunk(moduleEntries);
|
|
156
|
+
writeFileSync(join(assetsDir, "search-modules.js"), `window.searchData = "${modulesChunk}";`);
|
|
157
|
+
|
|
158
|
+
// Replace original search.js with a null stub
|
|
159
|
+
writeFileSync(join(assetsDir, "search.js"), "window.searchData = null;");
|
|
160
|
+
|
|
161
|
+
const totalChunks = moduleMap.size + 1; // +1 for modules index
|
|
162
|
+
console.log(`[search-splitter] Split ${rows.length} entries into ${totalChunks} chunks (${moduleMap.size} modules)`);
|
|
163
|
+
}
|
package/src/theme.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import type { NavigationElement, ProjectReflection, Renderer } from "typedoc";
|
|
5
5
|
import { DefaultTheme, RendererEvent } from "typedoc";
|
|
6
6
|
import { GiDocgenThemeRenderContext } from "./context.ts";
|
|
7
|
+
import { splitSearchIndex } from "./search-splitter.ts";
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
@@ -79,4 +80,8 @@ export class GiDocgenTheme extends DefaultTheme {
|
|
|
79
80
|
override getNavigation(project: ProjectReflection): NavigationElement[] {
|
|
80
81
|
return buildShallowNavigation(super.getNavigation(project));
|
|
81
82
|
}
|
|
83
|
+
|
|
84
|
+
override async postRender(event: RendererEvent): Promise<void> {
|
|
85
|
+
await splitSearchIndex(event.outputDirectory);
|
|
86
|
+
}
|
|
82
87
|
}
|
package/src/utils.ts
CHANGED
|
@@ -202,7 +202,7 @@ export function getGirNamespaceMetadata(reflection: Reflection): GirNamespaceMet
|
|
|
202
202
|
// Companion namespace detection
|
|
203
203
|
// ---------------------------------------------------------------------------
|
|
204
204
|
|
|
205
|
-
const COMPANION_OWNER_KINDS = ReflectionKind.Class | ReflectionKind.Interface;
|
|
205
|
+
const COMPANION_OWNER_KINDS = ReflectionKind.Class | ReflectionKind.Interface | ReflectionKind.Enum;
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Find the companion namespace for a class or interface reflection.
|