@riebeckite/core 0.0.1

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.
Files changed (61) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +94 -0
  3. package/README_ja.md +75 -0
  4. package/dist/index.d.ts +61 -0
  5. package/dist/index.js +3040 -0
  6. package/dist/src/config.d.ts +8 -0
  7. package/dist/src/config_validation.d.ts +7 -0
  8. package/dist/src/content/affected_content.d.ts +7 -0
  9. package/dist/src/content/attachment.d.ts +7 -0
  10. package/dist/src/content/content_build_coordinator.d.ts +25 -0
  11. package/dist/src/content/content_build_state.d.ts +22 -0
  12. package/dist/src/content/content_build_state_store.d.ts +20 -0
  13. package/dist/src/content/content_change_set.d.ts +10 -0
  14. package/dist/src/content/content_collection.d.ts +78 -0
  15. package/dist/src/content/content_entry_reader.d.ts +12 -0
  16. package/dist/src/content/content_fingerprint.d.ts +3 -0
  17. package/dist/src/content/content_graph.d.ts +23 -0
  18. package/dist/src/content/content_index_builder.d.ts +7 -0
  19. package/dist/src/content/content_links.d.ts +2 -0
  20. package/dist/src/content/content_location.d.ts +2 -0
  21. package/dist/src/content/content_location_resolver.d.ts +18 -0
  22. package/dist/src/content/content_manager.d.ts +59 -0
  23. package/dist/src/content/content_metadata.d.ts +3 -0
  24. package/dist/src/content/content_query.d.ts +76 -0
  25. package/dist/src/content/content_source.d.ts +17 -0
  26. package/dist/src/content/file_system_content_source.d.ts +11 -0
  27. package/dist/src/content/graph_layout.d.ts +24 -0
  28. package/dist/src/content/image_extensions.d.ts +1 -0
  29. package/dist/src/content/manifest_builder.d.ts +6 -0
  30. package/dist/src/content/read_only_content_graph.d.ts +4 -0
  31. package/dist/src/observability.d.ts +70 -0
  32. package/dist/src/pipeline.d.ts +25 -0
  33. package/dist/src/plugin/plugin_cache.d.ts +22 -0
  34. package/dist/src/plugin/plugin_dependency.d.ts +2 -0
  35. package/dist/src/plugin/plugin_dependency_error.d.ts +20 -0
  36. package/dist/src/plugin/plugin_lifecycle.d.ts +11 -0
  37. package/dist/src/plugin/plugin_memo.d.ts +20 -0
  38. package/dist/src/plugin/plugin_runtime.d.ts +59 -0
  39. package/dist/src/types/config_validation.d.ts +7 -0
  40. package/dist/src/types/content_manifest.d.ts +87 -0
  41. package/dist/src/types/diagnostic.d.ts +15 -0
  42. package/dist/src/types/generated_output.d.ts +28 -0
  43. package/dist/src/types/plugin.d.ts +71 -0
  44. package/dist/src/types/plugin_asset.d.ts +13 -0
  45. package/dist/src/types/plugin_context.d.ts +85 -0
  46. package/dist/src/types/plugin_diagnostic.d.ts +5 -0
  47. package/dist/src/types/plugin_endpoint.d.ts +22 -0
  48. package/dist/src/types/plugin_head.d.ts +18 -0
  49. package/dist/src/types/plugin_pipeline.d.ts +20 -0
  50. package/dist/src/types/plugin_seo.d.ts +40 -0
  51. package/dist/src/types/post_content.d.ts +20 -0
  52. package/dist/src/types/publish_strategy.d.ts +19 -0
  53. package/dist/src/types/resolved_riebeckite_config.d.ts +26 -0
  54. package/dist/src/types/riebeckite_config.d.ts +23 -0
  55. package/dist/src/types/site_config.d.ts +14 -0
  56. package/dist/src/types/theme_config.d.ts +55 -0
  57. package/dist/src/utils/collections.d.ts +1 -0
  58. package/dist/src/utils/html.d.ts +2 -0
  59. package/dist/src/utils/tags.d.ts +1 -0
  60. package/dist/src/utils/text.d.ts +2 -0
  61. package/package.json +71 -0
package/dist/index.js ADDED
@@ -0,0 +1,3040 @@
1
+ // src/config_validation.ts
2
+ var ConfigValidationError = class extends Error {
3
+ issues;
4
+ constructor(issues) {
5
+ super(formatValidationIssues(issues));
6
+ this.name = "ConfigValidationError";
7
+ this.issues = issues;
8
+ }
9
+ };
10
+ function validateConfig(config) {
11
+ const issues = validateConfigIssues(config);
12
+ if (issues.length > 0) throw new ConfigValidationError(issues);
13
+ }
14
+ function validateConfigIssues(config) {
15
+ const issues = [];
16
+ const value = config;
17
+ if (!isRecord(value)) {
18
+ return [{ path: "config", message: "Expected an object." }];
19
+ }
20
+ validateSite(value.site, issues);
21
+ validateContent(value.content, issues);
22
+ validateTheme(value.theme, issues);
23
+ validatePlugins(value.plugins, issues);
24
+ return issues;
25
+ }
26
+ function validateSite(value, issues) {
27
+ if (!isRecord(value)) {
28
+ issues.push({ path: "site", message: "Expected an object." });
29
+ return;
30
+ }
31
+ validateRequiredString(value.title, "site.title", issues);
32
+ validateOptionalString(value.description, "site.description", issues);
33
+ validateOptionalString(value.author, "site.author", issues);
34
+ validateOptionalString(value.locale, "site.locale", issues);
35
+ validateOptionalString(value.twitterSite, "site.twitterSite", issues);
36
+ validateOptionalString(value.defaultOgImage, "site.defaultOgImage", issues);
37
+ validateAbsoluteUrl(value.baseUrl, "site.baseUrl", issues);
38
+ validateFeed(value.feed, issues);
39
+ }
40
+ function validateFeed(value, issues) {
41
+ if (value === void 0) return;
42
+ if (!isRecord(value)) {
43
+ issues.push({ path: "site.feed", message: "Expected an object." });
44
+ return;
45
+ }
46
+ validateOptionalString(value.title, "site.feed.title", issues);
47
+ validateOptionalString(value.description, "site.feed.description", issues);
48
+ validateOptionalString(value.language, "site.feed.language", issues);
49
+ }
50
+ function validateContent(value, issues) {
51
+ if (value === void 0) return;
52
+ if (!isRecord(value)) {
53
+ issues.push({ path: "content", message: "Expected an object." });
54
+ return;
55
+ }
56
+ validateOptionalString(value.directory, "content.directory", issues);
57
+ validateContentSource(value.source, issues);
58
+ validateStringArray(value.exclude, "content.exclude", issues);
59
+ validatePublishStrategy(value.filters, issues);
60
+ }
61
+ function validateContentSource(value, issues) {
62
+ if (value === void 0) return;
63
+ if (!isRecord(value)) {
64
+ issues.push({
65
+ path: "content.source",
66
+ message: "Expected a ContentSource."
67
+ });
68
+ return;
69
+ }
70
+ if (typeof value.scan !== "function") {
71
+ issues.push({
72
+ path: "content.source.scan",
73
+ message: "Expected a function."
74
+ });
75
+ }
76
+ if (typeof value.read !== "function") {
77
+ issues.push({
78
+ path: "content.source.read",
79
+ message: "Expected a function."
80
+ });
81
+ }
82
+ }
83
+ function validatePublishStrategy(value, issues) {
84
+ if (value === void 0) return;
85
+ if (!isRecord(value)) {
86
+ issues.push({ path: "content.filters", message: "Expected an object." });
87
+ return;
88
+ }
89
+ if (value.publishStrategy !== void 0 && value.publishStrategy !== "explicit" && value.publishStrategy !== "selective") {
90
+ issues.push({
91
+ path: "content.filters.publishStrategy",
92
+ message: 'Expected "explicit" or "selective".'
93
+ });
94
+ }
95
+ }
96
+ function validateTheme(value, issues) {
97
+ if (value === void 0) return;
98
+ if (!isRecord(value)) {
99
+ issues.push({ path: "theme", message: "Expected an object." });
100
+ return;
101
+ }
102
+ validateThemeConfig(value, "theme", issues);
103
+ validateThemeStyles(value.styles, "theme.styles", issues);
104
+ if (value.config !== void 0) {
105
+ if (!isRecord(value.config)) {
106
+ issues.push({ path: "theme.config", message: "Expected an object." });
107
+ } else {
108
+ validateThemeConfig(value.config, "theme.config", issues);
109
+ }
110
+ }
111
+ }
112
+ function validateThemeConfig(value, path4, issues) {
113
+ validateOptionalString(value.name, `${path4}.name`, issues);
114
+ validateOneOf(
115
+ value.colorMode,
116
+ `${path4}.colorMode`,
117
+ ["light", "dark", "system"],
118
+ issues
119
+ );
120
+ validateOneOf(
121
+ value.typography,
122
+ `${path4}.typography`,
123
+ ["system", "serif", "sans"],
124
+ issues
125
+ );
126
+ validateOneOf(
127
+ value.articleLayout,
128
+ `${path4}.articleLayout`,
129
+ ["article", "sidebar", "full-width"],
130
+ issues
131
+ );
132
+ validateStringArray(value.userCss, `${path4}.userCss`, issues);
133
+ if (value.attributes !== void 0 && !isRecord(value.attributes)) {
134
+ issues.push({ path: `${path4}.attributes`, message: "Expected an object." });
135
+ }
136
+ }
137
+ function validateThemeStyles(value, path4, issues) {
138
+ if (value === void 0) return;
139
+ if (!Array.isArray(value)) {
140
+ issues.push({ path: path4, message: "Expected an array." });
141
+ return;
142
+ }
143
+ for (const [index, style] of value.entries()) {
144
+ if (!isRecord(style) || !isNonEmptyString(style.moduleSpecifier)) {
145
+ issues.push({
146
+ path: `${path4}[${index}].moduleSpecifier`,
147
+ message: "Expected a non-empty string."
148
+ });
149
+ }
150
+ }
151
+ }
152
+ function validatePlugins(value, issues) {
153
+ if (value === void 0) return;
154
+ if (!Array.isArray(value)) {
155
+ issues.push({ path: "plugins", message: "Expected an array." });
156
+ return;
157
+ }
158
+ const names = /* @__PURE__ */ new Map();
159
+ for (const [index, input] of value.entries()) {
160
+ if (input === false || input === null || input === void 0) continue;
161
+ if (!isRecord(input)) {
162
+ issues.push({ path: `plugins[${index}]`, message: "Expected a plugin." });
163
+ continue;
164
+ }
165
+ const plugin = input;
166
+ const path4 = `plugins[${index}]`;
167
+ if (!isNonEmptyString(plugin.name)) {
168
+ issues.push({
169
+ path: `${path4}.name`,
170
+ message: "Expected a non-empty string."
171
+ });
172
+ } else if (plugin.enabled !== false) {
173
+ const duplicateIndex = names.get(plugin.name);
174
+ if (duplicateIndex !== void 0) {
175
+ issues.push({
176
+ path: `${path4}.name`,
177
+ message: `Duplicate plugin name "${plugin.name}" (also plugins[${duplicateIndex}]).`
178
+ });
179
+ } else {
180
+ names.set(plugin.name, index);
181
+ }
182
+ }
183
+ validateOptionalBoolean(plugin.enabled, `${path4}.enabled`, issues);
184
+ validateCapabilityMetadata(plugin, path4, issues);
185
+ if (plugin.enabled !== false) validatePluginOptions(plugin, path4, issues);
186
+ }
187
+ }
188
+ function validateCapabilityMetadata(plugin, path4, issues) {
189
+ validateStringArray(plugin.provides, `${path4}.provides`, issues, true);
190
+ validateStringArray(plugin.requires, `${path4}.requires`, issues, true);
191
+ validateStringArray(plugin.optional, `${path4}.optional`, issues, true);
192
+ }
193
+ function validatePluginOptions(plugin, path4, issues) {
194
+ if (!plugin.validateOptions) return;
195
+ try {
196
+ const optionIssues = plugin.validateOptions(plugin.options) ?? [];
197
+ for (const issue of optionIssues) {
198
+ issues.push({
199
+ path: issue.path ? `${path4}.options.${issue.path}` : `${path4}.options`,
200
+ message: issue.message
201
+ });
202
+ }
203
+ } catch (error) {
204
+ issues.push({
205
+ path: `${path4}.options`,
206
+ message: `Validator failed: ${toErrorMessage(error)}`
207
+ });
208
+ }
209
+ }
210
+ function validateRequiredString(value, path4, issues) {
211
+ if (!isNonEmptyString(value)) {
212
+ issues.push({ path: path4, message: "Expected a non-empty string." });
213
+ }
214
+ }
215
+ function validateOptionalString(value, path4, issues) {
216
+ if (value !== void 0 && typeof value !== "string") {
217
+ issues.push({ path: path4, message: "Expected a string." });
218
+ }
219
+ }
220
+ function validateOptionalBoolean(value, path4, issues) {
221
+ if (value !== void 0 && typeof value !== "boolean") {
222
+ issues.push({ path: path4, message: "Expected a boolean." });
223
+ }
224
+ }
225
+ function validateAbsoluteUrl(value, path4, issues) {
226
+ if (value === void 0 || value === "") return;
227
+ if (typeof value !== "string") {
228
+ issues.push({ path: path4, message: "Expected an absolute URL." });
229
+ return;
230
+ }
231
+ try {
232
+ const url = new URL(value);
233
+ if (url.protocol !== "http:" && url.protocol !== "https:")
234
+ throw new Error();
235
+ } catch {
236
+ issues.push({ path: path4, message: "Expected an absolute HTTP(S) URL." });
237
+ }
238
+ }
239
+ function validateStringArray(value, path4, issues, nonEmpty = false) {
240
+ if (value === void 0) return;
241
+ if (!Array.isArray(value)) {
242
+ issues.push({ path: path4, message: "Expected an array." });
243
+ return;
244
+ }
245
+ for (const [index, item] of value.entries()) {
246
+ if (typeof item !== "string" || nonEmpty && !item.trim()) {
247
+ issues.push({
248
+ path: `${path4}[${index}]`,
249
+ message: nonEmpty ? "Expected a non-empty string." : "Expected a string."
250
+ });
251
+ }
252
+ }
253
+ }
254
+ function validateOneOf(value, path4, choices, issues) {
255
+ if (value !== void 0 && (typeof value !== "string" || !choices.includes(value))) {
256
+ issues.push({ path: path4, message: `Expected one of: ${choices.join(", ")}.` });
257
+ }
258
+ }
259
+ function formatValidationIssues(issues) {
260
+ return [
261
+ "Invalid Riebeckite configuration:",
262
+ ...issues.map((issue) => `
263
+ ${issue.path}
264
+ ${issue.message}`)
265
+ ].join("\n");
266
+ }
267
+ function isRecord(value) {
268
+ return typeof value === "object" && value !== null && !Array.isArray(value);
269
+ }
270
+ function isNonEmptyString(value) {
271
+ return typeof value === "string" && value.trim().length > 0;
272
+ }
273
+ function toErrorMessage(error) {
274
+ return error instanceof Error ? error.message : String(error);
275
+ }
276
+
277
+ // src/plugin/plugin_dependency_error.ts
278
+ var PluginDependencyError = class extends Error {
279
+ kind;
280
+ pluginName;
281
+ capability;
282
+ declaration;
283
+ pluginNames;
284
+ chain;
285
+ availableCapabilities;
286
+ constructor(input) {
287
+ super(createMessage(input));
288
+ this.name = "PluginDependencyError";
289
+ this.kind = input.kind;
290
+ this.pluginName = input.pluginName;
291
+ this.capability = input.capability;
292
+ this.declaration = input.declaration;
293
+ this.pluginNames = input.pluginNames ?? [];
294
+ this.chain = input.chain ?? [];
295
+ this.availableCapabilities = input.availableCapabilities ?? [];
296
+ }
297
+ };
298
+ function createMessage(input) {
299
+ if (input.kind === "missing-capability") {
300
+ return [
301
+ `Plugin "${input.pluginName}" requires missing capability "${input.capability}".`,
302
+ formatAvailableCapabilities(input.availableCapabilities)
303
+ ].join("\n");
304
+ }
305
+ if (input.kind === "self-dependency") {
306
+ return `Plugin "${input.pluginName}" requires capability "${input.capability}" that it also provides.`;
307
+ }
308
+ if (input.kind === "invalid-capability") {
309
+ return `Plugin "${input.pluginName}" declares an empty or invalid capability in "${input.declaration}".`;
310
+ }
311
+ if (input.kind === "duplicate-provider") {
312
+ return [
313
+ `Capability "${input.capability}" is provided by multiple plugins:`,
314
+ ...(input.pluginNames ?? []).map((name) => `- ${name}`),
315
+ "A capability must have exactly one provider."
316
+ ].join("\n");
317
+ }
318
+ return [
319
+ `Plugin dependency cycle detected for capability "${input.capability}":`,
320
+ (input.chain ?? []).join(" -> ")
321
+ ].join("\n");
322
+ }
323
+ function formatAvailableCapabilities(capabilities) {
324
+ if (!capabilities || capabilities.length === 0) {
325
+ return "No plugin declares a provided capability.";
326
+ }
327
+ return [
328
+ "Available capabilities:",
329
+ ...[...capabilities].sort().map((capability) => `- ${capability}`)
330
+ ].join("\n");
331
+ }
332
+
333
+ // src/plugin/plugin_dependency.ts
334
+ function resolvePluginDependencies(plugins) {
335
+ validateCapabilityNames(plugins);
336
+ const providers = collectProviders(plugins);
337
+ const dependencies = collectDependencies(plugins, providers);
338
+ return stableTopologicalSort(plugins, dependencies);
339
+ }
340
+ var CAPABILITY_DECLARATIONS = [
341
+ "provides",
342
+ "requires",
343
+ "optional"
344
+ ];
345
+ function validateCapabilityNames(plugins) {
346
+ for (const plugin of plugins) {
347
+ for (const declaration of CAPABILITY_DECLARATIONS) {
348
+ for (const capability of plugin[declaration] ?? []) {
349
+ if (capability.trim() === "") {
350
+ throw new PluginDependencyError({
351
+ kind: "invalid-capability",
352
+ pluginName: plugin.name,
353
+ declaration
354
+ });
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ function collectProviders(plugins) {
361
+ const providerIndices = /* @__PURE__ */ new Map();
362
+ for (const [index, plugin] of plugins.entries()) {
363
+ for (const capability of new Set((plugin.provides ?? []).map(normalize))) {
364
+ const indices = providerIndices.get(capability) ?? [];
365
+ indices.push(index);
366
+ providerIndices.set(capability, indices);
367
+ }
368
+ }
369
+ const providers = /* @__PURE__ */ new Map();
370
+ for (const [capability, indices] of providerIndices) {
371
+ if (indices.length > 1) {
372
+ throw new PluginDependencyError({
373
+ kind: "duplicate-provider",
374
+ capability,
375
+ pluginNames: indices.map((index) => plugins[index].name)
376
+ });
377
+ }
378
+ providers.set(capability, indices[0]);
379
+ }
380
+ return providers;
381
+ }
382
+ function collectDependencies(plugins, providers) {
383
+ const availableCapabilities = [...providers.keys()];
384
+ return plugins.map((plugin, pluginIndex) => {
385
+ const requiredDependencies = (plugin.requires ?? []).map((rawCapability) => {
386
+ const capability = normalize(rawCapability);
387
+ return resolveDependency({
388
+ pluginIndex,
389
+ pluginName: plugin.name,
390
+ capability,
391
+ availableCapabilities,
392
+ providers
393
+ });
394
+ });
395
+ const optionalDependencies = (plugin.optional ?? []).flatMap(
396
+ (rawCapability) => {
397
+ const capability = normalize(rawCapability);
398
+ const providerIndex = providers.get(capability);
399
+ if (providerIndex === void 0) return [];
400
+ assertNotSelfDependency({
401
+ pluginIndex,
402
+ pluginName: plugin.name,
403
+ capability,
404
+ providerIndex
405
+ });
406
+ return [{ providerIndex, capability }];
407
+ }
408
+ );
409
+ return [...requiredDependencies, ...optionalDependencies];
410
+ });
411
+ }
412
+ function resolveDependency(input) {
413
+ const providerIndex = input.providers.get(input.capability);
414
+ if (providerIndex === void 0) {
415
+ throw new PluginDependencyError({
416
+ kind: "missing-capability",
417
+ pluginName: input.pluginName,
418
+ capability: input.capability,
419
+ availableCapabilities: input.availableCapabilities
420
+ });
421
+ }
422
+ assertNotSelfDependency({
423
+ pluginIndex: input.pluginIndex,
424
+ pluginName: input.pluginName,
425
+ capability: input.capability,
426
+ providerIndex
427
+ });
428
+ return { providerIndex, capability: input.capability };
429
+ }
430
+ function assertNotSelfDependency(input) {
431
+ if (input.providerIndex !== input.pluginIndex) return;
432
+ throw new PluginDependencyError({
433
+ kind: "self-dependency",
434
+ pluginName: input.pluginName,
435
+ capability: input.capability
436
+ });
437
+ }
438
+ function normalize(capability) {
439
+ return capability.trim();
440
+ }
441
+ function stableTopologicalSort(plugins, dependencies) {
442
+ const dependents = plugins.map(() => /* @__PURE__ */ new Set());
443
+ const remainingDependencies = dependencies.map(
444
+ (pluginDependencies, index) => {
445
+ for (const dependency of pluginDependencies) {
446
+ dependents[dependency.providerIndex].add(index);
447
+ }
448
+ return new Set(
449
+ pluginDependencies.map((dependency) => dependency.providerIndex)
450
+ ).size;
451
+ }
452
+ );
453
+ const available = remainingDependencies.map((count, index) => count === 0 ? index : -1).filter((index) => index >= 0);
454
+ const resolved = [];
455
+ while (available.length > 0) {
456
+ const index = available.shift();
457
+ if (index === void 0) break;
458
+ resolved.push(plugins[index]);
459
+ for (const dependentIndex of dependents[index]) {
460
+ remainingDependencies[dependentIndex] -= 1;
461
+ if (remainingDependencies[dependentIndex] === 0) {
462
+ insertByOriginalOrder(available, dependentIndex);
463
+ }
464
+ }
465
+ }
466
+ if (resolved.length !== plugins.length) {
467
+ throw createCycleError(plugins, dependencies, remainingDependencies);
468
+ }
469
+ return resolved;
470
+ }
471
+ function insertByOriginalOrder(indices, index) {
472
+ const insertionIndex = indices.findIndex((current) => current > index);
473
+ if (insertionIndex === -1) {
474
+ indices.push(index);
475
+ return;
476
+ }
477
+ indices.splice(insertionIndex, 0, index);
478
+ }
479
+ function createCycleError(plugins, dependencies, remainingDependencies) {
480
+ const remaining = new Set(
481
+ remainingDependencies.map((count, index) => count > 0 ? index : -1).filter((index) => index >= 0)
482
+ );
483
+ const cycle = findCycle(dependencies, remaining);
484
+ const chain = cycle.indices.map((index) => plugins[index].name);
485
+ return new PluginDependencyError({
486
+ kind: "cycle",
487
+ capability: cycle.capability,
488
+ pluginNames: chain,
489
+ chain
490
+ });
491
+ }
492
+ function findCycle(dependencies, remaining) {
493
+ const visiting = [];
494
+ const visited = /* @__PURE__ */ new Set();
495
+ for (const index of remaining) {
496
+ const cycle = visitDependency(
497
+ index,
498
+ dependencies,
499
+ remaining,
500
+ visiting,
501
+ visited
502
+ );
503
+ if (cycle) return cycle;
504
+ }
505
+ throw new Error("Plugin dependency cycle could not be resolved.");
506
+ }
507
+ function visitDependency(index, dependencies, remaining, visiting, visited) {
508
+ if (visited.has(index)) return void 0;
509
+ visiting.push(index);
510
+ for (const dependency of dependencies[index]) {
511
+ if (!remaining.has(dependency.providerIndex)) continue;
512
+ const cycleStart = visiting.indexOf(dependency.providerIndex);
513
+ if (cycleStart >= 0) {
514
+ return {
515
+ indices: [...visiting.slice(cycleStart), dependency.providerIndex],
516
+ capability: dependency.capability
517
+ };
518
+ }
519
+ const cycle = visitDependency(
520
+ dependency.providerIndex,
521
+ dependencies,
522
+ remaining,
523
+ visiting,
524
+ visited
525
+ );
526
+ if (cycle) return cycle;
527
+ }
528
+ visiting.pop();
529
+ visited.add(index);
530
+ return void 0;
531
+ }
532
+
533
+ // src/types/plugin.ts
534
+ function definePlugin(plugin) {
535
+ return plugin;
536
+ }
537
+ function resolvePlugins(plugins = []) {
538
+ const orderedPlugins = plugins.filter((plugin) => Boolean(plugin)).filter((plugin) => plugin.enabled !== false).toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
539
+ return resolvePluginDependencies(orderedPlugins);
540
+ }
541
+ function getResolvedPluginMetadata(plugins) {
542
+ return plugins.map((plugin) => ({
543
+ name: plugin.name,
544
+ enabled: plugin.enabled !== false,
545
+ provides: plugin.provides ?? [],
546
+ requires: plugin.requires ?? [],
547
+ optional: plugin.optional ?? []
548
+ }));
549
+ }
550
+
551
+ // src/types/publish_strategy.ts
552
+ function isPublishable(strategy, frontmatter) {
553
+ if (strategy === "explicit") {
554
+ return frontmatter?.publish === true;
555
+ }
556
+ return !(frontmatter?.private === true || frontmatter?.draft === true);
557
+ }
558
+
559
+ // src/types/theme_config.ts
560
+ function defineTheme(theme) {
561
+ return theme;
562
+ }
563
+ function isRiebeckiteTheme(theme) {
564
+ return "styles" in theme || "config" in theme;
565
+ }
566
+
567
+ // src/config.ts
568
+ var defaultThemeStyle = {
569
+ moduleSpecifier: "@riebeckite/theme-default/style.css"
570
+ };
571
+ function defineConfig(config) {
572
+ return config;
573
+ }
574
+ function resolveConfigModule(configModule) {
575
+ return resolveConfig(unwrapConfigModule(configModule));
576
+ }
577
+ var configModuleWrapperKeys = /* @__PURE__ */ new Set([
578
+ "default",
579
+ "module.exports",
580
+ "__esModule"
581
+ ]);
582
+ function unwrapConfigModule(configModule) {
583
+ let candidate = configModule;
584
+ while (isConfigModuleWrapper(candidate)) {
585
+ candidate = candidate.default;
586
+ }
587
+ return candidate;
588
+ }
589
+ function isConfigModuleWrapper(value) {
590
+ if (typeof value !== "object" || value === null) return false;
591
+ if (!("default" in value)) return false;
592
+ const keys = Object.keys(value);
593
+ return keys.length > 0 && keys.every((key) => configModuleWrapperKeys.has(key));
594
+ }
595
+ function resolveConfig(config) {
596
+ validateConfig(config);
597
+ const theme = resolveThemeConfig(config.theme);
598
+ return {
599
+ site: {
600
+ title: config.site?.title ?? "",
601
+ description: config.site?.description ?? "",
602
+ author: config.site?.author ?? "",
603
+ baseUrl: config.site?.baseUrl ?? "",
604
+ locale: config.site?.locale ?? "en",
605
+ twitterSite: config.site?.twitterSite ?? "",
606
+ defaultOgImage: config.site?.defaultOgImage ?? "",
607
+ feed: {
608
+ title: config.site?.feed?.title ?? config.site?.title ?? "",
609
+ description: config.site?.feed?.description ?? config.site?.description ?? "",
610
+ language: config.site?.feed?.language ?? config.site?.locale ?? "en"
611
+ }
612
+ },
613
+ content: {
614
+ directory: config.content?.directory ?? "../../content",
615
+ source: config.content?.source,
616
+ exclude: config.content?.exclude ?? [],
617
+ filters: {
618
+ publishStrategy: config.content?.filters?.publishStrategy ?? "explicit"
619
+ }
620
+ },
621
+ markdown: {
622
+ syntaxHighlight: {
623
+ theme: config.markdown?.syntaxHighlight?.theme ?? ""
624
+ }
625
+ },
626
+ theme,
627
+ plugins: resolvePlugins(config.plugins)
628
+ };
629
+ }
630
+ function resolveThemeConfig(themeInput) {
631
+ const theme = themeInput ? isRiebeckiteTheme(themeInput) ? { name: themeInput.name, ...themeInput.config } : themeInput : void 0;
632
+ const styles = themeInput ? isRiebeckiteTheme(themeInput) ? themeInput.styles ?? [] : [defaultThemeStyle] : [defaultThemeStyle];
633
+ const attributes = themeInput ? isRiebeckiteTheme(themeInput) ? { ...themeInput.config?.attributes, ...themeInput.attributes } : themeInput.attributes : void 0;
634
+ return {
635
+ name: theme?.name ?? "riebeckite",
636
+ colorMode: theme?.colorMode ?? "system",
637
+ typography: theme?.typography ?? "system",
638
+ articleLayout: theme?.articleLayout ?? "article",
639
+ tokens: theme?.tokens ?? {},
640
+ attributes: sanitizeThemeAttributes(attributes),
641
+ userCss: theme?.userCss ?? [],
642
+ styles
643
+ };
644
+ }
645
+ function sanitizeThemeAttributes(attributes) {
646
+ if (!attributes) return {};
647
+ const reservedAttributes = /* @__PURE__ */ new Set([
648
+ "data-theme",
649
+ "data-theme-name",
650
+ "data-typography",
651
+ "data-article-layout"
652
+ ]);
653
+ return Object.fromEntries(
654
+ Object.entries(attributes).filter(
655
+ ([name, value]) => name.startsWith("data-") && value !== void 0
656
+ ).filter(([name]) => !reservedAttributes.has(name))
657
+ );
658
+ }
659
+ function isPublished(config, frontmatter) {
660
+ return isPublishable(config.content.filters.publishStrategy, frontmatter);
661
+ }
662
+ function isExcluded(patterns, relativePath) {
663
+ const normalized = relativePath.replace(/\\/g, "/");
664
+ return patterns.some((pattern) => matchGlob(pattern, normalized));
665
+ }
666
+ function matchGlob(pattern, value) {
667
+ return globToRegExp(pattern).test(value);
668
+ }
669
+ function globToRegExp(glob) {
670
+ let re = "";
671
+ for (let i = 0; i < glob.length; i++) {
672
+ const ch = glob[i];
673
+ if (ch === "*") {
674
+ if (glob[i + 1] === "*") {
675
+ i++;
676
+ if (glob[i + 1] === "/") {
677
+ i++;
678
+ re += "(?:[^/]+/)*";
679
+ } else {
680
+ re += ".*";
681
+ }
682
+ } else {
683
+ re += "[^/]*";
684
+ }
685
+ } else if (ch === "?") {
686
+ re += "[^/]";
687
+ } else {
688
+ re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
689
+ }
690
+ }
691
+ return new RegExp(`^${re}$`);
692
+ }
693
+
694
+ // src/content/image_extensions.ts
695
+ var IMAGE_EXTENSIONS = [
696
+ "png",
697
+ "jpg",
698
+ "jpeg",
699
+ "gif",
700
+ "svg",
701
+ "webp",
702
+ "bmp"
703
+ ];
704
+
705
+ // src/content/attachment.ts
706
+ var ATTACHMENTS_BASE_PATH = "/assets/attachments";
707
+ function isMarkdownPath(filePath) {
708
+ return getExtension(filePath) === "md";
709
+ }
710
+ function isImagePath(filePath) {
711
+ return IMAGE_EXTENSIONS.includes(getExtension(filePath));
712
+ }
713
+ function isAttachmentPath(filePath) {
714
+ return hasFileExtension(filePath) && !isMarkdownPath(filePath) && !isImagePath(filePath);
715
+ }
716
+ function getExtension(filePath) {
717
+ const lowerPath = filePath.toLowerCase();
718
+ if (lowerPath.endsWith(".tar.gz")) return "tar.gz";
719
+ const fileName = getFileName(filePath);
720
+ const extensionStart = fileName.lastIndexOf(".");
721
+ if (extensionStart <= 0) return "";
722
+ return fileName.slice(extensionStart + 1).toLowerCase();
723
+ }
724
+ function attachmentUrl(filePath) {
725
+ const encodedPath = normalizeContentPath(filePath).split("/").map(encodeURIComponent).join("/");
726
+ return `${ATTACHMENTS_BASE_PATH}/${encodedPath}`;
727
+ }
728
+ function normalizeContentPath(filePath) {
729
+ return filePath.replace(/\\/g, "/").normalize("NFC");
730
+ }
731
+ function hasFileExtension(filePath) {
732
+ const fileName = getFileName(filePath);
733
+ const extensionStart = fileName.lastIndexOf(".");
734
+ return extensionStart > 0 && extensionStart < fileName.length - 1;
735
+ }
736
+ function getFileName(filePath) {
737
+ const normalizedPath = normalizeContentPath(filePath);
738
+ return normalizedPath.slice(normalizedPath.lastIndexOf("/") + 1);
739
+ }
740
+
741
+ // src/content/content_build_state_store.ts
742
+ import { randomUUID } from "node:crypto";
743
+ import fs from "node:fs/promises";
744
+ import path from "node:path";
745
+
746
+ // src/content/content_build_state.ts
747
+ var CONTENT_BUILD_STATE_VERSION = 2;
748
+ var CONTENT_BUILD_STATE_DIRECTORY = ".riebeckite";
749
+ var CONTENT_BUILD_STATE_EXCLUDE = `${CONTENT_BUILD_STATE_DIRECTORY}/**`;
750
+
751
+ // src/content/content_build_state_store.ts
752
+ async function loadContentBuildState(statePath) {
753
+ try {
754
+ const parsed = JSON.parse(await fs.readFile(statePath, "utf8"));
755
+ return isContentBuildState(parsed) ? parsed : void 0;
756
+ } catch {
757
+ return void 0;
758
+ }
759
+ }
760
+ async function readContentBuildStateStatus(statePath) {
761
+ let parsed;
762
+ try {
763
+ parsed = JSON.parse(await fs.readFile(statePath, "utf8"));
764
+ } catch (error) {
765
+ if (isNotFoundError(error)) return { kind: "missing", path: statePath };
766
+ if (error instanceof SyntaxError) {
767
+ return { kind: "invalid", path: statePath, reason: "malformed-json" };
768
+ }
769
+ throw error;
770
+ }
771
+ if (!isRecord2(parsed)) {
772
+ return { kind: "invalid", path: statePath, reason: "invalid-shape" };
773
+ }
774
+ if (parsed.version !== CONTENT_BUILD_STATE_VERSION) {
775
+ return { kind: "invalid", path: statePath, reason: "unsupported-version" };
776
+ }
777
+ if (!isContentBuildStateShape(parsed)) {
778
+ return { kind: "invalid", path: statePath, reason: "invalid-shape" };
779
+ }
780
+ return {
781
+ kind: "valid",
782
+ path: statePath,
783
+ version: parsed.version,
784
+ entryCount: Object.keys(parsed.entries).length
785
+ };
786
+ }
787
+ async function saveContentBuildState(statePath, state) {
788
+ const directory = path.dirname(statePath);
789
+ const temporaryPath = `${statePath}.${randomUUID()}.tmp`;
790
+ await fs.mkdir(directory, { recursive: true });
791
+ try {
792
+ await fs.writeFile(temporaryPath, JSON.stringify(state), "utf8");
793
+ await fs.rename(temporaryPath, statePath);
794
+ } catch (error) {
795
+ await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
796
+ throw error;
797
+ }
798
+ }
799
+ function resolveContentBuildStatePath(config, contentDirectory) {
800
+ const directory = config?.content.directory ?? contentDirectory ?? process.cwd();
801
+ return path.resolve(
802
+ directory,
803
+ CONTENT_BUILD_STATE_DIRECTORY,
804
+ "content-state.json"
805
+ );
806
+ }
807
+ function isContentBuildState(value) {
808
+ return isRecord2(value) && value.version === CONTENT_BUILD_STATE_VERSION && isContentBuildStateShape(value);
809
+ }
810
+ function isContentBuildStateShape(value) {
811
+ if (!isRecord2(value.entries) || !isRecord2(value.contentIndex)) return false;
812
+ return Object.values(value.entries).every(
813
+ (entry) => isRecord2(entry) && typeof entry.fingerprint === "string" && entry.fingerprint.length > 0 && Array.isArray(entry.dependencies) && entry.dependencies.every(
814
+ (dependency) => typeof dependency === "string"
815
+ )
816
+ ) && Object.values(value.contentIndex).every(
817
+ (entry) => typeof entry === "string"
818
+ );
819
+ }
820
+ function isRecord2(value) {
821
+ return typeof value === "object" && value !== null && !Array.isArray(value);
822
+ }
823
+ function isNotFoundError(error) {
824
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
825
+ }
826
+
827
+ // src/content/content_fingerprint.ts
828
+ import { createHash } from "node:crypto";
829
+ async function fingerprintContentEntries(entries, read) {
830
+ return await Promise.all(
831
+ [...entries].sort((left, right) => left.path.localeCompare(right.path)).map(async (entry) => ({
832
+ entry,
833
+ fingerprint: await fingerprintContentEntry(entry, read)
834
+ }))
835
+ );
836
+ }
837
+ async function fingerprintContentEntry(entry, read) {
838
+ if (entry.metadata?.hash) return `hash:${entry.metadata.hash}`;
839
+ if (entry.metadata?.etag) return `etag:${entry.metadata.etag}`;
840
+ const content = await read(entry);
841
+ return `sha256:${createHash("sha256").update(typeof content === "string" ? content : content).digest("hex")}`;
842
+ }
843
+
844
+ // src/content/content_query.ts
845
+ function queryContentEntries(entries, spec = {}) {
846
+ const filter = spec.filter ?? {};
847
+ const matched = entries.filter((entry) => matchesFilter(entry, filter));
848
+ const sorted = applySort(matched, spec.sort);
849
+ return applyPagination(sorted, spec.offset, spec.limit);
850
+ }
851
+ function queryContentPage(entries, spec = {}) {
852
+ const ordered = queryContentEntries(entries, {
853
+ filter: spec.filter,
854
+ sort: spec.sort
855
+ });
856
+ const page = resolveContentQueryPagination(ordered.length, spec);
857
+ const end = page.limit > 0 ? page.offset + page.limit : void 0;
858
+ return { entries: ordered.slice(page.offset, end), page };
859
+ }
860
+ function resolveContentQueryPagination(total, spec = {}) {
861
+ const limit = typeof spec.limit === "number" && Number.isFinite(spec.limit) && spec.limit > 0 ? Math.floor(spec.limit) : 0;
862
+ const offset = typeof spec.offset === "number" && Number.isFinite(spec.offset) && spec.offset > 0 ? Math.floor(spec.offset) : 0;
863
+ const pageCount = limit > 0 ? Math.ceil(total / limit) : total > 0 ? 1 : 0;
864
+ const page = limit > 0 ? Math.floor(offset / limit) + 1 : 1;
865
+ return {
866
+ offset,
867
+ limit,
868
+ total,
869
+ page,
870
+ pageCount,
871
+ hasPrevious: offset > 0,
872
+ hasNext: limit > 0 ? offset + limit < total : false
873
+ };
874
+ }
875
+ function matchesFilter(entry, filter) {
876
+ return matchesTags(entry.tags, filter.tags) && matchesFolder(entry.slug, filter.folder) && matchesFrontmatter(entry.frontmatter, filter.frontmatter) && matchesDate(entry.frontmatter, filter.date);
877
+ }
878
+ function matchesTags(tags, filter) {
879
+ if (!filter) return true;
880
+ const normalized = tags.map((tag) => tag.toLowerCase());
881
+ const has = (tag) => normalized.includes(tag.toLowerCase());
882
+ if (filter.any && filter.any.length > 0 && !filter.any.some(has)) {
883
+ return false;
884
+ }
885
+ if (filter.all && filter.all.length > 0 && !filter.all.every(has)) {
886
+ return false;
887
+ }
888
+ if (filter.none && filter.none.length > 0 && filter.none.some(has)) {
889
+ return false;
890
+ }
891
+ return true;
892
+ }
893
+ function matchesFolder(slug, folder) {
894
+ if (folder === void 0) return true;
895
+ const folders = (Array.isArray(folder) ? folder : [folder]).map(normalizeFolder).filter((value) => value.length > 0);
896
+ if (folders.length === 0) return true;
897
+ return folders.some(
898
+ (value) => slug === value || slug.startsWith(`${value}/`)
899
+ );
900
+ }
901
+ function normalizeFolder(value) {
902
+ return value.trim().replace(/^\/+|\/+$/g, "");
903
+ }
904
+ function matchesFrontmatter(frontmatter, filter) {
905
+ if (!filter) return true;
906
+ for (const [key, expected] of Object.entries(filter)) {
907
+ const actual = frontmatter[key];
908
+ const expectedValues = Array.isArray(expected) ? expected : [expected];
909
+ if (expectedValues.length === 0) continue;
910
+ if (!expectedValues.some((value) => matchesFrontmatterValue(actual, value))) {
911
+ return false;
912
+ }
913
+ }
914
+ return true;
915
+ }
916
+ function matchesFrontmatterValue(actual, expected) {
917
+ if (Array.isArray(actual)) {
918
+ return actual.some((item) => scalarEquals(item, expected));
919
+ }
920
+ return scalarEquals(actual, expected);
921
+ }
922
+ function scalarEquals(actual, expected) {
923
+ if (actual === void 0 || actual === null) return false;
924
+ if (typeof actual === "string" && typeof expected === "string") {
925
+ return actual.toLowerCase() === expected.toLowerCase();
926
+ }
927
+ if (actual instanceof Date) {
928
+ const time = toTime(expected);
929
+ return time !== null && actual.getTime() === time;
930
+ }
931
+ return actual === expected;
932
+ }
933
+ function matchesDate(frontmatter, filter) {
934
+ if (!filter) return true;
935
+ const from = filter.from ? parseLowerBound(filter.from) : null;
936
+ const to = filter.to ? parseUpperBound(filter.to) : null;
937
+ if (from === null && to === null) return true;
938
+ const value = toTime(frontmatter[filter.field ?? "date"]);
939
+ if (value === null) return false;
940
+ if (from !== null && value < from) return false;
941
+ if (to !== null && value > to) return false;
942
+ return true;
943
+ }
944
+ function parseLowerBound(value) {
945
+ return toTime(value);
946
+ }
947
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
948
+ function parseUpperBound(value) {
949
+ const time = toTime(
950
+ DATE_ONLY.test(value.trim()) ? `${value.trim()}T23:59:59.999Z` : value
951
+ );
952
+ return time;
953
+ }
954
+ function toTime(value) {
955
+ if (value instanceof Date) {
956
+ const time2 = value.getTime();
957
+ return Number.isFinite(time2) ? time2 : null;
958
+ }
959
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
960
+ if (typeof value !== "string") return null;
961
+ const trimmed = value.trim();
962
+ if (trimmed.length === 0) return null;
963
+ const time = Date.parse(trimmed);
964
+ return Number.isNaN(time) ? null : time;
965
+ }
966
+ var TITLE_FIELDS = /* @__PURE__ */ new Set(["title", "slug", "permalink"]);
967
+ function applySort(entries, sort) {
968
+ const keys = normalizeSort(sort);
969
+ if (keys.length === 0) return [...entries];
970
+ return [...entries].sort((left, right) => {
971
+ for (const key of keys) {
972
+ const result = compareEntries(left, right, key);
973
+ if (result !== 0) return result;
974
+ }
975
+ return 0;
976
+ });
977
+ }
978
+ function normalizeSort(sort) {
979
+ if (!sort) return [];
980
+ const list = Array.isArray(sort) ? sort : [sort];
981
+ return list.map((value) => ({
982
+ field: value.field?.trim() || "date",
983
+ order: value.order === "desc" ? "desc" : "asc"
984
+ }));
985
+ }
986
+ function compareEntries(left, right, sort) {
987
+ const leftValue = resolveSortValue(left, sort.field);
988
+ const rightValue = resolveSortValue(right, sort.field);
989
+ if (leftValue === void 0 && rightValue === void 0) return 0;
990
+ if (leftValue === void 0) return 1;
991
+ if (rightValue === void 0) return -1;
992
+ const direction = sort.order === "desc" ? -1 : 1;
993
+ return direction * compareValues(leftValue, rightValue);
994
+ }
995
+ function resolveSortValue(entry, field) {
996
+ if (!TITLE_FIELDS.has(field)) return entry.frontmatter[field];
997
+ if (field === "title") return entry.title;
998
+ if (field === "slug") return entry.slug;
999
+ return entry.permalink;
1000
+ }
1001
+ function compareValues(left, right) {
1002
+ const leftComparable = toComparable(left);
1003
+ const rightComparable = toComparable(right);
1004
+ if (typeof leftComparable === "number" && typeof rightComparable === "number") {
1005
+ return leftComparable - rightComparable;
1006
+ }
1007
+ return String(leftComparable).localeCompare(String(rightComparable), "en");
1008
+ }
1009
+ function toComparable(value) {
1010
+ if (value instanceof Date) return value.getTime();
1011
+ if (typeof value === "number") return value;
1012
+ if (typeof value === "boolean") return value ? 1 : 0;
1013
+ if (value === void 0 || value === null) return "";
1014
+ return String(value);
1015
+ }
1016
+ function applyPagination(entries, offset, limit) {
1017
+ const start = typeof offset === "number" && Number.isFinite(offset) && offset > 0 ? Math.floor(offset) : 0;
1018
+ let result = start > 0 ? entries.slice(start) : [...entries];
1019
+ if (typeof limit === "number" && Number.isFinite(limit) && limit >= 0) {
1020
+ result = result.slice(0, Math.floor(limit));
1021
+ }
1022
+ return result;
1023
+ }
1024
+ function groupContentEntries(entries, groupBy, options = {}) {
1025
+ const matched = queryContentEntries(entries, {
1026
+ filter: options.filter,
1027
+ sort: options.sort
1028
+ });
1029
+ const groups = /* @__PURE__ */ new Map();
1030
+ for (const entry of matched) {
1031
+ for (const key of resolveGroupKeys(entry, groupBy)) {
1032
+ appendGroup(groups, key, entry);
1033
+ }
1034
+ }
1035
+ return [...groups.entries()].sort(
1036
+ ([left], [right]) => options.order === "desc" ? right.localeCompare(left, "en") : left.localeCompare(right, "en")
1037
+ ).map(([key, groupEntries]) => ({ key, entries: groupEntries }));
1038
+ }
1039
+ function appendGroup(groups, key, entry) {
1040
+ const values = groups.get(key);
1041
+ if (values) {
1042
+ values.push(entry);
1043
+ } else {
1044
+ groups.set(key, [entry]);
1045
+ }
1046
+ }
1047
+ function resolveGroupKeys(entry, groupBy) {
1048
+ switch (groupBy.by) {
1049
+ case "tags":
1050
+ return [...entry.tags];
1051
+ case "folder":
1052
+ return resolveFolderKeys(entry.slug, groupBy.depth);
1053
+ case "date":
1054
+ return resolveDateKeys(
1055
+ entry.frontmatter,
1056
+ groupBy.fields ?? [groupBy.field ?? "date"],
1057
+ groupBy.granularity ?? "month"
1058
+ );
1059
+ case "frontmatter":
1060
+ return resolveFrontmatterKeys(entry.frontmatter[groupBy.field]);
1061
+ default:
1062
+ return [];
1063
+ }
1064
+ }
1065
+ function resolveFolderKeys(slug, depth) {
1066
+ const segments = slug.split("/");
1067
+ segments.pop();
1068
+ const directories = typeof depth === "number" && Number.isFinite(depth) && depth > 0 ? segments.slice(0, Math.floor(depth)) : segments;
1069
+ return [directories.join("/")];
1070
+ }
1071
+ var DATE_GRANULARITY_LENGTH = {
1072
+ year: 4,
1073
+ month: 7,
1074
+ day: 10
1075
+ };
1076
+ function resolveDateKeys(frontmatter, fields, granularity) {
1077
+ for (const field of fields) {
1078
+ const time = toTime(frontmatter[field]);
1079
+ if (time === null) continue;
1080
+ return [
1081
+ new Date(time).toISOString().slice(0, DATE_GRANULARITY_LENGTH[granularity])
1082
+ ];
1083
+ }
1084
+ return [];
1085
+ }
1086
+ function resolveFrontmatterKeys(value) {
1087
+ if (value === void 0 || value === null) return [];
1088
+ if (value instanceof Date) {
1089
+ const time = value.getTime();
1090
+ return Number.isFinite(time) ? [value.toISOString().slice(0, 10)] : [];
1091
+ }
1092
+ if (Array.isArray(value)) {
1093
+ return value.flatMap((item) => resolveFrontmatterKeys(item));
1094
+ }
1095
+ const text = String(value).trim();
1096
+ return text.length > 0 ? [text] : [];
1097
+ }
1098
+
1099
+ // src/content/content_collection.ts
1100
+ function buildContentCollections(entries, definitions) {
1101
+ const collections = [];
1102
+ for (const definition of definitions) {
1103
+ const basePath = normalizeBasePath(definition.basePath);
1104
+ const pageSize = normalizePageSize(definition.pageSize);
1105
+ const groups = groupContentEntries(entries, definition.groupBy, {
1106
+ filter: definition.filter,
1107
+ sort: definition.sort,
1108
+ order: definition.order
1109
+ });
1110
+ for (const group of groups) {
1111
+ const context = {
1112
+ kind: definition.kind,
1113
+ value: group.key,
1114
+ basePath
1115
+ };
1116
+ const title = definition.resolveTitle?.(context) ?? group.key;
1117
+ const collectionPath = normalizePath(
1118
+ definition.resolvePath?.(context) ?? defaultPath(context)
1119
+ );
1120
+ const total = group.entries.length;
1121
+ const pageCount = pageSize > 0 ? Math.ceil(total / pageSize) : 1;
1122
+ for (let index = 0; index < pageCount; index++) {
1123
+ const start = pageSize > 0 ? index * pageSize : 0;
1124
+ const end = pageSize > 0 ? start + pageSize : void 0;
1125
+ collections.push({
1126
+ kind: definition.kind,
1127
+ value: group.key,
1128
+ title,
1129
+ path: withPage(collectionPath, index),
1130
+ page: {
1131
+ current: index + 1,
1132
+ count: pageCount,
1133
+ size: pageSize,
1134
+ total,
1135
+ previousPath: index > 0 ? withPage(collectionPath, index - 1) : null,
1136
+ nextPath: index < pageCount - 1 ? withPage(collectionPath, index + 1) : null
1137
+ },
1138
+ entries: group.entries.slice(start, end)
1139
+ });
1140
+ }
1141
+ }
1142
+ }
1143
+ return collections;
1144
+ }
1145
+ function normalizePageSize(value) {
1146
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1147
+ }
1148
+ function withPage(path4, index) {
1149
+ if (index === 0) return path4;
1150
+ const base = path4 === "/" ? "" : path4;
1151
+ return normalizePath(`${base}/page/${index + 1}`);
1152
+ }
1153
+ function defaultPath(context) {
1154
+ const slug = slugifyValue(context.value);
1155
+ if (!slug) return context.basePath || "/";
1156
+ return `${context.basePath}/${slug}`;
1157
+ }
1158
+ function slugifyValue(value) {
1159
+ return value.split("/").map(
1160
+ (segment) => segment.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
1161
+ ).filter((segment) => segment.length > 0).join("/");
1162
+ }
1163
+ function normalizeBasePath(value) {
1164
+ const trimmed = value.trim().replace(/\/+$/, "");
1165
+ if (trimmed.length === 0) return "";
1166
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
1167
+ }
1168
+ function normalizePath(value) {
1169
+ const trimmed = value.trim();
1170
+ if (trimmed.length === 0) return "/";
1171
+ const absolute = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
1172
+ return absolute.replace(/\/{2,}/g, "/").replace(/\/+$/, "") || "/";
1173
+ }
1174
+
1175
+ // src/utils/collections.ts
1176
+ function uniqueStrings(values) {
1177
+ return Array.from(new Set(values));
1178
+ }
1179
+
1180
+ // src/content/content_graph.ts
1181
+ function createContentGraph(manifest) {
1182
+ return {
1183
+ nodes: () => [...manifest.entries],
1184
+ get: (slug) => manifest.bySlug.get(slug) ?? null,
1185
+ outgoing: (slug) => toEntries(manifest, getOutgoingSlugs(manifest, slug)),
1186
+ incoming: (slug) => toEntries(manifest, getIncomingSlugs(manifest, slug)),
1187
+ neighbors: (slug) => toEntries(manifest, getNeighborSlugs(manifest, slug)),
1188
+ outgoingSlugs: (slug) => getOutgoingSlugs(manifest, slug),
1189
+ incomingSlugs: (slug) => getIncomingSlugs(manifest, slug),
1190
+ neighborSlugs: (slug) => getNeighborSlugs(manifest, slug),
1191
+ filterNeighbors: (slug, include) => ({
1192
+ outgoingSlugs: uniqueStrings(
1193
+ getOutgoingSlugs(manifest, slug).filter(include)
1194
+ ),
1195
+ incomingSlugs: uniqueStrings(
1196
+ getIncomingSlugs(manifest, slug).filter(include)
1197
+ )
1198
+ })
1199
+ };
1200
+ }
1201
+ function getOutgoingSlugs(manifest, slug) {
1202
+ const links = manifest.outgoingLinks.get(slug) ?? [];
1203
+ return uniqueStrings(
1204
+ links.filter(isResolvedNoteLink).map((link) => link.slug).filter((targetSlug) => targetSlug !== slug)
1205
+ );
1206
+ }
1207
+ function getIncomingSlugs(manifest, slug) {
1208
+ return manifest.incomingLinks.get(slug) ?? [];
1209
+ }
1210
+ function getNeighborSlugs(manifest, slug) {
1211
+ return uniqueStrings([
1212
+ ...getOutgoingSlugs(manifest, slug),
1213
+ ...getIncomingSlugs(manifest, slug)
1214
+ ]);
1215
+ }
1216
+ function toEntries(manifest, slugs) {
1217
+ return slugs.map((slug) => manifest.bySlug.get(slug) ?? null).filter((entry) => entry !== null);
1218
+ }
1219
+ function isResolvedNoteLink(link) {
1220
+ return link.kind === "note" && link.slug !== null;
1221
+ }
1222
+
1223
+ // src/content/graph_layout.ts
1224
+ function buildGraphEdges(nodes, visibleSlugs = new Set(nodes.map((node) => node.slug))) {
1225
+ const edges = /* @__PURE__ */ new Map();
1226
+ for (const node of nodes) {
1227
+ for (const target of node.outgoing) {
1228
+ if (!visibleSlugs.has(node.slug) || !visibleSlugs.has(target)) continue;
1229
+ const key = `${node.slug}\0${target}`;
1230
+ edges.set(key, { source: node.slug, target });
1231
+ }
1232
+ }
1233
+ return Array.from(edges.values());
1234
+ }
1235
+ function layoutRadialGraph(nodes, options) {
1236
+ const layout = /* @__PURE__ */ new Map();
1237
+ const centerX = options.width / 2;
1238
+ const centerY = options.height / 2;
1239
+ const radius = Math.min(options.width, options.height) * (options.radiusRatio ?? 0.36);
1240
+ const minNodeRadius = options.minNodeRadius ?? 8;
1241
+ const maxNodeRadius = options.maxNodeRadius ?? 14;
1242
+ const selectedIndex = Math.max(
1243
+ nodes.findIndex((node) => node.slug === options.centerSlug),
1244
+ 0
1245
+ );
1246
+ nodes.forEach((node, index) => {
1247
+ const shiftedIndex = (index - selectedIndex + nodes.length) % nodes.length;
1248
+ const angle = shiftedIndex / Math.max(nodes.length, 1) * Math.PI * 2;
1249
+ const linkCount = node.outgoing.length + node.backlinks.length;
1250
+ const isCenter = node.slug === options.centerSlug;
1251
+ layout.set(node.slug, {
1252
+ x: isCenter ? centerX : centerX + Math.cos(angle) * radius,
1253
+ y: isCenter ? centerY : centerY + Math.sin(angle) * radius,
1254
+ radius: clamp(7 + linkCount, minNodeRadius, maxNodeRadius)
1255
+ });
1256
+ });
1257
+ return layout;
1258
+ }
1259
+ function clamp(value, min, max) {
1260
+ return Math.min(Math.max(value, min), max);
1261
+ }
1262
+
1263
+ // src/content/content_location.ts
1264
+ function resolveDefaultContentLocation(content) {
1265
+ return {
1266
+ slug: content.slug,
1267
+ permalink: content.slug === "index" ? "/" : `/${content.slug}`
1268
+ };
1269
+ }
1270
+
1271
+ // src/observability.ts
1272
+ var NoopLogger = class {
1273
+ debug() {
1274
+ }
1275
+ info() {
1276
+ }
1277
+ warn() {
1278
+ }
1279
+ error() {
1280
+ }
1281
+ child() {
1282
+ return this;
1283
+ }
1284
+ };
1285
+ var ConsoleLogger = class _ConsoleLogger {
1286
+ constructor(context = {}) {
1287
+ this.context = context;
1288
+ }
1289
+ context;
1290
+ debug(message, context) {
1291
+ this.write("debug", message, context);
1292
+ }
1293
+ info(message, context) {
1294
+ this.write("info", message, context);
1295
+ }
1296
+ warn(message, context) {
1297
+ this.write("warn", message, context);
1298
+ }
1299
+ error(message, context, cause) {
1300
+ const details = mergeContext(this.context, context);
1301
+ if (cause) {
1302
+ console.error(message, details, cause);
1303
+ return;
1304
+ }
1305
+ console.error(message, details);
1306
+ }
1307
+ child(context) {
1308
+ return new _ConsoleLogger(mergeContext(this.context, context));
1309
+ }
1310
+ write(level, message, context) {
1311
+ console[level](message, mergeContext(this.context, context));
1312
+ }
1313
+ };
1314
+ var NoopTracer = class {
1315
+ async span(_name, _attributes, fn) {
1316
+ return await fn();
1317
+ }
1318
+ event() {
1319
+ }
1320
+ };
1321
+ var SinkTracer = class {
1322
+ constructor(sink) {
1323
+ this.sink = sink;
1324
+ }
1325
+ sink;
1326
+ async span(name, attributes, fn) {
1327
+ const startTime = wallClockNow();
1328
+ const startTick = monotonicNow();
1329
+ try {
1330
+ const result = await fn();
1331
+ this.emitSpan({
1332
+ name,
1333
+ startTime,
1334
+ durationMs: monotonicNow() - startTick,
1335
+ status: "ok",
1336
+ attributes
1337
+ });
1338
+ return result;
1339
+ } catch (error) {
1340
+ this.emitSpan({
1341
+ name,
1342
+ startTime,
1343
+ durationMs: monotonicNow() - startTick,
1344
+ status: "error",
1345
+ attributes
1346
+ });
1347
+ throw error;
1348
+ }
1349
+ }
1350
+ event(name, attributes = {}) {
1351
+ try {
1352
+ this.sink.onEvent({ name, time: wallClockNow(), attributes });
1353
+ } catch {
1354
+ }
1355
+ }
1356
+ emitSpan(span) {
1357
+ try {
1358
+ this.sink.onSpan(span);
1359
+ } catch {
1360
+ }
1361
+ }
1362
+ };
1363
+ var CompositeTraceSink = class {
1364
+ constructor(sinks) {
1365
+ this.sinks = sinks;
1366
+ }
1367
+ sinks;
1368
+ onSpan(span) {
1369
+ for (const sink of this.sinks) {
1370
+ try {
1371
+ sink.onSpan(span);
1372
+ } catch {
1373
+ }
1374
+ }
1375
+ }
1376
+ onEvent(event) {
1377
+ for (const sink of this.sinks) {
1378
+ try {
1379
+ sink.onEvent(event);
1380
+ } catch {
1381
+ }
1382
+ }
1383
+ }
1384
+ };
1385
+ var noopObservability = {
1386
+ logger: new NoopLogger(),
1387
+ tracer: new NoopTracer()
1388
+ };
1389
+ function mergeContext(base, context) {
1390
+ return context ? { ...base, ...context } : base;
1391
+ }
1392
+ function wallClockNow() {
1393
+ return Date.now();
1394
+ }
1395
+ function monotonicNow() {
1396
+ return typeof performance === "undefined" ? Date.now() : performance.now();
1397
+ }
1398
+
1399
+ // src/pipeline.ts
1400
+ import rehypeFormat from "rehype-format";
1401
+ import rehypeKatex from "rehype-katex";
1402
+ import rehypeRaw from "rehype-raw";
1403
+ import rehypeSlug from "rehype-slug";
1404
+ import rehypeStringify from "rehype-stringify";
1405
+ import remarkDirective from "remark-directive";
1406
+ import remarkFrontmatter from "remark-frontmatter";
1407
+ import remarkGfm from "remark-gfm";
1408
+ import remarkMath from "remark-math";
1409
+ import remarkParse from "remark-parse";
1410
+ import remarkRehype from "remark-rehype";
1411
+ import { unified } from "unified";
1412
+ import { matter } from "vfile-matter";
1413
+
1414
+ // src/plugin/plugin_cache.ts
1415
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
1416
+ import fs2 from "node:fs/promises";
1417
+ import path2 from "node:path";
1418
+ function createUnavailablePluginCache() {
1419
+ return {
1420
+ get: unavailableAtRuntime,
1421
+ set: unavailableAtRuntime,
1422
+ delete: unavailableAtRuntime,
1423
+ clear: unavailableAtRuntime
1424
+ };
1425
+ }
1426
+ function createPluginCache(options) {
1427
+ const pluginDirectory = path2.join(
1428
+ options.cacheDirectory,
1429
+ pluginNamespace(options.pluginName)
1430
+ );
1431
+ const versionDirectory = path2.join(
1432
+ pluginDirectory,
1433
+ hash(options.cacheVersion ?? "1")
1434
+ );
1435
+ const writes = /* @__PURE__ */ new Map();
1436
+ return {
1437
+ async get(key) {
1438
+ let raw;
1439
+ try {
1440
+ raw = await fs2.readFile(cachePath(versionDirectory, key), "utf8");
1441
+ } catch (error) {
1442
+ if (isNotFoundError2(error)) {
1443
+ options.tracer?.event("cache.miss", { plugin: options.pluginName });
1444
+ return void 0;
1445
+ }
1446
+ throw error;
1447
+ }
1448
+ try {
1449
+ const value = JSON.parse(raw);
1450
+ options.tracer?.event("cache.hit", { plugin: options.pluginName });
1451
+ return value;
1452
+ } catch {
1453
+ options.logger?.warn(
1454
+ "Plugin cache entry is corrupted and will be ignored.",
1455
+ {
1456
+ plugin: options.pluginName
1457
+ }
1458
+ );
1459
+ return void 0;
1460
+ }
1461
+ },
1462
+ async set(key, value) {
1463
+ const targetPath = cachePath(versionDirectory, key);
1464
+ const serialized = JSON.stringify(value);
1465
+ if (serialized === void 0) {
1466
+ throw new TypeError("Plugin cache values must be JSON serializable.");
1467
+ }
1468
+ const previousWrite = writes.get(targetPath) ?? Promise.resolve();
1469
+ const write = previousWrite.catch(() => void 0).then(() => writeAtomically(targetPath, versionDirectory, serialized));
1470
+ writes.set(targetPath, write);
1471
+ try {
1472
+ await write;
1473
+ } finally {
1474
+ if (writes.get(targetPath) === write) {
1475
+ writes.delete(targetPath);
1476
+ }
1477
+ }
1478
+ },
1479
+ async delete(key) {
1480
+ await fs2.rm(cachePath(versionDirectory, key), { force: true });
1481
+ },
1482
+ async clear() {
1483
+ await fs2.rm(pluginDirectory, { recursive: true, force: true });
1484
+ }
1485
+ };
1486
+ }
1487
+ async function writeAtomically(targetPath, versionDirectory, serialized) {
1488
+ await fs2.mkdir(versionDirectory, { recursive: true });
1489
+ const temporaryPath = `${targetPath}.${randomUUID2()}.tmp`;
1490
+ try {
1491
+ await fs2.writeFile(temporaryPath, serialized, "utf8");
1492
+ await fs2.rename(temporaryPath, targetPath);
1493
+ } catch (error) {
1494
+ await fs2.rm(temporaryPath, { force: true }).catch(() => void 0);
1495
+ throw error;
1496
+ }
1497
+ }
1498
+ function resolvePluginCacheDirectory(config) {
1499
+ if (!config) return path2.resolve(process.cwd(), ".riebeckite", "cache");
1500
+ return path2.resolve(config.content.directory, "..", ".riebeckite", "cache");
1501
+ }
1502
+ function cachePath(versionDirectory, key) {
1503
+ return path2.join(versionDirectory, `${hash(key)}.json`);
1504
+ }
1505
+ function pluginNamespace(pluginName) {
1506
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(pluginName) ? pluginName : hash(pluginName);
1507
+ }
1508
+ function hash(value) {
1509
+ return createHash2("sha256").update(value).digest("hex");
1510
+ }
1511
+ function isNotFoundError2(error) {
1512
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1513
+ }
1514
+ async function unavailableAtRuntime() {
1515
+ throw new Error(
1516
+ "Plugin cache is available only through an explicit build context."
1517
+ );
1518
+ }
1519
+
1520
+ // src/types/generated_output.ts
1521
+ function createUnavailableGeneratedOutputSink() {
1522
+ return {
1523
+ emit() {
1524
+ throw new Error(
1525
+ "Generated output is available only during a build lifecycle."
1526
+ );
1527
+ }
1528
+ };
1529
+ }
1530
+ function normalizeGeneratedOutputPath(input) {
1531
+ if (typeof input !== "string" || input.length === 0) {
1532
+ throw new Error("Generated output path must be a non-empty string.");
1533
+ }
1534
+ const replaced = input.replace(/\\/g, "/");
1535
+ if (replaced.includes("\0")) {
1536
+ throw new Error(
1537
+ `Generated output path must not contain a null byte: "${input}".`
1538
+ );
1539
+ }
1540
+ if (replaced.startsWith("/") || replaced.startsWith("~") || /^[A-Za-z]:/.test(replaced)) {
1541
+ throw new Error(`Generated output path must be relative: "${input}".`);
1542
+ }
1543
+ const segments = replaced.split("/").filter((segment) => segment.length > 0);
1544
+ if (segments.length === 0) {
1545
+ throw new Error(`Generated output path must not be empty: "${input}".`);
1546
+ }
1547
+ for (const segment of segments) {
1548
+ if (segment === "." || segment === "..") {
1549
+ throw new Error(
1550
+ `Generated output path must not contain "." or ".." segments: "${input}".`
1551
+ );
1552
+ }
1553
+ }
1554
+ const normalized = segments.join("/");
1555
+ if (normalized === "assets" || normalized.startsWith("assets/")) {
1556
+ throw new Error(
1557
+ `Generated output path must not conflict with the static assets namespace: "${input}".`
1558
+ );
1559
+ }
1560
+ if (normalized === ".riebeckite" || normalized.startsWith(".riebeckite/")) {
1561
+ throw new Error(
1562
+ `Generated output path must not target internal Riebeckite files: "${input}".`
1563
+ );
1564
+ }
1565
+ return normalized;
1566
+ }
1567
+
1568
+ // src/pipeline.ts
1569
+ var Pipeline = class {
1570
+ constructor(contentIndex, permalinks, getMarkdownBySlug, options = {}, isBuildTime = false) {
1571
+ this.contentIndex = contentIndex;
1572
+ this.permalinks = permalinks;
1573
+ this.getMarkdownBySlug = getMarkdownBySlug;
1574
+ this.options = options;
1575
+ this.isBuildTime = isBuildTime;
1576
+ }
1577
+ contentIndex;
1578
+ permalinks;
1579
+ getMarkdownBySlug;
1580
+ options;
1581
+ isBuildTime;
1582
+ pluginCaches = /* @__PURE__ */ new Map();
1583
+ async execute(markDownContent, embedDepth = 0, embedTrail = /* @__PURE__ */ new Set()) {
1584
+ const processor = unified();
1585
+ const plugins = resolvePlugins(this.options.plugins);
1586
+ this.use(processor, remarkParse);
1587
+ this.use(processor, remarkDirective);
1588
+ this.use(processor, remarkFrontmatter, ["yaml", "toml"]);
1589
+ this.use(processor, function loadFrontmatter() {
1590
+ return (_tree, file2) => {
1591
+ matter(file2);
1592
+ };
1593
+ });
1594
+ this.use(processor, remarkMath);
1595
+ this.use(processor, remarkGfm);
1596
+ const markdownPipelineContext = {
1597
+ contentIndex: this.contentIndex,
1598
+ resolvePermalink: (slug) => this.getPermalink(slug),
1599
+ renderNoteEmbed: this.createNoteEmbedRenderer(embedDepth, embedTrail),
1600
+ renderContent: this.createContentRenderer(),
1601
+ contentSource: this.options.contentSource
1602
+ };
1603
+ for (const plugin of plugins) {
1604
+ for (const remarkPlugin of plugin.remarkPlugins ?? []) {
1605
+ this.use(processor, remarkPlugin);
1606
+ }
1607
+ plugin.extendMarkdownPipeline?.(
1608
+ {
1609
+ use: (pipelinePlugin, options) => {
1610
+ this.use(processor, pipelinePlugin, options);
1611
+ }
1612
+ },
1613
+ markdownPipelineContext
1614
+ );
1615
+ }
1616
+ this.use(processor, remarkRehype, { allowDangerousHtml: true });
1617
+ this.use(processor, rehypeRaw);
1618
+ for (const plugin of plugins) {
1619
+ for (const rehypePlugin of plugin.rehypePlugins ?? []) {
1620
+ this.use(processor, rehypePlugin);
1621
+ }
1622
+ plugin.extendHtmlPipeline?.({
1623
+ use: (pipelinePlugin, options) => {
1624
+ this.use(processor, pipelinePlugin, options);
1625
+ }
1626
+ });
1627
+ }
1628
+ this.use(processor, rehypeSlug);
1629
+ this.use(processor, rehypeKatex, { output: "mathml", strict: false });
1630
+ this.use(processor, rehypeFormat);
1631
+ this.use(processor, rehypeStringify, { allowDangerousHtml: true });
1632
+ const file = await processor.process(markDownContent.trim());
1633
+ return {
1634
+ frontmatter: file.data.matter || {},
1635
+ html: String(file.value)
1636
+ };
1637
+ }
1638
+ getPermalink(slug) {
1639
+ const permalink = this.permalinks.get(slug);
1640
+ if (!permalink) {
1641
+ throw new Error(`Content public location was not resolved: ${slug}`);
1642
+ }
1643
+ return permalink;
1644
+ }
1645
+ use(processor, plugin, options) {
1646
+ const register = processor.use.bind(processor);
1647
+ if (options === void 0) {
1648
+ register(plugin);
1649
+ } else {
1650
+ register(plugin, options);
1651
+ }
1652
+ }
1653
+ createNoteEmbedRenderer(embedDepth, embedTrail) {
1654
+ if (!this.getMarkdownBySlug || embedDepth >= 3) return void 0;
1655
+ return async (slug, fragment) => {
1656
+ if (embedTrail.has(slug)) return null;
1657
+ const sourceMarkdown = await this.getMarkdownBySlug?.(slug);
1658
+ const markdown = sourceMarkdown ? selectEmbedMarkdownFragment(sourceMarkdown, fragment) : null;
1659
+ if (!markdown) return null;
1660
+ const nextEmbedTrail = new Set(embedTrail);
1661
+ nextEmbedTrail.add(slug);
1662
+ const content = await this.execute(
1663
+ markdown,
1664
+ embedDepth + 1,
1665
+ nextEmbedTrail
1666
+ );
1667
+ return content.html;
1668
+ };
1669
+ }
1670
+ createContentRenderer() {
1671
+ const renderers = resolvePlugins(this.options.plugins).flatMap(
1672
+ (plugin) => (plugin.renderers ?? []).map((renderer) => ({ plugin, renderer }))
1673
+ );
1674
+ if (renderers.length === 0) return void 0;
1675
+ return async (input) => {
1676
+ for (const { plugin, renderer } of renderers) {
1677
+ const observability = this.options.observability ?? noopObservability;
1678
+ const html = await observability.tracer.span(
1679
+ "plugin.render",
1680
+ {
1681
+ plugin: plugin.name,
1682
+ contentPath: input.path,
1683
+ target: input.kind
1684
+ },
1685
+ () => renderer.render({
1686
+ config: this.options.config,
1687
+ contentIndex: this.contentIndex,
1688
+ diagnostics: [],
1689
+ cache: this.cacheFor(plugin, observability),
1690
+ output: createUnavailableGeneratedOutputSink(),
1691
+ logger: observability.logger.child({ plugin: plugin.name }),
1692
+ tracer: observability.tracer,
1693
+ contentSource: this.options.contentSource,
1694
+ ...input
1695
+ })
1696
+ );
1697
+ if (html) return html;
1698
+ }
1699
+ return null;
1700
+ };
1701
+ }
1702
+ cacheFor(plugin, observability) {
1703
+ const cached = this.pluginCaches.get(plugin.name);
1704
+ if (cached) return cached;
1705
+ const cache = this.isBuildTime ? createPluginCache({
1706
+ pluginName: plugin.name,
1707
+ cacheVersion: plugin.cacheVersion,
1708
+ cacheDirectory: resolvePluginCacheDirectory(this.options.config),
1709
+ tracer: observability.tracer,
1710
+ logger: observability.logger.child({ plugin: plugin.name })
1711
+ }) : createUnavailablePluginCache();
1712
+ this.pluginCaches.set(plugin.name, cache);
1713
+ return cache;
1714
+ }
1715
+ };
1716
+ function selectEmbedMarkdownFragment(markdown, fragment) {
1717
+ if (!fragment) return markdown;
1718
+ if (fragment.kind === "block")
1719
+ return selectBlockFragment(markdown, fragment.value);
1720
+ return selectHeadingFragment(markdown, fragment.value);
1721
+ }
1722
+ function selectBlockFragment(markdown, blockId) {
1723
+ const lines = markdown.split(/\r?\n/);
1724
+ const blockIdRe = new RegExp(`(?:^|\\s)\\^${escapeRegExp(blockId)}\\s*$`);
1725
+ const line = lines.find((currentLine) => blockIdRe.test(currentLine));
1726
+ return line?.replace(blockIdRe, "").trimEnd() || null;
1727
+ }
1728
+ function selectHeadingFragment(markdown, heading) {
1729
+ const lines = markdown.split(/\r?\n/);
1730
+ const targetHeading = heading.trim().toLowerCase();
1731
+ const startIndex = lines.findIndex((line) => {
1732
+ const match = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
1733
+ return match?.[2]?.trim().toLowerCase() === targetHeading;
1734
+ });
1735
+ if (startIndex < 0) return null;
1736
+ const level = lines[startIndex]?.match(/^(#{1,6})\s+/)?.[1]?.length ?? 6;
1737
+ const endIndex = lines.findIndex((line, index) => {
1738
+ if (index <= startIndex) return false;
1739
+ const match = line.match(/^(#{1,6})\s+/);
1740
+ return match !== null && match[1].length <= level;
1741
+ });
1742
+ const selectedLines = lines.slice(
1743
+ startIndex,
1744
+ endIndex === -1 ? void 0 : endIndex
1745
+ );
1746
+ return selectedLines.join("\n").trim() || null;
1747
+ }
1748
+ function escapeRegExp(value) {
1749
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1750
+ }
1751
+
1752
+ // src/plugin/plugin_lifecycle.ts
1753
+ var PluginLifecycleError = class extends Error {
1754
+ constructor(pluginName, hookName, cause) {
1755
+ super(`Plugin "${pluginName}" failed during "${hookName}"`, { cause });
1756
+ this.name = "PluginLifecycleError";
1757
+ }
1758
+ };
1759
+ async function runSetup(plugins, createContext) {
1760
+ await runLifecycleHook(
1761
+ plugins,
1762
+ "setup",
1763
+ (plugin) => plugin.setup,
1764
+ createContext
1765
+ );
1766
+ }
1767
+ async function runBuildStart(plugins, createContext) {
1768
+ await runLifecycleHook(
1769
+ plugins,
1770
+ "buildStart",
1771
+ (plugin) => plugin.buildStart,
1772
+ createContext
1773
+ );
1774
+ }
1775
+ async function runBuildEnd(plugins, createContext) {
1776
+ await runLifecycleHook(
1777
+ plugins,
1778
+ "buildEnd",
1779
+ (plugin) => plugin.buildEnd,
1780
+ createContext
1781
+ );
1782
+ }
1783
+ async function runDispose(plugins, createContext) {
1784
+ await runLifecycleHook(
1785
+ [...plugins].reverse(),
1786
+ "dispose",
1787
+ (plugin) => plugin.dispose,
1788
+ createContext
1789
+ );
1790
+ }
1791
+ async function runLifecycleHook(plugins, hookName, getHook, createContext) {
1792
+ for (const plugin of plugins) {
1793
+ const hook = getHook(plugin);
1794
+ if (!hook) continue;
1795
+ try {
1796
+ const context = createContext(plugin);
1797
+ await context.tracer.span(
1798
+ `plugin.${hookName}`,
1799
+ {
1800
+ plugin: plugin.name,
1801
+ hook: hookName
1802
+ },
1803
+ () => hook(context)
1804
+ );
1805
+ } catch (error) {
1806
+ throw new PluginLifecycleError(plugin.name, hookName, error);
1807
+ }
1808
+ }
1809
+ }
1810
+
1811
+ // src/plugin/plugin_runtime.ts
1812
+ var PluginRuntime = class {
1813
+ constructor(pipelineOptions = {}) {
1814
+ this.pipelineOptions = pipelineOptions;
1815
+ }
1816
+ pipelineOptions;
1817
+ buildLifecycle = null;
1818
+ buildStage = "idle";
1819
+ disposed = false;
1820
+ diagnostics = [];
1821
+ pluginCaches = /* @__PURE__ */ new Map();
1822
+ generatedOutputs = [];
1823
+ generatedOutputOwners = /* @__PURE__ */ new Map();
1824
+ isBuildTime = false;
1825
+ getDiagnostics() {
1826
+ return this.diagnostics;
1827
+ }
1828
+ getBuildStage() {
1829
+ return this.buildStage;
1830
+ }
1831
+ /**
1832
+ * Starts the plugin build lifecycle exactly once, regardless of which public
1833
+ * entry point (content processing or location resolution) reaches it first.
1834
+ * Concurrent callers await the same run, so the ordered hooks
1835
+ * (`setup` → `buildStart` → `onBuildStart` → `onConfigResolved`) are never
1836
+ * duplicated and no caller proceeds before the lifecycle completes.
1837
+ */
1838
+ startBuild(contentIndex) {
1839
+ this.buildLifecycle ??= this.runBuildLifecycle(contentIndex);
1840
+ return this.buildLifecycle;
1841
+ }
1842
+ async runContentLoaded(slug, markdown, contentIndex) {
1843
+ await this.runHook((plugin) => plugin.onContentLoaded, {
1844
+ ...this.createContext(contentIndex),
1845
+ slug,
1846
+ markdown
1847
+ });
1848
+ }
1849
+ async resolveContentLocations(entries, contentIndex) {
1850
+ const context = { ...this.createContext(contentIndex), entries };
1851
+ const locations = [];
1852
+ for (const plugin of this.plugins()) {
1853
+ const resolver = plugin.resolveContentLocations;
1854
+ if (!resolver) continue;
1855
+ const resolved = await resolver(
1856
+ this.createPluginContext(plugin, context)
1857
+ );
1858
+ locations.push(...resolved);
1859
+ }
1860
+ return locations;
1861
+ }
1862
+ async extendContentLocations(entries, locations, contentIndex) {
1863
+ await this.runHook((plugin) => plugin.extendContentLocations, {
1864
+ ...this.createContext(contentIndex),
1865
+ entries,
1866
+ locations
1867
+ });
1868
+ }
1869
+ async runPostHook(hookName, slug, markdown, content, contentIndex) {
1870
+ await this.runHook((plugin) => plugin[hookName], {
1871
+ ...this.createContext(contentIndex),
1872
+ slug,
1873
+ markdown,
1874
+ content
1875
+ });
1876
+ }
1877
+ async runGraphHook(entries, contentIndex) {
1878
+ await this.runHook((plugin) => plugin.extendContentGraph, {
1879
+ ...this.createContext(contentIndex),
1880
+ entries
1881
+ });
1882
+ }
1883
+ async runManifestCreated(manifest, contentIndex) {
1884
+ await this.runHook((plugin) => plugin.onManifestCreated, {
1885
+ ...this.createContext(contentIndex),
1886
+ manifest
1887
+ });
1888
+ }
1889
+ async runBuildEnd(manifest, contentIndex) {
1890
+ const context = {
1891
+ ...this.createContext(contentIndex),
1892
+ manifest
1893
+ };
1894
+ await runBuildEnd(
1895
+ this.plugins(),
1896
+ (plugin) => this.createPluginContext(plugin, context)
1897
+ );
1898
+ await this.runHook((plugin) => plugin.onBuildEnd, {
1899
+ ...this.createContext(contentIndex),
1900
+ manifest
1901
+ });
1902
+ }
1903
+ enableBuildTime() {
1904
+ this.isBuildTime = true;
1905
+ }
1906
+ async dispose(contentIndex) {
1907
+ if (this.disposed || this.buildLifecycle === null) return;
1908
+ this.disposed = true;
1909
+ const context = this.createContext(contentIndex);
1910
+ await runDispose(
1911
+ this.plugins(),
1912
+ (plugin) => this.createPluginContext(plugin, context)
1913
+ );
1914
+ }
1915
+ collectAssets() {
1916
+ return this.plugins().flatMap(
1917
+ (plugin) => (plugin.assets ?? []).map((asset) => ({
1918
+ ...asset,
1919
+ path: asset.moduleSpecifier,
1920
+ pluginName: asset.pluginName || plugin.name
1921
+ }))
1922
+ );
1923
+ }
1924
+ /** Returns registered outputs sorted by path for a deterministic build. */
1925
+ collectGeneratedOutputs() {
1926
+ return [...this.generatedOutputs].sort(
1927
+ (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
1928
+ );
1929
+ }
1930
+ async collectDiagnostics(contentIndex) {
1931
+ const tracer = this.observability().tracer;
1932
+ return await tracer.span("diagnostics.run", {}, async () => {
1933
+ const results = [];
1934
+ const context = this.createContext(contentIndex);
1935
+ for (const plugin of this.plugins()) {
1936
+ const diagnostics = await plugin.addDiagnostics?.(
1937
+ this.createPluginContext(plugin, context)
1938
+ );
1939
+ for (const diagnostic of diagnostics ?? []) {
1940
+ results.push({
1941
+ ...diagnostic,
1942
+ pluginName: diagnostic.pluginName || plugin.name
1943
+ });
1944
+ }
1945
+ }
1946
+ tracer.event("diagnostics.summary", {
1947
+ total: results.length,
1948
+ errors: countSeverity(results, "error"),
1949
+ warnings: countSeverity(results, "warning"),
1950
+ info: countSeverity(results, "info")
1951
+ });
1952
+ return results;
1953
+ });
1954
+ }
1955
+ async runBuildLifecycle(contentIndex) {
1956
+ const context = this.createContext(contentIndex);
1957
+ const createPluginContext = (plugin) => this.createPluginContext(plugin, context);
1958
+ const stages = [
1959
+ {
1960
+ stage: "setup",
1961
+ run: () => runSetup(this.plugins(), createPluginContext)
1962
+ },
1963
+ {
1964
+ stage: "buildStart",
1965
+ run: () => runBuildStart(this.plugins(), createPluginContext)
1966
+ },
1967
+ {
1968
+ stage: "onBuildStart",
1969
+ run: () => this.runHook((plugin) => plugin.onBuildStart, context)
1970
+ },
1971
+ {
1972
+ stage: "onConfigResolved",
1973
+ run: () => this.runHook((plugin) => plugin.onConfigResolved, context)
1974
+ }
1975
+ ];
1976
+ for (const { stage, run } of stages) {
1977
+ this.buildStage = stage;
1978
+ await run();
1979
+ }
1980
+ this.buildStage = "started";
1981
+ }
1982
+ createContext(contentIndex) {
1983
+ return {
1984
+ config: this.pipelineOptions.config,
1985
+ contentIndex,
1986
+ diagnostics: this.diagnostics
1987
+ };
1988
+ }
1989
+ createPluginContext(plugin, context) {
1990
+ const observability = this.observability();
1991
+ return {
1992
+ ...context,
1993
+ cache: this.cacheFor(plugin),
1994
+ output: this.generatedOutputSinkFor(plugin),
1995
+ logger: observability.logger.child({ plugin: plugin.name }),
1996
+ tracer: observability.tracer
1997
+ };
1998
+ }
1999
+ generatedOutputSinkFor(plugin) {
2000
+ return {
2001
+ emit: (output) => {
2002
+ const path4 = normalizeGeneratedOutputPath(output.path);
2003
+ const owner = this.generatedOutputOwners.get(path4);
2004
+ if (owner) {
2005
+ throw new Error(
2006
+ `Duplicate generated output path "${path4}" declared by "${owner}" and "${plugin.name}".`
2007
+ );
2008
+ }
2009
+ this.generatedOutputOwners.set(path4, plugin.name);
2010
+ this.generatedOutputs.push({
2011
+ path: path4,
2012
+ content: output.content,
2013
+ owner: plugin.name
2014
+ });
2015
+ }
2016
+ };
2017
+ }
2018
+ cacheFor(plugin) {
2019
+ const cached = this.pluginCaches.get(plugin.name);
2020
+ if (cached) return cached;
2021
+ const cache = this.isBuildTime ? createPluginCache({
2022
+ pluginName: plugin.name,
2023
+ cacheVersion: plugin.cacheVersion,
2024
+ cacheDirectory: resolvePluginCacheDirectory(
2025
+ this.pipelineOptions.config
2026
+ ),
2027
+ logger: this.observability().logger.child({ plugin: plugin.name }),
2028
+ tracer: this.observability().tracer
2029
+ }) : createUnavailablePluginCache();
2030
+ this.pluginCaches.set(plugin.name, cache);
2031
+ return cache;
2032
+ }
2033
+ async runHook(hook, context) {
2034
+ for (const plugin of this.plugins()) {
2035
+ const pluginHook = hook(plugin);
2036
+ if (!pluginHook) continue;
2037
+ const pluginContext = this.createPluginContext(plugin, context);
2038
+ await pluginContext.tracer.span(
2039
+ "plugin.hook",
2040
+ {
2041
+ plugin: plugin.name
2042
+ },
2043
+ () => pluginHook(pluginContext)
2044
+ );
2045
+ }
2046
+ }
2047
+ observability() {
2048
+ return this.pipelineOptions.observability ?? noopObservability;
2049
+ }
2050
+ plugins() {
2051
+ return resolvePlugins(this.pipelineOptions.plugins);
2052
+ }
2053
+ };
2054
+ function countSeverity(diagnostics, severity) {
2055
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
2056
+ }
2057
+
2058
+ // src/content/affected_content.ts
2059
+ function determineAffectedContent(changeSet, previousState, currentPaths) {
2060
+ const changedPaths = [
2061
+ ...changeSet.added,
2062
+ ...changeSet.changed,
2063
+ ...changeSet.removed
2064
+ ];
2065
+ const direct = new Set(
2066
+ changedPaths.filter(isMarkdownPath2).map(toDependencyKey)
2067
+ );
2068
+ const dependent = /* @__PURE__ */ new Set();
2069
+ if (!previousState) {
2070
+ addAllNotes(direct, currentPaths);
2071
+ return { direct, dependent };
2072
+ }
2073
+ if (changeSet.added.length > 0 || changeSet.removed.length > 0) {
2074
+ addAllNotes(dependent, currentPaths, direct);
2075
+ }
2076
+ const pending = changedPaths.map(toDependencyKey);
2077
+ const seen = new Set(pending);
2078
+ while (pending.length > 0) {
2079
+ const changed = pending.pop();
2080
+ if (changed === void 0) continue;
2081
+ for (const [path4, entry] of Object.entries(previousState.entries)) {
2082
+ if (!isMarkdownPath2(path4)) continue;
2083
+ const slug = toDependencyKey(path4);
2084
+ if (direct.has(slug) || dependent.has(slug)) continue;
2085
+ if (!entry.dependencies.includes(changed)) continue;
2086
+ dependent.add(slug);
2087
+ if (!seen.has(slug)) {
2088
+ seen.add(slug);
2089
+ pending.push(slug);
2090
+ }
2091
+ }
2092
+ }
2093
+ return { direct, dependent };
2094
+ }
2095
+ function addAllNotes(target, currentPaths, exclude) {
2096
+ for (const path4 of currentPaths) {
2097
+ if (!isMarkdownPath2(path4)) continue;
2098
+ const slug = toDependencyKey(path4);
2099
+ if (exclude?.has(slug)) continue;
2100
+ target.add(slug);
2101
+ }
2102
+ }
2103
+ function isMarkdownPath2(path4) {
2104
+ return path4.endsWith(".md");
2105
+ }
2106
+ function toDependencyKey(path4) {
2107
+ return path4.replace(/\.md$/, "");
2108
+ }
2109
+
2110
+ // src/content/content_change_set.ts
2111
+ function diffContentEntries(previous, current) {
2112
+ const currentFingerprints = new Map(
2113
+ current.map(({ entry, fingerprint }) => [entry.path, fingerprint])
2114
+ );
2115
+ const added = [];
2116
+ const changed = [];
2117
+ const unchanged = [];
2118
+ for (const [path4, fingerprint] of currentFingerprints) {
2119
+ const previousEntry = previous.entries[path4];
2120
+ if (!previousEntry) {
2121
+ added.push(path4);
2122
+ } else if (previousEntry.fingerprint !== fingerprint) {
2123
+ changed.push(path4);
2124
+ } else {
2125
+ unchanged.push(path4);
2126
+ }
2127
+ }
2128
+ const removed = Object.keys(previous.entries).filter(
2129
+ (path4) => !currentFingerprints.has(path4)
2130
+ );
2131
+ return sortChangeSet({ added, changed, removed, unchanged });
2132
+ }
2133
+ function allContentChanged(entries) {
2134
+ return sortChangeSet({
2135
+ added: entries.map(({ entry }) => entry.path),
2136
+ changed: [],
2137
+ removed: [],
2138
+ unchanged: []
2139
+ });
2140
+ }
2141
+ function hasContentChanges(changeSet) {
2142
+ return changeSet.added.length > 0 || changeSet.changed.length > 0 || changeSet.removed.length > 0;
2143
+ }
2144
+ function sortChangeSet(changeSet) {
2145
+ return {
2146
+ added: [...changeSet.added].sort(),
2147
+ changed: [...changeSet.changed].sort(),
2148
+ removed: [...changeSet.removed].sort(),
2149
+ unchanged: [...changeSet.unchanged].sort()
2150
+ };
2151
+ }
2152
+
2153
+ // src/content/content_build_coordinator.ts
2154
+ var ContentBuildCoordinator = class {
2155
+ constructor(dependencies) {
2156
+ this.dependencies = dependencies;
2157
+ }
2158
+ dependencies;
2159
+ preparation = null;
2160
+ getPreparation(incremental) {
2161
+ this.preparation ??= this.prepare(incremental);
2162
+ return this.preparation;
2163
+ }
2164
+ async commit(preparation, manifest) {
2165
+ const entriesBySlug = new Map(
2166
+ manifest.entries.map((entry) => [entry.slug, entry])
2167
+ );
2168
+ const state = {
2169
+ version: CONTENT_BUILD_STATE_VERSION,
2170
+ entries: Object.fromEntries(
2171
+ preparation.currentEntries.map(({ entry, fingerprint }) => {
2172
+ const manifestEntry = entriesBySlug.get(toSlug(entry.path));
2173
+ return [
2174
+ entry.path,
2175
+ {
2176
+ fingerprint,
2177
+ dependencies: collectDependencies2(manifestEntry)
2178
+ }
2179
+ ];
2180
+ })
2181
+ ),
2182
+ contentIndex: Object.fromEntries(
2183
+ [...manifest.contentIndex.entries()].sort(
2184
+ ([left], [right]) => left.localeCompare(right)
2185
+ )
2186
+ )
2187
+ };
2188
+ try {
2189
+ await saveContentBuildState(this.dependencies.buildStatePath, state);
2190
+ } catch {
2191
+ }
2192
+ }
2193
+ async prepare(incremental) {
2194
+ const currentEntries = await fingerprintContentEntries(
2195
+ await this.dependencies.getEntries(),
2196
+ (entry) => this.dependencies.readEntry(entry)
2197
+ );
2198
+ const previousState = incremental === false ? void 0 : await loadContentBuildState(this.dependencies.buildStatePath);
2199
+ const changeSet = previousState ? diffContentEntries(previousState, currentEntries) : allContentChanged(currentEntries);
2200
+ const affected = determineAffectedContent(
2201
+ changeSet,
2202
+ previousState,
2203
+ currentEntries.map(({ entry }) => entry.path)
2204
+ );
2205
+ this.dependencies.observability.tracer.event("build.incremental", {
2206
+ incremental: incremental !== false,
2207
+ added: changeSet.added.length,
2208
+ changed: changeSet.changed.length,
2209
+ removed: changeSet.removed.length,
2210
+ unchanged: changeSet.unchanged.length,
2211
+ affected: affected.direct.size + affected.dependent.size
2212
+ });
2213
+ return { previousState, currentEntries, changeSet };
2214
+ }
2215
+ };
2216
+ function toSlug(path4) {
2217
+ return path4.replace(/\.md$/, "");
2218
+ }
2219
+ function collectDependencies2(manifestEntry) {
2220
+ if (!manifestEntry) return [];
2221
+ return [
2222
+ ...new Set(
2223
+ manifestEntry.links.filter(
2224
+ (link) => link.slug !== null
2225
+ ).map((link) => link.slug)
2226
+ )
2227
+ ].sort();
2228
+ }
2229
+
2230
+ // src/content/content_entry_reader.ts
2231
+ var ContentEntryReader = class {
2232
+ constructor(source, observability) {
2233
+ this.source = source;
2234
+ this.observability = observability;
2235
+ }
2236
+ source;
2237
+ observability;
2238
+ entries = null;
2239
+ texts = /* @__PURE__ */ new Map();
2240
+ async getEntries() {
2241
+ this.entries ??= await this.observability.tracer.span(
2242
+ "content.scan",
2243
+ {},
2244
+ () => this.source.scan()
2245
+ );
2246
+ return this.entries;
2247
+ }
2248
+ async readText(logicalPath) {
2249
+ const entry = (await this.getEntries()).find(
2250
+ (current) => current.path === logicalPath
2251
+ );
2252
+ if (!entry) throw new Error(`Content entry was not found: ${logicalPath}`);
2253
+ return await this.read(entry);
2254
+ }
2255
+ read(entry) {
2256
+ const cached = this.texts.get(entry.path);
2257
+ if (cached) return cached;
2258
+ const content = this.source.read(entry).then(
2259
+ (value) => typeof value === "string" ? value : new TextDecoder().decode(value)
2260
+ );
2261
+ this.texts.set(entry.path, content);
2262
+ return content;
2263
+ }
2264
+ };
2265
+
2266
+ // src/utils/tags.ts
2267
+ function normalizeTag(raw) {
2268
+ const cleaned = raw.replace(/[/-]+$/, "");
2269
+ if (!cleaned) return null;
2270
+ const isPurelyNumeric = /^[\p{N}/\-_]+$/u.test(cleaned);
2271
+ return isPurelyNumeric ? null : cleaned;
2272
+ }
2273
+
2274
+ // src/content/content_metadata.ts
2275
+ var TAG_PATTERN = /(^|[\s([{"'])#([\p{L}\p{N}_\-/]+)/gu;
2276
+ function extractContentTags(markdown) {
2277
+ TAG_PATTERN.lastIndex = 0;
2278
+ const tags = [];
2279
+ for (let match = TAG_PATTERN.exec(markdown); match !== null; match = TAG_PATTERN.exec(markdown)) {
2280
+ const tag = normalizeTag(match[2] ?? "");
2281
+ if (tag) tags.push(tag);
2282
+ }
2283
+ return uniqueStrings(tags);
2284
+ }
2285
+ function normalizeFrontmatterTags(tags) {
2286
+ if (Array.isArray(tags)) {
2287
+ return tags.flatMap((tag) => normalizeFrontmatterTags(tag));
2288
+ }
2289
+ if (typeof tags !== "string") return [];
2290
+ return tags.split(/[\s,]+/).map((tag) => normalizeTag(tag.replace(/^#/, ""))).filter((tag) => tag !== null);
2291
+ }
2292
+ function extractFrontmatterAliases(markdown) {
2293
+ const frontmatter = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/);
2294
+ if (!frontmatter?.[1]) return [];
2295
+ const aliases = [];
2296
+ const lines = frontmatter[1].split(/\r?\n/);
2297
+ for (let i = 0; i < lines.length; i++) {
2298
+ const line = lines[i] ?? "";
2299
+ const inlineMatch = line.match(/^aliases?:\s*(.+)$/i);
2300
+ if (inlineMatch?.[1]) {
2301
+ aliases.push(...parseYamlScalarOrList(inlineMatch[1]));
2302
+ continue;
2303
+ }
2304
+ if (/^aliases?:\s*$/i.test(line)) {
2305
+ for (let j = i + 1; j < lines.length; j++) {
2306
+ const itemMatch = lines[j]?.match(/^\s*-\s*(.+)$/);
2307
+ if (!itemMatch?.[1]) break;
2308
+ aliases.push(stripYamlQuotes(itemMatch[1]));
2309
+ }
2310
+ }
2311
+ }
2312
+ return uniqueStrings(aliases.map((alias) => alias.trim()).filter(Boolean));
2313
+ }
2314
+ function parseYamlScalarOrList(value) {
2315
+ const trimmed = value.trim();
2316
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
2317
+ return trimmed.slice(1, -1).split(",").map(stripYamlQuotes).filter(Boolean);
2318
+ }
2319
+ return [stripYamlQuotes(trimmed)];
2320
+ }
2321
+ function stripYamlQuotes(value) {
2322
+ return value.trim().replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
2323
+ }
2324
+
2325
+ // src/content/content_index_builder.ts
2326
+ var ContentIndexBuilder = class {
2327
+ constructor(source) {
2328
+ this.source = source;
2329
+ }
2330
+ source;
2331
+ async build(contentEntries, read = (entry) => this.source.read(entry)) {
2332
+ const index = /* @__PURE__ */ new Map();
2333
+ for (const contentEntry of contentEntries) {
2334
+ await this.indexContentEntry(index, contentEntry, read);
2335
+ }
2336
+ return index;
2337
+ }
2338
+ async indexContentEntry(index, contentEntry, read) {
2339
+ const contentPath = contentEntry.path;
2340
+ const ext = contentPath.split(".").pop()?.toLowerCase() ?? "";
2341
+ const value = ext === "md" ? contentPath.replace(/\.md$/, "") : contentPath;
2342
+ const parts = value.split("/");
2343
+ for (let i = parts.length - 1; i >= 0; i--) {
2344
+ const rawSuffix = parts.slice(i).join("/");
2345
+ addIndexEntry(index, rawSuffix, value);
2346
+ if (ext !== "md") {
2347
+ addIndexEntry(index, rawSuffix.replace(/\.[^/.]+$/, ""), value);
2348
+ }
2349
+ }
2350
+ if (ext === "md") {
2351
+ const markdown = readText(await read(contentEntry));
2352
+ for (const alias of extractFrontmatterAliases(markdown)) {
2353
+ addIndexEntry(index, alias, value);
2354
+ }
2355
+ }
2356
+ }
2357
+ };
2358
+ function readText(content) {
2359
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
2360
+ }
2361
+ function addIndexEntry(index, key, value) {
2362
+ const normalizedKey = key.toLowerCase();
2363
+ if (!index.has(normalizedKey)) index.set(normalizedKey, value);
2364
+ }
2365
+
2366
+ // src/content/content_location_resolver.ts
2367
+ var ContentLocationResolver = class {
2368
+ constructor(dependencies) {
2369
+ this.dependencies = dependencies;
2370
+ }
2371
+ dependencies;
2372
+ locations = null;
2373
+ async getLocations() {
2374
+ if (this.locations) return this.locations;
2375
+ const [entries, contentIndex] = await Promise.all([
2376
+ this.dependencies.getEntries(),
2377
+ this.dependencies.getContentIndex()
2378
+ ]);
2379
+ await this.dependencies.pluginRuntime.startBuild(contentIndex);
2380
+ const inputs = await Promise.all(
2381
+ entries.filter((entry) => entry.path.endsWith(".md")).map(async (entry) => ({
2382
+ slug: toSlug2(entry.path),
2383
+ path: entry.path,
2384
+ markdown: await this.dependencies.readEntry(entry)
2385
+ }))
2386
+ );
2387
+ const locations = new Map(
2388
+ inputs.map((input) => [input.slug, resolveDefaultContentLocation(input)])
2389
+ );
2390
+ for (const location of await this.dependencies.pluginRuntime.resolveContentLocations(
2391
+ inputs,
2392
+ contentIndex
2393
+ )) {
2394
+ if (!locations.has(location.slug)) {
2395
+ throw new Error(
2396
+ `Content public location references an unknown slug: ${location.slug}`
2397
+ );
2398
+ }
2399
+ locations.set(location.slug, location);
2400
+ }
2401
+ await this.dependencies.pluginRuntime.extendContentLocations(
2402
+ inputs,
2403
+ locations,
2404
+ contentIndex
2405
+ );
2406
+ this.locations = locations;
2407
+ return locations;
2408
+ }
2409
+ async getPermalinks() {
2410
+ return new Map(
2411
+ Array.from(
2412
+ (await this.getLocations()).entries(),
2413
+ ([slug, location]) => [slug, location.permalink]
2414
+ )
2415
+ );
2416
+ }
2417
+ populateRedirects(manifest, locations) {
2418
+ for (const [slug, location] of locations) {
2419
+ for (const redirect of location.redirects ?? []) {
2420
+ manifest.redirects.set(redirect.path, { ...redirect, slug });
2421
+ }
2422
+ }
2423
+ }
2424
+ };
2425
+ function toSlug2(path4) {
2426
+ return path4.replace(/\.md$/, "");
2427
+ }
2428
+
2429
+ // src/content/file_system_content_source.ts
2430
+ import fs3 from "node:fs/promises";
2431
+ import path3 from "node:path";
2432
+ var FileSystemContentSource = class {
2433
+ constructor(contentDirectory, exclude = []) {
2434
+ this.contentDirectory = contentDirectory;
2435
+ this.exclude = exclude;
2436
+ }
2437
+ contentDirectory;
2438
+ exclude;
2439
+ async scan() {
2440
+ return await this.scanDirectory(this.contentDirectory, "");
2441
+ }
2442
+ async read(entry) {
2443
+ const filePath = this.resolveFilePath(entry.path);
2444
+ const stats = await fs3.lstat(filePath);
2445
+ if (!stats.isFile() || stats.isSymbolicLink()) {
2446
+ throw new Error(`Content entry is not a regular file: ${entry.path}`);
2447
+ }
2448
+ return await fs3.readFile(filePath);
2449
+ }
2450
+ async scanDirectory(directory, parentPath) {
2451
+ const directoryEntries = await fs3.readdir(directory, {
2452
+ withFileTypes: true
2453
+ });
2454
+ const entries = [];
2455
+ for (const directoryEntry of directoryEntries) {
2456
+ if (directoryEntry.isSymbolicLink()) continue;
2457
+ const logicalPath = normalizeContentPath(
2458
+ parentPath ? `${parentPath}/${directoryEntry.name}` : directoryEntry.name
2459
+ );
2460
+ const filePath = path3.join(directory, directoryEntry.name);
2461
+ if (directoryEntry.isDirectory()) {
2462
+ entries.push(...await this.scanDirectory(filePath, logicalPath));
2463
+ continue;
2464
+ }
2465
+ if (!directoryEntry.isFile() || this.isExcluded(logicalPath)) continue;
2466
+ const stats = await fs3.stat(filePath);
2467
+ entries.push({
2468
+ path: logicalPath,
2469
+ metadata: {
2470
+ modifiedAt: stats.mtimeMs,
2471
+ size: stats.size
2472
+ }
2473
+ });
2474
+ }
2475
+ return entries;
2476
+ }
2477
+ isExcluded(logicalPath) {
2478
+ if (isExcluded(this.exclude, logicalPath)) return true;
2479
+ return logicalPath.endsWith(".md") && isExcluded(this.exclude, logicalPath.slice(0, -3));
2480
+ }
2481
+ resolveFilePath(logicalPath) {
2482
+ const normalizedPath = normalizeLogicalPath(logicalPath);
2483
+ const filePath = path3.resolve(this.contentDirectory, normalizedPath);
2484
+ const relativePath = path3.relative(this.contentDirectory, filePath);
2485
+ if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
2486
+ throw new Error(`Invalid content path: ${logicalPath}`);
2487
+ }
2488
+ return filePath;
2489
+ }
2490
+ };
2491
+ function normalizeLogicalPath(logicalPath) {
2492
+ const normalizedPath = normalizeContentPath(logicalPath);
2493
+ if (normalizedPath.length === 0 || normalizedPath.startsWith("/") || path3.isAbsolute(normalizedPath) || path3.win32.isAbsolute(normalizedPath) || normalizedPath.split("/").some((segment) => segment === "..")) {
2494
+ throw new Error(`Invalid content path: ${logicalPath}`);
2495
+ }
2496
+ return normalizedPath;
2497
+ }
2498
+
2499
+ // src/content/content_links.ts
2500
+ var WIKILINK_PATTERN = /(!)?\[\[([^\]|#^]+)(?:[#^][^\]|]+)?(?:\|[^\]]+)?\]\]/g;
2501
+ function extractContentLinks(markdown, contentIndex) {
2502
+ WIKILINK_PATTERN.lastIndex = 0;
2503
+ const links = [];
2504
+ for (let match = WIKILINK_PATTERN.exec(markdown); match !== null; match = WIKILINK_PATTERN.exec(markdown)) {
2505
+ const rawTarget = match[2]?.trim();
2506
+ if (!rawTarget) continue;
2507
+ const resolved = contentIndex.get(rawTarget.toLowerCase()) ?? null;
2508
+ links.push({
2509
+ raw: rawTarget,
2510
+ slug: resolved,
2511
+ kind: resolved ? linkKind(resolved) : "unresolved",
2512
+ embed: match[1] === "!"
2513
+ });
2514
+ }
2515
+ return links;
2516
+ }
2517
+ function linkKind(value) {
2518
+ if (isImagePath(value)) return "image";
2519
+ if (isAttachmentPath(value)) return "attachment";
2520
+ return "note";
2521
+ }
2522
+
2523
+ // src/content/manifest_builder.ts
2524
+ var ManifestBuilder = class {
2525
+ createEntry(slug, markdown, processed, contentIndex, location) {
2526
+ const links = extractContentLinks(markdown, contentIndex);
2527
+ const assets = links.filter(
2528
+ (link) => (link.kind === "image" || link.kind === "attachment") && link.slug !== null
2529
+ ).map((link) => ({ path: link.slug }));
2530
+ return {
2531
+ slug,
2532
+ permalink: location.permalink,
2533
+ publicLocation: location,
2534
+ title: getManifestTitle(slug, processed.frontmatter.title),
2535
+ frontmatter: processed.frontmatter,
2536
+ html: processed.html,
2537
+ tags: uniqueStrings([
2538
+ ...normalizeFrontmatterTags(processed.frontmatter.tags),
2539
+ ...extractContentTags(markdown)
2540
+ ]),
2541
+ links,
2542
+ backlinks: [],
2543
+ assets: uniqueAssets(assets)
2544
+ };
2545
+ }
2546
+ build(entries, contentIndex) {
2547
+ const bySlug = new Map(entries.map((entry) => [entry.slug, entry]));
2548
+ const byPermalink = new Map(
2549
+ entries.map((entry) => [entry.permalink, entry])
2550
+ );
2551
+ const byTag = /* @__PURE__ */ new Map();
2552
+ const byAsset = /* @__PURE__ */ new Map();
2553
+ const outgoingLinks = /* @__PURE__ */ new Map();
2554
+ const incomingLinks = /* @__PURE__ */ new Map();
2555
+ for (const entry of entries) {
2556
+ outgoingLinks.set(entry.slug, entry.links);
2557
+ for (const tag of entry.tags) appendToMap(byTag, tag, entry);
2558
+ for (const asset of entry.assets) appendToMap(byAsset, asset.path, entry);
2559
+ for (const link of entry.links) {
2560
+ if (link.kind !== "note" || !link.slug || link.slug === entry.slug) {
2561
+ continue;
2562
+ }
2563
+ appendUniqueToMap(incomingLinks, link.slug, entry.slug);
2564
+ }
2565
+ }
2566
+ for (const entry of entries) {
2567
+ entry.backlinks = incomingLinks.get(entry.slug) ?? [];
2568
+ }
2569
+ const manifest = {
2570
+ entries,
2571
+ publicEntries: [...entries],
2572
+ bySlug,
2573
+ byPermalink,
2574
+ redirects: /* @__PURE__ */ new Map(),
2575
+ publicRedirects: /* @__PURE__ */ new Map(),
2576
+ byTag,
2577
+ byAsset,
2578
+ outgoingLinks,
2579
+ incomingLinks,
2580
+ contentIndex,
2581
+ assets: [],
2582
+ diagnostics: [],
2583
+ generatedOutputs: []
2584
+ };
2585
+ return { ...manifest, graph: createContentGraph(manifest) };
2586
+ }
2587
+ };
2588
+ function getManifestTitle(slug, title) {
2589
+ return typeof title === "string" && title.trim() ? title : slug;
2590
+ }
2591
+ function uniqueAssets(assets) {
2592
+ return Array.from(
2593
+ new Map(assets.map((asset) => [asset.path, asset])).values()
2594
+ );
2595
+ }
2596
+ function appendToMap(map, key, value) {
2597
+ const values = map.get(key) ?? [];
2598
+ values.push(value);
2599
+ map.set(key, values);
2600
+ }
2601
+ function appendUniqueToMap(map, key, value) {
2602
+ const values = map.get(key) ?? [];
2603
+ if (!values.includes(value)) values.push(value);
2604
+ map.set(key, values);
2605
+ }
2606
+
2607
+ // src/content/content_manager.ts
2608
+ var ContentManager = class {
2609
+ constructor(content, exclude = [], pipelineOptions = {}) {
2610
+ this.pipelineOptions = pipelineOptions;
2611
+ this.pipelineOptions = {
2612
+ ...pipelineOptions,
2613
+ plugins: pipelineOptions.plugins ?? pipelineOptions.config?.plugins
2614
+ };
2615
+ this.source = typeof content === "string" ? pipelineOptions.config?.content.source ?? new FileSystemContentSource(content, [
2616
+ ...exclude,
2617
+ CONTENT_BUILD_STATE_EXCLUDE
2618
+ ]) : content;
2619
+ this.pipelineOptions = {
2620
+ ...this.pipelineOptions,
2621
+ contentSource: this.source
2622
+ };
2623
+ this.contentIndexBuilder = new ContentIndexBuilder(this.source);
2624
+ this.pluginRuntime = new PluginRuntime(this.pipelineOptions);
2625
+ const buildStatePath = resolveContentBuildStatePath(
2626
+ pipelineOptions.config,
2627
+ typeof content === "string" ? content : void 0
2628
+ );
2629
+ const observability = this.observability();
2630
+ this.entryReader = new ContentEntryReader(this.source, observability);
2631
+ this.locationResolver = new ContentLocationResolver({
2632
+ getEntries: () => this.entryReader.getEntries(),
2633
+ readEntry: (entry) => this.entryReader.read(entry),
2634
+ getContentIndex: () => this.getContentIndex(),
2635
+ pluginRuntime: this.pluginRuntime
2636
+ });
2637
+ this.buildCoordinator = new ContentBuildCoordinator({
2638
+ buildStatePath,
2639
+ getEntries: () => this.entryReader.getEntries(),
2640
+ readEntry: (entry) => this.entryReader.read(entry),
2641
+ observability
2642
+ });
2643
+ }
2644
+ pipelineOptions;
2645
+ source;
2646
+ contentIndexBuilder;
2647
+ manifestBuilder = new ManifestBuilder();
2648
+ pluginRuntime;
2649
+ contentIndex = null;
2650
+ contentCache = /* @__PURE__ */ new Map();
2651
+ manifest = null;
2652
+ pipeline = null;
2653
+ entryReader;
2654
+ locationResolver;
2655
+ buildCoordinator;
2656
+ isBuildTime = false;
2657
+ async getAllPosts() {
2658
+ return (await this.scan()).filter((entry) => entry.path.endsWith(".md")).map((entry) => ({ slug: entry.path.replace(/\.md$/, "") }));
2659
+ }
2660
+ async scan() {
2661
+ return await this.entryReader.getEntries();
2662
+ }
2663
+ async getPost(slug) {
2664
+ return await this.entryReader.readText(`${slug}.md`);
2665
+ }
2666
+ async getContentIndex(preparation) {
2667
+ if (this.contentIndex) return this.contentIndex;
2668
+ if (preparation?.previousState && !hasContentChanges(preparation.changeSet)) {
2669
+ this.contentIndex = new Map(
2670
+ Object.entries(preparation.previousState.contentIndex)
2671
+ );
2672
+ return this.contentIndex;
2673
+ }
2674
+ this.contentIndex = await this.contentIndexBuilder.build(
2675
+ await this.entryReader.getEntries(),
2676
+ (entry) => this.entryReader.read(entry)
2677
+ );
2678
+ return this.contentIndex;
2679
+ }
2680
+ async getProcessedContent(slug) {
2681
+ const cached = this.contentCache.get(slug);
2682
+ if (cached) return cached;
2683
+ return await this.observability().tracer.span(
2684
+ "content.process",
2685
+ {
2686
+ contentPath: `${slug}.md`
2687
+ },
2688
+ async () => {
2689
+ const [contentIndex, rawPost] = await Promise.all([
2690
+ this.getContentIndex(),
2691
+ this.getPost(slug)
2692
+ ]);
2693
+ await this.pluginRuntime.startBuild(contentIndex);
2694
+ await this.pluginRuntime.runContentLoaded(slug, rawPost, contentIndex);
2695
+ this.pipeline ??= new Pipeline(
2696
+ contentIndex,
2697
+ await this.locationResolver.getPermalinks(),
2698
+ (postSlug) => this.getPost(postSlug),
2699
+ this.pipelineOptions,
2700
+ this.isBuildTime
2701
+ );
2702
+ const content = await this.pipeline.execute(
2703
+ rawPost,
2704
+ 0,
2705
+ /* @__PURE__ */ new Set([slug])
2706
+ );
2707
+ await this.pluginRuntime.runPostHook(
2708
+ "onPostParsed",
2709
+ slug,
2710
+ rawPost,
2711
+ content,
2712
+ contentIndex
2713
+ );
2714
+ await this.pluginRuntime.runPostHook(
2715
+ "onPostProcessed",
2716
+ slug,
2717
+ rawPost,
2718
+ content,
2719
+ contentIndex
2720
+ );
2721
+ this.contentCache.set(slug, content);
2722
+ return content;
2723
+ }
2724
+ );
2725
+ }
2726
+ async getManifest(options) {
2727
+ if (this.manifest) return this.manifest;
2728
+ this.enableBuildTime();
2729
+ const preparation = options ? await this.buildCoordinator.getPreparation(options.incremental) : void 0;
2730
+ return await this.observability().tracer.span(
2731
+ "content.manifest",
2732
+ {},
2733
+ async () => {
2734
+ const [posts, contentIndex, locations] = await Promise.all([
2735
+ this.getAllPosts(),
2736
+ this.getContentIndex(preparation),
2737
+ this.getContentLocations()
2738
+ ]);
2739
+ const entries = await Promise.all(
2740
+ posts.map(async (post) => {
2741
+ const [rawPost, processed] = await Promise.all([
2742
+ this.getPost(post.slug),
2743
+ this.getProcessedContent(post.slug)
2744
+ ]);
2745
+ const location = locations.get(post.slug);
2746
+ if (!location) {
2747
+ throw new Error(
2748
+ `Content public location was not resolved: ${post.slug}`
2749
+ );
2750
+ }
2751
+ return this.manifestBuilder.createEntry(
2752
+ post.slug,
2753
+ rawPost,
2754
+ processed,
2755
+ contentIndex,
2756
+ location
2757
+ );
2758
+ })
2759
+ );
2760
+ await this.pluginRuntime.runGraphHook(entries, contentIndex);
2761
+ const manifest = await this.observability().tracer.span(
2762
+ "content.graph",
2763
+ {},
2764
+ () => this.manifestBuilder.build(entries, contentIndex)
2765
+ );
2766
+ this.locationResolver.populateRedirects(manifest, locations);
2767
+ await this.pluginRuntime.runManifestCreated(manifest, contentIndex);
2768
+ this.applyPublicView(manifest);
2769
+ manifest.assets = this.pluginRuntime.collectAssets();
2770
+ manifest.diagnostics = [
2771
+ ...this.pluginRuntime.getDiagnostics(),
2772
+ ...await this.pluginRuntime.collectDiagnostics(contentIndex)
2773
+ ];
2774
+ await this.pluginRuntime.runBuildEnd(manifest, contentIndex);
2775
+ manifest.generatedOutputs = this.pluginRuntime.collectGeneratedOutputs();
2776
+ if (preparation)
2777
+ await this.buildCoordinator.commit(preparation, manifest);
2778
+ this.manifest = manifest;
2779
+ return manifest;
2780
+ }
2781
+ );
2782
+ }
2783
+ async build(options = {}) {
2784
+ return await this.getManifest(options);
2785
+ }
2786
+ async getBacklinks(targetSlug) {
2787
+ const graph = await this.getContentGraph();
2788
+ return graph.incomingSlugs(targetSlug).map((slug) => ({
2789
+ slug
2790
+ }));
2791
+ }
2792
+ async getContentGraph() {
2793
+ const manifest = await this.getManifest();
2794
+ return manifest.graph;
2795
+ }
2796
+ async getDiagnostics() {
2797
+ const manifest = await this.getManifest();
2798
+ return manifest.diagnostics;
2799
+ }
2800
+ async inspect() {
2801
+ const [entries, contentIndex] = await Promise.all([
2802
+ this.entryReader.getEntries(),
2803
+ this.getContentIndex()
2804
+ ]);
2805
+ const diagnostics = await this.pluginRuntime.collectDiagnostics(contentIndex);
2806
+ return { entries, contentIndex, diagnostics };
2807
+ }
2808
+ async dispose() {
2809
+ if (!this.contentIndex) return;
2810
+ await this.pluginRuntime.dispose(this.contentIndex);
2811
+ }
2812
+ async getContentLocations() {
2813
+ return await this.locationResolver.getLocations();
2814
+ }
2815
+ applyPublicView(manifest) {
2816
+ const strategy = this.pipelineOptions.config?.content.filters.publishStrategy ?? "explicit";
2817
+ manifest.publicEntries = manifest.entries.filter(
2818
+ (entry) => isPublishable(strategy, entry.frontmatter)
2819
+ );
2820
+ const publicSlugs = new Set(
2821
+ manifest.publicEntries.map((entry) => entry.slug)
2822
+ );
2823
+ manifest.publicRedirects = new Map(
2824
+ [...manifest.redirects].filter(
2825
+ ([, redirect]) => publicSlugs.has(redirect.slug)
2826
+ )
2827
+ );
2828
+ }
2829
+ enableBuildTime() {
2830
+ this.isBuildTime = true;
2831
+ this.pluginRuntime.enableBuildTime();
2832
+ }
2833
+ observability() {
2834
+ return this.pipelineOptions.observability ?? noopObservability;
2835
+ }
2836
+ };
2837
+
2838
+ // src/content/content_source.ts
2839
+ async function readContentSourceEntry(source, logicalPath) {
2840
+ const entry = await getContentSourceEntry(source, logicalPath);
2841
+ return entry ? await source.read(entry) : null;
2842
+ }
2843
+ async function getContentSourceEntry(source, logicalPath) {
2844
+ return (await source.scan()).find((candidate) => candidate.path === logicalPath) ?? null;
2845
+ }
2846
+
2847
+ // src/content/read_only_content_graph.ts
2848
+ async function readOnlyContentGraph(source, locations) {
2849
+ const entries = await source.scan();
2850
+ const contentIndex = await new ContentIndexBuilder(source).build(entries);
2851
+ const markdownEntries = entries.filter((entry) => entry.path.endsWith(".md")).toSorted((left, right) => left.path.localeCompare(right.path));
2852
+ const markdownContents = await Promise.all(
2853
+ markdownEntries.map(async (entry) => ({
2854
+ entry,
2855
+ markdown: await readText2(source, entry)
2856
+ }))
2857
+ );
2858
+ const graphEntries = markdownContents.map(
2859
+ ({ entry, markdown }) => createGraphEntry(entry, markdown, contentIndex, locations)
2860
+ );
2861
+ return new ManifestBuilder().build(graphEntries, contentIndex).graph;
2862
+ }
2863
+ function createGraphEntry(entry, markdown, contentIndex, locations) {
2864
+ const slug = entry.path.replace(/\.md$/, "");
2865
+ const location = locations.get(slug);
2866
+ if (!location) {
2867
+ throw new Error(`Content public location was not resolved: ${slug}`);
2868
+ }
2869
+ return {
2870
+ slug,
2871
+ permalink: location.permalink,
2872
+ publicLocation: location,
2873
+ title: slug,
2874
+ frontmatter: {},
2875
+ html: "",
2876
+ tags: [],
2877
+ links: extractContentLinks(markdown, contentIndex),
2878
+ backlinks: [],
2879
+ assets: []
2880
+ };
2881
+ }
2882
+ async function readText2(source, entry) {
2883
+ const content = await source.read(entry);
2884
+ return typeof content === "string" ? content : new TextDecoder().decode(content);
2885
+ }
2886
+
2887
+ // src/utils/html.ts
2888
+ function escapeHtml(value) {
2889
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2890
+ }
2891
+ function escapeHtmlAttribute(value) {
2892
+ return escapeHtml(value);
2893
+ }
2894
+
2895
+ // src/utils/text.ts
2896
+ function stripHtml(html) {
2897
+ return html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ");
2898
+ }
2899
+ function calculateReadingTime(html) {
2900
+ const text = stripHtml(html).trim();
2901
+ if (!text) return 1;
2902
+ const cjkCharacters = text.match(/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/gu)?.length ?? 0;
2903
+ const latinWords = text.replace(/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/gu, " ").match(/[\p{L}\p{N}]+/gu)?.length ?? 0;
2904
+ return Math.max(1, Math.ceil(cjkCharacters / 500 + latinWords / 220));
2905
+ }
2906
+
2907
+ // src/plugin/plugin_memo.ts
2908
+ import { createHash as createHash3 } from "node:crypto";
2909
+ function createPluginMemo(cache, options = {}) {
2910
+ return {
2911
+ async memo(key, dependencies, compute) {
2912
+ const memoKey = `${key}:${hash2(stableStringify(dependencies))}`;
2913
+ try {
2914
+ const cached = await cache.get(memoKey);
2915
+ if (cached !== void 0) return cached;
2916
+ } catch (error) {
2917
+ warn(options.logger, "Plugin cache read failed; recomputing.", key, error);
2918
+ }
2919
+ const value = await compute();
2920
+ try {
2921
+ await cache.set(memoKey, value);
2922
+ } catch (error) {
2923
+ warn(options.logger, "Plugin cache write failed; result not cached.", key, error);
2924
+ }
2925
+ return value;
2926
+ }
2927
+ };
2928
+ }
2929
+ function stableStringify(value) {
2930
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
2931
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
2932
+ const keys = Object.keys(value).sort();
2933
+ const body = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",");
2934
+ return `{${body}}`;
2935
+ }
2936
+ function hash2(value) {
2937
+ return createHash3("sha256").update(value).digest("hex");
2938
+ }
2939
+ function warn(logger, message, key, error) {
2940
+ logger?.warn(message, {
2941
+ memoKey: key,
2942
+ error: error instanceof Error ? error.message : String(error)
2943
+ });
2944
+ }
2945
+
2946
+ // src/types/plugin_asset.ts
2947
+ function createStyleAsset(pluginName) {
2948
+ return {
2949
+ pluginName,
2950
+ kind: "style",
2951
+ moduleSpecifier: `@riebeckite/plugin-${pluginName}/style.css`
2952
+ };
2953
+ }
2954
+ function createClientEntry(pluginName, exportName) {
2955
+ return {
2956
+ pluginName,
2957
+ moduleSpecifier: `@riebeckite/plugin-${pluginName}/client`,
2958
+ ...exportName === void 0 ? {} : { exportName }
2959
+ };
2960
+ }
2961
+
2962
+ // src/types/plugin_endpoint.ts
2963
+ function defineEndpoint(path4, handler, options = {}) {
2964
+ if (options.cacheControl === void 0) {
2965
+ return { path: path4, handler };
2966
+ }
2967
+ return {
2968
+ path: path4,
2969
+ handler: async (context) => {
2970
+ const response = await handler(context);
2971
+ return {
2972
+ ...response,
2973
+ headers: {
2974
+ ...response.headers,
2975
+ "Cache-Control": options.cacheControl
2976
+ }
2977
+ };
2978
+ }
2979
+ };
2980
+ }
2981
+ export {
2982
+ ATTACHMENTS_BASE_PATH,
2983
+ CompositeTraceSink,
2984
+ ConfigValidationError,
2985
+ ConsoleLogger,
2986
+ ContentManager,
2987
+ FileSystemContentSource,
2988
+ IMAGE_EXTENSIONS,
2989
+ NoopLogger,
2990
+ NoopTracer,
2991
+ Pipeline,
2992
+ PluginDependencyError,
2993
+ SinkTracer,
2994
+ attachmentUrl,
2995
+ buildContentCollections,
2996
+ buildGraphEdges,
2997
+ calculateReadingTime,
2998
+ createClientEntry,
2999
+ createContentGraph,
3000
+ createPluginMemo,
3001
+ createStyleAsset,
3002
+ createUnavailableGeneratedOutputSink,
3003
+ defineConfig,
3004
+ defineEndpoint,
3005
+ definePlugin,
3006
+ defineTheme,
3007
+ escapeHtml,
3008
+ escapeHtmlAttribute,
3009
+ extractFrontmatterAliases,
3010
+ fingerprintContentEntries,
3011
+ getContentSourceEntry,
3012
+ getExtension,
3013
+ getResolvedPluginMetadata,
3014
+ groupContentEntries,
3015
+ isAttachmentPath,
3016
+ isExcluded,
3017
+ isImagePath,
3018
+ isMarkdownPath,
3019
+ isPublishable,
3020
+ isPublished,
3021
+ layoutRadialGraph,
3022
+ loadContentBuildState,
3023
+ normalizeContentPath,
3024
+ normalizeGeneratedOutputPath,
3025
+ normalizeTag,
3026
+ queryContentEntries,
3027
+ queryContentPage,
3028
+ readContentBuildStateStatus,
3029
+ readContentSourceEntry,
3030
+ readOnlyContentGraph,
3031
+ resolveConfig,
3032
+ resolveConfigModule,
3033
+ resolveContentBuildStatePath,
3034
+ resolveContentQueryPagination,
3035
+ resolveDefaultContentLocation,
3036
+ resolvePlugins,
3037
+ stableStringify,
3038
+ stripHtml,
3039
+ uniqueStrings
3040
+ };