cantip 0.1.0
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 +21 -0
- package/README.md +61 -0
- package/app/components/CanvasMount.tsx +62 -0
- package/app/components/CodeWrapToggle.tsx +78 -0
- package/app/components/FindOnPage.tsx +224 -0
- package/app/components/MobileBottomBar.tsx +93 -0
- package/app/components/MobileProjectsPanel.tsx +113 -0
- package/app/components/PageFloatingMenu.tsx +224 -0
- package/app/components/ProjectSwitcher.tsx +124 -0
- package/app/components/Search.tsx +930 -0
- package/app/components/ShortcutsHelp.tsx +113 -0
- package/app/components/Sidebar.tsx +1049 -0
- package/app/components/TabBar.tsx +227 -0
- package/app/components/Toc.tsx +129 -0
- package/app/components/TopBar.tsx +74 -0
- package/app/components/theme-toggle.tsx +71 -0
- package/app/components/ui/button.tsx +56 -0
- package/app/components/ui/card.tsx +55 -0
- package/app/components/ui/dropdown-menu.tsx +156 -0
- package/app/components/ui/input.tsx +21 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +155 -0
- package/app/generated/site.ts +19 -0
- package/app/generated/slots.ts +10 -0
- package/app/generated/theme.generated.css +60 -0
- package/app/lib/config/config.server.ts +50 -0
- package/app/lib/config/defaults.ts +120 -0
- package/app/lib/config/load.ts +82 -0
- package/app/lib/config/schema.ts +131 -0
- package/app/lib/config/site.ts +43 -0
- package/app/lib/content.server.ts +105 -0
- package/app/lib/projects.ts +86 -0
- package/app/lib/sidebar.server.ts +113 -0
- package/app/lib/site.ts +27 -0
- package/app/lib/slots.tsx +33 -0
- package/app/lib/tabs.tsx +128 -0
- package/app/lib/useKeyboardShortcuts.ts +149 -0
- package/app/lib/utils.ts +17 -0
- package/app/root.tsx +171 -0
- package/app/routes/$.tsx +158 -0
- package/app/routes/_index.tsx +60 -0
- package/app/styles/app.css +461 -0
- package/app/styles/obsidian.css +83 -0
- package/app/styles/tailwind.css +227 -0
- package/cli.js +119 -0
- package/components.json +21 -0
- package/dist/config.mjs +87 -0
- package/dist/generate-content.mjs +1665 -0
- package/package.json +112 -0
- package/scripts/build-search-index.ts +129 -0
- package/scripts/canonical.ts +34 -0
- package/scripts/canvas-to-md.ts +73 -0
- package/scripts/compile.ts +242 -0
- package/scripts/emit-config.ts +163 -0
- package/scripts/generate-content.ts +197 -0
- package/scripts/obsidian/files.ts +222 -0
- package/scripts/obsidian/fs.ts +34 -0
- package/scripts/obsidian/generate.ts +36 -0
- package/scripts/obsidian/html.ts +17 -0
- package/scripts/obsidian/logger.ts +10 -0
- package/scripts/obsidian/markdown.ts +56 -0
- package/scripts/obsidian/obsidian.ts +229 -0
- package/scripts/obsidian/path.ts +60 -0
- package/scripts/obsidian/rehype.ts +60 -0
- package/scripts/obsidian/remark.ts +712 -0
- package/scripts/obsidian/types.ts +31 -0
- package/vite.config.ts +62 -0
|
@@ -0,0 +1,1665 @@
|
|
|
1
|
+
// scripts/generate-content.ts
|
|
2
|
+
import fs9 from "node:fs/promises";
|
|
3
|
+
import path10 from "node:path";
|
|
4
|
+
|
|
5
|
+
// scripts/obsidian/types.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// scripts/obsidian/path.ts
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { slug } from "github-slugger";
|
|
11
|
+
function getExtension(filePath) {
|
|
12
|
+
return path.parse(filePath).ext;
|
|
13
|
+
}
|
|
14
|
+
function stripExtension(filePath) {
|
|
15
|
+
return path.parse(filePath).name;
|
|
16
|
+
}
|
|
17
|
+
function extractPathAndAnchor(filePathAndAnchor) {
|
|
18
|
+
const [filePath, fileAnchor] = filePathAndAnchor.split("#", 2);
|
|
19
|
+
return [filePath, fileAnchor];
|
|
20
|
+
}
|
|
21
|
+
function isAnchor(filePath) {
|
|
22
|
+
return filePath.startsWith("#");
|
|
23
|
+
}
|
|
24
|
+
function slugifyPath(filePath) {
|
|
25
|
+
const segments = filePath.split("/");
|
|
26
|
+
return segments.map((segment, index) => {
|
|
27
|
+
const isLastSegment = index === segments.length - 1;
|
|
28
|
+
if (!isLastSegment) {
|
|
29
|
+
return slug(segment);
|
|
30
|
+
}
|
|
31
|
+
const parsedPath = path.parse(segment);
|
|
32
|
+
return `${slug(parsedPath.name)}${parsedPath.ext}`;
|
|
33
|
+
}).join("/");
|
|
34
|
+
}
|
|
35
|
+
function slashify(filePath) {
|
|
36
|
+
const isExtendedLengthPath = filePath.startsWith("\\\\?\\");
|
|
37
|
+
if (isExtendedLengthPath) {
|
|
38
|
+
return filePath;
|
|
39
|
+
}
|
|
40
|
+
return filePath.replaceAll("\\", "/");
|
|
41
|
+
}
|
|
42
|
+
function stripLeadingSlash(href) {
|
|
43
|
+
if (href.startsWith("/"))
|
|
44
|
+
href = href.slice(1);
|
|
45
|
+
return href;
|
|
46
|
+
}
|
|
47
|
+
function stripTrailingSlash(href) {
|
|
48
|
+
if (href.endsWith("/"))
|
|
49
|
+
href = href.slice(0, -1);
|
|
50
|
+
return href;
|
|
51
|
+
}
|
|
52
|
+
function stripLeadingAndTrailingSlashes(href) {
|
|
53
|
+
return stripTrailingSlash(stripLeadingSlash(href));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// scripts/obsidian/types.ts
|
|
57
|
+
var obsidianConfigSchema = z.object({
|
|
58
|
+
configFolder: z.string().startsWith(".").default(".obsidian"),
|
|
59
|
+
copyFrontmatter: z.boolean().default(false),
|
|
60
|
+
ignore: z.array(z.string()).default([]),
|
|
61
|
+
math: z.object({
|
|
62
|
+
singleDollarTextMath: z.boolean().default(true)
|
|
63
|
+
}).prefault({}),
|
|
64
|
+
output: z.string().default("notes").refine(
|
|
65
|
+
(value) => {
|
|
66
|
+
const label = stripLeadingAndTrailingSlashes(value);
|
|
67
|
+
return label === "." || label !== "" && !label.startsWith("..");
|
|
68
|
+
},
|
|
69
|
+
{ error: "The `output` directory cannot be empty or start with '..'." }
|
|
70
|
+
),
|
|
71
|
+
skipGeneration: z.boolean().default(false),
|
|
72
|
+
vault: z.string()
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// scripts/obsidian/obsidian.ts
|
|
76
|
+
import fs4 from "node:fs/promises";
|
|
77
|
+
import path5 from "node:path";
|
|
78
|
+
import { z as z2 } from "zod";
|
|
79
|
+
import decodeUriComponent from "decode-uri-component";
|
|
80
|
+
import { slug as slug2 } from "github-slugger";
|
|
81
|
+
import { globby } from "globby";
|
|
82
|
+
import yaml2 from "yaml";
|
|
83
|
+
|
|
84
|
+
// scripts/obsidian/fs.ts
|
|
85
|
+
import fs from "node:fs/promises";
|
|
86
|
+
import path2 from "node:path";
|
|
87
|
+
async function isDirectory(path11) {
|
|
88
|
+
try {
|
|
89
|
+
const stats = await fs.stat(path11);
|
|
90
|
+
return stats.isDirectory();
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function isFile(path11) {
|
|
96
|
+
try {
|
|
97
|
+
const stats = await fs.stat(path11);
|
|
98
|
+
return stats.isFile();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function ensureDirectory(path11) {
|
|
104
|
+
return fs.mkdir(path11, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
function removeDirectory(path11) {
|
|
107
|
+
return fs.rm(path11, { force: true, recursive: true });
|
|
108
|
+
}
|
|
109
|
+
async function copyFile(sourcePath, destinationPath) {
|
|
110
|
+
const dirPath = path2.dirname(destinationPath);
|
|
111
|
+
await ensureDirectory(dirPath);
|
|
112
|
+
return fs.copyFile(sourcePath, destinationPath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// scripts/obsidian/files.ts
|
|
116
|
+
import fs3 from "node:fs/promises";
|
|
117
|
+
import path4 from "node:path";
|
|
118
|
+
|
|
119
|
+
// scripts/obsidian/markdown.ts
|
|
120
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
121
|
+
import { remark } from "remark";
|
|
122
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
123
|
+
import remarkGfm from "remark-gfm";
|
|
124
|
+
import remarkMath from "remark-math";
|
|
125
|
+
import { VFile } from "vfile";
|
|
126
|
+
|
|
127
|
+
// scripts/obsidian/remark.ts
|
|
128
|
+
import fs2 from "node:fs";
|
|
129
|
+
import path3 from "node:path";
|
|
130
|
+
import { toHtml } from "hast-util-to-html";
|
|
131
|
+
import isAbsoluteUrl from "is-absolute-url";
|
|
132
|
+
import { findAndReplace } from "mdast-util-find-and-replace";
|
|
133
|
+
import { toHast } from "mdast-util-to-hast";
|
|
134
|
+
import { customAlphabet } from "nanoid";
|
|
135
|
+
import { CONTINUE, EXIT, SKIP, visit } from "unist-util-visit";
|
|
136
|
+
import yaml from "yaml";
|
|
137
|
+
|
|
138
|
+
// scripts/obsidian/html.ts
|
|
139
|
+
import { rehype } from "rehype";
|
|
140
|
+
import rehypeMermaid from "rehype-mermaid";
|
|
141
|
+
var processor = rehype().data("settings", {
|
|
142
|
+
fragment: true,
|
|
143
|
+
closeSelfClosing: true
|
|
144
|
+
}).use(rehypeMermaid, {
|
|
145
|
+
dark: true,
|
|
146
|
+
strategy: "img-svg"
|
|
147
|
+
});
|
|
148
|
+
async function transformHtmlToString(html) {
|
|
149
|
+
const file = await processor.process(html);
|
|
150
|
+
return String(file);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// scripts/obsidian/remark.ts
|
|
154
|
+
var generateAssetImportId = customAlphabet("abcdefghijklmnopqrstuvwxyz", 6);
|
|
155
|
+
var highlightReplacementRegex = /==(?<highlight>(?:(?!==).)+)==/g;
|
|
156
|
+
var commentReplacementRegex = /%%(?<comment>(?:(?!%%).)+)%%/gs;
|
|
157
|
+
var wikilinkReplacementRegex = /!?\[\[(?<url>(?:(?![[\]|]).)+)(?:\|(?<maybeText>(?:(?![[\]]).)+))?]]/g;
|
|
158
|
+
var tagReplacementRegex = /(?:^|\s)#(?<tag>[\w/-]+)/g;
|
|
159
|
+
var calloutRegex = /^\[!(?<type>\w+)][+-]? ?(?<title>.*)$/;
|
|
160
|
+
var imageSizeRegex = /^(?:(?<altText>.*)\|)?(?:(?<widthOnly>\d+)|(?:(?<width>\d+)x(?<height>\d+)))$/;
|
|
161
|
+
var mdxNonClosingVoidElementRegex = /<(?<tag>br|hr)(?<attrs>[^/>]*)>/g;
|
|
162
|
+
function remarkObsidian() {
|
|
163
|
+
return async function transformer(tree, file) {
|
|
164
|
+
const obsidianFrontmatter = getObsidianFrontmatter(tree);
|
|
165
|
+
if (obsidianFrontmatter && obsidianFrontmatter.publish === false) {
|
|
166
|
+
file.data.skip = true;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
markBlankGaps(tree);
|
|
170
|
+
handleReplacements(tree, file);
|
|
171
|
+
await handleMermaid(tree, file);
|
|
172
|
+
await handleImagesAndNoteEmbeds(tree, file);
|
|
173
|
+
visit(tree, (node, index, parent) => {
|
|
174
|
+
const context = { file, index, parent };
|
|
175
|
+
switch (node.type) {
|
|
176
|
+
case "math":
|
|
177
|
+
case "inlineMath": {
|
|
178
|
+
return handleMath(context);
|
|
179
|
+
}
|
|
180
|
+
case "link": {
|
|
181
|
+
return handleLinks(node, context);
|
|
182
|
+
}
|
|
183
|
+
case "blockquote": {
|
|
184
|
+
return handleBlockquotes(node, context);
|
|
185
|
+
}
|
|
186
|
+
default: {
|
|
187
|
+
return CONTINUE;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
handleFrontmatter(tree, file, obsidianFrontmatter);
|
|
192
|
+
handleImports(tree, file);
|
|
193
|
+
if (file.data.isMdx) {
|
|
194
|
+
closeVoidElements(tree);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
var BLANK_GAP_MARKER = "<!--blank-gap-->";
|
|
199
|
+
var FLOW_PARENTS = /* @__PURE__ */ new Set(["root", "blockquote", "listItem"]);
|
|
200
|
+
function markBlankGaps(tree) {
|
|
201
|
+
function walk(parent) {
|
|
202
|
+
const children = parent.children;
|
|
203
|
+
for (let i = 0; i < children.length; i++) {
|
|
204
|
+
const node = children[i];
|
|
205
|
+
const prev = children[i - 1];
|
|
206
|
+
if (prev && node.type !== "html" && prev.position && node.position && node.position.start.line - prev.position.end.line >= 2) {
|
|
207
|
+
children.splice(i, 0, { type: "html", value: BLANK_GAP_MARKER });
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
if (node.type === "list") {
|
|
211
|
+
for (const item of node.children)
|
|
212
|
+
walk(item);
|
|
213
|
+
} else if (FLOW_PARENTS.has(node.type) && "children" in node) {
|
|
214
|
+
walk(node);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
walk(tree);
|
|
219
|
+
}
|
|
220
|
+
function getObsidianFrontmatter(tree) {
|
|
221
|
+
for (const node of tree.children) {
|
|
222
|
+
if (node.type !== "yaml") {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const obsidianFrontmatter = parseObsidianFrontmatter(node.value);
|
|
226
|
+
if (obsidianFrontmatter) {
|
|
227
|
+
return obsidianFrontmatter;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
function handleFrontmatter(tree, file, obsidianFrontmatter) {
|
|
233
|
+
if (file.data.embedded) {
|
|
234
|
+
for (const [index, node] of tree.children.entries()) {
|
|
235
|
+
if (node.type !== "yaml") {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
tree.children.splice(index, 1);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
let hasFrontmatter = false;
|
|
244
|
+
for (const node of tree.children) {
|
|
245
|
+
if (node.type !== "yaml") {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
node.value = getFrontmatterNodeValue(file, obsidianFrontmatter);
|
|
249
|
+
hasFrontmatter = true;
|
|
250
|
+
if (obsidianFrontmatter?.aliases && obsidianFrontmatter.aliases.length > 0) {
|
|
251
|
+
file.data.aliases = obsidianFrontmatter.aliases;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (!hasFrontmatter) {
|
|
256
|
+
tree.children.unshift({ type: "yaml", value: getFrontmatterNodeValue(file) });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function handleImports(_tree, _file) {
|
|
260
|
+
}
|
|
261
|
+
function handleReplacements(tree, file) {
|
|
262
|
+
findAndReplace(tree, [
|
|
263
|
+
[
|
|
264
|
+
highlightReplacementRegex,
|
|
265
|
+
(_match, highlight) => ({
|
|
266
|
+
type: "html",
|
|
267
|
+
value: `<mark class="obs-highlight">${highlight}</mark>`
|
|
268
|
+
})
|
|
269
|
+
],
|
|
270
|
+
[commentReplacementRegex, null],
|
|
271
|
+
[
|
|
272
|
+
wikilinkReplacementRegex,
|
|
273
|
+
(match, url, maybeText) => {
|
|
274
|
+
ensureTransformContext(file);
|
|
275
|
+
let fileUrl;
|
|
276
|
+
let text = maybeText ?? url;
|
|
277
|
+
if (isAnchor(url)) {
|
|
278
|
+
fileUrl = slugifyObsidianAnchor(url);
|
|
279
|
+
text = maybeText ?? url.slice(isObsidianBlockAnchor(url) ? 2 : 1);
|
|
280
|
+
} else {
|
|
281
|
+
const [urlPath, urlAnchor] = extractPathAndAnchor(url);
|
|
282
|
+
switch (file.data.vault.options.linkFormat) {
|
|
283
|
+
case "relative": {
|
|
284
|
+
fileUrl = getFileUrl(file.data.output, getRelativeFilePath(file, urlPath), urlAnchor);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "absolute":
|
|
288
|
+
case "shortest": {
|
|
289
|
+
const matchingFile = file.data.files.find(
|
|
290
|
+
(vaultFile) => vaultFile.isEqualStem(urlPath) || vaultFile.isEqualFileName(urlPath)
|
|
291
|
+
);
|
|
292
|
+
fileUrl = getFileUrl(
|
|
293
|
+
file.data.output,
|
|
294
|
+
matchingFile ? getFilePathFromVaultFile(matchingFile, urlPath) : urlPath,
|
|
295
|
+
urlAnchor
|
|
296
|
+
);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (match.startsWith("!")) {
|
|
302
|
+
const isMarkdown = isMarkdownFile(url, file);
|
|
303
|
+
return {
|
|
304
|
+
type: "image",
|
|
305
|
+
url: isMarkdown ? url : fileUrl,
|
|
306
|
+
alt: text,
|
|
307
|
+
data: { isAssetResolved: !isMarkdown }
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
children: [{ type: "text", value: text }],
|
|
312
|
+
type: "link",
|
|
313
|
+
url: fileUrl
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
[
|
|
318
|
+
tagReplacementRegex,
|
|
319
|
+
(_match, tag) => {
|
|
320
|
+
if (/^\d+$/.test(tag)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
type: "html",
|
|
325
|
+
value: ` <span class="obs-tag">#${tag}</span>`
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
function handleMath({ file }) {
|
|
332
|
+
file.data.includeKatexStyles = true;
|
|
333
|
+
return SKIP;
|
|
334
|
+
}
|
|
335
|
+
function handleLinks(node, { file }) {
|
|
336
|
+
ensureTransformContext(file);
|
|
337
|
+
if (file.data.vault.options.linkSyntax === "wikilink" || isAbsoluteUrl(node.url) || !file.dirname) {
|
|
338
|
+
return SKIP;
|
|
339
|
+
}
|
|
340
|
+
if (isAnchor(node.url)) {
|
|
341
|
+
node.url = slugifyObsidianAnchor(node.url);
|
|
342
|
+
return SKIP;
|
|
343
|
+
}
|
|
344
|
+
const url = path3.basename(decodeURIComponent(node.url));
|
|
345
|
+
const [urlPath, urlAnchor] = extractPathAndAnchor(url);
|
|
346
|
+
const matchingFile = file.data.files.find((vaultFile) => vaultFile.isEqualFileName(urlPath));
|
|
347
|
+
if (!matchingFile) {
|
|
348
|
+
return SKIP;
|
|
349
|
+
}
|
|
350
|
+
switch (file.data.vault.options.linkFormat) {
|
|
351
|
+
case "relative": {
|
|
352
|
+
node.url = getFileUrl(file.data.output, getRelativeFilePath(file, node.url), urlAnchor);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case "absolute":
|
|
356
|
+
case "shortest": {
|
|
357
|
+
node.url = getFileUrl(file.data.output, getFilePathFromVaultFile(matchingFile, node.url), urlAnchor);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return SKIP;
|
|
362
|
+
}
|
|
363
|
+
async function handleImages(node, context) {
|
|
364
|
+
const { file } = context;
|
|
365
|
+
ensureTransformContext(file);
|
|
366
|
+
if (!file.dirname) {
|
|
367
|
+
return SKIP;
|
|
368
|
+
}
|
|
369
|
+
if (isAbsoluteUrl(node.url)) {
|
|
370
|
+
if (isObsidianFile(node.url, "image")) {
|
|
371
|
+
handleImagesWithSize(node, context, "external");
|
|
372
|
+
}
|
|
373
|
+
return SKIP;
|
|
374
|
+
}
|
|
375
|
+
if (isMarkdownFile(node.url, file)) {
|
|
376
|
+
replaceNode(context, await getMarkdownFileNode(file, node.url));
|
|
377
|
+
return SKIP;
|
|
378
|
+
}
|
|
379
|
+
let fileUrl = node.url;
|
|
380
|
+
if (!node.data?.isAssetResolved) {
|
|
381
|
+
switch (file.data.vault.options.linkFormat) {
|
|
382
|
+
case "relative": {
|
|
383
|
+
fileUrl = getFileUrl(file.data.output, getRelativeFilePath(file, node.url));
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
case "absolute": {
|
|
387
|
+
fileUrl = getFileUrl(file.data.output, slugifyObsidianPath(node.url));
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case "shortest": {
|
|
391
|
+
const url = path3.basename(decodeURIComponent(node.url));
|
|
392
|
+
const [urlPath] = extractPathAndAnchor(url);
|
|
393
|
+
const matchingFile = file.data.files.find((vaultFile) => vaultFile.isEqualFileName(urlPath));
|
|
394
|
+
if (!matchingFile) {
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
fileUrl = getFileUrl(file.data.output, getFilePathFromVaultFile(matchingFile, node.url));
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (isCustomFile(node.url)) {
|
|
403
|
+
replaceNode(context, getCustomFileNode(fileUrl));
|
|
404
|
+
return SKIP;
|
|
405
|
+
}
|
|
406
|
+
node.url = fileUrl;
|
|
407
|
+
if (isAssetFile(node.url)) {
|
|
408
|
+
handleImagesWithSize(node, context, "asset");
|
|
409
|
+
}
|
|
410
|
+
return SKIP;
|
|
411
|
+
}
|
|
412
|
+
function handleBlockquotes(node, context) {
|
|
413
|
+
const [firstChild, ...otherChildren] = node.children;
|
|
414
|
+
if (firstChild?.type !== "paragraph") {
|
|
415
|
+
return SKIP;
|
|
416
|
+
}
|
|
417
|
+
const [firstGrandChild, ...otherGrandChildren] = firstChild.children;
|
|
418
|
+
if (firstGrandChild?.type !== "text") {
|
|
419
|
+
return SKIP;
|
|
420
|
+
}
|
|
421
|
+
const [firstLine, ...otherLines] = firstGrandChild.value.split(/\r?\n/);
|
|
422
|
+
if (!firstLine) {
|
|
423
|
+
return SKIP;
|
|
424
|
+
}
|
|
425
|
+
const match = calloutRegex.exec(firstLine);
|
|
426
|
+
const { title, type } = match?.groups ?? {};
|
|
427
|
+
if (!match || !type) {
|
|
428
|
+
return SKIP;
|
|
429
|
+
}
|
|
430
|
+
const calloutType = getCalloutType(type);
|
|
431
|
+
const calloutTitle = title?.trim() || type.charAt(0).toUpperCase() + type.slice(1);
|
|
432
|
+
const escapedTitle = calloutTitle.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
433
|
+
const openTag = `<div class="callout callout-${calloutType}"><p class="callout-title">${escapedTitle}</p>`;
|
|
434
|
+
const contentChildren = [];
|
|
435
|
+
if (otherLines.length > 0) {
|
|
436
|
+
contentChildren.push({ type: "text", value: otherLines.join("\n") });
|
|
437
|
+
}
|
|
438
|
+
contentChildren.push(...otherGrandChildren);
|
|
439
|
+
const aside = [
|
|
440
|
+
{ type: "html", value: openTag }
|
|
441
|
+
];
|
|
442
|
+
if (contentChildren.length > 0) {
|
|
443
|
+
aside.push({
|
|
444
|
+
type: "paragraph",
|
|
445
|
+
children: contentChildren
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
aside.push(...otherChildren);
|
|
449
|
+
aside.push({ type: "html", value: "</div>" });
|
|
450
|
+
replaceNode(context, aside);
|
|
451
|
+
return CONTINUE;
|
|
452
|
+
}
|
|
453
|
+
async function handleMermaid(tree, file) {
|
|
454
|
+
const mermaidNodes = [];
|
|
455
|
+
visit(tree, "code", (node, index, parent) => {
|
|
456
|
+
if (node.lang === "mermaid") {
|
|
457
|
+
mermaidNodes.push([node, { file, index, parent }]);
|
|
458
|
+
return SKIP;
|
|
459
|
+
}
|
|
460
|
+
return CONTINUE;
|
|
461
|
+
});
|
|
462
|
+
await Promise.all(
|
|
463
|
+
mermaidNodes.map(async ([node, context]) => {
|
|
464
|
+
const html = toHtml(toHast(node));
|
|
465
|
+
const processedHtml = await transformHtmlToString(html);
|
|
466
|
+
replaceNode(context, { type: "html", value: processedHtml });
|
|
467
|
+
})
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
async function handleImagesAndNoteEmbeds(tree, file) {
|
|
471
|
+
const imageNodes = [];
|
|
472
|
+
visit(tree, "image", (node, index, parent) => {
|
|
473
|
+
imageNodes.push([node, { file, index, parent }]);
|
|
474
|
+
return SKIP;
|
|
475
|
+
});
|
|
476
|
+
await Promise.all(
|
|
477
|
+
imageNodes.map(async ([node, context]) => {
|
|
478
|
+
await handleImages(node, context);
|
|
479
|
+
})
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
function getFrontmatterNodeValue(file, obsidianFrontmatter) {
|
|
483
|
+
let frontmatter = {
|
|
484
|
+
title: file.stem
|
|
485
|
+
};
|
|
486
|
+
if (obsidianFrontmatter && file.data.copyFrontmatter) {
|
|
487
|
+
const { cover, image, description, permalink, tags, publish, aliases, ...restFrontmatter } = obsidianFrontmatter.raw;
|
|
488
|
+
frontmatter = { ...frontmatter, ...restFrontmatter };
|
|
489
|
+
}
|
|
490
|
+
if (obsidianFrontmatter?.description && obsidianFrontmatter.description.length > 0) {
|
|
491
|
+
frontmatter.description = obsidianFrontmatter.description;
|
|
492
|
+
}
|
|
493
|
+
if (obsidianFrontmatter?.permalink && obsidianFrontmatter.permalink.length > 0) {
|
|
494
|
+
frontmatter.permalink = obsidianFrontmatter.permalink;
|
|
495
|
+
}
|
|
496
|
+
if (obsidianFrontmatter?.tags && obsidianFrontmatter.tags.length > 0) {
|
|
497
|
+
frontmatter.tags = obsidianFrontmatter.tags;
|
|
498
|
+
}
|
|
499
|
+
const { title, ...frontmatterWithoutTitle } = frontmatter;
|
|
500
|
+
let result = yaml.stringify({ title }, { version: "1.1" });
|
|
501
|
+
if (Object.keys(frontmatterWithoutTitle).length > 0) {
|
|
502
|
+
result += yaml.stringify(frontmatterWithoutTitle);
|
|
503
|
+
}
|
|
504
|
+
return result.trim();
|
|
505
|
+
}
|
|
506
|
+
function getFileUrl(output, filePath, anchor) {
|
|
507
|
+
return `${path3.posix.join(path3.posix.sep, output, slugifyObsidianPath(filePath))}${slugifyObsidianAnchor(anchor ?? "")}`;
|
|
508
|
+
}
|
|
509
|
+
function getRelativeFilePath(file, relativePath) {
|
|
510
|
+
ensureTransformContext(file);
|
|
511
|
+
return path3.posix.join(getObsidianRelativePath(file.data.vault, file.dirname), relativePath);
|
|
512
|
+
}
|
|
513
|
+
function getFilePathFromVaultFile(vaultFile, url) {
|
|
514
|
+
return vaultFile.uniqueFileName ? vaultFile.slug : slugifyObsidianPath(url);
|
|
515
|
+
}
|
|
516
|
+
function isMarkdownFile(filePath, file) {
|
|
517
|
+
return file.data.vault?.options.linkSyntax === "markdown" && filePath.endsWith(".md") || getExtension(filePath).length === 0;
|
|
518
|
+
}
|
|
519
|
+
function handleImagesWithSize(node, context, type) {
|
|
520
|
+
if (!node.alt) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const match = imageSizeRegex.exec(node.alt);
|
|
524
|
+
const { altText, width, widthOnly, height } = match?.groups ?? {};
|
|
525
|
+
if (widthOnly === void 0 && width === void 0) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const imgAltText = altText ?? "";
|
|
529
|
+
const imgWidth = widthOnly ?? width;
|
|
530
|
+
const imgHeight = height ?? "auto";
|
|
531
|
+
const imgStyle = height === void 0 ? "" : ` style="height: ${height}px !important;"`;
|
|
532
|
+
replaceNode(context, {
|
|
533
|
+
type: "html",
|
|
534
|
+
value: `<img src="${node.url}" alt="${imgAltText}" width="${imgWidth}" height="${imgHeight}"${imgStyle} />`
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function isCustomFile(filePath) {
|
|
538
|
+
return isObsidianFile(filePath) && !isObsidianFile(filePath, "image");
|
|
539
|
+
}
|
|
540
|
+
function getCustomFileNode(filePath) {
|
|
541
|
+
if (isObsidianFile(filePath, "audio")) {
|
|
542
|
+
return {
|
|
543
|
+
type: "html",
|
|
544
|
+
value: `<audio class="obs-embed-audio" controls src="${filePath}"></audio>`
|
|
545
|
+
};
|
|
546
|
+
} else if (isObsidianFile(filePath, "video")) {
|
|
547
|
+
return {
|
|
548
|
+
type: "html",
|
|
549
|
+
value: `<video class="obs-embed-video" controls src="${filePath}"></video>`
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
type: "html",
|
|
554
|
+
value: `<iframe class="obs-embed-pdf" src="${filePath}"></iframe>`
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
async function getMarkdownFileNode(file, fileUrl) {
|
|
558
|
+
ensureTransformContext(file);
|
|
559
|
+
const [fileName, ...anchorSegments] = fileUrl.split("#");
|
|
560
|
+
const fileAnchor = anchorSegments.join("#");
|
|
561
|
+
const fileExt = file.data.vault.options.linkSyntax === "wikilink" ? ".md" : "";
|
|
562
|
+
const filePath = decodeURIComponent(
|
|
563
|
+
file.data.vault.options.linkFormat === "relative" ? getRelativeFilePath(file, fileName ?? fileUrl) : fileName ?? fileUrl
|
|
564
|
+
);
|
|
565
|
+
const url = path3.posix.join(path3.posix.sep, `${filePath}${fileExt}`);
|
|
566
|
+
const matchingFile = file.data.files.find(
|
|
567
|
+
(vaultFile) => vaultFile.path === url || vaultFile.isEqualStem(filePath) || vaultFile.isEqualFileName(filePath)
|
|
568
|
+
);
|
|
569
|
+
if (!matchingFile) {
|
|
570
|
+
return { type: "text", value: "" };
|
|
571
|
+
}
|
|
572
|
+
const content = fs2.readFileSync(matchingFile.fsPath, "utf8");
|
|
573
|
+
const root = await transformMarkdownToAST(matchingFile.fsPath, content, { ...file.data, embedded: true });
|
|
574
|
+
if (fileAnchor) {
|
|
575
|
+
root.children = extractMarkdownSection(root, fileAnchor);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
type: "blockquote",
|
|
579
|
+
children: [
|
|
580
|
+
{
|
|
581
|
+
type: "html",
|
|
582
|
+
value: `<strong>${matchingFile.stem}</strong>`
|
|
583
|
+
},
|
|
584
|
+
...root.children
|
|
585
|
+
]
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function replaceNode({ index, parent }, replacement) {
|
|
589
|
+
if (!parent || index === void 0) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
parent.children.splice(index, 1, ...Array.isArray(replacement) ? replacement : [replacement]);
|
|
593
|
+
}
|
|
594
|
+
function extractMarkdownSection(root, sectionAnchor) {
|
|
595
|
+
const children = [];
|
|
596
|
+
visit(root, (node, index, parent) => {
|
|
597
|
+
switch (node.type) {
|
|
598
|
+
case "heading": {
|
|
599
|
+
if (!parent || index === void 0)
|
|
600
|
+
return CONTINUE;
|
|
601
|
+
const headingText = node.children.find((child) => child.type === "text")?.value;
|
|
602
|
+
if (headingText !== sectionAnchor)
|
|
603
|
+
return CONTINUE;
|
|
604
|
+
children.push(node);
|
|
605
|
+
let nextNode = parent.children[index + 1];
|
|
606
|
+
while (nextNode && (nextNode.type !== "heading" || nextNode.depth > node.depth)) {
|
|
607
|
+
children.push(nextNode);
|
|
608
|
+
nextNode = parent.children[index + children.length];
|
|
609
|
+
}
|
|
610
|
+
return EXIT;
|
|
611
|
+
}
|
|
612
|
+
default: {
|
|
613
|
+
return CONTINUE;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
return children;
|
|
618
|
+
}
|
|
619
|
+
function closeVoidElements(tree) {
|
|
620
|
+
visit(tree, "html", (node) => {
|
|
621
|
+
node.value = node.value.replaceAll(mdxNonClosingVoidElementRegex, "<$<tag>$<attrs>/>");
|
|
622
|
+
return SKIP;
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
function ensureTransformContext(file) {
|
|
626
|
+
if (!file.dirname || !file.data.files || file.data.output === void 0 || !file.data.vault) {
|
|
627
|
+
throw new Error("Invalid transform context.");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// scripts/obsidian/markdown.ts
|
|
632
|
+
var processor2;
|
|
633
|
+
async function transformMarkdownToString(filePath, markdown, context) {
|
|
634
|
+
const file = await getProcessor(context).process(getVFile(filePath, markdown, context));
|
|
635
|
+
return {
|
|
636
|
+
aliases: file.data.aliases,
|
|
637
|
+
content: String(file),
|
|
638
|
+
skip: file.data.skip === true,
|
|
639
|
+
type: file.data.isMdx === true ? "mdx" : "markdown"
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async function transformMarkdownToAST(filePath, markdown, context) {
|
|
643
|
+
const { content } = await transformMarkdownToString(filePath, markdown, context);
|
|
644
|
+
return fromMarkdown(content);
|
|
645
|
+
}
|
|
646
|
+
function getProcessor(context) {
|
|
647
|
+
processor2 ??= remark().data("settings", { resourceLink: true }).use(remarkGfm).use(remarkMath, { singleDollarTextMath: context.singleDollarTextMath }).use(remarkFrontmatter).use(remarkObsidian);
|
|
648
|
+
return processor2;
|
|
649
|
+
}
|
|
650
|
+
function getVFile(filePath, markdown, context) {
|
|
651
|
+
return new VFile({
|
|
652
|
+
data: { ...context },
|
|
653
|
+
path: filePath,
|
|
654
|
+
value: markdown
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// scripts/obsidian/files.ts
|
|
659
|
+
var DEFAULT_OUTPUT_ROOTS = { content: "content", public: "public" };
|
|
660
|
+
var calloutTypeMap = {
|
|
661
|
+
note: "note",
|
|
662
|
+
abstract: "tip",
|
|
663
|
+
summary: "tip",
|
|
664
|
+
tldr: "tip",
|
|
665
|
+
info: "note",
|
|
666
|
+
todo: "note",
|
|
667
|
+
tip: "tip",
|
|
668
|
+
hint: "tip",
|
|
669
|
+
important: "tip",
|
|
670
|
+
success: "note",
|
|
671
|
+
check: "note",
|
|
672
|
+
done: "note",
|
|
673
|
+
question: "caution",
|
|
674
|
+
help: "caution",
|
|
675
|
+
faq: "caution",
|
|
676
|
+
warning: "caution",
|
|
677
|
+
caution: "caution",
|
|
678
|
+
attention: "caution",
|
|
679
|
+
failure: "danger",
|
|
680
|
+
fail: "danger",
|
|
681
|
+
missing: "danger",
|
|
682
|
+
danger: "danger",
|
|
683
|
+
error: "danger",
|
|
684
|
+
bug: "danger",
|
|
685
|
+
example: "tip",
|
|
686
|
+
quote: "note",
|
|
687
|
+
cite: "note"
|
|
688
|
+
};
|
|
689
|
+
function getCalloutType(obsidianCalloutType) {
|
|
690
|
+
return calloutTypeMap[obsidianCalloutType] ?? "note";
|
|
691
|
+
}
|
|
692
|
+
function isAssetFile(filePath) {
|
|
693
|
+
return getExtension(filePath) !== ".bmp" && isObsidianFile(filePath, "image");
|
|
694
|
+
}
|
|
695
|
+
async function addObsidianFiles(config, vault, obsidianPaths, logger2, roots = DEFAULT_OUTPUT_ROOTS) {
|
|
696
|
+
const outputPaths = getOutputPaths(config, roots);
|
|
697
|
+
if (stripLeadingAndTrailingSlashes(config.output) !== ".") {
|
|
698
|
+
await cleanOutputPaths(outputPaths);
|
|
699
|
+
}
|
|
700
|
+
const vaultFiles = getObsidianVaultFiles(vault, obsidianPaths);
|
|
701
|
+
const results = await Promise.allSettled(
|
|
702
|
+
vaultFiles.map(async (vaultFile) => {
|
|
703
|
+
await (vaultFile.type === "asset" ? addAsset(outputPaths, vaultFile) : vaultFile.type === "file" ? addFile(outputPaths, vaultFile) : addContent(config, vault, outputPaths, vaultFiles, vaultFile));
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
let didFail = false;
|
|
707
|
+
for (const result of results) {
|
|
708
|
+
if (result.status === "rejected") {
|
|
709
|
+
didFail = true;
|
|
710
|
+
logger2.error(result.reason instanceof Error ? result.reason.message : String(result.reason));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (didFail) {
|
|
714
|
+
throw new Error("Failed to generate some pages. See the error(s) above for more information.");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function addContent(config, vault, outputPaths, vaultFiles, vaultFile) {
|
|
718
|
+
try {
|
|
719
|
+
const obsidianContent = await fs3.readFile(vaultFile.fsPath, "utf8");
|
|
720
|
+
const {
|
|
721
|
+
content,
|
|
722
|
+
aliases,
|
|
723
|
+
skip,
|
|
724
|
+
type
|
|
725
|
+
} = await transformMarkdownToString(vaultFile.fsPath, obsidianContent, {
|
|
726
|
+
files: vaultFiles,
|
|
727
|
+
copyFrontmatter: config.copyFrontmatter,
|
|
728
|
+
output: config.output,
|
|
729
|
+
singleDollarTextMath: config.math.singleDollarTextMath,
|
|
730
|
+
vault
|
|
731
|
+
});
|
|
732
|
+
if (skip) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const outputPath = path4.join(
|
|
736
|
+
outputPaths.content,
|
|
737
|
+
type === "markdown" ? vaultFile.path : vaultFile.path.replace(/\.md$/, ".mdx")
|
|
738
|
+
);
|
|
739
|
+
const outputDirPath = path4.dirname(outputPath);
|
|
740
|
+
await ensureDirectory(outputDirPath);
|
|
741
|
+
await fs3.writeFile(outputPath, content);
|
|
742
|
+
if (aliases) {
|
|
743
|
+
for (const alias of aliases) {
|
|
744
|
+
await addAlias(config, outputPaths, vaultFile, alias);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} catch (error) {
|
|
748
|
+
throwVaultFileError(error, vaultFile);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function addFile(outputPaths, vaultFile) {
|
|
752
|
+
try {
|
|
753
|
+
await copyFile(vaultFile.fsPath, path4.join(outputPaths.file, vaultFile.slug));
|
|
754
|
+
} catch (error) {
|
|
755
|
+
throwVaultFileError(error, vaultFile);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function addAsset(outputPaths, vaultFile) {
|
|
759
|
+
try {
|
|
760
|
+
await copyFile(vaultFile.fsPath, path4.join(outputPaths.asset, vaultFile.slug));
|
|
761
|
+
} catch (error) {
|
|
762
|
+
throwVaultFileError(error, vaultFile);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function addAlias(config, outputPaths, vaultFile, alias) {
|
|
766
|
+
const htmlPath = path4.join(outputPaths.file, path4.dirname(vaultFile.path), alias, "index.html");
|
|
767
|
+
const htmlDirPath = path4.dirname(htmlPath);
|
|
768
|
+
const to = path4.posix.join(path4.posix.sep, config.output, vaultFile.slug);
|
|
769
|
+
const from = path4.posix.join(path4.dirname(to), alias);
|
|
770
|
+
await ensureDirectory(htmlDirPath);
|
|
771
|
+
await fs3.writeFile(
|
|
772
|
+
htmlPath,
|
|
773
|
+
`<!doctype html>
|
|
774
|
+
<html lang="en">
|
|
775
|
+
<head>
|
|
776
|
+
<title>${vaultFile.stem}</title>
|
|
777
|
+
<meta http-equiv="refresh" content="0;url=${to}">
|
|
778
|
+
<meta name="robots" content="noindex">
|
|
779
|
+
<link rel="canonical" href="${to}">
|
|
780
|
+
</head>
|
|
781
|
+
<body>
|
|
782
|
+
<a href="${to}">Redirecting from <code>${from}</code> to <code>${to}</code></a>
|
|
783
|
+
</body>
|
|
784
|
+
</html>`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
function getOutputPaths(config, roots) {
|
|
788
|
+
return {
|
|
789
|
+
asset: path4.join(roots.public, config.output),
|
|
790
|
+
content: path4.join(roots.content, config.output),
|
|
791
|
+
file: path4.join(roots.public, config.output)
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
async function cleanOutputPaths(outputPaths) {
|
|
795
|
+
await removeDirectory(outputPaths.asset);
|
|
796
|
+
await removeDirectory(outputPaths.content);
|
|
797
|
+
await removeDirectory(outputPaths.file);
|
|
798
|
+
}
|
|
799
|
+
function throwVaultFileError(error, vaultFile) {
|
|
800
|
+
throw new Error(`${vaultFile.path} \u2014 ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// scripts/obsidian/obsidian.ts
|
|
804
|
+
var obsidianAppConfigSchema = z2.object({
|
|
805
|
+
newLinkFormat: z2.union([z2.literal("absolute"), z2.literal("relative"), z2.literal("shortest")]).default("shortest"),
|
|
806
|
+
useMarkdownLinks: z2.boolean().default(false)
|
|
807
|
+
});
|
|
808
|
+
var obsidianFrontmatterSchema = z2.object({
|
|
809
|
+
aliases: z2.array(z2.string()).optional().nullable().transform((aliases) => aliases?.map((alias) => slug2(alias))),
|
|
810
|
+
cover: z2.string().optional().nullable(),
|
|
811
|
+
description: z2.string().optional().nullable(),
|
|
812
|
+
image: z2.string().optional().nullable(),
|
|
813
|
+
permalink: z2.string().optional().nullable(),
|
|
814
|
+
publish: z2.union([z2.boolean(), z2.literal("true"), z2.literal("false")]).optional().nullable().transform((publish) => publish === void 0 || publish === "true" || publish === true),
|
|
815
|
+
tags: z2.array(z2.string()).optional().nullable()
|
|
816
|
+
});
|
|
817
|
+
var imageFileFormats = /* @__PURE__ */ new Set([".avif", ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp"]);
|
|
818
|
+
var audioFileFormats = /* @__PURE__ */ new Set([".flac", ".m4a", ".mp3", ".wav", ".ogg", ".wav", ".3gp"]);
|
|
819
|
+
var videoFileFormats = /* @__PURE__ */ new Set([".mkv", ".mov", ".mp4", ".ogv", ".webm"]);
|
|
820
|
+
var otherFileFormats = /* @__PURE__ */ new Set([".pdf"]);
|
|
821
|
+
var fileFormats = /* @__PURE__ */ new Set([...imageFileFormats, ...audioFileFormats, ...videoFileFormats, ...otherFileFormats]);
|
|
822
|
+
async function getVault(config) {
|
|
823
|
+
const vaultPath = path5.resolve(config.vault);
|
|
824
|
+
if (!await isDirectory(vaultPath)) {
|
|
825
|
+
throw new Error(`The provided vault path is not a directory.
|
|
826
|
+
> Provided path: ${vaultPath}`);
|
|
827
|
+
}
|
|
828
|
+
const options = await isVaultDirectory(config, vaultPath) ? await getVaultOptions(config, vaultPath) : { linkFormat: "shortest", linkSyntax: "wikilink" };
|
|
829
|
+
return {
|
|
830
|
+
options,
|
|
831
|
+
path: slashify(vaultPath)
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function getObsidianPaths(vault, ignore = []) {
|
|
835
|
+
return globby(["**/*.md", ...[...fileFormats].map((fileFormat) => `**/*${fileFormat}`)], {
|
|
836
|
+
absolute: true,
|
|
837
|
+
cwd: vault.path,
|
|
838
|
+
ignore
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
function getObsidianVaultFiles(vault, obsidianPaths) {
|
|
842
|
+
const allFileNames = obsidianPaths.map((obsidianPath) => path5.basename(obsidianPath));
|
|
843
|
+
return obsidianPaths.map((obsidianPath, index) => {
|
|
844
|
+
const baseFileName = allFileNames[index];
|
|
845
|
+
let fileName = baseFileName;
|
|
846
|
+
const type = isAssetFile(fileName) ? "asset" : isObsidianFile(fileName) ? "file" : "content";
|
|
847
|
+
if (type === "asset") {
|
|
848
|
+
fileName = slugifyPath(fileName);
|
|
849
|
+
}
|
|
850
|
+
const filePath = getObsidianRelativePath(vault, obsidianPath);
|
|
851
|
+
const fileSlug = slugifyObsidianPath(filePath);
|
|
852
|
+
return createVaultFile({
|
|
853
|
+
fileName,
|
|
854
|
+
fsPath: obsidianPath,
|
|
855
|
+
path: type === "asset" ? fileSlug : filePath,
|
|
856
|
+
slug: fileSlug,
|
|
857
|
+
stem: stripExtension(fileName),
|
|
858
|
+
type,
|
|
859
|
+
uniqueFileName: allFileNames.filter((currentFileName) => currentFileName === baseFileName).length === 1
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
function getObsidianRelativePath(vault, obsidianPath) {
|
|
864
|
+
return obsidianPath.replace(vault.path, "");
|
|
865
|
+
}
|
|
866
|
+
function slugifyObsidianPath(obsidianPath) {
|
|
867
|
+
const segments = obsidianPath.split("/");
|
|
868
|
+
return segments.map((segment, index) => {
|
|
869
|
+
const isLastSegment = index === segments.length - 1;
|
|
870
|
+
if (!isLastSegment) {
|
|
871
|
+
return slug2(decodeUriComponent(segment));
|
|
872
|
+
} else if (isObsidianFile(segment) && !isAssetFile(segment)) {
|
|
873
|
+
return decodeUriComponent(segment);
|
|
874
|
+
} else if (isAssetFile(segment)) {
|
|
875
|
+
return `${slug2(decodeUriComponent(stripExtension(segment)))}${getExtension(segment)}`;
|
|
876
|
+
}
|
|
877
|
+
return slug2(decodeUriComponent(stripExtension(segment)));
|
|
878
|
+
}).join("/");
|
|
879
|
+
}
|
|
880
|
+
function slugifyObsidianAnchor(obsidianAnchor) {
|
|
881
|
+
if (obsidianAnchor.length === 0) {
|
|
882
|
+
return "";
|
|
883
|
+
}
|
|
884
|
+
let anchor = isAnchor(obsidianAnchor) ? obsidianAnchor.slice(1) : obsidianAnchor;
|
|
885
|
+
if (isObsidianBlockAnchor(anchor)) {
|
|
886
|
+
anchor = anchor.replace("^", "block-");
|
|
887
|
+
}
|
|
888
|
+
return `#${slug2(decodeURIComponent(anchor))}`;
|
|
889
|
+
}
|
|
890
|
+
function isObsidianBlockAnchor(anchor) {
|
|
891
|
+
return anchor.startsWith("#^") || anchor.startsWith("^");
|
|
892
|
+
}
|
|
893
|
+
function isObsidianFile(filePath, type) {
|
|
894
|
+
const formats = type === void 0 ? fileFormats : type === "image" ? imageFileFormats : type === "audio" ? audioFileFormats : type === "video" ? videoFileFormats : otherFileFormats;
|
|
895
|
+
return formats.has(getExtension(filePath));
|
|
896
|
+
}
|
|
897
|
+
function parseObsidianFrontmatter(content) {
|
|
898
|
+
try {
|
|
899
|
+
const raw = yaml2.parse(content);
|
|
900
|
+
return { ...obsidianFrontmatterSchema.parse(raw), raw };
|
|
901
|
+
} catch {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function createVaultFile(baseVaultFile) {
|
|
906
|
+
return {
|
|
907
|
+
...baseVaultFile,
|
|
908
|
+
isEqualFileName(otherFileName) {
|
|
909
|
+
return (isAssetFile(otherFileName) ? slugifyPath(otherFileName) : otherFileName) === this.fileName;
|
|
910
|
+
},
|
|
911
|
+
isEqualStem(otherStem) {
|
|
912
|
+
return (isAssetFile(otherStem) ? slugifyPath(otherStem) : otherStem) === this.stem;
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
async function isVaultDirectory(config, vaultPath) {
|
|
917
|
+
const configPath = path5.join(vaultPath, config.configFolder);
|
|
918
|
+
return await isDirectory(configPath) && await isFile(path5.join(configPath, "app.json"));
|
|
919
|
+
}
|
|
920
|
+
async function getVaultOptions(config, vaultPath) {
|
|
921
|
+
const appConfigPath = path5.join(vaultPath, config.configFolder, "app.json");
|
|
922
|
+
try {
|
|
923
|
+
const appConfigData = await fs4.readFile(appConfigPath, "utf8");
|
|
924
|
+
const appConfig = obsidianAppConfigSchema.parse(JSON.parse(appConfigData));
|
|
925
|
+
return {
|
|
926
|
+
linkFormat: appConfig.newLinkFormat,
|
|
927
|
+
linkSyntax: appConfig.useMarkdownLinks ? "markdown" : "wikilink"
|
|
928
|
+
};
|
|
929
|
+
} catch (error) {
|
|
930
|
+
throw new Error("Failed to read Obsidian vault app configuration.", { cause: error });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// scripts/obsidian/generate.ts
|
|
935
|
+
async function generateObsidian(userConfig, logger2, roots) {
|
|
936
|
+
const parsed = obsidianConfigSchema.safeParse(userConfig);
|
|
937
|
+
if (!parsed.success) {
|
|
938
|
+
throw new Error(`Invalid obsidian configuration:
|
|
939
|
+
|
|
940
|
+
${JSON.stringify(parsed.error.format(), null, 2)}`);
|
|
941
|
+
}
|
|
942
|
+
const config = parsed.data;
|
|
943
|
+
if (config.skipGeneration) {
|
|
944
|
+
logger2.warn(`Skipping generation for '${config.output}' (skipGeneration enabled).`);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const start = performance.now();
|
|
948
|
+
logger2.info(`Generating pages from Obsidian vault '${config.vault}'\u2026`);
|
|
949
|
+
const vault = await getVault(config);
|
|
950
|
+
const obsidianPaths = await getObsidianPaths(vault, config.ignore);
|
|
951
|
+
await addObsidianFiles(config, vault, obsidianPaths, logger2, roots);
|
|
952
|
+
logger2.info(`Generated '${config.output}' in ${Math.round(performance.now() - start)}ms.`);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// scripts/canvas-to-md.ts
|
|
956
|
+
import path6 from "node:path";
|
|
957
|
+
import fs5 from "node:fs/promises";
|
|
958
|
+
import { glob } from "tinyglobby";
|
|
959
|
+
function escapeYamlString(s) {
|
|
960
|
+
if (/[:"'#\[\]{}&*!|>%@`]/.test(s) || s.includes("\n")) {
|
|
961
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
962
|
+
}
|
|
963
|
+
return s;
|
|
964
|
+
}
|
|
965
|
+
async function generateCanvasFile(vaultDir, outDir, relPath, log) {
|
|
966
|
+
const absPath = path6.join(vaultDir, relPath);
|
|
967
|
+
const contents = await fs5.readFile(absPath, "utf-8");
|
|
968
|
+
let canvasObj;
|
|
969
|
+
try {
|
|
970
|
+
canvasObj = JSON.parse(contents);
|
|
971
|
+
} catch (err) {
|
|
972
|
+
log(`Invalid JSON in ${relPath}: ${err.message}`);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const title = path6.basename(relPath, ".canvas");
|
|
976
|
+
const safeJson = JSON.stringify(canvasObj).replace(/</g, "\\u003c");
|
|
977
|
+
const md = [
|
|
978
|
+
"---",
|
|
979
|
+
`title: ${escapeYamlString(title)}`,
|
|
980
|
+
"tableOfContents: false",
|
|
981
|
+
"---",
|
|
982
|
+
"",
|
|
983
|
+
`<div class="canvas-container not-content" data-canvas-mount><script type="application/json">${safeJson}</script></div>`,
|
|
984
|
+
""
|
|
985
|
+
].join("\n");
|
|
986
|
+
const mdRelPath = relPath.replace(/\.canvas$/, ".md");
|
|
987
|
+
const outPath = path6.join(outDir, mdRelPath);
|
|
988
|
+
await fs5.mkdir(path6.dirname(outPath), { recursive: true });
|
|
989
|
+
await fs5.writeFile(outPath, md, "utf-8");
|
|
990
|
+
}
|
|
991
|
+
async function generateCanvas(options, logger2) {
|
|
992
|
+
const { vault, output, contentRoot = "content" } = options;
|
|
993
|
+
const vaultDir = path6.resolve(vault);
|
|
994
|
+
const outDir = path6.resolve(contentRoot, output);
|
|
995
|
+
const entries = await glob("**/[^_]*.canvas", { cwd: vaultDir });
|
|
996
|
+
await Promise.all(entries.map((relPath) => generateCanvasFile(vaultDir, outDir, relPath, (m) => logger2.info(m))));
|
|
997
|
+
if (entries.length > 0) {
|
|
998
|
+
logger2.info(`Generated ${entries.length} canvas page(s) for '${output}'`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// scripts/compile.ts
|
|
1003
|
+
import fs6 from "node:fs/promises";
|
|
1004
|
+
import path7 from "node:path";
|
|
1005
|
+
import { unified } from "unified";
|
|
1006
|
+
import remarkParse from "remark-parse";
|
|
1007
|
+
import remarkGfm2 from "remark-gfm";
|
|
1008
|
+
import remarkBreaks from "remark-breaks";
|
|
1009
|
+
import remarkMath2 from "remark-math";
|
|
1010
|
+
import remarkFrontmatter2 from "remark-frontmatter";
|
|
1011
|
+
import remarkRehype from "remark-rehype";
|
|
1012
|
+
import rehypeKatex from "rehype-katex";
|
|
1013
|
+
import rehypeStringify from "rehype-stringify";
|
|
1014
|
+
import { visit as visit2 } from "unist-util-visit";
|
|
1015
|
+
import { slug as slugify } from "github-slugger";
|
|
1016
|
+
import yaml3 from "yaml";
|
|
1017
|
+
function extractFrontmatter(tree) {
|
|
1018
|
+
for (const node of tree.children) {
|
|
1019
|
+
if (node.type === "yaml") {
|
|
1020
|
+
try {
|
|
1021
|
+
const parsed = yaml3.parse(node.value);
|
|
1022
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1023
|
+
} catch {
|
|
1024
|
+
return {};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return {};
|
|
1029
|
+
}
|
|
1030
|
+
function nodeText(node) {
|
|
1031
|
+
let out = "";
|
|
1032
|
+
visit2(node, "text", (t) => {
|
|
1033
|
+
out += t.value;
|
|
1034
|
+
});
|
|
1035
|
+
return out;
|
|
1036
|
+
}
|
|
1037
|
+
function rehypeHeadings() {
|
|
1038
|
+
return (tree, file) => {
|
|
1039
|
+
const headings = [];
|
|
1040
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1041
|
+
visit2(tree, "element", (node) => {
|
|
1042
|
+
const m = /^h([1-6])$/.exec(node.tagName);
|
|
1043
|
+
if (!m)
|
|
1044
|
+
return;
|
|
1045
|
+
const depth = Number(m[1]);
|
|
1046
|
+
const text = nodeText(node).trim();
|
|
1047
|
+
let id = slugify(text);
|
|
1048
|
+
if (seen.has(id)) {
|
|
1049
|
+
const n = seen.get(id) + 1;
|
|
1050
|
+
seen.set(id, n);
|
|
1051
|
+
id = `${id}-${n}`;
|
|
1052
|
+
} else {
|
|
1053
|
+
seen.set(id, 0);
|
|
1054
|
+
}
|
|
1055
|
+
node.properties = node.properties ?? {};
|
|
1056
|
+
if (!node.properties.id)
|
|
1057
|
+
node.properties.id = id;
|
|
1058
|
+
headings.push({ depth, slug: String(node.properties.id), text });
|
|
1059
|
+
});
|
|
1060
|
+
file.data.headings = headings;
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function rehypeHoistBlankBeforeToPre() {
|
|
1064
|
+
return (tree) => {
|
|
1065
|
+
visit2(tree, "element", (node) => {
|
|
1066
|
+
if (node.tagName !== "pre")
|
|
1067
|
+
return;
|
|
1068
|
+
const code = node.children.find(
|
|
1069
|
+
(c) => c.type === "element" && c.tagName === "code"
|
|
1070
|
+
);
|
|
1071
|
+
if (!code)
|
|
1072
|
+
return;
|
|
1073
|
+
const codeCls = code.properties?.className;
|
|
1074
|
+
if (!Array.isArray(codeCls) || !codeCls.includes("has-blank-before"))
|
|
1075
|
+
return;
|
|
1076
|
+
const remaining = codeCls.filter((c) => c !== "has-blank-before");
|
|
1077
|
+
if (remaining.length > 0)
|
|
1078
|
+
code.properties.className = remaining;
|
|
1079
|
+
else
|
|
1080
|
+
delete code.properties.className;
|
|
1081
|
+
node.properties = node.properties ?? {};
|
|
1082
|
+
const preCls = node.properties.className;
|
|
1083
|
+
node.properties.className = Array.isArray(preCls) ? [...preCls, "has-blank-before"] : ["has-blank-before"];
|
|
1084
|
+
});
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
var BLANK_GAP_MARKER2 = "<!--blank-gap-->";
|
|
1088
|
+
function remarkBlankLineGaps() {
|
|
1089
|
+
const isGapMarker = (n) => n?.type === "html" && n.value?.trim() === BLANK_GAP_MARKER2;
|
|
1090
|
+
function walk(parent) {
|
|
1091
|
+
const children = parent.children;
|
|
1092
|
+
if (!children)
|
|
1093
|
+
return;
|
|
1094
|
+
const gapped = /* @__PURE__ */ new Set();
|
|
1095
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
1096
|
+
if (isGapMarker(children[i])) {
|
|
1097
|
+
const next = children[i + 1];
|
|
1098
|
+
if (next)
|
|
1099
|
+
gapped.add(next);
|
|
1100
|
+
children.splice(i, 1);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
for (const node of children) {
|
|
1104
|
+
if (gapped.has(node)) {
|
|
1105
|
+
node.data ??= {};
|
|
1106
|
+
node.data.hProperties ??= {};
|
|
1107
|
+
const cls = node.data.hProperties.className ?? [];
|
|
1108
|
+
cls.push("has-blank-before");
|
|
1109
|
+
node.data.hProperties.className = cls;
|
|
1110
|
+
}
|
|
1111
|
+
walk(node);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return (tree) => {
|
|
1115
|
+
walk(tree);
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
var processor3 = unified().use(remarkParse).use(remarkFrontmatter2).use(remarkGfm2).use(remarkBreaks).use(remarkBlankLineGaps).use(remarkMath2, { singleDollarTextMath: true }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeKatex).use(rehypeHoistBlankBeforeToPre).use(rehypeHeadings).use(rehypeStringify, { allowDangerousHtml: true });
|
|
1119
|
+
async function compileMarkdown(markdown) {
|
|
1120
|
+
const mdast = processor3.parse(markdown);
|
|
1121
|
+
const frontmatter = extractFrontmatter(mdast);
|
|
1122
|
+
const file = await processor3.process(markdown);
|
|
1123
|
+
return {
|
|
1124
|
+
frontmatter,
|
|
1125
|
+
headings: file.data.headings ?? [],
|
|
1126
|
+
html: String(file)
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
async function compileDir(contentRoot, logger2) {
|
|
1130
|
+
const docs = [];
|
|
1131
|
+
async function walk(dir) {
|
|
1132
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1133
|
+
await Promise.all(
|
|
1134
|
+
entries.map(async (entry) => {
|
|
1135
|
+
const full = path7.join(dir, entry.name);
|
|
1136
|
+
if (entry.isDirectory()) {
|
|
1137
|
+
await walk(full);
|
|
1138
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1139
|
+
const raw = await fs6.readFile(full, "utf8");
|
|
1140
|
+
const compiled = await compileMarkdown(raw);
|
|
1141
|
+
const rel = path7.relative(contentRoot, full).replace(/\\/g, "/").replace(/\.md$/, "");
|
|
1142
|
+
const id = slugifyObsidianPath(rel);
|
|
1143
|
+
docs.push({ id, ...compiled });
|
|
1144
|
+
}
|
|
1145
|
+
})
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
await walk(contentRoot);
|
|
1149
|
+
logger2.info(`Compiled ${docs.length} markdown page(s) to HTML.`);
|
|
1150
|
+
return docs;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// scripts/build-search-index.ts
|
|
1154
|
+
async function buildSearchIndex(docs, canonicalUrl, logger2, options = { outputPath: "public/pagefind", lang: "ru" }) {
|
|
1155
|
+
const { createIndex } = await import("pagefind");
|
|
1156
|
+
const { index, errors } = await createIndex({
|
|
1157
|
+
// Set the default site language so tokenisation and stemming are correct.
|
|
1158
|
+
// Pagefind still detects per-page `lang` if present.
|
|
1159
|
+
forceLanguage: options.lang
|
|
1160
|
+
});
|
|
1161
|
+
if (!index) {
|
|
1162
|
+
logger2.warn(`Pagefind index could not be created: ${errors.join("; ")}`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
let indexed = 0;
|
|
1166
|
+
let skipped = 0;
|
|
1167
|
+
for (const doc of docs) {
|
|
1168
|
+
if (doc.frontmatter.draft === true) {
|
|
1169
|
+
skipped++;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (doc.html.includes("data-canvas-mount")) {
|
|
1173
|
+
skipped++;
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const title = doc.frontmatter.title ?? doc.id.split("/").pop()?.replace(/-/g, " ") ?? doc.id;
|
|
1177
|
+
const url = canonicalUrl.get(doc.id) ?? `/${doc.id}/`;
|
|
1178
|
+
const segments = doc.id.split("/");
|
|
1179
|
+
const project = segments[0];
|
|
1180
|
+
const dirSegments = segments.slice(0, -1);
|
|
1181
|
+
const dirPrefixes = dirSegments.map((_, i) => dirSegments.slice(0, i + 1).join("/"));
|
|
1182
|
+
const filterTags = `<span data-pagefind-filter="project" style="display:none">${escapeHtml(project)}</span>` + dirPrefixes.map(
|
|
1183
|
+
(d) => `<span data-pagefind-filter="dir" style="display:none">${escapeHtml(d)}</span>`
|
|
1184
|
+
).join("");
|
|
1185
|
+
const content = `<!DOCTYPE html><html lang="${escapeHtml(options.lang)}"><head><title>${escapeHtml(
|
|
1186
|
+
title
|
|
1187
|
+
)}</title></head><body><main data-pagefind-body>${filterTags}<h1>${escapeHtml(
|
|
1188
|
+
title
|
|
1189
|
+
)}</h1>${doc.html}</main></body></html>`;
|
|
1190
|
+
const { errors: fileErrors } = await index.addHTMLFile({
|
|
1191
|
+
url,
|
|
1192
|
+
content
|
|
1193
|
+
});
|
|
1194
|
+
if (fileErrors.length) {
|
|
1195
|
+
logger2.warn(`Pagefind failed on ${doc.id}: ${fileErrors.join("; ")}`);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
indexed++;
|
|
1199
|
+
}
|
|
1200
|
+
const { errors: writeErrors } = await index.writeFiles({
|
|
1201
|
+
outputPath: options.outputPath
|
|
1202
|
+
});
|
|
1203
|
+
if (writeErrors.length) {
|
|
1204
|
+
logger2.warn(`Pagefind writeFiles errors: ${writeErrors.join("; ")}`);
|
|
1205
|
+
}
|
|
1206
|
+
await index.deleteIndex();
|
|
1207
|
+
logger2.info(`Pagefind: indexed ${indexed} pages (skipped ${skipped}) \u2192 ${options.outputPath}`);
|
|
1208
|
+
}
|
|
1209
|
+
function escapeHtml(s) {
|
|
1210
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// scripts/canonical.ts
|
|
1214
|
+
function projectOf(id) {
|
|
1215
|
+
return id.split("/")[0] ?? "";
|
|
1216
|
+
}
|
|
1217
|
+
function buildIdToCanonicalUrl(docs) {
|
|
1218
|
+
const map = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (const d of docs) {
|
|
1220
|
+
const raw = d.frontmatter.permalink;
|
|
1221
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
1222
|
+
const rel = raw.trim().replace(/^\/+|\/+$/g, "");
|
|
1223
|
+
if (rel) {
|
|
1224
|
+
map.set(d.id, `/${projectOf(d.id)}/${rel}/`);
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
map.set(d.id, `/${d.id}/`);
|
|
1229
|
+
}
|
|
1230
|
+
return map;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// app/lib/config/load.ts
|
|
1234
|
+
import { pathToFileURL } from "node:url";
|
|
1235
|
+
import path8 from "node:path";
|
|
1236
|
+
import fs7 from "node:fs";
|
|
1237
|
+
|
|
1238
|
+
// app/lib/config/schema.ts
|
|
1239
|
+
import { z as z3 } from "zod";
|
|
1240
|
+
var colorMap = z3.record(z3.string(), z3.string());
|
|
1241
|
+
var projectSchema = z3.object({
|
|
1242
|
+
/** First id segment + output dir name, e.g. `krista`. */
|
|
1243
|
+
id: z3.string().min(1),
|
|
1244
|
+
/** Display name in the switcher / home cards. */
|
|
1245
|
+
name: z3.string().min(1),
|
|
1246
|
+
/** Directory of markdown/canvas files. Submodule, loose folder, or any path. */
|
|
1247
|
+
source: z3.string().min(1),
|
|
1248
|
+
/** Logo under the user's `/public`. Defaults to `/projects/<id>.svg`. */
|
|
1249
|
+
logo: z3.string().optional(),
|
|
1250
|
+
/** Landing URL when switching to this project. Defaults to its first doc. */
|
|
1251
|
+
landing: z3.string().optional(),
|
|
1252
|
+
/** Short blurb, reused on home cards. */
|
|
1253
|
+
description: z3.string().default(""),
|
|
1254
|
+
/** Ingest `.canvas` files from this source too. */
|
|
1255
|
+
canvas: z3.boolean().default(false),
|
|
1256
|
+
/** Globs (relative to `source`) to skip, e.g. `['CLAUDE.md']`. */
|
|
1257
|
+
ignore: z3.array(z3.string()).default([])
|
|
1258
|
+
});
|
|
1259
|
+
var generalSchema = z3.object({
|
|
1260
|
+
enabled: z3.boolean().default(false),
|
|
1261
|
+
name: z3.string().default("\u0411\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430"),
|
|
1262
|
+
/** Directory of loose docs. When unset, the bucket has no docs. */
|
|
1263
|
+
source: z3.string().optional(),
|
|
1264
|
+
logo: z3.string().default("/projects/general.svg"),
|
|
1265
|
+
description: z3.string().default(""),
|
|
1266
|
+
ignore: z3.array(z3.string()).default([])
|
|
1267
|
+
});
|
|
1268
|
+
var siteSchema = z3.object({
|
|
1269
|
+
title: z3.string().default("Docs"),
|
|
1270
|
+
/** Home-page blurb under the title. */
|
|
1271
|
+
description: z3.string().default(""),
|
|
1272
|
+
/** BCP-47-ish tag; drives `localeCompare` sorting + Pagefind `forceLanguage`. */
|
|
1273
|
+
lang: z3.string().default("ru"),
|
|
1274
|
+
favicon: z3.string().default("/favicon.svg"),
|
|
1275
|
+
logo: z3.object({
|
|
1276
|
+
light: z3.string().default("/iti-logo-light.svg"),
|
|
1277
|
+
dark: z3.string().default("/iti-logo-dark.svg")
|
|
1278
|
+
}).prefault({}),
|
|
1279
|
+
defaultTheme: z3.enum(["dark", "light"]).default("dark")
|
|
1280
|
+
});
|
|
1281
|
+
var themeSchema = z3.object({
|
|
1282
|
+
/** OKLCH (or any CSS color) token overrides, merged OVER the shipped defaults. */
|
|
1283
|
+
colors: z3.object({
|
|
1284
|
+
light: colorMap.default({}),
|
|
1285
|
+
dark: colorMap.default({})
|
|
1286
|
+
}).prefault({})
|
|
1287
|
+
});
|
|
1288
|
+
var componentsSchema = z3.object({
|
|
1289
|
+
Home: z3.string().optional(),
|
|
1290
|
+
DocPage: z3.string().optional(),
|
|
1291
|
+
Sidebar: z3.string().optional(),
|
|
1292
|
+
TopBar: z3.string().optional(),
|
|
1293
|
+
Toc: z3.string().optional(),
|
|
1294
|
+
Search: z3.string().optional(),
|
|
1295
|
+
Layout: z3.string().optional()
|
|
1296
|
+
}).default({});
|
|
1297
|
+
var uiSchema = z3.record(z3.string(), z3.string()).default({});
|
|
1298
|
+
var docsConfigSchema = z3.object({
|
|
1299
|
+
site: siteSchema.prefault({}),
|
|
1300
|
+
projects: z3.array(projectSchema).default([]),
|
|
1301
|
+
general: generalSchema.prefault({}),
|
|
1302
|
+
theme: themeSchema.prefault({}),
|
|
1303
|
+
components: componentsSchema,
|
|
1304
|
+
ui: uiSchema,
|
|
1305
|
+
/**
|
|
1306
|
+
* Reserved for custom remark/rehype plugins + callout types. NOT wired in this
|
|
1307
|
+
* phase (the markdown pipeline stays fixed); accepted so configs are
|
|
1308
|
+
* forward-compatible.
|
|
1309
|
+
*/
|
|
1310
|
+
markdown: z3.unknown().optional()
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
// app/lib/config/defaults.ts
|
|
1314
|
+
var DEFAULT_THEME = {
|
|
1315
|
+
light: {
|
|
1316
|
+
"--brand": "oklch(0.42 0.158 286)",
|
|
1317
|
+
"--background": "oklch(1 0 0)",
|
|
1318
|
+
"--foreground": "oklch(0.145 0 0)",
|
|
1319
|
+
"--card": "oklch(1 0 0)",
|
|
1320
|
+
"--card-foreground": "oklch(0.145 0 0)",
|
|
1321
|
+
"--popover": "oklch(1 0 0)",
|
|
1322
|
+
"--popover-foreground": "oklch(0.145 0 0)",
|
|
1323
|
+
"--primary": "var(--brand)",
|
|
1324
|
+
"--primary-foreground": "oklch(0.985 0 0)",
|
|
1325
|
+
"--secondary": "oklch(0.97 0 0)",
|
|
1326
|
+
"--secondary-foreground": "oklch(0.205 0 0)",
|
|
1327
|
+
"--muted": "oklch(0.97 0 0)",
|
|
1328
|
+
"--muted-foreground": "oklch(0.556 0 0)",
|
|
1329
|
+
"--accent": "oklch(0.97 0 0)",
|
|
1330
|
+
"--accent-foreground": "oklch(0.205 0 0)",
|
|
1331
|
+
"--destructive": "oklch(0.577 0.245 27.325)",
|
|
1332
|
+
"--border": "oklch(0.922 0 0)",
|
|
1333
|
+
"--input": "oklch(0.922 0 0)",
|
|
1334
|
+
"--ring": "oklch(0.708 0 0)",
|
|
1335
|
+
"--sidebar": "oklch(0.985 0 0)",
|
|
1336
|
+
"--sidebar-foreground": "oklch(0.145 0 0)",
|
|
1337
|
+
"--sidebar-primary": "oklch(0.205 0 0)",
|
|
1338
|
+
"--sidebar-primary-foreground": "oklch(0.985 0 0)",
|
|
1339
|
+
"--sidebar-accent": "oklch(0.97 0 0)",
|
|
1340
|
+
"--sidebar-accent-foreground": "oklch(0.205 0 0)",
|
|
1341
|
+
"--sidebar-border": "oklch(0.922 0 0)",
|
|
1342
|
+
"--sidebar-ring": "oklch(0.708 0 0)"
|
|
1343
|
+
},
|
|
1344
|
+
dark: {
|
|
1345
|
+
"--brand": "oklch(0.68 0.14 286)",
|
|
1346
|
+
"--background": "oklch(0.145 0 0)",
|
|
1347
|
+
"--foreground": "oklch(0.985 0 0)",
|
|
1348
|
+
"--card": "oklch(0.205 0 0)",
|
|
1349
|
+
"--card-foreground": "oklch(0.985 0 0)",
|
|
1350
|
+
"--popover": "oklch(0.205 0 0)",
|
|
1351
|
+
"--popover-foreground": "oklch(0.985 0 0)",
|
|
1352
|
+
"--primary": "var(--brand)",
|
|
1353
|
+
"--primary-foreground": "oklch(0.205 0 0)",
|
|
1354
|
+
"--secondary": "oklch(0.269 0 0)",
|
|
1355
|
+
"--secondary-foreground": "oklch(0.985 0 0)",
|
|
1356
|
+
"--muted": "oklch(0.269 0 0)",
|
|
1357
|
+
"--muted-foreground": "oklch(0.708 0 0)",
|
|
1358
|
+
"--accent": "oklch(0.269 0 0)",
|
|
1359
|
+
"--accent-foreground": "oklch(0.985 0 0)",
|
|
1360
|
+
"--destructive": "oklch(0.704 0.191 22.216)",
|
|
1361
|
+
"--border": "oklch(1 0 0 / 10%)",
|
|
1362
|
+
"--input": "oklch(1 0 0 / 15%)",
|
|
1363
|
+
"--ring": "oklch(0.556 0 0)",
|
|
1364
|
+
"--sidebar": "oklch(0.205 0 0)",
|
|
1365
|
+
"--sidebar-foreground": "oklch(0.985 0 0)",
|
|
1366
|
+
"--sidebar-primary": "oklch(0.488 0.243 264.376)",
|
|
1367
|
+
"--sidebar-primary-foreground": "oklch(0.985 0 0)",
|
|
1368
|
+
"--sidebar-accent": "oklch(0.269 0 0)",
|
|
1369
|
+
"--sidebar-accent-foreground": "oklch(0.985 0 0)",
|
|
1370
|
+
"--sidebar-border": "oklch(1 0 0 / 10%)",
|
|
1371
|
+
"--sidebar-ring": "oklch(0.556 0 0)"
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
var UI_STRINGS = {
|
|
1375
|
+
ru: {
|
|
1376
|
+
projects: "\u041F\u0440\u043E\u0435\u043A\u0442\u044B",
|
|
1377
|
+
selectProject: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0440\u043E\u0435\u043A\u0442",
|
|
1378
|
+
noProject: "\u0411\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430",
|
|
1379
|
+
onThisPage: "\u041D\u0430 \u044D\u0442\u043E\u0439 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435",
|
|
1380
|
+
properties: "\u0421\u0432\u043E\u0439\u0441\u0442\u0432\u0430",
|
|
1381
|
+
shortcuts: "\u0413\u043E\u0440\u044F\u0447\u0438\u0435 \u043A\u043B\u0430\u0432\u0438\u0448\u0438",
|
|
1382
|
+
searchProjectFilter: "\u041F\u0440\u043E\u0435\u043A\u0442",
|
|
1383
|
+
close: "\u0417\u0430\u043A\u0440\u044B\u0442\u044C",
|
|
1384
|
+
closePanel: "\u0417\u0430\u043A\u0440\u044B\u0442\u044C \u043F\u0430\u043D\u0435\u043B\u044C",
|
|
1385
|
+
closeSearch: "\u0417\u0430\u043A\u0440\u044B\u0442\u044C \u043F\u043E\u0438\u0441\u043A",
|
|
1386
|
+
closeTab: "\u0417\u0430\u043A\u0440\u044B\u0442\u044C \u0432\u043A\u043B\u0430\u0434\u043A\u0443",
|
|
1387
|
+
home: "\u0413\u043B\u0430\u0432\u043D\u0430\u044F",
|
|
1388
|
+
files: "\u0424\u0430\u0439\u043B\u044B",
|
|
1389
|
+
search: "\u041F\u043E\u0438\u0441\u043A",
|
|
1390
|
+
toggleTheme: "\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0442\u0435\u043C\u0443"
|
|
1391
|
+
},
|
|
1392
|
+
en: {
|
|
1393
|
+
projects: "Projects",
|
|
1394
|
+
selectProject: "Select a project",
|
|
1395
|
+
noProject: "No project",
|
|
1396
|
+
onThisPage: "On this page",
|
|
1397
|
+
properties: "Properties",
|
|
1398
|
+
shortcuts: "Keyboard shortcuts",
|
|
1399
|
+
searchProjectFilter: "Project",
|
|
1400
|
+
close: "Close",
|
|
1401
|
+
closePanel: "Close panel",
|
|
1402
|
+
closeSearch: "Close search",
|
|
1403
|
+
closeTab: "Close tab",
|
|
1404
|
+
home: "Home",
|
|
1405
|
+
files: "Files",
|
|
1406
|
+
search: "Search",
|
|
1407
|
+
toggleTheme: "Toggle theme"
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
function defaultUiFor(lang) {
|
|
1411
|
+
return UI_STRINGS[lang] ?? UI_STRINGS.en ?? UI_STRINGS.ru;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// app/lib/config/load.ts
|
|
1415
|
+
var CONFIG_NAMES = ["docs.config.ts", "docs.config.js", "docs.config.mjs"];
|
|
1416
|
+
function findConfigFile(cwd) {
|
|
1417
|
+
for (const name of CONFIG_NAMES) {
|
|
1418
|
+
const p = path8.join(cwd, name);
|
|
1419
|
+
if (fs7.existsSync(p))
|
|
1420
|
+
return p;
|
|
1421
|
+
}
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
1425
|
+
const file = findConfigFile(cwd);
|
|
1426
|
+
let authored = {};
|
|
1427
|
+
if (file) {
|
|
1428
|
+
const mtime = fs7.statSync(file).mtimeMs;
|
|
1429
|
+
const mod = await import(`${pathToFileURL(file).href}?t=${mtime}`);
|
|
1430
|
+
authored = mod.default ?? mod.config ?? mod;
|
|
1431
|
+
}
|
|
1432
|
+
const parsed = docsConfigSchema.safeParse(authored);
|
|
1433
|
+
if (!parsed.success) {
|
|
1434
|
+
throw new Error(
|
|
1435
|
+
`Invalid docs.config:
|
|
1436
|
+
|
|
1437
|
+
${JSON.stringify(parsed.error.format(), null, 2)}`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
return resolveDerived(parsed.data);
|
|
1441
|
+
}
|
|
1442
|
+
function resolveDerived(config) {
|
|
1443
|
+
const projects = config.projects.map((p) => ({
|
|
1444
|
+
...p,
|
|
1445
|
+
logo: p.logo ?? `/projects/${p.id}.svg`
|
|
1446
|
+
}));
|
|
1447
|
+
const theme = {
|
|
1448
|
+
colors: {
|
|
1449
|
+
light: { ...DEFAULT_THEME.light, ...config.theme.colors.light },
|
|
1450
|
+
dark: { ...DEFAULT_THEME.dark, ...config.theme.colors.dark }
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
const ui = { ...defaultUiFor(config.site.lang), ...config.ui };
|
|
1454
|
+
return { ...config, projects, theme, ui };
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// scripts/emit-config.ts
|
|
1458
|
+
import fs8 from "node:fs/promises";
|
|
1459
|
+
import path9 from "node:path";
|
|
1460
|
+
var GENERAL_PROJECT_ID = "general";
|
|
1461
|
+
var SLOT_NAMES = ["Home", "DocPage", "TopBar", "Toc"];
|
|
1462
|
+
function renderSlots(components, manifestDir) {
|
|
1463
|
+
const imports = [];
|
|
1464
|
+
const exports = [];
|
|
1465
|
+
for (const name of SLOT_NAMES) {
|
|
1466
|
+
const src = components[name];
|
|
1467
|
+
if (src) {
|
|
1468
|
+
let rel = path9.relative(manifestDir, path9.resolve(process.cwd(), src)).replace(/\\/g, "/");
|
|
1469
|
+
if (!rel.startsWith("."))
|
|
1470
|
+
rel = `./${rel}`;
|
|
1471
|
+
imports.push(`import ${name}Override from '${rel}'`);
|
|
1472
|
+
exports.push(`export const ${name} = ${name}Override`);
|
|
1473
|
+
} else {
|
|
1474
|
+
exports.push(`export const ${name}: ComponentType<any> | null = null`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return `// AUTO-GENERATED by scripts/generate-content.ts \u2014 DO NOT EDIT.
|
|
1478
|
+
import type { ComponentType } from 'react'
|
|
1479
|
+
` + (imports.length ? imports.join("\n") + "\n" : "") + `
|
|
1480
|
+
` + exports.join("\n") + `
|
|
1481
|
+
`;
|
|
1482
|
+
}
|
|
1483
|
+
function renderThemeCss(colors) {
|
|
1484
|
+
const block = (selector, vars) => {
|
|
1485
|
+
const lines = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`);
|
|
1486
|
+
return `${selector} {
|
|
1487
|
+
${lines.join("\n")}
|
|
1488
|
+
}`;
|
|
1489
|
+
};
|
|
1490
|
+
return `/* AUTO-GENERATED by scripts/generate-content.ts from docs.config.ts theme. DO NOT EDIT. */
|
|
1491
|
+
${block(":root", colors.light)}
|
|
1492
|
+
|
|
1493
|
+
${block(".dark", colors.dark)}
|
|
1494
|
+
`;
|
|
1495
|
+
}
|
|
1496
|
+
function firstDocOfProject(index, projectId) {
|
|
1497
|
+
for (const e of index) {
|
|
1498
|
+
if ((e.id.split("/")[0] ?? "") === projectId)
|
|
1499
|
+
return e.id;
|
|
1500
|
+
}
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
function firstGeneralDoc(index, projectIds) {
|
|
1504
|
+
for (const e of index) {
|
|
1505
|
+
if (!projectIds.has(e.id.split("/")[0] ?? ""))
|
|
1506
|
+
return e.id;
|
|
1507
|
+
}
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
async function emitGeneratedConfig({ config, manifestDir, index, logger: logger2 }) {
|
|
1511
|
+
const projectIds = new Set(config.projects.map((p) => p.id));
|
|
1512
|
+
const projects = config.projects.map((p) => {
|
|
1513
|
+
const first = firstDocOfProject(index, p.id);
|
|
1514
|
+
const landing = p.landing ?? (first ? `/${first}/` : "/");
|
|
1515
|
+
return {
|
|
1516
|
+
id: p.id,
|
|
1517
|
+
name: p.name,
|
|
1518
|
+
logo: p.logo ?? `/projects/${p.id}.svg`,
|
|
1519
|
+
landing,
|
|
1520
|
+
description: p.description
|
|
1521
|
+
};
|
|
1522
|
+
});
|
|
1523
|
+
const generalHasDocs = config.general.enabled && firstGeneralDoc(index, projectIds) !== null;
|
|
1524
|
+
const site = {
|
|
1525
|
+
site: {
|
|
1526
|
+
title: config.site.title,
|
|
1527
|
+
description: config.site.description,
|
|
1528
|
+
lang: config.site.lang,
|
|
1529
|
+
favicon: config.site.favicon,
|
|
1530
|
+
logo: config.site.logo,
|
|
1531
|
+
defaultTheme: config.site.defaultTheme
|
|
1532
|
+
},
|
|
1533
|
+
projects,
|
|
1534
|
+
general: {
|
|
1535
|
+
enabled: generalHasDocs,
|
|
1536
|
+
id: GENERAL_PROJECT_ID,
|
|
1537
|
+
name: config.general.name,
|
|
1538
|
+
logo: config.general.logo,
|
|
1539
|
+
description: config.general.description
|
|
1540
|
+
},
|
|
1541
|
+
ui: config.ui
|
|
1542
|
+
};
|
|
1543
|
+
const fullConfig = { ...config, projects: config.projects.map((p, i) => ({ ...p, landing: projects[i].landing })) };
|
|
1544
|
+
await fs8.writeFile(path9.join(manifestDir, "config.json"), JSON.stringify(fullConfig));
|
|
1545
|
+
const siteModule = `// AUTO-GENERATED by scripts/generate-content.ts \u2014 DO NOT EDIT.
|
|
1546
|
+
import type { GeneratedSite } from '~/lib/config/site'
|
|
1547
|
+
|
|
1548
|
+
export const SITE: GeneratedSite = ${JSON.stringify(site, null, " ")}
|
|
1549
|
+
`;
|
|
1550
|
+
await fs8.writeFile(path9.join(manifestDir, "site.ts"), siteModule);
|
|
1551
|
+
await fs8.writeFile(path9.join(manifestDir, "theme.generated.css"), renderThemeCss(config.theme.colors));
|
|
1552
|
+
await fs8.writeFile(path9.join(manifestDir, "slots.ts"), renderSlots(config.components, manifestDir));
|
|
1553
|
+
logger2.info(`Emitted config.json + site.ts + theme.generated.css + slots.ts (${projects.length} project(s)${generalHasDocs ? " + general" : ""}).`);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// scripts/generate-content.ts
|
|
1557
|
+
var logger = {
|
|
1558
|
+
info: (m) => console.log(` ${m}`),
|
|
1559
|
+
warn: (m) => console.warn(` \u26A0 ${m}`),
|
|
1560
|
+
error: (m) => console.error(` \u2716 ${m}`)
|
|
1561
|
+
};
|
|
1562
|
+
var CWD = process.cwd();
|
|
1563
|
+
var CONTENT_ROOT = path10.resolve(CWD, "content");
|
|
1564
|
+
var PUBLIC_ROOT = path10.resolve(CWD, "public");
|
|
1565
|
+
var MANIFEST_DIR = path10.resolve(CWD, "app/generated");
|
|
1566
|
+
var OUTPUT_ROOTS = { content: CONTENT_ROOT, public: PUBLIC_ROOT };
|
|
1567
|
+
function buildWorkLists(config) {
|
|
1568
|
+
const vaults = config.projects.map((p) => ({ vault: p.source, output: p.id, ignore: p.ignore }));
|
|
1569
|
+
const canvas = config.projects.filter((p) => p.canvas).map((p) => ({ vault: p.source, output: p.id }));
|
|
1570
|
+
if (config.general.enabled && config.general.source) {
|
|
1571
|
+
vaults.push({ vault: config.general.source, output: ".", ignore: config.general.ignore });
|
|
1572
|
+
}
|
|
1573
|
+
return { vaults, canvas };
|
|
1574
|
+
}
|
|
1575
|
+
function rewriteContentLinks(docs, canonicalUrl) {
|
|
1576
|
+
let count = 0;
|
|
1577
|
+
for (const d of docs) {
|
|
1578
|
+
d.html = d.html.replace(/href="(\/[^"#]+)(#[^"]*)?"/g, (whole, rawPath, anchor) => {
|
|
1579
|
+
let decoded;
|
|
1580
|
+
try {
|
|
1581
|
+
decoded = decodeURIComponent(rawPath);
|
|
1582
|
+
} catch {
|
|
1583
|
+
return whole;
|
|
1584
|
+
}
|
|
1585
|
+
const id = decoded.replace(/^\/+|\/+$/g, "");
|
|
1586
|
+
const canonical = canonicalUrl.get(id);
|
|
1587
|
+
if (!canonical || canonical === `/${id}/`)
|
|
1588
|
+
return whole;
|
|
1589
|
+
count++;
|
|
1590
|
+
return `href="${canonical}${anchor ?? ""}"`;
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
return count;
|
|
1594
|
+
}
|
|
1595
|
+
async function main() {
|
|
1596
|
+
const start = performance.now();
|
|
1597
|
+
console.log("\u25B6 Generating content from Obsidian vaults\u2026");
|
|
1598
|
+
const config = await loadConfig(CWD);
|
|
1599
|
+
const { vaults, canvas } = buildWorkLists(config);
|
|
1600
|
+
await fs9.rm(CONTENT_ROOT, { recursive: true, force: true });
|
|
1601
|
+
const orderedVaults = [...vaults].sort((a, b) => a.output === "." ? -1 : b.output === "." ? 1 : 0);
|
|
1602
|
+
for (const v of orderedVaults) {
|
|
1603
|
+
await generateObsidian(v, logger, OUTPUT_ROOTS);
|
|
1604
|
+
}
|
|
1605
|
+
for (const c of canvas) {
|
|
1606
|
+
await generateCanvas({ ...c, contentRoot: CONTENT_ROOT }, logger);
|
|
1607
|
+
}
|
|
1608
|
+
const docs = await compileDir(CONTENT_ROOT, logger);
|
|
1609
|
+
const canonicalUrl = buildIdToCanonicalUrl(docs);
|
|
1610
|
+
const rewrites = rewriteContentLinks(docs, canonicalUrl);
|
|
1611
|
+
if (rewrites > 0) {
|
|
1612
|
+
logger.info(`Rewrote ${rewrites} in-content link(s) to permalinks.`);
|
|
1613
|
+
}
|
|
1614
|
+
await fs9.rm(MANIFEST_DIR, { recursive: true, force: true });
|
|
1615
|
+
await fs9.mkdir(path10.join(MANIFEST_DIR, "docs"), { recursive: true });
|
|
1616
|
+
const index = docs.map((d) => ({
|
|
1617
|
+
id: d.id,
|
|
1618
|
+
title: d.frontmatter.title ?? null,
|
|
1619
|
+
draft: d.frontmatter.draft === true,
|
|
1620
|
+
tableOfContents: d.frontmatter.tableOfContents !== false,
|
|
1621
|
+
tags: d.frontmatter.tags ?? [],
|
|
1622
|
+
isCanvas: d.html.includes("data-canvas-mount")
|
|
1623
|
+
})).sort((a, b) => a.id.localeCompare(b.id, config.site.lang));
|
|
1624
|
+
await fs9.writeFile(path10.join(MANIFEST_DIR, "index.json"), JSON.stringify(index));
|
|
1625
|
+
const permalinks = {};
|
|
1626
|
+
for (const d of docs) {
|
|
1627
|
+
const raw = d.frontmatter.permalink;
|
|
1628
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
1629
|
+
continue;
|
|
1630
|
+
const rel = raw.trim().replace(/^\/+|\/+$/g, "");
|
|
1631
|
+
if (!rel)
|
|
1632
|
+
continue;
|
|
1633
|
+
const project = d.id.split("/")[0];
|
|
1634
|
+
const key = `${project}/${rel}`;
|
|
1635
|
+
if (permalinks[key] && permalinks[key] !== d.id) {
|
|
1636
|
+
logger.warn(`Duplicate permalink "${raw}" in project "${project}" on ${d.id} (already used by ${permalinks[key]}); keeping the first.`);
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
permalinks[key] = d.id;
|
|
1640
|
+
}
|
|
1641
|
+
await fs9.writeFile(path10.join(MANIFEST_DIR, "permalinks.json"), JSON.stringify(permalinks));
|
|
1642
|
+
if (Object.keys(permalinks).length > 0) {
|
|
1643
|
+
logger.info(`Registered ${Object.keys(permalinks).length} permalink(s).`);
|
|
1644
|
+
}
|
|
1645
|
+
await Promise.all(
|
|
1646
|
+
docs.map(async (d) => {
|
|
1647
|
+
const outPath = path10.join(MANIFEST_DIR, "docs", `${d.id}.json`);
|
|
1648
|
+
await fs9.mkdir(path10.dirname(outPath), { recursive: true });
|
|
1649
|
+
await fs9.writeFile(
|
|
1650
|
+
outPath,
|
|
1651
|
+
JSON.stringify({ id: d.id, frontmatter: d.frontmatter, headings: d.headings, html: d.html })
|
|
1652
|
+
);
|
|
1653
|
+
})
|
|
1654
|
+
);
|
|
1655
|
+
await buildSearchIndex(docs, canonicalUrl, logger, {
|
|
1656
|
+
outputPath: path10.join(PUBLIC_ROOT, "pagefind"),
|
|
1657
|
+
lang: config.site.lang
|
|
1658
|
+
});
|
|
1659
|
+
await emitGeneratedConfig({ config, manifestDir: MANIFEST_DIR, index, logger });
|
|
1660
|
+
console.log(`\u2714 Done: ${docs.length} pages in ${Math.round(performance.now() - start)}ms`);
|
|
1661
|
+
}
|
|
1662
|
+
main().catch((err) => {
|
|
1663
|
+
console.error(err);
|
|
1664
|
+
process.exit(1);
|
|
1665
|
+
});
|