@analogjs/content 3.0.0-alpha.10 → 3.0.0-alpha.12
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/fesm2022/analogjs-content-md4x.mjs +290 -0
- package/fesm2022/analogjs-content-mdc.mjs +170 -0
- package/fesm2022/analogjs-content.mjs +158 -281
- package/fesm2022/content-list-loader.mjs +284 -0
- package/md4x/package.json +4 -0
- package/mdc/package.json +4 -0
- package/package.json +16 -2
- package/src/lib/devtools/content-devtools-client.ts +215 -0
- package/src/lib/devtools/content-devtools.styles.css +194 -0
- package/types/md4x/src/index.d.ts +5 -0
- package/types/md4x/src/lib/md4x-content-renderer.service.d.ts +33 -0
- package/types/md4x/src/lib/md4x-wasm-content-renderer.service.d.ts +16 -0
- package/types/md4x/src/lib/provide-md4x.d.ts +26 -0
- package/types/md4x/src/lib/streaming-markdown-renderer.d.ts +21 -0
- package/types/mdc/src/index.d.ts +2 -0
- package/types/mdc/src/lib/mdc-component-registry.d.ts +25 -0
- package/types/mdc/src/lib/mdc-renderer.directive.d.ts +33 -0
- package/types/src/index.d.ts +2 -0
- package/types/src/lib/devtools/content-devtools-plugin.d.ts +23 -0
- package/types/src/lib/devtools/content-devtools-renderer.d.ts +23 -0
- package/types/src/lib/devtools/index.d.ts +23 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { Injectable, InjectionToken, TransferState, inject, makeStateKey, signal, ɵPendingTasksInternal } from "@angular/core";
|
|
3
|
+
//#region packages/content/src/lib/content-renderer.ts
|
|
4
|
+
var ContentRenderer = class ContentRenderer {
|
|
5
|
+
async render(content) {
|
|
6
|
+
return {
|
|
7
|
+
content,
|
|
8
|
+
toc: []
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
getContentHeadings(_content) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
enhance() {}
|
|
15
|
+
static {
|
|
16
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
17
|
+
minVersion: "12.0.0",
|
|
18
|
+
version: "21.1.1",
|
|
19
|
+
ngImport: i0,
|
|
20
|
+
type: ContentRenderer,
|
|
21
|
+
deps: [],
|
|
22
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
static {
|
|
26
|
+
this.ɵprov = i0.ɵɵngDeclareInjectable({
|
|
27
|
+
minVersion: "12.0.0",
|
|
28
|
+
version: "21.1.1",
|
|
29
|
+
ngImport: i0,
|
|
30
|
+
type: ContentRenderer
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
35
|
+
minVersion: "12.0.0",
|
|
36
|
+
version: "21.1.1",
|
|
37
|
+
ngImport: i0,
|
|
38
|
+
type: ContentRenderer,
|
|
39
|
+
decorators: [{ type: Injectable }]
|
|
40
|
+
});
|
|
41
|
+
var NoopContentRenderer = class {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.transferState = inject(TransferState);
|
|
44
|
+
this.contentId = 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generates a hash from the content string
|
|
48
|
+
* to be used with the transfer state
|
|
49
|
+
*/
|
|
50
|
+
generateHash(str) {
|
|
51
|
+
let hash = 0;
|
|
52
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
53
|
+
const chr = str.charCodeAt(i);
|
|
54
|
+
hash = (hash << 5) - hash + chr;
|
|
55
|
+
hash |= 0;
|
|
56
|
+
}
|
|
57
|
+
return hash;
|
|
58
|
+
}
|
|
59
|
+
async render(content) {
|
|
60
|
+
this.contentId = this.generateHash(content);
|
|
61
|
+
const toc = this.getContentHeadings(content);
|
|
62
|
+
const key = makeStateKey(`content-headings-${this.contentId}`);
|
|
63
|
+
return {
|
|
64
|
+
content,
|
|
65
|
+
toc: this.transferState.get(key, toc)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
enhance() {}
|
|
69
|
+
getContentHeadings(content) {
|
|
70
|
+
return this.extractHeadings(content);
|
|
71
|
+
}
|
|
72
|
+
extractHeadings(content) {
|
|
73
|
+
const markdownHeadings = this.extractHeadingsFromMarkdown(content);
|
|
74
|
+
if (markdownHeadings.length > 0) return markdownHeadings;
|
|
75
|
+
return this.extractHeadingsFromHtml(content);
|
|
76
|
+
}
|
|
77
|
+
extractHeadingsFromMarkdown(content) {
|
|
78
|
+
const lines = content.split("\n");
|
|
79
|
+
const toc = [];
|
|
80
|
+
const slugCounts = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
83
|
+
if (!match) continue;
|
|
84
|
+
const level = match[1].length;
|
|
85
|
+
const text = match[2].trim();
|
|
86
|
+
if (!text) continue;
|
|
87
|
+
const baseSlug = text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
|
|
88
|
+
const count = slugCounts.get(baseSlug) ?? 0;
|
|
89
|
+
slugCounts.set(baseSlug, count + 1);
|
|
90
|
+
const id = count === 0 ? baseSlug : `${baseSlug}-${count}`;
|
|
91
|
+
toc.push({
|
|
92
|
+
id,
|
|
93
|
+
level,
|
|
94
|
+
text
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return toc;
|
|
98
|
+
}
|
|
99
|
+
extractHeadingsFromHtml(content) {
|
|
100
|
+
const toc = [];
|
|
101
|
+
const slugCounts = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const match of content.matchAll(/<h([1-6])([^>]*)>([\s\S]*?)<\/h\1>/gi)) {
|
|
103
|
+
const level = Number(match[1]);
|
|
104
|
+
const attrs = match[2] ?? "";
|
|
105
|
+
const text = (match[3] ?? "").replace(/<[^>]+>/g, "").trim();
|
|
106
|
+
if (!text) continue;
|
|
107
|
+
const idMatch = /\sid=(['"])(.*?)\1/i.exec(attrs) ?? /\sid=([^\s>]+)/i.exec(attrs);
|
|
108
|
+
let id = idMatch?.[2] ?? idMatch?.[1] ?? "";
|
|
109
|
+
if (!id) id = this.makeSlug(text, slugCounts);
|
|
110
|
+
toc.push({
|
|
111
|
+
id,
|
|
112
|
+
level,
|
|
113
|
+
text
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return toc;
|
|
117
|
+
}
|
|
118
|
+
makeSlug(text, slugCounts) {
|
|
119
|
+
const baseSlug = text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
|
|
120
|
+
const count = slugCounts.get(baseSlug) ?? 0;
|
|
121
|
+
slugCounts.set(baseSlug, count + 1);
|
|
122
|
+
return count === 0 ? baseSlug : `${baseSlug}-${count}`;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region packages/content/src/lib/get-content-files.ts
|
|
127
|
+
/**
|
|
128
|
+
* Returns the list of content files by filename with ?analog-content-list=true.
|
|
129
|
+
* We use the query param to transform the return into an array of
|
|
130
|
+
* just front matter attributes.
|
|
131
|
+
*
|
|
132
|
+
* @returns
|
|
133
|
+
*/
|
|
134
|
+
var getContentFilesList = () => {
|
|
135
|
+
return {};
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Returns the lazy loaded content files for lookups.
|
|
139
|
+
*
|
|
140
|
+
* @returns
|
|
141
|
+
*/
|
|
142
|
+
var getContentFiles = () => {
|
|
143
|
+
return {};
|
|
144
|
+
};
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region packages/content/src/lib/content-files-list-token.ts
|
|
147
|
+
function getSlug(filename) {
|
|
148
|
+
const base = (filename.split(/[/\\]/).pop() || "").trim().replace(/\.[^./\\]+$/, "");
|
|
149
|
+
return base === "index" ? "" : base;
|
|
150
|
+
}
|
|
151
|
+
var CONTENT_FILES_LIST_TOKEN = new InjectionToken("@analogjs/content Content Files List", {
|
|
152
|
+
providedIn: "root",
|
|
153
|
+
factory() {
|
|
154
|
+
const contentFiles = getContentFilesList();
|
|
155
|
+
return Object.keys(contentFiles).map((filename) => {
|
|
156
|
+
const attributes = contentFiles[filename];
|
|
157
|
+
const slug = attributes["slug"];
|
|
158
|
+
return {
|
|
159
|
+
filename,
|
|
160
|
+
attributes,
|
|
161
|
+
slug: slug ? encodeURI(slug) : encodeURI(getSlug(filename))
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region packages/content/src/lib/content-files-token.ts
|
|
168
|
+
var CONTENT_FILES_TOKEN = new InjectionToken("@analogjs/content Content Files", {
|
|
169
|
+
providedIn: "root",
|
|
170
|
+
factory() {
|
|
171
|
+
const allFiles = { ...getContentFiles() };
|
|
172
|
+
const contentFilesList = inject(CONTENT_FILES_LIST_TOKEN);
|
|
173
|
+
const lookup = {};
|
|
174
|
+
contentFilesList.forEach((item) => {
|
|
175
|
+
const contentFilename = item.filename.replace(/(.*?)\/content/, "/src/content");
|
|
176
|
+
const fileParts = contentFilename.split("/");
|
|
177
|
+
const filePath = fileParts.slice(0, fileParts.length - 1).join("/");
|
|
178
|
+
const fileNameParts = fileParts[fileParts.length - 1].split(".");
|
|
179
|
+
const ext = fileNameParts[fileNameParts.length - 1];
|
|
180
|
+
let slug = item.slug ?? "";
|
|
181
|
+
if (slug === "") slug = "index";
|
|
182
|
+
lookup[contentFilename] = `${slug.includes("/") ? `/src/content/${slug}` : `${filePath}/${slug}`}.${ext}`.replace(/\/{2,}/g, "/");
|
|
183
|
+
});
|
|
184
|
+
const objectUsingSlugAttribute = {};
|
|
185
|
+
Object.entries(allFiles).forEach((entry) => {
|
|
186
|
+
const filename = entry[0];
|
|
187
|
+
const value = entry[1];
|
|
188
|
+
const newFilename = lookup[filename.replace(/^\/(.*?)\/content/, "/src/content")];
|
|
189
|
+
if (newFilename !== void 0) {
|
|
190
|
+
const objectFilename = newFilename.replace(/^\/(.*?)\/content/, "/src/content");
|
|
191
|
+
objectUsingSlugAttribute[objectFilename] = value;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return objectUsingSlugAttribute;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
new InjectionToken("@analogjs/content Content Files", {
|
|
198
|
+
providedIn: "root",
|
|
199
|
+
factory() {
|
|
200
|
+
return signal(inject(CONTENT_FILES_TOKEN));
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region packages/content/src/lib/render-task.service.ts
|
|
205
|
+
var RenderTaskService = class RenderTaskService {
|
|
206
|
+
#pendingTasks = inject(ɵPendingTasksInternal);
|
|
207
|
+
addRenderTask() {
|
|
208
|
+
return this.#pendingTasks.add();
|
|
209
|
+
}
|
|
210
|
+
clearRenderTask(clear) {
|
|
211
|
+
if (typeof clear === "function") clear();
|
|
212
|
+
else if (typeof this.#pendingTasks.remove === "function") this.#pendingTasks.remove(clear);
|
|
213
|
+
}
|
|
214
|
+
static {
|
|
215
|
+
this.ɵfac = i0.ɵɵngDeclareFactory({
|
|
216
|
+
minVersion: "12.0.0",
|
|
217
|
+
version: "21.1.1",
|
|
218
|
+
ngImport: i0,
|
|
219
|
+
type: RenderTaskService,
|
|
220
|
+
deps: [],
|
|
221
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
static {
|
|
225
|
+
this.ɵprov = i0.ɵɵngDeclareInjectable({
|
|
226
|
+
minVersion: "12.0.0",
|
|
227
|
+
version: "21.1.1",
|
|
228
|
+
ngImport: i0,
|
|
229
|
+
type: RenderTaskService
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
234
|
+
minVersion: "12.0.0",
|
|
235
|
+
version: "21.1.1",
|
|
236
|
+
ngImport: i0,
|
|
237
|
+
type: RenderTaskService,
|
|
238
|
+
decorators: [{ type: Injectable }]
|
|
239
|
+
});
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region packages/content/src/lib/inject-content-files.ts
|
|
242
|
+
function injectContentFiles(filterFn) {
|
|
243
|
+
const renderTaskService = inject(RenderTaskService);
|
|
244
|
+
const task = renderTaskService.addRenderTask();
|
|
245
|
+
const allContentFiles = inject(CONTENT_FILES_LIST_TOKEN);
|
|
246
|
+
renderTaskService.clearRenderTask(task);
|
|
247
|
+
if (filterFn) return allContentFiles.filter(filterFn);
|
|
248
|
+
return allContentFiles;
|
|
249
|
+
}
|
|
250
|
+
function injectContentFilesMap() {
|
|
251
|
+
return inject(CONTENT_FILES_TOKEN);
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region packages/content/src/lib/content-file-loader.ts
|
|
255
|
+
var CONTENT_FILE_LOADER = new InjectionToken("@analogjs/content/resource File Loader");
|
|
256
|
+
function injectContentFileLoader() {
|
|
257
|
+
return inject(CONTENT_FILE_LOADER);
|
|
258
|
+
}
|
|
259
|
+
function withContentFileLoader() {
|
|
260
|
+
return {
|
|
261
|
+
provide: CONTENT_FILE_LOADER,
|
|
262
|
+
useFactory() {
|
|
263
|
+
return async () => injectContentFilesMap();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region packages/content/src/lib/content-list-loader.ts
|
|
269
|
+
var CONTENT_LIST_LOADER = new InjectionToken("@analogjs/content/resource List Loader");
|
|
270
|
+
function injectContentListLoader() {
|
|
271
|
+
return inject(CONTENT_LIST_LOADER);
|
|
272
|
+
}
|
|
273
|
+
function withContentListLoader() {
|
|
274
|
+
return {
|
|
275
|
+
provide: CONTENT_LIST_LOADER,
|
|
276
|
+
useFactory() {
|
|
277
|
+
return async () => injectContentFiles();
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
export { injectContentFileLoader as a, injectContentFilesMap as c, ContentRenderer as d, NoopContentRenderer as f, CONTENT_FILE_LOADER as i, RenderTaskService as l, injectContentListLoader as n, withContentFileLoader as o, withContentListLoader as r, injectContentFiles as s, CONTENT_LIST_LOADER as t, CONTENT_FILES_TOKEN as u };
|
|
283
|
+
|
|
284
|
+
//# sourceMappingURL=content-list-loader.mjs.map
|
package/mdc/package.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analogjs/content",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.12",
|
|
4
4
|
"description": "Content Rendering for Analog",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Brandon Roberts <robertsbt@gmail.com>",
|
|
@@ -38,12 +38,16 @@
|
|
|
38
38
|
"prismjs": "^1.29.0",
|
|
39
39
|
"satori": "^0.10.14",
|
|
40
40
|
"satori-html": "^0.3.2",
|
|
41
|
-
"sharp": "^0.33.5"
|
|
41
|
+
"sharp": "^0.33.5",
|
|
42
|
+
"md4x": ">=0.0.20"
|
|
42
43
|
},
|
|
43
44
|
"peerDependenciesMeta": {
|
|
44
45
|
"@nx/devkit": {
|
|
45
46
|
"optional": true
|
|
46
47
|
},
|
|
48
|
+
"md4x": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
47
51
|
"satori": {
|
|
48
52
|
"optional": true
|
|
49
53
|
},
|
|
@@ -90,6 +94,16 @@
|
|
|
90
94
|
"import": "./fesm2022/analogjs-content.mjs",
|
|
91
95
|
"default": "./fesm2022/analogjs-content.mjs"
|
|
92
96
|
},
|
|
97
|
+
"./md4x": {
|
|
98
|
+
"types": "./types/md4x/src/index.d.ts",
|
|
99
|
+
"import": "./fesm2022/analogjs-content-md4x.mjs",
|
|
100
|
+
"default": "./fesm2022/analogjs-content-md4x.mjs"
|
|
101
|
+
},
|
|
102
|
+
"./mdc": {
|
|
103
|
+
"types": "./types/mdc/src/index.d.ts",
|
|
104
|
+
"import": "./fesm2022/analogjs-content-mdc.mjs",
|
|
105
|
+
"default": "./fesm2022/analogjs-content-mdc.mjs"
|
|
106
|
+
},
|
|
93
107
|
"./og": {
|
|
94
108
|
"types": "./types/og/src/index.d.ts",
|
|
95
109
|
"import": "./fesm2022/analogjs-content-og.mjs",
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side script for the Analog Content DevTools panel.
|
|
3
|
+
* Injected by the Vite plugin in dev mode only.
|
|
4
|
+
*
|
|
5
|
+
* @experimental Content DevTools is experimental and may change in future releases.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface DevToolsData {
|
|
9
|
+
renderer: string;
|
|
10
|
+
renderTimeMs: number;
|
|
11
|
+
frontmatter: Record<string, unknown>;
|
|
12
|
+
toc: Array<{ id: string; level: number; text: string }>;
|
|
13
|
+
contentLength: number;
|
|
14
|
+
headingCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const STORAGE_KEY = 'analog-content-devtools-open';
|
|
18
|
+
|
|
19
|
+
// Cache the latest devtools payload at module level so events fired
|
|
20
|
+
// during initial bootstrap (before the panel is created) are not lost.
|
|
21
|
+
let latestDevToolsData: DevToolsData | null = null;
|
|
22
|
+
let panelUpdateCallback: ((data: DevToolsData) => void) | null = null;
|
|
23
|
+
|
|
24
|
+
window.addEventListener('analog-content-devtools-data', ((
|
|
25
|
+
e: CustomEvent<DevToolsData>,
|
|
26
|
+
) => {
|
|
27
|
+
latestDevToolsData = e.detail;
|
|
28
|
+
if (panelUpdateCallback) {
|
|
29
|
+
panelUpdateCallback(e.detail);
|
|
30
|
+
}
|
|
31
|
+
}) as EventListener);
|
|
32
|
+
|
|
33
|
+
function createPanel(): HTMLElement {
|
|
34
|
+
const root = document.createElement('div');
|
|
35
|
+
root.id = 'analog-content-devtools';
|
|
36
|
+
root.innerHTML = `
|
|
37
|
+
<button class="acd-toggle" title="Analog Content DevTools">
|
|
38
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
39
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h5v7h7v9H6z"/>
|
|
40
|
+
<path d="M8 13h8v1H8zm0 3h6v1H8z" opacity=".6"/>
|
|
41
|
+
</svg>
|
|
42
|
+
</button>
|
|
43
|
+
<div class="acd-panel" style="display:none">
|
|
44
|
+
<div class="acd-header">
|
|
45
|
+
<span>Analog Content DevTools <span class="acd-badge acd-badge-experimental">experimental</span></span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="acd-tabs">
|
|
48
|
+
<button class="acd-tab" data-tab="overview" data-active="true">Overview</button>
|
|
49
|
+
<button class="acd-tab" data-tab="frontmatter">Frontmatter</button>
|
|
50
|
+
<button class="acd-tab" data-tab="toc">TOC</button>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="acd-body">
|
|
53
|
+
<div class="acd-empty">No content data available. Navigate to a content page.</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
return root;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderOverview(data: DevToolsData): string {
|
|
61
|
+
const speedClass =
|
|
62
|
+
data.renderTimeMs < 5
|
|
63
|
+
? 'acd-fast'
|
|
64
|
+
: data.renderTimeMs > 50
|
|
65
|
+
? 'acd-slow'
|
|
66
|
+
: '';
|
|
67
|
+
return `
|
|
68
|
+
<div class="acd-section">
|
|
69
|
+
<div class="acd-section-title">Renderer</div>
|
|
70
|
+
<div class="acd-kv">
|
|
71
|
+
<span class="acd-key">Active</span>
|
|
72
|
+
<span class="acd-value"><span class="acd-badge acd-badge-renderer">${data.renderer}</span></span>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="acd-kv">
|
|
75
|
+
<span class="acd-key">Render time</span>
|
|
76
|
+
<span class="acd-value ${speedClass}">${data.renderTimeMs.toFixed(2)}ms</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="acd-section">
|
|
80
|
+
<div class="acd-section-title">Content</div>
|
|
81
|
+
<div class="acd-kv">
|
|
82
|
+
<span class="acd-key">Length</span>
|
|
83
|
+
<span class="acd-value">${data.contentLength.toLocaleString()} chars</span>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="acd-kv">
|
|
86
|
+
<span class="acd-key">Headings</span>
|
|
87
|
+
<span class="acd-value">${data.headingCount}</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="acd-kv">
|
|
90
|
+
<span class="acd-key">Frontmatter keys</span>
|
|
91
|
+
<span class="acd-value">${Object.keys(data.frontmatter).length}</span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderFrontmatter(data: DevToolsData): string {
|
|
98
|
+
const keys = Object.keys(data.frontmatter);
|
|
99
|
+
if (keys.length === 0) {
|
|
100
|
+
return '<div class="acd-empty">No frontmatter found.</div>';
|
|
101
|
+
}
|
|
102
|
+
return `
|
|
103
|
+
<div class="acd-section">
|
|
104
|
+
<div class="acd-section-title">Frontmatter attributes</div>
|
|
105
|
+
<div class="acd-pre">${escapeHtml(JSON.stringify(data.frontmatter, null, 2))}</div>
|
|
106
|
+
</div>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderToc(data: DevToolsData): HTMLElement {
|
|
111
|
+
if (data.toc.length === 0) {
|
|
112
|
+
const empty = document.createElement('div');
|
|
113
|
+
empty.className = 'acd-empty';
|
|
114
|
+
empty.textContent = 'No headings found.';
|
|
115
|
+
return empty;
|
|
116
|
+
}
|
|
117
|
+
const section = document.createElement('div');
|
|
118
|
+
section.className = 'acd-section';
|
|
119
|
+
const title = document.createElement('div');
|
|
120
|
+
title.className = 'acd-section-title';
|
|
121
|
+
title.textContent = `Table of Contents (${data.toc.length} headings)`;
|
|
122
|
+
section.appendChild(title);
|
|
123
|
+
|
|
124
|
+
for (const h of data.toc) {
|
|
125
|
+
const item = document.createElement('div');
|
|
126
|
+
item.className = 'acd-toc-item';
|
|
127
|
+
item.style.paddingLeft = `${(h.level - 1) * 12}px`;
|
|
128
|
+
const anchor = document.createElement('a');
|
|
129
|
+
anchor.setAttribute('href', `#${encodeURIComponent(h.id)}`);
|
|
130
|
+
anchor.textContent = `${'#'.repeat(h.level)} ${h.text}`;
|
|
131
|
+
item.appendChild(anchor);
|
|
132
|
+
section.appendChild(item);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return section;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function escapeHtml(str: string): string {
|
|
139
|
+
return str
|
|
140
|
+
.replace(/&/g, '&')
|
|
141
|
+
.replace(/</g, '<')
|
|
142
|
+
.replace(/>/g, '>')
|
|
143
|
+
.replace(/"/g, '"');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function initDevTools() {
|
|
147
|
+
if (document.getElementById('analog-content-devtools')) return;
|
|
148
|
+
|
|
149
|
+
const panel = createPanel();
|
|
150
|
+
document.body.appendChild(panel);
|
|
151
|
+
|
|
152
|
+
const toggle = panel.querySelector('.acd-toggle') as HTMLElement;
|
|
153
|
+
const panelEl = panel.querySelector('.acd-panel') as HTMLElement;
|
|
154
|
+
const body = panel.querySelector('.acd-body') as HTMLElement;
|
|
155
|
+
const tabs = panel.querySelectorAll('.acd-tab');
|
|
156
|
+
|
|
157
|
+
let isOpen = localStorage.getItem(STORAGE_KEY) === 'true';
|
|
158
|
+
let activeTab = 'overview';
|
|
159
|
+
let currentData: DevToolsData | null = latestDevToolsData;
|
|
160
|
+
|
|
161
|
+
function updateVisibility() {
|
|
162
|
+
panelEl.style.display = isOpen ? 'flex' : 'none';
|
|
163
|
+
localStorage.setItem(STORAGE_KEY, String(isOpen));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function updateBody() {
|
|
167
|
+
if (!currentData) {
|
|
168
|
+
body.innerHTML =
|
|
169
|
+
'<div class="acd-empty">No content data available. Navigate to a content page.</div>';
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
switch (activeTab) {
|
|
173
|
+
case 'overview':
|
|
174
|
+
body.innerHTML = renderOverview(currentData);
|
|
175
|
+
break;
|
|
176
|
+
case 'frontmatter':
|
|
177
|
+
body.innerHTML = renderFrontmatter(currentData);
|
|
178
|
+
break;
|
|
179
|
+
case 'toc':
|
|
180
|
+
body.replaceChildren(renderToc(currentData));
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
toggle.addEventListener('click', () => {
|
|
186
|
+
isOpen = !isOpen;
|
|
187
|
+
updateVisibility();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
tabs.forEach((tab) => {
|
|
191
|
+
tab.addEventListener('click', () => {
|
|
192
|
+
activeTab = (tab as HTMLElement).dataset['tab'] || 'overview';
|
|
193
|
+
tabs.forEach((t) =>
|
|
194
|
+
(t as HTMLElement).setAttribute('data-active', String(t === tab)),
|
|
195
|
+
);
|
|
196
|
+
updateBody();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Wire the module-level listener to update the panel
|
|
201
|
+
panelUpdateCallback = (data: DevToolsData) => {
|
|
202
|
+
currentData = data;
|
|
203
|
+
updateBody();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
updateVisibility();
|
|
207
|
+
updateBody();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Init when DOM is ready
|
|
211
|
+
if (document.readyState === 'loading') {
|
|
212
|
+
document.addEventListener('DOMContentLoaded', initDevTools);
|
|
213
|
+
} else {
|
|
214
|
+
initDevTools();
|
|
215
|
+
}
|