@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.
- package/LICENSE +201 -0
- package/README.md +94 -0
- package/README_ja.md +75 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +3040 -0
- package/dist/src/config.d.ts +8 -0
- package/dist/src/config_validation.d.ts +7 -0
- package/dist/src/content/affected_content.d.ts +7 -0
- package/dist/src/content/attachment.d.ts +7 -0
- package/dist/src/content/content_build_coordinator.d.ts +25 -0
- package/dist/src/content/content_build_state.d.ts +22 -0
- package/dist/src/content/content_build_state_store.d.ts +20 -0
- package/dist/src/content/content_change_set.d.ts +10 -0
- package/dist/src/content/content_collection.d.ts +78 -0
- package/dist/src/content/content_entry_reader.d.ts +12 -0
- package/dist/src/content/content_fingerprint.d.ts +3 -0
- package/dist/src/content/content_graph.d.ts +23 -0
- package/dist/src/content/content_index_builder.d.ts +7 -0
- package/dist/src/content/content_links.d.ts +2 -0
- package/dist/src/content/content_location.d.ts +2 -0
- package/dist/src/content/content_location_resolver.d.ts +18 -0
- package/dist/src/content/content_manager.d.ts +59 -0
- package/dist/src/content/content_metadata.d.ts +3 -0
- package/dist/src/content/content_query.d.ts +76 -0
- package/dist/src/content/content_source.d.ts +17 -0
- package/dist/src/content/file_system_content_source.d.ts +11 -0
- package/dist/src/content/graph_layout.d.ts +24 -0
- package/dist/src/content/image_extensions.d.ts +1 -0
- package/dist/src/content/manifest_builder.d.ts +6 -0
- package/dist/src/content/read_only_content_graph.d.ts +4 -0
- package/dist/src/observability.d.ts +70 -0
- package/dist/src/pipeline.d.ts +25 -0
- package/dist/src/plugin/plugin_cache.d.ts +22 -0
- package/dist/src/plugin/plugin_dependency.d.ts +2 -0
- package/dist/src/plugin/plugin_dependency_error.d.ts +20 -0
- package/dist/src/plugin/plugin_lifecycle.d.ts +11 -0
- package/dist/src/plugin/plugin_memo.d.ts +20 -0
- package/dist/src/plugin/plugin_runtime.d.ts +59 -0
- package/dist/src/types/config_validation.d.ts +7 -0
- package/dist/src/types/content_manifest.d.ts +87 -0
- package/dist/src/types/diagnostic.d.ts +15 -0
- package/dist/src/types/generated_output.d.ts +28 -0
- package/dist/src/types/plugin.d.ts +71 -0
- package/dist/src/types/plugin_asset.d.ts +13 -0
- package/dist/src/types/plugin_context.d.ts +85 -0
- package/dist/src/types/plugin_diagnostic.d.ts +5 -0
- package/dist/src/types/plugin_endpoint.d.ts +22 -0
- package/dist/src/types/plugin_head.d.ts +18 -0
- package/dist/src/types/plugin_pipeline.d.ts +20 -0
- package/dist/src/types/plugin_seo.d.ts +40 -0
- package/dist/src/types/post_content.d.ts +20 -0
- package/dist/src/types/publish_strategy.d.ts +19 -0
- package/dist/src/types/resolved_riebeckite_config.d.ts +26 -0
- package/dist/src/types/riebeckite_config.d.ts +23 -0
- package/dist/src/types/site_config.d.ts +14 -0
- package/dist/src/types/theme_config.d.ts +55 -0
- package/dist/src/utils/collections.d.ts +1 -0
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/tags.d.ts +1 -0
- package/dist/src/utils/text.d.ts +2 -0
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
+
};
|