@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.
@@ -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
@@ -0,0 +1,4 @@
1
+ {
2
+ "module": "../fesm2022/analogjs-content-md4x.mjs",
3
+ "typings": "../types/md4x/src/index.d.ts"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "module": "../fesm2022/analogjs-content-mdc.mjs",
3
+ "typings": "../types/mdc/src/index.d.ts"
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@analogjs/content",
3
- "version": "3.0.0-alpha.10",
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, '&amp;')
141
+ .replace(/</g, '&lt;')
142
+ .replace(/>/g, '&gt;')
143
+ .replace(/"/g, '&quot;');
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
+ }