@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-for-gir/typedoc-theme",
3
- "version": "4.0.0-beta.41",
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.41",
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
  }
@@ -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 { getDisplayName, getHierarchyRoots } from "../utils.ts";
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("assets/search.js", true),
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 companionClass = siblings?.find(
165
- (child) => child !== mod && child.kindOf(ReflectionKind.Class) && child.name === mod.name,
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 (companionClass) {
168
- const classUrl = context.urlTo(companionClass);
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: classUrl }, companionClass.name),
177
- " class. See the class page for full documentation.",
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("${classUrl}");` }),
190
+ JSX.createElement(JSX.Raw, { html: `window.location.replace("${ownerUrl}");` }),
183
191
  ),
184
192
  );
185
193
  }
@@ -11,9 +11,17 @@ import {
11
11
  isCompanionNamespace,
12
12
  } from "../utils.ts";
13
13
 
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const TSFOR_GIR_VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf8"))
16
- .version as string;
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.